diff --git a/Moose Development/Moose/Core/Astar.lua b/Moose Development/Moose/Core/Astar.lua new file mode 100644 index 000000000..435bec5a4 --- /dev/null +++ b/Moose Development/Moose/Core/Astar.lua @@ -0,0 +1,898 @@ +--- **Core** - A* Pathfinding. +-- +-- **Main Features:** +-- +-- * Find path from A to B. +-- * Pre-defined as well as custom valid neighbour functions. +-- * Pre-defined as well as custom cost functions. +-- * Easy rectangular grid setup. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Core.Astar +-- @image CORE_Astar.png + + +--- ASTAR class. +-- @type ASTAR +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode. Messages to all about status. +-- @field #string lid Class id string for output to DCS log file. +-- @field #table nodes Table of nodes. +-- @field #number counter Node counter. +-- @field #number Nnodes Number of nodes. +-- @field #number nvalid Number of nvalid calls. +-- @field #number nvalidcache Number of cached valid evals. +-- @field #number ncost Number of cost evaluations. +-- @field #number ncostcache Number of cached cost evals. +-- @field #ASTAR.Node startNode Start node. +-- @field #ASTAR.Node endNode End node. +-- @field Core.Point#COORDINATE startCoord Start coordinate. +-- @field Core.Point#COORDINATE endCoord End coordinate. +-- @field #function ValidNeighbourFunc Function to check if a node is valid. +-- @field #table ValidNeighbourArg Optional arguments passed to the valid neighbour function. +-- @field #function CostFunc Function to calculate the heuristic "cost" to go from one node to another. +-- @field #table CostArg Optional arguments passed to the cost function. +-- @extends Core.Base#BASE + +--- **When nothing goes right... Go left!** +-- +-- === +-- +-- ![Banner Image](..\Presentations\Astar\ASTAR_Main.jpg) +-- +-- # The ASTAR Concept +-- +-- Pathfinding algorithm. +-- +-- +-- # Start and Goal +-- +-- The first thing we need to define is obviously the place where we want to start and where we want to go eventually. +-- +-- ## Start +-- +-- The start +-- +-- ## Goal +-- +-- +-- # Nodes +-- +-- ## Rectangular Grid +-- +-- A rectangular grid can be created using the @{#ASTAR.CreateGrid}(*ValidSurfaceTypes, BoxHY, SpaceX, deltaX, deltaY, MarkGrid*), where +-- +-- * *ValidSurfaceTypes* is a table of valid surface types. By default all surface types are valid. +-- * *BoxXY* is the width of the grid perpendicular the the line between start and end node. Default is 40,000 meters (40 km). +-- * *SpaceX* is the additional space behind the start and end nodes. Default is 20,000 meters (20 km). +-- * *deltaX* is the grid spacing between nodes in the direction of start and end node. Default is 2,000 meters (2 km). +-- * *deltaY* is the grid spacing perpendicular to the direction of start and end node. Default is the same as *deltaX*. +-- * *MarkGrid* If set to *true*, this places marker on the F10 map on each grid node. Note that this can stall DCS if too many nodes are created. +-- +-- ## Valid Surfaces +-- +-- Certain unit types can only travel on certain surfaces types, for example +-- +-- * Naval units can only travel on water (that also excludes shallow water in DCS currently), +-- * Ground units can only traval on land. +-- +-- By restricting the surface type in the grid construction, we also reduce the number of nodes, which makes the algorithm more efficient. +-- +-- ## Box Width (BoxHY) +-- +-- The box width needs to be large enough to capture all paths you want to consider. +-- +-- ## Space in X +-- +-- The space in X value is important if the algorithm needs to to backwards from the start node or needs to extend even further than the end node. +-- +-- ## Grid Spacing +-- +-- The grid spacing is an important factor as it determines the number of nodes and hence the performance of the algorithm. It should be as large as possible. +-- However, if the value is too large, the algorithm might fail to get a valid path. +-- +-- A good estimate of the grid spacing is to set it to be smaller (~ half the size) of the smallest gap you need to path. +-- +-- # Valid Neighbours +-- +-- The A* algorithm needs to know if a transition from one node to another is allowed or not. By default, hopping from one node to another is always possible. +-- +-- ## Line of Sight +-- +-- For naval +-- +-- +-- # Heuristic Cost +-- +-- In order to determine the optimal path, the pathfinding algorithm needs to know, how costly it is to go from one node to another. +-- Often, this can simply be determined by the distance between two nodes. Therefore, the default cost function is set to be the 2D distance between two nodes. +-- +-- +-- # Calculate the Path +-- +-- Finally, we have to calculate the path. This is done by the @{ASTAR.GetPath}(*ExcludeStart, ExcludeEnd*) function. This function returns a table of nodes, which +-- describe the optimal path from the start node to the end node. +-- +-- By default, the start and end node are include in the table that is returned. +-- +-- Note that a valid path must not always exist. So you should check if the function returns *nil*. +-- +-- Common reasons that a path cannot be found are: +-- +-- * The grid is too small ==> increase grid size, e.g. *BoxHY* and/or *SpaceX* if you use a rectangular grid. +-- * The grid spacing is too large ==> decrease *deltaX* and/or *deltaY* +-- * There simply is no valid path ==> you are screwed :( +-- +-- +-- # Examples +-- +-- ## Strait of Hormuz +-- +-- Carrier Group finds its way through the Stait of Hormuz. +-- +-- ## +-- +-- +-- +-- @field #ASTAR +ASTAR = { + ClassName = "ASTAR", + Debug = nil, + lid = nil, + nodes = {}, + counter = 1, + Nnodes = 0, + ncost = 0, + ncostcache = 0, + nvalid = 0, + nvalidcache = 0, +} + +--- Node data. +-- @type ASTAR.Node +-- @field #number id Node id. +-- @field Core.Point#COORDINATE coordinate Coordinate of the node. +-- @field #number surfacetype Surface type. +-- @field #table valid Cached valid/invalid nodes. +-- @field #table cost Cached cost. + +--- ASTAR infinity. +-- @field #number INF +ASTAR.INF=1/0 + +--- ASTAR class version. +-- @field #string version +ASTAR.version="0.4.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Add more valid neighbour functions. +-- TODO: Write docs. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new ASTAR object. +-- @param #ASTAR self +-- @return #ASTAR self +function ASTAR:New() + + -- Inherit everything from INTEL class. + local self=BASE:Inherit(self, BASE:New()) --#ASTAR + + self.lid="ASTAR | " + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set coordinate from where to start. +-- @param #ASTAR self +-- @param Core.Point#COORDINATE Coordinate Start coordinate. +-- @return #ASTAR self +function ASTAR:SetStartCoordinate(Coordinate) + + self.startCoord=Coordinate + + return self +end + +--- Set coordinate where you want to go. +-- @param #ASTAR self +-- @param Core.Point#COORDINATE Coordinate end coordinate. +-- @return #ASTAR self +function ASTAR:SetEndCoordinate(Coordinate) + + self.endCoord=Coordinate + + return self +end + +--- Create a node from a given coordinate. +-- @param #ASTAR self +-- @param Core.Point#COORDINATE Coordinate The coordinate where to create the node. +-- @return #ASTAR.Node The node. +function ASTAR:GetNodeFromCoordinate(Coordinate) + + local node={} --#ASTAR.Node + + node.coordinate=Coordinate + node.surfacetype=Coordinate:GetSurfaceType() + node.id=self.counter + + node.valid={} + node.cost={} + + self.counter=self.counter+1 + + return node +end + + +--- Add a node to the table of grid nodes. +-- @param #ASTAR self +-- @param #ASTAR.Node Node The node to be added. +-- @return #ASTAR self +function ASTAR:AddNode(Node) + + self.nodes[Node.id]=Node + self.Nnodes=self.Nnodes+1 + + return self +end + +--- Add a node to the table of grid nodes specifying its coordinate. +-- @param #ASTAR self +-- @param Core.Point#COORDINATE Coordinate The coordinate where the node is created. +-- @return #ASTAR.Node The node. +function ASTAR:AddNodeFromCoordinate(Coordinate) + + local node=self:GetNodeFromCoordinate(Coordinate) + + self:AddNode(node) + + return node +end + +--- Check if the coordinate of a node has is at a valid surface type. +-- @param #ASTAR self +-- @param #ASTAR.Node Node The node to be added. +-- @param #table SurfaceTypes Surface types, for example `{land.SurfaceType.WATER}`. By default all surface types are valid. +-- @return #boolean If true, surface type of node is valid. +function ASTAR:CheckValidSurfaceType(Node, SurfaceTypes) + + if SurfaceTypes then + + if type(SurfaceTypes)~="table" then + SurfaceTypes={SurfaceTypes} + end + + for _,surface in pairs(SurfaceTypes) do + if surface==Node.surfacetype then + return true + end + end + + return false + + else + return true + end + +end + +--- Add a function to determine if a neighbour of a node is valid. +-- @param #ASTAR self +-- @param #function NeighbourFunction Function that needs to return *true* for a neighbour to be valid. +-- @param ... Condition function arguments if any. +-- @return #ASTAR self +function ASTAR:SetValidNeighbourFunction(NeighbourFunction, ...) + + self.ValidNeighbourFunc=NeighbourFunction + + self.ValidNeighbourArg={} + if arg then + self.ValidNeighbourArg=arg + end + + return self +end + + +--- Set valid neighbours to require line of sight between two nodes. +-- @param #ASTAR self +-- @param #number CorridorWidth Width of LoS corridor in meters. +-- @return #ASTAR self +function ASTAR:SetValidNeighbourLoS(CorridorWidth) + + self:SetValidNeighbourFunction(ASTAR.LoS, CorridorWidth) + + return self +end + +--- Set valid neighbours to be in a certain distance. +-- @param #ASTAR self +-- @param #number MaxDistance Max distance between nodes in meters. Default is 2000 m. +-- @return #ASTAR self +function ASTAR:SetValidNeighbourDistance(MaxDistance) + + self:SetValidNeighbourFunction(ASTAR.DistMax, MaxDistance) + + return self +end + +--- Set the function which calculates the "cost" to go from one to another node. +-- The first to arguments of this function are always the two nodes under consideration. But you can add optional arguments. +-- Very often the distance between nodes is a good measure for the cost. +-- @param #ASTAR self +-- @param #function CostFunction Function that returns the "cost". +-- @param ... Condition function arguments if any. +-- @return #ASTAR self +function ASTAR:SetCostFunction(CostFunction, ...) + + self.CostFunc=CostFunction + + self.CostArg={} + if arg then + self.CostArg=arg + end + + return self +end + +--- Set heuristic cost to go from one node to another to be their 2D distance. +-- @param #ASTAR self +-- @return #ASTAR self +function ASTAR:SetCostDist2D() + + self:SetCostFunction(ASTAR.Dist2D) + + return self +end + +--- Set heuristic cost to go from one node to another to be their 3D distance. +-- @param #ASTAR self +-- @return #ASTAR self +function ASTAR:SetCostDist3D() + + self:SetCostFunction(ASTAR.Dist3D) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Grid functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a rectangular grid of nodes between star and end coordinate. +-- The coordinate system is oriented along the line between start and end point. +-- @param #ASTAR self +-- @param #table ValidSurfaceTypes Valid surface types. By default is all surfaces are allowed. +-- @param #number BoxHY Box "height" in meters along the y-coordinate. Default 40000 meters (40 km). +-- @param #number SpaceX Additional space in meters before start and after end coordinate. Default 10000 meters (10 km). +-- @param #number deltaX Increment in the direction of start to end coordinate in meters. Default 2000 meters. +-- @param #number deltaY Increment perpendicular to the direction of start to end coordinate in meters. Default is same as deltaX. +-- @param #boolean MarkGrid If true, create F10 map markers at grid nodes. +-- @return #ASTAR self +function ASTAR:CreateGrid(ValidSurfaceTypes, BoxHY, SpaceX, deltaX, deltaY, MarkGrid) + + -- Note that internally + -- x coordinate is z: x-->z Line from start to end + -- y coordinate is x: y-->x Perpendicular + + -- Grid length and width. + local Dz=SpaceX or 10000 + local Dx=BoxHY and BoxHY/2 or 20000 + + -- Increments. + local dz=deltaX or 2000 + local dx=deltaY or dz + + -- Heading from start to end coordinate. + local angle=self.startCoord:HeadingTo(self.endCoord) + + --Distance between start and end. + local dist=self.startCoord:Get2DDistance(self.endCoord)+2*Dz + + -- Origin of map. Needed to translate back to wanted position. + local co=COORDINATE:New(0, 0, 0) + local do1=co:Get2DDistance(self.startCoord) + local ho1=co:HeadingTo(self.startCoord) + + -- Start of grid. + local xmin=-Dx + local zmin=-Dz + + -- Number of grid points. + local nz=dist/dz+1 + local nx=2*Dx/dx+1 + + -- Debug info. + local text=string.format("Building grid with nx=%d ny=%d => total=%d nodes", nx, nz, nx*nz) + self:I(self.lid..text) + MESSAGE:New(text, 10, "ASTAR"):ToAllIf(self.Debug) + + + -- Loop over x and z coordinate to create a 2D grid. + for i=1,nx do + + -- x coordinate perpendicular to z. + local x=xmin+dx*(i-1) + + for j=1,nz do + + -- z coordinate connecting start and end. + local z=zmin+dz*(j-1) + + -- Rotate 2D. + local vec3=UTILS.Rotate2D({x=x, y=0, z=z}, angle) + + -- Coordinate of the node. + local c=COORDINATE:New(vec3.z, vec3.y, vec3.x):Translate(do1, ho1, true) + + -- Create a node at this coordinate. + local node=self:GetNodeFromCoordinate(c) + + -- Check if node has valid surface type. + if self:CheckValidSurfaceType(node, ValidSurfaceTypes) then + + if MarkGrid then + c:MarkToAll(string.format("i=%d, j=%d surface=%d", i, j, node.surfacetype)) + end + + -- Add node to grid. + self:AddNode(node) + + end + + end + end + + -- Debug info. + local text=string.format("Done building grid!") + self:I(self.lid..text) + MESSAGE:New(text, 10, "ASTAR"):ToAllIf(self.Debug) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Valid neighbour functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Function to check if two nodes have line of sight (LoS). +-- @param #ASTAR.Node nodeA First node. +-- @param #ASTAR.Node nodeB Other node. +-- @param #number corridor (Optional) Width of corridor in meters. +-- @return #boolean If true, two nodes have LoS. +function ASTAR.LoS(nodeA, nodeB, corridor) + + local offset=1 + + local dx=corridor and corridor/2 or nil + local dy=dx + + local cA=nodeA.coordinate:GetVec3() + local cB=nodeB.coordinate:GetVec3() + cA.y=offset + cB.y=offset + + local los=land.isVisible(cA, cB) + + if los and corridor then + + -- Heading from A to B. + local heading=nodeA.coordinate:HeadingTo(nodeB.coordinate) + + local Ap=UTILS.VecTranslate(cA, dx, heading+90) + local Bp=UTILS.VecTranslate(cB, dx, heading+90) + + los=land.isVisible(Ap, Bp) + + if los then + + local Am=UTILS.VecTranslate(cA, dx, heading-90) + local Bm=UTILS.VecTranslate(cB, dx, heading-90) + + los=land.isVisible(Am, Bm) + end + + end + + return los +end + +--- Function to check if distance between two nodes is less than a threshold distance. +-- @param #ASTAR.Node nodeA First node. +-- @param #ASTAR.Node nodeB Other node. +-- @param #number distmax Max distance in meters. Default is 2000 m. +-- @return #boolean If true, distance between the two nodes is below threshold. +function ASTAR.DistMax(nodeA, nodeB, distmax) + + distmax=distmax or 2000 + + local dist=nodeA.coordinate:Get2DDistance(nodeB.coordinate) + + return dist<=distmax +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Heuristic cost functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Heuristic cost is given by the 2D distance between the nodes. +-- @param #ASTAR.Node nodeA First node. +-- @param #ASTAR.Node nodeB Other node. +-- @return #number Distance between the two nodes. +function ASTAR.Dist2D(nodeA, nodeB) + return nodeA.coordinate:Get2DDistance(nodeB) +end + +--- Heuristic cost is given by the 3D distance between the nodes. +-- @param #ASTAR.Node nodeA First node. +-- @param #ASTAR.Node nodeB Other node. +-- @return #number Distance between the two nodes. +function ASTAR.Dist3D(nodeA, nodeB) + return nodeA.coordinate:Get3DDistance(nodeB.coordinate) +end + +--- Heuristic cost is given by the distance between the nodes on road. +-- @param #ASTAR.Node nodeA First node. +-- @param #ASTAR.Node nodeB Other node. +-- @return #number Distance between the two nodes. +function ASTAR.DistRoad(nodeA, nodeB) + + local path, dist, gotpath=nodeA.coordinate:GetPathOnRoad(nodeB.coordinate, IncludeEndpoints, Railroad, MarkPath, SmokePath) + + if gotpath then + return dist + else + return math.huge + end + + return nodeA.coordinate:Get3DDistance(nodeB.coordinate) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Find the closest node from a given coordinate. +-- @param #ASTAR self +-- @param Core.Point#COORDINATE Coordinate. +-- @return #ASTAR.Node Cloest node to the coordinate. +-- @return #number Distance to closest node in meters. +function ASTAR:FindClosestNode(Coordinate) + + local distMin=math.huge + local closeNode=nil + + for _,_node in pairs(self.nodes) do + local node=_node --#ASTAR.Node + + local dist=node.coordinate:Get2DDistance(Coordinate) + + if dist1000 then + self:I(self.lid.."Adding start node to node grid!") + self:AddNode(node) + end + + return self +end + +--- Add a node. +-- @param #ASTAR self +-- @param #ASTAR.Node Node The node to be added to the nodes table. +-- @return #ASTAR self +function ASTAR:FindEndNode() + + local node, dist=self:FindClosestNode(self.endCoord) + + self.endNode=node + + if dist>1000 then + self:I(self.lid.."Adding end node to node grid!") + self:AddNode(node) + end + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Main A* pathfinding function +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- A* pathfinding function. This seaches the path along nodes between start and end nodes/coordinates. +-- @param #ASTAR self +-- @param #boolean ExcludeStartNode If *true*, do not include start node in found path. Default is to include it. +-- @param #boolean ExcludeEndNode If *true*, do not include end node in found path. Default is to include it. +-- @return #table Table of nodes from start to finish. +function ASTAR:GetPath(ExcludeStartNode, ExcludeEndNode) + + self:FindStartNode() + self:FindEndNode() + + local nodes=self.nodes + local start=self.startNode + local goal=self.endNode + + -- Sets. + local openset = {} + local closedset = {} + local came_from = {} + local g_score = {} + local f_score = {} + + openset[start.id]=true + local Nopen=1 + + -- Initial scores. + g_score[start.id]=0 + f_score[start.id]=g_score[start.id]+self:_HeuristicCost(start, goal) + + -- Set start time. + local T0=timer.getAbsTime() + + -- Debug message. + local text=string.format("Starting A* pathfinding with %d Nodes", self.Nnodes) + self:I(self.lid..text) + MESSAGE:New(text, 10, "ASTAR"):ToAllIf(self.Debug) + + local Tstart=UTILS.GetOSTime() + + -- Loop while we still have an open set. + while Nopen > 0 do + + -- Get current node. + local current=self:_LowestFscore(openset, f_score) + + -- Check if we are at the end node. + if current.id==goal.id then + + local path=self:_UnwindPath({}, came_from, goal) + + if not ExcludeEndNode then + table.insert(path, goal) + end + + if ExcludeStartNode then + table.remove(path, 1) + end + + local Tstop=UTILS.GetOSTime() + + local dT=nil + if Tstart and Tstop then + dT=Tstop-Tstart + end + + -- Debug message. + local text=string.format("Found path with %d nodes (%d total)", #path, self.Nnodes) + if dT then + text=text..string.format(", OS Time %.6f sec", dT) + end + text=text..string.format(", Nvalid=%d [%d cached]", self.nvalid, self.nvalidcache) + text=text..string.format(", Ncost=%d [%d cached]", self.ncost, self.ncostcache) + self:I(self.lid..text) + MESSAGE:New(text, 60, "ASTAR"):ToAllIf(self.Debug) + + return path + end + + -- Move Node from open to closed set. + openset[current.id]=nil + Nopen=Nopen-1 + closedset[current.id]=true + + -- Get neighbour nodes. + local neighbors=self:_NeighbourNodes(current, nodes) + + -- Loop over neighbours. + for _,neighbor in pairs(neighbors) do + + if self:_NotIn(closedset, neighbor.id) then + + local tentative_g_score=g_score[current.id]+self:_DistNodes(current, neighbor) + + if self:_NotIn(openset, neighbor.id) or tentative_g_score < g_score[neighbor.id] then + + came_from[neighbor]=current + + g_score[neighbor.id]=tentative_g_score + f_score[neighbor.id]=g_score[neighbor.id]+self:_HeuristicCost(neighbor, goal) + + if self:_NotIn(openset, neighbor.id) then + -- Add to open set. + openset[neighbor.id]=true + Nopen=Nopen+1 + end + + end + end + end + end + + -- Debug message. + local text=string.format("WARNING: Could NOT find valid path!") + self:E(self.lid..text) + MESSAGE:New(text, 60, "ASTAR"):ToAllIf(self.Debug) + + return nil -- no valid path +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- A* pathfinding helper functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Heuristic "cost" function to go from node A to node B. Default is the distance between the nodes. +-- @param #ASTAR self +-- @param #ASTAR.Node nodeA Node A. +-- @param #ASTAR.Node nodeB Node B. +-- @return #number "Cost" to go from node A to node B. +function ASTAR:_HeuristicCost(nodeA, nodeB) + + -- Counter. + self.ncost=self.ncost+1 + + -- Get chached cost if available. + local cost=nodeA.cost[nodeB.id] + if cost~=nil then + self.ncostcache=self.ncostcache+1 + return cost + end + + local cost=nil + if self.CostFunc then + cost=self.CostFunc(nodeA, nodeB, unpack(self.CostArg)) + else + cost=self:_DistNodes(nodeA, nodeB) + end + + nodeA.cost[nodeB.id]=cost + nodeB.cost[nodeA.id]=cost -- Symmetric problem. + + return cost +end + +--- Check if going from a node to a neighbour is possible. +-- @param #ASTAR self +-- @param #ASTAR.Node node A node. +-- @param #ASTAR.Node neighbor Neighbour node. +-- @return #boolean If true, transition between nodes is possible. +function ASTAR:_IsValidNeighbour(node, neighbor) + + -- Counter. + self.nvalid=self.nvalid+1 + + local valid=node.valid[neighbor.id] + if valid~=nil then + --env.info(string.format("Node %d has valid=%s neighbour %d", node.id, tostring(valid), neighbor.id)) + self.nvalidcache=self.nvalidcache+1 + return valid + end + + local valid=nil + if self.ValidNeighbourFunc then + valid=self.ValidNeighbourFunc(node, neighbor, unpack(self.ValidNeighbourArg)) + else + valid=true + end + + node.valid[neighbor.id]=valid + neighbor.valid[node.id]=valid -- Symmetric problem. + + return valid +end + +--- Calculate 2D distance between two nodes. +-- @param #ASTAR self +-- @param #ASTAR.Node nodeA Node A. +-- @param #ASTAR.Node nodeB Node B. +-- @return #number Distance between nodes in meters. +function ASTAR:_DistNodes(nodeA, nodeB) + return nodeA.coordinate:Get2DDistance(nodeB.coordinate) +end + +--- Function that calculates the lowest F score. +-- @param #ASTAR self +-- @param #table set The set of nodes IDs. +-- @param #number f_score F score. +-- @return #ASTAR.Node Best node. +function ASTAR:_LowestFscore(set, f_score) + + local lowest, bestNode = ASTAR.INF, nil + + for nid,node in pairs(set) do + + local score=f_score[nid] + + if score +-- @list Table of 2D vectors. + +--- A 3D points array. +-- @type ZONE_POLYGON_BASE.ListVec3 +-- @list Table of 3D vectors. --- Constructor to create a ZONE_POLYGON_BASE instance, taking the zone name and an array of @{DCS#Vec2}, forming a polygon. -- The @{Wrapper.Group#GROUP} waypoints define the polygon corners. The first and the last point are automatically connected. -- @param #ZONE_POLYGON_BASE self -- @param #string ZoneName Name of the zone. --- @param #ZONE_POLYGON_BASE.ListVec2 PointsArray An array of @{DCS#Vec2}, forming a polygon.. +-- @param #ZONE_POLYGON_BASE.ListVec2 PointsArray An array of @{DCS#Vec2}, forming a polygon. -- @return #ZONE_POLYGON_BASE self function ZONE_POLYGON_BASE:New( ZoneName, PointsArray ) + + -- Inherit ZONE_BASE. local self = BASE:Inherit( self, ZONE_BASE:New( ZoneName ) ) self:F( { ZoneName, PointsArray } ) - local i = 0 - + if PointsArray then + + self._.Polygon = {} + + for i = 1, #PointsArray do + self._.Polygon[i] = {} + self._.Polygon[i].x = PointsArray[i].x + self._.Polygon[i].y = PointsArray[i].y + end + + end + + return self +end + +--- Update polygon points with an array of @{DCS#Vec2}. +-- @param #ZONE_POLYGON_BASE self +-- @param #ZONE_POLYGON_BASE.ListVec2 Vec2Array An array of @{DCS#Vec2}, forming a polygon. +-- @return #ZONE_POLYGON_BASE self +function ZONE_POLYGON_BASE:UpdateFromVec2(Vec2Array) + self._.Polygon = {} - for i = 1, #PointsArray do + for i=1,#Vec2Array do self._.Polygon[i] = {} - self._.Polygon[i].x = PointsArray[i].x - self._.Polygon[i].y = PointsArray[i].y + self._.Polygon[i].x=Vec2Array[i].x + self._.Polygon[i].y=Vec2Array[i].y + end + + return self +end + +--- Update polygon points with an array of @{DCS#Vec3}. +-- @param #ZONE_POLYGON_BASE self +-- @param #ZONE_POLYGON_BASE.ListVec3 Vec2Array An array of @{DCS#Vec3}, forming a polygon. +-- @return #ZONE_POLYGON_BASE self +function ZONE_POLYGON_BASE:UpdateFromVec3(Vec3Array) + + self._.Polygon = {} + + for i=1,#Vec3Array do + self._.Polygon[i] = {} + self._.Polygon[i].x=Vec3Array[i].x + self._.Polygon[i].y=Vec3Array[i].z end return self diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 297082e22..e9661a754 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -46,6 +46,7 @@ -- @type WAREHOUSE -- @field #string ClassName Name of the class. -- @field #boolean Debug If true, send debug messages to all. +-- @field #number verbosity Verbosity level. -- @field #string wid Identifier of the warehouse printed before other output to DCS.log file. -- @field #boolean Report If true, send status messages to coalition. -- @field Wrapper.Static#STATIC warehouse The phyical warehouse structure. @@ -57,6 +58,10 @@ -- @field Core.Point#COORDINATE rail Closest point to warehouse on rail. -- @field Core.Zone#ZONE spawnzone Zone in which assets are spawned. -- @field #number uid Unique ID of the warehouse. +-- @field #boolean markerOn If true, markers are displayed on the F10 map. +-- @field Wrapper.Marker#MARKER markerWarehouse Marker warehouse. +-- @field Wrapper.Marker#MARKER markerRoad Road connection. +-- @field Wrapper.Marker#MARKER markerRail Rail road connection. -- @field #number markerid ID of the warehouse marker at the airbase. -- @field #number dTstatus Time interval in seconds of updating the warehouse status and processing new events. Default 30 seconds. -- @field #number queueid Unit id of each request in the queue. Essentially a running number starting at one and incremented when a new request is added. @@ -79,7 +84,6 @@ -- @field #number lowfuelthresh Low fuel threshold. Triggers the event AssetLowFuel if for any unit fuel goes below this number. -- @field #boolean respawnafterdestroyed If true, warehouse is respawned after it was destroyed. Assets are kept. -- @field #number respawndelay Delay before respawn in seconds. --- @field #boolean markerOn If true, markers are displayed on the F10 map. -- @extends Core.Fsm#FSM --- Have your assets at the right place at the right time - or not! @@ -1548,6 +1552,7 @@ WAREHOUSE = { ClassName = "WAREHOUSE", Debug = false, + verbosity = 0, lid = nil, Report = true, warehouse = nil, @@ -1559,7 +1564,6 @@ WAREHOUSE = { rail = nil, spawnzone = nil, uid = nil, - markerid = nil, dTstatus = 30, queueid = 0, stock = {}, @@ -1609,6 +1613,7 @@ WAREHOUSE = { -- @field #boolean iscargo If true, asset is cargo. If false asset is transport. Nil if in stock. -- @field #number rid The request ID of this asset. -- @field #boolean arrived If true, asset arrived at its destination. +-- @field #number damage Damage of asset group in percent. --- Item of the warehouse queue table. -- @type WAREHOUSE.Queueitem @@ -1829,14 +1834,12 @@ WAREHOUSE.version="1.0.2" -- @param #string alias (Optional) Alias of the warehouse, i.e. the name it will be called when sending messages etc. Default is the name of the static -- @return #WAREHOUSE self function WAREHOUSE:New(warehouse, alias) - BASE:T({warehouse=warehouse}) -- Check if just a string was given and convert to static. if type(warehouse)=="string" then local warehousename=warehouse warehouse=UNIT:FindByName(warehousename) if warehouse==nil then - --env.info(string.format("No warehouse unit with name %s found trying static.", tostring(warehousename))) warehouse=STATIC:FindByName(warehousename, true) self.isunit=false else @@ -1857,7 +1860,7 @@ function WAREHOUSE:New(warehouse, alias) env.info(string.format("Adding warehouse v%s for structure %s with alias %s", WAREHOUSE.version, warehouse:GetName(), self.alias)) -- Inherit everthing from FSM class. - local self = BASE:Inherit(self, FSM:New()) -- #WAREHOUSE + local self=BASE:Inherit(self, FSM:New()) -- #WAREHOUSE -- Set some string id for output to DCS.log file. self.lid=string.format("WAREHOUSE %s | ", self.alias) @@ -1889,6 +1892,8 @@ function WAREHOUSE:New(warehouse, alias) -- Defaults self:SetMarker(true) + self:SetReportOff() + --self:SetVerbosityLevel(0) -- Add warehouse to database. _WAREHOUSEDB.Warehouses[self.uid]=self @@ -2550,6 +2555,15 @@ function WAREHOUSE:SetStatusUpdate(timeinterval) return self end +--- Set verbosity level. +-- @param #WAREHOUSE self +-- @param #number VerbosityLevel Level of output (higher=more). Default 0. +-- @return #WAREHOUSE self +function WAREHOUSE:SetVerbosityLevel(VerbosityLevel) + self.verbosity=VerbosityLevel or 0 + return self +end + --- Set a zone where the (ground) assets of the warehouse are spawned once requested. -- @param #WAREHOUSE self -- @param Core.Zone#ZONE zone The spawn zone. @@ -3252,16 +3266,6 @@ function WAREHOUSE:onafterStart(From, Event, To) -- Save self in static object. Easier to retrieve later. self.warehouse:SetState(self.warehouse, "WAREHOUSE", self) - -- THIS! caused aircraft to be spawned and started but they would never begin their route! - -- VERY strange. Need to test more. - --[[ - -- Debug mark warehouse & spawn zone. - self.zone:BoundZone(30, self.country) - self.spawnzone:BoundZone(30, self.country) - ]] - - --self.spawnzone:GetCoordinate():MarkToCoalition(string.format("Warehouse %s spawn zone", self.alias), self:GetCoalition()) - -- Get the closest point on road wrt spawnzone of ground assets. local _road=self.spawnzone:GetCoordinate():GetClosestPointToRoad() if _road and self.road==nil then @@ -3371,7 +3375,7 @@ function WAREHOUSE:onafterStop(From, Event, To) self:_UpdateWarehouseMarkText() -- Clear all pending schedules. - --self.CallScheduler:Clear() + self.CallScheduler:Clear() end --- On after "Pause" event. Pauses the warehouse, i.e. no requests are processed. However, new requests and new assets can be added in this state. @@ -3401,13 +3405,17 @@ end -- @param #string To To state. function WAREHOUSE:onafterStatus(From, Event, To) - local FSMstate=self:GetState() - - local coalition=self:GetCoalitionName() - local country=self:GetCountryName() - - -- Info. - self:I(self.lid..string.format("State=%s %s [%s]: Assets=%d, Requests: waiting=%d, pending=%d", FSMstate, country, coalition, #self.stock, #self.queue, #self.pending)) + -- General info. + if self.verbosity>=1 then + + local FSMstate=self:GetState() + + local coalition=self:GetCoalitionName() + local country=self:GetCountryName() + + -- Info. + self:I(self.lid..string.format("State=%s %s [%s]: Assets=%d, Requests: waiting=%d, pending=%d", FSMstate, country, coalition, #self.stock, #self.queue, #self.pending)) + end -- Check if any pending jobs are done and can be deleted from the queue. self:_JobDone() @@ -3464,170 +3472,174 @@ function WAREHOUSE:_JobDone() -- Loop over all pending requests of this warehouse. for _,request in pairs(self.pending) do local request=request --#WAREHOUSE.Pendingitem + + if request.born then - -- Count number of cargo groups. - local ncargo=0 - if request.cargogroupset then - ncargo=request.cargogroupset:Count() - end - - -- Count number of transport groups (if any). - local ntransport=0 - if request.transportgroupset then - ntransport=request.transportgroupset:Count() - end - - local ncargotot=request.nasset - local ncargodelivered=request.ndelivered - - -- Dead cargo: Ndead=Ntot-Ndeliverd-Nalive, - local ncargodead=ncargotot-ncargodelivered-ncargo - - - local ntransporttot=request.ntransport - local ntransporthome=request.ntransporthome - - -- Dead transport: Ndead=Ntot-Nhome-Nalive. - local ntransportdead=ntransporttot-ntransporthome-ntransport - - local text=string.format("Request id=%d: Cargo: Ntot=%d, Nalive=%d, Ndelivered=%d, Ndead=%d | Transport: Ntot=%d, Nalive=%d, Nhome=%d, Ndead=%d", - request.uid, ncargotot, ncargo, ncargodelivered, ncargodead, ntransporttot, ntransport, ntransporthome, ntransportdead) - self:T(self.lid..text) - - - -- Handle different cases depending on what asset are still around. - if ncargo==0 then - --------------------- - -- Cargo delivered -- - --------------------- - - -- Trigger delivered event. - if not self.delivered[request.uid] then - self:Delivered(request) + -- Count number of cargo groups. + local ncargo=0 + if request.cargogroupset then + ncargo=request.cargogroupset:Count() end - - -- Check if transports are back home? - if ntransport==0 then - --------------- - -- Job done! -- - --------------- - - -- Info on job. - local text=string.format("Warehouse %s: Job on request id=%d for warehouse %s done!\n", self.alias, request.uid, request.warehouse.alias) - text=text..string.format("- %d of %d assets delivered. Casualties %d.", ncargodelivered, ncargotot, ncargodead) - if request.ntransport>0 then - text=text..string.format("\n- %d of %d transports returned home. Casualties %d.", ntransporthome, ntransporttot, ntransportdead) + + -- Count number of transport groups (if any). + local ntransport=0 + if request.transportgroupset then + ntransport=request.transportgroupset:Count() + end + + local ncargotot=request.nasset + local ncargodelivered=request.ndelivered + + -- Dead cargo: Ndead=Ntot-Ndeliverd-Nalive, + local ncargodead=ncargotot-ncargodelivered-ncargo + + + local ntransporttot=request.ntransport + local ntransporthome=request.ntransporthome + + -- Dead transport: Ndead=Ntot-Nhome-Nalive. + local ntransportdead=ntransporttot-ntransporthome-ntransport + + local text=string.format("Request id=%d: Cargo: Ntot=%d, Nalive=%d, Ndelivered=%d, Ndead=%d | Transport: Ntot=%d, Nalive=%d, Nhome=%d, Ndead=%d", + request.uid, ncargotot, ncargo, ncargodelivered, ncargodead, ntransporttot, ntransport, ntransporthome, ntransportdead) + self:T(self.lid..text) + + + -- Handle different cases depending on what asset are still around. + if ncargo==0 then + --------------------- + -- Cargo delivered -- + --------------------- + + -- Trigger delivered event. + if not self.delivered[request.uid] then + self:Delivered(request) end - self:_InfoMessage(text, 20) - - -- Mark request for deletion. - table.insert(done, request) - + + -- Check if transports are back home? + if ntransport==0 then + --------------- + -- Job done! -- + --------------- + + -- Info on job. + if self.verbosity>=1 then + local text=string.format("Warehouse %s: Job on request id=%d for warehouse %s done!\n", self.alias, request.uid, request.warehouse.alias) + text=text..string.format("- %d of %d assets delivered. Casualties %d.", ncargodelivered, ncargotot, ncargodead) + if request.ntransport>0 then + text=text..string.format("\n- %d of %d transports returned home. Casualties %d.", ntransporthome, ntransporttot, ntransportdead) + end + self:_InfoMessage(text, 20) + end + + -- Mark request for deletion. + table.insert(done, request) + + else + ----------------------------------- + -- No cargo but still transports -- + ----------------------------------- + + -- This is difficult! How do I know if transports were unused? They could also be just on their way back home. + -- ==> Need to do a lot of checks. + + -- All transports are dead but there is still cargo left ==> Put cargo back into stock. + for _,_group in pairs(request.transportgroupset:GetSetObjects()) do + local group=_group --Wrapper.Group#GROUP + + -- Check if group is alive. + if group and group:IsAlive() then + + -- Check if group is in the spawn zone? + local category=group:GetCategory() + + -- Get current speed. + local speed=group:GetVelocityKMH() + local notmoving=speed<1 + + -- Closest airbase. + local airbase=group:GetCoordinate():GetClosestAirbase():GetName() + local athomebase=self.airbase and self.airbase:GetName()==airbase + + -- On ground + local onground=not group:InAir() + + -- In spawn zone. + local inspawnzone=group:IsPartlyOrCompletelyInZone(self.spawnzone) + + -- Check conditions for being back home. + local ishome=false + if category==Group.Category.GROUND or category==Group.Category.HELICOPTER then + -- Units go back to the spawn zone, helicopters land and they should not move any more. + ishome=inspawnzone and onground and notmoving + elseif category==Group.Category.AIRPLANE then + -- Planes need to be on ground at their home airbase and should not move any more. + ishome=athomebase and onground and notmoving + end + + -- Debug text. + local text=string.format("Group %s: speed=%d km/h, onground=%s , airbase=%s, spawnzone=%s ==> ishome=%s", group:GetName(), speed, tostring(onground), airbase, tostring(inspawnzone), tostring(ishome)) + self:T(self.lid..text) + + if ishome then + + -- Info message. + local text=string.format("Warehouse %s: Transport group arrived back home and no cargo left for request id=%d.\nSending transport group %s back to stock.", self.alias, request.uid, group:GetName()) + self:T(self.lid..text) + + -- Debug smoke. + if self.Debug then + group:SmokeRed() + end + + -- Group arrived. + self:Arrived(group) + end + end + end + + end + else - ----------------------------------- - -- No cargo but still transports -- - ----------------------------------- - - -- This is difficult! How do I know if transports were unused? They could also be just on their way back home. - -- ==> Need to do a lot of checks. - - -- All transports are dead but there is still cargo left ==> Put cargo back into stock. - for _,_group in pairs(request.transportgroupset:GetSetObjects()) do - local group=_group --Wrapper.Group#GROUP - - -- Check if group is alive. - if group and group:IsAlive() then - - -- Check if group is in the spawn zone? - local category=group:GetCategory() - - -- Get current speed. - local speed=group:GetVelocityKMH() - local notmoving=speed<1 - - -- Closest airbase. - local airbase=group:GetCoordinate():GetClosestAirbase():GetName() - local athomebase=self.airbase and self.airbase:GetName()==airbase - - -- On ground - local onground=not group:InAir() - - -- In spawn zone. - local inspawnzone=group:IsPartlyOrCompletelyInZone(self.spawnzone) - - -- Check conditions for being back home. - local ishome=false - if category==Group.Category.GROUND or category==Group.Category.HELICOPTER then - -- Units go back to the spawn zone, helicopters land and they should not move any more. - ishome=inspawnzone and onground and notmoving - elseif category==Group.Category.AIRPLANE then - -- Planes need to be on ground at their home airbase and should not move any more. - ishome=athomebase and onground and notmoving - end - - -- Debug text. - local text=string.format("Group %s: speed=%d km/h, onground=%s , airbase=%s, spawnzone=%s ==> ishome=%s", group:GetName(), speed, tostring(onground), airbase, tostring(inspawnzone), tostring(ishome)) - self:I(self.lid..text) - - if ishome then - - -- Info message. - local text=string.format("Warehouse %s: Transport group arrived back home and no cargo left for request id=%d.\nSending transport group %s back to stock.", self.alias, request.uid, group:GetName()) - self:_InfoMessage(text) - - -- Debug smoke. - if self.Debug then - group:SmokeRed() + + if ntransport==0 and request.ntransport>0 then + ----------------------------------- + -- Still cargo but no transports -- + ----------------------------------- + + local ncargoalive=0 + + -- All transports are dead but there is still cargo left ==> Put cargo back into stock. + for _,_group in pairs(request.cargogroupset:GetSetObjects()) do + --local group=group --Wrapper.Group#GROUP + + -- These groups have been respawned as cargo, i.e. their name changed! + local groupname=_group:GetName() + local group=GROUP:FindByName(groupname.."#CARGO") + + -- Check if group is alive. + if group and group:IsAlive() then + + -- Check if group is in spawn zone? + if group:IsPartlyOrCompletelyInZone(self.spawnzone) then + -- Debug smoke. + if self.Debug then + group:SmokeBlue() + end + -- Add asset group back to stock. + self:AddAsset(group) + ncargoalive=ncargoalive+1 end - - -- Group arrived. - self:Arrived(group) end + end + + -- Info message. + self:_InfoMessage(string.format("Warehouse %s: All transports of request id=%s dead! Putting remaining %s cargo assets back into warehouse!", self.alias, request.uid, ncargoalive)) end - + end - - else - - if ntransport==0 and request.ntransport>0 then - ----------------------------------- - -- Still cargo but no transports -- - ----------------------------------- - - local ncargoalive=0 - - -- All transports are dead but there is still cargo left ==> Put cargo back into stock. - for _,_group in pairs(request.cargogroupset:GetSetObjects()) do - --local group=group --Wrapper.Group#GROUP - - -- These groups have been respawned as cargo, i.e. their name changed! - local groupname=_group:GetName() - local group=GROUP:FindByName(groupname.."#CARGO") - - -- Check if group is alive. - if group and group:IsAlive() then - - -- Check if group is in spawn zone? - if group:IsPartlyOrCompletelyInZone(self.spawnzone) then - -- Debug smoke. - if self.Debug then - group:SmokeBlue() - end - -- Add asset group back to stock. - self:AddAsset(group) - ncargoalive=ncargoalive+1 - end - end - - end - - -- Info message. - self:_InfoMessage(string.format("Warehouse %s: All transports of request id=%s dead! Putting remaining %s cargo assets back into warehouse!", self.alias, request.uid, ncargoalive)) - end - - end - + end -- born check end -- loop over requests -- Remove pending requests if done. @@ -3821,6 +3833,11 @@ function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribu asset.spawned=false asset.iscargo=nil asset.arrived=nil + + -- Destroy group if it is alive. + if group:IsAlive()==true then + asset.damage=asset.life0-group:GetLife() + end -- Add asset to stock. table.insert(self.stock, asset) @@ -3865,7 +3882,8 @@ function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribu if group:IsAlive()==true then self:_DebugMessage(string.format("Removing group %s", group:GetName()), 5) -- Setting parameter to false, i.e. creating NO dead or remove unit event, seems to not confuse the dispatcher logic. - group:Destroy(false) + -- TODO: It would be nice, however, to have the remove event. + group:Destroy() --(false) end else @@ -3991,6 +4009,8 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, asset.skill=skill asset.assignment=assignment asset.spawned=false + asset.life0=group:GetLife0() + asset.damage=0 asset.spawngroupname=string.format("%s_AID-%d", templategroupname, asset.uid) if i==1 then @@ -4262,10 +4282,12 @@ end function WAREHOUSE:onafterRequest(From, Event, To, Request) -- Info message. - local text=string.format("Warehouse %s: Processing request id=%d from warehouse %s.\n", self.alias, Request.uid, Request.warehouse.alias) - text=text..string.format("Requested %s assets of %s=%s.\n", tostring(Request.nasset), Request.assetdesc, Request.assetdesc==WAREHOUSE.Descriptor.ASSETLIST and "Asset list" or Request.assetdescval) - text=text..string.format("Transports %s of type %s.", tostring(Request.ntransport), tostring(Request.transporttype)) - self:_InfoMessage(text, 5) + if self.verbosity>=1 then + local text=string.format("Warehouse %s: Processing request id=%d from warehouse %s.\n", self.alias, Request.uid, Request.warehouse.alias) + text=text..string.format("Requested %s assets of %s=%s.\n", tostring(Request.nasset), Request.assetdesc, Request.assetdesc==WAREHOUSE.Descriptor.ASSETLIST and "Asset list" or Request.assetdescval) + text=text..string.format("Transports %s of type %s.", tostring(Request.ntransport), tostring(Request.transporttype)) + self:_InfoMessage(text, 5) + end ------------------------------------------------------------------------------------------------------------------------------------ -- Cargo assets. @@ -4790,22 +4812,8 @@ function WAREHOUSE:onafterArrived(From, Event, To, group) group:RouteGroundTo(warehouse:GetCoordinate(), group:GetSpeedMax()*0.3, "Off Road") end - -- NOTE: This is done in the AddAsset() function. Dont know, why we do it also here. - --[[ - if istransport==true then - request.ntransporthome=request.ntransporthome+1 - request.transportgroupset:Remove(group:GetName(), true) - self:T2(warehouse.lid..string.format("Transport %d of %s returned home.", request.ntransporthome, tostring(request.ntransport))) - elseif istransport==false then - request.ndelivered=request.ndelivered+1 - request.cargogroupset:Remove(self:_GetNameWithOut(group), true) - self:T2(warehouse.lid..string.format("Cargo %d of %s delivered.", request.ndelivered, tostring(request.nasset))) - else - self:E(warehouse.lid..string.format("ERROR: Group %s is neither cargo nor transport!", group:GetName())) - end - ]] - -- Move asset from pending queue into new warehouse. + self:T(self.lid.."Asset arrived at warehouse adding in 60 sec") warehouse:__AddAsset(60, group) end @@ -4820,8 +4828,10 @@ end function WAREHOUSE:onafterDelivered(From, Event, To, request) -- Debug info - local text=string.format("Warehouse %s: All assets delivered to warehouse %s!", self.alias, request.warehouse.alias) - self:_InfoMessage(text, 5) + if self.verbosity>=1 then + local text=string.format("Warehouse %s: All assets delivered to warehouse %s!", self.alias, request.warehouse.alias) + self:_InfoMessage(text, 5) + end -- Make some noise :) if self.Debug then @@ -5150,7 +5160,7 @@ end -- @param #WAREHOUSE.Pendingitem request The request of the dead asset. function WAREHOUSE:onafterAssetSpawned(From, Event, To, group, asset, request) local text=string.format("Asset %s from request id=%d was spawned!", asset.spawngroupname, request.uid) - self:I(self.lid..text) + self:T(self.lid..text) -- Sete asset state to spawned. asset.spawned=true @@ -5161,22 +5171,23 @@ function WAREHOUSE:onafterAssetSpawned(From, Event, To, group, asset, request) local assetitem=_asset --#WAREHOUSE.Assetitem -- Debug info. - self:I(self.lid..string.format("Asset %s spawned %s as %s", assetitem.templatename, tostring(assetitem.spawned), tostring(assetitem.spawngroupname))) + self:T(self.lid..string.format("Asset %s spawned %s as %s", assetitem.templatename, tostring(assetitem.spawned), tostring(assetitem.spawngroupname))) if assetitem.spawned then n=n+1 else - self:E(self.lid.."FF What?! This should not happen!") + -- Now this can happend if multiple groups need to be spawned in one request. + --self:I(self.lid.."FF What?! This should not happen!") end end -- Trigger event. if n==request.nasset+request.ntransport then - self:T3(self.lid..string.format("All assets %d (ncargo=%d + ntransport=%d) of request rid=%d spawned. Calling RequestSpawned", n, request.nasset, request.ntransport, request.uid)) + self:T(self.lid..string.format("All assets %d (ncargo=%d + ntransport=%d) of request rid=%d spawned. Calling RequestSpawned", n, request.nasset, request.ntransport, request.uid)) self:RequestSpawned(request, request.cargogroupset, request.transportgroupset) else - self:T3(self.lid..string.format("Not all assets %d (ncargo=%d + ntransport=%d) of request rid=%d spawned YET", n, request.nasset, request.ntransport, request.uid)) + self:T(self.lid..string.format("Not all assets %d (ncargo=%d + ntransport=%d) of request rid=%d spawned YET", n, request.nasset, request.ntransport, request.uid)) end end @@ -5787,7 +5798,7 @@ function WAREHOUSE:_SpawnAssetPrepareTemplate(asset, alias) template.lateActivation=false if asset.missionTask then - self:I(self.lid..string.format("Setting mission task to %s", tostring(asset.missionTask))) + self:T(self.lid..string.format("Setting mission task to %s", tostring(asset.missionTask))) template.task=asset.missionTask end @@ -6111,36 +6122,46 @@ function WAREHOUSE:_OnEventBirth(EventData) -- Get asset and request from id. local asset=self:GetAssetByID(aid) local request=self:GetRequestByID(rid) - - -- Debug message. - self:T(self.lid..string.format("Warehouse %s captured event birth of its asset unit %s. spawned=%s", self.alias, EventData.IniUnitName, tostring(asset.spawned))) - - -- Birth is triggered for each unit. We need to make sure not to call this too often! - if not asset.spawned then - - -- Remove asset from stock. - self:_DeleteStockItem(asset) - - -- Set spawned switch. - asset.spawned=true - asset.spawngroupname=group:GetName() - - -- Add group. - if asset.iscargo==true then - request.cargogroupset=request.cargogroupset or SET_GROUP:New() - request.cargogroupset:AddGroup(group) - else - request.transportgroupset=request.transportgroupset or SET_GROUP:New() - request.transportgroupset:AddGroup(group) + + if asset and request then + + -- Debug message. + self:T(self.lid..string.format("Warehouse %s captured event birth of request ID=%d, asset ID=%d, unit %s spawned=%s", self.alias, request.uid, asset.uid, EventData.IniUnitName, tostring(asset.spawned))) + + -- Set born to true. + request.born=true + + -- Birth is triggered for each unit. We need to make sure not to call this too often! + if not asset.spawned then + + -- Remove asset from stock. + self:_DeleteStockItem(asset) + + -- Set spawned switch. + asset.spawned=true + asset.spawngroupname=group:GetName() + + -- Add group. + if asset.iscargo==true then + request.cargogroupset=request.cargogroupset or SET_GROUP:New() + request.cargogroupset:AddGroup(group) + else + request.transportgroupset=request.transportgroupset or SET_GROUP:New() + request.transportgroupset:AddGroup(group) + end + + -- Set warehouse state. + group:SetState(group, "WAREHOUSE", self) + + -- Asset spawned FSM function. + --self:__AssetSpawned(1, group, asset, request) + --env.info(string.format("FF asset spawned %s, %s", asset.spawngroupname, EventData.IniUnitName)) + self:AssetSpawned(group, asset, request) + end - - -- Set warehouse state. - group:SetState(group, "WAREHOUSE", self) - - -- Asset spawned FSM function. - --self:__AssetSpawned(1, group, asset, request) - self:AssetSpawned(group, asset, request) - + + else + self:E(self.lid..string.format("ERROR: Either asset AID=%s or request RID=%s are nil in event birth of unit %s", tostring(aid), tostring(rid), tostring(EventData.IniUnitName))) end else @@ -6260,7 +6281,6 @@ function WAREHOUSE:_OnEventArrived(EventData) local istransport=self:_GroupIsTransport(group, request) -- Get closest airbase. - -- Note, this crashed at somepoint when the Tarawa was in the mission. Don't know why. Deleting the Tarawa and adding it again solved the problem. local closest=group:GetCoordinate():GetClosestAirbase() -- Check if engine shutdown happend at right airbase because the event is also triggered in other situations. @@ -6269,15 +6289,19 @@ function WAREHOUSE:_OnEventArrived(EventData) -- Check that group is cargo and not transport. if istransport==false and rightairbase then - -- Debug info. - local text=string.format("Air asset group %s from warehouse %s arrived at its destination.", group:GetName(), self.alias) - self:_InfoMessage(text) - -- Trigger arrived event for this group. Note that each unit of a group will trigger this event. So the onafterArrived function needs to take care of that. -- Actually, we only take the first unit of the group that arrives. If it does, we assume the whole group arrived, which might not be the case, since -- some units might still be taxiing or whatever. Therefore, we add 10 seconds for each additional unit of the group until the first arrived event is triggered. local nunits=#group:GetUnits() local dt=10*(nunits-1)+1 -- one unit = 1 sec, two units = 11 sec, three units = 21 sec before we call the group arrived. + + -- Debug info. + if self.verbosity>=1 then + local text=string.format("Air asset group %s from warehouse %s arrived at its destination. Trigger Arrived event in %d sec", group:GetName(), self.alias, dt) + self:_InfoMessage(text) + end + + -- Arrived event. self:__Arrived(dt, group) end @@ -7497,7 +7521,7 @@ end function WAREHOUSE:_FindParkingForAssets(airbase, assets) -- Init default - local scanradius=100 + local scanradius=25 local scanunits=true local scanstatics=true local scanscenery=false @@ -7521,23 +7545,26 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) for i,unit in pairs(units) do local coord=COORDINATE:New(unit.x, unit.alt, unit.y) coords[unit.name]=coord - --[[ - local airbase=coord:GetClosestAirbase() - local _,TermID, dist, spot=coord:GetClosestParkingSpot(airbase) - if dist<=10 then - env.info(string.format("Found client %s on parking spot %d at airbase %s", unit.name, TermID, airbase:GetName())) - end - ]] end end return coords end -- Get parking spot data table. This contains all free and "non-free" spots. - local parkingdata=airbase:GetParkingSpotsTable() + local parkingdata=airbase.parking --airbase:GetParkingSpotsTable() + + --- + -- Find all obstacles + --- -- List of obstacles. local obstacles={} + + -- Check all clients. Clients dont change so we can put that out of the loop. + self.clientcoords=self.clientcoords or _clients() + for clientname,_coord in pairs(self.clientcoords) do + table.insert(obstacles, {coord=_coord, size=15, name=clientname, type="client"}) + end -- Loop over all parking spots and get the currently present obstacles. -- How long does this take on very large airbases, i.e. those with hundereds of parking spots? Seems to be okay! @@ -7553,22 +7580,16 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) -- Check all units. for _,_unit in pairs(_units) do local unit=_unit --Wrapper.Unit#UNIT - local _coord=unit:GetCoordinate() + local _coord=unit:GetVec3() local _size=self:_GetObjectSize(unit:GetDCSObject()) local _name=unit:GetName() table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="unit"}) end - -- Check all clients. - local clientcoords=_clients() - for clientname,_coord in pairs(clientcoords) do - table.insert(obstacles, {coord=_coord, size=15, name=clientname, type="client"}) - end - -- Check all statics. for _,static in pairs(_statics) do - local _vec3=static:getPoint() - local _coord=COORDINATE:NewFromVec3(_vec3) + local _coord=static:getPoint() + --local _coord=COORDINATE:NewFromVec3(_vec3) local _name=static:getName() local _size=self:_GetObjectSize(static) table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="static"}) @@ -7576,14 +7597,18 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) -- Check all scenery. for _,scenery in pairs(_sceneries) do - local _vec3=scenery:getPoint() - local _coord=COORDINATE:NewFromVec3(_vec3) + local _coord=scenery:getPoint() + --local _coord=COORDINATE:NewFromVec3(_vec3) local _name=scenery:getTypeName() local _size=self:_GetObjectSize(scenery) - table.insert(obstacles,{coord=_coord, size=_size, name=_name, type="scenery"}) + table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="scenery"}) end end + + --- + -- Get Parking Spots + --- -- Parking data for all assets. local parking={} @@ -7612,20 +7637,9 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) -- Coordinate of the parking spot. local _spot=parkingspot.Coordinate -- Core.Point#COORDINATE local _termid=parkingspot.TerminalID - local _toac=parkingspot.TOAC - - --env.info(string.format("FF asset=%s (id=%d): needs terminal type=%d, id=%d, #obstacles=%d", _asset.templatename, _asset.uid, terminaltype, _termid, #obstacles)) - local free=true local problem=nil - -- Safe parking using TO_AC from DCS result. - self:I(self.lid..string.format("Parking spot %d TOAC=%s (safe park=%s).", _termid, tostring(_toac), tostring(self.safeparking))) - if self.safeparking and _toac then - free=false - self:I(self.lid..string.format("Parking spot %d is occupied by other aircraft taking off (TOAC).", _termid)) - end - -- Loop over all obstacles. for _,obstacle in pairs(obstacles) do @@ -7652,7 +7666,8 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) -- Add parkingspot for this asset unit. table.insert(parking[_asset.uid], parkingspot) - self:I(self.lid..string.format("Parking spot %d is free for asset id=%d!", _termid, _asset.uid)) + -- Debug + self:T(self.lid..string.format("Parking spot %d is free for asset id=%d!", _termid, _asset.uid)) -- Add the unit as obstacle so that this spot will not be available for the next unit. table.insert(obstacles, {coord=_spot, size=_asset.size, name=_asset.templatename, type="asset"}) @@ -7663,7 +7678,7 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) else -- Debug output for occupied spots. - self:I(self.lid..string.format("Parking spot %d is occupied or not big enough!", _termid)) + self:T(self.lid..string.format("Parking spot %d is occupied or not big enough!", _termid)) if self.Debug then local coord=problem.coord --Core.Point#COORDINATE local text=string.format("Obstacle blocking spot #%d is %s type %s with size=%.1f m and distance=%.1f m.", _termid, problem.name, problem.type, problem.size, problem.dist) @@ -8288,67 +8303,70 @@ end -- @param #string name Name of the queue for info reasons. function WAREHOUSE:_PrintQueue(queue, name) - local total="Empty" - if #queue>0 then - total=string.format("Total = %d", #queue) - end + if self.verbosity>=2 then - -- Init string. - local text=string.format("%s at %s: %s",name, self.alias, total) - - for i,qitem in ipairs(queue) do - local qitem=qitem --#WAREHOUSE.Pendingitem - - local uid=qitem.uid - local prio=qitem.prio - local clock="N/A" - if qitem.timestamp then - clock=tostring(UTILS.SecondsToClock(qitem.timestamp)) + local total="Empty" + if #queue>0 then + total=string.format("Total = %d", #queue) end - local assignment=tostring(qitem.assignment) - local requestor=qitem.warehouse.alias - local airbasename=qitem.warehouse:GetAirbaseName() - local requestorAirbaseCat=qitem.warehouse:GetAirbaseCategory() - local assetdesc=qitem.assetdesc - local assetdescval=qitem.assetdescval - if assetdesc==WAREHOUSE.Descriptor.ASSETLIST then - assetdescval="Asset list" + + -- Init string. + local text=string.format("%s at %s: %s",name, self.alias, total) + + for i,qitem in ipairs(queue) do + local qitem=qitem --#WAREHOUSE.Pendingitem + + local uid=qitem.uid + local prio=qitem.prio + local clock="N/A" + if qitem.timestamp then + clock=tostring(UTILS.SecondsToClock(qitem.timestamp)) + end + local assignment=tostring(qitem.assignment) + local requestor=qitem.warehouse.alias + local airbasename=qitem.warehouse:GetAirbaseName() + local requestorAirbaseCat=qitem.warehouse:GetAirbaseCategory() + local assetdesc=qitem.assetdesc + local assetdescval=qitem.assetdescval + if assetdesc==WAREHOUSE.Descriptor.ASSETLIST then + assetdescval="Asset list" + end + local nasset=tostring(qitem.nasset) + local ndelivered=tostring(qitem.ndelivered) + local ncargogroupset="N/A" + if qitem.cargogroupset then + ncargogroupset=tostring(qitem.cargogroupset:Count()) + end + local transporttype="N/A" + if qitem.transporttype then + transporttype=qitem.transporttype + end + local ntransport="N/A" + if qitem.ntransport then + ntransport=tostring(qitem.ntransport) + end + local ntransportalive="N/A" + if qitem.transportgroupset then + ntransportalive=tostring(qitem.transportgroupset:Count()) + end + local ntransporthome="N/A" + if qitem.ntransporthome then + ntransporthome=tostring(qitem.ntransporthome) + end + + -- Output text: + text=text..string.format( + "\n%d) UID=%d, Prio=%d, Clock=%s, Assignment=%s | Requestor=%s [Airbase=%s, category=%d] | Assets(%s)=%s: #requested=%s / #alive=%s / #delivered=%s | Transport=%s: #requested=%s / #alive=%s / #home=%s", + i, uid, prio, clock, assignment, requestor, airbasename, requestorAirbaseCat, assetdesc, assetdescval, nasset, ncargogroupset, ndelivered, transporttype, ntransport, ntransportalive, ntransporthome) + end - local nasset=tostring(qitem.nasset) - local ndelivered=tostring(qitem.ndelivered) - local ncargogroupset="N/A" - if qitem.cargogroupset then - ncargogroupset=tostring(qitem.cargogroupset:Count()) - end - local transporttype="N/A" - if qitem.transporttype then - transporttype=qitem.transporttype - end - local ntransport="N/A" - if qitem.ntransport then - ntransport=tostring(qitem.ntransport) - end - local ntransportalive="N/A" - if qitem.transportgroupset then - ntransportalive=tostring(qitem.transportgroupset:Count()) - end - local ntransporthome="N/A" - if qitem.ntransporthome then - ntransporthome=tostring(qitem.ntransporthome) - end - - -- Output text: - text=text..string.format( - "\n%d) UID=%d, Prio=%d, Clock=%s, Assignment=%s | Requestor=%s [Airbase=%s, category=%d] | Assets(%s)=%s: #requested=%s / #alive=%s / #delivered=%s | Transport=%s: #requested=%s / #alive=%s / #home=%s", - i, uid, prio, clock, assignment, requestor, airbasename, requestorAirbaseCat, assetdesc, assetdescval, nasset, ncargogroupset, ndelivered, transporttype, ntransport, ntransportalive, ntransporthome) - - end - - if #queue==0 then - self:T(self.lid..text) - else - if total~="Empty" then + + if #queue==0 then self:I(self.lid..text) + else + if total~="Empty" then + self:I(self.lid..text) + end end end end @@ -8356,17 +8374,19 @@ end --- Display status of warehouse. -- @param #WAREHOUSE self function WAREHOUSE:_DisplayStatus() - local text=string.format("\n------------------------------------------------------\n") - text=text..string.format("Warehouse %s status: %s\n", self.alias, self:GetState()) - text=text..string.format("------------------------------------------------------\n") - text=text..string.format("Coalition name = %s\n", self:GetCoalitionName()) - text=text..string.format("Country name = %s\n", self:GetCountryName()) - text=text..string.format("Airbase name = %s (category=%d)\n", self:GetAirbaseName(), self:GetAirbaseCategory()) - text=text..string.format("Queued requests = %d\n", #self.queue) - text=text..string.format("Pending requests = %d\n", #self.pending) - text=text..string.format("------------------------------------------------------\n") - text=text..self:_GetStockAssetsText() - self:T(text) + if self.verbosity>=3 then + local text=string.format("\n------------------------------------------------------\n") + text=text..string.format("Warehouse %s status: %s\n", self.alias, self:GetState()) + text=text..string.format("------------------------------------------------------\n") + text=text..string.format("Coalition name = %s\n", self:GetCoalitionName()) + text=text..string.format("Country name = %s\n", self:GetCountryName()) + text=text..string.format("Airbase name = %s (category=%d)\n", self:GetAirbaseName(), self:GetAirbaseCategory()) + text=text..string.format("Queued requests = %d\n", #self.queue) + text=text..string.format("Pending requests = %d\n", #self.pending) + text=text..string.format("------------------------------------------------------\n") + text=text..self:_GetStockAssetsText() + self:I(text) + end end --- Get text about warehouse stock. @@ -8406,27 +8426,47 @@ function WAREHOUSE:_UpdateWarehouseMarkText() if self.markerOn then - -- Create a mark with the current assets in stock. - if self.markerid~=nil then - trigger.action.removeMark(self.markerid) - end - - -- Get assets in stock. - local _data=self:GetStockInfo(self.stock) - - -- Text. + -- Marker text. local text=string.format("Warehouse state: %s\nTotal assets in stock %d:\n", self:GetState(), #self.stock) - - for _attribute,_count in pairs(_data) do + for _attribute,_count in pairs(self:GetStockInfo(self.stock) or {}) do if _count>0 then local attribute=tostring(UTILS.Split(_attribute, "_")[2]) text=text..string.format("%s=%d, ", attribute,_count) end end + + local coordinate=self:GetCoordinate() + local coalition=self:GetCoalition() - -- Create/update marker at warehouse in F10 map. - self.markerid=self:GetCoordinate():MarkToCoalition(text, self:GetCoalition(), true) - + if not self.markerWarehouse then + + -- Create a new marker. + self.markerWarehouse=MARKER:New(coordinate, text):ToCoalition(coalition) + + else + + local refresh=false + + if self.markerWarehouse.text~=text then + self.markerWarehouse.text=text + refresh=true + end + + if self.markerWarehouse.coordinate~=coordinate then + self.markerWarehouse.coordinate=coordinate + refresh=true + end + + if self.markerWarehouse.coalition~=coalition then + self.markerWarehouse.coalition=coalition + refresh=true + end + + if refresh then + self.markerWarehouse:Refresh() + end + + end end end @@ -8479,8 +8519,8 @@ end -- @param #number duration Message display duration in seconds. Default 20 sec. If duration is zero, no message is displayed. function WAREHOUSE:_InfoMessage(text, duration) duration=duration or 20 - if duration>0 then - MESSAGE:New(text, duration):ToCoalitionIf(self:GetCoalition(), self.Debug or self.Report) + if duration>0 and self.Debug or self.Report then + MESSAGE:New(text, duration):ToCoalition(self:GetCoalition()) end self:I(self.lid..text) end diff --git a/Moose Development/Moose/Globals.lua b/Moose Development/Moose/Globals.lua index bdde44b6d..d972cf38b 100644 --- a/Moose Development/Moose/Globals.lua +++ b/Moose Development/Moose/Globals.lua @@ -15,4 +15,5 @@ _SETTINGS:SetPlayerMenuOn() _DATABASE:_RegisterCargos() _DATABASE:_RegisterZones() +_DATABASE:_RegisterAirbases() diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index 1f6a1662b..8e1cd2860 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -28,6 +28,7 @@ __Moose.Include( 'Scripts/Moose/Core/SpawnStatic.lua' ) __Moose.Include( 'Scripts/Moose/Core/Timer.lua' ) __Moose.Include( 'Scripts/Moose/Core/Goal.lua' ) __Moose.Include( 'Scripts/Moose/Core/Spot.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Astar.lua' ) __Moose.Include( 'Scripts/Moose/Wrapper/Object.lua' ) __Moose.Include( 'Scripts/Moose/Wrapper/Identifiable.lua' ) @@ -73,9 +74,11 @@ __Moose.Include( 'Scripts/Moose/Ops/RecoveryTanker.lua' ) __Moose.Include( 'Scripts/Moose/Ops/RescueHelo.lua' ) __Moose.Include( 'Scripts/Moose/Ops/ATIS.lua' ) __Moose.Include( 'Scripts/Moose/Ops/Auftrag.lua' ) +__Moose.Include( 'Scripts/Moose/Ops/Target.lua' ) __Moose.Include( 'Scripts/Moose/Ops/OpsGroup.lua' ) __Moose.Include( 'Scripts/Moose/Ops/FlightGroup.lua' ) __Moose.Include( 'Scripts/Moose/Ops/NavyGroup.lua' ) +__Moose.Include( 'Scripts/Moose/Ops/ArmyGroup.lua' ) __Moose.Include( 'Scripts/Moose/Ops/Squadron.lua' ) __Moose.Include( 'Scripts/Moose/Ops/AirWing.lua' ) __Moose.Include( 'Scripts/Moose/Ops/Intelligence.lua' ) diff --git a/Moose Development/Moose/Ops/AirWing.lua b/Moose Development/Moose/Ops/AirWing.lua index e12e082c1..9a768dd73 100644 --- a/Moose Development/Moose/Ops/AirWing.lua +++ b/Moose Development/Moose/Ops/AirWing.lua @@ -14,7 +14,6 @@ --- AIRWING class. -- @type AIRWING -- @field #string ClassName Name of the class. --- @field #boolean Debug Debug mode. Messages to all about status. -- @field #number verbose Verbosity of output. -- @field #string lid Class id string for output to DCS log file. -- @field #table menu Table of menu items. @@ -44,7 +43,7 @@ -- -- === -- --- ![Banner Image](..\Presentations\AirWing\AIRWING_Main.jpg) +-- ![Banner Image](..\Presentations\OPS\AirWing\_Main.png) -- -- # The AIRWING Concept -- @@ -66,7 +65,7 @@ -- At this point the airwing does not have any assets (aircraft). In order to add these, one needs to first define SQUADRONS. -- -- VFA151=SQUADRON:New("F-14 Group", 8, "VFA-151 (Vigilantes)") --- VFA151:AddMissonCapability({AUFTRAG.Type.PATROL, AUFTRAG.Type.INTERCEPT}) +-- VFA151:AddMissionCapability({AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT}) -- -- airwing:AddSquadron(VFA151) -- @@ -78,8 +77,8 @@ -- defined in the Mission Editor. -- -- -- F-14 payloads for CAP and INTERCEPT. Phoenix are first, sparrows are second choice. --- airwing:NewPayload(GROUP:FindByName("F-14 Payload AIM-54C"), 2, {AUFTRAG.Type.INTERCEPT, AUFTRAG.Type.PATROL}, 80) --- airwing:NewPayload(GROUP:FindByName("F-14 Payload AIM-7M"), 20, {AUFTRAG.Type.INTERCEPT, AUFTRAG.Type.PATROL}) +-- airwing:NewPayload(GROUP:FindByName("F-14 Payload AIM-54C"), 2, {AUFTRAG.Type.INTERCEPT, AUFTRAG.Type.GCICAP}, 80) +-- airwing:NewPayload(GROUP:FindByName("F-14 Payload AIM-7M"), 20, {AUFTRAG.Type.INTERCEPT, AUFTRAG.Type.GCICAP}) -- -- This will add two AIM-54C and 20 AIM-7M payloads. -- @@ -109,13 +108,13 @@ -- @field #AIRWING AIRWING = { ClassName = "AIRWING", - Debug = false, verbose = 0, lid = nil, menu = nil, squadrons = {}, missionqueue = {}, payloads = {}, + payloadcounter = 0, pointsCAP = {}, pointsTANKER = {}, pointsAWACS = {}, @@ -127,10 +126,12 @@ AIRWING = { -- @field #AIRWING.Payload payload The payload of the asset. -- @field Ops.FlightGroup#FLIGHTGROUP flightgroup The flightgroup object. -- @field #string squadname Name of the squadron this asset belongs to. +-- @field #number Treturned Time stamp when asset returned to the airwing. -- @extends Functional.Warehouse#WAREHOUSE.Assetitem --- Payload data. -- @type AIRWING.Payload +-- @field #number uid Unique payload ID. -- @field #string unitname Name of the unit this pylon was extracted from. -- @field #string aircrafttype Type of aircraft, which can use this payload. -- @field #table capabilities Mission types and performances for which this payload can be used. @@ -151,7 +152,7 @@ AIRWING = { --- AIRWING class version. -- @field #string version -AIRWING.version="0.2.1" +AIRWING.version="0.5.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list @@ -192,11 +193,16 @@ function AIRWING:New(warehousename, airwingname) self.lid=string.format("AIRWING %s | ", self.alias) -- Add FSM transitions. - -- From State --> Event --> To State - self:AddTransition("*", "MissionRequest", "*") -- Add a (mission) request to the warehouse. - self:AddTransition("*", "MissionCancel", "*") -- Cancel mission. + -- From State --> Event --> To State + self:AddTransition("*", "MissionRequest", "*") -- Add a (mission) request to the warehouse. + self:AddTransition("*", "MissionCancel", "*") -- Cancel mission. + + self:AddTransition("*", "SquadAssetReturned", "*") -- Flight was spawned with a mission. + + self:AddTransition("*", "FlightOnMission", "*") -- Flight was spawned with a mission. -- Defaults: + --self:SetVerbosity(0) self.nflightsCAP=0 self.nflightsAWACS=0 self.nflightsTANKERboom=0 @@ -225,15 +231,6 @@ function AIRWING:New(warehousename, airwingname) -- @param #AIRWING self -- @param #number delay Delay in seconds. - - -- Debug trace. - if false then - self.Debug=true - self:TraceOnOff(true) - self:TraceClass(self.ClassName) - self:TraceLevel(1) - end - return self end @@ -284,26 +281,28 @@ function AIRWING:NewPayload(Unit, Npayloads, MissionTypes, Performance) Performance=Performance or 50 if type(Unit)=="string" then - Unit=UNIT:FindByName(Unit) + local name=Unit + Unit=UNIT:FindByName(name) if not Unit then - Unit=GROUP:FindByName(Unit) + Unit=GROUP:FindByName(name) end end - -- If a GROUP object was given, get the first unit. - if Unit:IsInstanceOf("GROUP") then - Unit=Unit:GetUnit(1) - end - - -- Ensure Missiontypes is a table. - if MissionTypes and type(MissionTypes)~="table" then - MissionTypes={MissionTypes} - end - if Unit then + + -- If a GROUP object was given, get the first unit. + if Unit:IsInstanceOf("GROUP") then + Unit=Unit:GetUnit(1) + end + + -- Ensure Missiontypes is a table. + if MissionTypes and type(MissionTypes)~="table" then + MissionTypes={MissionTypes} + end + -- Create payload. local payload={} --#AIRWING.Payload - + payload.uid=self.payloadcounter payload.unitname=Unit:GetName() payload.aircrafttype=Unit:GetTypeName() payload.pylons=Unit:GetTemplatePayload() @@ -331,15 +330,20 @@ function AIRWING:NewPayload(Unit, Npayloads, MissionTypes, Performance) end -- Info - self:I(self.lid..string.format("Adding new payload from unit %s for aircraft type %s: N=%d (unlimited=%s), performance=%d, missions: %s", - payload.unitname, payload.aircrafttype, payload.navail, tostring(payload.unlimited), Performance, table.concat(MissionTypes, ", "))) + self:T(self.lid..string.format("Adding new payload from unit %s for aircraft type %s: ID=%d, N=%d (unlimited=%s), performance=%d, missions: %s", + payload.unitname, payload.aircrafttype, payload.uid, payload.navail, tostring(payload.unlimited), Performance, table.concat(MissionTypes, ", "))) -- Add payload table.insert(self.payloads, payload) + -- Increase counter + self.payloadcounter=self.payloadcounter+1 + return payload + end + self:E(self.lid.."ERROR: No UNIT found to create PAYLOAD!") return nil end @@ -377,17 +381,18 @@ end -- @param #AIRWING self -- @param #string UnitType The type of the unit. -- @param #string MissionType The mission type. +-- @param #table Payloads Specific payloads only to be considered. -- @return #AIRWING.Payload Payload table or *nil*. -function AIRWING:FetchPayloadFromStock(UnitType, MissionType) +function AIRWING:FetchPayloadFromStock(UnitType, MissionType, Payloads) -- Quick check if we have any payloads. if not self.payloads or #self.payloads==0 then - self:I(self.lid.."WARNING: No payloads in stock!") + self:T(self.lid.."WARNING: No payloads in stock!") return nil end -- Debug. - if self.Debug then + if self.verbose>=4 then self:I(self.lid..string.format("Looking for payload for unit type=%s and mission type=%s", UnitType, MissionType)) for i,_payload in pairs(self.payloads) do local payload=_payload --#AIRWING.Payload @@ -419,18 +424,38 @@ function AIRWING:FetchPayloadFromStock(UnitType, MissionType) end end + local function _checkPayloads(payload) + if Payloads then + for _,Payload in pairs(Payloads) do + if Payload.uid==payload.uid then + return true + end + end + else + -- Payload was not specified. + return nil + end + return false + end + -- Pre-selection: filter out only those payloads that are valid for the airframe and mission type and are available. local payloads={} for _,_payload in pairs(self.payloads) do local payload=_payload --#AIRWING.Payload - if payload.aircrafttype==UnitType and self:CheckMissionCapability(MissionType, payload.capabilities) and payload.navail>0 then + + local specialpayload=_checkPayloads(payload) + local compatible=self:CheckMissionCapability(MissionType, payload.capabilities) + + local goforit = specialpayload or (specialpayload==nil and compatible) + + if payload.aircrafttype==UnitType and payload.navail>0 and goforit then table.insert(payloads, payload) end end -- Debug. - if self.Debug then - self:I(self.lid..string.format("FF Sorted payloads for mission type X and aircraft type=Y:")) + if self.verbose>=4 then + self:I(self.lid..string.format("Sorted payloads for mission type X and aircraft type=Y:")) for _,_payload in ipairs(self.payloads) do local payload=_payload --#AIRWING.Payload if payload.aircrafttype==UnitType and self:CheckMissionCapability(MissionType, payload.capabilities) then @@ -530,6 +555,15 @@ function AIRWING:GetSquadron(SquadronName) return nil end +--- Set verbosity level. +-- @param #AIRWING self +-- @param #number VerbosityLevel Level of output (higher=more). Default 0. +-- @return #AIRWING self +function AIRWING:SetVerbosity(VerbosityLevel) + self.verbose=VerbosityLevel or 0 + return self +end + --- Get squadron of an asset. -- @param #AIRWING self -- @param #AIRWING.SquadronAsset Asset The squadron asset. @@ -540,7 +574,7 @@ end --- Remove asset from squadron. -- @param #AIRWING self --- @param #AIRWING.SquadronAsset Asset +-- @param #AIRWING.SquadronAsset Asset The squad asset. function AIRWING:RemoveAssetFromSquadron(Asset) local squad=self:GetSquadronOfAsset(Asset) if squad then @@ -563,7 +597,7 @@ function AIRWING:AddMission(Mission) -- Info text. local text=string.format("Added mission %s (type=%s). Starting at %s. Stopping at %s", tostring(Mission.name), tostring(Mission.type), UTILS.SecondsToClock(Mission.Tstart, true), Mission.Tstop and UTILS.SecondsToClock(Mission.Tstop, true) or "INF") - self:I(self.lid..text) + self:T(self.lid..text) return self end @@ -596,13 +630,20 @@ function AIRWING:SetNumberCAP(n) return self end ---- Set number of TANKER flights constantly in the air. +--- Set number of TANKER flights with Boom constantly in the air. -- @param #AIRWING self -- @param #number Nboom Number of flights. Default 1. +-- @return #AIRWING self +function AIRWING:SetNumberTankerBoom(Nboom) + self.nflightsTANKERboom=Nboom or 1 + return self +end + +--- Set number of TANKER flights with Probe constantly in the air. +-- @param #AIRWING self -- @param #number Nprobe Number of flights. Default 1. -- @return #AIRWING self -function AIRWING:SetNumberTANKER(Nboom, Nprobe) - self.nflightsTANKERboom=Nboom or 1 +function AIRWING:SetNumberTankerProbe(Nprobe) self.nflightsTANKERprobe=Nprobe or 1 return self end @@ -740,19 +781,6 @@ function AIRWING:onafterStart(From, Event, To) -- Info. self:I(self.lid..string.format("Starting AIRWING v%s", AIRWING.version)) - -- Menu. - if false then - - -- Add F10 radio menu. - self:_SetMenuCoalition() - - for _,_squadron in pairs(self.squadrons) do - local squadron=_squadron --Ops.Squadron#SQUADRON - self:_AddSquadonMenu(squadron) - end - - end - end --- Update status. @@ -776,93 +804,60 @@ function AIRWING:onafterStatus(From, Event, To) -- Check Rescue Helo missions. self:CheckRescuhelo() - -- Count missions not over yet. - local nmissions=self:CountMissionsInQueue() - - -- Count ALL payloads in stock. If any payload is unlimited, this gives 999. - local Npayloads=self:CountPayloadsInStock(AUFTRAG.Type) -- General info: - -- TODO: assets total - local text=string.format("Status %s: missions=%d, payloads=%d (%d), squads=%d", fsmstate, nmissions, #self.payloads, Npayloads, #self.squadrons) - self:I(self.lid..text) + if self.verbose>=1 then + + -- Count missions not over yet. + local Nmissions=self:CountMissionsInQueue() + + -- Count ALL payloads in stock. If any payload is unlimited, this gives 999. + local Npayloads=self:CountPayloadsInStock(AUFTRAG.Type) + + -- Assets tot + local Npq, Np, Nq=self:CountAssetsOnMission() + + local assets=string.format("%d (OnMission: Total=%d, Active=%d, Queued=%d)", self:CountAssets(), Npq, Np, Nq) + + -- Output. + local text=string.format("%s: Missions=%d, Payloads=%d (%d), Squads=%d, Assets=%s", fsmstate, Nmissions, Npayloads, #self.payloads, #self.squadrons, assets) + self:I(self.lid..text) + end ------------------ -- Mission Info -- ------------------ - local text=string.format("Missions Total=%d:", #self.missionqueue) - for i,_mission in pairs(self.missionqueue) do - local mission=_mission --Ops.Auftrag#AUFTRAG - text=text..string.format("\n[%d] %s: Status=%s, Nassets=%d, Prio=%d, ID=%d (%s)", i, mission.type, mission.status, mission.nassets, mission.prio, mission.auftragsnummer, mission.name) + if self.verbose>=2 then + local text=string.format("Missions Total=%d:", #self.missionqueue) + for i,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + local prio=string.format("%d/%s", mission.prio, tostring(mission.importance)) ; if mission.urgent then prio=prio.." (!)" end + local assets=string.format("%d/%d", mission:CountOpsGroups(), mission.nassets) + local target=string.format("%d/%d Damage=%.1f", mission:CountMissionTargets(), mission:GetTargetInitialNumber(), mission:GetTargetDamage()) + + text=text..string.format("\n[%d] %s %s: Status=%s, Prio=%s, Assets=%s, Targets=%s", i, mission.name, mission.type, mission.status, prio, assets, target) + end + self:I(self.lid..text) end - self:I(self.lid..text) - + ------------------- -- Squadron Info -- ------------------- - local text="Squadrons:" - for i,_squadron in pairs(self.squadrons) do - local squadron=_squadron --Ops.Squadron#SQUADRON - - local callsign=squadron.callsignName and UTILS.GetCallsignName(squadron.callsignName) or "N/A" - local modex=squadron.modex and squadron.modex or -1 - local skill=squadron.skill and tostring(squadron.skill) or "N/A" - - -- Squadron text - text=text..string.format("\n* %s %s: %s*%d/%d, Callsign=%s, Modex=%d, Skill=%s", squadron.name, squadron:GetState(), squadron.aircrafttype, squadron:CountAssetsInStock(), #squadron.assets, callsign, modex, skill) - - -- Loop over all assets. - if self.verbose>0 then - for j,_asset in pairs(squadron.assets) do - local asset=_asset --#AIRWING.SquadronAsset - local assignment=asset.assignment or "none" - local name=asset.templatename - local spawned=tostring(asset.spawned) - local groupname=asset.spawngroupname - local typename=asset.unittype - - local mission=self:GetAssetCurrentMission(asset) - local missiontext="" - if mission then - local distance=asset.flightgroup and UTILS.MetersToNM(mission:GetTargetDistance(asset.flightgroup.group:GetCoordinate())) or 0 - missiontext=string.format(" [%s (%s): status=%s, distance=%.1f NM]", mission.type, mission.name, mission.status, distance) - end - - -- Mission info. - text=text..string.format("\n -[%d] %s*%d \"%s\": spawned=%s, mission=%s%s", j, typename, asset.nunits, asset.spawngroupname, spawned, tostring(self:IsAssetOnMission(asset)), missiontext) - - -- Payload info. - local payload=asset.payload and table.concat(self:GetPayloadMissionTypes(asset.payload), ", ") or "None" - text=text.." payload="..payload - - -- Flight status. - text=text..", flight: " - if asset.flightgroup and asset.flightgroup:IsAlive() then - local status=asset.flightgroup:GetState() - local fuelmin=asset.flightgroup:GetFuelMin() - local fuellow=asset.flightgroup:IsFuelLow() - local fuelcri=asset.flightgroup:IsFuelCritical() - - text=text..string.format("%s fuel=%d", status, fuelmin) - if fuelcri then - text=text.." (critical!)" - elseif fuellow then - text=text.." (low)" - end - - local lifept, lifept0=asset.flightgroup:GetLifePoints() - text=text..string.format(" life=%d/%d", lifept, lifept0) - - local ammo=asset.flightgroup:GetAmmoTot() - text=text..string.format(" ammo=[G=%d, R=%d, B=%d, M=%d]", ammo.Guns, ammo.Rockets, ammo.Bombs, ammo.Missiles) - else - text=text.."N/A" - end - end + if self.verbose>=3 then + local text="Squadrons:" + for i,_squadron in pairs(self.squadrons) do + local squadron=_squadron --Ops.Squadron#SQUADRON + local callsign=squadron.callsignName and UTILS.GetCallsignName(squadron.callsignName) or "N/A" + local modex=squadron.modex and squadron.modex or -1 + local skill=squadron.skill and tostring(squadron.skill) or "N/A" + + -- Squadron text + text=text..string.format("\n* %s %s: %s*%d/%d, Callsign=%s, Modex=%d, Skill=%s", squadron.name, squadron:GetState(), squadron.aircrafttype, squadron:CountAssetsInStock(), #squadron.assets, callsign, modex, skill) end + self:I(self.lid..text) end - self:I(self.lid..text) -------------- -- Mission --- @@ -911,7 +906,7 @@ end -- @return #AIRWING self function AIRWING:CheckCAP() - local Ncap=self:CountMissionsInQueue({AUFTRAG.Type.PATROL, AUFTRAG.Type.INTERCEPT}) + local Ncap=self:CountMissionsInQueue({AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT}) for i=1,self.nflightsCAP-Ncap do @@ -919,7 +914,7 @@ function AIRWING:CheckCAP() local altitude=patrol.altitude+1000*patrol.noccupied - local missionCAP=AUFTRAG:NewPATROL(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg) + local missionCAP=AUFTRAG:NewGCICAP(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg) missionCAP.patroldata=patrol @@ -1031,7 +1026,9 @@ function AIRWING:CheckRescuhelo() local N=self:CountMissionsInQueue({AUFTRAG.Type.RESCUEHELO}) - local carrier=UNIT:FindByName(self.airbase:GetName()) + local name=self.airbase:GetName() + + local carrier=UNIT:FindByName(name) for i=1,self.nflightsRescueHelo-N do @@ -1066,7 +1063,11 @@ function AIRWING:GetTankerForFlight(flightgroup) local dist=assetcoord:Get2DDistance(tankercoord) - table.insert(tankeropt, {tanker=tanker, dist=dist}) + -- Ensure that the flight does not find itself. Asset could be a tanker! + if dist>5 then + table.insert(tankeropt, {tanker=tanker, dist=dist}) + end + end end @@ -1074,14 +1075,18 @@ function AIRWING:GetTankerForFlight(flightgroup) table.sort(tankeropt, function(a,b) return a.dist0 then + return tankeropt[1].tanker + else + return nil + end end return nil end ---- Get next mission. +--- Check if mission is not over and ready to cancel. -- @param #AIRWING self function AIRWING:_CheckMissions() @@ -1089,12 +1094,8 @@ function AIRWING:_CheckMissions() for _,_mission in pairs(self.missionqueue) do local mission=_mission --Ops.Auftrag#AUFTRAG - if mission:IsNotOver() then - - if mission:IsReadyToCancel() then - mission:Cancel() - end - + if mission:IsNotOver() and mission:IsReadyToCancel() then + mission:Cancel() end end @@ -1120,6 +1121,15 @@ function AIRWING:_GetNextMission() end table.sort(self.missionqueue, _sort) + -- Look for first mission that is SCHEDULED. + local vip=math.huge + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + if mission.importance and mission.importancenunits then + table.insert(template.units, UTILS.DeepCopy(template.units[1])) + end + + -- Remove units if original template contains more than in grouping. + if squad.ngroupingnunits then + unit=nil + end + end + + asset.nunits=squad.ngrouping + end + + -- Create callsign and modex (needs to be after grouping). squad:GetCallsign(asset) squad:GetModex(asset) + + -- Set spawn group name. This has to include "AID-" for warehouse. + asset.spawngroupname=string.format("%s_AID-%d", squad.name, asset.uid) -- Add asset to squadron. squad:AddAsset(asset) - --asset.terminalType=AIRBASE.TerminalType.OpenBig + -- TODO + --asset.terminalType=AIRBASE.TerminalType.OpenBig else - self:I(self.lid..string.format("Asset %s from squadron %s returned! asset.assignment=\"%s\", assignment=\"%s\"", asset.spawngroupname, squad.name, tostring(asset.assignment), tostring(assignment))) - self:ReturnPayloadFromAsset(asset) - + --env.info("FF squad asset returned") + self:SquadAssetReturned(squad, asset) + end end end - +--- On after "AssetReturned" event. Triggered when an asset group returned to its airwing. +-- @param #AIRWING self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Squadron#SQUADRON Squadron The asset squadron. +-- @param #AIRWING.SquadronAsset Asset The asset that returned. +function AIRWING:onafterSquadAssetReturned(From, Event, To, Squadron, Asset) + -- Debug message. + self:T(self.lid..string.format("Asset %s from squadron %s returned! asset.assignment=\"%s\"", Asset.spawngroupname, Squadron.name, tostring(Asset.assignment))) + + -- Stop flightgroup. + if Asset.flightgroup and not Asset.flightgroup:IsStopped() then + Asset.flightgroup:Stop() + end + + -- Return payload. + self:ReturnPayloadFromAsset(Asset) + + -- Return tacan channel. + if Asset.tacan then + Squadron:ReturnTacan(Asset.tacan) + end + + -- Set timestamp. + Asset.Treturned=timer.getAbsTime() +end --- On after "AssetSpawned" event triggered when an asset group is spawned into the cruel world. @@ -1498,32 +1572,50 @@ function AIRWING:onafterAssetSpawned(From, Event, To, group, asset, request) -- Create a flight group. local flightgroup=self:_CreateFlightGroup(asset) - -- Set RTB on fuel critical. - flightgroup:SetFuelCriticalThreshold(nil, true) - -- Set airwing. - flightgroup:SetAirwing(self) + --- + -- Asset + --- -- Set asset flightgroup. asset.flightgroup=flightgroup + -- Not requested any more. + asset.requested=nil + + -- Did not return yet. + asset.Treturned=nil + + --- + -- Squadron + --- + -- Get the SQUADRON of the asset. local squadron=self:GetSquadronOfAsset(asset) - -- Set default TACAN channel. - local Tacan=squadron:GetTACAN() + -- Get TACAN channel. + local Tacan=squadron:FetchTacan() if Tacan then - flightgroup:SetDefaultTACAN(Tacan) + asset.tacan=Tacan end -- Set radio frequency and modulation local radioFreq, radioModu=squadron:GetRadio() if radioFreq then - flightgroup:SetDefaultRadio(radioFreq, radioModu) + flightgroup:SwitchRadio(radioFreq, radioModu) + end + + if squadron.fuellow then + flightgroup:SetFuelLowThreshold(squadron.fuellow) end - -- Not requested any more. - asset.requested=nil + if squadron.fuellowRefuel then + flightgroup:SetFuelLowRefuel(squadron.fuellowRefuel) + end + + --- + -- Mission + --- -- Get Mission (if any). local mission=self:GetMissionByID(request.assignment) @@ -1531,18 +1623,29 @@ function AIRWING:onafterAssetSpawned(From, Event, To, group, asset, request) -- Add mission to flightgroup queue. if mission then - -- RTB on low fuel if on PATROL. - if mission.type==AUFTRAG.Type.PATROL then - flightgroup:SetFuelLowThreshold(nil, true) + if Tacan then + mission:SetTACAN(Tacan, Morse, UnitName, Band) end -- Add mission to flightgroup queue. asset.flightgroup:AddMission(mission) + + -- Trigger event. + self:FlightOnMission(flightgroup, mission) + + else + + if Tacan then + flightgroup:SwitchTACAN(Tacan, Morse, UnitName, Band) + end + end + + -- Add group to the detection set of the WINGCOMMANDER. - if self.wingcommander then - self.wingcommander.detectionset:AddGroup(asset.flightgroup.group) + if self.wingcommander and self.wingcommander.chief then + self.wingcommander.chief.detectionset:AddGroup(asset.flightgroup.group) end end @@ -1560,8 +1663,8 @@ function AIRWING:onafterAssetDead(From, Event, To, asset, request) self:GetParent(self).onafterAssetDead(self, From, Event, To, asset, request) -- Add group to the detection set of the WINGCOMMANDER. - if self.wingcommander then - self.wingcommander.detectionset:RemoveGroupsByName({asset.spawngroupname}) + if self.wingcommander and self.wingcommander.chief then + self.wingcommander.chief.detectionset:RemoveGroupsByName({asset.spawngroupname}) end -- Remove asset from mission is done via Mission:AssetDead() call from flightgroup onafterFlightDead function @@ -1663,7 +1766,15 @@ function AIRWING:_CreateFlightGroup(asset) local flightgroup=FLIGHTGROUP:New(asset.spawngroupname) -- Set airwing. - flightgroup:SetAirwing(self) + flightgroup:SetAirwing(self) + + -- Set squadron. + flightgroup.squadron=self:GetSquadronOfAsset(asset) + + -- Set home base. + flightgroup.homebase=self.airbase + + --[[ --- Check if out of missiles. For A2A missions ==> RTB. function flightgroup:OnAfterOutOfMissiles() @@ -1689,6 +1800,8 @@ function AIRWING:_CreateFlightGroup(asset) end + ]] + return flightgroup end @@ -1770,8 +1883,15 @@ end -- @param #AIRWING self -- @param #table MissionTypes Types on mission to be checked. Default *all* possible types `AUFTRAG.Type`. -- @param #table UnitTypes Types of units. +-- @param #table Payloads Specific payloads to be counted only. -- @return #number Count of available payloads in stock. -function AIRWING:CountPayloadsInStock(MissionTypes, UnitTypes) +function AIRWING:CountPayloadsInStock(MissionTypes, UnitTypes, Payloads) + + if MissionTypes then + if type(MissionTypes)=="string" then + MissionTypes={MissionTypes} + end + end if UnitTypes then if type(UnitTypes)=="string" then @@ -1792,22 +1912,44 @@ function AIRWING:CountPayloadsInStock(MissionTypes, UnitTypes) end return false end + + local function _checkPayloads(payload) + if Payloads then + for _,Payload in pairs(Payloads) do + if Payload.uid==payload.uid then + return true + end + end + else + -- Payload was not specified. + return nil + end + return false + end local n=0 for _,_payload in pairs(self.payloads) do local payload=_payload --#AIRWING.Payload - if self:CheckMissionCapability(MissionTypes, payload.capabilities) and _checkUnitTypes(payload) then + for _,MissionType in pairs(MissionTypes) do - if payload.unlimited then - -- Payload is unlimited. Return a BIG number. - return 999 - else - n=n+payload.navail + local specialpayload=_checkPayloads(payload) + local compatible=self:CheckMissionCapability(MissionType, payload.capabilities) + + local goforit = specialpayload or (specialpayload==nil and compatible) + + if goforit and _checkUnitTypes(payload) then + + if payload.unlimited then + -- Payload is unlimited. Return a BIG number. + return 999 + else + n=n+payload.navail + end + end end - end return n @@ -1835,13 +1977,29 @@ function AIRWING:CountMissionsInQueue(MissionTypes) return N end +--- Count total number of assets. This is the sum of all squadron assets. +-- @param #AIRWING self +-- @return #number Amount of asset groups. +function AIRWING:CountAssets() + + local N=0 + + for _,_squad in pairs(self.squadrons) do + local squad=_squad --Ops.Squadron#SQUADRON + N=N+#squad.assets + end + + return N +end + --- Count assets on mission. -- @param #AIRWING self -- @param #table MissionTypes Types on mission to be checked. Default all. +-- @param Ops.Squadron#SQUADRON Squadron Only count assets of this squadron. Default count assets of all squadrons. -- @return #number Number of pending and queued assets. -- @return #number Number of pending assets. -- @return #number Number of queued assets. -function AIRWING:CountAssetsOnMission(MissionTypes) +function AIRWING:CountAssetsOnMission(MissionTypes, Squadron) local Nq=0 local Np=0 @@ -1850,23 +2008,28 @@ function AIRWING:CountAssetsOnMission(MissionTypes) local mission=_mission --Ops.Auftrag#AUFTRAG -- Check if this mission type is requested. - if self:CheckMissionType(mission.type, MissionTypes) then + if self:CheckMissionType(mission.type, MissionTypes or AUFTRAG.Type) then for _,_asset in pairs(mission.assets or {}) do local asset=_asset --#AIRWING.SquadronAsset - local request, isqueued=self:GetRequestByID(mission.requestID) + if Squadron==nil or Squadron.name==asset.squadname then - if isqueued then - Nq=Nq+1 - else - Np=Np+1 + local request, isqueued=self:GetRequestByID(mission.requestID) + + if isqueued then + Nq=Nq+1 + else + Np=Np+1 + end + end end end end + --env.info(string.format("FF N=%d Np=%d, Nq=%d", Np+Nq, Np, Nq)) return Np+Nq, Np, Nq end @@ -1874,7 +2037,7 @@ end -- @param #AIRWING self -- @param #table MissionTypes Types on mission to be checked. Default all. -- @return #table Assets on pending requests. -function AIRWING:GetAssetsOnMission(MissionTypes, IncludeQueued) +function AIRWING:GetAssetsOnMission(MissionTypes) local assets={} local Np=0 @@ -1897,14 +2060,6 @@ function AIRWING:GetAssetsOnMission(MissionTypes, IncludeQueued) return assets end ---- Check --- @param #AIRWING self --- @param #boolean onlyactive Count only the active ones. --- @return #table Table of unit types. -function AIRWING:_CheckSquads(onlyactive) - -end - --- Get the aircraft types of this airwing. -- @param #AIRWING self -- @param #boolean onlyactive Count only the active ones. @@ -1956,10 +2111,10 @@ function AIRWING:CanMission(Mission) local unittypes=self:GetAircraftTypes(true, squadrons) -- Count all payloads in stock. - local Npayloads=self:CountPayloadsInStock(Mission.type, unittypes) + local Npayloads=self:CountPayloadsInStock(Mission.type, unittypes, Mission.payloads) if Npayloads #Assets then - self:I(self.lid..string.format("INFO: Not enough assets available! Got %d but need at least %d", #Assets, Mission.nassets)) + self:T(self.lid..string.format("INFO: Not enough assets available! Got %d but need at least %d", #Assets, Mission.nassets)) Can=false end @@ -2135,140 +2278,6 @@ function AIRWING:GetMissionFromRequest(Request) return self:GetMissionFromRequestID(Request.uid) end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Menu Functions -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Patrol carrier. --- @param #AIRWING self --- @return #AIRWING self -function AIRWING:_SetMenuCoalition() - - -- Get coalition. - local Coalition=self:GetCoalition() - - -- Init menu table. - self.menu=self.menu or {} - - -- Init menu coalition table. - self.menu[Coalition]=self.menu[Coalition] or {} - - -- Shortcut. - local menu=self.menu[Coalition] - - if self.menusingle then - -- F10/Skipper/... - if not menu.AIRWING then - menu.AIRWING=MENU_COALITION:New(Coalition, "AIRWING") - end - else - -- F10/Skipper//... - if not menu.Root then - menu.Root=MENU_COALITION:New(Coalition, "AIRWING") - end - menu.AIRWING=MENU_COALITION:New(Coalition, self.alias, menu.Root) - end - - ------------------- - -- Squadron Menu -- - ------------------- - - menu.Squadron={} - menu.Squadron.Main= MENU_COALITION:New(Coalition, "Squadrons", menu.AIRWING) - - menu.Warehouse={} - menu.Warehouse.Main = MENU_COALITION:New(Coalition, "Warehouse", menu.AIRWING) - menu.Warehouse.Reports = MENU_COALITION_COMMAND:New(Coalition, "Reports On/Off", menu.Warehouse.Main, self.WarehouseReportsToggle, self) - menu.Warehouse.Assets = MENU_COALITION_COMMAND:New(Coalition, "Report Assets", menu.Warehouse.Main, self.ReportWarehouseStock, self) - - menu.ReportSquadrons = MENU_COALITION_COMMAND:New(Coalition, "Report Squadrons", menu.AIRWING, self.ReportSquadrons, self) - -end - ---- Report squadron status. --- @param #AIRWING self -function AIRWING:ReportSquadrons() - - local text="Squadron Report:" - - for i,_squadron in pairs(self.squadrons) do - local squadron=_squadron - - local name=squadron.name - - local nspawned=0 - local nstock=0 - for _,_asset in pairs(squadron.assets) do - local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem - - local n=asset.nunits - - if asset.spawned then - nspawned=nspawned+n - else - nstock=nstock+n - end - - end - - text=string.format("\n%s: AC on duty=%d, in stock=%d", name, nspawned, nstock) - - end - - self:I(self.lid..text) - MESSAGE:New(text, 10, "AIRWING", true):ToCoalition(self:GetCoalition()) - -end - - ---- Add sub menu for this intruder. --- @param #AIRWING self --- @param Ops.Squadron#SQUADRON squadron The squadron data. -function AIRWING:_AddSquadonMenu(squadron) - - local Coalition=self:GetCoalition() - - local root=self.menu[Coalition].Squadron.Main - - local menu=MENU_COALITION:New(Coalition, squadron.name, root) - - MENU_COALITION_COMMAND:New(Coalition, "Report", menu, self._ReportSq, self, squadron) - MENU_COALITION_COMMAND:New(Coalition, "Launch CAP", menu, self._LaunchCAP, self, squadron) - - -- Set menu. - squadron.menu=menu - -end - - ---- Report squadron status. --- @param #AIRWING self --- @param Ops.Squadron#SQUADRON squadron The squadron object. -function AIRWING:_ReportSq(squadron) - - local text=string.format("%s: %s assets:", squadron.name, tostring(squadron.categoryname)) - for i,_asset in pairs(squadron.assets) do - local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem - text=text..string.format("%d.) ") - end -end - ---- Warehouse reports on/off. --- @param #AIRWING self -function AIRWING:WarehouseReportsToggle() - self.Report=not self.Report - MESSAGE:New(string.format("Warehouse reports are now %s", tostring(self.Report)), 10, "AIRWING", true):ToCoalition(self:GetCoalition()) -end - - ---- Report warehouse stock. --- @param #AIRWING self -function AIRWING:ReportWarehouseStock() - local text=self:_GetStockAssetsText(false) - MESSAGE:New(text, 10, "AIRWING", true):ToCoalition(self:GetCoalition()) -end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index aa34cee89..c0009886d 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -260,7 +260,7 @@ -- -- That being said, this script allows you to use any of the three cases to be used at any time. Or, in other words, *you* need to specify when which case is safe and appropriate. -- --- This is a lot of responsability. *You* are the boss, but *you* need to make the right decisions or things will go terribly wrong! +-- This is a lot of responsibility. *You* are the boss, but *you* need to make the right decisions or things will go terribly wrong! -- -- Recovery windows can be set up via the @{#AIRBOSS.AddRecoveryWindow} function as explained below. With this it is possible to seamlessly (within reason!) switch recovery cases in the same mission. -- @@ -287,7 +287,7 @@ -- -- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case1_Landing.png) -- --- Once the aircraft reaches the Inital, the landing pattern begins. The important steps of the pattern are shown in the image above. +-- Once the aircraft reaches the Initial, the landing pattern begins. The important steps of the pattern are shown in the image above. -- -- -- ## CASE III @@ -1931,6 +1931,11 @@ function AIRBOSS:New(carriername, alias) -- Welcome players. self:SetWelcomePlayers(true) + + -- Coordinates + self.landingcoord=COORDINATE:New(0,0,0) --Core.Point#COORDINATE + self.sterncoord=COORDINATE:New(0, 0, 0) --Core.Point#COORDINATE + self.landingspotcoord=COORDINATE:New(0,0,0) --Core.Point#COORDINATE -- Init carrier parameters. if self.carriertype==AIRBOSS.CarrierType.STENNIS then @@ -2464,7 +2469,7 @@ function AIRBOSS:AddRecoveryWindow(starttime, stoptime, case, holdingoffset, tur local Tstart=UTILS.ClockToSeconds(starttime) -- Set stop time. - local Tstop=UTILS.ClockToSeconds(stoptime or Tstart+90*60) + local Tstop=stoptime and UTILS.ClockToSeconds(stoptime) or Tstart+90*60 -- Consistancy check for timing. if Tstart>Tstop then @@ -3379,6 +3384,12 @@ function AIRBOSS:onafterStart(From, Event, To) self:HandleEvent(EVENTS.Ejection) self:HandleEvent(EVENTS.PlayerLeaveUnit, self._PlayerLeft) self:HandleEvent(EVENTS.MissionEnd) + self:HandleEvent(EVENTS.RemoveUnit) + + --self.StatusScheduler=SCHEDULER:New(self) + --self.StatusScheduler:Schedule(self, self._Status, {}, 1, 0.5) + + self.StatusTimer=TIMER:New(self._Status, self):Start(2, 0.5) -- Start status check in 1 second. self:__Status(1) @@ -3391,19 +3402,12 @@ end -- @param #string To To state. function AIRBOSS:onafterStatus(From, Event, To) - if true then - --env.info("FF Status ==> return") - --return - end - -- Get current time. local time=timer.getTime() -- Update marshal and pattern queue every 30 seconds. if time-self.Tqueue>self.dTqueue then - --collectgarbage() - -- Get time. local clock=UTILS.SecondsToClock(timer.getAbsTime()) local eta=UTILS.SecondsToClock(self:_GetETAatNextWP()) @@ -3414,7 +3418,7 @@ function AIRBOSS:onafterStatus(From, Event, To) local speed=self.carrier:GetVelocityKNOTS() -- Check water is ahead. - local collision=self:_CheckCollisionCoord(pos:Translate(self.collisiondist, hdg)) + local collision=false --self:_CheckCollisionCoord(pos:Translate(self.collisiondist, hdg)) local holdtime=0 if self.holdtimestamp then @@ -3470,15 +3474,9 @@ function AIRBOSS:onafterStatus(From, Event, To) -- Disable turn into the wind for this window so that we do not do this all over again. self.recoverywindow.WIND=false end - - else - - -- Find path around the obstacle. - if not self.detour then - --self:_Pathfinder() - end - + end + end @@ -3509,14 +3507,21 @@ function AIRBOSS:onafterStatus(From, Event, To) self:_ActivateBeacons() end + -- Call status every ~0.5 seconds. + self:__Status(-30) + +end + +--- Check AI status. Pattern queue AI in the groove? Marshal queue AI arrived in holding zone? +-- @param #AIRBOSS self +function AIRBOSS:_Status() + -- Check player status. self:_CheckPlayerStatus() -- Check AI landing pattern status self:_CheckAIStatus() - -- Call status every ~0.5 seconds. - self:__Status(-self.dTstatus) end --- Check AI status. Pattern queue AI in the groove? Marshal queue AI arrived in holding zone? @@ -3567,12 +3572,16 @@ function AIRBOSS:_CheckAIStatus() -- Get lineup and distance to carrier. local lineup=self:_Lineup(unit, true) + + local unitcoord=unit:GetCoord() + + local dist=unitcoord:Get2DDistance(self:GetCoord()) -- Distance in NM. - local distance=UTILS.MetersToNM(unit:GetCoordinate():Get2DDistance(self:GetCoordinate())) + local distance=UTILS.MetersToNM(dist) -- Altitude in ft. - local alt=UTILS.MetersToFeet(unit:GetAltitude()) + local alt=UTILS.MetersToFeet(unitcoord.y) -- Check if parameters are right and flight is in the groove. if lineup<2 and distance<=0.75 and alt<500 and not element.ballcall then @@ -8825,6 +8834,58 @@ function AIRBOSS:OnEventEjection(EventData) end +--- Airboss event handler for event REMOVEUNIT. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData +function AIRBOSS:OnEventRemoveUnit(EventData) + self:F3({eventland = EventData}) + + -- Nil checks. + if EventData==nil then + self:E(self.lid.."ERROR: EventData=nil in event REMOVEUNIT!") + self:E(EventData) + return + end + if EventData.IniUnit==nil then + self:E(self.lid.."ERROR: EventData.IniUnit=nil in event REMOVEUNIT!") + self:E(EventData) + return + end + + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + self:T3(self.lid.."EJECT: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.lid.."EJECT: group = "..tostring(EventData.IniGroupName)) + self:T3(self.lid.."EJECT: player = "..tostring(_playername)) + + if _unit and _playername then + self:T(self.lid..string.format("Player %s removed!",_playername)) + + -- Get player flight. + local flight=self.players[_playername] + + -- Remove flight completely from all queues and collapse marshal if necessary. + if flight then + self:_RemoveFlight(flight, true) + end + + else + -- Debug message. + self:T(self.lid..string.format("AI unit %s removed!", EventData.IniUnitName)) + + -- Remove element/unit from flight group and from all queues if no elements alive. + self:_RemoveUnitFromFlight(EventData.IniUnit) + + -- What could happen is, that another element has landed (recovered) already and this one crashes. + -- This would mean that the flight would not be deleted from the queue ==> Check if section recovered. + local flight=self:_GetFlightFromGroupInQueue(EventData.IniGroup, self.flights) + self:_CheckSectionRecovered(flight) + end + +end + --- Airboss event handler for event player leave unit. -- @param #AIRBOSS self -- @param Core.Event#EVENTDATA EventData @@ -10287,24 +10348,25 @@ function AIRBOSS:_GetSternCoord() local FB=self:GetFinalBearing() -- Stern coordinate (sterndist<0). Also translate 10 meters starboard wrt Final bearing. - local stern=self:GetCoordinate() + self.sterncoord:UpdateFromCoordinate(self:GetCoordinate()) + --local stern=self:GetCoordinate() -- Stern coordinate (sterndist<0). if self.carriertype==AIRBOSS.CarrierType.TARAWA then -- Tarawa: Translate 8 meters port. - stern=stern:Translate(self.carrierparam.sterndist, hdg):Translate(8, FB-90) + self.sterncoord:Translate(self.carrierparam.sterndist, hdg, true, true):Translate(8, FB-90, true, true) elseif self.carriertype==AIRBOSS.CarrierType.STENNIS then -- Stennis: translate 7 meters starboard wrt Final bearing. - stern=stern:Translate(self.carrierparam.sterndist, hdg):Translate(7, FB+90) + self.sterncoord:Translate(self.carrierparam.sterndist, hdg, true, true):Translate(7, FB+90, true, true) else -- Nimitz SC: translate 8 meters starboard wrt Final bearing. - stern=stern:Translate(self.carrierparam.sterndist, hdg):Translate(8.5, FB+90) + self.sterncoord:Translate(self.carrierparam.sterndist, hdg, true, true):Translate(8.5, FB+90, true, true) end -- Set altitude. - stern:SetAltitude(self.carrierparam.deckheight) + self.sterncoord:SetAltitude(self.carrierparam.deckheight) - return stern + return self.sterncoord end --- Get wire from landing position. @@ -10504,6 +10566,8 @@ end -- @return Core.Zone#ZONE_POLYGON_BASE Initial zone. function AIRBOSS:_GetZoneInitial(case) + self.zoneInitial=self.zoneInitial or ZONE_POLYGON_BASE:New("Zone CASE I/II Initial") + -- Get radial, i.e. inverse of BRC. local radial=self:GetRadial(2, false, false) @@ -10511,7 +10575,7 @@ function AIRBOSS:_GetZoneInitial(case) local cv=self:GetCoordinate() -- Vec2 array. - local vec2 + local vec2={} if case==1 then -- Case I @@ -10542,9 +10606,12 @@ function AIRBOSS:_GetZoneInitial(case) end -- Polygon zone. - local zone=ZONE_POLYGON_BASE:New("Zone CASE I/II Initial", vec2) + --local zone=ZONE_POLYGON_BASE:New("Zone CASE I/II Initial", vec2) + + self.zoneInitial:UpdateFromVec2(vec2) - return zone + --return zone + return self.zoneInitial end --- Get lineup groove zone. @@ -10552,6 +10619,8 @@ end -- @return Core.Zone#ZONE_POLYGON_BASE Lineup zone. function AIRBOSS:_GetZoneLineup() + self.zoneLineup=self.zoneLineup or ZONE_POLYGON_BASE:New("Zone Lineup") + -- Get radial, i.e. inverse of BRC. local fbi=self:GetRadial(1, false, false) @@ -10567,11 +10636,14 @@ function AIRBOSS:_GetZoneLineup() -- Vec2 array. local vec2={c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2()} + + self.zoneLineup:UpdateFromVec2(vec2) -- Polygon zone. - local zone=ZONE_POLYGON_BASE:New("Zone Lineup", vec2) - - return zone + --local zone=ZONE_POLYGON_BASE:New("Zone Lineup", vec2) + --return zone + + return self.zoneLineup end @@ -10583,6 +10655,8 @@ end -- @return Core.Zone#ZONE_POLYGON_BASE Groove zone. function AIRBOSS:_GetZoneGroove(l, w, b) + self.zoneGroove=self.zoneGroove or ZONE_POLYGON_BASE:New("Zone Groove") + l=l or 1.50 w=w or 0.25 b=b or 0.10 @@ -10603,11 +10677,14 @@ function AIRBOSS:_GetZoneGroove(l, w, b) -- Vec2 array. local vec2={c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2(), c6:GetVec2()} + + self.zoneGroove:UpdateFromVec2(vec2) -- Polygon zone. - local zone=ZONE_POLYGON_BASE:New("Zone Groove", vec2) - - return zone + --local zone=ZONE_POLYGON_BASE:New("Zone Groove", vec2) + --return zone + + return self.zoneGroove end --- Get Bullseye zone with radius 1 NM and DME 3 NM from the carrier. Radial depends on recovery case. @@ -10631,8 +10708,9 @@ function AIRBOSS:_GetZoneBullseye(case) -- Create zone. local zone=ZONE_RADIUS:New("Zone Bullseye", vec2, radius) - return zone + + --self.zoneBullseye=self.zoneBullseye or ZONE_RADIUS:New("Zone Bullseye", vec2, radius) end --- Get dirty up zone with radius 1 NM and DME 9 NM from the carrier. Radial depends on recovery case. @@ -10828,6 +10906,8 @@ end -- @return Core.Zone#ZONE Zone surrounding the carrier. function AIRBOSS:_GetZoneCarrierBox() + self.zoneCarrierbox=self.zoneCarrierbox or ZONE_POLYGON_BASE:New("Carrier Box Zone") + -- Stern coordinate. local S=self:_GetSternCoord() @@ -10856,9 +10936,12 @@ function AIRBOSS:_GetZoneCarrierBox() end -- Create polygon zone. - local zone=ZONE_POLYGON_BASE:New("Carrier Box Zone", vec2) - - return zone + --local zone=ZONE_POLYGON_BASE:New("Carrier Box Zone", vec2) + --return zone + + self.zoneCarrierbox:UpdateFromVec2(vec2) + + return self.zoneCarrierbox end --- Get zone of landing runway. @@ -10866,6 +10949,8 @@ end -- @return Core.Zone#ZONE_POLYGON Zone surrounding landing runway. function AIRBOSS:_GetZoneRunwayBox() + self.zoneRunwaybox=self.zoneRunwaybox or ZONE_POLYGON_BASE:New("Landing Runway Zone") + -- Stern coordinate. local S=self:_GetSternCoord() @@ -10888,9 +10973,12 @@ function AIRBOSS:_GetZoneRunwayBox() end -- Create polygon zone. - local zone=ZONE_POLYGON_BASE:New("Landing Runway Zone", vec2) - - return zone + --local zone=ZONE_POLYGON_BASE:New("Landing Runway Zone", vec2) + --return zone + + self.zoneRunwaybox:UpdateFromVec2(vec2) + + return self.zoneRunwaybox end @@ -10993,12 +11081,14 @@ function AIRBOSS:_GetZoneHolding(case, stack) -- Post 2.5 NM port of carrier. local Post=self:GetCoordinate():Translate(D, hdg+270) + --TODO: update zone not creating a new one. + -- Create holding zone. - zoneHolding=ZONE_RADIUS:New("CASE I Holding Zone", Post:GetVec2(), self.marshalradius) + self.zoneHolding=ZONE_RADIUS:New("CASE I Holding Zone", Post:GetVec2(), self.marshalradius) -- Delta pattern. if self.carriertype==AIRBOSS.CarrierType.TARAWA then - zoneHolding=ZONE_RADIUS:New("CASE I Holding Zone", self.carrier:GetVec2(), UTILS.NMToMeters(5)) + self.zoneHolding=ZONE_RADIUS:New("CASE I Holding Zone", self.carrier:GetVec2(), UTILS.NMToMeters(5)) end @@ -11017,10 +11107,12 @@ function AIRBOSS:_GetZoneHolding(case, stack) -- Square zone length=7NM width=6 NM behind the carrier starting at angels+15 NM behind the carrier. -- So stay 0-5 NM (+1 NM error margin) port of carrier. - zoneHolding=ZONE_POLYGON_BASE:New("CASE II/III Holding Zone", p) + self.zoneHolding=self.zoneHolding or ZONE_POLYGON_BASE:New("CASE II/III Holding Zone") + + self.zoneHolding:UpdateFromVec2(p) end - return zoneHolding + return self.zoneHolding end --- Get zone where player are automatically commence when enter. @@ -11061,7 +11153,9 @@ function AIRBOSS:_GetZoneCommence(case, stack) end -- Create holding zone. - zone=ZONE_RADIUS:New("CASE I Commence Zone", Three:GetVec2(), R) + self.zoneCommence=self.zoneCommence or ZONE_RADIUS:New("CASE I Commence Zone") + + self.zoneCommence:UpdateFromVec2(Three:GetVec2(), R) else -- Case II/III @@ -11092,11 +11186,13 @@ function AIRBOSS:_GetZoneCommence(case, stack) end -- Zone polygon. - zone=ZONE_POLYGON_BASE:New("CASE II/III Commence Zone", p) + self.zoneCommence=self.zoneCommence or ZONE_POLYGON_BASE:New("CASE II/III Commence Zone") + + self.zoneCommence:UpdateFromVec2(p) end - return zone + return self.zoneCommence end ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -11339,9 +11435,12 @@ end -- @param #AIRBOSS self -- @return Core.Point#COORDINATE Optimal landing coordinate. function AIRBOSS:_GetOptLandingCoordinate() + + -- Start with stern coordiante. + self.landingcoord:UpdateFromCoordinate(self:_GetSternCoord()) -- Stern coordinate. - local stern=self:_GetSternCoord() + --local stern=self:_GetSternCoord() -- Final bearing. local FB=self:GetFinalBearing(false) @@ -11349,10 +11448,11 @@ function AIRBOSS:_GetOptLandingCoordinate() if self.carriertype==AIRBOSS.CarrierType.TARAWA then -- Landing 100 ft abeam, 120 ft alt. - stern=self:_GetLandingSpotCoordinate():Translate(35, FB-90) + self.landingcoord:UpdateFromCoordinate(self:_GetLandingSpotCoordinate()):Translate(35, FB-90, true, true) + --stern=self:_GetLandingSpotCoordinate():Translate(35, FB-90) -- Alitude 120 ft. - stern:SetAltitude(UTILS.FeetToMeters(120)) + self.landingcoord:SetAltitude(UTILS.FeetToMeters(120)) else @@ -11360,15 +11460,15 @@ function AIRBOSS:_GetOptLandingCoordinate() if self.carrierparam.wire3 then -- We take the position of the 3rd wire to approximately account for the length of the aircraft. local w3=self.carrierparam.wire3 - stern=stern:Translate(w3, FB, true) + self.landingcoord:Translate(w3, FB, true, true) end -- Add 2 meters to account for aircraft height. - stern.y=stern.y+2 + self.landingcoord.y=self.landingcoord.y+2 end - return stern + return self.landingcoord end --- Get landing spot on Tarawa. @@ -11376,8 +11476,10 @@ end -- @return Core.Point#COORDINATE Primary landing spot coordinate. function AIRBOSS:_GetLandingSpotCoordinate() + self.landingspotcoord:UpdateFromCoordinate(self:_GetSternCoord()) + -- Stern coordinate. - local stern=self:_GetSternCoord() + --local stern=self:_GetSternCoord() if self.carriertype==AIRBOSS.CarrierType.TARAWA then @@ -11385,11 +11487,11 @@ function AIRBOSS:_GetLandingSpotCoordinate() local hdg=self:GetHeading() -- Primary landing spot 7.5 - stern=stern:Translate(57, hdg):SetAltitude(self.carrierparam.deckheight) + self.landingspotcoord:Translate(57, hdg, true, true):SetAltitude(self.carrierparam.deckheight) end - return stern + return self.landingspotcoord end --- Get true (or magnetic) heading of carrier. @@ -14333,9 +14435,15 @@ end -- @param #AIRBOSS self -- @return Core.Point#COORDINATE Carrier coordinate. function AIRBOSS:GetCoordinate() - return self.carrier:GetCoordinate() + return self.carrier:GetCoord() end +--- Get carrier coordinate. +-- @param #AIRBOSS self +-- @return Core.Point#COORDINATE Carrier coordinate. +function AIRBOSS:GetCoord() + return self.carrier:GetCoord() +end --- Get static weather of this mission from env.mission.weather. -- @param #AIRBOSS self diff --git a/Moose Development/Moose/Ops/ArmyGroup.lua b/Moose Development/Moose/Ops/ArmyGroup.lua new file mode 100644 index 000000000..94197f066 --- /dev/null +++ b/Moose Development/Moose/Ops/ArmyGroup.lua @@ -0,0 +1,838 @@ +--- **Ops** - Enhanced Ground Group. +-- +-- **Main Features:** +-- +-- * Dynamically add and remove waypoints. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Ops.ArmyGroup +-- @image OPS_ArmyGroup.png + + +--- ARMYGROUP class. +-- @type ARMYGROUP +-- @field #boolean adinfinitum Resume route at first waypoint when final waypoint is reached. +-- @field #boolean formationPerma Formation that is used permanently and overrules waypoint formations. +-- @extends Ops.OpsGroup#OPSGROUP + +--- *Your soul may belong to Jesus, but your ass belongs to the marines.* -- Eugene B. Sledge +-- +-- === +-- +-- ![Banner Image](..\Presentations\OPS\ArmyGroup\_Main.png) +-- +-- # The ARMYGROUP Concept +-- +-- This class enhances naval groups. +-- +-- @field #ARMYGROUP +ARMYGROUP = { + ClassName = "ARMYGROUP", + formationPerma = nil, +} + +--- Army group element. +-- @type ARMYGROUP.Element +-- @field #string name Name of the element, i.e. the unit. +-- @field #string typename Type name. + +--- Army Group version. +-- @field #string version +ARMYGROUP.version="0.1.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: A lot. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new ARMYGROUP class object. +-- @param #ARMYGROUP self +-- @param #string GroupName Name of the group. +-- @return #ARMYGROUP self +function ARMYGROUP:New(GroupName) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, OPSGROUP:New(GroupName)) -- #ARMYGROUP + + -- Set some string id for output to DCS.log file. + self.lid=string.format("ARMYGROUP %s | ", self.groupname) + + -- Defaults + self:SetDefaultROE() + self:SetDefaultAlarmstate() + self:SetDetection() + self:SetPatrolAdInfinitum(false) + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("*", "FullStop", "Holding") -- Hold position. + self:AddTransition("*", "Cruise", "Cruising") -- Hold position. + + self:AddTransition("*", "Detour", "OnDetour") -- Make a detour to a coordinate and resume route afterwards. + self:AddTransition("OnDetour", "DetourReached", "Cruising") -- Group reached the detour coordinate. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Stop". Stops the ARMYGROUP and all its event handlers. + -- @param #ARMYGROUP self + + --- Triggers the FSM event "Stop" after a delay. Stops the ARMYGROUP and all its event handlers. + -- @function [parent=#ARMYGROUP] __Stop + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + + -- TODO: Add pseudo functions. + + + -- Init waypoints. + self:InitWaypoints() + + -- Initialize the group. + self:_InitGroup() + + -- Handle events: + self:HandleEvent(EVENTS.Birth, self.OnEventBirth) + self:HandleEvent(EVENTS.Dead, self.OnEventDead) + self:HandleEvent(EVENTS.RemoveUnit, self.OnEventRemoveUnit) + + -- Start the status monitoring. + self:__Status(-1) + + -- Start queue update timer. + self.timerQueueUpdate=TIMER:New(self._QueueUpdate, self):Start(2, 5) + + -- Start check zone timer. + self.timerCheckZone=TIMER:New(self._CheckInZones, self):Start(2, 30) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Group patrols ad inifintum. If the last waypoint is reached, it will go to waypoint one and repeat its route. +-- @param #ARMYGROUP self +-- @param #boolean switch If true or nil, patrol until the end of time. If false, go along the waypoints once and stop. +-- @return #ARMYGROUP self +function ARMYGROUP:SetPatrolAdInfinitum(switch) + if switch==false then + self.adinfinitum=false + else + self.adinfinitum=true + end + return self +end + +--- Get coordinate of the closest road. +-- @param #ARMYGROUP self +-- @return Core.Point#COORDINATE Coordinate of a road closest to the group. +function ARMYGROUP:GetClosestRoad() + return self:GetCoordinate():GetClosestPointToRoad() +end + + +--- Add a *scheduled* task to fire at a given coordinate. +-- @param #ARMYGROUP self +-- @param Core.Point#COORDINATE Coordinate Coordinate of the target. +-- @param #string Clock Time when to start the attack. +-- @param #number Radius Radius in meters. Default 100 m. +-- @param #number Nshots Number of shots to fire. Default 3. +-- @param #number WeaponType Type of weapon. Default auto. +-- @param #number Prio Priority of the task. +-- @return Ops.OpsGroup#OPSGROUP.Task The task table. +function ARMYGROUP:AddTaskFireAtPoint(Coordinate, Clock, Radius, Nshots, WeaponType, Prio) + + local DCStask=CONTROLLABLE.TaskFireAtPoint(nil, Coordinate:GetVec2(), Radius, Nshots, WeaponType) + + local task=self:AddTask(DCStask, Clock, nil, Prio) + + return task +end + +--- Add a *waypoint* task to fire at a given coordinate. +-- @param #ARMYGROUP self +-- @param Core.Point#COORDINATE Coordinate Coordinate of the target. +-- @param Ops.OpsGroup#OPSGROUP.Waypoint Waypoint Where the task is executed. Default is next waypoint. +-- @param #number Radius Radius in meters. Default 100 m. +-- @param #number Nshots Number of shots to fire. Default 3. +-- @param #number WeaponType Type of weapon. Default auto. +-- @param #number Prio Priority of the task. +function ARMYGROUP:AddTaskWaypointFireAtPoint(Coordinate, Waypoint, Radius, Nshots, WeaponType, Prio) + + Waypoint=Waypoint or self:GetWaypointNext() + + local DCStask=CONTROLLABLE.TaskFireAtPoint(nil, Coordinate:GetVec2(), Radius, Nshots, WeaponType) + + self:AddTaskWaypoint(DCStask, Waypoint, nil, Prio) + +end + +--- Add a *scheduled* task. +-- @param #ARMYGROUP self +-- @param Wrapper.Group#GROUP TargetGroup Target group. +-- @param #number WeaponExpend How much weapons does are used. +-- @param #number WeaponType Type of weapon. Default auto. +-- @param #string Clock Time when to start the attack. +-- @param #number Prio Priority of the task. +function ARMYGROUP:AddTaskAttackGroup(TargetGroup, WeaponExpend, WeaponType, Clock, Prio) + + local DCStask=CONTROLLABLE.TaskAttackGroup(nil, TargetGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit, GroupAttack) + + self:AddTask(DCStask, Clock, nil, Prio) + +end + +--- Check if the group is currently holding its positon. +-- @param #ARMYGROUP self +-- @return #boolean If true, group was ordered to hold. +function ARMYGROUP:IsHolding() + return self:Is("Holding") +end + +--- Check if the group is currently cruising. +-- @param #ARMYGROUP self +-- @return #boolean If true, group cruising. +function ARMYGROUP:IsCruising() + return self:Is("Cruising") +end + +--- Check if the group is currently on a detour. +-- @param #ARMYGROUP self +-- @return #boolean If true, group is on a detour +function ARMYGROUP:IsOnDetour() + return self:Is("OnDetour") +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +---- Update status. +-- @param #ARMYGROUP self +function ARMYGROUP:onbeforeStatus(From, Event, To) + + if self:IsDead() then + self:T(self.lid..string.format("Onbefore Status DEAD ==> false")) + return false + elseif self:IsStopped() then + self:T(self.lid..string.format("Onbefore Status STOPPED ==> false")) + return false + end + + return true +end + +--- Update status. +-- @param #ARMYGROUP self +function ARMYGROUP:onafterStatus(From, Event, To) + + -- FSM state. + local fsmstate=self:GetState() + + if self:IsAlive() then + + --- + -- Detection + --- + + -- Check if group has detected any units. + if self.detectionOn then + self:_CheckDetectedUnits() + end + + -- Update position etc. + self:_UpdatePosition() + + -- Check if group got stuck. + self:_CheckStuck() + + if self.verbose>=1 then + + -- Get number of tasks and missions. + local nTaskTot, nTaskSched, nTaskWP=self:CountRemainingTasks() + local nMissions=self:CountRemainingMissison() + + local roe=self:GetROE() + local alarm=self:GetAlarmstate() + local speed=UTILS.MpsToKnots(self.velocity) + local speedEx=UTILS.MpsToKnots(self:GetExpectedSpeed()) + local formation=self.option.Formation + + -- Info text. + local text=string.format("%s: Wp=%d/%d-->%d Speed=%.1f (%d) Heading=%03d ROE=%d Alarm=%d Formation=%s Tasks=%d Missions=%d", + fsmstate, self.currentwp, #self.waypoints, self:GetWaypointIndexNext(), speed, speedEx, self.heading, roe, alarm, formation, nTaskTot, nMissions) + self:I(self.lid..text) + + end + + else + + -- Info text. + local text=string.format("State %s: Alive=%s", fsmstate, tostring(self:IsAlive())) + self:T2(self.lid..text) + + end + + + --- + -- Tasks & Missions + --- + + self:_PrintTaskAndMissionStatus() + + + -- Next status update. + self:__Status(-30) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Events +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "ElementSpawned" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #ARMYGROUP.Element Element The group element. +function ARMYGROUP:onafterElementSpawned(From, Event, To, Element) + self:T(self.lid..string.format("Element spawned %s", Element.name)) + + -- Set element status. + self:_UpdateStatus(Element, OPSGROUP.ElementStatus.SPAWNED) + +end + +--- On after "Spawned" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARMYGROUP:onafterSpawned(From, Event, To) + self:T(self.lid..string.format("Group spawned!")) + + -- Update position. + self:_UpdatePosition() + + if self.ai then + + -- Set default ROE. + self:SwitchROE(self.option.ROE) + + -- Set default Alarm State. + self:SwitchAlarmstate(self.option.Alarm) + + -- Set TACAN to default. + self:_SwitchTACAN() + + -- Turn on the radio. + if self.radioDefault then + self:SwitchRadio(self.radioDefault.Freq, self.radioDefault.Modu) + else + self:SetDefaultRadio(self.radio.Freq, self.radio.Modu, true) + end + + end + + -- Update route. + self:Cruise() + +end + +--- On after "UpdateRoute" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number n Waypoint number. Default is next waypoint. +-- @param #number Speed Speed in knots. Default cruise speed. +-- @param #number Formation Formation of the group. +function ARMYGROUP:onafterUpdateRoute(From, Event, To, n, Speed, Formation) + + -- Update route from this waypoint number onwards. + n=n or self:GetWaypointIndexNext(self.adinfinitum) + + -- Update waypoint tasks, i.e. inject WP tasks into waypoint table. + self:_UpdateWaypointTasks(n) + + -- Waypoints. + local waypoints={} + + -- Total number of waypoints + local N=#self.waypoints + + -- Add remaining waypoints to route. + for i=n, N do + + -- Copy waypoint. + local wp=UTILS.DeepCopy(self.waypoints[i]) --Ops.OpsGroup#OPSGROUP.Waypoint + + if i==n then + + --- + -- Next Waypoint + --- + + if Speed then + wp.speed=UTILS.KnotsToMps(Speed) + else + -- Take default waypoint speed. + end + + if self.formationPerma then + --if self.formationPerma==ENUMS.Formation.Vehicle.OnRoad then + wp.action=self.formationPerma + --end + elseif Formation then + wp.action=Formation + end + + -- Current set formation. + self.option.Formation=wp.action + + -- Current set speed in m/s. + self.speedWp=wp.speed + + else + + --- + -- Later Waypoint(s) + --- + + if self.formationPerma then + wp.action=self.formationPerma + else + -- Take default waypoint speed. + end + + end + + if wp.roaddist>100 and wp.action==ENUMS.Formation.Vehicle.OnRoad then + + -- Waypoint is actually off road! + wp.action=ENUMS.Formation.Vehicle.OffRoad + + -- Add "On Road" waypoint in between. + local wproad=wp.roadcoord:WaypointGround(wp.speed, ENUMS.Formation.Vehicle.OnRoad) + table.insert(waypoints, wproad) + end + + -- Debug info. + self:T(string.format("WP %d %s: Speed=%d m/s, alt=%d m, Action=%s", i, wp.type, wp.speed, wp.alt, wp.action)) + + -- Add waypoint. + table.insert(waypoints, wp) + end + + + -- Current waypoint. + local current=self:GetCoordinate():WaypointGround(UTILS.MpsToKmph(self.speedWp), self.option.Formation) + table.insert(waypoints, 1, current) + table.insert(waypoints, 1, current) -- Seems to be better to add this twice. Otherwise, the passing waypoint functions is triggered to early! + + if #waypoints>2 then + + self:T(self.lid..string.format("Updateing route: WP %d-->%d-->%d (#%d), Speed=%.1f knots, Formation=%s", + self.currentwp, n, #self.waypoints, #waypoints-2, UTILS.MpsToKnots(self.speedWp), tostring(self.option.Formation))) + + -- Route group to all defined waypoints remaining. + self:Route(waypoints) + + else + + --- + -- No waypoints left + --- + + self:E(self.lid..string.format("WARNING: No waypoints left ==> Full Stop!")) + self:FullStop() + + end + +end + +--- On after "Detour" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Coordinate Coordinate where to go. +-- @param #number Speed Speed in knots. Default cruise speed. +-- @param #number Formation Formation of the group. +-- @param #number ResumeRoute If true, resume route after detour point was reached. If false, the group will stop at the detour point and wait for futher commands. +function ARMYGROUP:onafterDetour(From, Event, To, Coordinate, Speed, Formation, ResumeRoute) + + -- Speed in knots. + Speed=Speed or self:GetSpeedCruise() + + -- ID of current waypoint. + local uid=self:GetWaypointCurrent().uid + + -- Add waypoint after current. + local wp=self:AddWaypoint(Coordinate, Speed, uid, Formation, true) + + -- Set if we want to resume route after reaching the detour waypoint. + if ResumeRoute then + wp.detour=1 + else + wp.detour=0 + end + +end + +--- On after "DetourReached" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARMYGROUP:onafterDetourReached(From, Event, To) + self:I(self.lid.."Group reached detour coordinate.") +end + + +--- On after "FullStop" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARMYGROUP:onafterFullStop(From, Event, To) + + -- Get current position. + local pos=self:GetCoordinate() + + -- Create a new waypoint. + local wp=pos:WaypointGround(0) + + -- Create new route consisting of only this position ==> Stop! + self:Route({wp}) + +end + +--- On after "Cruise" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number Speed Speed in knots. +-- @param #number Formation Formation. +function ARMYGROUP:onafterCruise(From, Event, To, Speed, Formation) + + self:__UpdateRoute(-1, nil, Speed, Formation) + +end + +--- On after Start event. Starts the ARMYGROUP FSM and event handlers. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARMYGROUP:onafterStop(From, Event, To) + + -- Handle events: + self:UnHandleEvent(EVENTS.Birth) + self:UnHandleEvent(EVENTS.Dead) + self:UnHandleEvent(EVENTS.RemoveUnit) + + -- Call OPSGROUP function. + self:GetParent(self).onafterStop(self, From, Event, To) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Events DCS +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Event function handling the birth of a unit. +-- @param #ARMYGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function ARMYGROUP:OnEventBirth(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then + local unit=EventData.IniUnit + local group=EventData.IniGroup + local unitname=EventData.IniUnitName + + if self.respawning then + + local function reset() + self.respawning=nil + end + + -- Reset switch in 1 sec. This should allow all birth events of n>1 groups to have passed. + -- TODO: Can I do this more rigorously? + self:ScheduleOnce(1, reset) + + else + + -- Get element. + local element=self:GetElementByName(unitname) + + -- Set element to spawned state. + self:T3(self.lid..string.format("EVENT: Element %s born ==> spawned", element.name)) + self:ElementSpawned(element) + + end + + end + +end + +--- Event function handling the crash of a unit. +-- @param #ARMYGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function ARMYGROUP:OnEventDead(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then + self:T(self.lid..string.format("EVENT: Unit %s dead!", EventData.IniUnitName)) + + local unit=EventData.IniUnit + local group=EventData.IniGroup + local unitname=EventData.IniUnitName + + -- Get element. + local element=self:GetElementByName(unitname) + + if element then + self:T(self.lid..string.format("EVENT: Element %s dead ==> destroyed", element.name)) + self:ElementDestroyed(element) + end + + end + +end + +--- Event function handling the crash of a unit. +-- @param #ARMYGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function ARMYGROUP:OnEventRemoveUnit(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then + local unit=EventData.IniUnit + local group=EventData.IniGroup + local unitname=EventData.IniUnitName + + -- Get element. + local element=self:GetElementByName(unitname) + + if element then + self:T(self.lid..string.format("EVENT: Element %s removed ==> dead", element.name)) + self:ElementDead(element) + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Routing +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add an a waypoint to the route. +-- @param #ARMYGROUP self +-- @param Core.Point#COORDINATE Coordinate The coordinate of the waypoint. Use COORDINATE:SetAltitude(altitude) to define the altitude. +-- @param #number Speed Speed in knots. Default is default cruise speed or 70% of max speed. +-- @param #number AfterWaypointWithID Insert waypoint after waypoint given ID. Default is to insert as last waypoint. +-- @param #number Formation Formation the group will use. +-- @param #boolean Updateroute If true or nil, call UpdateRoute. If false, no call. +-- @return Ops.OpsGroup#OPSGROUP.Waypoint Waypoint table. +function ARMYGROUP:AddWaypoint(Coordinate, Speed, AfterWaypointWithID, Formation, Updateroute) + + -- Set waypoint index. + local wpnumber=self:GetWaypointIndexAfterID(AfterWaypointWithID) + + -- Check if final waypoint is still passed. + if wpnumber>self.currentwp then + self.passedfinalwp=false + end + + -- Speed in knots. + Speed=Speed or self:GetSpeedCruise() + + -- Create a Naval waypoint. + local wp=Coordinate:WaypointGround(UTILS.KnotsToKmph(Speed), Formation) + + -- Create waypoint data table. + local waypoint=self:_CreateWaypoint(wp) + + -- Add waypoint to table. + self:_AddWaypoint(waypoint, wpnumber) + + -- Get closest point to road. + waypoint.roadcoord=Coordinate:GetClosestPointToRoad(false) + if waypoint.roadcoord then + waypoint.roaddist=Coordinate:Get2DDistance(waypoint.roadcoord) + else + waypoint.roaddist=1000*1000 --1000 km. + end + + -- Debug info. + self:T(self.lid..string.format("Adding waypoint UID=%d (index=%d), Speed=%.1f knots, Dist2Road=%d m, Action=%s", waypoint.uid, wpnumber, Speed, waypoint.roaddist, waypoint.action)) + + -- Update route. + if Updateroute==nil or Updateroute==true then + self:_CheckGroupDone(1) + end + + return waypoint +end + +--- Initialize group parameters. Also initializes waypoints if self.waypoints is nil. +-- @param #ARMYGROUP self +-- @return #ARMYGROUP self +function ARMYGROUP:_InitGroup() + + -- First check if group was already initialized. + if self.groupinitialized then + self:E(self.lid.."WARNING: Group was already initialized!") + return + end + + -- Get template of group. + self.template=self.group:GetTemplate() + + -- Define category. + self.isAircraft=false + self.isNaval=false + self.isGround=true + + -- Ships are always AI. + self.ai=true + + -- Is (template) group late activated. + self.isLateActivated=self.template.lateActivation + + -- Ground groups cannot be uncontrolled. + self.isUncontrolled=false + + -- Max speed in km/h. + self.speedMax=self.group:GetSpeedMax() + + -- Cruise speed in km/h + self.speedCruise=self.speedMax*0.7 + + -- Group ammo. + self.ammo=self:GetAmmoTot() + + -- Radio parameters from template. + self.radio.On=false -- Radio is always OFF for ground. + self.radio.Freq=133 + self.radio.Modu=radio.modulation.AM + + -- Set default radio. + self:SetDefaultRadio(self.radio.Freq, self.radio.Modu, self.radio.On) + + -- Set default formation from first waypoint. + self.option.Formation=self:GetWaypoint(1).action + self.optionDefault.Formation=self.option.Formation + + -- Units of the group. + local units=self.group:GetUnits() + + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + + local element={} --#ARMYGROUP.Element + element.name=unit:GetName() + element.typename=unit:GetTypeName() + element.status=OPSGROUP.ElementStatus.INUTERO + element.unit=unit + table.insert(self.elements, element) + + self:GetAmmoUnit(unit, false) + + if unit:IsAlive() then + self:ElementSpawned(element) + end + + end + + -- Get first unit. This is used to extract other parameters. + local unit=self.group:GetUnit(1) + + if unit then + + self.descriptors=unit:GetDesc() + + self.actype=unit:GetTypeName() + + -- Debug info. + if self.verbose>=1 then + local text=string.format("Initialized Army Group %s:\n", self.groupname) + text=text..string.format("Unit type = %s\n", self.actype) + text=text..string.format("Speed max = %.1f Knots\n", UTILS.KmphToKnots(self.speedMax)) + text=text..string.format("Speed cruise = %.1f Knots\n", UTILS.KmphToKnots(self.speedCruise)) + text=text..string.format("Elements = %d\n", #self.elements) + text=text..string.format("Waypoints = %d\n", #self.waypoints) + text=text..string.format("Radio = %.1f MHz %s %s\n", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu), tostring(self.radio.On)) + text=text..string.format("Ammo = %d (G=%d/R=%d/M=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Missiles) + text=text..string.format("FSM state = %s\n", self:GetState()) + text=text..string.format("Is alive = %s\n", tostring(self:IsAlive())) + text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated())) + self:I(self.lid..text) + end + + -- Init done. + self.groupinitialized=true + + end + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Option Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Switch to a specific formation. +-- @param #ARMYGROUP self +-- @param #number Formation New formation the group will fly in. Default is the setting of `SetDefaultFormation()`. +-- @param #boolean Permanently If true, formation always used from now on. +-- @return #ARMYGROUP self +function ARMYGROUP:SwitchFormation(Formation, Permanently) + + if self:IsAlive() then + + Formation=Formation or self.optionDefault.Formation + + if Permanently then + self.formationPerma=Formation + else + self.formationPerma=nil + end + + -- Set current formation. + self.option.Formation=Formation + + -- Update route with the new formation. + self:__UpdateRoute(-1, nil, nil, Formation) + + -- Debug info. + self:T(self.lid..string.format("Switching formation to %s (permanently=%s)", self.option.Formation, tostring(Permanently))) + + end + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Auftrag.lua b/Moose Development/Moose/Ops/Auftrag.lua index a801c5499..affd7e1b3 100644 --- a/Moose Development/Moose/Ops/Auftrag.lua +++ b/Moose Development/Moose/Ops/Auftrag.lua @@ -21,6 +21,7 @@ -- @type AUFTRAG -- @field #string ClassName Name of the class. -- @field #boolean Debug Debug mode. Messages to all about status. +-- @field #number verbose Verbosity level. -- @field #string lid Class id string for output to DCS log file. -- @field #number auftragsnummer Auftragsnummer. -- @field #string type Mission type. @@ -29,12 +30,16 @@ -- @field #string name Mission name. -- @field #number prio Mission priority. -- @field #boolean urgent Mission is urgent. Running missions with lower prio might be cancelled. +-- @field #number importance Importance. -- @field #number Tstart Mission start time in seconds. -- @field #number Tstop Mission stop time in seconds. -- @field #number duration Mission duration in seconds. -- @field Wrapper.Marker#MARKER marker F10 map marker. +-- @field #boolean markerOn If true, display marker on F10 map with the AUFTRAG status. +-- @field #number markerCoaliton Coalition to which the marker is dispayed. -- @field #table DCStask DCS task structure. --- @field #number Ntargets Number of mission targets. +-- @field #number Ncasualties Number of own casualties during mission. +-- @field #number Nelements Number of elements (units) assigned to mission. -- @field #number dTevaluate Time interval in seconds before the mission result is evaluated after mission is over. -- @field #number Tover Mission abs. time stamp, when mission was over. -- @field #table conditionStart Condition(s) that have to be true, before the mission will be started. @@ -47,7 +52,7 @@ -- @field #number orbitLeg Length of orbit leg in meters. -- @field Core.Point#COORDINATE orbitRaceTrack Race-track orbit coordinate. -- --- @field #AUFTRAG.TargetData engageTarget Target data to engage. +-- @field Ops.Target#TARGET engageTarget Target data to engage. -- -- @field Core.Zone#ZONE_RADIUS engageZone *Circular* engagement zone. -- @field #table engageTargetTypes Table of target types that are engaged in the engagement zone. @@ -81,46 +86,52 @@ -- @field #number nassets Number of required assets by the Airwing. -- @field #number requestID The ID of the queued warehouse request. Necessary to cancel the request if the mission was cancelled before the request is processed. -- @field #boolean cancelContactLost If true, cancel mission if the contact is lost. --- @field #table squadrons User specifed airwing squadrons assigned for this mission. Only these will be considered for the job! +-- @field #table squadrons User specified airwing squadrons assigned for this mission. Only these will be considered for the job! +-- @field #table payloads User specified airwing payloads for this mission. Only these will be considered for the job! -- @field Ops.AirWing#AIRWING.PatrolData patroldata Patrol data. -- -- @field #string missionTask Mission task. See `ENUMS.MissionTask`. -- @field #number missionAltitude Mission altitude in meters. -- @field #number missionFraction Mission coordiante fraction. Default is 0.5. -- @field #number missionRange Mission range in meters. Used in AIRWING class. +-- @field Core.Point#COORDINATE missionWaypointCoord Mission waypoint coordinate. -- -- @field #table enrouteTasks Mission enroute tasks. -- --- @field #number radioFreq Mission radio frequency in MHz. --- @field #number radioModu Mission radio modulation (0=AM and 1=FM). --- @field #number tacanChannel Mission TACAN channel. --- @field #number tacanMorse Mission TACAN morse code. +-- @field #number repeated Number of times mission was repeated. +-- @field #number repeatedSuccess Number of times mission was repeated after a success. +-- @field #number repeatedFailure Number of times mission was repeated after a failure. +-- @field #number Nrepeat Number of times the mission is repeated. +-- @field #number NrepeatFailure Number of times mission is repeated if failed. +-- @field #number NrepeatSuccess Number of times mission is repeated if successful. -- --- @field #number missionRepeated Number of times mission was repeated. --- @field #number missionRepeatMax Number of times mission is repeated if failed. +-- @field Ops.OpsGroup#OPSGROUP.Radio radio Radio freq and modulation. +-- @field Ops.OpsGroup#OPSGROUP.Beacon tacan TACAN setting. +-- @field Ops.OpsGroup#OPSGROUP.Beacon icls ICLS setting. -- -- @field #number optionROE ROE. -- @field #number optionROT ROT. --- @field #number optionCM Counter measures. +-- @field #number optionAlarm Alarm state. -- @field #number optionFormation Formation. +-- @field #number optionCM Counter measures. -- @field #number optionRTBammo RTB on out-of-ammo. -- @field #number optionRTBfuel RTB on out-of-fuel. -- @field #number optionECM ECM. -- -- @extends Core.Fsm#FSM ---- *A warrior's mission is to foster the success of others.* --- Morihei Ueshiba +--- *A warrior's mission is to foster the success of others.* - Morihei Ueshiba -- -- === -- --- ![Banner Image](..\Presentations\CarrierAirWing\AUFTRAG_Main.jpg) +-- ![Banner Image](..\Presentations\OPS\Auftrag\_Main.png) -- -- # The AUFTRAG Concept -- --- As you probably know, setting tasks in DCS is often tedious. The AUFTRAG class significantly simplifies the necessary workflow by using optimized default parameters. +-- The AUFTRAG class significantly simplifies the workflow of using DCS tasks. -- -- You can think of an AUFTRAG as document, which contains the mission briefing, i.e. information about the target location, mission altitude, speed and various other parameters. --- This document can be handed over directly to a pilot (or multiple pilots) via the FLIGHTGROUP class. The pilots will then execute the mission. +-- This document can be handed over directly to a pilot (or multiple pilots) via the @{Ops.FlightGroup#FLIGHTGROUP} class. The pilots will then execute the mission. -- The AUFTRAG document can also be given to an AIRWING. The airwing will then determine the best assets (pilots and payloads) available for the job. -- One more up the food chain, an AUFTRAG can be passed to a WINGCOMMANDER. The wing commander will find the best AIRWING and pass the job over to it. -- @@ -180,9 +191,9 @@ -- -- An orbit mission can be created with the @{#AUFTRAG.NewORBIT}() function. -- --- ## PATROL +-- ## GCICAP -- --- An patrol mission can be created with the @{#AUFTRAG.NewPATROL}() function. +-- An patrol mission can be created with the @{#AUFTRAG.NewGCICAP}() function. -- -- ## RECON -- @@ -252,13 +263,16 @@ AUFTRAG = { ClassName = "AUFTRAG", Debug = false, + verbose = 0, lid = nil, auftragsnummer = nil, - groupdata = {}, + groupdata = {}, assets = {}, missionFraction = 0.5, enrouteTasks = {}, marker = nil, + markerOn = nil, + markerCoalition = nil, conditionStart = {}, conditionSuccess = {}, conditionFailure = {}, @@ -283,7 +297,7 @@ _AUFTRAGSNR=0 -- @field #string FERRY Ferry flight mission. -- @field #string INTERCEPT Intercept mission. -- @field #string ORBIT Orbit mission. --- @field #string PATROL Similar to CAP but no auto engage targets. +-- @field #string GCICAP Similar to CAP but no auto engage targets. -- @field #string RECON Recon mission. -- @field #string RECOVERYTANKER Recovery tanker mission. Not implemented yet. -- @field #string RESCUEHELO Rescue helo. @@ -306,7 +320,7 @@ AUFTRAG.Type={ FERRY="Ferry Flight", INTERCEPT="Intercept", ORBIT="Orbit", - PATROL="Patrol", + GCICAP="Ground Controlled CAP", RECON="Recon", RECOVERYTANKER="Recovery Tanker", RESCUEHELO="Rescue Helo", @@ -416,30 +430,30 @@ AUFTRAG.TargetType={ --- AUFTRAG class version. -- @field #string version -AUFTRAG.version="0.3.0" +AUFTRAG.version="0.5.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Clone mission. How? Deepcopy? --- DONE: Option to assign mission to specific squadrons (requires an AIRWING). --- TODO: Option to assign a specific payload for the mission (requires an AIRWING). --- DONE: Add mission start conditions. --- TODO: Add recovery tanker mission for boat ops. --- DONE: Add rescue helo mission for boat ops. +-- DONE: Option to assign a specific payload for the mission (requires an AIRWING). -- TODO: Mission success options damaged, destroyed. +-- TODO: Recon mission. What input? Set of coordinates? +-- NOPE: Clone mission. How? Deepcopy? ==> Create a new auftrag. +-- TODO: F10 marker to create new missions. +-- TODO: Add recovery tanker mission for boat ops. +-- DONE: Option to assign mission to specific squadrons (requires an AIRWING). +-- DONE: Add mission start conditions. +-- DONE: Add rescue helo mission for boat ops. -- DONE: Mission ROE and ROT. -- DONE: Mission frequency and TACAN. --- TODO: Mission formation, etc. +-- DONE: Mission formation, etc. -- DONE: FSM events. -- DONE: F10 marker functions that are updated on Status event. --- TODO: F10 marker to create new missions. -- DONE: Evaluate mission result ==> SUCCESS/FAILURE -- DONE: NewAUTO() NewA2G NewA2A -- DONE: Transport mission. --- TODO: Recon mission. What input? Set of coordinates? --- TODO: Set mission coalition, e.g. for F10 markers. Could be derived from target if target has a coalition. +-- DONE: Set mission coalition, e.g. for F10 markers. Could be derived from target if target has a coalition. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor @@ -463,50 +477,59 @@ function AUFTRAG:New(Type) -- Auftragsnummer. self.auftragsnummer=_AUFTRAGSNR - -- Log id. + -- Log ID. self:_SetLogID() -- State is planned. self.status=AUFTRAG.Status.PLANNED -- Defaults + --self:SetVerbosity(0) self:SetName() self:SetPriority() self:SetTime() self.engageAsGroup=true - self.missionRepeated=0 - self.missionRepeatMax=0 + self.repeated=0 + self.repeatedSuccess=0 + self.repeatedFailure=0 + self.Nrepeat=0 + self.NrepeatFailure=0 + self.NrepeatSuccess=0 self.nassets=1 self.dTevaluate=0 + self.Ncasualties=0 + self.Nelements=0 -- FMS start state is PLANNED. self:SetStartState(self.status) -- PLANNED --> (QUEUED) --> (REQUESTED) --> SCHEDULED --> STARTED --> EXECUTING --> DONE - self:AddTransition(AUFTRAG.Status.PLANNED, "Queued", AUFTRAG.Status.QUEUED) -- Mission is in queue of an AIRWING. - self:AddTransition(AUFTRAG.Status.QUEUED, "Requested", AUFTRAG.Status.REQUESTED) -- Mission assets have been requested from the warehouse. - self:AddTransition(AUFTRAG.Status.REQUESTED, "Scheduled", AUFTRAG.Status.SCHEDULED) -- Mission added to the first ops group queue. + self:AddTransition("*", "Planned", AUFTRAG.Status.PLANNED) -- Mission is in planning stage. + self:AddTransition(AUFTRAG.Status.PLANNED, "Queued", AUFTRAG.Status.QUEUED) -- Mission is in queue of an AIRWING. + self:AddTransition(AUFTRAG.Status.QUEUED, "Requested", AUFTRAG.Status.REQUESTED) -- Mission assets have been requested from the warehouse. + self:AddTransition(AUFTRAG.Status.REQUESTED, "Scheduled", AUFTRAG.Status.SCHEDULED) -- Mission added to the first ops group queue. - self:AddTransition(AUFTRAG.Status.PLANNED, "Scheduled", AUFTRAG.Status.SCHEDULED) -- From planned directly to scheduled. + self:AddTransition(AUFTRAG.Status.PLANNED, "Scheduled", AUFTRAG.Status.SCHEDULED) -- From planned directly to scheduled. - self:AddTransition(AUFTRAG.Status.SCHEDULED, "Started", AUFTRAG.Status.STARTED) -- First asset has started the mission - self:AddTransition(AUFTRAG.Status.STARTED, "Executing", AUFTRAG.Status.EXECUTING) -- First asset is executing the mission. + self:AddTransition(AUFTRAG.Status.SCHEDULED, "Started", AUFTRAG.Status.STARTED) -- First asset has started the mission + self:AddTransition(AUFTRAG.Status.STARTED, "Executing", AUFTRAG.Status.EXECUTING) -- First asset is executing the mission. - self:AddTransition("*", "Done", AUFTRAG.Status.DONE) -- All assets have reported that mission is done. + self:AddTransition("*", "Done", AUFTRAG.Status.DONE) -- All assets have reported that mission is done. - self:AddTransition("*", "Cancel", "*") -- Command to cancel the mission. + self:AddTransition("*", "Cancel", "*") -- Command to cancel the mission. - self:AddTransition("*", "Success", AUFTRAG.Status.SUCCESS) - self:AddTransition("*", "Failed", AUFTRAG.Status.FAILED) + self:AddTransition("*", "Success", AUFTRAG.Status.SUCCESS) + self:AddTransition("*", "Failed", AUFTRAG.Status.FAILED) - self:AddTransition("*", "Status", "*") - self:AddTransition("*", "Stop", "*") + self:AddTransition("*", "Status", "*") + self:AddTransition("*", "Stop", "*") - self:AddTransition("*", "Repeat", AUFTRAG.Status.PLANNED) + self:AddTransition("*", "Repeat", AUFTRAG.Status.PLANNED) - self:AddTransition("*", "GroupDead", "*") - self:AddTransition("*", "AssetDead", "*") + self:AddTransition("*", "ElementDestroyed", "*") + self:AddTransition("*", "GroupDead", "*") + self:AddTransition("*", "AssetDead", "*") -- Init status update. self:__Status(-1) @@ -532,7 +555,7 @@ function AUFTRAG:NewANTISHIP(Target, Altitude) -- DCS task parameters: mission.engageWeaponType=ENUMS.WeaponFlag.Auto mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL - mission.engageAltitude=Altitude or UTILS.FeetToMeters(2000) + mission.engageAltitude=UTILS.FeetToMeters(Altitude or 2000) -- Mission options: mission.missionTask=ENUMS.MissionTask.ANTISHIPSTRIKE @@ -619,7 +642,8 @@ function AUFTRAG:NewORBIT_RACETRACK(Coordinate, Altitude, Speed, Heading, Leg) return mission end ---- Create a PATROL mission. +--- Create a Ground Controlled CAP (GCICAP) mission. Flights with this task are considered for A2A INTERCEPT missions by the CHIEF class. They will perform a compat air patrol but not engage by +-- themselfs. They wait for the CHIEF to tell them whom to engage. -- @param #AUFTRAG self -- @param Core.Point#COORDINATE Coordinate Where to orbit. -- @param #number Altitude Orbit altitude in feet. Default is y component of `Coordinate`. @@ -627,13 +651,13 @@ end -- @param #number Heading Heading of race-track pattern in degrees. Default random in [0, 360) degrees. -- @param #number Leg Length of race-track in NM. Default 10 NM. -- @return #AUFTRAG self -function AUFTRAG:NewPATROL(Coordinate, Altitude, Speed, Heading, Leg) +function AUFTRAG:NewGCICAP(Coordinate, Altitude, Speed, Heading, Leg) -- Create ORBIT first. local mission=AUFTRAG:NewORBIT_RACETRACK(Coordinate, Altitude, Speed, Heading, Leg) - -- Mission type PATROL. - mission.type=AUFTRAG.Type.PATROL + -- Mission type GCICAP. + mission.type=AUFTRAG.Type.GCICAP mission:_SetLogID() @@ -651,14 +675,14 @@ end -- @param #number Speed Orbit speed in knots. Default 350 kts. -- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West). -- @param #number Leg Length of race-track in NM. Default 10 NM. --- @param #number RefuelSystem Refueling system. +-- @param #number RefuelSystem Refueling system (0=boom, 1=probe). This info is *only* for AIRWINGs so they launch the right tanker type. -- @return #AUFTRAG self function AUFTRAG:NewTANKER(Coordinate, Altitude, Speed, Heading, Leg, RefuelSystem) -- Create ORBIT first. local mission=AUFTRAG:NewORBIT_RACETRACK(Coordinate, Altitude, Speed, Heading, Leg) - -- Mission type PATROL. + -- Mission type TANKER. mission.type=AUFTRAG.Type.TANKER mission:_SetLogID() @@ -688,7 +712,7 @@ function AUFTRAG:NewAWACS(Coordinate, Altitude, Speed, Heading, Leg) -- Create ORBIT first. local mission=AUFTRAG:NewORBIT_RACETRACK(Coordinate, Altitude, Speed, Heading, Leg) - -- Mission type PATROL. + -- Mission type AWACS. mission.type=AUFTRAG.Type.AWACS mission:_SetLogID() @@ -855,7 +879,7 @@ function AUFTRAG:NewBAI(Target, Altitude) -- DCS Task options: mission.engageWeaponType=ENUMS.WeaponFlag.AnyAG mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL - mission.engageAltitude=Altitude or UTILS.FeetToMeters(2000) + mission.engageAltitude=UTILS.FeetToMeters(Altitude or 2000) -- Mission options: mission.missionTask=ENUMS.MissionTask.GROUNDATTACK @@ -883,7 +907,7 @@ function AUFTRAG:NewSEAD(Target, Altitude) -- DCS Task options: mission.engageWeaponType=ENUMS.WeaponFlag.AnyAG --ENUMS.WeaponFlag.Cannons mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL - mission.engageAltitude=Altitude or UTILS.FeetToMeters(2000) + mission.engageAltitude=UTILS.FeetToMeters(Altitude or 2000) -- Mission options: mission.missionTask=ENUMS.MissionTask.SEAD @@ -968,6 +992,10 @@ function AUFTRAG:NewBOMBRUNWAY(Airdrome, Altitude) if type(Airdrome)=="string" then Airdrome=AIRBASE:FindByName(Airdrome) end + + if Airdrome:IsInstanceOf("AIRBASE") then + + end local mission=AUFTRAG:New(AUFTRAG.Type.BOMBRUNWAY) @@ -981,7 +1009,7 @@ function AUFTRAG:NewBOMBRUNWAY(Airdrome, Altitude) -- Mission options: mission.missionTask=ENUMS.MissionTask.RUNWAYATTACK mission.missionAltitude=mission.engageAltitude*0.8 - mission.missionFraction=0.2 + mission.missionFraction=0.75 mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.PassiveDefense @@ -1034,7 +1062,7 @@ end --- Create an ESCORT (or FOLLOW) mission. Flight will escort another group and automatically engage certain target types. -- @param #AUFTRAG self -- @param Wrapper.Group#GROUP EscortGroup The group to escort. --- @param DCS#Vec3 OffsetVector A table with x, y and z components specifying the offset of the flight to the escorted group. Default {x=200, y=0, z=-100} for 200 meters to the right, same alitude, 100 meters behind. +-- @param DCS#Vec3 OffsetVector A table with x, y and z components specifying the offset of the flight to the escorted group. Default {x=-100, y=0, z=200} for z=200 meters to the right, same alitude, x=100 meters behind. -- @param #number EngageMaxDistance Max engage distance of targets in meters. Default auto (*nil*). -- @param #table TargetTypes Types of targets to engage automatically. Default is {"Air"}, i.e. all enemy airborne units. Use an empty set {} for a simple "FOLLOW" mission. -- @return #AUFTRAG self @@ -1045,7 +1073,7 @@ function AUFTRAG:NewESCORT(EscortGroup, OffsetVector, EngageMaxDistance, TargetT mission:_TargetFromObject(EscortGroup) -- DCS task parameters: - mission.escortVec3=OffsetVector or {x=200, y=0, z=-100} + mission.escortVec3=OffsetVector or {x=-100, y=0, z=200} mission.engageMaxDistance=EngageMaxDistance mission.engageTargetTypes=TargetTypes or {"Air"} @@ -1069,6 +1097,8 @@ function AUFTRAG:NewRESCUEHELO(Carrier) local mission=AUFTRAG:New(AUFTRAG.Type.RESCUEHELO) + --mission.carrier=Carrier + mission:_TargetFromObject(Carrier) -- Mission options: @@ -1136,84 +1166,32 @@ function AUFTRAG:NewARTY(Target, Nshots, Radius) mission.artyShots=Nshots or 3 mission.artyRadius=Radius or 100 - mission.optionROE=ENUMS.ROE.OpenFire - mission.missionFraction=0.1 + mission.optionROE=ENUMS.ROE.OpenFire -- Ground/naval need open fire! + mission.optionAlarm=0 + + mission.missionFraction=0.0 + + -- Evaluate after 8 min. + mission.dTevaluate=8*60 mission.DCStask=mission:GetDCSMissionTask() return mission end - --- Create a mission to attack a group. Mission type is automatically chosen from the group category. -- @param #AUFTRAG self --- @param Wrapper.Group#GROUP EngageGroup Group to be engaged. +-- @param Ops.Target#TARGET Target The target. -- @return #AUFTRAG self -function AUFTRAG:NewAUTO(EngageGroup) +function AUFTRAG:NewTargetAir(Target) local mission=nil --#AUFTRAG - local group=EngageGroup - - if group and group:IsAlive() then + self.engageTarget=Target + + local target=self.engageTarget:GetObject() - local category=group:GetCategory() - local attribute=group:GetAttribute() - local threatlevel=group:GetThreatLevel() - - if category==Group.Category.AIRPLANE or category==Group.Category.HELICOPTER then - - --- - -- AIR - --- - - mission=AUFTRAG:NewINTERCEPT(group) - - elseif category==Group.Category.GROUND then - - --- - -- GROUND - --- - - --TODO: action depends on type - -- AA/SAM ==> SEAD - -- Tanks ==> - -- Artillery ==> - -- Infantry ==> - -- - - if attribute==GROUP.Attribute.GROUND_AAA or attribute==GROUP.Attribute.GROUND_SAM then - - -- SEAD/DEAD - - -- TODO: Attack radars first? Attack launchers? - - mission=AUFTRAG:NewSEAD(group) - - elseif attribute==GROUP.Attribute.GROUND_ARTILLERY then - - mission=AUFTRAG:NewBAI(group) - - elseif attribute==GROUP.Attribute.GROUND_INFANTRY then - - mission=AUFTRAG:NewBAI(group) - - else - - mission=AUFTRAG:NewBAI(group) - - end - - elseif category==Group.Category.SHIP then - - --- - -- NAVAL - --- - - mission=AUFTRAG:NewANTISHIP(group) - - end - end + local mission=self:NewAUTO(target) if mission then mission:SetPriority(10, true) @@ -1223,6 +1201,159 @@ function AUFTRAG:NewAUTO(EngageGroup) end +--- Create a mission to attack a group. Mission type is automatically chosen from the group category. +-- @param #AUFTRAG self +-- @param Wrapper.Positionable#POSITIONABLE Target Target object. +-- @return #string Auftrag type, e.g. `AUFTRAG.Type.BAI` (="BAI"). +function AUFTRAG:_DetermineAuftragType(Target) + + local group=nil --Wrapper.Group#GROUP + local airbase=nil --Wrapper.Airbase#AIRBASE + local scenery=nil --Wrapper.Scenery#SCENERY + local coordinate=nil --Core.Point#COORDINATE + local auftrag=nil + + if Target:IsInstanceOf("GROUP") then + group=Target --Target is already a group. + elseif Target:IsInstanceOf("UNIT") then + group=Target:GetGroup() + elseif Target:IsInstanceOf("AIRBASE") then + airbase=Target + elseif Target:IsInstanceOf("SCENERY") then + scenery=Target + end + + if group then + + local category=group:GetCategory() + local attribute=group:GetAttribute() + + if category==Group.Category.AIRPLANE or category==Group.Category.HELICOPTER then + + --- + -- A2A: Intercept + --- + + auftrag=AUFTRAG.Type.INTERCEPT + + elseif category==Group.Category.GROUND or category==Group.Category.TRAIN then + + --- + -- GROUND + --- + + if attribute==GROUP.Attribute.GROUND_SAM then + + -- SEAD/DEAD + + auftrag=AUFTRAG.Type.SEAD + + elseif attribute==GROUP.Attribute.GROUND_AAA then + + auftrag=AUFTRAG.Type.BAI + + elseif attribute==GROUP.Attribute.GROUND_ARTILLERY then + + auftrag=AUFTRAG.Type.BAI + + elseif attribute==GROUP.Attribute.GROUND_INFANTRY then + + auftrag=AUFTRAG.Type.BAI + + else + + auftrag=AUFTRAG.Type.BAI + + end + + + elseif category==Group.Category.SHIP then + + --- + -- NAVAL + --- + + auftrag=AUFTRAG.Type.ANTISHIP + + else + self:E(self.lid.."ERROR: Unknown Group category!") + end + + elseif airbase then + auftrag=AUFTRAG.Type.BOMBRUNWAY + elseif scenery then + auftrag=AUFTRAG.Type.STRIKE + elseif coordinate then + auftrag=AUFTRAG.Type.BOMBING + end + + return auftrag +end + +--- Create a mission to attack a group. Mission type is automatically chosen from the group category. +-- @param #AUFTRAG self +-- @param Wrapper.Group#GROUP EngageGroup Group to be engaged. +-- @return #AUFTRAG self +function AUFTRAG:NewAUTO(EngageGroup) + + local mission=nil --#AUFTRAG + + local Target=EngageGroup + + local auftrag=self:_DetermineAuftragType(EngageGroup) + + if auftrag==AUFTRAG.Type.ANTISHIP then + mission=AUFTRAG:NewANTISHIP(Target) + elseif auftrag==AUFTRAG.Type.ARTY then + mission=AUFTRAG:NewARTY(Target) + elseif auftrag==AUFTRAG.Type.AWACS then + mission=AUFTRAG:NewAWACS(Coordinate, Altitude,Speed,Heading,Leg) + elseif auftrag==AUFTRAG.Type.BAI then + mission=AUFTRAG:NewBAI(Target,Altitude) + elseif auftrag==AUFTRAG.Type.BOMBING then + mission=AUFTRAG:NewBOMBING(Target,Altitude) + elseif auftrag==AUFTRAG.Type.BOMBRUNWAY then + mission=AUFTRAG:NewBOMBRUNWAY(Airdrome,Altitude) + elseif auftrag==AUFTRAG.Type.BOMBCARPET then + mission=AUFTRAG:NewBOMBCARPET(Target,Altitude,CarpetLength) + elseif auftrag==AUFTRAG.Type.CAP then + mission=AUFTRAG:NewCAP(ZoneCAP,Altitude,Speed,Coordinate,Heading,Leg,TargetTypes) + elseif auftrag==AUFTRAG.Type.CAS then + mission=AUFTRAG:NewCAS(ZoneCAS,Altitude,Speed,Coordinate,Heading,Leg,TargetTypes) + elseif auftrag==AUFTRAG.Type.ESCORT then + mission=AUFTRAG:NewESCORT(EscortGroup,OffsetVector,EngageMaxDistance,TargetTypes) + elseif auftrag==AUFTRAG.Type.FACA then + mission=AUFTRAG:NewFACA(Target,Designation,DataLink,Frequency,Modulation) + elseif auftrag==AUFTRAG.Type.FERRY then + -- Not implemented yet. + elseif auftrag==AUFTRAG.Type.GCICAP then + mission=AUFTRAG:NewGCICAP(Coordinate,Altitude,Speed,Heading,Leg) + elseif auftrag==AUFTRAG.Type.INTERCEPT then + mission=AUFTRAG:NewINTERCEPT(Target) + elseif auftrag==AUFTRAG.Type.ORBIT then + mission=AUFTRAG:NewORBIT(Coordinate,Altitude,Speed,Heading,Leg) + elseif auftrag==AUFTRAG.Type.RECON then + -- Not implemented yet. + elseif auftrag==AUFTRAG.Type.RESCUEHELO then + mission=AUFTRAG:NewRESCUEHELO(Carrier) + elseif auftrag==AUFTRAG.Type.SEAD then + mission=AUFTRAG:NewSEAD(Target,Altitude) + elseif auftrag==AUFTRAG.Type.STRIKE then + mission=AUFTRAG:NewSTRIKE(Target,Altitude) + elseif auftrag==AUFTRAG.Type.TANKER then + mission=AUFTRAG:NewTANKER(Coordinate,Altitude,Speed,Heading,Leg,RefuelSystem) + elseif auftrag==AUFTRAG.Type.TROOPTRANSPORT then + mission=AUFTRAG:NewTROOPTRANSPORT(TransportGroupSet,DropoffCoordinate,PickupCoordinate) + else + + end + + if mission then + mission:SetPriority(10, true) + end + + return mission +end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- User API Functions @@ -1268,23 +1399,43 @@ end -- @param #AUFTRAG self -- @param #number Prio Priority 1=high, 100=low. Default 50. -- @param #boolean Urgent If *true*, another running mission might be cancelled if it has a lower priority. +-- @param #number Importance Number 1-10. If missions with lower value are in the queue, these have to be finished first. Default is `nil`. -- @return #AUFTRAG self -function AUFTRAG:SetPriority(Prio, Urgent) +function AUFTRAG:SetPriority(Prio, Urgent, Importance) self.prio=Prio or 50 self.urgent=Urgent + self.importance=Importance return self end ---- Set how many times the mission is repeated if it fails. +--- Set how many times the mission is repeated. Only valid if the mission is handled by an AIRWING or higher level. +-- @param #AUFTRAG self +-- @param #number Nrepeat Number of repeats. Default 0. +-- @return #AUFTRAG self +function AUFTRAG:SetRepeat(Nrepeat) + self.Nrepeat=Nrepeat or 0 + return self +end + +--- Set how many times the mission is repeated if it fails. Only valid if the mission is handled by an AIRWING or higher level. -- @param #AUFTRAG self -- @param #number Nrepeat Number of repeats. Default 0. -- @return #AUFTRAG self function AUFTRAG:SetRepeatOnFailure(Nrepeat) - self.missionRepeatMax=Nrepeat or 0 + self.NrepeatFailure=Nrepeat or 0 return self end ---- Define how many assets are required to do the job. +--- Set how many times the mission is repeated if it was successful. Only valid if the mission is handled by an AIRWING or higher level. +-- @param #AUFTRAG self +-- @param #number Nrepeat Number of repeats. Default 0. +-- @return #AUFTRAG self +function AUFTRAG:SetRepeatOnSuccess(Nrepeat) + self.NrepeatSuccess=Nrepeat or 0 + return self +end + +--- Define how many assets are required to do the job. Only valid if the mission is handled by an AIRWING or higher level. -- @param #AUFTRAG self -- @param #number Nassets Number of asset groups. Default 1. -- @return #AUFTRAG self @@ -1302,9 +1453,28 @@ function AUFTRAG:SetName(Name) return self end +--- Enable markers, which dispay the mission status on the F10 map. +-- @param #AUFTRAG self +-- @param #number Coalition The coaliton side to which the markers are dispayed. Default is to all. +-- @return #AUFTRAG self +function AUFTRAG:SetEnableMarkers(Coalition) + self.markerOn=true + self.markerCoaliton=Coalition or -1 + return self +end + +--- Set verbosity level. +-- @param #AUFTRAG self +-- @param #number VerbosityLevel Level of output (higher=more). Default 0. +-- @return #AUFTRAG self +function AUFTRAG:SetVerbosity(VerbosityLevel) + self.verbose=VerbosityLevel or 0 + return self +end + --- Set weapon type used for the engagement. -- @param #AUFTRAG self --- @param #number WeaponType Weapon type. Default is ENUMS.WeaponFlag.Auto +-- @param #number WeaponType Weapon type. Default is `ENUMS.WeaponFlag.Auto`. -- @return #AUFTRAG self function AUFTRAG:SetWeaponType(WeaponType) @@ -1348,7 +1518,7 @@ function AUFTRAG:SetEngageAsGroup(Switch) return self end ---- Set engage altitude. +--- Set engage altitude. This is the altitude passed to the DCS task. In the ME it is the tickbox ALTITUDE ABOVE. -- @param #AUFTRAG self -- @param #string Altitude Altitude in feet. Default 6000 ft. -- @return #AUFTRAG self @@ -1362,7 +1532,7 @@ function AUFTRAG:SetEngageAltitude(Altitude) return self end ---- Set mission altitude. +--- Set mission altitude. This is the altitude of the waypoint create where the DCS task is executed. -- @param #AUFTRAG self -- @param #string Altitude Altitude in feet. -- @return #AUFTRAG self @@ -1371,11 +1541,20 @@ function AUFTRAG:SetMissionAltitude(Altitude) return self end ---- Set max engage range. +--- Set mission speed. That is the speed the group uses to get to the mission waypoint. +-- @param #AUFTRAG self +-- @param #string Speed Mission speed in knots. +-- @return #AUFTRAG self +function AUFTRAG:SetMissionSpeed(Speed) + self.missionSpeed=Speed and UTILS.KnotsToKmph(Speed) or nil + return self +end + +--- Set max mission range. Only applies if the AUFTRAG is handled by an AIRWING or CHIEF. This is the max allowed distance from the airbase to the target. -- @param #AUFTRAG self -- @param #number Range Max range in NM. Default 100 NM. -- @return #AUFTRAG self -function AUFTRAG:SetEngageRange(Range) +function AUFTRAG:SetMissionRange(Range) self.engageRange=UTILS.NMToMeters(Range or 100) return self end @@ -1394,7 +1573,7 @@ end --- Set Reaction on Threat (ROT) for this mission. -- @param #AUFTRAG self --- @param #string roe Mission ROT. +-- @param #string rot Mission ROT. -- @return #AUFTRAG self function AUFTRAG:SetROT(rot) @@ -1403,6 +1582,17 @@ function AUFTRAG:SetROT(rot) return self end +--- Set alarm state for this mission. +-- @param #AUFTRAG self +-- @param #number Alarmstate Alarm state 0=Auto, 1=Green, 2=Red. +-- @return #AUFTRAG self +function AUFTRAG:SetAlarmstate(Alarmstate) + + self.optionAlarm=Alarmstate + + return self +end + --- Set formation for this mission. -- @param #AUFTRAG self -- @param #number Formation Formation. @@ -1420,9 +1610,10 @@ end -- @param #number Modulation Radio modulation. Default 0=AM. -- @return #AUFTRAG self function AUFTRAG:SetRadio(Frequency, Modulation) - - self.radioFreq=Frequency - self.radioModu=Modulation or 0 + + self.radio={} + self.radio.Freq=Frequency + self.radio.Modu=Modulation return self end @@ -1431,15 +1622,77 @@ end -- @param #AUFTRAG self -- @param #number Channel TACAN channel. -- @param #string Morse Morse code. Default "XXX". +-- @param #string UnitName Name of the unit in the group for which acts as TACAN beacon. Default is the first unit in the group. +-- @param #string Band Tacan channel mode ("X" or "Y"). Default is "X" for ground/naval and "Y" for aircraft. -- @return #AUFTRAG self -function AUFTRAG:SetTACAN(Channel, Morse) +function AUFTRAG:SetTACAN(Channel, Morse, UnitName, Band) - self.tacanChannel=Channel - self.tacanMorse=Morse or "XXX" + self.tacan={} + self.tacan.Channel=Channel + self.tacan.Morse=Morse or "XXX" + self.tacan.UnitName=UnitName + self.tacan.Band=Band return self end +--- Set ICLS beacon channel and Morse code for this mission. +-- @param #AUFTRAG self +-- @param #number Channel ICLS channel. +-- @param #string Morse Morse code. Default "XXX". +-- @param #string UnitName Name of the unit in the group for which acts as ICLS beacon. Default is the first unit in the group. +-- @return #AUFTRAG self +function AUFTRAG:SetICLS(Channel, Morse, UnitName) + + self.icls={} + self.icls.Channel=Channel + self.icls.Morse=Morse or "XXX" + self.icls.UnitName=UnitName + + return self +end + +--- Get mission type. +-- @param #AUFTRAG self +-- @return #string Mission type, e.g. "BAI". +function AUFTRAG:GetType() + return self.type +end + +--- Get mission name. +-- @param #AUFTRAG self +-- @return #string Mission name, e.g. "Auftrag Nr.1". +function AUFTRAG:GetName() + return self.name +end + +--- Get number of required assets. +-- @param #AUFTRAG self +-- @return #number Numer of required assets. +function AUFTRAG:GetNumberOfRequiredAssets() + return self.nassets +end + +--- Get mission priority. +-- @param #AUFTRAG self +-- @return #number Priority. Smaller is higher. +function AUFTRAG:GetPriority() + return self.prio +end + +--- Check if mission is "urgent". +-- @param #AUFTRAG self +-- @return #boolean If `true`, mission is "urgent". +function AUFTRAG:IsUrgent() + return self.urgent +end + +--- Get mission importance. +-- @param #AUFTRAG self +-- @return #number Importance. Smaller is higher. +function AUFTRAG:GetImportance() + return self.importance +end --- Add start condition. -- @param #AUFTRAG self @@ -1504,13 +1757,9 @@ end --- Assign airwing squadron(s) to the mission. Only these squads will be considered for the job. -- @param #AUFTRAG self --- @param #table Squadrons A table of SQUADRONs or a single SQUADRON object. +-- @param #table Squadrons A table of SQUADRON(s). **Has to be a table {}** even if a single squad is given. -- @return #AUFTRAG self function AUFTRAG:AssignSquadrons(Squadrons) - - if Squadrons:IsInstanceOf("SQUADRON") then - Squadrons={Squadrons} - end for _,_squad in pairs(Squadrons) do local squadron=_squad --Ops.Squadron#SQUADRON @@ -1520,12 +1769,24 @@ function AUFTRAG:AssignSquadrons(Squadrons) self.squadrons=Squadrons end +--- Add a required payload for this mission. Only these payloads will be used for this mission. If they are not available, the mission cannot start. Only available for use with an AIRWING. +-- @param #AUFTRAG self +-- @param Ops.AirWing#AIRWING.Payload Payload Required payload. +-- @return #AUFTRAG self +function AUFTRAG:AddRequiredPayload(Payload) + + self.payloads=self.payloads or {} + + table.insert(self.payloads, Payload) + +end + --- Add a Ops group to the mission. -- @param #AUFTRAG self -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPSGROUP object. function AUFTRAG:AddOpsGroup(OpsGroup) - self:I(self.lid..string.format("Adding Ops group %s", OpsGroup.groupname)) + self:T(self.lid..string.format("Adding Ops group %s", OpsGroup.groupname)) local groupdata={} --#AUFTRAG.GroupData groupdata.opsgroup=OpsGroup @@ -1542,7 +1803,7 @@ end -- @param #AUFTRAG self -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPSGROUP object. function AUFTRAG:DelOpsGroup(OpsGroup) - self:I(self.lid..string.format("Removing OPS group %s", OpsGroup and OpsGroup.groupname or "nil (ERROR)!")) + self:T(self.lid..string.format("Removing OPS group %s", OpsGroup and OpsGroup.groupname or "nil (ERROR)!")) if OpsGroup then @@ -1758,8 +2019,12 @@ end -- @param #string To To state. function AUFTRAG:onafterStatus(From, Event, To) + -- Current abs. mission time. + local Tnow=timer.getAbsTime() + -- Number of alive mission targets. local Ntargets=self:CountMissionTargets() + local Ntargets0=self:GetTargetInitialNumber() -- Number of alive groups attached to this mission. local Ngroups=self:CountOpsGroups() @@ -1772,7 +2037,7 @@ function AUFTRAG:onafterStatus(From, Event, To) -- All groups have reported MISSON DONE. self:Done() - elseif (self.Tstop and timer.getAbsTime()>self.Tstop+10) or (self.Ntargets>0 and Ntargets==0) then + elseif (self.Tstop and Tnow>self.Tstop+10) or (Ntargets0>0 and Ntargets==0) then -- Cancel mission if stop time passed. self:Cancel() @@ -1780,36 +2045,43 @@ function AUFTRAG:onafterStatus(From, Event, To) end end - - + -- Current FSM state. local fsmstate=self:GetState() - local Tnow=timer.getAbsTime() - -- Mission start stop time. - local Cstart=UTILS.SecondsToClock(self.Tstart, true) - local Cstop=self.Tstop and UTILS.SecondsToClock(self.Tstop, true) or "INF" - - local targetname=self:GetTargetName() or "unknown" - - local airwing=self.airwing and self.airwing.alias or "N/A" - local commander=self.wingcommander and tostring(self.wingcommander.coalition) or "N/A" - - -- Info message. - self:I(self.lid..string.format("Status %s: Target=%s, T=%s-%s, assets=%d, groups=%d, targets=%d, wing=%s, commander=%s", self.status, targetname, Cstart, Cstop, #self.assets, Ngroups, Ntargets, airwing, commander)) - -- Check for error. if fsmstate~=self.status then self:E(self.lid..string.format("ERROR: FSM state %s != %s mission status!", fsmstate, self.status)) end - - local text="Group data:" - for groupname,_groupdata in pairs(self.groupdata) do - local groupdata=_groupdata --#AUFTRAG.GroupData - text=text..string.format("\n- %s: status mission=%s opsgroup=%s", groupname, groupdata.status, groupdata.opsgroup and groupdata.opsgroup:GetState() or "N/A") + + -- General info. + if self.verbose>=1 then + + -- Mission start stop time. + local Cstart=UTILS.SecondsToClock(self.Tstart, true) + local Cstop=self.Tstop and UTILS.SecondsToClock(self.Tstop, true) or "INF" + + local targetname=self:GetTargetName() or "unknown" + + local airwing=self.airwing and self.airwing.alias or "N/A" + local commander=self.wingcommander and tostring(self.wingcommander.coalition) or "N/A" + + -- Info message. + self:I(self.lid..string.format("Status %s: Target=%s, T=%s-%s, assets=%d, groups=%d, targets=%d, wing=%s, commander=%s", self.status, targetname, Cstart, Cstop, #self.assets, Ngroups, Ntargets, airwing, commander)) end - self:I(self.lid..text) + -- Group info. + if self.verbose>=2 then + -- Data on assigned groups. + local text="Group data:" + for groupname,_groupdata in pairs(self.groupdata) do + local groupdata=_groupdata --#AUFTRAG.GroupData + text=text..string.format("\n- %s: status mission=%s opsgroup=%s", groupname, groupdata.status, groupdata.opsgroup and groupdata.opsgroup:GetState() or "N/A") + end + self:I(self.lid..text) + end + + -- Ready to evaluate mission outcome? local ready2evaluate=self.Tover and Tnow-self.Tover>=self.dTevaluate or false -- Check if mission is OVER (done or cancelled) and enough time passed to evaluate the result. @@ -1821,7 +2093,10 @@ function AUFTRAG:onafterStatus(From, Event, To) end -- Update F10 marker. - self:UpdateMarker() + if self.markerOn then + self:UpdateMarker() + end + end --- Evaluate mission outcome - success or failure. @@ -1832,48 +2107,92 @@ function AUFTRAG:Evaluate() -- Assume success and check if any failed condition applies. local failed=false + -- Target damage in %. + local targetdamage=self:GetTargetDamage() + + -- Own damage in %. + local owndamage=self.Ncasualties/self.Nelements*100 + + -- Current number of mission targets. + local Ntargets=self:CountMissionTargets() + local Ntargets0=self:GetTargetInitialNumber() + + local Life=self:GetTargetLife() + local Life0=self:GetTargetInitialLife() + + + if Ntargets0>0 then + + --- + -- Mission had targets + --- + + -- Check if failed. + if self.type==AUFTRAG.Type.TROOPTRANSPORT or self.type==AUFTRAG.Type.ESCORT then + + -- Transported or escorted groups have to survive. + if Ntargets0 then + failed=true + end + + end + + else + + --- + -- Mission had NO targets + --- + + -- No targets and everybody died ==> mission failed. Well, unless success condition is true. + if self.Nelements==self.Ncasualties then + failed=true + end + + end + + -- Any success condition true? local successCondition=self:EvalConditionsAny(self.conditionSuccess) -- Any failure condition true? local failureCondition=self:EvalConditionsAny(self.conditionFailure) - - -- Target damage in %. - local targetdamage=self:GetTargetDamage() - - -- Current number of mission targets. - local Ntargets=self:CountMissionTargets() - - -- Number of current targets is still >0 ==> Not everything was destroyed. - if self.type==AUFTRAG.Type.TROOPTRANSPORT then - - if self.Ntargets>0 and Ntargets0 and Ntargets>0 then - failed=true - end - - end - - --TODO: all assets dead? Is this a FAILED criterion even if all targets have been destroyed? What if there are no initial targets (e.g. when ORBIT, PATROL, RECON missions). if failureCondition then failed=true elseif successCondition then failed=false - end + end -- Debug text. local text=string.format("Evaluating mission:\n") - text=text..string.format("Targets = %d/%d\n", self.Ntargets, Ntargets) - text=text..string.format("Damage = %.1f %%\n", targetdamage) - text=text..string.format("Success Cond = %s\n", tostring(successCondition)) - text=text..string.format("Failure Cond = %s\n", tostring(failureCondition)) - text=text..string.format("Failed = %s", tostring(failed)) + text=text..string.format("Own casualties = %d/%d\n", self.Ncasualties, self.Nelements) + text=text..string.format("Own losses = %.1f %%\n", owndamage) + text=text..string.format("--------------------------\n") + text=text..string.format("Targets left = %d/%d\n", Ntargets, Ntargets0) + text=text..string.format("Targets life = %.1f/%.1f\n", Life, Life0) + text=text..string.format("Enemy losses = %.1f %%\n", targetdamage) + text=text..string.format("--------------------------\n") + --text=text..string.format("Loss ratio = %.1f %%\n", targetdamage) + --text=text..string.format("--------------------------\n") + text=text..string.format("Success Cond = %s\n", tostring(successCondition)) + text=text..string.format("Failure Cond = %s\n", tostring(failureCondition)) + text=text..string.format("--------------------------\n") + text=text..string.format("Final Success = %s\n", tostring(not failed)) + text=text..string.format("=========================") self:I(self.lid..text) if failed then @@ -1997,7 +2316,7 @@ end -- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group. -- @param Ops.OpsGroup#OPSGROUP.Task task Waypoint task. function AUFTRAG:SetGroupWaypointTask(opsgroup, task) - self:I(self.lid..string.format("Setting waypoint task %s", task and task.description or "WTF")) + self:T2(self.lid..string.format("Setting waypoint task %s", task and task.description or "WTF")) local groupdata=self:GetGroupData(opsgroup) if groupdata then groupdata.waypointtask=task @@ -2020,7 +2339,7 @@ end -- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group. -- @param #number waypointindex Waypoint index. function AUFTRAG:SetGroupWaypointIndex(opsgroup, waypointindex) - self:I(self.lid..string.format("Setting waypoint index %d", waypointindex)) + self:T2(self.lid..string.format("Setting waypoint index %d", waypointindex)) local groupdata=self:GetGroupData(opsgroup) if groupdata then groupdata.waypointindex=waypointindex @@ -2101,6 +2420,17 @@ end -- FSM Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "Planned" event. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AUFTRAG:onafterPlanned(From, Event, To) + self.status=AUFTRAG.Status.PLANNED + self:T(self.lid..string.format("New mission status=%s", self.status)) +end + --- On after "Queue" event. Mission is added to the mission queue of an AIRWING. -- @param #AUFTRAG self -- @param #string From From state. @@ -2110,7 +2440,7 @@ end function AUFTRAG:onafterQueued(From, Event, To, Airwing) self.status=AUFTRAG.Status.QUEUED self.airwing=Airwing - self:I(self.lid..string.format("New mission status=%s at airwing %s", self.status, tostring(Airwing.alias))) + self:T(self.lid..string.format("New mission status=%s at airwing %s", self.status, tostring(Airwing.alias))) end @@ -2121,7 +2451,7 @@ end -- @param #string To To state. function AUFTRAG:onafterRequested(From, Event, To) self.status=AUFTRAG.Status.REQUESTED - self:I(self.lid..string.format("New mission status=%s", self.status)) + self:T(self.lid..string.format("New mission status=%s", self.status)) end --- On after "Assign" event. @@ -2131,7 +2461,7 @@ end -- @param #string To To state. function AUFTRAG:onafterAssign(From, Event, To) self.status=AUFTRAG.Status.ASSIGNED - self:I(self.lid..string.format("New mission status=%s", self.status)) + self:T(self.lid..string.format("New mission status=%s", self.status)) end --- On after "Schedule" event. Mission is added to the mission queue of a FLIGHTGROUP. @@ -2139,10 +2469,9 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param Ops.OpsGroup#OPSGROUP FlightGroup -function AUFTRAG:onafterScheduled(From, Event, To, FlightGroup) +function AUFTRAG:onafterScheduled(From, Event, To) self.status=AUFTRAG.Status.SCHEDULED - self:I(self.lid..string.format("New mission status=%s", self.status)) + self:T(self.lid..string.format("New mission status=%s", self.status)) end --- On after "Start" event. @@ -2152,7 +2481,7 @@ end -- @param #string To To state. function AUFTRAG:onafterStarted(From, Event, To) self.status=AUFTRAG.Status.STARTED - self:I(self.lid..string.format("New mission status=%s", self.status)) + self:T(self.lid..string.format("New mission status=%s", self.status)) end --- On after "Execute" event. @@ -2162,7 +2491,7 @@ end -- @param #string To To state. function AUFTRAG:onafterExecuting(From, Event, To) self.status=AUFTRAG.Status.EXECUTING - self:I(self.lid..string.format("New mission status=%s", self.status)) + self:T(self.lid..string.format("New mission status=%s", self.status)) end --- On after "Done" event. @@ -2172,13 +2501,23 @@ end -- @param #string To To state. function AUFTRAG:onafterDone(From, Event, To) self.status=AUFTRAG.Status.DONE - self:I(self.lid..string.format("New mission status=%s", self.status)) + self:T(self.lid..string.format("New mission status=%s", self.status)) -- Set time stamp. self.Tover=timer.getAbsTime() end +--- On after "ElementDestroyed" event. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP OpsGroup The ops group that is dead now. +function AUFTRAG:onafterElementDestroyed(From, Event, To, OpsGroup, Element) + -- Increase number of own casualties. + self.Ncasualties=self.Ncasualties+1 +end --- On after "GroupDead" event. -- @param #AUFTRAG self @@ -2202,10 +2541,8 @@ end -- @param #string To To state. -- @param Ops.AirWing#AIRWING.SquadronAsset Asset The asset. function AUFTRAG:onafterAssetDead(From, Event, To, Asset) - - -- Remove opsgroup from mission. - --self:DelOpsGroup(Asset.opsgroup) - + + -- Number of groups alive. local N=self:CountOpsGroups() -- All assets dead? @@ -2218,37 +2555,17 @@ function AUFTRAG:onafterAssetDead(From, Event, To, Asset) else - self:E(self.lid.."ERROR: All assets are dead not but mission was already over... Investigate!") + --self:E(self.lid.."ERROR: All assets are dead not but mission was already over... Investigate!") -- Now this can happen, because when a opsgroup dies (sometimes!), the mission is DONE end end - - -- Remove asset from airwing. - if self.airwing then - self.airwing:RemoveAssetFromSquadron(Asset) - end -- Delete asset from mission. self:DelAsset(Asset) end ---- On after "Success" event. --- @param #AUFTRAG self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. -function AUFTRAG:onafterSuccess(From, Event, To) - - self.status=AUFTRAG.Status.SUCCESS - self:I(self.lid..string.format("New mission status=%s", self.status)) - - -- Stop mission. - self:Stop() - -end - --- On after "Cancel" event. Cancells the mission. -- @param #AUFTRAG self -- @param #string From From state. @@ -2263,27 +2580,29 @@ function AUFTRAG:onafterCancel(From, Event, To) self.Tover=timer.getAbsTime() -- No more repeats. - self.missionRepeatMax=self.missionRepeated + self.Nrepeat=self.repeated + self.NrepeatFailure=self.repeatedFailure + self.NrepeatSuccess=self.repeatedSuccess -- Not necessary to delay the evaluaton?! self.dTevaluate=0 if self.wingcommander then - self:I(self.lid..string.format("Wingcommander will cancel the mission. Will wait for mission DONE before evaluation!")) + self:T(self.lid..string.format("Wingcommander will cancel the mission. Will wait for mission DONE before evaluation!")) self.wingcommander:CancelMission(self) elseif self.airwing then - self:I(self.lid..string.format("Airwing %s will cancel the mission. Will wait for mission DONE before evaluation!", self.airwing.alias)) + self:T(self.lid..string.format("Airwing %s will cancel the mission. Will wait for mission DONE before evaluation!", self.airwing.alias)) -- Airwing will cancel all flight missions and remove queued request from warehouse queue. self.airwing:MissionCancel(self) else - self:I(self.lid..string.format("No airwing or wingcommander. Attached flights will cancel the mission on their own. Will wait for mission DONE before evaluation!")) + self:T(self.lid..string.format("No airwing or wingcommander. Attached flights will cancel the mission on their own. Will wait for mission DONE before evaluation!")) for _,_groupdata in pairs(self.groupdata) do local groupdata=_groupdata --#AUFTRAG.GroupData @@ -2294,12 +2613,46 @@ function AUFTRAG:onafterCancel(From, Event, To) -- Special mission states. if self.status==AUFTRAG.Status.PLANNED then - self:I(self.lid..string.format("Cancelled mission was in planned stage. Call it done!")) + self:T(self.lid..string.format("Cancelled mission was in planned stage. Call it done!")) self:Done() end end +--- On after "Success" event. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AUFTRAG:onafterSuccess(From, Event, To) + + self.status=AUFTRAG.Status.SUCCESS + self:T(self.lid..string.format("New mission status=%s", self.status)) + + local repeatme=self.repeatedSuccess Repeat mission!", self.repeated+1, N)) + self:Repeat() + + else + + -- Stop mission. + self:I(self.lid..string.format("Mission SUCCESS! Number of max repeats %d reached ==> Stopping mission!", self.repeated+1)) + self:Stop() + + end + +end + --- On after "Failed" event. -- @param #AUFTRAG self -- @param #string From From state. @@ -2308,19 +2661,28 @@ end function AUFTRAG:onafterFailed(From, Event, To) self.status=AUFTRAG.Status.FAILED - self:I(self.lid..string.format("New mission status=%s", self.status)) + self:T(self.lid..string.format("New mission status=%s", self.status)) - if self.missionRepeated>=self.missionRepeatMax then + local repeatme=self.repeatedFailure=%d] ==> Stopping mission!", self.missionRepeated, self.missionRepeatMax)) - self:Stop() + if repeatme then + + -- Increase counter. + self.repeatedFailure=self.repeatedFailure+1 - else + -- Number of repeats. + local N=math.max(self.NrepeatFailure, self.Nrepeat) -- Repeat mission. - self:I(self.lid..string.format("Mission failed! Repeating mission for the %d time (max %d times) ==> Repeat mission!", self.missionRepeated+1, self.missionRepeatMax)) + self:I(self.lid..string.format("Mission FAILED! Repeating mission for the %d time (max %d times) ==> Repeat mission!", self.repeated+1, N)) self:Repeat() + else + + -- Stop mission. + self:I(self.lid..string.format("Mission FAILED! Number of max repeats %d reached ==> Stopping mission!", self.repeated+1)) + self:Stop() + end end @@ -2336,20 +2698,30 @@ function AUFTRAG:onafterRepeat(From, Event, To) -- Set mission status to PLANNED. self.status=AUFTRAG.Status.PLANNED - self:I(self.lid..string.format("New mission status=%s (on Repeat)", self.status)) + self:T(self.lid..string.format("New mission status=%s (on Repeat)", self.status)) -- Increase repeat counter. - self.missionRepeated=self.missionRepeated+1 + self.repeated=self.repeated+1 - if self.wingcommander then + if self.chief then + + --TODO + elseif self.wingcommander then + + -- Remove mission from airwing because WC will assign it again but maybe to a different wing. + if self.airwing then + self.airwing:RemoveMission(self) + end + elseif self.airwing then -- Already at the airwing ==> Queued() - self:Queued(self.airwing) + self:Queued(self.airwing) else - + self:E(self.lid.."ERROR: Mission can only be repeated by a CHIEF, WINGCOMMANDER or AIRWING! Stopping AUFTRAG") + self:Stop() end @@ -2367,6 +2739,10 @@ function AUFTRAG:onafterRepeat(From, Event, To) -- No flight data. self.groupdata={} + -- Reset casualties and units assigned. + self.Ncasualties=0 + self.Nelements=0 + -- Call status again. self:__Status(-30) @@ -2409,156 +2785,75 @@ function AUFTRAG:onafterStop(From, Event, To) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Misc Functions +-- Target Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Add asset to mission. +--- Create target data from a given object. -- @param #AUFTRAG self --- @param Ops.AirWing#AIRWING.SquadronAsset Asset The asset to be added to the mission. --- @return #AUFTRAG self -function AUFTRAG:AddAsset(Asset) +-- @param Wrapper.Positionable#POSITIONABLE Object The target GROUP, UNIT, STATIC. +function AUFTRAG:_TargetFromObject(Object) - self.assets=self.assets or {} + if not self.engageTarget then - table.insert(self.assets, Asset) - - return self -end - ---- Delete asset from mission. --- @param #AUFTRAG self --- @param Ops.AirWing#AIRWING.SquadronAsset Asset The asset to be removed. --- @return #AUFTRAG self -function AUFTRAG:DelAsset(Asset) - - for i,_asset in pairs(self.assets or {}) do - local asset=_asset --Ops.AirWing#AIRWING.SquadronAsset + if Object:IsInstanceOf("TARGET") then - if asset.uid==Asset.uid then - self:I(self.lid..string.format("Removing asset \"%s\" from mission", tostring(asset.spawngroupname))) - table.remove(self.assets, i) - return self + self.engageTarget=Object + + else + + self.engageTarget=TARGET:New(Object) + end - + + else + + -- Target was already specified elsewhere. + end + -- Debug info. + self:T(self.lid..string.format("Mission Target %s Type=%s, Ntargets=%d, Lifepoints=%d", self.engageTarget.lid, self.engageTarget.lid, self.engageTarget.Ntargets0, self.engageTarget:GetLife())) + return self end ---- Get asset by its spawn group name. --- @param #AUFTRAG self --- @param #string Name Asset spawn group name. --- @return Ops.AirWing#AIRWING.SquadronAsset -function AUFTRAG:GetAssetByName(Name) - - for i,_asset in pairs(self.assets or {}) do - local asset=_asset --Ops.AirWing#AIRWING.SquadronAsset - - if asset.spawngroupname==Name then - return asset - end - - end - - return nil -end - --- Count alive mission targets. -- @param #AUFTRAG self --- @param #AUFTRAG.TargetData Target (Optional) The target object. -- @return #number Number of alive target units. -function AUFTRAG:CountMissionTargets(Target) - - local N=0 - - Target=Target or self:GetTargetData() - - if Target then - - if Target.Type==AUFTRAG.TargetType.GROUP then - - local target=Target.Target --Wrapper.Group#GROUP - - local units=target:GetUnits() - - for _,_unit in pairs(units or {}) do - local unit=_unit --Wrapper.Unit#UNIT - - -- We check that unit is "alive" and has health >1. Somtimes units get heavily damanged but are still alive. - -- TODO: here I could introduce and count that if units have only health < 50% if mission objective is to just "damage" the units. - if unit and unit:IsAlive() and unit:GetLife()>1 then - N=N+1 - end - end - - elseif Target.Type==AUFTRAG.TargetType.UNIT then - - local target=Target.Target --Wrapper.Unit#UNIT - - if target and target:IsAlive() and target:GetLife()>1 then - N=N+1 - end - - elseif Target.Type==AUFTRAG.TargetType.STATIC then - - local target=Target.Target --Wrapper.Static#STATIC - - if target and target:IsAlive() then - N=N+1 - end - - elseif Target.Type==AUFTRAG.TargetType.AIRBASE then - - -- TODO: any (good) way to tell whether an airbase was "destroyed" or at least damaged? Is :GetLive() working? - - elseif Target.Type==AUFTRAG.TargetType.COORDINATE then - - -- No target! +function AUFTRAG:CountMissionTargets() - elseif Target.Type==AUFTRAG.TargetType.SETGROUP then - - for _,_group in pairs(Target.Target.Set or {}) do - local group=_group --Wrapper.Group#GROUP - - local units=group:GetUnits() - - for _,_unit in pairs(units or {}) do - local unit=_unit --Wrapper.Unit#UNIT - - -- We check that unit is "alive". - if unit and unit:IsAlive() and unit:GetLife()>1 then - N=N+1 - end - end - - end - - elseif Target.Type==AUFTRAG.TargetType.SETUNIT then - - for _,_unit in pairs(Target.Target.Set or {}) do - local unit=_unit --Wrapper.Unit#UNIT - - -- We check that unit is "alive". - if unit and unit:IsAlive() and unit:GetLife()>1 then - N=N+1 - end - - end - - else - self:E("ERROR unknown target type") - end + if self.engageTarget then + return self.engageTarget:CountTargets() + else + return 0 end - return N end +--- Get initial number of targets. +-- @param #AUFTRAG self +-- @return #number Number of initial life points when mission was planned. +function AUFTRAG:GetTargetInitialNumber() + local target=self:GetTargetData() + if target then + return target.Ntargets0 + else + return 0 + end +end + + --- Get target life points. -- @param #AUFTRAG self -- @return #number Number of initial life points when mission was planned. function AUFTRAG:GetTargetInitialLife() - return self:GetTargetData().Lifepoints + local target=self:GetTargetData() + if target then + return target.life0 + else + return 0 + end end --- Get target damage. @@ -2566,9 +2861,11 @@ end -- @return #number Damage in percent. function AUFTRAG:GetTargetDamage() local target=self:GetTargetData() - local life=self:GetTargetLife()/self:GetTargetInitialLife() - local damage=1-life - return damage*100 + if target then + return target:GetDamage() + else + return 0 + end end @@ -2576,133 +2873,17 @@ end -- @param #AUFTRAG self -- @return #number Life points of target. function AUFTRAG:GetTargetLife() - return self:_GetTargetLife(nil, false) + local target=self:GetTargetData() + if target then + return target:GetLife() + else + return 0 + end end ---- Get target life points. +--- Get target. -- @param #AUFTRAG self --- @param #AUFTRAG.TargetData Target (Optional) The target object. --- @param #boolean Healthy Get the life points of the healthy target. --- @return #number Life points of target. -function AUFTRAG:_GetTargetLife(Target, Healthy) - - local N=0 - - Target=Target or self:GetTargetData() - - local function _GetLife(unit) - local unit=unit --Wrapper.Unit#UNIT - if Healthy then - local life=unit:GetLife() - local life0=unit:GetLife0() - - return math.max(life, life0) - else - return unit:GetLife() - end - end - - if Target then - - if Target.Type==AUFTRAG.TargetType.GROUP then - - local target=Target.Target --Wrapper.Group#GROUP - - local units=target:GetUnits() - - for _,_unit in pairs(units or {}) do - local unit=_unit --Wrapper.Unit#UNIT - - -- We check that unit is "alive". - if unit and unit:IsAlive() then - N=N+_GetLife(unit) - end - end - - elseif Target.Type==AUFTRAG.TargetType.UNIT then - - local target=Target.Target --Wrapper.Unit#UNIT - - if target and target:IsAlive() then - N=N+_GetLife(target) - end - - elseif Target.Type==AUFTRAG.TargetType.STATIC then - - local target=Target.Target --Wrapper.Static#STATIC - - -- Statics are alive or not. - if target and target:IsAlive() then - N=N+1 --_GetLife(target) - else - N=N+0 - end - - elseif Target.Type==AUFTRAG.TargetType.AIRBASE then - - -- TODO: any (good) way to tell whether an airbase was "destroyed" or at least damaged? Is :GetLive() working? - N=N+1 - - elseif Target.Type==AUFTRAG.TargetType.COORDINATE then - - -- A coordinate does not live. - N=N+1 - - elseif Target.Type==AUFTRAG.TargetType.SETGROUP then - - for _,_group in pairs(Target.Target.Set or {}) do - local group=_group --Wrapper.Group#GROUP - - local units=group:GetUnits() - - for _,_unit in pairs(units or {}) do - local unit=_unit --Wrapper.Unit#UNIT - - -- We check that unit is "alive". - if unit and unit:IsAlive() then - N=N+_GetLife(unit) - end - end - - end - - elseif Target.Type==AUFTRAG.TargetType.SETUNIT then - - for _,_unit in pairs(Target.Target.Set or {}) do - local unit=_unit --Wrapper.Unit#UNIT - - -- We check that unit is "alive". - if unit and unit:IsAlive() then - N=N+_GetLife(unit) - end - - end - - else - self:E(self.lid.."ERROR unknown target type") - end - end - - return N -end - ---- Count alive flight groups assigned for this mission. --- @param #AUFTRAG self --- @return #number Number of alive flight groups. -function AUFTRAG:CountOpsGroups() - local N=0 - for _,_groupdata in pairs(self.groupdata) do - local groupdata=_groupdata --#AUFTRAG.GroupData - if groupdata and groupdata.opsgroup and groupdata.opsgroup:IsAlive() then - N=N+1 - end - end - return N -end - ---- Get coordinate of target. --- @param #AUFTRAG self --- @return #AUFTRAG.TargetData The target object. Could be many things. +-- @return Ops.Target#TARGET The target object. Could be many things. function AUFTRAG:GetTargetData() return self.engageTarget end @@ -2742,33 +2923,12 @@ function AUFTRAG:GetTargetCoordinate() -- Special case where we defined a return self.transportPickup + elseif self.engageTarget then + + return self.engageTarget:GetCoordinate() + else - - local target - - if self:GetTargetType()==AUFTRAG.TargetType.COORDINATE then - - -- Here the objective itself is a COORDINATE. - return self:GetObjective() - - elseif self:GetTargetType()==AUFTRAG.TargetType.SETGROUP then - - -- Return the first group in the set. - -- TODO: does this only return ALIVE groups?! - return self:GetObjective():GetFirst():GetCoordinate() - - elseif self:GetTargetType()==AUFTRAG.TargetType.SETUNIT then - - -- Return the first unit in the set. - -- TODO: does this only return ALIVE units?! - return self:GetObjective():GetFirst():GetCoordinate() - - else - - -- In all other cases the GetCoordinate() function should work. - return self:GetObjective():GetCoordinate() - - end + self:E(self.lid.."ERROR: Cannot get target coordinate!") end return nil @@ -2779,8 +2939,8 @@ end -- @return #string Name of the target or "N/A". function AUFTRAG:GetTargetName() - if self.engageTarget.Target then - return self.engageTarget.Name + if self.engageTarget then + return self.engageTarget:GetName() end return "N/A" @@ -2804,9 +2964,81 @@ function AUFTRAG:GetTargetDistance(FromCoord) return 0 end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add asset to mission. +-- @param #AUFTRAG self +-- @param Ops.AirWing#AIRWING.SquadronAsset Asset The asset to be added to the mission. +-- @return #AUFTRAG self +function AUFTRAG:AddAsset(Asset) + + self.assets=self.assets or {} + + table.insert(self.assets, Asset) + + return self +end + +--- Delete asset from mission. +-- @param #AUFTRAG self +-- @param Ops.AirWing#AIRWING.SquadronAsset Asset The asset to be removed. +-- @return #AUFTRAG self +function AUFTRAG:DelAsset(Asset) + + for i,_asset in pairs(self.assets or {}) do + local asset=_asset --Ops.AirWing#AIRWING.SquadronAsset + + if asset.uid==Asset.uid then + self:T(self.lid..string.format("Removing asset \"%s\" from mission", tostring(asset.spawngroupname))) + table.remove(self.assets, i) + return self + end + + end + + return self +end + +--- Get asset by its spawn group name. +-- @param #AUFTRAG self +-- @param #string Name Asset spawn group name. +-- @return Ops.AirWing#AIRWING.SquadronAsset +function AUFTRAG:GetAssetByName(Name) + + for i,_asset in pairs(self.assets or {}) do + local asset=_asset --Ops.AirWing#AIRWING.SquadronAsset + + if asset.spawngroupname==Name then + return asset + end + + end + + return nil +end + +--- Count alive ops groups assigned for this mission. +-- @param #AUFTRAG self +-- @return #number Number of alive flight groups. +function AUFTRAG:CountOpsGroups() + local N=0 + for _,_groupdata in pairs(self.groupdata) do + local groupdata=_groupdata --#AUFTRAG.GroupData + if groupdata and groupdata.opsgroup and groupdata.opsgroup:IsAlive() then + N=N+1 + end + end + return N +end + + --- Get coordinate of target. First unit/group of the set is used. -- @param #AUFTRAG self --- @return #string +-- @param #table MissionTypes A table of mission types. +-- @return #string Comma separated list of mission types. function AUFTRAG:GetMissionTypesText(MissionTypes) local text="" @@ -2817,12 +3049,29 @@ function AUFTRAG:GetMissionTypesText(MissionTypes) return text end +--- Set the mission waypoint coordinate where the mission is executed. +-- @param #AUFTRAG self +-- @return Core.Point#COORDINATE Coordinate where the mission is executed. +-- @return #AUFTRAG self +function AUFTRAG:SetMissionWaypointCoord(Coordinate) + self.missionWaypointCoord=Coordinate +end + --- Get coordinate of target. First unit/group of the set is used. -- @param #AUFTRAG self -- @param Wrapper.Group#GROUP group Group. -- @return Core.Point#COORDINATE Coordinate where the mission is executed. function AUFTRAG:GetMissionWaypointCoord(group) + -- Check if a coord has been explicitly set. + if self.missionWaypointCoord then + local coord=self.missionWaypointCoord + if self.missionAltitude then + coord.y=self.missionAltitude + end + return coord + end + -- Create waypoint coordinate half way between us and the target. local waypointcoord=group:GetCoordinate():GetIntermediateCoordinate(self:GetTargetCoordinate(), self.missionFraction) local alt=waypointcoord.y @@ -2855,7 +3104,7 @@ function AUFTRAG:UpdateMarker() -- Marker text. local text=string.format("%s %s: %s", self.name, self.type:upper(), self.status:upper()) text=text..string.format("\n%s", self:GetTargetName()) - text=text..string.format("\nTargets %d/%d, Life Points=%d/%d", self:CountMissionTargets(), self.Ntargets, self:GetTargetLife(), self:GetTargetInitialLife()) + text=text..string.format("\nTargets %d/%d, Life Points=%d/%d", self:CountMissionTargets(), self:GetTargetInitialNumber(), self:GetTargetLife(), self:GetTargetInitialLife()) text=text..string.format("\nFlights %d/%d", self:CountOpsGroups(), self.nassets) if not self.marker then @@ -2863,7 +3112,11 @@ function AUFTRAG:UpdateMarker() -- Get target coordinates. Can be nil! local targetcoord=self:GetTargetCoordinate() - self.marker=MARKER:New(targetcoord, text):ReadOnly():ToAll() + if self.markerCoaliton and self.markerCoaliton>=0 then + self.marker=MARKER:New(targetcoord, text):ReadOnly():ToCoalition(self.markerCoaliton) + else + self.marker=MARKER:New(targetcoord, text):ReadOnly():ToAll() + end else @@ -2927,7 +3180,7 @@ function AUFTRAG:GetDCSMissionTask(TaskControllable) -- BOMBRUNWAY Mission -- ------------------------ - local DCStask=CONTROLLABLE.TaskBombingRunway(nil, self.engageTarget.Target, self.engageWeaponType, self.engageWeaponExpend, self.engageQuantity, self.engageDirection, self.engageAsGroup) + local DCStask=CONTROLLABLE.TaskBombingRunway(nil, self.engageTarget:GetObject(), self.engageWeaponType, self.engageWeaponExpend, self.engageQuantity, self.engageDirection, self.engageAsGroup) table.insert(DCStasks, DCStask) @@ -2967,7 +3220,7 @@ function AUFTRAG:GetDCSMissionTask(TaskControllable) -- ESCORT Mission -- -------------------- - local DCStask=CONTROLLABLE.TaskEscort(nil, self.engageTarget.Target, self.escortVec3, LastWaypointIndex, self.engageMaxDistance, self.engageTargetTypes) + local DCStask=CONTROLLABLE.TaskEscort(nil, self.engageTarget:GetObject(), self.escortVec3, LastWaypointIndex, self.engageMaxDistance, self.engageTargetTypes) table.insert(DCStasks, DCStask) @@ -2977,7 +3230,7 @@ function AUFTRAG:GetDCSMissionTask(TaskControllable) -- FAC Mission -- ----------------- - local DCStask=CONTROLLABLE.TaskFAC_AttackGroup(nil, self.engageTarget.Target, self.engageWeaponType, self.facDesignation, self.facDatalink, self.facFreq, self.facModu, CallsignName, CallsignNumber) + local DCStask=CONTROLLABLE.TaskFAC_AttackGroup(nil, self.engageTarget:GetObject(), self.engageWeaponType, self.facDesignation, self.facDatalink, self.facFreq, self.facModu, CallsignName, CallsignNumber) table.insert(DCStasks, DCStask) @@ -3005,10 +3258,10 @@ function AUFTRAG:GetDCSMissionTask(TaskControllable) -- Done below as also other mission types use the orbit task. - elseif self.type==AUFTRAG.Type.PATROL then + elseif self.type==AUFTRAG.Type.GCICAP then -------------------- - -- PATROL Mission -- + -- GCICAP Mission -- -------------------- -- Done below as also other mission types use the orbit task. @@ -3082,7 +3335,7 @@ function AUFTRAG:GetDCSMissionTask(TaskControllable) -- We create a "fake" DCS task and pass the parameters to the FLIGHTGROUP. local param={} - param.unitname=self:GetTargetName() + param.unitname=self:GetTargetName() --self.carrier:GetName() param.offsetX=200 param.offsetZ=240 param.altitude=70 @@ -3112,7 +3365,7 @@ function AUFTRAG:GetDCSMissionTask(TaskControllable) if self.type==AUFTRAG.Type.ORBIT or self.type==AUFTRAG.Type.CAP or self.type==AUFTRAG.Type.CAS or - self.type==AUFTRAG.Type.PATROL or + self.type==AUFTRAG.Type.GCICAP or self.type==AUFTRAG.Type.AWACS or self.type==AUFTRAG.Type.TANKER then @@ -3142,136 +3395,35 @@ end --- Get DCS task table for an attack group or unit task. -- @param #AUFTRAG self --- @param #AUFTRAG.TargetData target Target data. +-- @param Ops.Target#TARGET Target Target data. -- @param #table DCStasks DCS DCS tasks table to which the task is added. -- @return DCS#Task The DCS task table. -function AUFTRAG:_GetDCSAttackTask(target, DCStasks) +function AUFTRAG:_GetDCSAttackTask(Target, DCStasks) - local DCStask=nil + DCStasks=DCStasks or {} + + for _,_target in pairs(Target.targets) do + local target=_target --Ops.Target#TARGET.Object - if target.Type==AUFTRAG.TargetType.GROUP then - - DCStask=CONTROLLABLE.TaskAttackGroup(nil, target.Target, self.engageWeaponType, self.engageWeaponExpend, self.engageQuantity, self.engageDirection, self.engageAltitude, self.engageAsGroup) - - table.insert(DCStasks, DCStask) + if target.Type==TARGET.ObjectType.GROUP then - elseif target.Type==AUFTRAG.TargetType.UNIT or target.Type==AUFTRAG.TargetType.STATIC then - - DCStask=CONTROLLABLE.TaskAttackUnit(nil, target.Target, self.engageAsGroup, self.WeaponExpend, self.engageQuantity, self.engageDirection, self.engageAltitude, self.engageWeaponType) - - table.insert(DCStasks, DCStask) - - elseif target.Type==AUFTRAG.TargetType.SETGROUP then - - -- Add all groups. - for _,group in pairs(target.Target.Set or {}) do - DCStask=CONTROLLABLE.TaskAttackGroup(nil, group, self.engageWeaponType, self.engageWeaponExpend, self.engageQuantity, self.engageDirection, self.engageAltitude, self.engageAsGroup) + local DCStask=CONTROLLABLE.TaskAttackGroup(nil, target.Object, self.engageWeaponType, self.engageWeaponExpend, self.engageQuantity, self.engageDirection, self.engageAltitude, self.engageAsGroup) + table.insert(DCStasks, DCStask) - end - - elseif target.Type==AUFTRAG.TargetType.SETUNIT then - - -- Add tasks to attack all units. - for _,unit in pairs(target.Target.Set or {}) do - DCStask=CONTROLLABLE.TaskAttackUnit(nil, unit, self.engageAsGroup, self.WeaponExpend, self.engageQuantity, self.engageDirection, self.engageAltitude, self.engageWeaponType) + + elseif target.Type==TARGET.ObjectType.UNIT or target.Type==TARGET.ObjectType.STATIC then + + local DCStask=CONTROLLABLE.TaskAttackUnit(nil, target.Object, self.engageAsGroup, self.WeaponExpend, self.engageQuantity, self.engageDirection, self.engageAltitude, self.engageWeaponType) + table.insert(DCStasks, DCStask) + end - + end - + return DCStasks end ---- Create target data from a given object. --- @param #AUFTRAG self --- @param Wrapper.Positionable#POSITIONABLE Object The target GROUP, UNIT, STATIC. --- @return #AUFTRAG.TargetData Target. -function AUFTRAG:_TargetFromObject(Object) - - local target={} --#AUFTRAG.TargetData - - -- The object. - target.Target=Object - - if Object:IsInstanceOf("GROUP") then - - target.Type=AUFTRAG.TargetType.GROUP - - local object=Object --Wrapper.Group#GROUP - - target.Name=object:GetName() - - elseif Object:IsInstanceOf("UNIT") then - - target.Type=AUFTRAG.TargetType.UNIT - - local object=Object --Wrapper.Unit#UNIT - - target.Name=object:GetName() - - elseif Object:IsInstanceOf("STATIC") then - - target.Type=AUFTRAG.TargetType.STATIC - - target.Name=Object:GetName() - - elseif Object:IsInstanceOf("COORDINATE") then - - target.Type=AUFTRAG.TargetType.COORDINATE - - local object=Object --Core.Point#COORDINATE - - target.Name=object:ToStringLLDMS() - - elseif Object:IsInstanceOf("AIRBASE") then - - target.Type=AUFTRAG.TargetType.AIRBASE - - local object=Object --Wrapper.Airbase#AIRBASE - - target.Name=object:GetName() - - elseif Object:IsInstanceOf("SET_GROUP") then - - target.Type=AUFTRAG.TargetType.SETGROUP - - local object=Object --Core.Set#SET_GROUP - - target.Name=object:GetFirst():GetName() - - elseif Object:IsInstanceOf("SET_UNIT") then - - target.Type=AUFTRAG.TargetType.SETUNIT - - local object=Object --Core.Set#SET_UNIT - - target.Name=object:GetFirst():GetName() - - else - self:E(self.lid.."ERROR: Unknown object given as target. Needs to be a GROUP, UNIT, STATIC, COORDINATE") - return nil - end - - - -- Number of initial targets. - local Ninitial=self:CountMissionTargets(target) - - -- Initial total life point. - local Lifepoints=self:_GetTargetLife(target, true) - - -- Set engage Target. - self.engageTarget=target - self.engageTarget.Ninital=Ninitial - self.engageTarget.Lifepoints=Lifepoints - - -- TODO: get rid of this. - self.Ntargets=Ninitial - - -- Debug info. - self:I(self.lid..string.format("Mission Target %s Type=%s, Ntargets=%d, Lifepoints=%d", target.Name, target.Type, Ninitial, Lifepoints)) - - return target -end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua index c4f53b443..81a8f4bf6 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -26,7 +26,7 @@ -- @field #number ceiling Max altitude the aircraft can fly at in meters. -- @field #number tankertype The refueling system type (0=boom, 1=probe), if the group is a tanker. -- @field #number refueltype The refueling system type (0=boom, 1=probe), if the group can refuel from a tanker. --- @field #FLIGHTGROUP.Ammo ammo Ammunition data. Number of Guns, Rockets, Bombs, Missiles. +-- @field Ops.OpsGroup#OPSGROUP.Ammo ammo Ammunition data. Number of Guns, Rockets, Bombs, Missiles. -- @field #boolean ai If true, flight is purely AI. If false, flight contains at least one human player. -- @field #boolean fuellow Fuel low switch. -- @field #number fuellowthresh Low fuel threshold in percent. @@ -34,6 +34,7 @@ -- @field #boolean fuelcritical Fuel critical switch. -- @field #number fuelcriticalthresh Critical fuel threshold in percent. -- @field #boolean fuelcriticalrtb RTB on critical fuel switch. +-- @field Ops.Squadron#SQUADRON squadron The squadron of this flight group. -- @field Ops.AirWing#AIRWING airwing The airwing the flight group belongs to. -- @field Ops.FlightControl#FLIGHTCONTROL flightcontrol The flightcontrol handling this group. -- @field Ops.Airboss#AIRBOSS airboss The airboss handling this group. @@ -45,6 +46,7 @@ -- @field #boolean ishelo If true, the is a helicopter group. -- @field #number callsignName Callsign name. -- @field #number callsignNumber Callsign number. +-- @field #boolean despawnAfterLanding If true, group is despawned after landed at an airbase. -- -- @extends Ops.OpsGroup#OPSGROUP @@ -52,7 +54,7 @@ -- -- === -- --- ![Banner Image](..\Presentations\FlightGroup\FLIGHTGROUP_Main.jpg) +-- ![Banner Image](..\Presentations\OPS\FlightGroup\_Main.png) -- -- # The FLIGHTGROUP Concept -- @@ -115,7 +117,7 @@ FLIGHTGROUP = { homezone = nil, destzone = nil, actype = nil, - speedmax = nil, + speedMax = nil, rangemax = nil, ceiling = nil, fuellow = false, @@ -226,8 +228,11 @@ function FLIGHTGROUP:New(group) self.lid=string.format("FLIGHTGROUP %s | ", self.groupname) -- Defaults + --self:SetVerbosity(0) self:SetFuelLowThreshold() + self:SetFuelLowRTB() self:SetFuelCriticalThreshold() + self:SetFuelCriticalRTB() self:SetDefaultROE() self:SetDefaultROT() self:SetDetection() @@ -242,13 +247,13 @@ function FLIGHTGROUP:New(group) self:AddTransition("*", "RTZ", "Inbound") -- Group is returning to destination zone. Not implemented yet! self:AddTransition("Inbound", "Holding", "Holding") -- Group is in holding pattern. - self:AddTransition("*", "Refuel", "Going4Fuel") -- Group is send to refuel at a tanker. Not implemented yet! - self:AddTransition("Going4Fuel", "Refueled", "Airborne") -- Group is send to refuel at a tanker. Not implemented yet! + self:AddTransition("*", "Refuel", "Going4Fuel") -- Group is send to refuel at a tanker. + self:AddTransition("Going4Fuel", "Refueled", "Airborne") -- Group finished refueling. self:AddTransition("*", "LandAt", "LandingAt") -- Helo group is ordered to land at a specific point. self:AddTransition("LandingAt", "LandedAt", "LandedAt") -- Helo group landed landed at a specific point. - self:AddTransition("*", "Wait", "Waiting") -- Group is orbiting. + self:AddTransition("*", "Wait", "*") -- Group is orbiting. self:AddTransition("*", "FuelLow", "*") -- Fuel state of group is low. Default ~25%. self:AddTransition("*", "FuelCritical", "*") -- Fuel state of group is critical. Default ~10%. @@ -269,7 +274,6 @@ function FLIGHTGROUP:New(group) self:AddTransition("*", "ElementLanded", "*") -- An element landed. self:AddTransition("*", "ElementArrived", "*") -- An element arrived. - self:AddTransition("*", "ElementOutOfAmmo", "*") -- An element is completely out of ammo. @@ -325,9 +329,13 @@ function FLIGHTGROUP:New(group) self:_InitGroup() -- Start the status monitoring. - self:__CheckZone(-1) - self:__Status(-2) - self:__QueueUpdate(-3) + self:__Status(-1) + + -- Start queue update timer. + self.timerQueueUpdate=TIMER:New(self._QueueUpdate, self):Start(2, 5) + + -- Start check zone timer. + self.timerCheckZone=TIMER:New(self._CheckInZones, self):Start(3, 10) return self end @@ -351,7 +359,7 @@ end -- @param Ops.AirWing#AIRWING airwing The AIRWING object. -- @return #FLIGHTGROUP self function FLIGHTGROUP:SetAirwing(airwing) - self:I(self.lid..string.format("Add flight to AIRWING %s", airwing.alias)) + self:T(self.lid..string.format("Add flight to AIRWING %s", airwing.alias)) self.airwing=airwing return self end @@ -403,6 +411,25 @@ function FLIGHTGROUP:GetFlightControl() end +--- Set the homebase. +-- @param #FLIGHTGROUP self +-- @param Wrapper.Airbase#AIRBASE HomeAirbase The home airbase. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetHomebase(HomeAirbase) + self.homebase=HomeAirbase + return self +end + +--- Set the destination airbase. This is where the flight will go, when the final waypoint is reached. +-- @param #FLIGHTGROUP self +-- @param Wrapper.Airbase#AIRBASE DestinationAirbase The destination airbase. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetDestinationbase(DestinationAirbase) + self.destbase=DestinationAirbase + return self +end + + --- Set the AIRBOSS controlling this flight group. -- @param #FLIGHTGROUP self -- @param Ops.Airboss#AIRBOSS airboss The AIRBOSS object. @@ -450,11 +477,30 @@ end --- Set fuel critical threshold. Triggers event "FuelCritical" and event function "OnAfterFuelCritical". -- @param #FLIGHTGROUP self -- @param #number threshold Fuel threshold in percent. Default 10 %. --- @param #boolean rtb If true, RTB on fuel critical event. -- @return #FLIGHTGROUP self -function FLIGHTGROUP:SetFuelCriticalThreshold(threshold, rtb) +function FLIGHTGROUP:SetFuelCriticalThreshold(threshold) self.fuelcriticalthresh=threshold or 10 - self.fuelcriticalrtb=rtb + return self +end + +--- Set if critical fuel threshold is reached, flight goes RTB. +-- @param #FLIGHTGROUP self +-- @param #boolean switch If true or nil, flight goes RTB. If false, turn this off. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetFuelCriticalRTB(switch) + if switch==false then + self.fuelcriticalrtb=false + else + self.fuelcriticalrtb=true + end + return self +end + +--- Enable that the group is despawned after landing. This can be useful to avoid DCS taxi issues with other AI or players or jamming taxiways. +-- @param #FLIGHTGROUP self +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetDespawnAfterLanding() + self.despawnAfterLanding=true return self end @@ -598,7 +644,7 @@ function FLIGHTGROUP:StartUncontrolled(delay) if self:IsAlive() then --TODO: check Alive==true and Alive==false ==> Activate first - self:I(self.lid.."Starting uncontrolled group") + self:T(self.lid.."Starting uncontrolled group") self.group:StartUncontrolled(delay) self.isUncontrolled=true else @@ -621,7 +667,7 @@ function FLIGHTGROUP:ClearToLand(Delay) else if self:IsHolding() then - self:I(self.lid..string.format("Clear to land ==> setting holding flag to 1 (true)")) + self:T(self.lid..string.format("Clear to land ==> setting holding flag to 1 (true)")) self.flaghold:Set(1) end @@ -658,6 +704,56 @@ end -- Status ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +---- Update status. +-- @param #FLIHGTGROUP self +function FLIGHTGROUP:onbeforeStatus(From, Event, To) + + -- First we check if elements are still alive. Could be that they were despawned without notice, e.g. when landing on a too small airbase. + for i,_element in pairs(self.elements) do + local element=_element --#FLIGHTGROUP.Element + + -- Check that element is not already dead or not yet alive. + if element.status~=OPSGROUP.ElementStatus.DEAD and element.status~=OPSGROUP.ElementStatus.INUTERO then + + -- Unit shortcut. + local unit=element.unit + + local isdead=false + if unit and unit:IsAlive() then + + -- Get life points. + local life=unit:GetLife() or 0 + + -- Units with life <=1 are dead. + if life<=1 then + isdead=true + end + + else + -- Not alive any more. + isdead=true + end + + -- This one is dead. + if isdead then + self:E(self.lid..string.format("Element %s is dead! Probably despawned without notice or landed at a too small airbase", tostring(element.name))) + self:ElementDead(element) + end + + end + end + + if self:IsDead() then + self:T(self.lid..string.format("Onbefore Status DEAD ==> false")) + return false + elseif self:IsStopped() then + self:T(self.lid..string.format("Onbefore Status STOPPED ==> false")) + return false + end + + return true +end + --- On after "Status" event. -- @param #FLIGHTGROUP self -- @param #string From From state. @@ -667,6 +763,9 @@ function FLIGHTGROUP:onafterStatus(From, Event, To) -- FSM state. local fsmstate=self:GetState() + + -- Update position. + self:_UpdatePosition() --- -- Detection @@ -706,22 +805,28 @@ function FLIGHTGROUP:onafterStatus(From, Event, To) end --- - -- Elements + -- Group --- - local nTaskTot, nTaskSched, nTaskWP=self:CountRemainingTasks() - local nMissions=self:CountRemainingMissison() - -- Short info. - if self.verbose>0 then - local text=string.format("Status %s [%d/%d]: Tasks=%d (%d,%d) Current=%d. Missions=%s. Waypoint=%d/%d. Detected=%d. Destination=%s, FC=%s", + if self.verbose>=1 then + + local nTaskTot, nTaskSched, nTaskWP=self:CountRemainingTasks() + local nMissions=self:CountRemainingMissison() + + + local text=string.format("Status %s [%d/%d]: Tasks=%d (%d,%d) Curr=%d, Missions=%s, Waypoint=%d/%d, Detected=%d, Home=%s, Destination=%s", fsmstate, #self.elements, #self.elements, nTaskTot, nTaskSched, nTaskWP, self.taskcurrent, nMissions, self.currentwp or 0, self.waypoints and #self.waypoints or 0, - self.detectedunits:Count(), self.destbase and self.destbase:GetName() or "unknown", self.flightcontrol and self.flightcontrol.airbasename or "none") + self.detectedunits:Count(), self.homebase and self.homebase:GetName() or "unknown", self.destbase and self.destbase:GetName() or "unknown") self:I(self.lid..text) + end - -- Element status. - if self.verbose>1 then + --- + -- Elements + --- + + if self.verbose>=2 then local text="Elements:" for i,_element in pairs(self.elements) do local element=_element --#FLIGHTGROUP.Element @@ -755,25 +860,17 @@ function FLIGHTGROUP:onafterStatus(From, Event, To) -- Distance travelled --- - if self.verbose>1 and self:IsAlive() and self.position then - - local time=timer.getAbsTime() - - -- Current position. - local position=self:GetCoordinate() + if self.verbose>=3 and self:IsAlive() then -- Travelled distance since last check. - local ds=self.position:Get3DDistance(position) + local ds=self.travelds -- Time interval. - local dt=time-self.traveltime + local dt=self.dTpositionUpdate -- Speed. local v=ds/dt - -- Add up travelled distance. - self.traveldist=self.traveldist+ds - -- Max fuel time remaining. local TmaxFuel=math.huge @@ -806,68 +903,14 @@ function FLIGHTGROUP:onafterStatus(From, Event, To) -- Log outut. self:I(self.lid..string.format("Travelled ds=%.1f km dt=%.1f s ==> v=%.1f knots. Fuel left for %.1f min", self.traveldist/1000, dt, UTILS.MpsToKnots(v), TmaxFuel/60)) - - - -- Update parameters. - self.traveltime=time - self.position=position + end --- - -- Tasks + -- Tasks & Missions --- - -- Task queue. - if #self.taskqueue>0 and self.verbose>1 then - local text=string.format("Tasks #%d", #self.taskqueue) - for i,_task in pairs(self.taskqueue) do - local task=_task --Ops.OpsGroup#OPSGROUP.Task - local name=task.description - local taskid=task.dcstask.id or "unknown" - local status=task.status - local clock=UTILS.SecondsToClock(task.time, true) - local eta=task.time-timer.getAbsTime() - local started=task.timestamp and UTILS.SecondsToClock(task.timestamp, true) or "N/A" - local duration=-1 - if task.duration then - duration=task.duration - if task.timestamp then - -- Time the task is running. - duration=task.duration-(timer.getAbsTime()-task.timestamp) - else - -- Time the task is supposed to run. - duration=task.duration - end - end - -- Output text for element. - if task.type==OPSGROUP.TaskType.SCHEDULED then - text=text..string.format("\n[%d] %s (%s): status=%s, scheduled=%s (%d sec), started=%s, duration=%d", i, taskid, name, status, clock, eta, started, duration) - elseif task.type==OPSGROUP.TaskType.WAYPOINT then - text=text..string.format("\n[%d] %s (%s): status=%s, waypoint=%d, started=%s, duration=%d, stopflag=%d", i, taskid, name, status, task.waypoint, started, duration, task.stopflag:Get()) - end - end - self:I(self.lid..text) - end - - --- - -- Missions - --- - - -- Current mission name. - if self.verbose>0 then - local Mission=self:GetMissionByID(self.currentmission) - - -- Current status. - local text=string.format("Missions %d, Current: %s", self:CountRemainingMissison(), Mission and Mission.name or "none") - for i,_mission in pairs(self.missionqueue) do - local mission=_mission --Ops.Auftrag#AUFTRAG - local Cstart= UTILS.SecondsToClock(mission.Tstart, true) - local Cstop = mission.Tstop and UTILS.SecondsToClock(mission.Tstop, true) or "INF" - text=text..string.format("\n[%d] %s (%s) status=%s (%s), Time=%s-%s, prio=%d wp=%s targets=%d", - i, tostring(mission.name), mission.type, mission:GetGroupStatus(self), tostring(mission.status), Cstart, Cstop, mission.prio, tostring(mission:GetGroupWaypointIndex(self)), mission:CountMissionTargets()) - end - self:I(self.lid..text) - end + self:_PrintTaskAndMissionStatus() --- -- Fuel State @@ -915,8 +958,6 @@ function FLIGHTGROUP:onafterStatus(From, Event, To) end end - - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Events ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -926,6 +967,8 @@ end -- @param Core.Event#EVENTDATA EventData Event data. function FLIGHTGROUP:OnEventBirth(EventData) + --env.info(string.format("EVENT: Birth for unit %s", tostring(EventData.IniUnitName))) + -- Check that this is the right group. if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then local unit=EventData.IniUnit @@ -935,11 +978,6 @@ function FLIGHTGROUP:OnEventBirth(EventData) -- Set group. self.group=self.group or EventData.IniGroup - if not self.groupinitialized then - --TODO: actually that is not very good here as if the first unit is born and in initgroup we initialize all elements! - self:_InitGroup() - end - if self.respawning then local function reset() @@ -1089,7 +1127,7 @@ function FLIGHTGROUP:OnEventEngineShutdown(EventData) if element.unit and element.unit:IsAlive() then - local airbase=self:GetClosestAirbase() --element.unit:GetCoordinate():GetClosestAirbase() + local airbase=self:GetClosestAirbase() local parking=self:GetParkingSpot(element, 10, airbase) if airbase and parking then @@ -1097,18 +1135,13 @@ function FLIGHTGROUP:OnEventEngineShutdown(EventData) self:T3(self.lid..string.format("EVENT: Element %s shut down engines ==> arrived", element.name)) else self:T3(self.lid..string.format("EVENT: Element %s shut down engines but is not parking. Is it dead?", element.name)) - --self:ElementDead(element) end else - --self:I(self.lid..string.format("EVENT: Element %s shut down engines but is NOT alive ==> waiting for crash event (==> dead)", element.name)) - end - else - -- element is nil - end + end -- element nil? end @@ -1155,8 +1188,8 @@ function FLIGHTGROUP:OnEventUnitLost(EventData) local element=self:GetElementByName(unitname) if element then - self:I(self.lid..string.format("EVENT: Element %s unit lost ==> dead", element.name)) - self:ElementDead(element) + self:T3(self.lid..string.format("EVENT: Element %s unit lost ==> destroyed", element.name)) + self:ElementDestroyed(element) end end @@ -1178,7 +1211,7 @@ function FLIGHTGROUP:OnEventRemoveUnit(EventData) local element=self:GetElementByName(unitname) if element then - self:I(self.lid..string.format("EVENT: Element %s removed ==> dead", element.name)) + self:T3(self.lid..string.format("EVENT: Element %s removed ==> dead", element.name)) self:ElementDead(element) end @@ -1217,7 +1250,7 @@ function FLIGHTGROUP:onafterElementSpawned(From, Event, To, Element) else -- TODO: This can happen if spawned on deck of a carrier! - self:E(self.lid..string.format("Element spawned not in air but not on any parking spot.")) + self:T(self.lid..string.format("Element spawned not in air but not on any parking spot.")) self:__ElementParking(0.11, Element) end end @@ -1329,18 +1362,27 @@ end -- @param Wrapper.Airbase#AIRBASE airbase The airbase if applicable or nil. function FLIGHTGROUP:onafterElementLanded(From, Event, To, Element, airbase) self:T2(self.lid..string.format("Element landed %s at %s airbase", Element.name, airbase and airbase:GetName() or "unknown")) + + if self.despawnAfterLanding then + + -- Despawn the element. + self:DespawnElement(Element) + + else - -- Helos with skids land directly on parking spots. - if self.ishelo then - - local Spot=self:GetParkingSpot(Element, 10, airbase) - - self:_SetElementParkingAt(Element, Spot) - + -- Helos with skids land directly on parking spots. + if self.ishelo then + + local Spot=self:GetParkingSpot(Element, 10, airbase) + + self:_SetElementParkingAt(Element, Spot) + + end + + -- Set element status. + self:_UpdateStatus(Element, OPSGROUP.ElementStatus.LANDED, airbase) + end - - -- Set element status. - self:_UpdateStatus(Element, OPSGROUP.ElementStatus.LANDED, airbase) end --- On after "ElementArrived" event. @@ -1360,6 +1402,19 @@ function FLIGHTGROUP:onafterElementArrived(From, Event, To, Element, airbase, Pa self:_UpdateStatus(Element, OPSGROUP.ElementStatus.ARRIVED) end +--- On after "ElementDestroyed" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #FLIGHTGROUP.Element Element The flight group element. +function FLIGHTGROUP:onafterElementDestroyed(From, Event, To, Element) + + -- Call OPSGROUP function. + self:GetParent(self).onafterElementDestroyed(self, From, Event, To, Element) + +end + --- On after "ElementDead" event. -- @param #FLIGHTGROUP self -- @param #string From From state. @@ -1367,7 +1422,9 @@ end -- @param #string To To state. -- @param #FLIGHTGROUP.Element Element The flight group element. function FLIGHTGROUP:onafterElementDead(From, Event, To, Element) - self:T(self.lid..string.format("Element dead %s.", Element.name)) + + -- Call OPSGROUP function. + self:GetParent(self).onafterElementDead(self, From, Event, To, Element) if self.flightcontrol and Element.parking then self.flightcontrol:SetParkingFree(Element.parking) @@ -1375,9 +1432,7 @@ function FLIGHTGROUP:onafterElementDead(From, Event, To, Element) -- Not parking any more. Element.parking=nil - - -- Set element status. - self:_UpdateStatus(Element, OPSGROUP.ElementStatus.DEAD) + end @@ -1387,34 +1442,44 @@ end -- @param #string Event Event. -- @param #string To To state. function FLIGHTGROUP:onafterSpawned(From, Event, To) - self:I(self.lid..string.format("Flight spawned!")) + self:T(self.lid..string.format("Flight spawned")) + + -- Update position. + self:_UpdatePosition() if self.ai then - -- Set default ROE and ROT options. - self:SetOptionROE(self.roe) - self:SetOptionROT(self.rot) + -- Set ROE. + self:SwitchROE(self.option.ROE) - -- TODO: make this input. - self.group:SetOption(AI.Option.Air.id.PROHIBIT_JETT, true) - self.group:SetOption(AI.Option.Air.id.PROHIBIT_AB, true) -- Does not seem to work. AI still used the after burner. - self.group:SetOption(AI.Option.Air.id.RTB_ON_BINGO, false) - --self.group:SetOption(AI.Option.Air.id.RADAR_USING, AI.Option.Air.val.RADAR_USING.FOR_CONTINUOUS_SEARCH) - - -- Turn TACAN beacon on. - if self.tacanChannelDefault then - self:SwitchTACANOn(self.tacanChannelDefault, self.tacanMorseDefault) - end - - -- Turn on the radio. - if self.radioFreqDefault then - self:SwitchRadioOn(self.radioFreqDefault, self.radioModuDefault) + -- Set ROT. + self:SwitchROT(self.option.ROT) + + -- Set Formation + self:SwitchFormation(self.option.Formation) + + -- Set TACAN beacon. + self:_SwitchTACAN() + + -- Set radio freq and modu. + if self.radioDefault then + self:SwitchRadio() + else + self:SetDefaultRadio(self.radio.Freq, self.radio.Modu, self.radio.On) end -- Set callsign. - if self.callsignNameDefault then - self:SwitchCallsign(self.callsignNameDefault, self.callsignNumberDefault) + if self.callsignDefault then + self:SwitchCallsign(self.callsignDefault.NumberSquad, self.callsignDefault.NumberGroup) + else + self:SetDefaultCallsign(self.callsign.NumberSquad, self.callsign.NumberGroup) end + + -- TODO: make this input. + self.group:SetOption(AI.Option.Air.id.PROHIBIT_JETT, true) + self.group:SetOption(AI.Option.Air.id.PROHIBIT_AB, true) -- Does not seem to work. AI still used the after burner. + self.group:SetOption(AI.Option.Air.id.RTB_ON_BINGO, false) + --self.group:SetOption(AI.Option.Air.id.RADAR_USING, AI.Option.Air.val.RADAR_USING.FOR_CONTINUOUS_SEARCH) -- Update route. self:__UpdateRoute(-0.5) @@ -1519,12 +1584,11 @@ end -- @param #string Event Event. -- @param #string To To state. function FLIGHTGROUP:onafterAirborne(From, Event, To) - self:I(self.lid..string.format("Flight airborne")) + self:T(self.lid..string.format("Flight airborne")) if self.ai then self:_CheckGroupDone(1) else - --if not self.ai then self:_UpdateMenu() end end @@ -1541,6 +1605,7 @@ function FLIGHTGROUP:onafterLanding(From, Event, To) end + --- On after "Landed" event. -- @param #FLIGHTGROUP self -- @param #string From From state. @@ -1550,16 +1615,23 @@ end function FLIGHTGROUP:onafterLanded(From, Event, To, airbase) self:T(self.lid..string.format("Flight landed at %s", airbase and airbase:GetName() or "unknown place")) - if self:IsLandingAt() then - self:LandedAt() - else - if self.flightcontrol and airbase and self.flightcontrol.airbasename==airbase:GetName() then - -- Add flight to taxiinb queue. - self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.TAXIINB) - end + if self.flightcontrol and airbase and self.flightcontrol.airbasename==airbase:GetName() then + -- Add flight to taxiinb queue. + self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.TAXIINB) end + end +--- On after "LandedAt" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterLandedAt(From, Event, To) + self:T(self.lid..string.format("Flight landed at")) +end + + --- On after "Arrived" event. -- @param #FLIGHTGROUP self -- @param #string From From state. @@ -1574,39 +1646,40 @@ function FLIGHTGROUP:onafterArrived(From, Event, To) self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.ARRIVED) end - -- Stop and despawn in 5 min. - self:__Stop(5*60) + -- Despawn in 5 min. + if not self.airwing then + self:Despawn(5*60) + end end ---- On after "FlightDead" event. +--- On after "Dead" event. -- @param #FLIGHTGROUP self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. function FLIGHTGROUP:onafterDead(From, Event, To) - self:I(self.lid..string.format("Flight dead!")) - - -- Delete waypoints so they are re-initialized at the next spawn. - self.waypoints=nil - self.groupinitialized=false -- Remove flight from all FC queues. if self.flightcontrol then self.flightcontrol:_RemoveFlight(self) self.flightcontrol=nil end + + if self.Ndestroyed==#self.elements then + if self.squadron then + -- All elements were destroyed ==> Asset group is gone. + self.squadron:DelGroup(self.groupname) + end + else + if self.airwing then + -- Not all assets were destroyed (despawn) ==> Add asset back to airwing. + self.airwing:AddAsset(self.group, 1) + end + end - -- Cancel all mission. - for _,_mission in pairs(self.missionqueue) do - local mission=_mission --Ops.Auftrag#AUFTRAG - - self:MissionCancel(mission) - mission:GroupDead(self) - - end - - -- Stop - self:Stop() + -- Call OPSGROUP function. + self:GetParent(self).onafterDead(self, From, Event, To) + end @@ -1625,14 +1698,14 @@ function FLIGHTGROUP:onbeforeUpdateRoute(From, Event, To, n) if self:IsAlive() then -- and (self:IsAirborne() or self:IsWaiting() or self:IsInbound() or self:IsHolding()) then -- Alive & Airborne ==> Update route possible. - self:T3(self.lid.."Update route possible. Group is ALIVE and AIRBORNE or WAITING or INBOUND or HOLDING") + self:T3(self.lid.."Update route possible. Group is ALIVE") elseif self:IsDead() then -- Group is dead! No more updates. self:E(self.lid.."Update route denied. Group is DEAD!") allowed=false else - -- Not airborne yet. Try again in 1 sec. - self:I(self.lid.."FF update route denied ==> checking back in 5 sec") + -- Not airborne yet. Try again in 5 sec. + self:T(self.lid.."Update route denied ==> checking back in 5 sec") trepeat=-5 allowed=false end @@ -1649,7 +1722,7 @@ function FLIGHTGROUP:onbeforeUpdateRoute(From, Event, To, n) local N=n or self.currentwp+1 if not N or N<1 then - self:E(self.lid.."FF update route denied because N=nil or N<1") + self:E(self.lid.."Update route denied because N=nil or N<1") trepeat=-5 allowed=false end @@ -1699,11 +1772,13 @@ function FLIGHTGROUP:onafterUpdateRoute(From, Event, To, n) local speed=self.group and self.group:GetVelocityKMH() or 100 -- Set current waypoint or we get problem that the _PassingWaypoint function is triggered too early, i.e. right now and not when passing the next WP. - local current=self.group:GetCoordinate():WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, speed, true, nil, {}, "Current") + local current=self.group:GetCoordinate():WaypointAir(COORDINATE.WaypointAltType.BARO, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, speed, true, nil, {}, "Current") table.insert(wp, current) + + local Nwp=self.waypoints and #self.waypoints or 0 -- Add remaining waypoints to route. - for i=n, #self.waypoints do + for i=n, Nwp do table.insert(wp, self.waypoints[i]) end @@ -1725,7 +1800,7 @@ function FLIGHTGROUP:onafterUpdateRoute(From, Event, To, n) --- if self:IsAirborne() then - self:T2(self.lid.."No waypoints left ==> CheckGroupDone") + self:T(self.lid.."No waypoints left ==> CheckGroupDone") self:_CheckGroupDone() end @@ -1778,7 +1853,6 @@ function FLIGHTGROUP:_CheckGroupDone(delay) return end - -- Number of tasks remaining. local nTasks=self:CountRemainingTasks() @@ -1793,28 +1867,31 @@ function FLIGHTGROUP:_CheckGroupDone(delay) -- Number of remaining tasks/missions? if nTasks==0 and nMissions==0 then + + local destbase=self.destbase or self.homebase + local destzone=self.destzone or self.homezone -- Send flight to destination. - if self.destbase then - self:I(self.lid.."Passed Final WP and No current and/or future missions/task ==> RTB!") - self:__RTB(-3, self.destbase) - elseif self.destzone then - self:I(self.lid.."Passed Final WP and No current and/or future missions/task ==> RTZ!") - self:__RTZ(-3, self.destzone) + if destbase then + self:T(self.lid.."Passed Final WP and No current and/or future missions/task ==> RTB!") + self:__RTB(-3, destbase) + elseif destzone then + self:T(self.lid.."Passed Final WP and No current and/or future missions/task ==> RTZ!") + self:__RTZ(-3, destzone) else - self:I(self.lid.."Passed Final WP and NO Tasks/Missions left. No DestBase or DestZone ==> Wait!") + self:T(self.lid.."Passed Final WP and NO Tasks/Missions left. No DestBase or DestZone ==> Wait!") self:__Wait(-1) end else - self:I(self.lid..string.format("Passed Final WP but Tasks=%d or Missions=%d left in the queue. Wait!", nTasks, nMissions)) + self:T(self.lid..string.format("Passed Final WP but Tasks=%d or Missions=%d left in the queue. Wait!", nTasks, nMissions)) self:__Wait(-1) end else - self:I(self.lid..string.format("Passed Final WP but still have current Task (#%s) or Mission (#%s) left to do", tostring(self.taskcurrent), tostring(self.currentmission))) + self:T(self.lid..string.format("Passed Final WP but still have current Task (#%s) or Mission (#%s) left to do", tostring(self.taskcurrent), tostring(self.currentmission))) end else - self:I(self.lid..string.format("Flight (status=%s) did NOT pass the final waypoint yet ==> update route", self:GetState())) + self:T(self.lid..string.format("Flight (status=%s) did NOT pass the final waypoint yet ==> update route", self:GetState())) self:__UpdateRoute(-1) end end @@ -1913,6 +1990,20 @@ function FLIGHTGROUP:onafterRTB(From, Event, To, airbase, SpeedTo, SpeedHold, Sp -- Clear holding time in any case. self.Tholding=nil + + -- Cancel all missions. + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + local mystatus=mission:GetGroupStatus(self) + + -- Check if mission is already over! + if not (mystatus==AUFTRAG.GroupStatus.DONE or mystatus==AUFTRAG.GroupStatus.CANCELLED) then + local text=string.format("Canceling mission %s in state=%s", mission.name, mission.status) + self:T(self.lid..text) + self:MissionCancel(mission) + end + + end -- Defaults: SpeedTo=SpeedTo or UTILS.KmphToKnots(self.speedCruise) @@ -1921,7 +2012,6 @@ function FLIGHTGROUP:onafterRTB(From, Event, To, airbase, SpeedTo, SpeedHold, Sp -- Debug message. local text=string.format("Flight group set to hold at airbase %s. SpeedTo=%d, SpeedHold=%d, SpeedLand=%d", airbase:GetName(), SpeedTo, SpeedHold, SpeedLand) - MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) self:T(self.lid..text) local althold=self.ishelo and 1000+math.random(10)*100 or math.random(4,10)*1000 @@ -2101,7 +2191,6 @@ function FLIGHTGROUP:onafterWait(From, Event, To, Coord, Altitude, Speed) -- Debug message. local text=string.format("Flight group set to wait/orbit at altitude %d m and speed %.1f km/h", Altitude, Speed) - MESSAGE:New(text, 30, "DEBUG"):ToAllIf(self.Debug) self:T(self.lid..text) --TODO: set ROE passive. introduce roe event/state/variable. @@ -2125,13 +2214,12 @@ function FLIGHTGROUP:onafterRefuel(From, Event, To, Coordinate) -- Debug message. local text=string.format("Flight group set to refuel at the nearest tanker") - MESSAGE:New(text, 30, "DEBUG"):ToAllIf(self.Debug) self:I(self.lid..text) --TODO: set ROE passive. introduce roe event/state/variable. - --TODO: cancel current task + -- Pause current mission if there is any. self:PauseMission() -- Refueling task. @@ -2148,7 +2236,7 @@ function FLIGHTGROUP:onafterRefuel(From, Event, To, Coordinate) local wp0=coordinate:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, Speed, true) local wp9=Coordinate:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, Speed, true, nil, DCSTasks, "Refuel") - self:Route({wp0, wp9}) + self:Route({wp0, wp9}, 1) end @@ -2158,9 +2246,9 @@ end -- @param #string Event Event. -- @param #string To To state. function FLIGHTGROUP:onafterRefueled(From, Event, To) + -- Debug message. local text=string.format("Flight group finished refuelling") - MESSAGE:New(text, 30, "DEBUG"):ToAllIf(self.Debug) self:I(self.lid..text) -- Check if flight is done. @@ -2183,8 +2271,7 @@ function FLIGHTGROUP:onafterHolding(From, Event, To) self.Tholding=timer.getAbsTime() local text=string.format("Flight group %s is HOLDING now", self.groupname) - MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) - self:I(self.lid..text) + self:T(self.lid..text) -- Add flight to waiting/holding queue. if self.flightcontrol then @@ -2294,7 +2381,6 @@ function FLIGHTGROUP:onafterFuelLow(From, Event, To) -- Debug message. local text=string.format("Low fuel for flight group %s", self.groupname) - MESSAGE:New(text, 30, "DEBUG"):ToAllIf(self.Debug) self:I(self.lid..text) -- Set switch to true. @@ -2309,9 +2395,14 @@ function FLIGHTGROUP:onafterFuelLow(From, Event, To) local tanker=self.airwing:GetTankerForFlight(self) if tanker then + + self:I(self.lid..string.format("Send to refuel at tanker %s", tanker.flightgroup:GetName())) + + -- Get a coordinate towards the tanker. + local coordinate=self:GetCoordinate():GetIntermediateCoordinate(tanker.flightgroup:GetCoordinate(), 0.75) -- Send flight to tanker with refueling task. - self:Refuel(tanker.flightgroup:GetCoordinate()) + self:Refuel(coordinate) else @@ -2332,7 +2423,10 @@ function FLIGHTGROUP:onafterFuelLow(From, Event, To) self:I(self.lid..string.format("Send to refuel at tanker %s", tanker:GetName())) - self:Refuel() + -- Get a coordinate towards the tanker. + local coordinate=self:GetCoordinate():GetIntermediateCoordinate(tanker.flightgroup:GetCoordinate(), 0.75) + + self:Refuel(coordinate) return end @@ -2356,7 +2450,6 @@ function FLIGHTGROUP:onafterFuelCritical(From, Event, To) -- Debug message. local text=string.format("Critical fuel for flight group %s", self.groupname) - MESSAGE:New(text, 30, "DEBUG"):ToAllIf(self.Debug) self:I(self.lid..text) -- Set switch to true. @@ -2371,7 +2464,7 @@ function FLIGHTGROUP:onafterFuelCritical(From, Event, To) end end ---- On after Start event. Starts the FLIGHTGROUP FSM and event handlers. +--- On after "Stop" event. -- @param #FLIGHTGROUP self -- @param #string From From state. -- @param #string Event Event. @@ -2389,8 +2482,6 @@ function FLIGHTGROUP:onafterStop(From, Event, To) end end - -- Destroy group. No event is generated. - self.group:Destroy(false) end -- Handle events: @@ -2403,12 +2494,12 @@ function FLIGHTGROUP:onafterStop(From, Event, To) self:UnHandleEvent(EVENTS.Ejection) self:UnHandleEvent(EVENTS.Crash) self:UnHandleEvent(EVENTS.RemoveUnit) - - self.CallScheduler:Clear() - - _DATABASE.FLIGHTGROUPS[self.groupname]=nil - - self:I(self.lid.."STOPPED! Unhandled events, cleared scheduler and removed from database.") + + -- Call OPSGROUP function. + self:GetParent(self).onafterStop(self, From, Event, To) + + -- Remove flight from data base. + _DATABASE.FLIGHTGROUPS[self.groupname]=nil end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2431,7 +2522,7 @@ end -- @param Wrapper.Group#GROUP group Group object. -- @param #FLIGHTGROUP flightgroup Flight group object. function FLIGHTGROUP._ReachedHolding(group, flightgroup) - flightgroup:I(flightgroup.lid..string.format("Group reached holding point")) + flightgroup:T2(flightgroup.lid..string.format("Group reached holding point")) -- Trigger Holding event. flightgroup:__Holding(-1) @@ -2441,7 +2532,7 @@ end -- @param Wrapper.Group#GROUP group Group object. -- @param #FLIGHTGROUP flightgroup Flight group object. function FLIGHTGROUP._ClearedToLand(group, flightgroup) - flightgroup:I(flightgroup.lid..string.format("Group was cleared to land")) + flightgroup:T2(flightgroup.lid..string.format("Group was cleared to land")) -- Trigger Landing event. flightgroup:__Landing(-1) @@ -2451,7 +2542,7 @@ end -- @param Wrapper.Group#GROUP group Group object. -- @param #FLIGHTGROUP flightgroup Flight group object. function FLIGHTGROUP._FinishedRefuelling(group, flightgroup) - flightgroup:T(flightgroup.lid..string.format("Group finished refueling")) + flightgroup:T2(flightgroup.lid..string.format("Group finished refueling")) -- Trigger Holding event. flightgroup:__Refueled(-1) @@ -2471,9 +2562,12 @@ function FLIGHTGROUP:_InitGroup() self:E(self.lid.."WARNING: Group was already initialized!") return end + + -- Group object. + local group=self.group --Wrapper.Group#GROUP -- Get template of group. - self.template=self.group:GetTemplate() + self.template=group:GetTemplate() -- Define category. self.isAircraft=true @@ -2481,7 +2575,7 @@ function FLIGHTGROUP:_InitGroup() self.isGround=false -- Helo group. - self.ishelo=self.group:IsHelicopter() + self.ishelo=group:IsHelicopter() -- Is (template) group uncontrolled. self.isUncontrolled=self.template.uncontrolled @@ -2490,55 +2584,57 @@ function FLIGHTGROUP:_InitGroup() self.isLateActivated=self.template.lateActivation -- Max speed in km/h. - self.speedmax=self.group:GetSpeedMax() + self.speedMax=group:GetSpeedMax() -- Cruise speed limit 350 kts for fixed and 80 knots for rotary wings. local speedCruiseLimit=self.ishelo and UTILS.KnotsToKmph(80) or UTILS.KnotsToKmph(350) -- Cruise speed: 70% of max speed but within limit. - self.speedCruise=math.min(self.speedmax*0.7, speedCruiseLimit) + self.speedCruise=math.min(self.speedMax*0.7, speedCruiseLimit) -- Group ammo. self.ammo=self:GetAmmoTot() - -- Initial fuel mass. - -- TODO: this is a unit property! - self.fuelmass=0 - - self.traveldist=0 - self.traveltime=timer.getAbsTime() - self.position=self:GetCoordinate() - - -- Radio parameters from template. - self.radioOn=self.template.communication - self.radioFreq=self.template.frequency - self.radioModu=self.template.modulation - - -- If not set by the use explicitly yet, we take the template values as defaults. - if not self.radioFreqDefault then - self.radioFreqDefault=self.radioFreq - self.radioModuDefault=self.radioModu + -- Radio parameters from template. Default is set on spawn if not modified by user. + self.radio.Freq=tonumber(self.template.frequency) + self.radio.Modu=tonumber(self.template.modulation) + self.radio.On=self.template.communication + + -- Set callsign. Default is set on spawn if not modified by user. + local callsign=self.template.units[1].callsign + if type(callsign)=="number" then -- Sometimes callsign is just "101". + local cs=tostring(callsign) + callsign={} + callsign[1]=cs:sub(1,1) + callsign[2]=cs:sub(2,2) + callsign[3]=cs:sub(3,3) end + self.callsign.NumberSquad=callsign[1] + self.callsign.NumberGroup=callsign[2] + self.callsign.NumberElement=callsign[3] -- First element only + self.callsign.NameSquad=UTILS.GetCallsignName(self.callsign.NumberSquad) -- Set default formation. - if not self.formationDefault then - if self.ishelo then - self.formationDefault=ENUMS.Formation.RotaryWing.EchelonLeft.D300 - else - self.formationDefault=ENUMS.Formation.FixedWing.EchelonLeft.Group - end + if self.ishelo then + self.optionDefault.Formation=ENUMS.Formation.RotaryWing.EchelonLeft.D300 + else + self.optionDefault.Formation=ENUMS.Formation.FixedWing.EchelonLeft.Group end + + -- Default TACAN off. + self:SetDefaultTACAN(nil, nil, nil, nil, true) + self.tacan=UTILS.DeepCopy(self.tacanDefault) - self.ai=not self:_IsHuman(self.group) + -- Is this purely AI? + self.ai=not self:_IsHuman(group) + -- Create Menu. if not self.ai then self.menu=self.menu or {} self.menu.atc=self.menu.atc or {} self.menu.atc.root=self.menu.atc.root or MENU_GROUP:New(self.group, "ATC") end - self:SwitchFormation(self.formationDefault) - -- Add elemets. for _,unit in pairs(self.group:GetUnits()) do local element=self:AddElementByName(unit:GetName()) @@ -2561,28 +2657,30 @@ function FLIGHTGROUP:_InitGroup() self.refueltype=select(2, unit:IsRefuelable()) -- Debug info. - local text=string.format("Initialized Flight Group %s:\n", self.groupname) - text=text..string.format("AC type = %s\n", self.actype) - text=text..string.format("Speed max = %.1f Knots\n", UTILS.KmphToKnots(self.speedmax)) - text=text..string.format("Range max = %.1f km\n", self.rangemax/1000) - text=text..string.format("Ceiling = %.1f feet\n", UTILS.MetersToFeet(self.ceiling)) - text=text..string.format("Tanker type = %s\n", tostring(self.tankertype)) - text=text..string.format("Refuel type = %s\n", tostring(self.refueltype)) - text=text..string.format("AI = %s\n", tostring(self.ai)) - text=text..string.format("Helicopter = %s\n", tostring(self.group:IsHelicopter())) - text=text..string.format("Elements = %d\n", #self.elements) - text=text..string.format("Waypoints = %d\n", #self.waypoints) - text=text..string.format("Radio = %.1f MHz %s %s\n", self.radioFreq, UTILS.GetModulationName(self.radioModu), tostring(self.radioOn)) - text=text..string.format("Ammo = %d (G=%d/R=%d/B=%d/M=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Bombs, self.ammo.Missiles) - text=text..string.format("FSM state = %s\n", self:GetState()) - text=text..string.format("Is alive = %s\n", tostring(self.group:IsAlive())) - text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated())) - text=text..string.format("Uncontrolled = %s\n", tostring(self:IsUncontrolled())) - text=text..string.format("Start Air = %s\n", tostring(self:IsTakeoffAir())) - text=text..string.format("Start Cold = %s\n", tostring(self:IsTakeoffCold())) - text=text..string.format("Start Hot = %s\n", tostring(self:IsTakeoffHot())) - text=text..string.format("Start Rwy = %s\n", tostring(self:IsTakeoffRunway())) - self:I(self.lid..text) + if self.verbose>=1 then + local text=string.format("Initialized Flight Group %s:\n", self.groupname) + text=text..string.format("Unit type = %s\n", self.actype) + text=text..string.format("Speed max = %.1f Knots\n", UTILS.KmphToKnots(self.speedMax)) + text=text..string.format("Range max = %.1f km\n", self.rangemax/1000) + text=text..string.format("Ceiling = %.1f feet\n", UTILS.MetersToFeet(self.ceiling)) + text=text..string.format("Tanker type = %s\n", tostring(self.tankertype)) + text=text..string.format("Refuel type = %s\n", tostring(self.refueltype)) + text=text..string.format("AI = %s\n", tostring(self.ai)) + text=text..string.format("Helicopter = %s\n", tostring(self.group:IsHelicopter())) + text=text..string.format("Elements = %d\n", #self.elements) + text=text..string.format("Waypoints = %d\n", #self.waypoints) + text=text..string.format("Radio = %.1f MHz %s %s\n", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu), tostring(self.radio.On)) + text=text..string.format("Ammo = %d (G=%d/R=%d/B=%d/M=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Bombs, self.ammo.Missiles) + text=text..string.format("FSM state = %s\n", self:GetState()) + text=text..string.format("Is alive = %s\n", tostring(self.group:IsAlive())) + text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated())) + text=text..string.format("Uncontrolled = %s\n", tostring(self:IsUncontrolled())) + text=text..string.format("Start Air = %s\n", tostring(self:IsTakeoffAir())) + text=text..string.format("Start Cold = %s\n", tostring(self:IsTakeoffCold())) + text=text..string.format("Start Hot = %s\n", tostring(self:IsTakeoffHot())) + text=text..string.format("Start Rwy = %s\n", tostring(self:IsTakeoffRunway())) + self:I(self.lid..text) + end -- Init done. self.groupinitialized=true @@ -2609,10 +2707,14 @@ function FLIGHTGROUP:AddElementByName(unitname) element.status=OPSGROUP.ElementStatus.INUTERO element.group=unit:GetGroup() - element.modex=element.unit:GetTemplate().onboard_num - element.skill=element.unit:GetTemplate().skill - element.pylons=element.unit:GetTemplatePylons() - element.fuelmass0=element.unit:GetTemplatePayload().fuel + -- TODO: this is wrong when grouping is used! + local unittemplate=element.unit:GetTemplate() + + element.modex=unittemplate.onboard_num + element.skill=unittemplate.skill + element.payload=unittemplate.payload + element.pylons=unittemplate.payload and unittemplate.payload.pylons or nil --element.unit:GetTemplatePylons() + element.fuelmass0=unittemplate.payload and unittemplate.payload.fuel or 0 --element.unit:GetTemplatePayload().fuel element.fuelmass=element.fuelmass0 element.fuelrel=element.unit:GetFuel() element.category=element.unit:GetUnitCategory() @@ -2627,9 +2729,9 @@ function FLIGHTGROUP:AddElementByName(unitname) element.ai=true end - local text=string.format("Adding element %s: status=%s, skill=%s, modex=%s, fuelmass=%.1f (%d %%), category=%d, categoryname=%s, callsign=%s, ai=%s", - element.name, element.status, element.skill, element.modex, element.fuelmass, element.fuelrel, element.category, element.categoryname, element.callsign, tostring(element.ai)) - self:I(self.lid..text) + local text=string.format("Adding element %s: status=%s, skill=%s, modex=%s, fuelmass=%.1f (%d), category=%d, categoryname=%s, callsign=%s, ai=%s", + element.name, element.status, element.skill, element.modex, element.fuelmass, element.fuelrel*100, element.category, element.categoryname, element.callsign, tostring(element.ai)) + self:T(self.lid..text) -- Add element to table. table.insert(self.elements, element) @@ -2718,7 +2820,7 @@ end --- Find the nearest tanker. -- @param #FLIGHTGROUP self -- @param #number Radius Search radius in NM. Default 50 NM. --- @return Wrapper.Group#GROUP Closest tanker group #nil. +-- @return Wrapper.Group#GROUP Closest tanker group or `nil` if no tanker is in the given radius. function FLIGHTGROUP:FindNearestTanker(Radius) Radius=UTILS.NMToMeters(Radius or 50) @@ -2910,15 +3012,21 @@ end --- Initialize Mission Editor waypoints. -- @param #FLIGHTGROUP self --- @param #table waypoints Table of waypoints. Default is from group template. -- @return #FLIGHTGROUP self -function FLIGHTGROUP:InitWaypoints(waypoints) +function FLIGHTGROUP:InitWaypoints() -- Template waypoints. self.waypoints0=self.group:GetTemplateRoutePoints() - -- Waypoints of group as defined in the ME. - self.waypoints=waypoints or UTILS.DeepCopy(self.waypoints0) + -- Waypoints + self.waypoints={} + + for index,wp in pairs(self.waypoints0) do + + local waypoint=self:_CreateWaypoint(wp) + self:_AddWaypoint(waypoint) + + end -- Get home and destination airbases from waypoints. self.homebase=self.homebase or self:GetHomebaseFromWaypoints() @@ -2932,7 +3040,7 @@ function FLIGHTGROUP:InitWaypoints(waypoints) end -- Debug info. - self:I(self.lid..string.format("Initializing %d waypoints. Homebase %s ==> %s Destination", #self.waypoints, self.homebase and self.homebase:GetName() or "unknown", self.destbase and self.destbase:GetName() or "uknown")) + self:T(self.lid..string.format("Initializing %d waypoints. Homebase %s ==> %s Destination", #self.waypoints, self.homebase and self.homebase:GetName() or "unknown", self.destbase and self.destbase:GetName() or "uknown")) -- Update route. if #self.waypoints>0 then @@ -2942,9 +3050,6 @@ function FLIGHTGROUP:InitWaypoints(waypoints) self.passedfinalwp=true end - -- Update route (when airborne). - --self:_CheckGroupDone(1) - --self:__UpdateRoute(-1) end return self @@ -2952,64 +3057,47 @@ end --- Add an AIR waypoint to the flight plan. -- @param #FLIGHTGROUP self --- @param Core.Point#COORDINATE coordinate The coordinate of the waypoint. Use COORDINATE:SetAltitude(altitude) to define the altitude. --- @param #number speed Speed in knots. Default 350 kts. --- @param #number wpnumber Waypoint number. Default at the end. --- @param #boolean updateroute If true or nil, call UpdateRoute. If false, no call. --- @return #number Waypoint index. -function FLIGHTGROUP:AddWaypoint(coordinate, speed, wpnumber, updateroute) +-- @param Core.Point#COORDINATE Coordinate The coordinate of the waypoint. Use COORDINATE:SetAltitude(altitude) to define the altitude. +-- @param #number Speed Speed in knots. Default 350 kts. +-- @param #number AfterWaypointWithID Insert waypoint after waypoint given ID. Default is to insert as last waypoint. +-- @param #number Altitude Altitude in feet. Default is y-component of Coordinate. Note that these altitudes are wrt to sea level (barometric altitude). +-- @param #boolean Updateroute If true or nil, call UpdateRoute. If false, no call. +-- @return Ops.OpsGroup#OPSGROUP.Waypoint Waypoint table. +function FLIGHTGROUP:AddWaypoint(Coordinate, Speed, AfterWaypointWithID, Altitude, Updateroute) - -- Waypoint number. Default is at the end. - wpnumber=wpnumber or #self.waypoints+1 + -- Set waypoint index. + local wpnumber=self:GetWaypointIndexAfterID(AfterWaypointWithID) if wpnumber>self.currentwp then self.passedfinalwp=false end -- Speed in knots. - speed=speed or 350 - - -- Speed at waypoint. - local speedkmh=UTILS.KnotsToKmph(speed) + Speed=Speed or 350 -- Create air waypoint. - local wp=coordinate:WaypointAir(COORDINATE.WaypointAltType.BARO, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, speedkmh, true, nil, {}, string.format("Added Waypoint #%d", wpnumber)) + local wp=Coordinate:WaypointAir(COORDINATE.WaypointAltType.BARO, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, UTILS.KnotsToKmph(Speed), true, nil, {}) - -- Add to table. - table.insert(self.waypoints, wpnumber, wp) + -- Create waypoint data table. + local waypoint=self:_CreateWaypoint(wp) + + -- Set altitude. + if Altitude then + waypoint.alt=UTILS.FeetToMeters(Altitude) + end + + -- Add waypoint to table. + self:_AddWaypoint(waypoint, wpnumber) -- Debug info. - self:T(self.lid..string.format("Adding AIR waypoint #%d, speed=%.1f knots. Last waypoint passed was #%s. Total waypoints #%d", wpnumber, speed, self.currentwp, #self.waypoints)) - - -- Shift all waypoint tasks after the inserted waypoint. - for _,_task in pairs(self.taskqueue) do - local task=_task --Ops.OpsGroup#OPSGROUP.Task - if task.type==OPSGROUP.TaskType.WAYPOINT and task.waypoint and task.waypoint>=wpnumber then - task.waypoint=task.waypoint+1 - end - end - - -- Shift all mission waypoints after the inserted waypoint. - for _,_mission in pairs(self.missionqueue) do - local mission=_mission --Ops.Auftrag#AUFTRAG - - -- Get mission waypoint index. - local wpidx=mission:GetGroupWaypointIndex(self) - - -- Increase number if this waypoint lies in the future. - if wpidx and wpidx>=wpnumber then - mission:SetGroupWaypointIndex(self, wpidx+1) - end - - end + self:T(self.lid..string.format("Adding AIR waypoint #%d, speed=%.1f knots. Last waypoint passed was #%s. Total waypoints #%d", wpnumber, Speed, self.currentwp, #self.waypoints)) -- Update route. - if updateroute==nil or updateroute==true then - --self:_CheckGroupDone(1) + if Updateroute==nil or Updateroute==true then self:__UpdateRoute(-1) end - return wpnumber + return waypoint end @@ -3234,7 +3322,9 @@ function FLIGHTGROUP:GetClosestAirbase() local coord=group:GetCoordinate() local coalition=self:GetCoalition() - return coord:GetClosestAirbase(nil, coalition) + local airbase=coord:GetClosestAirbase() --(nil, coalition) + + return airbase end --- Search unoccupied parking spots at the airbase for all flight elements. @@ -3520,208 +3610,7 @@ end -- OPTION FUNCTIONS ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Set default TACAN parameters. AA TACANs are always on "Y" band. --- @param #FLIGHTGROUP self --- @param #number Channel TACAN channel. --- @param #string Morse Morse code. Default "XXX". --- @return #FLIGHTGROUP self -function FLIGHTGROUP:SetDefaultTACAN(Channel, Morse) - self.tacanChannelDefault=Channel - self.tacanMorseDefault=Morse or "XXX" - - return self -end - ---- Activate TACAN beacon. --- @param #FLIGHTGROUP self --- @param #number TACANChannel TACAN Channel. --- @param #string TACANMorse TACAN morse code. --- @return #FLIGHTGROUP self -function FLIGHTGROUP:SwitchTACANOn(TACANChannel, TACANMorse) - - if self:IsAlive() then - - local unit=self.group:GetUnit(1) --Wrapper.Unit#UNIT - - if unit and unit:IsAlive() then - - local Type=4 - local System=5 - local UnitID=unit:GetID() - local TACANMode="Y" - local Frequency=UTILS.TACANToFrequency(TACANChannel, TACANMode) - - unit:CommandActivateBeacon(Type, System, Frequency, UnitID, TACANChannel, TACANMode, true, TACANMorse, true) - - self.tacanBeacon=unit - self.tacanChannel=TACANChannel - self.tacanMorse=TACANMorse - - self.tacanOn=true - - self:I(self.lid..string.format("Switching TACAN to Channel %dY Morse %s", self.tacanChannel, tostring(self.tacanMorse))) - - end - - end - - return self -end - ---- Deactivate TACAN beacon. --- @param #FLIGHTGROUP self --- @return #FLIGHTGROUP self -function FLIGHTGROUP:SwitchTACANOff() - - if self.tacanBeacon and self.tacanBeacon:IsAlive() then - self.tacanBeacon:CommandDeactivateBeacon() - end - - self:I(self.lid..string.format("Switching TACAN OFF")) - - self.tacanOn=false - -end - ---- Set default Radio frequency and modulation. --- @param #FLIGHTGROUP self --- @param #number Frequency Radio frequency in MHz. Default 251 MHz. --- @param #number Modulation Radio modulation. Default `radio.Modulation.AM`. --- @return #FLIGHTGROUP self -function FLIGHTGROUP:SetDefaultRadio(Frequency, Modulation) - - self.radioFreqDefault=Frequency or 251 - self.radioModuDefault=Modulation or radio.modulation.AM - - self.radioOn=false - - self.radioUse=true - - return self -end - ---- Get current Radio frequency and modulation. --- @param #FLIGHTGROUP self --- @return #number Radio frequency in MHz or nil. --- @return #number Radio modulation or nil. -function FLIGHTGROUP:GetRadio() - return self.radioFreq, self.radioModu -end - ---- Turn radio on. --- @param #FLIGHTGROUP self --- @param #number Frequency Radio frequency in MHz. --- @param #number Modulation Radio modulation. Default `radio.Modulation.AM`. --- @return #FLIGHTGROUP self -function FLIGHTGROUP:SwitchRadioOn(Frequency, Modulation) - - if self:IsAlive() and Frequency then - - Modulation=Modulation or radio.Modulation.AM - - local group=self.group --Wrapper.Group#GROUP - - group:SetOption(AI.Option.Air.id.SILENCE, false) - - group:CommandSetFrequency(Frequency, Modulation) - - self.radioFreq=Frequency - self.radioModu=Modulation - self.radioOn=true - - self:I(self.lid..string.format("Switching radio to frequency %.3f MHz %s", self.radioFreq, UTILS.GetModulationName(self.radioModu))) - - end - - return self -end - ---- Turn radio off. --- @param #FLIGHTGROUP self --- @return #FLIGHTGROUP self -function FLIGHTGROUP:SwitchRadioOff() - - if self:IsAlive() then - - self.group:SetOption(AI.Option.Air.id.SILENCE, true) - - self.radioFreq=nil - self.radioModu=nil - self.radioOn=false - - self:I(self.lid..string.format("Switching radio OFF")) - - end - - return self -end - ---- Set default formation. --- @param #FLIGHTGROUP self --- @param #number Formation The formation the groups flies in. --- @return #FLIGHTGROUP self -function FLIGHTGROUP:SetDefaultFormation(Formation) - - self.formationDefault=Formation - - return self -end - ---- Switch to a specific formation. --- @param #FLIGHTGROUP self --- @param #number Formation New formation the group will fly in. --- @return #FLIGHTGROUP self -function FLIGHTGROUP:SwitchFormation(Formation) - - if self:IsAlive() and Formation then - - self.group:SetOption(AI.Option.Air.id.FORMATION, Formation) - - self.formation=Formation - - self:I(self.lid..string.format("Switching formation to %d", self.formation)) - - end - - return self -end - ---- Set default formation. --- @param #FLIGHTGROUP self --- @param #number CallsignName Callsign name. --- @param #number CallsignNumber Callsign number. --- @return #FLIGHTGROUP self -function FLIGHTGROUP:SetDefaultCallsign(CallsignName, CallsignNumber) - - self.callsignNameDefault=CallsignName - self.callsignNumberDefault=CallsignNumber or 1 - - return self -end - ---- Switch to a specific callsign. --- @param #FLIGHTGROUP self --- @param #number CallsignName Callsign name. --- @param #number CallsignNumber Callsign number. --- @return #FLIGHTGROUP self -function FLIGHTGROUP:SwitchCallsign(CallsignName, CallsignNumber) - - if self:IsAlive() and CallsignName then - - self.callsignName=CallsignName - self.callsignNumber=CallsignNumber or 1 - - self:I(self.lid..string.format("Switching callsign to %d-%d", self.callsignName, self.callsignNumber)) - - local group=self.group --Wrapper.Group#GROUP - - group:CommandSetCallsign(self.callsignName, self.callsignNumber) - - end - - return self -end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Intelligence.lua b/Moose Development/Moose/Ops/Intelligence.lua index ad2e8c76b..584fb865e 100644 --- a/Moose Development/Moose/Ops/Intelligence.lua +++ b/Moose Development/Moose/Ops/Intelligence.lua @@ -15,15 +15,22 @@ -- @type INTEL -- @field #string ClassName Name of the class. -- @field #boolean Debug Debug mode. Messages to all about status. +-- @field #number verbose Verbosity level. -- @field #string lid Class id string for output to DCS log file. -- @field #number coalition Coalition side number, e.g. `coalition.side.RED`. -- @field #string alias Name of the agency. --- @field #table filterCategory Category filters. +-- @field Core.Set#SET_GROUP detectionset Set of detection groups, aka agents. +-- @field #table filterCategory Filter for unit categories. +-- @field #table filterCategoryGroup Filter for group categories. -- @field Core.Set#SET_ZONE acceptzoneset Set of accept zones. If defined, only contacts in these zones are considered. -- @field Core.Set#SET_ZONE rejectzoneset Set of reject zones. Contacts in these zones are not considered, even if they are in accept zones. -- @field #table Contacts Table of detected items. -- @field #table ContactsLost Table of lost detected items. -- @field #table ContactsUnknown Table of new detected items. +-- @field #table Clusters Clusters of detected groups. +-- @field #boolean clusteranalysis If true, create clusters of detected targets. +-- @field #boolean clustermarkers If true, create cluster markers on F10 map. +-- @field #number clustercounter Running number of clusters. -- @field #number dTforget Time interval in seconds before a known contact which is not detected any more is forgotten. -- @extends Core.Fsm#FSM @@ -41,6 +48,7 @@ INTEL = { ClassName = "INTEL", Debug = nil, + verbose = 2, lid = nil, alias = nil, filterCategory = {}, @@ -48,6 +56,8 @@ INTEL = { Contacts = {}, ContactsLost = {}, ContactsUnknown = {}, + Clusters = {}, + clustercounter = 1, } --- Detected item info. @@ -62,8 +72,22 @@ INTEL = { -- @field #number Tdetected Time stamp in abs. mission time seconds when this item was last detected. -- @field Core.Point#COORDINATE position Last known position of the item. -- @field DCS#Vec3 velocity 3D velocity vector. Components x,y and z in m/s. --- @field #number speed Last known speed. --- @field #number markerID F10 map marker ID. +-- @field #number speed Last known speed in m/s. +-- @field #boolean isship +-- @field #boolean ishelo +-- @field #boolean isgrund + +--- Cluster info. +-- @type INTEL.Cluster +-- @field #number index Cluster index. +-- @field #number size Number of groups in the cluster. +-- @field #table Contacts Table of contacts in the cluster. +-- @field #number threatlevelMax Max threat level of cluster. +-- @field #number threatlevelSum Sum of threat levels. +-- @field #number threatlevelAve Average of threat levels. +-- @field Core.Point#COORDINATE coordinate Coordinate of the cluster. +-- @field Wrapper.Marker#MARKER marker F10 marker. + --- INTEL class version. -- @field #string version @@ -170,7 +194,6 @@ function INTEL:New(DetectionSet, Coalition) BASE:TraceClass(self.ClassName) BASE:TraceLevel(1) end - self.Debug=true return self end @@ -223,6 +246,7 @@ function INTEL:SetFilterCategory(Categories) if type(Categories)~="table" then Categories={Categories} end + self.filterCategory=Categories local text="Filter categories: " @@ -243,7 +267,7 @@ end -- * Group.Category.TRAIN -- -- @param #INTEL self --- @param #table Categories Filter categories, e.g. {Group.Category.AIRPLANE, Group.Category.HELICOPTER}. +-- @param #table GroupCategories Filter categories, e.g. `{Group.Category.AIRPLANE, Group.Category.HELICOPTER}`. -- @return #INTEL self function INTEL:FilterCategoryGroup(GroupCategories) if type(GroupCategories)~="table" then @@ -261,6 +285,17 @@ function INTEL:FilterCategoryGroup(GroupCategories) return self end +--- Enable or disable cluster analysis of detected targets. +-- Targets will be grouped in coupled clusters. +-- @param #INTEL self +-- @param #boolean Switch If true, enable cluster analysis. +-- @param #boolean Markers If true, place markers on F10 map. +-- @return #INTEL self +function INTEL:SetClusterAnalysis(Switch, Markers) + self.clusteranalysis=Switch + self.clustermarkers=Markers + return self +end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Start & Status @@ -268,7 +303,6 @@ end --- On after Start event. Starts the FLIGHTGROUP FSM and event handlers. -- @param #INTEL self --- @param Wrapper.Group#GROUP Group Flight group. -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. @@ -284,7 +318,6 @@ end --- On after "Status" event. -- @param #INTEL self --- @param Wrapper.Group#GROUP Group Flight group. -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. @@ -304,12 +337,14 @@ function INTEL:onafterStatus(From, Event, To) local Ncontacts=#self.Contacts -- Short info. - local text=string.format("Status %s [Agents=%s]: Contacts=%d, New=%d, Lost=%d", fsmstate, self.detectionset:CountAlive(), Ncontacts, #self.ContactsUnknown, #self.ContactsLost) - self:I(self.lid..text) + if self.verbose>=1 then + local text=string.format("Status %s [Agents=%s]: Contacts=%d, New=%d, Lost=%d", fsmstate, self.detectionset:CountAlive(), Ncontacts, #self.ContactsUnknown, #self.ContactsLost) + self:I(self.lid..text) + end -- Detailed info. - if Ncontacts>0 then - text="Detected Contacts:" + if self.verbose>=2 and Ncontacts>0 then + local text="Detected Contacts:" for _,_contact in pairs(self.Contacts) do local contact=_contact --#INTEL.Contact local dT=timer.getAbsTime()-contact.Tdetected @@ -322,7 +357,7 @@ function INTEL:onafterStatus(From, Event, To) self:I(self.lid..text) end - self:__Status(-30) + self:__Status(-60) end @@ -331,28 +366,29 @@ end function INTEL:UpdateIntel() -- Set of all detected units. - local DetectedSet=SET_UNIT:New() + local DetectedUnits={} -- Loop over all units providing intel. - for _,_group in pairs(self.detectionset:GetSet()) do + for _,_group in pairs(self.detectionset.Set or {}) do local group=_group --Wrapper.Group#GROUP + if group and group:IsAlive() then + for _,_recce in pairs(group:GetUnits()) do local recce=_recce --Wrapper.Unit#UNIT - -- Get set of detected units. - local detectedunitset=recce:GetDetectedUnitSet() - - -- Add detected units to all set. - DetectedSet=DetectedSet:GetSetUnion(detectedunitset) + -- Get detected units. + self:GetDetectedUnits(recce, DetectedUnits) + end + end end -- TODO: Filter units from reject zones. -- TODO: Filter detection methods? local remove={} - for _,_unit in pairs(DetectedSet.Set) do + for unitname,_unit in pairs(DetectedUnits) do local unit=_unit --Wrapper.Unit#UNIT -- Check if unit is in any of the accept zones. @@ -368,7 +404,7 @@ function INTEL:UpdateIntel() -- Unit is not in accept zone ==> remove! if not inzone then - table.insert(remove, unit:GetName()) + table.insert(remove, unitname) end end @@ -383,8 +419,8 @@ function INTEL:UpdateIntel() end end if not keepit then - self:I(self.lid..string.format("Removing unit %s category=%d", unit:GetName(), unit:GetCategory())) - table.insert(remove, unit:GetName()) + self:I(self.lid..string.format("Removing unit %s category=%d", unitname, unit:GetCategory())) + table.insert(remove, unitname) end end @@ -392,42 +428,44 @@ function INTEL:UpdateIntel() -- Remove filtered units. for _,unitname in pairs(remove) do - DetectedSet:Remove(unitname, true) + DetectedUnits[unitname]=nil + end + + -- Create detected groups. + local DetectedGroups={} + for unitname,_unit in pairs(DetectedUnits) do + local unit=_unit --Wrapper.Unit#UNIT + local group=unit:GetGroup() + if group then + DetectedGroups[group:GetName()]=group + end end -- Create detected contacts. - self:CreateDetectedItems(DetectedSet) + self:CreateDetectedItems(DetectedGroups) + + -- Paint a picture of the battlefield. + if self.clusteranalysis then + self:PaintPicture() + end end + + + + --- Create detected items. -- @param #INTEL self --- @param Core.Set#SET_UNIT detectedunitset Set of detected units. -function INTEL:CreateDetectedItems(detectedunitset) - - local detectedgroupset=SET_GROUP:New() - - -- Convert detected UNIT set to detected GROUP set. - for _,_unit in pairs(detectedunitset:GetSet()) do - local unit=_unit --Wrapper.Unit#UNIT - - local group=unit:GetGroup() - - if group and group:IsAlive() then - local groupname=group:GetName() - detectedgroupset:Add(groupname, group) - end - - end +-- @param #table DetectedGroups Table of detected Groups +function INTEL:CreateDetectedItems(DetectedGroups) -- Current time. local Tnow=timer.getAbsTime() - for _,_group in pairs(detectedgroupset.Set) do + for groupname,_group in pairs(DetectedGroups) do local group=_group --Wrapper.Group#GROUP - -- Group name. - local groupname=group:GetName() -- Get contact if already known. local detecteditem=self:GetContactByName(groupname) @@ -457,7 +495,7 @@ function INTEL:CreateDetectedItems(detectedunitset) item.attribute=group:GetAttribute() item.category=group:GetCategory() item.categoryname=group:GetCategoryName() - item.threatlevel=group:GetUnit(1):GetThreatLevel() + item.threatlevel=group:GetThreatLevel() item.position=group:GetCoordinate() item.velocity=group:GetVelocityVec3() item.speed=group:GetVelocityMPS() @@ -475,10 +513,8 @@ function INTEL:CreateDetectedItems(detectedunitset) for i=#self.Contacts,1,-1 do local item=self.Contacts[i] --#INTEL.Contact - local group=detectedgroupset:FindGroup(item.groupname) - -- Check if deltaT>Tforget. We dont want quick oscillations between detected and undetected states. - if self:CheckContactLost(item) then + if self:_CheckContactLost(item) then -- Trigger LostContact event. This also adds the contact to the self.ContactsLost table. self:LostContact(item) @@ -491,6 +527,41 @@ function INTEL:CreateDetectedItems(detectedunitset) end +--- Return the detected target groups of the controllable as a @{SET_GROUP}. +-- The optional parametes specify the detection methods that can be applied. +-- If no detection method is given, the detection will use all the available methods by default. +-- @param #INTEL self +-- @param Wrapper.Unit#UNIT Unit The unit detecting. +-- @param #boolean DetectVisual (Optional) If *false*, do not include visually detected targets. +-- @param #boolean DetectOptical (Optional) If *false*, do not include optically detected targets. +-- @param #boolean DetectRadar (Optional) If *false*, do not include targets detected by radar. +-- @param #boolean DetectIRST (Optional) If *false*, do not include targets detected by IRST. +-- @param #boolean DetectRWR (Optional) If *false*, do not include targets detected by RWR. +-- @param #boolean DetectDLINK (Optional) If *false*, do not include targets detected by data link. +function INTEL:GetDetectedUnits(Unit, DetectedUnits, DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) + + -- Get detected DCS units. + local detectedtargets=Unit:GetDetectedTargets(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) + + for DetectionObjectID, Detection in pairs(detectedtargets or {}) do + local DetectedObject=Detection.object -- DCS#Object + + if DetectedObject and DetectedObject:isExist() and DetectedObject.id_<50000000 then + + local unit=UNIT:Find(DetectedObject) + + if unit and unit:IsAlive() then + + local unitname=unit:GetName() + + DetectedUnits[unitname]=unit + + end + end + end + +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- FSM Events ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -521,10 +592,10 @@ end -- Misc Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Create detected items. +--- Get a contact by name. -- @param #INTEL self -- @param #string groupname Name of the contact group. --- @return #INTEL.Contact +-- @return #INTEL.Contact The contact. function INTEL:GetContactByName(groupname) for i,_contact in pairs(self.Contacts) do @@ -539,7 +610,7 @@ end --- Add a contact to our list. -- @param #INTEL self --- @param #INTEL.Contact Contact The contact to be removed. +-- @param #INTEL.Contact Contact The contact to be added. function INTEL:AddContact(Contact) table.insert(self.Contacts, Contact) end @@ -560,11 +631,11 @@ function INTEL:RemoveContact(Contact) end ---- Remove a contact from our list. +--- Check if a contact was lost. -- @param #INTEL self -- @param #INTEL.Contact Contact The contact to be removed. -- @return #boolean If true, contact was not detected for at least *dTforget* seconds. -function INTEL:CheckContactLost(Contact) +function INTEL:_CheckContactLost(Contact) -- Group dead? if Contact.group==nil or not Contact.group:IsAlive() then @@ -595,6 +666,378 @@ function INTEL:CheckContactLost(Contact) end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Cluster Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Paint picture of the battle field. +-- @param #INTEL self +function INTEL:PaintPicture() + + -- First remove all lost contacts from clusters. + for _,_contact in pairs(self.ContactsLost) do + local contact=_contact --#INTEL.Contact + local cluster=self:GetClusterOfContact(contact) + if cluster then + self:RemoveContactFromCluster(contact, cluster) + end + end + + + for _,_contact in pairs(self.Contacts) do + local contact=_contact --#INTEL.Contact + + -- Check if this contact is in any cluster. + local isincluster=self:CheckContactInClusters(contact) + + -- Get the current cluster (if any) this contact belongs to. + local currentcluster=self:GetClusterOfContact(contact) + + if currentcluster then + + --- + -- Contact is currently part of a cluster. + --- + + -- Check if the contact is still connected to the cluster. + local isconnected=self:IsContactConnectedToCluster(contact, currentcluster) + + if not isconnected then + + local cluster=self:IsContactPartOfAnyClusters(contact) + + if cluster then + self:AddContactToCluster(contact, cluster) + else + + local newcluster=self:CreateCluster(contact.position) + self:AddContactToCluster(contact, newcluster) + end + + end + + + else + + --- + -- Contact is not in any cluster yet. + --- + + local cluster=self:IsContactPartOfAnyClusters(contact) + + if cluster then + self:AddContactToCluster(contact, cluster) + else + + local newcluster=self:CreateCluster(contact.position) + self:AddContactToCluster(contact, newcluster) + end + + end + + end + + + + -- Update F10 marker text if cluster has changed. + for _,_cluster in pairs(self.Clusters) do + local cluster=_cluster --#INTEL.Cluster + + local coordinate=self:GetClusterCoordinate(cluster) + + + -- Update F10 marker. + self:UpdateClusterMarker(cluster) + end + +end + +--- Create a new cluster. +-- @param #INTEL self +-- @param Core.Point#COORDINATE coordinate The coordinate of the cluster. +-- @return #INTEL.Cluster cluster The cluster. +function INTEL:CreateCluster(coordinate) + + -- Create new cluster + local cluster={} --#INTEL.Cluster + + cluster.index=self.clustercounter + cluster.coordinate=coordinate + cluster.threatlevelSum=0 + cluster.threatlevelMax=0 + cluster.size=0 + cluster.Contacts={} + + -- Add cluster. + table.insert(self.Clusters, cluster) + + -- Increase counter. + self.clustercounter=self.clustercounter+1 + + return cluster +end + +--- Add a contact to the cluster. +-- @param #INTEL self +-- @param #INTEL.Contact contact The contact. +-- @param #INTEL.Cluster cluster The cluster. +function INTEL:AddContactToCluster(contact, cluster) + + if contact and cluster then + + -- Add neighbour to cluster contacts. + table.insert(cluster.Contacts, contact) + + cluster.threatlevelSum=cluster.threatlevelSum+contact.threatlevel + + cluster.size=cluster.size+1 + end + +end + +--- Remove a contact from a cluster. +-- @param #INTEL self +-- @param #INTEL.Contact contact The contact. +-- @param #INTEL.Cluster cluster The cluster. +function INTEL:RemoveContactFromCluster(contact, cluster) + + if contact and cluster then + + for i,_contact in pairs(cluster.Contacts) do + local Contact=_contact --#INTEL.Contact + + if Contact.groupname==contact.groupname then + + cluster.threatlevelSum=cluster.threatlevelSum-contact.threatlevel + cluster.size=cluster.size-1 + + table.remove(cluster.Contacts, i) + + return + end + + end + + end + +end + +--- Calculate cluster threat level sum. +-- @param #INTEL self +-- @param #INTEL.Cluster cluster The cluster of contacts. +-- @return #number Sum of all threat levels of all groups in the cluster. +function INTEL:CalcClusterThreatlevelSum(cluster) + + local threatlevel=0 + + for _,_contact in pairs(cluster.Contacts) do + local contact=_contact --#INTEL.Contact + + threatlevel=threatlevel+contact.threatlevel + + end + + return threatlevel +end + +--- Calculate cluster threat level average. +-- @param #INTEL self +-- @param #INTEL.Cluster cluster The cluster of contacts. +-- @return #number Average of all threat levels of all groups in the cluster. +function INTEL:CalcClusterThreatlevelAverage(cluster) + + local threatlevel=self:CalcClusterThreatlevelSum(cluster) + threatlevel=threatlevel/cluster.size + + return threatlevel +end + +--- Calculate max cluster threat level. +-- @param #INTEL self +-- @param #INTEL.Cluster cluster The cluster of contacts. +-- @return #number Max threat levels of all groups in the cluster. +function INTEL:CalcClusterThreatlevelMax(cluster) + + local threatlevel=0 + + for _,_contact in pairs(cluster.Contacts) do + local contact=_contact --#INTEL.Contact + + if contact.threatlevel>threatlevel then + threatlevel=contact.threatlevel + end + + end + + return threatlevel +end + + +--- Check if contact is in any known cluster. +-- @param #INTEL self +-- @param #INTEL.Contact contact The contact. +-- @return #boolean If true, contact is in clusters +function INTEL:CheckContactInClusters(contact) + + for _,_cluster in pairs(self.Clusters) do + local cluster=_cluster --#INTEL.Cluster + + for _,_contact in pairs(cluster.Contacts) do + local Contact=_contact --#INTEL.Contact + + if Contact.groupname==contact.groupname then + return true + end + end + end + + return false +end + +--- Check if contact is close to any other contact this cluster. +-- @param #INTEL self +-- @param #INTEL.Contact contact The contact. +-- @param #INTEL.Cluster cluster The cluster the check. +-- @return #boolean If true, contact is connected to this cluster. +function INTEL:IsContactConnectedToCluster(contact, cluster) + + for _,_contact in pairs(cluster.Contacts) do + local Contact=_contact --#INTEL.Contact + + if Contact.groupname~=contact.groupname then + + local dist=Contact.position:Get2DDistance(contact.position) + + if dist<10*1000 then + return true + end + + end + + end + + return false +end + +--- Check if contact is close to any contact of known clusters. +-- @param #INTEL self +-- @param #INTEL.Contact contact The contact. +-- @return #INTEL.Cluster The cluster this contact is part of or nil otherwise. +function INTEL:IsContactPartOfAnyClusters(contact) + + for _,_cluster in pairs(self.Clusters) do + local cluster=_cluster --#INTEL.Cluster + + if self:IsContactConnectedToCluster(contact, cluster) then + return cluster + end + end + + return nil +end + +--- Get the cluster this contact belongs to (if any). +-- @param #INTEL self +-- @param #INTEL.Contact contact The contact. +-- @return #INTEL.Cluster The cluster this contact belongs to or nil. +function INTEL:GetClusterOfContact(contact) + + for _,_cluster in pairs(self.Clusters) do + local cluster=_cluster --#INTEL.Cluster + + for _,_contact in pairs(cluster.Contacts) do + local Contact=_contact --#INTEL.Contact + + if Contact.groupname==contact.groupname then + return cluster + end + end + end + + return nil +end + +--- Get the coordinate of a cluster. +-- @param #INTEL self +-- @param #INTEL.Cluster cluster The cluster. +-- @return Core.Point#COORDINATE The coordinate of this cluster. +function INTEL:GetClusterCoordinate(cluster) + + -- Init. + local x=0 ; local y=0 ; local z=0 ; local n=0 + + for _,_contact in pairs(cluster.Contacts) do + local contact=_contact --#INTEL.Contact + + x=x+contact.position.x + y=y+contact.position.y + z=z+contact.position.z + n=n+1 + + end + + -- Average. + x=x/n ; y=y/n ; z=z/n + + -- Create coordinate. + local coordinate=COORDINATE:New(x, y, z) + + return coordinate +end + +--- Get the coordinate of a cluster. +-- @param #INTEL self +-- @param #INTEL.Cluster cluster The cluster. +-- @param Core.Point#COORDINATE coordinate (Optional) Coordinate of the new cluster. Default is to calculate the current coordinate. +function INTEL:CheckClusterCoordinateChanged(cluster, coordinate) + + coordinate=coordinate or self:GetClusterCoordinate(cluster) + + local dist=cluster.coordinate:Get2DDistance(coordinate) + + if dist>1000 then + return true + else + return false + end + +end + + +--- Update cluster F10 marker. +-- @param #INTEL self +-- @param #INTEL.Cluster cluster The cluster. +-- @return #INTEL self +function INTEL:UpdateClusterMarker(cluster) + + -- Create a marker. + local text=string.format("Cluster #%d. Size %d, TLsum=%d", cluster.index, cluster.size, cluster.threatlevelSum) + + if not cluster.marker then + cluster.marker=MARKER:New(cluster.coordinate, text):ToAll() + else + + local refresh=false + + if cluster.marker.text~=text then + cluster.marker.text=text + refresh=true + end + + if cluster.marker.coordinate~=cluster.coordinate then + cluster.marker.coordinate=cluster.coordinate + refresh=true + end + + if refresh then + cluster.marker:Refresh() + end + + end + + return self +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/NavyGroup.lua b/Moose Development/Moose/Ops/NavyGroup.lua index bada43c13..ed7cb5029 100644 --- a/Moose Development/Moose/Ops/NavyGroup.lua +++ b/Moose Development/Moose/Ops/NavyGroup.lua @@ -19,15 +19,17 @@ -- @field #boolean turning If true, group is currently turning. -- @field #NAVYGROUP.IntoWind intowind Into wind info. -- @field #table Qintowind Queue of "into wind" turns. --- @field #boolean adinfinitum Resume route at first waypoint when final waypoint is reached. -- @field #number depth Ordered depth in meters. +-- @field #boolean collisionwarning If true, collition warning. +-- @field #boolean pathfindingOn If true, enable pathfining. +-- @field #boolean ispathfinding If true, group is currently path finding. -- @extends Ops.OpsGroup#OPSGROUP --- *Something must be left to chance; nothing is sure in a sea fight above all.* -- Horatio Nelson -- -- === -- --- ![Banner Image](..\Presentations\NAVYGROUP\NavyGroup_Main.jpg) +-- ![Banner Image](..\Presentations\OPS\NavyGroup\_Main.png) -- -- # The NAVYGROUP Concept -- @@ -55,6 +57,7 @@ NAVYGROUP = { -- @field #number Speed Speed in knots. -- @field #number Offset Offset angle in degrees. -- @field #number Id Unique ID of the turn. +-- @field Ops.OpsGroup#OPSGROUP.Waypoint waypoint Turn into wind waypoint. -- @field Core.Point#COORDINATE Coordinate Coordinate where we left the route. -- @field #number Heading Heading the boat will take in degrees. -- @field #boolean Open Currently active. @@ -63,12 +66,13 @@ NAVYGROUP = { --- NavyGroup version. -- @field #string version -NAVYGROUP.version="0.1.0" +NAVYGROUP.version="0.5.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - + +-- TODO: Collision warning. -- DONE: Detour, add temporary waypoint and resume route. -- DONE: Stop and resume route. -- DONE: Add waypoints. @@ -91,17 +95,19 @@ function NAVYGROUP:New(GroupName) self.lid=string.format("NAVYGROUP %s | ", self.groupname) -- Defaults - self:SetDefaultROE() self:SetDetection() + self:SetDefaultROE() + self:SetDefaultAlarmstate() self:SetPatrolAdInfinitum(true) + self:SetPathfinding(false) -- Add FSM transitions. -- From State --> Event --> To State self:AddTransition("*", "FullStop", "Holding") -- Hold position. self:AddTransition("*", "Cruise", "Cruising") -- Hold position. - self:AddTransition("*", "TurnIntoWind", "*") -- Command the group to turn into the wind. - self:AddTransition("*", "TurnIntoWindOver", "*") -- Turn into wind is over. + self:AddTransition("*", "TurnIntoWind", "IntoWind") -- Command the group to turn into the wind. + self:AddTransition("*", "TurnIntoWindOver", "Cruising") -- Turn into wind is over. self:AddTransition("*", "TurningStarted", "*") -- Group started turning. self:AddTransition("*", "TurningStopped", "*") -- Group stopped turning. @@ -109,6 +115,9 @@ function NAVYGROUP:New(GroupName) self:AddTransition("*", "Detour", "OnDetour") -- Make a detour to a coordinate and resume route afterwards. self:AddTransition("OnDetour", "DetourReached", "Cruising") -- Group reached the detour coordinate. + self:AddTransition("*", "CollisionWarning", "*") -- Collision warning. + self:AddTransition("*", "ClearAhead", "*") -- Clear ahead. + self:AddTransition("*", "Dive", "Diving") -- Command a submarine to dive. self:AddTransition("Diving", "Surface", "Cruising") -- Command a submarine to go to the surface. @@ -147,10 +156,14 @@ function NAVYGROUP:New(GroupName) self:HandleEvent(EVENTS.RemoveUnit, self.OnEventRemoveUnit) -- Start the status monitoring. - self:__CheckZone(-1) - self:__Status(-2) - self:__QueueUpdate(-3) - + self:__Status(-1) + + -- Start queue update timer. + self.timerQueueUpdate=TIMER:New(self._QueueUpdate, self):Start(2, 5) + + -- Start check zone timer. + self.timerCheckZone=TIMER:New(self._CheckInZones, self):Start(2, 60) + return self end @@ -171,34 +184,53 @@ function NAVYGROUP:SetPatrolAdInfinitum(switch) return self end ---- Group patrols ad inifintum. If the last waypoint is reached, it will go to waypoint one and repeat its route. +--- Enable/disable pathfinding. -- @param #NAVYGROUP self --- @param #number Speed Speed in knots. Default 70% of max speed. +-- @param #boolean Switch If true, enable pathfinding. -- @return #NAVYGROUP self -function NAVYGROUP:SetSpeedCruise(Speed) - - self.speedCruise=Speed and UTILS.KnotsToKmph(Speed) or self.speedmax*0.7 - - return self +function NAVYGROUP:SetPathfinding(Switch) + self.pathfindingOn=Switch end - --- Add a *scheduled* task. -- @param #NAVYGROUP self -- @param Core.Point#COORDINATE Coordinate Coordinate of the target. +-- @param #string Clock Time when to start the attack. -- @param #number Radius Radius in meters. Default 100 m. -- @param #number Nshots Number of shots to fire. Default 3. -- @param #number WeaponType Type of weapon. Default auto. --- @param #string Clock Time when to start the attack. -- @param #number Prio Priority of the task. +-- @return Ops.OpsGroup#OPSGROUP.Task The task data. function NAVYGROUP:AddTaskFireAtPoint(Coordinate, Radius, Nshots, WeaponType, Clock, Prio) local DCStask=CONTROLLABLE.TaskFireAtPoint(nil, Coordinate:GetVec2(), Radius, Nshots, WeaponType) - self:AddTask(DCStask, Clock, nil, Prio) + local task=self:AddTask(DCStask, Clock, nil, Prio) + return task end +--- Add a *waypoint* task. +-- @param #NAVYGROUP self +-- @param Core.Point#COORDINATE Coordinate Coordinate of the target. +-- @param Ops.OpsGroup#OPSGROUP.Waypoint Waypoint Where the task is executed. Default is next waypoint. +-- @param #number Radius Radius in meters. Default 100 m. +-- @param #number Nshots Number of shots to fire. Default 3. +-- @param #number WeaponType Type of weapon. Default auto. +-- @param #number Prio Priority of the task. +-- @return Ops.OpsGroup#OPSGROUP.Task The task table. +function NAVYGROUP:AddTaskWaypointFireAtPoint(Coordinate, Waypoint, Radius, Nshots, WeaponType, Prio) + + Waypoint=Waypoint or self:GetWaypointNext() + + local DCStask=CONTROLLABLE.TaskFireAtPoint(nil, Coordinate:GetVec2(), Radius, Nshots, WeaponType) + + local task=self:AddTaskWaypoint(DCStask, Waypoint, nil, Prio) + + return task +end + + --- Add a *scheduled* task. -- @param #NAVYGROUP self -- @param Wrapper.Group#GROUP TargetGroup Target group. @@ -206,12 +238,14 @@ end -- @param #number WeaponType Type of weapon. Default auto. -- @param #string Clock Time when to start the attack. -- @param #number Prio Priority of the task. +-- @return Ops.OpsGroup#OPSGROUP.Task The task data. function NAVYGROUP:AddTaskAttackGroup(TargetGroup, WeaponExpend, WeaponType, Clock, Prio) local DCStask=CONTROLLABLE.TaskAttackGroup(nil, TargetGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit, GroupAttack) - self:AddTask(DCStask, Clock, nil, Prio) - + local task=self:AddTask(DCStask, Clock, nil, Prio) + + return task end --- Add aircraft recovery time window and recovery case. @@ -227,14 +261,11 @@ function NAVYGROUP:CreateTurnIntoWind(starttime, stoptime, speed, uturn, offset) -- Absolute mission time in seconds. local Tnow=timer.getAbsTime() + -- Convert number to Clock. if starttime and type(starttime)=="number" then starttime=UTILS.SecondsToClock(Tnow+starttime) end - if stoptime and type(stoptime)=="number" then - stoptime=UTILS.SecondsToClock(Tnow+stoptime) - end - -- Input or now. starttime=starttime or UTILS.SecondsToClock(Tnow) @@ -242,7 +273,16 @@ function NAVYGROUP:CreateTurnIntoWind(starttime, stoptime, speed, uturn, offset) local Tstart=UTILS.ClockToSeconds(starttime) -- Set stop time. - local Tstop=UTILS.ClockToSeconds(stoptime or Tstart+90*60) + local Tstop=Tstart+90*60 + + if stoptime==nil then + Tstop=Tstart+90*60 + elseif type(stoptime)=="number" then + Tstop=Tstart+stoptime + else + Tstop=UTILS.ClockToSeconds(stoptime) + end + -- Consistancy check for timing. if Tstart>Tstop then @@ -250,7 +290,7 @@ function NAVYGROUP:CreateTurnIntoWind(starttime, stoptime, speed, uturn, offset) return self end if Tstop<=Tnow then - self:I(string.format("WARNING: Into wind stop time %s already over. Tnow=%s! Input rejected.", UTILS.SecondsToClock(Tstop), UTILS.SecondsToClock(Tnow))) + self:E(string.format("WARNING: Into wind stop time %s already over. Tnow=%s! Input rejected.", UTILS.SecondsToClock(Tstop), UTILS.SecondsToClock(Tnow))) return self end @@ -291,6 +331,36 @@ function NAVYGROUP:AddTurnIntoWind(starttime, stoptime, speed, uturn, offset) return recovery end + +--- Check if the group is currently holding its positon. +-- @param #NAVYGROUP self +-- @return #boolean If true, group was ordered to hold. +function NAVYGROUP:IsHolding() + return self:Is("Holding") +end + +--- Check if the group is currently cruising. +-- @param #NAVYGROUP self +-- @return #boolean If true, group cruising. +function NAVYGROUP:IsCruising() + return self:Is("Cruising") +end + +--- Check if the group is currently on a detour. +-- @param #NAVYGROUP self +-- @return #boolean If true, group is on a detour +function NAVYGROUP:IsOnDetour() + return self:Is("OnDetour") +end + + +--- Check if the group is currently diving. +-- @param #NAVYGROUP self +-- @return #boolean If true, group is currently diving. +function NAVYGROUP:IsDiving() + return self:Is("Diving") +end + --- Check if the group is currently turning. -- @param #NAVYGROUP self -- @return #boolean If true, group is currently turning. @@ -319,10 +389,10 @@ end function NAVYGROUP:onbeforeStatus(From, Event, To) if self:IsDead() then - self:I(self.lid..string.format("Onbefore Status DEAD ==> false")) + self:T(self.lid..string.format("Onbefore Status DEAD ==> false")) return false elseif self:IsStopped() then - self:I(self.lid..string.format("Onbefore Status STOPPED ==> false")) + self:T(self.lid..string.format("Onbefore Status STOPPED ==> false")) return false end @@ -335,118 +405,99 @@ function NAVYGROUP:onafterStatus(From, Event, To) -- FSM state. local fsmstate=self:GetState() - - --- - -- Detection - --- - -- Check if group has detected any units. - if self.detectionOn then - self:_CheckDetectedUnits() - end + if self:IsAlive() then - if self:IsAlive() and not self:IsDead() then - - -- Current heading and position of the carrier. - local hdg=self:GetHeading() - local pos=self:GetCoordinate() - local speed=self.group:GetVelocityKNOTS() + --- + -- Detection + --- + + -- Check if group has detected any units. + if self.detectionOn then + self:_CheckDetectedUnits() + end + + -- Update last known position, orientation, velocity. + self:_UpdatePosition() -- Check if group started or stopped turning. self:_CheckTurning() - -- Check water is ahead. - local collision=self:_CheckCollisionCoord(pos:Translate(self.collisiondist or 5000, hdg)) + local freepath=UTILS.NMToMeters(10) - self:_CheckTurnsIntoWind() - - if self.intowind then + -- Only check if not currently turning. + if not self:IsTurning() then - if timer.getAbsTime()>=self.intowind.Tstop then + -- Check free path ahead. + freepath=self:_CheckFreePath(freepath, 100) - self:TurnIntoWindOver() + if freepath<5000 then + + if not self.collisionwarning then + -- Issue a collision warning event. + self:CollisionWarning(freepath) + end + + if self.pathfindingOn and not self.ispathfinding then + self.ispathfinding=self:_FindPathToNextWaypoint() + end end - + end + + -- Check into wind queue. + self:_CheckTurnsIntoWind() + + -- Check if group got stuck. + self:_CheckStuck() + + if self.verbose>=1 then - -- Get number of tasks and missions. - local nTaskTot, nTaskSched, nTaskWP=self:CountRemainingTasks() - local nMissions=self:CountRemainingMissison() + -- Get number of tasks and missions. + local nTaskTot, nTaskSched, nTaskWP=self:CountRemainingTasks() + local nMissions=self:CountRemainingMissison() - -- Info text. - local text=string.format("State %s: Wp=%d/%d Speed=%.1f Heading=%03d intowind=%s turning=%s collision=%s Tasks=%d Missions=%d", - fsmstate, self.currentwp, #self.waypoints, speed, hdg, tostring(self:IsSteamingIntoWind()), tostring(self:IsTurning()), tostring(collision), nTaskTot, nMissions) - self:I(self.lid..text) + local intowind=self:IsSteamingIntoWind() and UTILS.SecondsToClock(self.intowind.Tstop-timer.getAbsTime(), true) or "N/A" + local turning=tostring(self:IsTurning()) + local alt=self.position.y + local speed=UTILS.MpsToKnots(self.velocity) + local speedExpected=UTILS.MpsToKnots(self.speedWp or 0) + + local wpidxCurr=self.currentwp + local wpuidCurr=0 + local wpidxNext=self:GetWaypointIndexNext() + local wpuidNext=0 + local wpDist=UTILS.MetersToNM(self:GetDistanceToWaypoint()) + local wpETA=UTILS.SecondsToClock(self:GetTimeToWaypoint(), true) + local roe=self:GetROE() or 0 + local als=self:GetAlarmstate() or 0 + + -- Info text. + local text=string.format("%s [ROE=%d,AS=%d, T/M=%d/%d]: Wp=%d[%d]-->%d[%d] (of %d) Dist=%.1f NM ETA=%s - Speed=%.1f (%.1f) kts, Depth=%.1f m, Hdg=%03d, Turn=%s Collision=%d IntoWind=%s", + fsmstate, roe, als, nTaskTot, nMissions, wpidxCurr, wpuidCurr, wpidxNext, wpuidNext, #self.waypoints, wpDist, wpETA, speed, speedExpected, alt, self.heading, turning, freepath, intowind) + self:I(self.lid..text) + + end else -- Info text. local text=string.format("State %s: Alive=%s", fsmstate, tostring(self:IsAlive())) - self:I(self.lid..text) + self:T(self.lid..text) end --- - -- Tasks + -- Tasks & Missions --- - - -- Task queue. - if #self.taskqueue>0 and self.verbose>1 then - local text=string.format("Tasks #%d", #self.taskqueue) - for i,_task in pairs(self.taskqueue) do - local task=_task --Ops.OpsGroup#OPSGROUP.Task - local name=task.description - local taskid=task.dcstask.id or "unknown" - local status=task.status - local clock=UTILS.SecondsToClock(task.time, true) - local eta=task.time-timer.getAbsTime() - local started=task.timestamp and UTILS.SecondsToClock(task.timestamp, true) or "N/A" - local duration=-1 - if task.duration then - duration=task.duration - if task.timestamp then - -- Time the task is running. - duration=task.duration-(timer.getAbsTime()-task.timestamp) - else - -- Time the task is supposed to run. - duration=task.duration - end - end - -- Output text for element. - if task.type==OPSGROUP.TaskType.SCHEDULED then - text=text..string.format("\n[%d] %s (%s): status=%s, scheduled=%s (%d sec), started=%s, duration=%d", i, taskid, name, status, clock, eta, started, duration) - elseif task.type==OPSGROUP.TaskType.WAYPOINT then - text=text..string.format("\n[%d] %s (%s): status=%s, waypoint=%d, started=%s, duration=%d, stopflag=%d", i, taskid, name, status, task.waypoint, started, duration, task.stopflag:Get()) - end - end - self:I(self.lid..text) - end - - --- - -- Missions - --- - - -- Current mission name. - if self.verbose>0 then - local Mission=self:GetMissionByID(self.currentmission) - - -- Current status. - local text=string.format("Missions %d, Current: %s", self:CountRemainingMissison(), Mission and Mission.name or "none") - for i,_mission in pairs(self.missionqueue) do - local mission=_mission --Ops.Auftrag#AUFTRAG - local Cstart= UTILS.SecondsToClock(mission.Tstart, true) - local Cstop = mission.Tstop and UTILS.SecondsToClock(mission.Tstop, true) or "INF" - text=text..string.format("\n[%d] %s (%s) status=%s (%s), Time=%s-%s, prio=%d wp=%s targets=%d", - i, tostring(mission.name), mission.type, mission:GetGroupStatus(self), tostring(mission.status), Cstart, Cstop, mission.prio, tostring(mission:GetGroupWaypointIndex(self)), mission:CountMissionTargets()) - end - self:I(self.lid..text) - end + + self:_PrintTaskAndMissionStatus() - - self:__Status(-10) + -- Next status update in 30 seconds. + self:__Status(-30) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -460,46 +511,47 @@ end -- @param #string To To state. -- @param #NAVYGROUP.Element Element The group element. function NAVYGROUP:onafterElementSpawned(From, Event, To, Element) - self:I(self.lid..string.format("Element spawned %s", Element.name)) + self:T(self.lid..string.format("Element spawned %s", Element.name)) -- Set element status. self:_UpdateStatus(Element, OPSGROUP.ElementStatus.SPAWNED) end ---- On after "ElementDead" event. --- @param #NAVYGROUP self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. --- @param #NAVYGROUP.Element Element The group element. -function NAVYGROUP:onafterElementDead(From, Event, To, Element) - self:T(self.lid..string.format("Element dead %s.", Element.name)) - - -- Set element status. - self:_UpdateStatus(Element, OPSGROUP.ElementStatus.DEAD) -end - --- On after "Spawned" event. -- @param #NAVYGROUP self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. function NAVYGROUP:onafterSpawned(From, Event, To) - self:I(self.lid..string.format("Group spawned!")) + self:T(self.lid..string.format("Group spawned!")) + + -- Update position. + self:_UpdatePosition() if self.ai then - - -- Set default ROE and ROT options. - self:SetOptionROE(self.roe) + + -- Set default ROE. + self:SwitchROE(self.option.ROE) + + -- Set default Alarm State. + self:SwitchAlarmstate(self.option.Alarm) + + -- Set TACAN beacon. + self:_SwitchTACAN() + + -- Turn ICLS on. + self:_SwitchICLS() + + -- Set radio. + if self.radioDefault then + self:SwitchRadio() + else + self:SetDefaultRadio(self.radio.Freq, self.radio.Modu, false) + end end - -- Get orientation. - self.Corientlast=self.group:GetUnit(1):GetOrientationX() - - self.depth=self.group:GetHeight() - -- Update route. self:Cruise() @@ -511,15 +563,15 @@ end -- @param #string Event Event. -- @param #string To To state. -- @param #number n Waypoint number. Default is next waypoint. --- @param #number Speed Speed in knots. Default cruise speed. --- @param #number Depth Depth in meters. Default 0 meters. +-- @param #number Speed Speed in knots to the next waypoint. +-- @param #number Depth Depth in meters to the next waypoint. function NAVYGROUP:onafterUpdateRoute(From, Event, To, n, Speed, Depth) -- Update route from this waypoint number onwards. - n=n or self:GetWaypointIndexNext(self.adinfinitum) + n=n or self:GetWaypointIndexNext() -- Debug info. - self:T(self.lid..string.format("FF Update route n=%d", n)) + self:T(self.lid..string.format("Update route n=%d", n)) -- Update waypoint tasks, i.e. inject WP tasks into waypoint table. self:_UpdateWaypointTasks(n) @@ -527,52 +579,63 @@ function NAVYGROUP:onafterUpdateRoute(From, Event, To, n, Speed, Depth) -- Waypoints. local waypoints={} - -- Depth for submarines. - local depth=Depth or 0 - - -- Get current speed in km/h. - local speed=Speed and UTILS.KnotsToKmph(Speed) or self.group:GetVelocityKMH() - - -- Current waypoint. - local current=self:GetCoordinate():WaypointNaval(speed, depth) - table.insert(waypoints, current) - -- Add remaining waypoints to route. + local depth=nil for i=n, #self.waypoints do - local wp=self.waypoints[i] - - -- Set speed. + + -- Waypoint. + local wp=self.waypoints[i] --Ops.OpsGroup#OPSGROUP.Waypoint + + -- Check if next wp. if i==n then + -- Speed. if Speed then + -- Take speed specified. wp.speed=UTILS.KnotsToMps(Speed) - elseif self.speedCruise then - wp.speed=UTILS.KmphToMps(self.speedCruise) - else - -- Take default waypoint speed. - end - - else - - if self.speedCruise then - wp.speed=UTILS.KmphToMps(self.speedCruise) else -- Take default waypoint speed. end + if Depth then + wp.alt=-Depth + elseif self.depth then + wp.alt=-self.depth + else + -- Take default waypoint alt. + end + + -- Current set speed in m/s. + self.speedWp=wp.speed + + -- Current set depth. + depth=wp.alt + + else + + -- Dive depth is applied to all other waypoints. + if self.depth then + wp.alt=-self.depth + else + -- Take default waypoint depth. + end + end - - -- Set depth. - wp.alt=-depth --Depth and -Depth or wp.alt - + -- Add waypoint. table.insert(waypoints, wp) end + + -- Current waypoint. + local current=self:GetCoordinate():WaypointNaval(UTILS.MpsToKmph(self.speedWp), depth) + table.insert(waypoints, 1, current) if #waypoints>1 then - - self:I(self.lid..string.format("Updateing route: WP=%d, Speed=%.1f knots, depth=%d meters", #self.waypoints-n+1, UTILS.KmphToKnots(speed), depth)) + + self:T(self.lid..string.format("Updateing route: WP %d-->%d-->%d (#%d), Speed=%.1f knots, Depth=%d m", + self.currentwp, n, #self.waypoints, #waypoints-1, UTILS.MpsToKnots(self.speedWp), depth)) + -- Route group to all defined waypoints remaining. self:Route(waypoints) @@ -580,16 +643,12 @@ function NAVYGROUP:onafterUpdateRoute(From, Event, To, n, Speed, Depth) else --- - -- No waypoints left + -- No waypoints left ==> Full Stop --- - self:I(self.lid..string.format("No waypoints left")) + self:E(self.lid..string.format("WARNING: No waypoints left ==> Full Stop!")) + self:FullStop() - if #self.waypoints>1 then - self:I(self.lid..string.format("Resuming route at first waypoint")) - self:__UpdateRoute(-1, 1, nil, self.depth) - end - end end @@ -602,29 +661,27 @@ end -- @param Core.Point#COORDINATE Coordinate Coordinate where to go. -- @param #number Speed Speed in knots. Default cruise speed. -- @param #number Depth Depth in meters. Default 0 meters. --- @param #number ResumeRoute If true, resume route after detour point was reached. +-- @param #number ResumeRoute If true, resume route after detour point was reached. If false, the group will stop at the detour point and wait for futher commands. function NAVYGROUP:onafterDetour(From, Event, To, Coordinate, Speed, Depth, ResumeRoute) - - -- Waypoints. - local waypoints={} -- Depth for submarines. - local depth=Depth or 0 + Depth=Depth or 0 - -- Get current speed in km/h. - local speed=Speed and UTILS.KnotsToKmph(Speed) or self.group:GetVelocityKMH() + -- Speed in knots. + Speed=Speed or self:GetSpeedCruise() - -- Current waypoint. - local current=self:GetCoordinate():WaypointNaval(speed, depth) - table.insert(waypoints, current) + -- ID of current waypoint. + local uid=self:GetWaypointCurrent().uid - -- At each waypoint report passing. - local Task=self.group:TaskFunction("NAVYGROUP._DetourReached", self, ResumeRoute) + -- Add waypoint after current. + local wp=self:AddWaypoint(Coordinate, Speed, uid, Depth, true) - local detour=Coordinate:WaypointNaval(speed, depth, {Task}) - table.insert(waypoints, detour) - - self:Route(waypoints) + -- Set if we want to resume route after reaching the detour waypoint. + if ResumeRoute then + wp.detour=1 + else + wp.detour=0 + end end @@ -634,27 +691,7 @@ end -- @param #string Event Event. -- @param #string To To state. function NAVYGROUP:onafterDetourReached(From, Event, To) - self:I(self.lid.."Group reached detour coordinate.") -end - ---- Function called when a group is passing a waypoint. ---@param Wrapper.Group#GROUP group Group that passed the waypoint ---@param #NAVYGROUP navygroup Navy group object. ---@param #boolean resume Resume route. -function NAVYGROUP._DetourReached(group, navygroup, resume) - - -- Debug message. - local text=string.format("Group reached detour coordinate") - navygroup:I(navygroup.lid..text) - - if resume then - local indx=navygroup:GetWaypointIndexNext(true) - local speed=navygroup:GetSpeedToWaypoint(indx) - navygroup:UpdateRoute(indx, speed, navygroup.depth) - end - - navygroup:DetourReached() - + self:T(self.lid.."Group reached detour coordinate.") end --- On after "TurnIntoWind" event. @@ -663,9 +700,6 @@ end -- @param #string Event Event. -- @param #string To To state. -- @param #NAVYGROUP.IntoWind Into wind parameters. --- @param #number Duration Duration in seconds. --- @param #number Speed Speed in knots. --- @param #boolean Uturn Return to the place we came from. function NAVYGROUP:onafterTurnIntoWind(From, Event, To, IntoWind) IntoWind.Heading=self:GetHeadingIntoWind(IntoWind.Offset) @@ -682,23 +716,28 @@ function NAVYGROUP:onafterTurnIntoWind(From, Event, To, IntoWind) -- Convert to knots. vwind=UTILS.MpsToKnots(vwind) - -- Speed of carrier in m/s but at least 2 knots. + -- Speed of carrier relative to wind but at least 2 knots. local speed=math.max(IntoWind.Speed-vwind, 2) -- Debug info. - self:I(self.lid..string.format("Steaming into wind: Heading=%03d Speed=%.1f Vwind=%.1f Vtot=%.1f knots, Tstart=%d Tstop=%d", IntoWind.Heading, speed, vwind, speed+vwind, IntoWind.Tstart, IntoWind.Tstop)) + self:T(self.lid..string.format("Steaming into wind: Heading=%03d Speed=%.1f Vwind=%.1f Vtot=%.1f knots, Tstart=%d Tstop=%d", IntoWind.Heading, speed, vwind, speed+vwind, IntoWind.Tstart, IntoWind.Tstop)) local distance=UTILS.NMToMeters(1000) - local wp={} - local coord=self:GetCoordinate() local Coord=coord:Translate(distance, IntoWind.Heading) - - wp[1]=coord:WaypointNaval(UTILS.KnotsToKmph(speed)) - wp[2]=Coord:WaypointNaval(UTILS.KnotsToKmph(speed)) - self:Route(wp) + -- ID of current waypoint. + local uid=self:GetWaypointCurrent().uid + + local wptiw=self:AddWaypoint(Coord, speed, uid) + wptiw.intowind=true + + IntoWind.waypoint=wptiw + + if IntoWind.Uturn and self.Debug then + IntoWind.Coordinate:MarkToAll("Return coord") + end end @@ -712,15 +751,23 @@ end -- @param #boolean Uturn Return to the place we came from. function NAVYGROUP:onafterTurnIntoWindOver(From, Event, To) + -- Debug message. + self:T2(self.lid.."Turn Into Wind Over!") + self.intowind.Over=true - self.intowind.Open=false + self.intowind.Open=false + + -- Remove additional waypoint. + self:RemoveWaypointByID(self.intowind.waypoint.uid) if self.intowind.Uturn then + self:T(self.lid.."Turn Into Wind Over ==> Uturn!") self:Detour(self.intowind.Coordinate, self:GetSpeedCruise(), 0, true) else - local indx=self:GetWaypointIndexNext(self.adinfinitum) + self:T(self.lid.."FF Turn Into Wind Over ==> Next WP!") + local indx=self:GetWaypointIndexNext() local speed=self:GetWaypointSpeed(indx) - self:UpdateRoute(indx, speed, self.depth) + self:__UpdateRoute(-1, indx, speed) end self.intowind=nil @@ -733,6 +780,7 @@ end -- @param #string Event Event. -- @param #string To To state. function NAVYGROUP:onafterFullStop(From, Event, To) + self:T(self.lid.."Full stop ==> holding") -- Get current position. local pos=self:GetCoordinate() @@ -750,10 +798,13 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #number Speed Speed in knots. +-- @param #number Speed Speed in knots until next waypoint is reached. Default is speed set for waypoint. function NAVYGROUP:onafterCruise(From, Event, To, Speed) - self:UpdateRoute(nil, Speed, self.depth) + -- No set depth. + self.depth=nil + + self:__UpdateRoute(-1, nil, Speed) end @@ -763,15 +814,16 @@ end -- @param #string Event Event. -- @param #string To To state. -- @param #number Depth Dive depth in meters. Default 50 meters. -function NAVYGROUP:onafterDive(From, Event, To, Depth) +-- @param #number Speed Speed in knots until next waypoint is reached. +function NAVYGROUP:onafterDive(From, Event, To, Depth, Speed) Depth=Depth or 50 - self:I(self.lid..string.format("Diving to %d meters", Depth)) + self:T(self.lid..string.format("Diving to %d meters", Depth)) self.depth=Depth - self:UpdateRoute(nil, nil, self.depth) + self:__UpdateRoute(-1, nil, Speed) end @@ -780,37 +832,43 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -function NAVYGROUP:onafterSurface(From, Event, To) +-- @param #number Speed Speed in knots until next waypoint is reached. +function NAVYGROUP:onafterSurface(From, Event, To, Speed) self.depth=0 - self:UpdateRoute(nil, nil, self.depth) + self:__UpdateRoute(-1, nil, Speed) end ---- On after "Dead" event. +--- On after "TurningStarted" event. -- @param #NAVYGROUP self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -function NAVYGROUP:onafterDead(From, Event, To) - self:I(self.lid..string.format("Group dead!")) +function NAVYGROUP:onafterTurningStarted(From, Event, To) + self.turning=true +end - -- Delete waypoints so they are re-initialized at the next spawn. - self.waypoints=nil - self.groupinitialized=false +--- On after "TurningStarted" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function NAVYGROUP:onafterTurningStopped(From, Event, To) + self.turning=false + self.collisionwarning=false +end - -- Cancel all mission. - for _,_mission in pairs(self.missionqueue) do - local mission=_mission --Ops.Auftrag#AUFTRAG - - self:MissionCancel(mission) - mission:GroupDead(self) - - end - - -- Stop - self:Stop() +--- On after "CollisionWarning" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number Distance Distance in meters where obstacle was detected. +function NAVYGROUP:onafterCollisionWarning(From, Event, To, Distance) + self:T(self.lid..string.format("Iceberg ahead in %d meters!", Distance or -1)) + self.collisionwarning=true end --- On after Start event. Starts the NAVYGROUP FSM and event handlers. @@ -820,20 +878,14 @@ end -- @param #string To To state. function NAVYGROUP:onafterStop(From, Event, To) - -- Check if group is still alive. - if self:IsAlive() then - -- Destroy group. No event is generated. - self.group:Destroy(false) - end - -- Handle events: self:UnHandleEvent(EVENTS.Birth) self:UnHandleEvent(EVENTS.Dead) self:UnHandleEvent(EVENTS.RemoveUnit) - - self.CallScheduler:Clear() - - self:I(self.lid.."STOPPED! Unhandled events, cleared scheduler and removed from database.") + + -- Call OPSGROUP function. + self:GetParent(self).onafterStop(self, From, Event, To) + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -893,8 +945,8 @@ function NAVYGROUP:OnEventDead(EventData) local element=self:GetElementByName(unitname) if element then - self:I(self.lid..string.format("EVENT: Element %s dead ==> dead", element.name)) - self:ElementDead(element) + self:T(self.lid..string.format("EVENT: Element %s dead ==> destroyed", element.name)) + self:ElementDestroyed(element) end end @@ -916,7 +968,7 @@ function NAVYGROUP:OnEventRemoveUnit(EventData) local element=self:GetElementByName(unitname) if element then - self:I(self.lid..string.format("EVENT: Element %s removed ==> dead", element.name)) + self:T(self.lid..string.format("EVENT: Element %s removed ==> dead", element.name)) self:ElementDead(element) end @@ -930,63 +982,54 @@ end --- Add an a waypoint to the route. -- @param #NAVYGROUP self --- @param Core.Point#COORDINATE coordinate The coordinate of the waypoint. Use COORDINATE:SetAltitude(altitude) to define the altitude. --- @param #number speed Speed in knots. Default is default cruise speed or 70% of max speed. --- @param #number wpnumber Waypoint number. Default at the end. --- @param #boolean updateroute If true or nil, call UpdateRoute. If false, no call. --- @return #number Waypoint index. -function NAVYGROUP:AddWaypoint(coordinate, speed, wpnumber, updateroute) +-- @param Core.Point#COORDINATE Coordinate The coordinate of the waypoint. Use COORDINATE:SetAltitude(altitude) to define the altitude. +-- @param #number Speed Speed in knots. Default is default cruise speed or 70% of max speed. +-- @param #number AfterWaypointWithID Insert waypoint after waypoint given ID. Default is to insert as last waypoint. +-- @param #number Depth Depth at waypoint in meters. Only for submarines. +-- @param #boolean Updateroute If true or nil, call UpdateRoute. If false, no call. +-- @return Ops.OpsGroup#OPSGROUP.Waypoint Waypoint table. +function NAVYGROUP:AddWaypoint(Coordinate, Speed, AfterWaypointWithID, Depth, Updateroute) - -- Waypoint number. Default is at the end. - wpnumber=wpnumber or #self.waypoints+1 + -- Check if a coordinate was given or at least a positionable. + if not Coordinate:IsInstanceOf("COORDINATE") then + if Coordinate:IsInstanceOf("POSITIONABLE") or Coordinate:IsInstanceOf("ZONE_BASE") then + self:T(self.lid.."WARNING: Coordinate is not a COORDINATE but a POSITIONABLE. Trying to get coordinate") + Coordinate=Coordinate:GetCoordinate() + else + self:E(self.lid.."ERROR: Coordinate is neither a COORDINATE nor any POSITIONABLE!") + return nil + end + end + -- Set waypoint index. + local wpnumber=self:GetWaypointIndexAfterID(AfterWaypointWithID) + + -- Check if final waypoint is still passed. if wpnumber>self.currentwp then self.passedfinalwp=false end -- Speed in knots. - speed=speed or self:GetSpeedCruise() - - -- Speed at waypoint. - local speedkmh=UTILS.KnotsToKmph(speed) + Speed=Speed or self:GetSpeedCruise() -- Create a Naval waypoint. - local wp=coordinate:WaypointNaval(speedkmh) - - -- Add to table. - table.insert(self.waypoints, wpnumber, wp) + local wp=Coordinate:WaypointNaval(UTILS.KnotsToKmph(Speed), Depth) + + -- Create waypoint data table. + local waypoint=self:_CreateWaypoint(wp) + + -- Add waypoint to table. + self:_AddWaypoint(waypoint, wpnumber) -- Debug info. - self:T(self.lid..string.format("Adding NAVAL waypoint #%d, speed=%.1f knots. Last waypoint passed was #%s. Total waypoints #%d", wpnumber, speed, self.currentwp, #self.waypoints)) - - -- Shift all waypoint tasks after the inserted waypoint. - for _,_task in pairs(self.taskqueue) do - local task=_task --Ops.OpsGroup#OPSGROUP.Task - if task.type==OPSGROUP.TaskType.WAYPOINT and task.waypoint and task.waypoint>=wpnumber then - task.waypoint=task.waypoint+1 - end - end + self:T(self.lid..string.format("Adding NAVAL waypoint index=%d uid=%d, speed=%.1f knots. Last waypoint passed was #%d. Total waypoints #%d", wpnumber, waypoint.uid, Speed, self.currentwp, #self.waypoints)) - -- Shift all mission waypoints after the inserted waypoint. - for _,_mission in pairs(self.missionqueue) do - local mission=_mission --Ops.Auftrag#AUFTRAG - - -- Get mission waypoint index. - local wpidx=mission:GetGroupWaypointIndex(self) - - -- Increase number if this waypoint lies in the future. - if wpidx and wpidx>=wpnumber then - mission:SetGroupWaypointIndex(self, wpidx+1) - end - - end - -- Update route. - if updateroute==nil or updateroute==true then + if Updateroute==nil or Updateroute==true then self:_CheckGroupDone(1) end - return wpnumber + return waypoint end --- Initialize group parameters. Also initializes waypoints if self.waypoints is nil. @@ -1022,38 +1065,24 @@ function NAVYGROUP:_InitGroup() self.isUncontrolled=false -- Max speed in km/h. - self.speedmax=self.group:GetSpeedMax() + self.speedMax=self.group:GetSpeedMax() - -- Cruise speed: 70% of max speed but within limit. - --self.speedCruise=self.speedmax*0.7 + -- Cruise speed: 70% of max speed. + self.speedCruise=self.speedMax*0.7 -- Group ammo. - --self.ammo=self:GetAmmoTot() + self.ammo=self:GetAmmoTot() - self.traveldist=0 - self.traveltime=timer.getAbsTime() - self.position=self:GetCoordinate() + -- Radio parameters from template. Default is set on spawn if not modified by the user. + self.radio.On=true -- Radio is always on for ships. + self.radio.Freq=tonumber(self.template.units[1].frequency)/1000000 + self.radio.Modu=tonumber(self.template.units[1].modulation) - -- Radio parameters from template. - self.radioOn=true -- Radio is always on for ships. - self.radioFreq=tonumber(self.template.units[1].frequency)/1000000 - self.radioModu=tonumber(self.template.units[1].modulation)/1000000 - - -- If not set by the use explicitly yet, we take the template values as defaults. - if not self.radioFreqDefault then - self.radioFreqDefault=self.radioFreq - self.radioModuDefault=self.radioModu - end - - -- Set default formation. - if not self.formationDefault then - if self.ishelo then - self.formationDefault=ENUMS.Formation.RotaryWing.EchelonLeft.D300 - else - self.formationDefault=ENUMS.Formation.FixedWing.EchelonLeft.Group - end - end + -- Set default formation. No really applicable for ships. + self.optionDefault.Formation="Off Road" + self.option.Formation=self.optionDefault.Formation + -- Get all units of the group. local units=self.group:GetUnits() for _,_unit in pairs(units) do @@ -1084,18 +1113,20 @@ function NAVYGROUP:_InitGroup() self.actype=unit:GetTypeName() -- Debug info. - local text=string.format("Initialized Navy Group %s:\n", self.groupname) - text=text..string.format("AC type = %s\n", self.actype) - text=text..string.format("Speed max = %.1f Knots\n", UTILS.KmphToKnots(self.speedmax)) - --text=text..string.format("Speed cruise = %.1f Knots\n", UTILS.KmphToKnots(self.speedCruise)) - text=text..string.format("Elements = %d\n", #self.elements) - text=text..string.format("Waypoints = %d\n", #self.waypoints) - text=text..string.format("Radio = %.1f MHz %s %s\n", self.radioFreq, UTILS.GetModulationName(self.radioModu), tostring(self.radioOn)) - --text=text..string.format("Ammo = %d (G=%d/R=%d/B=%d/M=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Bombs, self.ammo.Missiles) - text=text..string.format("FSM state = %s\n", self:GetState()) - text=text..string.format("Is alive = %s\n", tostring(self.group:IsAlive())) - text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated())) - self:I(self.lid..text) + if self.verbose>=1 then + local text=string.format("Initialized Navy Group %s:\n", self.groupname) + text=text..string.format("Unit type = %s\n", self.actype) + text=text..string.format("Speed max = %.1f Knots\n", UTILS.KmphToKnots(self.speedMax)) + text=text..string.format("Speed cruise = %.1f Knots\n", UTILS.KmphToKnots(self.speedCruise)) + text=text..string.format("Elements = %d\n", #self.elements) + text=text..string.format("Waypoints = %d\n", #self.waypoints) + text=text..string.format("Radio = %.1f MHz %s %s\n", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu), tostring(self.radio.On)) + text=text..string.format("Ammo = %d (G=%d/R=%d/M=%d/T=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Missiles, self.ammo.Torpedos) + text=text..string.format("FSM state = %s\n", self:GetState()) + text=text..string.format("Is alive = %s\n", tostring(self:IsAlive())) + text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated())) + self:I(self.lid..text) + end -- Init done. self.groupinitialized=true @@ -1105,70 +1136,101 @@ function NAVYGROUP:_InitGroup() return self end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Option Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Misc Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Check for possible collisions between two coordinates. -- @param #NAVYGROUP self --- @param Core.Point#COORDINATE coordto Coordinate to which the collision is check. --- @param Core.Point#COORDINATE coordfrom Coordinate from which the collision is check. --- @return #boolean If true, surface type ahead is not deep water. --- @return #number Max free distance in meters. -function NAVYGROUP:_CheckCollisionCoord(coordto, coordfrom) +-- @param #number DistanceMax Max distance in meters ahead to check. Default 5000. +-- @param #number dx +-- @return #number Free distance in meters. +function NAVYGROUP:_CheckFreePath(DistanceMax, dx) - -- Increment in meters. - local dx=100 + local distance=DistanceMax or 5000 + local dx=dx or 100 - -- From coordinate. Default 500 in front of the carrier. - local d=0 - if coordfrom then - d=0 - else - d=250 - coordfrom=self:GetCoordinate():Translate(d, self:GetHeading()) + -- If the group is turning, we cannot really tell anything about a possible collision. + if self:IsTurning() then + return distance + end + + -- Offset above sea level. + local offsetY=0.1 + + -- Current bug on Caucasus. LoS returns false. + if UTILS.GetDCSMap()==DCSMAP.Caucasus then + offsetY=5.01 + end + + -- Current coordinate. + --local coordinate=self:GetCoordinate():SetAltitude(offsetY, true) + + local vec3=self:GetVec3() + vec3.y=offsetY + + -- Current heading. + local heading=self:GetHeading() + + -- Check from 500 meters in front. + --coordinate=coordinate:Translate(500, heading, true) + + local function LoS(dist) + --local checkcoord=coordinate:Translate(dist, heading, true) + --return coordinate:IsLOS(checkcoord, offsetY) + local checkvec3=UTILS.VecTranslate(vec3, dist, heading) + local los=land.isVisible(vec3, checkvec3) + return los end - -- Distance between the two coordinates. - local dmax=coordfrom:Get2DDistance(coordto) + -- First check if everything is clear. + if LoS(DistanceMax) then + return DistanceMax + end + + local function check() + + local xmin=0 + local xmax=DistanceMax + + local Nmax=100 + local eps=100 - -- Direction. - local direction=coordfrom:HeadingTo(coordto) - - -- Scan path between the two coordinates. - local clear=true - while d<=dmax do - - -- Check point. - local cp=coordfrom:Translate(d, direction) - - -- Check if surface type is water. - if not cp:IsSurfaceTypeWater() then - - -- Debug mark points. - if self.Debug or true then - local st=cp:GetSurfaceType() - cp:MarkToAll(string.format("Collision check surface type %d", st)) + local N=1 + while N<=Nmax do + + local d=xmax-xmin + local x=xmin+d/2 + + local los=LoS(x) + + -- Debug message. + self:T2(self.lid..string.format("N=%d: xmin=%.1f xmax=%.1f x=%.1f d=%.3f los=%s", N, xmin, xmax, x, d, tostring(los))) + + if los and d<=eps then + return x end - - -- Collision WARNING! - clear=false - break + + if los then + xmin=x + else + xmax=x + end + + N=N+1 end - - -- Increase distance. - d=d+dx + + return 0 end - local text="" - if clear then - text=string.format("Path into direction %03d° is clear for the next %.1f NM.", direction, UTILS.MetersToNM(d)) - else - text=string.format("Detected obstacle at distance %.1f NM into direction %03d°.", UTILS.MetersToNM(d), direction) - end - self:T(self.lid..text) - return not clear, d + return check() end --- Check if group is turning. @@ -1180,10 +1242,10 @@ function NAVYGROUP:_CheckTurning() if unit and unit:IsAlive() then -- Current orientation of carrier. - local vNew=unit:GetOrientationX() + local vNew=self.orientX --unit:GetOrientationX() -- Last orientation from 30 seconds ago. - local vLast=self.Corientlast or vNew + local vLast=self.orientXLast -- We only need the X-Z plane. vNew.y=0 ; vLast.y=0 @@ -1191,9 +1253,6 @@ function NAVYGROUP:_CheckTurning() -- Angle between current heading and last time we checked ~30 seconds ago. local deltaLast=math.deg(math.acos(UTILS.VecDot(vNew,vLast)/UTILS.VecNorm(vNew)/UTILS.VecNorm(vLast))) - -- Last orientation becomes new orientation - self.Corientlast=vNew - -- Carrier is turning when its heading changed by at least two degrees since last check. local turning=math.abs(deltaLast)>=2 @@ -1217,47 +1276,6 @@ function NAVYGROUP:_CheckTurning() end ---- Check if group is done, i.e. --- --- * passed the final waypoint, --- * no current task --- * no current mission --- * number of remaining tasks is zero --- * number of remaining missions is zero --- --- @param #NAVYGROUP self --- @param #number delay Delay in seconds. -function NAVYGROUP:_CheckGroupDone(delay) - - if self:IsAlive() and self.ai then - - if delay and delay>0 then - -- Delayed call. - self:ScheduleOnce(delay, NAVYGROUP._CheckGroupDone, self) - else - - if self.passedfinalwp then - - if #self.waypoints>1 and self.adinfinitum then - - local speed=self:GetSpeedToWaypoint(1) - - -- Start route at first waypoint. - self:__UpdateRoute(-1, 1, speed, self.depth) - - end - - else - - self:UpdateRoute(nil, nil, self.depth) - - end - - end - - end - -end --- Check queued turns into wind. -- @param #NAVYGROUP self @@ -1304,28 +1322,14 @@ function NAVYGROUP:_CheckTurnsIntoWind() end end -end ---- Get default cruise speed. --- @param #NAVYGROUP self --- @return #number Cruise speed (>0) in knots. -function NAVYGROUP:GetSpeedCruise() - return UTILS.KmphToKnots(self.speedCruise or self.speedmax*0.7) -end - ---- Returns a non-zero speed to the next waypoint (even if the waypoint speed is zero). --- @param #NAVYGROUP self --- @param #number indx Waypoint index. --- @return #number Speed to next waypoint (>0) in knots. -function NAVYGROUP:GetSpeedToWaypoint(indx) - - local speed=self:GetWaypointSpeed(indx) + -- If into wind, check if over. + if self.intowind then + if timer.getAbsTime()>=self.intowind.Tstop then + self:TurnIntoWindOver() + end + end - if speed<=0.1 then - speed=self:GetSpeedCruise() - end - - return speed end --- Check queued turns into wind. @@ -1383,6 +1387,87 @@ function NAVYGROUP:GetHeadingIntoWind(Offset) return intowind end +--- Find free path to next waypoint. +-- @param #NAVYGROUP self +-- @return #boolean If true, a path was found. +function NAVYGROUP:_FindPathToNextWaypoint() + + -- Pathfinding A* + local astar=ASTAR:New() + + -- Current positon of the group. + local position=self:GetCoordinate() + + -- Next waypoint. + local wpnext=self:GetWaypointNext() + + -- Next waypoint coordinate. + local nextwp=wpnext.coordinate + + -- If we are currently turning into the wind... + if wpnext.intowind then + local hdg=self:GetHeading() + nextwp=position:Translate(UTILS.NMToMeters(20), hdg, true) + end + + local speed=UTILS.MpsToKnots(wpnext.speed) + + -- Set start coordinate. + astar:SetStartCoordinate(position) + + -- Set end coordinate. + astar:SetEndCoordinate(nextwp) + + -- Distance to next waypoint. + local dist=position:Get2DDistance(nextwp) + + local boxwidth=dist*2 + local spacex=dist*0.1 + local delta=dist/10 + + -- Create a grid of nodes. We only want nodes of surface type water. + astar:CreateGrid({land.SurfaceType.WATER}, boxwidth, spacex, delta, delta*2, self.Debug) + + -- Valid neighbour nodes need to have line of sight. + astar:SetValidNeighbourLoS(400) + + --- Function to find a path and add waypoints to the group. + local function findpath() + + -- Calculate path from start to end node. + local path=astar:GetPath(true, true) + + if path then + + -- Loop over nodes in found path. + local uid=self:GetWaypointCurrent().uid -- ID of current waypoint. + + for i,_node in ipairs(path) do + local node=_node --Core.Astar#ASTAR.Node + + -- Add waypoints along detour path to next waypoint. + local wp=self:AddWaypoint(node.coordinate, speed, uid) + wp.astar=true + + -- Update id so the next wp is added after this one. + uid=wp.uid + + -- Debug: smoke and mark path. + node.coordinate:MarkToAll(string.format("Path node #%d", i)) + + end + + return #path>0 + else + return false + end + + end + + return findpath() + +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index 65422467a..dc9030d4d 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -1,5 +1,6 @@ --- **Ops** - Generic group enhancement functions. -- +-- This class is **not** meant to be used itself by the end user. -- -- === -- @@ -26,7 +27,8 @@ -- @field #boolean isGround If true, group is some ground unit. -- @field #table waypoints Table of waypoints. -- @field #table waypoints0 Table of initial waypoints. --- @field #number currentwp Current waypoint. +-- @field #number currentwp Current waypoint index. This is the index of the last passed waypoint. +-- @field #boolean adinfinitum Resume route at first waypoint when final waypoint is reached. -- @field #table taskqueue Queue of tasks. -- @field #number taskcounter Running number of task ids. -- @field #number taskcurrent ID of current task. If 0, there is no current task assigned. @@ -36,58 +38,65 @@ -- @field #number currentmission The ID (auftragsnummer) of the currently assigned AUFTRAG. -- @field Core.Set#SET_UNIT detectedunits Set of detected units. -- @field #string attribute Generalized attribute. --- @field #number speedmax Max speed in km/h. +-- @field #number speedMax Max speed in km/h. -- @field #number speedCruise Cruising speed in km/h. +-- @field #number speedWp Speed to the next waypoint in m/s. -- @field #boolean passedfinalwp Group has passed the final waypoint. +-- @field #number wpcounter Running number counting waypoints. -- @field #boolean respawning Group is being respawned. -- @field Core.Set#SET_ZONE checkzones Set of zones. -- @field Core.Set#SET_ZONE inzones Set of zones in which the group is currently in. +-- @field Core.Timer#TIMER timerCheckZone Timer for check zones. +-- @field Core.Timer#TIMER timerQueueUpdate Timer for queue updates. -- @field #boolean groupinitialized If true, group parameters were initialized. -- @field #boolean detectionOn If true, detected units of the group are analyzed. -- @field Ops.Auftrag#AUFTRAG missionpaused Paused mission. +-- @field #number Ndestroyed Number of destroyed units. -- --- @field Core.Point#COORDINATE position Current position of the group. --- @field #number traveldist Distance traveled in meters. This is a lower bound! +-- @field Core.Point#COORDINATE coordinate Current coordinate. +-- +-- @field DCS#Vec3 position Position of the group at last status check. +-- @field DCS#Vec3 positionLast Backup of last position vec to monitor changes. +-- @field #number heading Heading of the group at last status check. +-- @field #number headingLast Backup of last heading to monitor changes. +-- @field DCS#Vec3 orientX Orientation at last status check. +-- @field DCS#Vec3 orientXLast Backup of last orientation to monitor changes. +-- @field #number traveldist Distance traveled in meters. This is a lower bound. -- @field #number traveltime Time. -- --- @field #number tacanChannelDefault The default TACAN channel. --- @field #string tacanMorseDefault The default TACAN morse code. --- @field #number tacanChannel The currenly used TACAN channel. --- @field #string tacanMorse The currently used TACAN morse code. --- @field #boolean tacanOn If true, TACAN is currently active. --- @field Wrapper.Unit#UNIT tacanBeacon The unit acting as TACAN beacon. +-- @field Core.Astar#ASTAR Astar path finding. +-- @field #boolean ispathfinding If true, group is on pathfinding route. -- --- @field #number radioFreqDefault Default radio frequency in MHz. --- @field #number radioFreq Currently used radio frequency in MHz. --- @field #number radioModuDefault Default Radio modulation `radio.modulation.AM` or `radio.modulation.FM`. --- @field #number radioModu Currently used radio modulation `radio.modulation.AM` or `radio.modulation.FM`. --- @field #boolean radioOn If true, radio is currently turned on. +-- @field #OPSGROUP.Radio radio Current radio settings. +-- @field #OPSGROUP.Radio radioDefault Default radio settings. -- @field Core.RadioQueue#RADIOQUEUE radioQueue Radio queue. -- --- @field #boolean eplrsDefault Default EPLRS data link setting. --- @field #boolean eplrs If true, EPLRS data link is on. +-- @field #OPSGROUP.Beacon tacan Current TACAN settings. +-- @field #OPSGROUP.Beacon tacanDefault Default TACAN settings. -- --- @field #string roeDefault Default ROE setting. --- @field #string rotDefault Default ROT setting. --- @field #string roe Current ROE setting. --- @field #string rot Current ROT setting. +-- @field #OPSGROUP.Beacon icls Current ICLS settings. +-- @field #OPSGROUP.Beacon iclsDefault Default ICLS settings. -- --- @field #number formationDefault Default formation setting. --- @field #number formation Current formation setting. +-- @field #OPSGROUP.Option option Current optional settings. +-- @field #OPSGROUP.Option optionDefault Default option settings. +-- +-- @field #OPSGROUP.Callsign callsign Current callsign settings. +-- @field #OPSGROUP.Callsign callsignDefault Default callsign settings. -- -- @extends Core.Fsm#FSM ---- *Something must be left to chance; nothing is sure in a sea fight above all.* --- Horatio Nelson +--- *A small group of determined and like-minded people can change the course of history.* --- Mahatma Gandhi -- -- === -- --- ![Banner Image](..\Presentations\OPSGROUP\OpsGroup_Main.jpg) +-- ![Banner Image](..\Presentations\OPS\OpsGroup\_Main.png) -- -- # The OPSGROUP Concept -- --- The OPSGROUP class contains common functions used by other classes such as FLIGHGROUP and NAVYGROUP. +-- The OPSGROUP class contains common functions used by other classes such as FLIGHGROUP, NAVYGROUP and ARMYGROUP. +-- Those classes inherit everything of this class and extend it with features specific to their unit category. -- --- This class is **not** meant to be used itself by the end user. +-- This class is **NOT** meant to be used by the end user itself. -- -- -- @field #OPSGROUP @@ -117,6 +126,14 @@ OPSGROUP = { inzones = nil, groupinitialized = nil, respawning = nil, + wpcounter = 1, + radio = {}, + option = {}, + optionDefault = {}, + tacan = {}, + icls = {}, + callsign = {}, + Ndestroyed = 0, } --- Status of group element. @@ -187,6 +204,46 @@ OPSGROUP.TaskType={ -- @field DCS#Task DCStask DCS task structure table. -- @field #number WaypointIndex Waypoint number at which the enroute task is added. +--- Beacon data. +-- @type OPSGROUP.Beacon +-- @field #number Channel Channel. +-- @field #number Morse Morse Code. +-- @field #string Band Band "X" or "Y" for TACAN beacon. +-- @field #string BeaconName Name of the unit acting as beacon. +-- @field Wrapper.Unit#UNIT BeaconUnit Unit object acting as beacon. +-- @field #boolean On If true, beacon is on, if false, beacon is turned off. If nil, has not been used yet. + +--- Radio data. +-- @type OPSGROUP.Radio +-- @field #number Freq Frequency +-- @field #number Modu Modulation. +-- @field #boolean On If true, radio is on, if false, radio is turned off. If nil, has not been used yet. + +--- Callsign data. +-- @type OPSGROUP.Callsign +-- @field #number NumberSquad Squadron number corresponding to a name like "Uzi". +-- @field #number NumberGroup Group number. First number after name, e.g. "Uzi-**1**-1". +-- @field #number NumberElement Element number.Second number after name, e.g. "Uzi-1-**1**" +-- @field #string NameSquad Name of the squad, e.g. "Uzi". +-- @field #string NameElement Name of group element, e.g. Uzi 11. + +--- Option data. +-- @type OPSGROUP.Option +-- @field #number ROE Rule of engagement. +-- @field #number ROT Reaction on threat. +-- @field #number Alarm Alarm state. +-- @field #number Formation Formation. +-- @field #boolean EPLRS data link. +-- @field #boolean Disperse Disperse under fire. + + +--- Weapon range data. +-- @type OPSGROUP.WeaponData +-- @field #number BitType Type of weapon. +-- @field #number RangeMin Min range in meters. +-- @field #number RangeMax Max range in meters. +-- @field #number ReloadTime Time to reload in seconds. + --- Ammo data. -- @type OPSGROUP.Ammo -- @field #number Total Total amount of ammo. @@ -201,9 +258,30 @@ OPSGROUP.TaskType={ -- @field #number MissilesCR Amount of cruise missiles. -- @field #number MissilesBM Amount of ballistic missiles. +--- Waypoint data. +-- @type OPSGROUP.Waypoint +-- @field #number uid Waypoint's unit id, which is a running number. +-- @field #number speed Speed in m/s. +-- @field #number alt Altitude in meters. For submaries use negative sign for depth. +-- @field #string action Waypoint action (turning point, etc.). Ground groups have the formation here. +-- @field #table task Waypoint DCS task combo. +-- @field #string type Waypoint type. +-- @field #string name Waypoint description. Shown in the F10 map. +-- @field #number x Waypoint x-coordinate. +-- @field #number y Waypoint y-coordinate. +-- @field #boolean detour If true, this waypoint is not part of the normal route. +-- @field #boolean intowind If true, this waypoint is a turn into wind route point. +-- @field #boolean astar If true, this waypint was found by A* pathfinding algorithm. +-- @field #number npassed Number of times a groups passed this waypoint. +-- @field Core.Point#COORDINATE coordinate Waypoint coordinate. +-- @field Core.Point#COORDINATE roadcoord Closest point to road. +-- @field #number roaddist Distance to closest point on road. +-- @field Wrapper.Marker#MARKER marker Marker on the F10 map. +-- @field #string formation Ground formation. Similar to action but on/off road. + --- NavyGroup version. -- @field #string version -OPSGROUP.version="0.1.0" +OPSGROUP.version="0.5.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -256,7 +334,6 @@ function OPSGROUP:New(Group) self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. self:AddTransition("*", "Status", "*") -- Status update. - self:AddTransition("*", "QueueUpdate", "*") -- Update task and mission queues. self:AddTransition("*", "UpdateRoute", "*") -- Update route of group. Only if airborne. self:AddTransition("*", "Respawn", "*") -- Respawn group. @@ -276,7 +353,6 @@ function OPSGROUP:New(Group) self:AddTransition("*", "OutOfBombs", "*") -- Group is out of bombs. self:AddTransition("*", "OutOfMissiles", "*") -- Group is out of missiles. - self:AddTransition("*", "CheckZone", "*") -- Check if group enters/leaves a certain zone. self:AddTransition("*", "EnterZone", "*") -- Group entered a certain zone. self:AddTransition("*", "LeaveZone", "*") -- Group leaves a certain zone. @@ -293,6 +369,7 @@ function OPSGROUP:New(Group) self:AddTransition("*", "MissionDone", "*") -- Mission is over. self:AddTransition("*", "ElementSpawned", "*") -- An element was spawned. + self:AddTransition("*", "ElementDestroyed", "*") -- An element was destroyed. self:AddTransition("*", "ElementDead", "*") -- An element is dead. ------------------------ @@ -342,9 +419,38 @@ function OPSGROUP:GetLifePoints() end end ---- Set detection on or off. + +--- Set verbosity level. -- @param #OPSGROUP self --- @param #boolean Switch If true, detection is on. If false or nil, detection is off. Default is off. +-- @param #number VerbosityLevel Level of output (higher=more). Default 0. +-- @return #OPSGROUP self +function OPSGROUP:SetVerbosity(VerbosityLevel) + self.verbose=VerbosityLevel or 0 + return self +end + +--- Set default cruise speed. +-- @param #OPSGROUP self +-- @param #number Speed Speed in knots. +-- @return #OPSGROUP self +function OPSGROUP:SetDefaultSpeed(Speed) + if Speed then + self.speedCruise=UTILS.KnotsToKmph(Speed) + end + return self +end + +--- Get default cruise speed. +-- @param #OPSGROUP self +-- @return #number Cruise speed (>0) in knots. +function OPSGROUP:GetSpeedCruise() + return UTILS.KmphToKnots(self.speedCruise or self.speedMax*0.7) +end + +--- Set detection on or off. +-- If detection is on, detected targets of the group will be evaluated and FSM events triggered. +-- @param #OPSGROUP self +-- @param #boolean Switch If `true`, detection is on. If `false` or `nil`, detection is off. Default is off. -- @return #OPSGROUP self function OPSGROUP:SetDetection(Switch) self.detectionOn=Switch @@ -372,6 +478,47 @@ function OPSGROUP:AddCheckZone(CheckZone) return self end + +--- Add a zone that triggers and event if the group enters or leaves any of the zones. +-- @param #OPSGROUP self +-- @param #number RangeMin +-- @param #number RangeMax +-- @param #number BitType +-- @return #OPSGROUP self +function OPSGROUP:AddWeaponRange(RangeMin, RangeMax, BitType) + + RangeMin=(RangeMin or 0)*1000 + RangeMax=(RangeMax or 10)*1000 + + local weapon={} --#OPSGROUP.WeaponData + + weapon.BitType=BitType or ENUMS.WeaponFlag.Auto + weapon.RangeMax=RangeMax + weapon.RangeMin=RangeMin + + self.weaponData=self.weaponData or {} + self.weaponData[weapon.BitType]=weapon + + return self +end + +--- +-- @param #OPSGROUP self +-- @param #number BitType +-- @return #OPSGROUP.WeaponData Weapon range data. +function OPSGROUP:GetWeaponData(BitType) + + BitType=BitType or ENUMS.WeaponFlag.Auto + + if self.weaponData[BitType] then + return self.weaponData[BitType] + else + return self.weaponData[ENUMS.WeaponFlag.Auto] + end + +end + + --- Get set of detected units. -- @param #OPSGROUP self -- @return Core.Set#SET_UNIT Set of detected units. @@ -393,15 +540,238 @@ function OPSGROUP:GetName() return self.groupname end +--- Get DCS GROUP object. +-- @param #OPSGROUP self +-- @return DCS#Group DCS group object. +function OPSGROUP:GetDCSGroup() + local DCSGroup=Group.getByName(self.groupname) + return DCSGroup +end + +--- Get MOOSE UNIT object. +-- @param #OPSGROUP self +-- @param #number UnitNumber Number of the unit in the group. Default first unit. +-- @return Wrapper.Unit#UNIT The MOOSE UNIT object. +function OPSGROUP:GetUnit(UnitNumber) + + local DCSUnit=self:GetDCSUnit(UnitNumber) + + if DCSUnit then + local unit=UNIT:Find(DCSUnit) + return unit + end + + return nil +end + +--- Get DCS GROUP object. +-- @param #OPSGROUP self +-- @param #number UnitNumber Number of the unit in the group. Default first unit. +-- @return DCS#Unit DCS group object. +function OPSGROUP:GetDCSUnit(UnitNumber) + + local DCSGroup=self:GetDCSGroup() + + if DCSGroup then + local unit=DCSGroup:getUnit(UnitNumber or 1) + return unit + end + + return nil +end + +--- Get DCS units. +-- @param #OPSGROUP self +-- @return #list DCS units. +function OPSGROUP:GetDCSUnits() + + local DCSGroup=self:GetDCSGroup() + + if DCSGroup then + local units=DCSGroup:getUnits() + return units + end + + return nil +end + +--- Despawn the group. The whole group is despawned and (optionally) a "Remove Unit" event is generated for all current units of the group. +-- @param #OPSGROUP self +-- @param #number Delay Delay in seconds before the group will be despawned. Default immediately. +-- @param #boolean NoEventRemoveUnit If true, no event "Remove Unit" is generated. +-- @return #OPSGROUP self +function OPSGROUP:Despawn(Delay, NoEventRemoveUnit) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP.Despawn, self, 0, NoEventRemoveUnit) + else + + local DCSGroup=self:GetDCSGroup() + + if DCSGroup then + + -- Destroy DCS group. + DCSGroup:destroy() + + if not NoEventRemoveUnit then + + -- Get all units. + local units=self:GetDCSUnits() + + -- Create a "Remove Unit" event. + local EventTime=timer.getTime() + for i=1,#units do + self:CreateEventRemoveUnit(EventTime, units[i]) + end + + end + end + end + + return self +end + +--- Destroy group. The whole group is despawned and a *Unit Lost* for aircraft or *Dead* event for ground/naval units is generated for all current units. +-- @param #OPSGROUP self +-- @param #number Delay Delay in seconds before the group will be destroyed. Default immediately. +-- @return #OPSGROUP self +function OPSGROUP:Destroy(Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP.Destroy, self) + else + + local DCSGroup=self:GetDCSGroup() + + if DCSGroup then + + self:T(self.lid.."Destroying group") + + -- Destroy DCS group. + DCSGroup:destroy() + + -- Get all units. + local units=self:GetDCSUnits() + + -- Create a "Unit Lost" event. + local EventTime=timer.getTime() + for i=1,#units do + if self.isAircraft then + self:CreateEventUnitLost(EventTime, units[i]) + else + self:CreateEventDead(EventTime, units[i]) + end + end + end + + end + + return self +end + +--- Despawn an element/unit of the group. +-- @param #OPSGROUP self +-- @param #OPSGROUP.Element Element The element that will be despawned. +-- @param #number Delay Delay in seconds before the element will be despawned. Default immediately. +-- @param #boolean NoEventRemoveUnit If true, no event "Remove Unit" is generated. +-- @return #OPSGROUP self +function OPSGROUP:DespawnElement(Element, Delay, NoEventRemoveUnit) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP.DespawnElement, self, Element, 0, NoEventRemoveUnit) + else + + if Element then + + -- Get DCS unit object. + local DCSunit=Unit.getByName(Element.name) + + if DCSunit then + + -- Destroy object. + DCSunit:destroy() + + -- Create a remove unit event. + if not NoEventRemoveUnit then + self:CreateEventRemoveUnit(timer.getTime(), DCSunit) + end + + end + + end + + end + + return self +end + + +--- Get current 3D vector of the group. +-- @param #OPSGROUP self +-- @return DCS#Vec3 Vector with x,y,z components. +function OPSGROUP:GetVec3() + if self:IsExist() then + + local unit=self:GetDCSUnit() + + if unit then + local vec3=unit:getPoint() + + return vec3 + end + + end + return nil +end + --- Get current coordinate of the group. -- @param #OPSGROUP self +-- @param #boolean NewObject Create a new coordiante object. -- @return Core.Point#COORDINATE The coordinate (of the first unit) of the group. -function OPSGROUP:GetCoordinate() - if self:IsAlive()~=nil then - return self.group:GetCoordinate() +function OPSGROUP:GetCoordinate(NewObject) + + local vec3=self:GetVec3() + + if vec3 then + + self.coordinate=self.coordinate or COORDINATE:New(0,0,0) + + self.coordinate.x=vec3.x + self.coordinate.y=vec3.y + self.coordinate.z=vec3.z + + if NewObject then + local coord=COORDINATE:NewFromCoordinate(self.coordinate) + else + return self.coordinate + end else self:E(self.lid.."WARNING: Group is not alive. Cannot get coordinate!") end + + return nil +end + +--- Get current velocity of the group. +-- @param #OPSGROUP self +-- @return #number Velocity in m/s. +function OPSGROUP:GetVelocity() + if self:IsExist() then + + local unit=self:GetDCSUnit(1) + + if unit then + + local velvec3=unit:getVelocity() + + local vel=UTILS.VecNorm(velvec3) + + return vel + + end + else + self:E(self.lid.."WARNING: Group does not exist. Cannot get velocity!") + end return nil end @@ -409,76 +779,69 @@ end -- @param #OPSGROUP self -- @return #number Current heading of the group in degrees. function OPSGROUP:GetHeading() - if self:IsAlive()~=nil then - return self.group:GetHeading() + + if self:IsExist() then + + local unit=self:GetDCSUnit() + + if unit then + + local pos=unit:getPosition() + + local heading=math.atan2(pos.x.z, pos.x.x) + + if heading<0 then + heading=heading+ 2*math.pi + end + + heading=math.deg(heading) + + return heading + end + else - self:E(self.lid.."WARNING: Group is not alive. Cannot get heading!") + self:E(self.lid.."WARNING: Group does not exist. Cannot get heading!") end + return nil end ---- Get next waypoint index. +--- Get current orientation of the first unit in the group. -- @param #OPSGROUP self --- @param #boolean cyclic If true, return first waypoint if last waypoint was reached. --- @return #number Next waypoint index. -function OPSGROUP:GetWaypointIndexNext(cyclic) +-- @return DCS#Vec3 Orientation X parallel to where the "nose" is pointing. +-- @return DCS#Vec3 Orientation Y pointing "upwards". +-- @return DCS#Vec3 Orientation Z perpendicular to the "nose". +function OPSGROUP:GetOrientation() - local n=math.min(self.currentwp+1, #self.waypoints) + if self:IsExist() then - if cyclic and self.currentwp==#self.waypoints then - n=1 + local unit=self:GetDCSUnit() + + if unit then + + local pos=unit:getPosition() + + return pos.x, pos.y, pos.z + end + + else + self:E(self.lid.."WARNING: Group does not exist. Cannot get orientation!") end - return n -end - ---- Get waypoint speed. --- @param #OPSGROUP self --- @param #number indx Waypoint index. --- @return #number Speed set at waypoint in knots. -function OPSGROUP:GetWaypointSpeed(indx) - - local waypoint=self:GetWaypoint(indx) - - if waypoint then - return UTILS.MpsToKnots(waypoint.speed) - end - return nil end ---- Get waypoint. +--- Get current orientation of the first unit in the group. -- @param #OPSGROUP self --- @param #number indx Waypoint index. --- @return #table Waypoint table. -function OPSGROUP:GetWaypoint(indx) - return self.waypoints[indx] -end +-- @return DCS#Vec3 Orientation X parallel to where the "nose" is pointing. +function OPSGROUP:GetOrientationX() ---- Get final waypoint. --- @param #OPSGROUP self --- @return #table Waypoint table. -function OPSGROUP:GetWaypointFinal() - return self.waypoints[#self.waypoints] -end - ---- Get next waypoint. --- @param #OPSGROUP self --- @param #boolean cyclic If true, return first waypoint if last waypoint was reached. --- @return #table Waypoint table. -function OPSGROUP:GetWaypointNext(cyclic) - - local n=self:GetWaypointIndexNext(cyclic) + local X,Y,Z=self:GetOrientation() - return self.waypoints[n] + return X end ---- Get current waypoint. --- @param #OPSGROUP self --- @return #table Waypoint table. -function OPSGROUP:GetWaypointCurrent() - return self.waypoints[self.currentwp] -end + --- Check if task description is unique. -- @param #OPSGROUP self @@ -497,29 +860,6 @@ function OPSGROUP:CheckTaskDescriptionUnique(description) return true end ---- Get coordinate of next waypoint of the group. --- @param #OPSGROUP self --- @return Core.Point#COORDINATE Coordinate of the next waypoint. --- @return #number Number of waypoint. -function OPSGROUP:GetNextWaypointCoordinate() - - -- Next waypoint. - local n=self:GetWaypointIndexNext(cyclic) - - -- Next waypoint. - local wp=self.waypoints[n] - - return self:GetWaypointCoordinate(wp) -end - ---- Get next waypoint coordinates. --- @param #OPSGROUP self --- @param #table wp Waypoint table. --- @return Core.Point#COORDINATE Coordinate of the next waypoint. -function OPSGROUP:GetWaypointCoordinate(wp) - -- TODO: move this to COORDINATE class. - return COORDINATE:New(wp.x, wp.alt, wp.y) -end --- Activate a *late activated* group. -- @param #OPSGROUP self @@ -574,13 +914,37 @@ function OPSGROUP:SelfDestruction(Delay, ExplosionPower) end + +--- Check if group is exists. +-- @param #OPSGROUP self +-- @return #boolean If true, the group exists or false if the group does not exist. If nil, the DCS group could not be found. +function OPSGROUP:IsExist() + + local DCSGroup=self:GetDCSGroup() + + if DCSGroup then + local exists=DCSGroup:isExist() + return exists + end + + return nil +end + +--- Check if group is activated. +-- @param #OPSGROUP self +-- @return #boolean If true, the group exists or false if the group does not exist. If nil, the DCS group could not be found. +function OPSGROUP:IsActive() + +end + --- Check if group is alive. -- @param #OPSGROUP self -- @return #boolean *true* if group is exists and is activated, *false* if group is exist but is NOT activated. *nil* otherwise, e.g. the GROUP object is *nil* or the group is not spawned yet. function OPSGROUP:IsAlive() if self.group then - return self.group:IsAlive() + local alive=self.group:IsAlive() + return alive end return nil @@ -623,15 +987,362 @@ end --- Check if this group is currently "uncontrolled" and needs to be "started" to begin its route. -- @param #OPSGROUP self --- @return #boolean If this group uncontrolled. +-- @return #boolean If true, this group uncontrolled. function OPSGROUP:IsUncontrolled() return self.isUncontrolled end +--- Check if this group has passed its final waypoint. +-- @param #OPSGROUP self +-- @return #boolean If true, this group has passed the final waypoint. +function OPSGROUP:HasPassedFinalWaypoint() + return self.passedfinalwp +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Waypoint Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Get the waypoints. +-- @param #OPSGROUP self +-- @return #table Table of all waypoints. +function OPSGROUP:GetWaypoints() + return self.waypoints +end + +--- Mark waypoints on F10 map. +-- @param #OPSGROUP self +-- @param #number Duration Duration in seconds how long the waypoints are displayed before they are automatically removed. Default is that they are never removed. +-- @return #OPSGROUP self +function OPSGROUP:MarkWaypoints(Duration) + + for i,_waypoint in pairs(self.waypoints or {}) do + local waypoint=_waypoint --#OPSGROUP.Waypoint + + local text=string.format("Waypoint ID=%d of %s", waypoint.uid, self.groupname) + text=text..string.format("\nSpeed=%.1f kts, Alt=%d ft (%s)", UTILS.MpsToKnots(waypoint.speed), UTILS.MetersToFeet(waypoint.alt), "BARO") + + if waypoint.marker then + if waypoint.marker.text~=text then + waypoint.marker.text=text + end + + else + waypoint.marker=MARKER:New(waypoint.coordinate, text):ToCoalition(self:GetCoalition()) + end + end + + + if Duration then + self:RemoveWaypointMarkers(Duration) + end + + return self +end + +--- Remove waypoints markers on the F10 map. +-- @param #OPSGROUP self +-- @param #number Delay Delay in seconds before the markers are removed. Default is immediately. +-- @return #OPSGROUP self +function OPSGROUP:RemoveWaypointMarkers(Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP.RemoveWaypointMarkers, self) + else + + for i,_waypoint in pairs(self.waypoints or {}) do + local waypoint=_waypoint --#OPSGROUP.Waypoint + + if waypoint.marker then + waypoint.marker:Remove() + end + end + + end + + return self +end + + +--- Get the waypoint from its unique ID. +-- @param #OPSGROUP self +-- @param #number uid Waypoint unique ID. +-- @return #OPSGROUP.Waypoint Waypoint data. +function OPSGROUP:GetWaypointByID(uid) + + for _,_waypoint in pairs(self.waypoints) do + local waypoint=_waypoint --#OPSGROUP.Waypoint + if waypoint.uid==uid then + return waypoint + end + end + + return nil +end + +--- Get the waypoint from its index. +-- @param #OPSGROUP self +-- @param #number index Waypoint index. +-- @return #OPSGROUP.Waypoint Waypoint data. +function OPSGROUP:GetWaypointByIndex(index) + + for i,_waypoint in pairs(self.waypoints) do + local waypoint=_waypoint --#OPSGROUP.Waypoint + if i==index then + return waypoint + end + end + + return nil +end + +--- Get the waypoint index (its position in the current waypoints table). +-- @param #OPSGROUP self +-- @param #number uid Waypoint unique ID. +-- @return #OPSGROUP.Waypoint Waypoint data. +function OPSGROUP:GetWaypointIndex(uid) + + if uid then + for i,_waypoint in pairs(self.waypoints or {}) do + local waypoint=_waypoint --#OPSGROUP.Waypoint + if waypoint.uid==uid then + return i + end + end + end + + return nil +end + +--- Get next waypoint index. +-- @param #OPSGROUP self +-- @param #boolean cyclic If true, return first waypoint if last waypoint was reached. Default is patrol ad infinitum value set. +-- @return #number Next waypoint index. +function OPSGROUP:GetWaypointIndexNext(cyclic) + + if cyclic==nil then + cyclic=self.adinfinitum + end + + local N=#self.waypoints + + local n=math.min(self.currentwp+1, N) + + if cyclic and self.currentwp==N then + n=1 + end + + return n +end + +--- Get current waypoint index. This is the index of the last passed waypoint. +-- @param #OPSGROUP self +-- @return #number Current waypoint index. +function OPSGROUP:GetWaypointIndexCurrent() + return self.currentwp or 1 +end + +--- Get waypoint index after waypoint with given ID. So if the waypoint has index 3 it will return 4. +-- @param #OPSGROUP self +-- @param #number uid Unique ID of the waypoint. Default is new waypoint index after the last current one. +-- @return #number Index after waypoint with given ID. +function OPSGROUP:GetWaypointIndexAfterID(uid) + + local index=self:GetWaypointIndex(uid) + if index then + return index+1 + else + return #self.waypoints+1 + end + +end + +--- Get waypoint. +-- @param #OPSGROUP self +-- @param #number indx Waypoint index. +-- @return #OPSGROUP.Waypoint Waypoint table. +function OPSGROUP:GetWaypoint(indx) + return self.waypoints[indx] +end + +--- Get final waypoint. +-- @param #OPSGROUP self +-- @return #OPSGROUP.Waypoint Final waypoint table. +function OPSGROUP:GetWaypointFinal() + return self.waypoints[#self.waypoints] +end + +--- Get next waypoint. +-- @param #OPSGROUP self +-- @param #boolean cyclic If true, return first waypoint if last waypoint was reached. +-- @return #OPSGROUP.Waypoint Next waypoint table. +function OPSGROUP:GetWaypointNext(cyclic) + + local n=self:GetWaypointIndexNext(cyclic) + + return self.waypoints[n] +end + +--- Get current waypoint. +-- @param #OPSGROUP self +-- @return #OPSGROUP.Waypoint Current waypoint table. +function OPSGROUP:GetWaypointCurrent() + return self.waypoints[self.currentwp] +end + +--- Get coordinate of next waypoint of the group. +-- @param #OPSGROUP self +-- @param #boolean cyclic If true, return first waypoint if last waypoint was reached. +-- @return Core.Point#COORDINATE Coordinate of the next waypoint. +function OPSGROUP:GetNextWaypointCoordinate(cyclic) + + -- Get next waypoint + local waypoint=self:GetWaypointNext(cyclic) + + return waypoint.coordinate +end + +--- Get waypoint coordinates. +-- @param #OPSGROUP self +-- @param #number index Waypoint index. +-- @return Core.Point#COORDINATE Coordinate of the next waypoint. +function OPSGROUP:GetWaypointCoordinate(index) + local waypoint=self:GetWaypoint(index) + if waypoint then + return waypoint.coordinate + end + return nil +end + +--- Get waypoint speed. +-- @param #OPSGROUP self +-- @param #number indx Waypoint index. +-- @return #number Speed set at waypoint in knots. +function OPSGROUP:GetWaypointSpeed(indx) + + local waypoint=self:GetWaypoint(indx) + + if waypoint then + return UTILS.MpsToKnots(waypoint.speed) + end + + return nil +end + +--- Get unique ID of waypoint. +-- @param #OPSGROUP self +-- @param #OPSGROUP.Waypoint waypoint The waypoint data table. +-- @return #number Unique ID. +function OPSGROUP:GetWaypointUID(waypoint) + return waypoint.uid +end + +--- Get unique ID of waypoint given its index. +-- @param #OPSGROUP self +-- @param #number indx Waypoint index. +-- @return #number Unique ID. +function OPSGROUP:GetWaypointID(indx) + + local waypoint=self:GetWaypoint(indx) + + if waypoint then + return waypoint.uid + end + + return nil + +end + +--- Returns a non-zero speed to the next waypoint (even if the waypoint speed is zero). +-- @param #OPSGROUP self +-- @param #number indx Waypoint index. +-- @return #number Speed to next waypoint (>0) in knots. +function OPSGROUP:GetSpeedToWaypoint(indx) + + local speed=self:GetWaypointSpeed(indx) + + if speed<=0.1 then + speed=self:GetSpeedCruise() + end + + return speed +end + +--- Get distance to waypoint. +-- @param #OPSGROUP self +-- @param #number indx Waypoint index. Default is the next waypoint. +-- @return #number Distance in meters. +function OPSGROUP:GetDistanceToWaypoint(indx) + local dist=0 + + if #self.waypoints>0 then + + indx=indx or self:GetWaypointIndexNext() + + local wp=self:GetWaypoint(indx) + + if wp then + + local coord=self:GetCoordinate() + + dist=coord:Get2DDistance(wp.coordinate) + end + + end + + return dist +end + +--- Get time to waypoint based on current velocity. +-- @param #OPSGROUP self +-- @param #number indx Waypoint index. Default is the next waypoint. +-- @return #number Time in seconds. If velocity is 0 +function OPSGROUP:GetTimeToWaypoint(indx) + + local s=self:GetDistanceToWaypoint(indx) + + local v=self:GetVelocity() + + local t=s/v + + if t==math.inf then + return 365*24*60*60 + elseif t==math.nan then + return 0 + else + return t + end + +end + +--- Returns the currently expected speed. +-- @param #OPSGROUP self +-- @return #number Expected speed in m/s. +function OPSGROUP:GetExpectedSpeed() + + if self:IsHolding() then + return 0 + else + return self.speedWp or 0 + end + +end + +--- Remove a waypoint with a ceratin UID. +-- @param #OPSGROUP self +-- @param #number uid Waypoint UID. +-- @return #OPSGROUP self +function OPSGROUP:RemoveWaypointByID(uid) + + local index=self:GetWaypointIndex(uid) + + if index then + self:RemoveWaypoint(index) + end + + return self +end + --- Remove a waypoint. -- @param #OPSGROUP self -- @param #number wpindex Waypoint number. @@ -642,6 +1353,12 @@ function OPSGROUP:RemoveWaypoint(wpindex) -- Number of waypoints before delete. local N=#self.waypoints + + -- Remove waypoint marker. + local wp=self:GetWaypoint(wpindex) + if wp and wp.marker then + wp.marker:Remove() + end -- Remove waypoint. table.remove(self.waypoints, wpindex) @@ -650,46 +1367,44 @@ function OPSGROUP:RemoveWaypoint(wpindex) local n=#self.waypoints -- Debug info. - self:I(self.lid..string.format("Removing waypoint %d. N %d-->%d", wpindex, N, n)) + self:T(self.lid..string.format("Removing waypoint index %d, current wp index %d. N %d-->%d", wpindex, self.currentwp, N, n)) - -- Shift all waypoint tasks after the removed waypoint. - for _,_task in pairs(self.taskqueue) do - local task=_task --#OPSGROUP.Task - if task.type==OPSGROUP.TaskType.WAYPOINT and task.waypoint and task.waypoint>wpindex then - task.waypoint=task.waypoint-1 - end - end - - -- Shift all mission waypoints after the removerd waypoint. - for _,_mission in pairs(self.missionqueue) do - local mission=_mission --Ops.Auftrag#AUFTRAG - - -- Get mission waypoint index. - local wpidx=mission:GetGroupWaypointIndex(self) - - -- Reduce number if this waypoint lies in the future. - if wpidx and wpidx>wpindex then - mission:SetGroupWaypointIndex(self, wpidx-1) - end - end - - -- Waypoint was not reached yet. if wpindex > self.currentwp then - -- Could be that we just removed the only remaining waypoint ==> passedfinalwp=true so we RTB or wait. + --- + -- Removed a FUTURE waypoint + --- + + -- TODO: patrol adinfinitum. + if self.currentwp>=n then self.passedfinalwp=true end - self:_CheckGroupDone() - + self:_CheckGroupDone(1) + else - -- If an already passed waypoint was deleted, we do not need to update the route. + --- + -- Removed a waypoint ALREADY PASSED + --- - -- TODO: But what about the self.currentwp number. This is now incorrect! - self.currentwp=self.currentwp-1 + -- If an already passed waypoint was deleted, we do not need to update the route. + + -- If current wp = 1 it stays 1. Otherwise decrease current wp. + + if self.currentwp==1 then + + if self.adinfinitum then + self.currentwp=#self.waypoints + else + self.currentwp=1 + end + + else + self.currentwp=self.currentwp-1 + end end @@ -744,7 +1459,7 @@ function OPSGROUP:SetTask(DCSTask) text=text..string.format("\n[%d] %s", i, tostring(task.id)) end end - self:I(self.lid..text) + self:T(self.lid..text) end return self @@ -768,7 +1483,7 @@ function OPSGROUP:PushTask(DCSTask) text=text..string.format("\n[%d] %s", i, tostring(task.id)) end end - self:I(self.lid..text) + self:T(self.lid..text) end return self @@ -802,7 +1517,7 @@ function OPSGROUP:AddTask(task, clock, description, prio, duration) table.insert(self.taskqueue, newtask) -- Info. - self:I(self.lid..string.format("Adding SCHEDULED task %s starting at %s", newtask.description, UTILS.SecondsToClock(newtask.time, true))) + self:T(self.lid..string.format("Adding SCHEDULED task %s starting at %s", newtask.description, UTILS.SecondsToClock(newtask.time, true))) self:T3({newtask=newtask}) return newtask @@ -851,42 +1566,49 @@ end --- Add a *waypoint* task. -- @param #OPSGROUP self -- @param #table task DCS task table structure. --- @param #number waypointindex Number of waypoint. Counting starts at one! Default is the as *next* waypoint. +-- @param #OPSGROUP.Waypoint Waypoint where the task is executed. Default is the at *next* waypoint. -- @param #string description Brief text describing the task, e.g. "Attack SAM". -- @param #number prio Priority of the task. Number between 1 and 100. Default is 50. -- @param #number duration Duration before task is cancelled in seconds counted after task started. Default never. -- @return #OPSGROUP.Task The task structure. -function OPSGROUP:AddTaskWaypoint(task, waypointindex, description, prio, duration) - - -- Increase counter. - self.taskcounter=self.taskcounter+1 - - -- Task data structure. - local newtask={} --#OPSGROUP.Task - newtask.description=description - newtask.status=OPSGROUP.TaskStatus.SCHEDULED - newtask.dcstask=task - newtask.prio=prio or 50 - newtask.id=self.taskcounter - newtask.duration=duration - newtask.time=0 - newtask.waypoint=waypointindex or (self.currentwp and self.currentwp+1 or 2) - newtask.type=OPSGROUP.TaskType.WAYPOINT - newtask.stopflag=USERFLAG:New(string.format("%s StopTaskFlag %d", self.groupname, newtask.id)) - newtask.stopflag:Set(0) - - -- Add to table. - table.insert(self.taskqueue, newtask) +function OPSGROUP:AddTaskWaypoint(task, Waypoint, description, prio, duration) - -- Info. - self:I(self.lid..string.format("Adding WAYPOINT task %s at WP %d", newtask.description, newtask.waypoint)) - self:T3({newtask=newtask}) - - -- Update route. - --self:_CheckGroupDone(1) - self:__UpdateRoute(-1) + -- Waypoint of task. + Waypoint=Waypoint or self:GetWaypointNext() + + if Waypoint then - return newtask + -- Increase counter. + self.taskcounter=self.taskcounter+1 + + -- Task data structure. + local newtask={} --#OPSGROUP.Task + newtask.description=description or string.format("Task #%d", self.taskcounter) + newtask.status=OPSGROUP.TaskStatus.SCHEDULED + newtask.dcstask=task + newtask.prio=prio or 50 + newtask.id=self.taskcounter + newtask.duration=duration + newtask.time=0 + newtask.waypoint=Waypoint.uid + newtask.type=OPSGROUP.TaskType.WAYPOINT + newtask.stopflag=USERFLAG:New(string.format("%s StopTaskFlag %d", self.groupname, newtask.id)) + newtask.stopflag:Set(0) + + -- Add to table. + table.insert(self.taskqueue, newtask) + + -- Info. + self:T(self.lid..string.format("Adding WAYPOINT task %s at WP ID=%d", newtask.description, newtask.waypoint)) + self:T3({newtask=newtask}) + + -- Update route. + self:__UpdateRoute(-1) + + return newtask + end + + return nil end --- Add an *enroute* task. @@ -915,9 +1637,9 @@ end --- Get the unfinished waypoint tasks -- @param #OPSGROUP self --- @param #number n Waypoint index. Counting starts at one. +-- @param #number id Unique waypoint ID. -- @return #table Table of tasks. Table could also be empty {}. -function OPSGROUP:GetTasksWaypoint(n) +function OPSGROUP:GetTasksWaypoint(id) -- Tasks table. local tasks={} @@ -928,7 +1650,7 @@ function OPSGROUP:GetTasksWaypoint(n) -- Look for first task that SCHEDULED. for _,_task in pairs(self.taskqueue) do local task=_task --#OPSGROUP.Task - if task.type==OPSGROUP.TaskType.WAYPOINT and task.status==OPSGROUP.TaskStatus.SCHEDULED and task.waypoint==n then + if task.type==OPSGROUP.TaskType.WAYPOINT and task.status==OPSGROUP.TaskStatus.SCHEDULED and task.waypoint==id then table.insert(tasks, task) end end @@ -1005,7 +1727,6 @@ function OPSGROUP:RemoveTask(Task) -- Update route if this is a waypoint task. if task.type==OPSGROUP.TaskType.WAYPOINT and task.status==OPSGROUP.TaskStatus.SCHEDULED then self:_CheckGroupDone(1) - --self:__UpdateRoute(-1) end return true @@ -1082,9 +1803,8 @@ end function OPSGROUP:onafterTaskExecute(From, Event, To, Task) -- Debug message. - local text=string.format("Task %s ID=%d execute.", tostring(Task.description), Task.id) - MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) - self:I(self.lid..text) + local text=string.format("Task %s ID=%d execute", tostring(Task.description), Task.id) + self:T(self.lid..text) -- Cancel current task if there is any. if self.taskcurrent>0 then @@ -1194,8 +1914,7 @@ function OPSGROUP:onafterTaskCancel(From, Event, To, Task) -- Debug info. local text=string.format("Current task %s ID=%d cancelled (flag %s=%d)", Task.description, Task.id, Task.stopflag:GetName(), stopflag) - MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) - self:I(self.lid..text) + self:T(self.lid..text) -- Set stop flag. When the flag is true, the _TaskDone function is executed and calls :TaskDone() Task.stopflag:Set(1) @@ -1203,7 +1922,7 @@ function OPSGROUP:onafterTaskCancel(From, Event, To, Task) if Task.dcstask.id=="Formation" then Task.formation:Stop() self:TaskDone(Task) - elseif stopflag==1 then + elseif stopflag==1 or not self:IsAlive() then -- Manual call TaskDone if setting flag to one was not successful. self:TaskDone(Task) end @@ -1211,30 +1930,16 @@ function OPSGROUP:onafterTaskCancel(From, Event, To, Task) else -- Debug info. - self:I(self.lid..string.format("TaskCancel: Setting task %s ID=%d to DONE", Task.description, Task.id)) + self:T(self.lid..string.format("TaskCancel: Setting task %s ID=%d to DONE", Task.description, Task.id)) -- Call task done function. self:TaskDone(Task) - - - --[[ - local mission=self:GetMissionByTaskID(Task.id) - - -- Is this a waypoint task? - if Task.type==OPSGROUP.TaskType.WAYPOINT and Task.waypoint then - -- Check that this is a mission waypoint and no other tasks are defined here. - if mission and #self:GetTasksWaypoint(Task.waypoint)==0 then - self:RemoveWaypoint(Task.waypoint) - end - end - ]] end else local text=string.format("WARNING: No (current) task to cancel!") - MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) self:E(self.lid..text) end @@ -1268,8 +1973,7 @@ function OPSGROUP:onafterTaskDone(From, Event, To, Task) -- Debug message. local text=string.format("Task done: %s ID=%d", Task.description, Task.id) - MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) - self:I(self.lid..text) + self:T(self.lid..text) -- No current task. if Task.id==self.taskcurrent then @@ -1317,6 +2021,9 @@ function OPSGROUP:AddMission(Mission) -- Set mission status to SCHEDULED. Mission:Scheduled() + + -- Add elements. + Mission.Nelements=Mission.Nelements+#self.elements -- Add mission to queue. table.insert(self.missionqueue, Mission) @@ -1324,7 +2031,7 @@ function OPSGROUP:AddMission(Mission) -- Info text. local text=string.format("Added %s mission %s starting at %s, stopping at %s", tostring(Mission.type), tostring(Mission.name), UTILS.SecondsToClock(Mission.Tstart, true), Mission.Tstop and UTILS.SecondsToClock(Mission.Tstop, true) or "INF") - self:I(self.lid..text) + self:T(self.lid..text) return self end @@ -1408,11 +2115,20 @@ function OPSGROUP:_GetNextMission() -- Current time. local time=timer.getAbsTime() + -- Look for first mission that is SCHEDULED. + local vip=math.huge + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + if mission.importance and mission.importanceweapondata.RangeMax then + + local d=(dist-weapondata.RangeMax)*1.1 + + -- New waypoint coord. + waypointcoord=self:GetCoordinate():Translate(d, heading) + + self:T(self.lid..string.format("Out of max range = %.1f km for weapon %d", weapondata.RangeMax/1000, mission.engageWeaponType)) + elseif dist0 then for i,_task in pairs(tasks) do local task=_task --#OPSGROUP.Task @@ -1895,20 +2724,8 @@ function OPSGROUP:onafterPassingWaypoint(From, Event, To, n, N) if #taskswp>0 then self:SetTask(self.group:TaskCombo(taskswp)) end - - -- Final AIR waypoint reached? - if n==N then - -- Set switch to true. - self.passedfinalwp=true - - -- Check if all tasks/mission are done? If so, RTB or WAIT. - -- Note, we delay it for a second to let the OnAfterPassingwaypoint function to be executed in case someone wants to add another waypoint there. - if #taskswp==0 then - self:_CheckGroupDone(1) - end - - end + return #taskswp end --- On after "GotoWaypoint" event. Group will got to the given waypoint and execute its route from there. @@ -1916,26 +2733,31 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #number n The goto waypoint number. -function OPSGROUP:onafterGotoWaypoint(From, Event, To, n) +-- @param #number UID The goto waypoint unique ID. +function OPSGROUP:onafterGotoWaypoint(From, Event, To, UID) - -- The last waypoint passed was n-1 - self.currentwp=n-1 + local n=self:GetWaypointIndex(UID) - -- TODO: switch to re-enable waypoint tasks. - if false then - local tasks=self:GetTasksWaypoint(n) - - for _,_task in pairs(tasks) do - local task=_task --#OPSGROUP.Task - task.status=OPSGROUP.TaskStatus.SCHEDULED + if n then + + -- TODO: switch to re-enable waypoint tasks. + if false then + local tasks=self:GetTasksWaypoint(n) + + for _,_task in pairs(tasks) do + local task=_task --#OPSGROUP.Task + task.status=OPSGROUP.TaskStatus.SCHEDULED + end + end + local Speed=self:GetSpeedToWaypoint(n) + + -- Update the route. + self:__UpdateRoute(-1, n, Speed) + end - -- Update the route. - self:UpdateRoute() - end --- On after "DetectedUnit" event. Add newly detected unit to detected units set. @@ -1945,8 +2767,22 @@ end -- @param #string To To state. -- @param Wrapper.Unit#UNIT Unit The detected unit. function OPSGROUP:onafterDetectedUnit(From, Event, To, Unit) - self:T2(self.lid..string.format("Detected unit %s", Unit:GetName())) - self.detectedunits:AddUnit(Unit) + + -- Get unit name. + local unitname=Unit and Unit:GetName() or "unknown" + + -- Debug. + self:T2(self.lid..string.format("Detected unit %s", unitname)) + + + if self.detectedunits:FindUnit(unitname) then + -- Unit is already in the detected unit set ==> Trigger "DetectedUnitKnown" event. + self:DetectedUnitKnown(Unit) + else + -- Unit is was not detected ==> Trigger "DetectedUnitNew" event. + self:DetectedUnitNew(Unit) + end + end --- On after "DetectedUnitNew" event. @@ -1957,6 +2793,9 @@ end -- @param Wrapper.Unit#UNIT Unit The detected unit. function OPSGROUP:onafterDetectedUnitNew(From, Event, To, Unit) self:T(self.lid..string.format("Detected New unit %s", Unit:GetName())) + + -- Add unit to detected unit set. + self.detectedunits:AddUnit(Unit) end --- On after "EnterZone" event. Sets self.inzones[zonename]=true. @@ -1983,27 +2822,104 @@ function OPSGROUP:onafterLeaveZone(From, Event, To, Zone) self.inzones:Remove(zonename, true) end ---- On after "CheckZone" event. + +--- On after "ElementDestroyed" event. -- @param #OPSGROUP self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -function OPSGROUP:onafterCheckZone(From, Event, To) +-- @param #OPSGROUP.Element Element The flight group element. +function OPSGROUP:onafterElementDestroyed(From, Event, To, Element) + self:T(self.lid..string.format("Element destroyed %s", Element.name)) + + -- Cancel all missions. + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG - if self:IsAlive()==true then - self:_CheckInZones() - end + mission:ElementDestroyed(self, Element) - if not self:IsStopped() then - self:__CheckZone(-1) end + + -- Increase counter. + self.Ndestroyed=self.Ndestroyed+1 + + -- Set element status. + self:_UpdateStatus(Element, OPSGROUP.ElementStatus.DEAD) + end +--- On after "ElementDead" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP.Element Element The flight group element. +function OPSGROUP:onafterElementDead(From, Event, To, Element) + self:T(self.lid..string.format("Element dead %s", Element.name)) + + -- Set element status. + self:_UpdateStatus(Element, OPSGROUP.ElementStatus.DEAD) +end + +--- On after "Dead" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterDead(From, Event, To) + self:T(self.lid..string.format("Group dead!")) + + -- Delete waypoints so they are re-initialized at the next spawn. + self.waypoints=nil + self.groupinitialized=false + + -- Cancel all missions. + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + self:MissionCancel(mission) + mission:GroupDead(self) + + end + + -- Stop in a sec. + self:__Stop(-5) +end + +--- On after "Stop" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterStop(From, Event, To) + + -- Stop check timers. + self.timerCheckZone:Stop() + self.timerQueueUpdate:Stop() + + -- Stop FSM scheduler. + self.CallScheduler:Clear() + + if self:IsAlive() and not (self:IsDead() or self:IsStopped()) then + local life, life0=self:GetLifePoints() + local state=self:GetState() + local text=string.format("WARNING: Group is still alive! Current state=%s. Life points=%d/%d. Use OPSGROUP:Destroy() or OPSGROUP:Despawn() for a clean stop", state, life, life0) + self:E(self.lid..text) + end + + -- Debug output. + self:T(self.lid.."STOPPED! Unhandled events, cleared scheduler and removed from database.") +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Internal Check Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + --- Check if group is in zones. -- @param #OPSGROUP self function OPSGROUP:_CheckInZones() - if self.checkzones then + if self.checkzones and self:IsAlive() then local Ncheck=self.checkzones:Count() local Ninside=self.inzones:Count() @@ -2016,7 +2932,7 @@ function OPSGROUP:_CheckInZones() for inzonename, inzone in pairs(self.inzones:GetSet()) do -- Check if group is still inside the zone. - local isstillinzone=self.group:IsPartlyOrCompletelyInZone(inzone) + local isstillinzone=self.group:IsInZone(inzone) --:IsPartlyOrCompletelyInZone(inzone) -- If not, trigger, LeaveZone event. if not isstillinzone then @@ -2036,7 +2952,7 @@ function OPSGROUP:_CheckInZones() local checkzone=_checkzone --Core.Zone#ZONE -- Is group currtently in this check zone? - local isincheckzone=self.group:IsPartlyOrCompletelyInZone(checkzone) + local isincheckzone=self.group:IsInZone(checkzone) --:IsPartlyOrCompletelyInZone(checkzone) if isincheckzone and not self.inzones:_Find(checkzonename) then table.insert(enterzones, checkzone) @@ -2048,7 +2964,6 @@ function OPSGROUP:_CheckInZones() self:EnterZone(enterzone) end - end end @@ -2067,6 +2982,8 @@ function OPSGROUP:_CheckDetectedUnits() local DetectedObject=Detection.object -- DCS#Object if DetectedObject and DetectedObject:isExist() and DetectedObject.id_<50000000 then + + -- Unit. local unit=UNIT:Find(DetectedObject) if unit and unit:IsAlive() then @@ -2077,17 +2994,9 @@ function OPSGROUP:_CheckDetectedUnits() -- Add unit to detected table of this run. table.insert(detected, unit) - -- Trigger detected unit event. + -- Trigger detected unit event ==> This also triggers the DetectedUnitNew and DetectedUnitKnown events. self:DetectedUnit(unit) - if self.detectedunits:FindUnit(unitname) then - -- Unit is already in the detected unit set ==> Trigger "DetectedUnitKnown" event. - self:DetectedUnitKnown(unit) - else - -- Unit is was not detected ==> Trigger "DetectedUnitNew" event. - self:DetectedUnitNew(unit) - end - end end end @@ -2121,24 +3030,260 @@ function OPSGROUP:_CheckDetectedUnits() end +--- Check if passed the final waypoint and, if necessary, update route. +-- @param #OPSGROUP self +-- @param #number delay Delay in seconds. +function OPSGROUP:_CheckGroupDone(delay) + + if self:IsAlive() and self.ai then + + if delay and delay>0 then + -- Delayed call. + self:ScheduleOnce(delay, self._CheckGroupDone, self) + else + + if self.passedfinalwp then + + --- + -- Passed FINAL waypoint + --- + + if #self.waypoints>1 then + + if self.adinfinitum then + + -- Get positive speed to first waypoint. + local speed=self:GetSpeedToWaypoint(1) + + -- Start route at first waypoint. + self:__UpdateRoute(-1, 1, speed) + + self:T(self.lid..string.format("Passed final WP, #WP>1, adinfinitum=TRUE ==> Goto WP 1 at speed>0")) + + self.passedfinalwp=false + + else + -- No further waypoints. Command a full stop. + self:__FullStop(-1) + + self:T(self.lid..string.format("Passed final WP, #WP>1, adinfinitum=FALSE ==> Full Stop")) + end + + elseif #self.waypoints==1 then + + --- Only one WP left + + -- The last waypoint. + local waypoint=self.waypoints[1] --Ops.OpsGroup#OPSGROUP.Waypoint + + local dist=self:GetCoordinate():Get2DDistance(waypoint.coordinate) + + + if self.adinfinitum and dist>1000 then -- Note that dist>100 caused the same wp to be passed a lot of times. + + self:T(self.lid..string.format("Passed final WP, #WP=1, adinfinitum=TRUE dist>1000 ==> Goto WP 1 at speed>0")) + + -- Get positive speed to first waypoint. + local speed=self:GetSpeedToWaypoint(1) + + -- Start route at first waypoint. + self:__UpdateRoute(-1, 1, speed) + + self.passedfinalwp=false + + else + + self:T(self.lid..string.format("Passed final WP, #WP=1, adinfinitum=FALSE or dist<1000 ==> Full Stop")) + + self:__FullStop(-1) + + end + + else + + --- No waypoints left + + -- No further waypoints. Command a full stop. + self:T(self.lid..string.format("No waypoints left ==> Full Stop")) + + self:__FullStop(-1) + + end + + else + + --- + -- Final waypoint NOT passed yet + --- + + if #self.waypoints>0 then + self:T(self.lid..string.format("NOT Passed final WP, #WP>0 ==> Update Route")) + self:__UpdateRoute(-1) + else + self:E(self.lid..string.format("WARNING: No waypoints left! Commanding a Full Stop")) + self:__FullStop(-1) + end + + end + + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Status Info Common to Air, Land and Sea +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Print info on mission and task status to DCS log file. +-- @param #OPSGROUP self +function OPSGROUP:_PrintTaskAndMissionStatus() + + --- + -- Tasks: verbose >= 3 + --- + + -- Task queue. + if self.verbose>=3 and #self.taskqueue>0 then + local text=string.format("Tasks #%d", #self.taskqueue) + for i,_task in pairs(self.taskqueue) do + local task=_task --Ops.OpsGroup#OPSGROUP.Task + local name=task.description + local taskid=task.dcstask.id or "unknown" + local status=task.status + local clock=UTILS.SecondsToClock(task.time, true) + local eta=task.time-timer.getAbsTime() + local started=task.timestamp and UTILS.SecondsToClock(task.timestamp, true) or "N/A" + local duration=-1 + if task.duration then + duration=task.duration + if task.timestamp then + -- Time the task is running. + duration=task.duration-(timer.getAbsTime()-task.timestamp) + else + -- Time the task is supposed to run. + duration=task.duration + end + end + -- Output text for element. + if task.type==OPSGROUP.TaskType.SCHEDULED then + text=text..string.format("\n[%d] %s (%s): status=%s, scheduled=%s (%d sec), started=%s, duration=%d", i, taskid, name, status, clock, eta, started, duration) + elseif task.type==OPSGROUP.TaskType.WAYPOINT then + text=text..string.format("\n[%d] %s (%s): status=%s, waypoint=%d, started=%s, duration=%d, stopflag=%d", i, taskid, name, status, task.waypoint, started, duration, task.stopflag:Get()) + end + end + self:I(self.lid..text) + end + + --- + -- Missions: verbose>=2 + --- + + -- Current mission name. + if self.verbose>=2 then + local Mission=self:GetMissionByID(self.currentmission) + + -- Current status. + local text=string.format("Missions %d, Current: %s", self:CountRemainingMissison(), Mission and Mission.name or "none") + for i,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + local Cstart= UTILS.SecondsToClock(mission.Tstart, true) + local Cstop = mission.Tstop and UTILS.SecondsToClock(mission.Tstop, true) or "INF" + text=text..string.format("\n[%d] %s (%s) status=%s (%s), Time=%s-%s, prio=%d wp=%s targets=%d", + i, tostring(mission.name), mission.type, mission:GetGroupStatus(self), tostring(mission.status), Cstart, Cstop, mission.prio, tostring(mission:GetGroupWaypointIndex(self)), mission:CountMissionTargets()) + end + self:I(self.lid..text) + end + +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Waypoints & Routing ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Enhance waypoint table. +-- @param #OPSGROUP self +-- @param #OPSGROUP.Waypoint Waypoint data. +-- @return #OPSGROUP.Waypoint Modified waypoint data. +function OPSGROUP:_CreateWaypoint(waypoint) + + -- Set uid. + waypoint.uid=self.wpcounter + + -- Waypoint has not been passed yet. + waypoint.npassed=0 + + -- Coordinate. + waypoint.coordinate=COORDINATE:New(waypoint.x, waypoint.alt, waypoint.y) + + -- Set waypoint name. + waypoint.name=string.format("Waypoint UID=%d", waypoint.uid) + + -- Set types. + waypoint.patrol=false + waypoint.detour=false + waypoint.astar=false + + -- Increase UID counter. + self.wpcounter=self.wpcounter+1 + + return waypoint +end + +--- Initialize Mission Editor waypoints. +-- @param #OPSGROUP self +-- @param #OPSGROUP.Waypoint waypoint Waypoint data. +-- @param #number wpnumber Waypoint index/number. Default is as last waypoint. +function OPSGROUP:_AddWaypoint(waypoint, wpnumber) + + -- Index. + wpnumber=wpnumber or #self.waypoints+1 + + -- Add waypoint to table. + table.insert(self.waypoints, wpnumber, waypoint) + + -- Debug info. + self:T(self.lid..string.format("Adding waypoint at index=%d id=%d", wpnumber, waypoint.uid)) + + -- Now we obviously did not pass the final waypoint. + self.passedfinalwp=false + + -- Switch to cruise mode. + if self:IsHolding() then + self:Cruise() + end +end + --- Initialize Mission Editor waypoints. -- @param #OPSGROUP self --- @param #table waypoints Table of waypoints. Default is from group template. -- @return #OPSGROUP self -function OPSGROUP:InitWaypoints(waypoints) +function OPSGROUP:InitWaypoints() -- Template waypoints. self.waypoints0=self.group:GetTemplateRoutePoints() - -- Waypoints of group as defined in the ME. - self.waypoints=waypoints or UTILS.DeepCopy(self.waypoints0) + -- Waypoints + self.waypoints={} + + for index,wp in pairs(self.waypoints0) do + + --local waypoint=self:_CreateWaypoint(wp) + --self:_AddWaypoint(waypoint) + + local coordinate=COORDINATE:New(wp.x, wp.alt, wp.y) + local speedknots=UTILS.MpsToKnots(wp.speed) + + if index==1 then + self.speedWp=wp.speed + end + + self:AddWaypoint(coordinate, speedknots, index-1, nil, false) + + end -- Debug info. - self:I(self.lid..string.format("Initializing %d waypoints", #self.waypoints)) + self:T(self.lid..string.format("Initializing %d waypoints", #self.waypoints)) -- Update route. if #self.waypoints>0 then @@ -2156,7 +3301,7 @@ end --- Route group along waypoints. -- @param #OPSGROUP self -- @param #table waypoints Table of waypoints. --- @default +-- @param #number delay Delay in seconds. -- @return #OPSGROUP self function OPSGROUP:Route(waypoints, delay) @@ -2198,21 +3343,22 @@ end -- @param #number n Waypoint function OPSGROUP:_UpdateWaypointTasks(n) - local waypoints=self.waypoints + local waypoints=self.waypoints or {} local nwaypoints=#waypoints - for i,wp in pairs(waypoints) do + for i,_wp in pairs(waypoints) do + local wp=_wp --Ops.OpsGroup#OPSGROUP.Waypoint if i>=n or nwaypoints==1 then -- Debug info. - self:T(self.lid..string.format("Updating waypoint task for waypoint %d/%d. Last waypoint passed %d", i, nwaypoints, self.currentwp)) + self:T(self.lid..string.format("Updating waypoint task for waypoint %d/%d ID=%d. Last waypoint passed %d", i, nwaypoints, wp.uid, self.currentwp)) -- Tasks of this waypoint local taskswp={} -- At each waypoint report passing. - local TaskPassingWaypoint=self.group:TaskFunction("OPSGROUP._PassingWaypoint", self, i) + local TaskPassingWaypoint=self.group:TaskFunction("OPSGROUP._PassingWaypoint", self, wp.uid) table.insert(taskswp, TaskPassingWaypoint) -- Waypoint task combo. @@ -2229,22 +3375,73 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Function called when a group is passing a waypoint. ---@param Wrapper.Group#GROUP group Group that passed the waypoint +--@param Wrapper.Group#GROUP group Group that passed the waypoint. --@param #OPSGROUP opsgroup Ops group object. ---@param #number i Waypoint number that has been reached. -function OPSGROUP._PassingWaypoint(group, opsgroup, i) - - local final=#opsgroup.waypoints or 1 - - -- Debug message. - local text=string.format("Group passing waypoint %d of %d", i, final) - opsgroup:I(opsgroup.lid..text) - - -- Set current waypoint. - opsgroup.currentwp=i - - -- Trigger PassingWaypoint event. - opsgroup:PassingWaypoint(i, final) +--@param #number uid Waypoint UID. +function OPSGROUP._PassingWaypoint(group, opsgroup, uid) + + -- Get waypoint data. + local waypoint=opsgroup:GetWaypointByID(uid) + + if waypoint then + + -- Get the current waypoint index. + opsgroup.currentwp=opsgroup:GetWaypointIndex(uid) + + -- Increase passing counter. + waypoint.npassed=waypoint.npassed+1 + + -- Set expected speed and formation from the next WP. + local wpnext=opsgroup:GetWaypointNext() + if wpnext then + + -- Set formation. + if opsgroup.isGround then + opsgroup.formation=wpnext.action + end + + -- Set speed. + opsgroup.speed=wpnext.speed + + end + + -- Check if the group is still pathfinding. + if opsgroup.ispathfinding and not waypoint.astar then + opsgroup.ispathfinding=false + end + + -- Check special waypoints. + if waypoint.astar then + + opsgroup:RemoveWaypointByID(uid) + + elseif waypoint.detour then + + opsgroup:RemoveWaypointByID(uid) + + -- Trigger event. + opsgroup:DetourReached() + + if waypoint.detour==0 then + opsgroup:FullStop() + elseif waypoint.detour==1 then + opsgroup:Cruise() + else + opsgroup:E("ERROR: waypoint.detour should be 0 or 1") + end + + end + + -- Debug message. + local text=string.format("Group passing waypoint uid=%d", uid) + opsgroup:T2(opsgroup.lid..text) + + -- Trigger PassingWaypoint event. + if not (waypoint.astar or waypoint.detour) then + opsgroup:PassingWaypoint(waypoint) + end + + end end @@ -2289,70 +3486,684 @@ end -- @param #number roe ROE of group. Default is `ENUMS.ROE.ReturnFire`. -- @return #OPSGROUP self function OPSGROUP:SetDefaultROE(roe) - self.roeDefault=roe or ENUMS.ROE.ReturnFire + self.optionDefault.ROE=roe or ENUMS.ROE.ReturnFire return self end --- Set current ROE for the group. -- @param #OPSGROUP self --- @param #string roe ROE of group. Default is the value defined by :SetDefaultROE(). +-- @param #string roe ROE of group. Default is value set in `SetDefaultROE` (usually `ENUMS.ROE.ReturnFire`). -- @return #OPSGROUP self -function OPSGROUP:SetOptionROE(roe) +function OPSGROUP:SwitchROE(roe) + + if self:IsAlive() or self:IsInUtero() then - self.roe=roe or self.roeDefault + self.option.ROE=roe or self.optionDefault.ROE - if self:IsAlive() then - - self.group:OptionROE(self.roe) + if self:IsInUtero() then + self:T2(self.lid..string.format("Setting current ROE=%d when GROUP is SPAWNED", self.option.ROE)) + else + + self.group:OptionROE(self.option.ROE) + + self:T(self.lid..string.format("Setting current ROE=%d (%s)", self.option.ROE, self:_GetROEName(self.option.ROE))) + end + - self:I(self.lid..string.format("Setting current ROE=%d (0=WeaponFree, 1=OpenFireWeaponFree, 2=OpenFire, 3=ReturnFire, 4=WeaponHold)", self.roe)) else - -- TODO WARNING + self:E(self.lid.."WARNING: Cannot switch ROE! Group is not alive") end return self end +--- Get name of ROE corresponding to the numerical value. +-- @param #OPSGROUP self +-- @return #string Name of ROE. +function OPSGROUP:_GetROEName(roe) + local name="unknown" + if roe==0 then + name="Weapon Free" + elseif roe==1 then + name="Open Fire/Weapon Free" + elseif roe==2 then + name="Open Fire" + elseif roe==3 then + name="Return Fire" + elseif roe==4 then + name="Weapon Hold" + end + return name +end + --- Get current ROE of the group. -- @param #OPSGROUP self -- @return #number Current ROE. function OPSGROUP:GetROE() - return self.roe + return self.option.ROE or self.optionDefault.ROE end --- Set the default ROT for the group. This is the ROT state gets when the group is spawned or to which it defaults back after a mission. -- @param #OPSGROUP self --- @param #number roe ROE of group. Default is ENUMS.ROT.PassiveDefense. +-- @param #number rot ROT of group. Default is `ENUMS.ROT.PassiveDefense`. -- @return #OPSGROUP self -function OPSGROUP:SetDefaultROT(roe) - self.rotDefault=roe or ENUMS.ROT.PassiveDefense +function OPSGROUP:SetDefaultROT(rot) + self.optionDefault.ROT=rot or ENUMS.ROT.PassiveDefense return self end --- Set ROT for the group. -- @param #OPSGROUP self --- @param #string rot ROT of group. Default is the value defined by :SetDefaultROT(). +-- @param #string rot ROT of group. Default is value set in `:SetDefaultROT` (usually `ENUMS.ROT.PassiveDefense`). -- @return #OPSGROUP self -function OPSGROUP:SetOptionROT(rot) - - self.rot=rot or self.rotDefault +function OPSGROUP:SwitchROT(rot) - if self:IsAlive() then + if self:IsAlive() or self:IsInUtero() then - self.group:OptionROT(self.rot) + self.option.ROT=rot or self.optionDefault.ROT + + if self:IsInUtero() then + self:T2(self.lid..string.format("Setting current ROT=%d when GROUP is SPAWNED", self.option.ROT)) + else - self:T2(self.lid..string.format("Setting current ROT=%d (0=NoReaction, 1=Passive, 2=Evade, 3=ByPass, 4=AllowAbort)", self.rot)) + self.group:OptionROT(self.option.ROT) + + self:T(self.lid..string.format("Setting current ROT=%d (0=NoReaction, 1=Passive, 2=Evade, 3=ByPass, 4=AllowAbort)", self.option.ROT)) + end + + else - -- TODO WARNING + self:E(self.lid.."WARNING: Cannot switch ROT! Group is not alive") end return self end +--- Get current ROT of the group. +-- @param #OPSGROUP self +-- @return #number Current ROT. +function OPSGROUP:GetROT() + return self.option.ROT or self.optionDefault.ROT +end + + +--- Set the default Alarm State for the group. This is the state gets when the group is spawned or to which it defaults back after a mission. +-- @param #OPSGROUP self +-- @param #number alarmstate Alarm state of group. Default is `AI.Option.Ground.val.ALARM_STATE.AUTO` (0). +-- @return #OPSGROUP self +function OPSGROUP:SetDefaultAlarmstate(alarmstate) + self.optionDefault.Alarm=alarmstate or 0 + return self +end + +--- Set current Alarm State of the group. +-- +-- * 0 = "Auto" +-- * 1 = "Green" +-- * 2 = "Red" +-- +-- @param #OPSGROUP self +-- @param #number alarmstate Alarm state of group. Default is 0="Auto". +-- @return #OPSGROUP self +function OPSGROUP:SwitchAlarmstate(alarmstate) + + if self:IsAlive() or self:IsInUtero() then + + self.option.Alarm=alarmstate or self.optionDefault.Alarm + + if self:IsInUtero() then + self:T2(self.lid..string.format("Setting current Alarm State=%d when GROUP is SPAWNED", self.option.Alarm)) + else + + if self.option.Alarm==0 then + self.group:OptionAlarmStateAuto() + elseif self.option.Alarm==1 then + self.group:OptionAlarmStateGreen() + elseif self.option.Alarm==2 then + self.group:OptionAlarmStateRed() + else + self:E("ERROR: Unknown Alarm State! Setting to AUTO") + self.group:OptionAlarmStateAuto() + self.option.Alarm=0 + end + + self:T(self.lid..string.format("Setting current Alarm State=%d (0=Auto, 1=Green, 2=Red)", self.option.Alarm)) + + end + else + self:E(self.lid.."WARNING: Cannot switch Alarm State! Group is not alive.") + end + + return self +end + +--- Get current Alarm State of the group. +-- @param #OPSGROUP self +-- @return #number Current Alarm State. +function OPSGROUP:GetAlarmstate() + return self.option.Alarm or self.optionDefault.Alarm +end + +--- Set default TACAN parameters. +-- @param #OPSGROUP self +-- @param #number Channel TACAN channel. Default is 74. +-- @param #string Morse Morse code. Default "XXX". +-- @param #string UnitName Name of the unit acting as beacon. +-- @param #string Band TACAN mode. Default is "X" for ground and "Y" for airborne units. +-- @param #boolean OffSwitch If true, TACAN is off by default. +-- @return #OPSGROUP self +function OPSGROUP:SetDefaultTACAN(Channel, Morse, UnitName, Band, OffSwitch) + + self.tacanDefault={} + self.tacanDefault.Channel=Channel or 74 + self.tacanDefault.Morse=Morse or "XXX" + self.tacanDefault.BeaconName=UnitName + + if self.isAircraft then + Band=Band or "Y" + else + Band=Band or "X" + end + self.tacanDefault.Band=Band + + + if OffSwitch then + self.tacanDefault.On=false + else + self.tacanDefault.On=true + end + + return self +end + + +--- Activate/switch TACAN beacon settings. +-- @param #OPSGROUP self +-- @param #OPSGROUP.Beacon Tacan TACAN data table. Default is the default TACAN settings. +-- @return #OPSGROUP self +function OPSGROUP:_SwitchTACAN(Tacan) + + if Tacan then + + self:SwitchTACAN(Tacan.Channel, Tacan.Morse, Tacan.BeaconName, Tacan.Band) + + else + + if self.tacanDefault.On then + self:SwitchTACAN() + else + self:TurnOffTACAN() + end + + end + +end + +--- Activate/switch TACAN beacon settings. +-- @param #OPSGROUP self +-- @param #number Channel TACAN Channel. +-- @param #string Morse TACAN morse code. Default is the value set in @{#OPSGROUP.SetDefaultTACAN} or if not set "XXX". +-- @param #string UnitName Name of the unit in the group which should activate the TACAN beacon. Can also be given as #number to specify the unit number. Default is the first unit of the group. +-- @param #string Band TACAN channel mode "X" or "Y". Default is "Y" for aircraft and "X" for ground and naval groups. +-- @return #OPSGROUP self +function OPSGROUP:SwitchTACAN(Channel, Morse, UnitName, Band) + + if self:IsInUtero() then + + self:T(self.lid..string.format("Switching TACAN to DEFAULT when group is spawned")) + self:SetDefaultTACAN(Channel, Morse, UnitName, Band) + + elseif self:IsAlive() then + + Channel=Channel or self.tacanDefault.Channel + Morse=Morse or self.tacanDefault.Morse + Band=Band or self.tacanDefault.Band + UnitName=UnitName or self.tacanDefault.BeaconName + local unit=self:GetUnit(1) --Wrapper.Unit#UNIT + + if UnitName then + if type(UnitName)=="number" then + unit=self.group:GetUnit(UnitName) + else + unit=UNIT:FindByName(UnitName) + end + end + + if not unit then + self:T(self.lid.."WARNING: Could not get TACAN unit. Trying first unit in the group") + unit=self:GetUnit(1) + end + + if unit and unit:IsAlive() then + + -- Unit ID. + local UnitID=unit:GetID() + + -- Type + local Type=BEACON.Type.TACAN + + -- System + local System=BEACON.System.TACAN + if self.isAircraft then + System=BEACON.System.TACAN_TANKER_Y + end + + -- Tacan frequency. + local Frequency=UTILS.TACANToFrequency(Channel, Band) + + -- Activate beacon. + unit:CommandActivateBeacon(Type, System, Frequency, UnitID, Channel, Band, true, Morse, true) + + -- Update info. + self.tacan.Channel=Channel + self.tacan.Morse=Morse + self.tacan.Band=Band + self.tacan.BeaconName=unit:GetName() + self.tacan.BeaconUnit=unit + self.tacan.On=true + + -- Debug info. + self:T(self.lid..string.format("Switching TACAN to Channel %d%s Morse %s on unit %s", self.tacan.Channel, self.tacan.Band, tostring(self.tacan.Morse), self.tacan.BeaconName)) + + else + self:E(self.lid.."ERROR: Cound not set TACAN! Unit is not alive") + end + + else + self:E(self.lid.."ERROR: Cound not set TACAN! Group is not alive and not in utero any more") + end + + return self +end + +--- Deactivate TACAN beacon. +-- @param #OPSGROUP self +-- @return #OPSGROUP self +function OPSGROUP:TurnOffTACAN() + + if self.tacan.BeaconUnit and self.tacan.BeaconUnit:IsAlive() then + self.tacan.BeaconUnit:CommandDeactivateBeacon() + end + + self:T(self.lid..string.format("Switching TACAN OFF")) + self.tacan.On=false + +end + +--- Get current TACAN parameters. +-- @param #OPSGROUP self +-- @return #number TACAN channel. +-- @return #string TACAN Morse code. +-- @return #string TACAN band ("X" or "Y"). +-- @return #boolean TACAN is On (true) or Off (false). +-- @return #string UnitName Name of the unit acting as beacon. +function OPSGROUP:GetTACAN() + return self.tacan.Channel, self.tacan.Morse, self.tacan.Band, self.tacan.On, self.tacan.BeaconName +end + + + +--- Set default ICLS parameters. +-- @param #OPSGROUP self +-- @param #number Channel ICLS channel. Default is 1. +-- @param #string Morse Morse code. Default "XXX". +-- @param #string UnitName Name of the unit acting as beacon. +-- @param #boolean OffSwitch If true, TACAN is off by default. +-- @return #OPSGROUP self +function OPSGROUP:SetDefaultICLS(Channel, Morse, UnitName, OffSwitch) + + self.iclsDefault={} + self.iclsDefault.Channel=Channel or 1 + self.iclsDefault.Morse=Morse or "XXX" + self.iclsDefault.BeaconName=UnitName + + if OffSwitch then + self.iclsDefault.On=false + else + self.iclsDefault.On=true + end + + return self +end + + +--- Activate/switch ICLS beacon settings. +-- @param #OPSGROUP self +-- @param #OPSGROUP.Beacon Icls ICLS data table. +-- @return #OPSGROUP self +function OPSGROUP:_SwitchICLS(Icls) + + if Icls then + + self:SwitchICLS(Icls.Channel, Icls.Morse, Icls.BeaconName) + + else + + if self.iclsDefault.On then + self:SwitchICLS() + else + self:TurnOffICLS() + end + + end + +end + +--- Activate/switch ICLS beacon settings. +-- @param #OPSGROUP self +-- @param #number Channel ICLS Channel. Default is what is set in `SetDefaultICLS()` so usually channel 1. +-- @param #string Morse ICLS morse code. Default is what is set in `SetDefaultICLS()` so usually "XXX". +-- @param #string UnitName Name of the unit in the group which should activate the ICLS beacon. Can also be given as #number to specify the unit number. Default is the first unit of the group. +-- @return #OPSGROUP self +function OPSGROUP:SwitchICLS(Channel, Morse, UnitName) + + if self:IsInUtero() then + + self:SetDefaultICLS(Channel,Morse,UnitName) + + self:T2(self.lid..string.format("Switching ICLS to Channel %d Morse %s on unit %s when GROUP is SPAWNED", self.iclsDefault.Channel, tostring(self.iclsDefault.Morse), self.iclsDefault.BeaconName)) + + elseif self:IsAlive() then + + Channel=Channel or self.iclsDefault.Channel + Morse=Morse or self.iclsDefault.Morse + local unit=self:GetUnit(1) --Wrapper.Unit#UNIT + + if UnitName then + if type(UnitName)=="number" then + unit=self:GetUnit(UnitName) + else + unit=UNIT:FindByName(UnitName) + end + end + + if not unit then + self:T(self.lid.."WARNING: Could not get ICLS unit. Trying first unit in the group") + unit=self:GetUnit(1) + end + + if unit and unit:IsAlive() then + + -- Unit ID. + local UnitID=unit:GetID() + + -- Activate beacon. + unit:CommandActivateICLS(Channel, UnitID, Morse) + + -- Update info. + self.icls.Channel=Channel + self.icls.Morse=Morse + self.icls.Band=nil + self.icls.BeaconName=unit:GetName() + self.icls.BeaconUnit=unit + self.icls.On=true + + -- Debug info. + self:T(self.lid..string.format("Switching ICLS to Channel %d Morse %s on unit %s", self.icls.Channel, tostring(self.icls.Morse), self.icls.BeaconName)) + + else + self:E(self.lid.."ERROR: Cound not set ICLS! Unit is not alive.") + end + + end + + return self +end + +--- Deactivate ICLS beacon. +-- @param #OPSGROUP self +-- @return #OPSGROUP self +function OPSGROUP:TurnOffICLS() + + if self.icls.BeaconUnit and self.icls.BeaconUnit:IsAlive() then + self.icls.BeaconUnit:CommandDeactivateICLS() + end + + self:T(self.lid..string.format("Switching ICLS OFF")) + self.icls.On=false + +end + + +--- Set default Radio frequency and modulation. +-- @param #OPSGROUP self +-- @param #number Frequency Radio frequency in MHz. Default 251 MHz. +-- @param #number Modulation Radio modulation. Default `radio.Modulation.AM`. +-- @param #boolean OffSwitch If true, radio is OFF by default. +-- @return #OPSGROUP self +function OPSGROUP:SetDefaultRadio(Frequency, Modulation, OffSwitch) + + self.radioDefault={} + self.radioDefault.Freq=Frequency or 251 + self.radioDefault.Modu=Modulation or radio.modulation.AM + if OffSwitch then + self.radioDefault.On=false + else + self.radioDefault.On=true + end + + return self +end + +--- Get current Radio frequency and modulation. +-- @param #OPSGROUP self +-- @return #number Radio frequency in MHz or nil. +-- @return #number Radio modulation or nil. +-- @return #boolean If true, the radio is on. Otherwise, radio is turned off. +function OPSGROUP:GetRadio() + return self.radio.Freq, self.radio.Modu, self.radio.On +end + +--- Turn radio on or switch frequency/modulation. +-- @param #OPSGROUP self +-- @param #number Frequency Radio frequency in MHz. Default is value set in `SetDefaultRadio` (usually 251 MHz). +-- @param #number Modulation Radio modulation. Default is value set in `SetDefaultRadio` (usually `radio.Modulation.AM`). +-- @return #OPSGROUP self +function OPSGROUP:SwitchRadio(Frequency, Modulation) + + if self:IsInUtero() then + + -- Set default radio. + self:SetDefaultRadio(Frequency, Modulation) + + -- Debug info. + self:T2(self.lid..string.format("Switching radio to frequency %.3f MHz %s when GROUP is SPAWNED", self.radioDefault.Freq, UTILS.GetModulationName(self.radioDefault.Modu))) + + elseif self:IsAlive() then + + Frequency=Frequency or self.radioDefault.Freq + Modulation=Modulation or self.radioDefault.Modu + + if self.isAircraft and not self.radio.On then + self.group:SetOption(AI.Option.Air.id.SILENCE, false) + end + + -- Give command + self.group:CommandSetFrequency(Frequency, Modulation) + + -- Update current settings. + self.radio.Freq=Frequency + self.radio.Modu=Modulation + self.radio.On=true + + -- Debug info. + self:T(self.lid..string.format("Switching radio to frequency %.3f MHz %s", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu))) + + else + self:E(self.lid.."ERROR: Cound not set Radio! Group is not alive or not in utero any more") + end + + return self +end + +--- Turn radio off. +-- @param #OPSGROUP self +-- @return #OPSGROUP self +function OPSGROUP:TurnOffRadio() + + if self:IsAlive() then + + if self.isAircraft then + + -- Set group to be silient. + self.group:SetOption(AI.Option.Air.id.SILENCE, true) + + -- Radio is off. + self.radio.On=false + + self:T(self.lid..string.format("Switching radio OFF")) + else + self:E(self.lid.."ERROR: Radio can only be turned off for aircraft!") + end + + end + + return self +end + + + +--- Set default formation. +-- @param #OPSGROUP self +-- @param #number Formation The formation the groups flies in. +-- @return #OPSGROUP self +function OPSGROUP:SetDefaultFormation(Formation) + + self.optionDefault.Formation=Formation + + return self +end + +--- Switch to a specific formation. +-- @param #OPSGROUP self +-- @param #number Formation New formation the group will fly in. Default is the setting of `SetDefaultFormation()`. +-- @return #OPSGROUP self +function OPSGROUP:SwitchFormation(Formation) + + if self:IsAlive() then + + Formation=Formation or self.optionDefault.Formation + + if self.isAircraft then + + self.group:SetOption(AI.Option.Air.id.FORMATION, Formation) + + elseif self.isGround then + + -- Polymorphic and overwritten in ARMYGROUP. + + else + self:E(self.lid.."ERROR: Formation can only be set for aircraft or ground units!") + return self + end + + -- Set current formation. + self.option.Formation=Formation + + -- Debug info. + self:T(self.lid..string.format("Switching formation to %d", self.option.Formation)) + + end + + return self +end + + + +--- Set default callsign. +-- @param #OPSGROUP self +-- @param #number CallsignName Callsign name. +-- @param #number CallsignNumber Callsign number. +-- @return #OPSGROUP self +function OPSGROUP:SetDefaultCallsign(CallsignName, CallsignNumber) + + self.callsignDefault={} + self.callsignDefault.NumberSquad=CallsignName + self.callsignDefault.NumberGroup=CallsignNumber or 1 + + return self +end + +--- Switch to a specific callsign. +-- @param #OPSGROUP self +-- @param #number CallsignName Callsign name. +-- @param #number CallsignNumber Callsign number. +-- @return #OPSGROUP self +function OPSGROUP:SwitchCallsign(CallsignName, CallsignNumber) + + if self:IsInUtero() then + + -- Set default callsign. We switch to this when group is spawned. + self:SetDefaultCallsign(CallsignName, CallsignNumber) + + elseif self:IsAlive() then + + CallsignName=CallsignName or self.callsignDefault.NumberSquad + CallsignNumber=CallsignNumber or self.callsignDefault.NumberGroup + + -- Set current callsign. + self.callsign.NumberSquad=CallsignName + self.callsign.NumberGroup=CallsignNumber + + -- Debug. + self:T(self.lid..string.format("Switching callsign to %d-%d", self.callsign.NumberSquad, self.callsign.NumberGroup)) + + -- Give command to change the callsign. + self.group:CommandSetCallsign(self.callsign.NumberSquad, self.callsign.NumberGroup) + + else + --TODO: Error + end + + return self +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Element and Group Status Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Check if all elements of the group have the same status (or are dead). +-- @param #OPSGROUP self +-- @return #OPSGROUP self +function OPSGROUP:_UpdatePosition() + + if self:IsAlive() then + + -- Backup last state to monitor differences. + self.positionLast=self.position or self:GetVec3() + self.headingLast=self.heading or self:GetHeading() + self.orientXLast=self.orientX or self:GetOrientationX() + self.velocityLast=self.velocity or self.group:GetVelocityMPS() + + -- Current state. + self.position=self:GetVec3() + self.heading=self:GetHeading() + self.orientX=self:GetOrientationX() + self.velocity=self:GetVelocity() + + -- Update time. + local Tnow=timer.getTime() + self.dTpositionUpdate=self.TpositionUpdate and Tnow-self.TpositionUpdate or 0 + self.TpositionUpdate=Tnow + + if not self.traveldist then + self.traveldist=0 + end + + self.travelds=UTILS.VecNorm(UTILS.VecSubstract(self.position, self.positionLast)) + + -- Add up travelled distance. + + self.traveldist=self.traveldist+self.travelds + + -- Debug info. + --env.info(string.format("FF Traveled %.1f m", self.traveldist)) + + end + + return self +end + --- Check if all elements of the group have the same status (or are dead). -- @param #OPSGROUP self -- @param #string unitname Name of unit. @@ -2578,7 +4389,11 @@ function OPSGROUP:_UpdateStatus(element, newstatus, airbase) --- if self:_AllSimilarStatus(newstatus) then - self:Landed(airbase) + if self:IsLandingAt() then + self:LandedAt() + else + self:Landed(airbase) + end end elseif newstatus==OPSGROUP.ElementStatus.ARRIVED then @@ -2603,7 +4418,7 @@ function OPSGROUP:_UpdateStatus(element, newstatus, airbase) --- if self:_AllSimilarStatus(newstatus) then - self:Dead() + self:__Dead(-1) end end @@ -2683,11 +4498,14 @@ function OPSGROUP:GetAmmoTot() Ammo.Guns=0 Ammo.Rockets=0 Ammo.Bombs=0 + Ammo.Torpedos=0 Ammo.Missiles=0 Ammo.MissilesAA=0 Ammo.MissilesAG=0 Ammo.MissilesAS=0 - + Ammo.MissilesCR=0 + Ammo.MissilesSA=0 + for _,_unit in pairs(units) do local unit=_unit --Wrapper.Unit#UNIT @@ -2701,10 +4519,13 @@ function OPSGROUP:GetAmmoTot() Ammo.Guns=Ammo.Guns+ammo.Guns Ammo.Rockets=Ammo.Rockets+ammo.Rockets Ammo.Bombs=Ammo.Bombs+ammo.Bombs + Ammo.Torpedos=Ammo.Torpedos+ammo.Torpedos Ammo.Missiles=Ammo.Missiles+ammo.Missiles Ammo.MissilesAA=Ammo.MissilesAA+ammo.MissilesAA Ammo.MissilesAG=Ammo.MissilesAG+ammo.MissilesAG Ammo.MissilesAS=Ammo.MissilesAS+ammo.MissilesAS + Ammo.MissilesCR=Ammo.MissilesCR+ammo.MissilesCR + Ammo.MissilesSA=Ammo.MissilesSA+ammo.MissilesSA end @@ -2845,7 +4666,6 @@ function OPSGROUP:GetAmmoUnit(unit, display) else self:T3(self.lid..text) end - MESSAGE:New(text, 10):ToAllIf(display) -- Total amount of ammunition. nammo=nshells+nrockets+nmissiles+nbombs+ntorps @@ -2889,6 +4709,56 @@ function OPSGROUP:_MissileCategoryName(categorynumber) return cat end +--- Check if group got stuck. +-- @param #OPSGROUP self +function OPSGROUP:_CheckStuck() + + -- Holding means we are not stuck. + if self:IsHolding() then + return + end + + -- Current time. + local Tnow=timer.getTime() + + -- Expected speed in m/s. + local ExpectedSpeed=self:GetExpectedSpeed() + + -- Current speed in m/s. + local speed=self:GetVelocity() + + -- Check speed. + if speed<0.5 then + + if ExpectedSpeed>0 and not self.stuckTimestamp then + self:T2(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected", speed, ExpectedSpeed)) + self.stuckTimestamp=Tnow + self.stuckVec3=self:GetVec3() + end + + else + -- Moving (again). + self.stuckTimestamp=nil + end + + -- Somehow we are not moving... + if self.stuckTimestamp then + + -- Time we are holding. + local holdtime=Tnow-self.stuckTimestamp + + if holdtime>=10*60 then + + self:E(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected for %d sec", speed, ExpectedSpeed, holdtime)) + + --TODO: Stuck event! + + end + + end + +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Squadron.lua b/Moose Development/Moose/Ops/Squadron.lua index 5ca14eec4..a99891e2c 100644 --- a/Moose Development/Moose/Ops/Squadron.lua +++ b/Moose Development/Moose/Ops/Squadron.lua @@ -17,14 +17,19 @@ --- SQUADRON class. -- @type SQUADRON -- @field #string ClassName Name of the class. --- @field #boolean Debug Debug mode. Messages to all about status. +-- @field #number verbose Verbosity level. -- @field #string lid Class id string for output to DCS log file. -- @field #string name Name of the squadron. -- @field #string templatename Name of the template group. -- @field #string aircrafttype Type of the airframe the squadron is using. -- @field Wrapper.Group#GROUP templategroup Template group. +-- @field #number ngrouping User defined number of units in the asset group. -- @field #table assets Squadron assets. -- @field #table missiontypes Capabilities (mission types and performances) of the squadron. +-- @field #number fuellow Low fuel threshold. +-- @field #boolean fuellowRefuel If `true`, flight tries to refuel at the nearest tanker. +-- @field #number maintenancetime Time in seconds needed for maintenance of a returned flight. +-- @field #number repairtime Time in seconds for each -- @field #string livery Livery of the squadron. -- @field #number skill Skill of squadron members. -- @field #number modex Modex. @@ -33,13 +38,11 @@ -- @field #number callsigncounter Counter to increase callsign names for new assets. -- @field Ops.AirWing#AIRWING airwing The AIRWING object the squadron belongs to. -- @field #number Ngroups Number of asset flight groups this squadron has. --- @field #number engageRange Engagement range in meters. +-- @field #number engageRange Mission range in meters. -- @field #string attribute Generalized attribute of the squadron template group. -- @field #number tankerSystem For tanker squads, the refuel system used (boom=0 or probpe=1). Default nil. --- @field #number refuelSystem For refuelable squads, the refuel system used (boom=0 or probpe=1). Default nil. --- @field #number TACANmin TACAN min channel. --- @field #number TACANmax TACAN max channel. --- @field #table TACANused Table of used TACAN channels. +-- @field #number refuelSystem For refuelable squads, the refuel system used (boom=0 or probe=1). Default nil. +-- @field #table tacanChannel List of TACAN channels available to the squadron. -- @field #number radioFreq Radio frequency in MHz the squad uses. -- @field #number radioModu Radio modulation the squad uses. -- @extends Core.Fsm#FSM @@ -48,7 +51,7 @@ -- -- === -- --- ![Banner Image](..\Presentations\Squadron\SQUADRON_Main.jpg) +-- ![Banner Image](..\Presentations\OPS\Squadron\_Main.png) -- -- # The SQUADRON Concept -- @@ -59,13 +62,15 @@ -- @field #SQUADRON SQUADRON = { ClassName = "SQUADRON", - Debug = nil, + verbose = 0, lid = nil, name = nil, templatename = nil, aircrafttype = nil, assets = {}, missiontypes = {}, + repairtime = 0, + maintenancetime= 0, livery = nil, skill = nil, modex = nil, @@ -77,14 +82,12 @@ SQUADRON = { engageRange = nil, tankerSystem = nil, refuelSystem = nil, - TACANmin = nil, - TACANmax = nil, - TACANused = {}, + tacanChannel = {}, } --- SQUADRON class version. -- @field #string version -SQUADRON.version="0.1.0" +SQUADRON.version="0.5.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -129,15 +132,20 @@ function SQUADRON:New(TemplateGroupName, Ngroups, SquadronName) -- Defaults. self.Ngroups=Ngroups or 3 - self:SetEngagementRange() + self:SetMissionRange() + self:SetSkill(AI.Skill.GOOD) + --self:SetVerbosity(0) -- Everyone can ORBIT. - self:AddMissonCapability(AUFTRAG.Type.ORBIT) + self:AddMissionCapability(AUFTRAG.Type.ORBIT) + -- Generalized attribute. self.attribute=self.templategroup:GetAttribute() + -- Aircraft type. self.aircrafttype=self.templategroup:GetTypeName() + -- Refueling system. self.refuelSystem=select(2, self.templategroup:GetUnit(1):IsRefuelable()) self.tankerSystem=select(2, self.templategroup:GetUnit(1):IsTanker()) @@ -149,8 +157,10 @@ function SQUADRON:New(TemplateGroupName, Ngroups, SquadronName) -- From State --> Event --> To State self:AddTransition("Stopped", "Start", "OnDuty") -- Start FSM. self:AddTransition("*", "Status", "*") -- Status update. + self:AddTransition("OnDuty", "Pause", "Paused") -- Pause squadron. self:AddTransition("Paused", "Unpause", "OnDuty") -- Unpause squadron. + self:AddTransition("*", "Stop", "Stopped") -- Stop squadron. @@ -187,13 +197,10 @@ function SQUADRON:New(TemplateGroupName, Ngroups, SquadronName) -- Debug trace. if false then - self.Debug=true BASE:TraceOnOff(true) BASE:TraceClass(self.ClassName) BASE:TraceLevel(1) end - self.Debug=true - return self end @@ -232,6 +239,26 @@ function SQUADRON:SetSkill(Skill) return self end +--- Set verbosity level. +-- @param #SQUADRON self +-- @param #number VerbosityLevel Level of output (higher=more). Default 0. +-- @return #SQUADRON self +function SQUADRON:SetVerbosity(VerbosityLevel) + self.verbose=VerbosityLevel or 0 + return self +end + +--- Set turnover and repair time. If an asset returns from a mission to the airwing, it will need some time until the asset is available for further missions. +-- @param #SQUADRON self +-- @param #number MaintenanceTime Time in minutes it takes until a flight is combat ready again. Default is 0 min. +-- @param #number RepairTime Time in minutes it takes to repair a flight for each life point taken. Default is 0 min. +-- @return #SQUADRON self +function SQUADRON:SetTurnoverTime(MaintenanceTime, RepairTime) + self.maintenancetime=MaintenanceTime and MaintenanceTime*60 or 0 + self.repairtime=RepairTime and RepairTime*60 or 0 + return self +end + --- Set radio frequency and modulation the squad uses. -- @param #SQUADRON self -- @param #number Frequency Radio frequency in MHz. Default 251 MHz. @@ -244,12 +271,23 @@ function SQUADRON:SetRadio(Frequency, Modulation) return self end +--- Set number of units in groups. +-- @param #SQUADRON self +-- @param #number nunits Number of units. Must be >=1 and <=4. Default 2. +-- @return #SQUADRON self +function SQUADRON:SetGrouping(nunits) + self.ngrouping=nunits or 2 + if self.ngrouping<1 then self.ngrouping=1 end + if self.ngrouping>4 then self.ngrouping=4 end + return self +end + --- Set mission types this squadron is able to perform. -- @param #SQUADRON self -- @param #table MissionTypes Table of mission types. Can also be passed as a #string if only one type. -- @param #number Performance Performance describing how good this mission can be performed. Higher is better. Default 50. Max 100. -- @return #SQUADRON self -function SQUADRON:AddMissonCapability(MissionTypes, Performance) +function SQUADRON:AddMissionCapability(MissionTypes, Performance) -- Ensure Missiontypes is a table. if MissionTypes and type(MissionTypes)~="table" then @@ -276,7 +314,7 @@ function SQUADRON:AddMissonCapability(MissionTypes, Performance) end -- Debug info. - self:I(self.missiontypes) + self:T2(self.missiontypes) return self end @@ -319,12 +357,12 @@ function SQUADRON:GetMissionPeformance(MissionType) return -1 end ---- Set max engagement range. +--- Set max mission range. Only missions in a circle of this radius around the squadron airbase are executed. -- @param #SQUADRON self --- @param #number EngageRange Engagement range in NM. Default 80 NM. +-- @param #number Range Range in NM. Default 100 NM. -- @return #SQUADRON self -function SQUADRON:SetEngagementRange(EngageRange) - self.engageRange=UTILS.NMToMeters(EngageRange or 80) +function SQUADRON:SetMissionRange(Range) + self.engageRange=UTILS.NMToMeters(Range or 100) return self end @@ -352,6 +390,28 @@ function SQUADRON:SetModex(Modex, Prefix, Suffix) return self end +--- Set low fuel threshold. +-- @param #SQUADRON self +-- @param #number LowFuel Low fuel threshold in percent. Default 25. +-- @return #SQUADRON self +function SQUADRON:SetFuelLowThreshold(LowFuel) + self.fuellow=LowFuel or 25 + return self +end + +--- Set if low fuel threshold is reached, flight tries to refuel at the neares tanker. +-- @param #SQUADRON self +-- @param #boolean switch If true or nil, flight goes for refuelling. If false, turn this off. +-- @return #SQUADRON self +function SQUADRON:SetFuelLowRefuel(switch) + if switch==false then + self.fuellowRefuel=false + else + self.fuellowRefuel=true + end + return self +end + --- Set airwing. -- @param #SQUADRON self -- @param Ops.AirWing#AIRWING Airwing The airwing. @@ -361,7 +421,6 @@ function SQUADRON:SetAirwing(Airwing) return self end - --- Add airwing asset to squadron. -- @param #SQUADRON self -- @param Ops.AirWing#AIRWING.SquadronAsset Asset The airwing asset. @@ -389,6 +448,29 @@ function SQUADRON:DelAsset(Asset) return self end +--- Remove airwing asset group from squadron. +-- @param #SQUADRON self +-- @param #string GroupName Name of the asset group. +-- @return #SQUADRON self +function SQUADRON:DelGroup(GroupName) + for i,_asset in pairs(self.assets) do + local asset=_asset --Ops.AirWing#AIRWING.SquadronAsset + if GroupName==asset.spawngroupname then + self:T2(self.lid..string.format("Removing asset %s", asset.spawngroupname)) + table.remove(self.assets, i) + break + end + end + return self +end + +--- Get name of the squadron +-- @param #SQUADRON self +-- @return #string Name of the squadron. +function SQUADRON:GetName() + return self.name +end + --- Get radio frequency and modulation. -- @param #SQUADRON self -- @return #number Radio frequency in MHz. @@ -410,6 +492,7 @@ function SQUADRON:GetCallsign(Asset) for i=1,Asset.nunits do local callsign={} + callsign[1]=self.callsignName callsign[2]=math.floor(self.callsigncounter / 10) callsign[3]=self.callsigncounter % 10 @@ -456,23 +539,34 @@ function SQUADRON:GetModex(Asset) end + +--- Add TACAN channels to the squadron. Note that channels can only range from 1 to 126. +-- @param #SQUADRON self +-- @param #number ChannelMin Channel. +-- @param #number ChannelMax Channel. +-- @return #SQUADRON self +-- @usage mysquad:AddTacanChannel(64,69) -- adds channels 64, 65, 66, 67, 68, 69 +function SQUADRON:AddTacanChannel(ChannelMin, ChannelMax) + + ChannelMax=ChannelMax or ChannelMin + + for i=ChannelMin,ChannelMax do + self.tacanChannel[i]=true + end + +end + --- Get an unused TACAN channel. -- @param #SQUADRON self --- @param Ops.AirWing#AIRWING.SquadronAsset Asset The airwing asset. -- @return #number TACAN channel or *nil* if no channel is free. -function SQUADRON:GetTACAN() +function SQUADRON:FetchTacan() - if self.TACANmin and self.TACANmax then - - for channel=self.TACANmin, self.TACANmax do - - if not self.TACANused[channel] then - self.TACANused[channel]=true - return channel - end - + for channel,free in pairs(self.tacanChannel) do + if free then + self:T(self.lid..string.format("Checking out Tacan channel %d", channel)) + self.tacanChannel[channel]=false + return channel end - end return nil @@ -481,8 +575,9 @@ end --- "Return" a used TACAN channel. -- @param #SQUADRON self -- @param #number channel The channel that is available again. -function SQUADRON:ReturnTACAN(channel) - self.TACANused[channel]=false +function SQUADRON:ReturnTacan(channel) + self:T(self.lid..string.format("Returning Tacan channel %d", channel)) + self.tacanChannel[channel]=true end --- Check if squadron is "OnDuty". @@ -520,7 +615,7 @@ function SQUADRON:onafterStart(From, Event, To) -- Short info. local text=string.format("Starting SQUADRON", self.name) - self:I(self.lid..text) + self:T(self.lid..text) -- Start the status monitoring. self:__Status(-1) @@ -533,18 +628,34 @@ end -- @param #string To To state. function SQUADRON:onafterStatus(From, Event, To) - -- FSM state. - local fsmstate=self:GetState() - - -- Check if group has detected any units. - --self:_CheckAssetStatus() + if self.verbose>=1 then - -- Short info. - local text=string.format("Status %s: Assets %d", fsmstate, #self.assets) - self:I(self.lid..text) + -- FSM state. + local fsmstate=self:GetState() + + local callsign=self.callsignName and UTILS.GetCallsignName(self.callsignName) or "N/A" + local modex=self.modex and self.modex or -1 + local skill=self.skill and tostring(self.skill) or "N/A" + + local NassetsTot=#self.assets + local NassetsInS=self:CountAssetsInStock() + local NassetsQP=0 ; local NassetsP=0 ; local NassetsQ=0 + if self.airwing then + NassetsQP, NassetsP, NassetsQ=self.airwing:CountAssetsOnMission(nil, self) + end + + -- Short info. + local text=string.format("%s [Type=%s, Call=%s, Modex=%d, Skill=%s]: Assets Total=%d, Stock=%d, Mission=%d [Active=%d, Queue=%d]", + fsmstate, self.aircrafttype, callsign, modex, skill, NassetsTot, NassetsInS, NassetsQP, NassetsP, NassetsQ) + self:I(self.lid..text) + + -- Check if group has detected any units. + self:_CheckAssetStatus() + + end if not self:IsStopped() then - self:__Status(-30) + self:__Status(-60) end end @@ -553,9 +664,85 @@ end -- @param #SQUADRON self function SQUADRON:_CheckAssetStatus() - for _,_asset in pairs(self.assets) do - local asset=_asset + if self.verbose>=2 and #self.assets>0 then + + local text="" + for j,_asset in pairs(self.assets) do + local asset=_asset --Ops.AirWing#AIRWING.SquadronAsset + + -- Text. + text=text..string.format("\n[%d] %s (%s*%d): ", j, asset.spawngroupname, asset.unittype, asset.nunits) + + if asset.spawned then + + --- + -- Spawned + --- + + -- Mission info. + local mission=self.airwing and self.airwing:GetAssetCurrentMission(asset) or false + if mission then + local distance=asset.flightgroup and UTILS.MetersToNM(mission:GetTargetDistance(asset.flightgroup.group:GetCoordinate())) or 0 + text=text..string.format("Mission %s - %s: Status=%s, Dist=%.1f NM", mission.name, mission.type, mission.status, distance) + else + text=text.."Mission None" + end + + -- Flight status. + text=text..", Flight: " + if asset.flightgroup and asset.flightgroup:IsAlive() then + local status=asset.flightgroup:GetState() + local fuelmin=asset.flightgroup:GetFuelMin() + local fuellow=asset.flightgroup:IsFuelLow() + local fuelcri=asset.flightgroup:IsFuelCritical() + + text=text..string.format("%s Fuel=%d", status, fuelmin) + if fuelcri then + text=text.." (Critical!)" + elseif fuellow then + text=text.." (Low)" + end + + local lifept, lifept0=asset.flightgroup:GetLifePoints() + text=text..string.format(", Life=%d/%d", lifept, lifept0) + + local ammo=asset.flightgroup:GetAmmoTot() + text=text..string.format(", Ammo=%d [G=%d, R=%d, B=%d, M=%d]", ammo.Total,ammo.Guns, ammo.Rockets, ammo.Bombs, ammo.Missiles) + else + text=text.."N/A" + end + + -- Payload info. + local payload=asset.payload and table.concat(self.airwing:GetPayloadMissionTypes(asset.payload), ", ") or "None" + text=text..", Payload={"..payload.."}" + + else + + --- + -- In Stock + --- + text=text..string.format("In Stock") + + if self:IsRepaired(asset) then + text=text..", Combat Ready" + else + + text=text..string.format(", Repaired in %d sec", self:GetRepairTime(asset)) + + if asset.damage then + text=text..string.format(" (Damage=%.1f)", asset.damage) + end + end + + if asset.Treturned then + local T=timer.getAbsTime()-asset.Treturned + text=text..string.format(", Returned for %d sec", T) + end + + end + end + self:I(self.lid..text) end end @@ -575,6 +762,8 @@ function SQUADRON:onafterStop(From, Event, To) self:DelAsset(asset) end + self.CallScheduler:Clear() + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -592,13 +781,13 @@ function SQUADRON:CanMission(Mission) -- On duty?= if not self:IsOnDuty() then - self:I(self.lid..string.format("Squad in not OnDuty but in state %s. Cannot do mission %s with target %s", self:GetState(), Mission.name, Mission:GetTargetName())) + self:T(self.lid..string.format("Squad in not OnDuty but in state %s. Cannot do mission %s with target %s", self:GetState(), Mission.name, Mission:GetTargetName())) return false end -- Check mission type. WARNING: This assumes that all assets of the squad can do the same mission types! if not self:CheckMissionType(Mission.type, self:GetMissionTypes()) then - self:I(self.lid..string.format("INFO: Squad cannot do mission type %s (%s, %s)", Mission.type, Mission.name, Mission:GetTargetName())) + self:T(self.lid..string.format("INFO: Squad cannot do mission type %s (%s, %s)", Mission.type, Mission.name, Mission:GetTargetName())) return false end @@ -608,7 +797,7 @@ function SQUADRON:CanMission(Mission) if Mission.refuelSystem and Mission.refuelSystem==self.tankerSystem then -- Correct refueling system. else - self:I(self.lid..string.format("INFO: Wrong refueling system requested=%s != %s=available", tostring(Mission.refuelSystem), tostring(self.tankerSystem))) + self:T(self.lid..string.format("INFO: Wrong refueling system requested=%s != %s=available", tostring(Mission.refuelSystem), tostring(self.tankerSystem))) return false end @@ -622,14 +811,14 @@ function SQUADRON:CanMission(Mission) -- Set range is valid. Mission engage distance can overrule the squad engage range. if TargetDistance>engagerange then - self:I(self.lid..string.format("INFO: Squad is not in range. Target dist=%d > %d NM max engage Range", UTILS.MetersToNM(TargetDistance), UTILS.MetersToNM(engagerange))) + self:I(self.lid..string.format("INFO: Squad is not in range. Target dist=%d > %d NM max mission Range", UTILS.MetersToNM(TargetDistance), UTILS.MetersToNM(engagerange))) return false end return true end ---- Get assets for a mission. +--- Count assets in airwing (warehous) stock. -- @param #SQUADRON self -- @return #number Assets not spawned. function SQUADRON:CountAssetsInStock() @@ -650,11 +839,12 @@ end --- Get assets for a mission. -- @param #SQUADRON self -- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @param #number Nplayloads Number of payloads available. -- @return #table Assets that can do the required mission. -function SQUADRON:RecruitAssets(Mission) +function SQUADRON:RecruitAssets(Mission, Npayloads) -- Number of payloads available. - local Npayloads=self.airwing:CountPayloadsInStock(Mission.type, self.aircrafttype) + Npayloads=Npayloads or self.airwing:CountPayloadsInStock(Mission.type, self.aircrafttype, Mission.payloads) local assets={} @@ -670,12 +860,12 @@ function SQUADRON:RecruitAssets(Mission) -- Asset is already on a mission. --- - -- Check if this asset is currently on a PATROL mission (STARTED or EXECUTING). - if self.airwing:IsAssetOnMission(asset, AUFTRAG.Type.PATROL) and Mission.type==AUFTRAG.Type.INTERCEPT then + -- Check if this asset is currently on a GCICAP mission (STARTED or EXECUTING). + if self.airwing:IsAssetOnMission(asset, AUFTRAG.Type.GCICAP) and Mission.type==AUFTRAG.Type.INTERCEPT then -- Check if the payload of this asset is compatible with the mission. - -- Note: we do not check the payload as an asset that is on a PATROL mission should be able to do an INTERCEPT as well! - self:I(self.lid.."Adding asset on PATROL mission for an INTERCEPT mission") + -- Note: we do not check the payload as an asset that is on a GCICAP mission should be able to do an INTERCEPT as well! + self:I(self.lid.."Adding asset on GCICAP mission for an INTERCEPT mission") table.insert(assets, asset) end @@ -683,7 +873,7 @@ function SQUADRON:RecruitAssets(Mission) else --- - -- Asset as no current mission + -- Asset as NO current mission --- if asset.spawned then @@ -703,7 +893,8 @@ function SQUADRON:RecruitAssets(Mission) if Mission.type==AUFTRAG.Type.INTERCEPT then combatready=flightgroup:CanAirToAir() else - combatready=flightgroup:CanAirToGround() + local excludeguns=Mission.type==AUFTRAG.Type.BOMBING or Mission.type==AUFTRAG.Type.BOMBRUNWAY or Mission.type==AUFTRAG.Type.BOMBCARPET or Mission.type==AUFTRAG.Type.SEAD or Mission.type==AUFTRAG.Type.ANTISHIP + combatready=flightgroup:CanAirToGround(excludeguns) end -- No more attacks if fuel is already low. Safety first! @@ -712,7 +903,7 @@ function SQUADRON:RecruitAssets(Mission) end -- Check if in a state where we really do not want to fight any more. - if flightgroup:IsLanding() or flightgroup:IsLanded() or flightgroup:IsArrived() or flightgroup:IsDead() then + if flightgroup:IsHolding() or flightgroup:IsLanding() or flightgroup:IsLanded() or flightgroup:IsArrived() or flightgroup:IsDead() or flightgroup:IsStopped() then combatready=false end @@ -731,7 +922,7 @@ function SQUADRON:RecruitAssets(Mission) --- -- Check that asset is not already requested for another mission. - if Npayloads>0 and not asset.requested then + if Npayloads>0 and self:IsRepaired(asset) and (not asset.requested) then -- Add this asset to the selection. table.insert(assets, asset) @@ -749,6 +940,51 @@ function SQUADRON:RecruitAssets(Mission) end +--- Get the time an asset needs to be repaired. +-- @param #SQUADRON self +-- @param Ops.AirWing#AIRWING.SquadronAsset Asset The asset. +-- @return #number Time in seconds until asset is repaired. +function SQUADRON:GetRepairTime(Asset) + + if Asset.Treturned then + + local t=self.maintenancetime + t=t+Asset.damage*self.repairtime + + -- Seconds after returned. + local dt=timer.getAbsTime()-Asset.Treturned + + local T=t-dt + + return T + else + return 0 + end + +end + +--- Checks if a mission type is contained in a table of possible types. +-- @param #SQUADRON self +-- @param Ops.AirWing#AIRWING.SquadronAsset Asset The asset. +-- @return #boolean If true, the requested mission type is part of the possible mission types. +function SQUADRON:IsRepaired(Asset) + + if Asset.Treturned then + local Tnow=timer.getAbsTime() + local Trepaired=Asset.Treturned+self.maintenancetime + if Tnow>=Trepaired then + return true + else + return false + end + + else + return true + end + +end + + --- Checks if a mission type is contained in a table of possible types. -- @param #SQUADRON self -- @param #string MissionType The requested mission type. diff --git a/Moose Development/Moose/Ops/Target.lua b/Moose Development/Moose/Ops/Target.lua new file mode 100644 index 000000000..b6f9c396c --- /dev/null +++ b/Moose Development/Moose/Ops/Target.lua @@ -0,0 +1,1020 @@ +--- **Ops** - Target. +-- +-- **Main Features:** +-- +-- * Manages target, number alive, life points, damage etc. +-- * Events when targets are damaged or destroyed +-- * Various target objects: UNIT, GROUP, STATIC, AIRBASE, COORDINATE, SET_GROUP, SET_UNIT +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Ops.Target +-- @image OPS_Target.png + + +--- TARGET class. +-- @type TARGET +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode. Messages to all about status. +-- @field #number verbose Verbosity level. +-- @field #string lid Class id string for output to DCS log file. +-- @field #table targets Table of target objects. +-- @field #number targetcounter Running number to generate target object IDs. +-- @field #number life Total life points on last status update. +-- @field #number life0 Total life points of completely healthy targets. +-- @field #number threatlevel0 Initial threat level. +-- @field #number category Target category (Ground, Air, Sea). +-- @field #number Ntargets0 Number of initial targets. +-- @extends Core.Fsm#FSM + +--- **It is far more important to be able to hit the target than it is to haggle over who makes a weapon or who pulls a trigger** -- Dwight D. Eisenhower +-- +-- === +-- +-- ![Banner Image](..\Presentations\OPS\Target\_Main.pngs) +-- +-- # The TARGET Concept +-- +-- Define a target of your mission and monitor its status. Events are triggered when the target is damaged or destroyed. +-- +-- A target can consist of one or multiple "objects". +-- +-- +-- @field #TARGET +TARGET = { + ClassName = "TARGET", + Debug = nil, + verbose = 0, + lid = nil, + targets = {}, + targetcounter = 0, + life = 0, + life0 = 0, + Ntargets0 = 0, + threatlevel0 = 0 +} + + +--- Type. +-- @type TARGET.ObjectType +-- @field #string GROUP Target is a GROUP object. +-- @field #string UNIT Target is a UNIT object. +-- @field #string STATIC Target is a STATIC object. +-- @field #string SCENERY Target is a SCENERY object. +-- @field #string COORDINATE Target is a COORDINATE. +-- @field #string AIRBASE Target is an AIRBASE. +TARGET.ObjectType={ + GROUP="Group", + UNIT="Unit", + STATIC="Static", + SCENERY="Scenery", + COORDINATE="Coordinate", + AIRBASE="Airbase", +} + + +--- Category. +-- @type TARGET.Category +-- @field #string AIRCRAFT +-- @field #string GROUND +-- @field #string NAVAL +-- @field #string AIRBASE +-- @field #string COORDINATE +TARGET.Category={ + AIRCRAFT="Aircraft", + GROUND="Grund", + NAVAL="Naval", + AIRBASE="Airbase", + COORDINATE="Coordinate", +} + +--- Object status. +-- @type TARGET.ObjectStatus +-- @field #string ALIVE Object is alive. +-- @field #string DEAD Object is dead. +TARGET.ObjectStatus={ + ALIVE="Alive", + DEAD="Dead", +} +--- Type. +-- @type TARGET.Object +-- @field #number ID Target unique ID. +-- @field #string Name Target name. +-- @field #string Type Target type. +-- @field Wrapper.Positionable#POSITIONABLE Object The object, which can be many things, e.g. a UNIT, GROUP, STATIC, AIRBASE or COORDINATE object. +-- @field #number Life Life points on last status update. +-- @field #number Life0 Life points of completely healthy target. +-- @field #string Status Status "Alive" or "Dead". +-- @field Core.Point#COORDINATE Coordinate of the target object. + +--- Global target ID counter. +_TARGETID=0 + +--- TARGET class version. +-- @field #string version +TARGET.version="0.1.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: A lot. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new TARGET object and start the FSM. +-- @param #TARGET self +-- @param #table TargetObject Target object. +-- @return #TARGET self +function TARGET:New(TargetObject) + + -- Inherit everything from INTEL class. + local self=BASE:Inherit(self, FSM:New()) --#TARGET + + -- Increase counter. + _TARGETID=_TARGETID+1 + + -- Add object. + self:AddObject(TargetObject) + + -- Get first target. + local Target=self.targets[1] --#TARGET.Object + + if not Target then + self:E(self.lid.."ERROR: No valid TARGET!") + return nil + end + + -- Target Name. + self.name=self:GetTargetName(Target) + + -- Target category. + self.category=self:GetTargetCategory(Target) + + -- Log ID. + self.lid=string.format("TARGET #%03d %s | ", _TARGETID, self.category) + + -- Start state. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Alive") -- Start FSM. + self:AddTransition("*", "Status", "*") -- Status update. + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + self:AddTransition("*", "ObjectDamaged", "*") -- A Target was damaged. + self:AddTransition("*", "ObjectDestroyed", "*") -- A Target was destroyed. + self:AddTransition("*", "ObjectRemoved", "*") -- A Target was removed. + + self:AddTransition("*", "Damaged", "*") -- Target was damaged. + self:AddTransition("*", "Destroyed", "Dead") -- Target was completely destroyed. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the TARGET. Initializes parameters and starts event handlers. + -- @function [parent=#TARGET] Start + -- @param #TARGET self + + --- Triggers the FSM event "Start" after a delay. Starts the TARGET. Initializes parameters and starts event handlers. + -- @function [parent=#TARGET] __Start + -- @param #TARGET self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the TARGET and all its event handlers. + -- @param #TARGET self + + --- Triggers the FSM event "Stop" after a delay. Stops the TARGET and all its event handlers. + -- @function [parent=#TARGET] __Stop + -- @param #TARGET self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Status". + -- @function [parent=#TARGET] Status + -- @param #TARGET self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#TARGET] __Status + -- @param #TARGET self + -- @param #number delay Delay in seconds. + + + + -- Start. + self:__Start(-1) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create target data from a given object. +-- @param #TARGET self +-- @param Wrapper.Positionable#POSITIONABLE Object The target GROUP, UNIT, STATIC, AIRBASE or COORDINATE. +function TARGET:AddObject(Object) + + if Object:IsInstanceOf("SET_GROUP") or Object:IsInstanceOf("SET_UNIT") then + + --- + -- Sets + --- + + local set=Object --Core.Set#SET_GROUP + + for _,object in pairs(set.Set) do + self:AddObject(object) + end + + else + + --- + -- Groups, Units, Statics, Airbases, Coordinates + --- + + self:_AddObject(Object) + + end + +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Start & Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. Starts the FLIGHTGROUP FSM and event handlers. +-- @param #TARGET self +-- @param Wrapper.Group#GROUP Group Flight group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function TARGET:onafterStart(From, Event, To) + + -- Short info. + local text=string.format("Starting Target") + self:T(self.lid..text) + + self:HandleEvent(EVENTS.Dead, self.OnEventUnitDeadOrLost) + self:HandleEvent(EVENTS.UnitLost, self.OnEventUnitDeadOrLost) + + self:HandleEvent(EVENTS.RemoveUnit, self.OnEventRemoveUnit) + + self:__Status(-1) +end + +--- On after "Status" event. +-- @param #TARGET self +-- @param Wrapper.Group#GROUP Group Flight group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function TARGET:onafterStatus(From, Event, To) + + -- FSM state. + local fsmstate=self:GetState() + + -- Update damage. + local damaged=false + for i,_target in pairs(self.targets) do + local target=_target --#TARGET.Object + + local life=target.Life + + target.Life=self:GetTargetLife(target) + + if target.Life=1 then + local text=string.format("%s: Targets=%d/%d Life=%.1f/%.1f Damage=%.1f", fsmstate, self:CountTargets(), self.Ntargets0, self:GetLife(), self:GetLife0(), self:GetDamage()) + if damaged then + text=text.." Damaged!" + end + self:I(self.lid..text) + end + + -- Log output verbose=2. + if self.verbose>=2 then + local text="Target:" + for i,_target in pairs(self.targets) do + local target=_target --#TARGET.Object + local damage=(1-target.Life/target.Life0)*100 + text=text..string.format("\n[%d] %s %s %s: Life=%.1f/%.1f, Damage=%.1f", i, target.Type, target.Name, target.Status, target.Life, target.Life0, damage) + end + self:I(self.lid..text) + end + + -- Update status again in 30 sec. + self:__Status(-30) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Events +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "ObjectDamaged" event. +-- @param #TARGET self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #TARGET.Object Target Target object. +function TARGET:onafterObjectDamaged(From, Event, To, Target) + + self:T(self.lid..string.format("Object %s damaged", Target.Name)) + +end + +--- On after "ObjectDestroyed" event. +-- @param #TARGET self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #TARGET.Object Target Target object. +function TARGET:onafterObjectDestroyed(From, Event, To, Target) + + self:T(self.lid..string.format("Object %s destroyed", Target.Name)) + + -- Set target status. + Target.Status=TARGET.ObjectStatus.DEAD + + -- Check if anyone is alive? + local dead=true + for _,_target in pairs(self.targets) do + local target=_target --#TARGET.Object + if target.Status==TARGET.ObjectStatus.ALIVE then + dead=false + end + end + + -- All dead ==> Trigger destroyed event. + if dead then + self:Destroyed() + end + +end + +--- On after "Damaged" event. +-- @param #TARGET self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function TARGET:onafterDamaged(From, Event, To) + + self:T(self.lid..string.format("Target damaged")) + +end + +--- On after "Destroyed" event. +-- @param #TARGET self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function TARGET:onafterDestroyed(From, Event, To) + + self:I(self.lid..string.format("Target destroyed")) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Event Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Event function handling the loss of a unit. +-- @param #TARGET self +-- @param Core.Event#EVENTDATA EventData Event data. +function TARGET:OnEventUnitDeadOrLost(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniUnitName then + + -- Debug info. + --self:T3(self.lid..string.format("EVENT: Unit %s dead or lost!", EventData.IniUnitName)) + + -- Get target. + local target=self:GetTargetByName(EventData.IniUnitName) + + -- Check if this is one of ours. + if target and target.Status==TARGET.ObjectStatus.ALIVE then + + -- Debug message. + self:T3(self.lid..string.format("EVENT: target unit %s dead or lost ==> destroyed", target.Name)) + + -- Trigger object destroyed event. + self:ObjectDestroyed(target) + + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Adding and Removing Targets +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create target data from a given object. +-- @param #TARGET self +-- @param Wrapper.Positionable#POSITIONABLE Object The target GROUP, UNIT, STATIC, AIRBASE or COORDINATE. +function TARGET:_AddObject(Object) + + local target={} --#TARGET.Object + + if Object:IsInstanceOf("GROUP") then + + local group=Object --Wrapper.Group#GROUP + + target.Type=TARGET.ObjectType.GROUP + target.Name=group:GetName() + + target.Coordinate=group:GetCoordinate() + + local units=group:GetUnits() + + target.Life=0 ; target.Life0=0 + for _,_unit in pairs(units or {}) do + local unit=_unit --Wrapper.Unit#UNIT + + local life=unit:GetLife() + + target.Life=target.Life+life + target.Life0=target.Life0+math.max(unit:GetLife0(), life) -- There was an issue with ships that life is greater life0, which cannot be! + + self.threatlevel0=self.threatlevel0+unit:GetThreatLevel() + + self.Ntargets0=self.Ntargets0+1 + end + + elseif Object:IsInstanceOf("UNIT") then + + local unit=Object --Wrapper.Unit#UNIT + + target.Type=TARGET.ObjectType.UNIT + target.Name=unit:GetName() + + target.Coordinate=unit:GetCoordinate() + + if unit and unit:IsAlive() then + target.Life=unit:GetLife() + target.Life0=math.max(unit:GetLife0(), target.Life) -- There was an issue with ships that life is greater life0! + + self.threatlevel0=self.threatlevel0+unit:GetThreatLevel() + + self.Ntargets0=self.Ntargets0+1 + end + + elseif Object:IsInstanceOf("STATIC") then + + local static=Object --Wrapper.Static#STATIC + + target.Type=TARGET.ObjectType.STATIC + target.Name=static:GetName() + + target.Coordinate=static:GetCoordinate() + + if static and static:IsAlive() then + target.Life0=1 + target.Life=1 + + self.Ntargets0=self.Ntargets0+1 + end + + elseif Object:IsInstanceOf("SCENERY") then + + local scenery=Object --Wrapper.Scenery#SCENERY + + target.Type=TARGET.ObjectType.SCENERY + target.Name=scenery:GetName() + + target.Coordinate=scenery:GetCoordinate() + + target.Life0=1 + target.Life=1 + + self.Ntargets0=self.Ntargets0+1 + + elseif Object:IsInstanceOf("AIRBASE") then + + local airbase=Object --Wrapper.Airbase#AIRBASE + + target.Type=TARGET.ObjectType.AIRBASE + target.Name=airbase:GetName() + + target.Coordinate=airbase:GetCoordinate() + + target.Life0=1 + target.Life=1 + + self.Ntargets0=self.Ntargets0+1 + + elseif Object:IsInstanceOf("COORDINATE") then + + local coord=Object --Core.Point#COORDINATE + + target.Type=TARGET.ObjectType.COORDINATE + target.Name=coord:ToStringMGRS() + + target.Coordinate=Object + + target.Life0=1 + target.Life=1 + + elseif Object:IsInstanceOf("ZONE_BASE") then + + local zone=Object --Core.Zone#ZONE_BASE + Object=zone:GetCoordinate() + + target.Type=TARGET.ObjectType.COORDINATE + target.Name=zone:GetName() + + target.Coordinate=Object + + target.Life0=1 + target.Life=1 + + else + self:E(self.lid.."ERROR: Unknown object type!") + return nil + end + + self.life=self.life+target.Life + self.life0=self.life0+target.Life0 + + -- Increase counter. + self.targetcounter=self.targetcounter+1 + + target.ID=self.targetcounter + target.Status=TARGET.ObjectStatus.ALIVE + target.Object=Object + + table.insert(self.targets, target) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Life and Damage Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get target life points. +-- @param #TARGET self +-- @return #number Number of initial life points when mission was planned. +function TARGET:GetLife0() + return self.life0 +end + +--- Get current damage. +-- @param #TARGET self +-- @return #number Damage in percent. +function TARGET:GetDamage() + local life=self:GetLife()/self:GetLife0() + local damage=1-life + return damage*100 +end + +--- Get target life points. +-- @param #TARGET self +-- @param #TARGET.Object Target Target object. +-- @return #number Life points of target. +function TARGET:GetTargetLife(Target) + + if Target.Type==TARGET.ObjectType.GROUP then + + if Target.Object and Target.Object:IsAlive() then + + local units=Target.Object:GetUnits() + + local life=0 + for _,_unit in pairs(units or {}) do + local unit=_unit --Wrapper.Unit#UNIT + life=life+unit:GetLife() + end + + return life + else + return 0 + end + + elseif Target.Type==TARGET.ObjectType.UNIT then + + local unit=Target.Object --Wrapper.Unit#UNIT + + if unit and unit:IsAlive() then + + -- Note! According to the profiler, there is a big difference if we "return unit:GetLife()" or "local life=unit:GetLife(); return life"! + local life=unit:GetLife() + return life + else + return 0 + end + + elseif Target.Type==TARGET.ObjectType.STATIC then + + if Target.Object and Target.Object:IsAlive() then + return 1 + else + return 0 + end + + elseif Target.Type==TARGET.ObjectType.SCENERY then + + if Target.Status==TARGET.ObjectStatus.ALIVE then + return 1 + else + return 0 + end + + elseif Target.Type==TARGET.ObjectType.AIRBASE then + + if Target.Status==TARGET.ObjectStatus.ALIVE then + return 1 + else + return 0 + end + + elseif Target.Type==TARGET.ObjectType.COORDINATE then + + return 1 + + end + +end + +--- Get current life points. +-- @param #TARGET self +-- @return #number Life points of target. +function TARGET:GetLife() + + local N=0 + + for _,_target in pairs(self.targets) do + local Target=_target --#TARGET.Object + + N=N+self:GetTargetLife(Target) + + end + + return N +end + +--- Get target 3D position vector. +-- @param #TARGET self +-- @param #TARGET.Object Target Target object. +-- @return DCS#Vec3 Vector with x,y,z components +function TARGET:GetTargetVec3(Target) + + if Target.Type==TARGET.ObjectType.GROUP then + + local object=Target.Object --Wrapper.Group#GROUP + + if object and object:IsAlive() then + + return object:GetVec3() + + end + + elseif Target.Type==TARGET.ObjectType.UNIT then + + local object=Target.Object --Wrapper.Unit#UNIT + + if object and object:IsAlive() then + return object:GetVec3() + end + + elseif Target.Type==TARGET.ObjectType.STATIC then + + local object=Target.Object --Wrapper.Static#STATIC + + if object and object:IsAlive() then + return object:GetVec3() + end + + elseif Target.Type==TARGET.ObjectType.SCENERY then + + local object=Target.Object --Wrapper.Scenery#SCENERY + + if object then + return object:GetVec3() + end + + elseif Target.Type==TARGET.ObjectType.AIRBASE then + + local object=Target.Object --Wrapper.Airbase#AIRBASE + + return object:GetVec3() + + --if Target.Status==TARGET.ObjectStatus.ALIVE then + --end + + elseif Target.Type==TARGET.ObjectType.COORDINATE then + + local object=Target.Object --Core.Point#COORDINATE + + return {x=object.x, y=object.y, z=object.z} + + end + + self:E(self.lid.."ERROR: Unknown TARGET type! Cannot get Vec3") +end + + +--- Get target coordinate. +-- @param #TARGET self +-- @param #TARGET.Object Target Target object. +-- @return Core.Point#COORDINATE Coordinate of the target. +function TARGET:GetTargetCoordinate(Target) + + if Target.Type==TARGET.ObjectType.COORDINATE then + + -- Coordinate is the object itself. + return Target.Object + + else + + -- Get updated position vector. + local vec3=self:GetTargetVec3(Target) + + -- Update position. This saves us to create a new COORDINATE object each time. + if vec3 then + Target.Coordinate.x=vec3.x + Target.Coordinate.y=vec3.y + Target.Coordinate.z=vec3.z + end + + return Target.Coordinate + + end + + return nil +end + +--- Get target name. +-- @param #TARGET self +-- @param #TARGET.Object Target Target object. +-- @return #string Name of the target object. +function TARGET:GetTargetName(Target) + + if Target.Type==TARGET.ObjectType.GROUP then + + if Target.Object and Target.Object:IsAlive() then + + return Target.Object:GetName() + + end + + elseif Target.Type==TARGET.ObjectType.UNIT then + + if Target.Object and Target.Object:IsAlive() then + return Target.Object:GetName() + end + + elseif Target.Type==TARGET.ObjectType.STATIC then + + if Target.Object and Target.Object:IsAlive() then + return Target.Object:GetName() + end + + elseif Target.Type==TARGET.ObjectType.AIRBASE then + + if Target.Status==TARGET.ObjectStatus.ALIVE then + return Target.Object:GetName() + end + + elseif Target.Type==TARGET.ObjectType.COORDINATE then + + local coord=Target.Object --Core.Point#COORDINATE + + return coord:ToStringMGRS() + + end + + return "Unknown" +end + +--- Get name. +-- @param #TARGET self +-- @return #string Name of the target usually the first object. +function TARGET:GetName() + return self.name +end + +--- Get coordinate. +-- @param #TARGET self +-- @return Core.Point#COORDINATE Coordinate of the target. +function TARGET:GetCoordinate() + + for _,_target in pairs(self.targets) do + local Target=_target --#TARGET.Object + + local coordinate=self:GetTargetCoordinate(Target) + + if coordinate then + return coordinate + end + + end + + self:E(self.lid..string.format("ERROR: Cannot get coordinate of target %s", self.name)) + return nil +end + + +--- Get target category. +-- @param #TARGET self +-- @param #TARGET.Object Target Target object. +-- @return #TARGET.Category Target category. +function TARGET:GetTargetCategory(Target) + + local category=nil + + if Target.Type==TARGET.ObjectType.GROUP then + + if Target.Object and Target.Object:IsAlive()~=nil then + + local group=Target.Object --Wrapper.Group#GROUP + + local cat=group:GetCategory() + + if cat==Group.Category.AIRPLANE or cat==Group.Category.HELICOPTER then + category=TARGET.Category.AIRCRAFT + elseif cat==Group.Category.GROUND or cat==Group.Category.TRAIN then + category=TARGET.Category.GROUND + elseif cat==Group.Category.SHIP then + category=TARGET.Category.NAVAL + end + + end + + elseif Target.Type==TARGET.ObjectType.UNIT then + + if Target.Object and Target.Object:IsAlive() then + local unit=Target.Object --Wrapper.Unit#UNIT + + local group=unit:GetGroup() + + local cat=group:GetCategory() + + if cat==Group.Category.AIRPLANE or cat==Group.Category.HELICOPTER then + category=TARGET.Category.AIRCRAFT + elseif cat==Group.Category.GROUND or cat==Group.Category.TRAIN then + category=TARGET.Category.GROUND + elseif cat==Group.Category.SHIP then + category=TARGET.Category.NAVAL + end + + end + + elseif Target.Type==TARGET.ObjectType.STATIC then + + return TARGET.Category.GROUND + + elseif Target.Type==TARGET.ObjectType.SCENERY then + + return TARGET.Category.GROUND + + elseif Target.Type==TARGET.ObjectType.AIRBASE then + + return TARGET.Category.AIRBASE + + elseif Target.Type==TARGET.ObjectType.COORDINATE then + + return TARGET.Category.COORDINATE + + end + + return category +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get a target object by its name. +-- @param #TARGET self +-- @param #string ObjectName Object name. +-- @return #TARGET.Object The target object table or nil. +function TARGET:GetTargetByName(ObjectName) + + for _,_target in pairs(self.targets) do + local target=_target --#TARGET.Object + if ObjectName==target.Name then + return target + end + end + + return nil +end + + +--- Get the first target objective alive. +-- @param #TARGET self +-- @return #TARGET.Object The target objective. +function TARGET:GetObjective() + + for _,_target in pairs(self.targets) do + local target=_target --#TARGET.Object + if target.Status==TARGET.ObjectStatus.ALIVE then + return target + end + end + + return nil +end + +--- Get the first target object alive. +-- @param #TARGET self +-- @return Wrapper.Positionable#POSITIONABLE The target object or nil. +function TARGET:GetObject() + + local target=self:GetObjective() + if target then + return target.Object + end + + return nil +end + + +--- Count alive targets. +-- @param #TARGET self +-- @return #number Number of alive target objects. +function TARGET:CountTargets() + + local N=0 + + for _,_target in pairs(self.targets) do + local Target=_target --#TARGET.Object + + if Target.Type==TARGET.ObjectType.GROUP then + + local target=Target.Object --Wrapper.Group#GROUP + + local units=target:GetUnits() + + for _,_unit in pairs(units or {}) do + local unit=_unit --Wrapper.Unit#UNIT + if unit and unit:IsAlive() and unit:GetLife()>1 then + N=N+1 + end + end + + elseif Target.Type==TARGET.ObjectType.UNIT then + + local target=Target.Object --Wrapper.Unit#UNIT + + if target and target:IsAlive() and target:GetLife()>1 then + N=N+1 + end + + elseif Target.Type==TARGET.ObjectType.STATIC then + + local target=Target.Object --Wrapper.Static#STATIC + + if target and target:IsAlive() then + N=N+1 + end + + elseif Target.Type==TARGET.ObjectType.SCENERY then + + if Target.Status==TARGET.ObjectStatus.ALIVE then + N=N+1 + end + + elseif Target.Type==TARGET.ObjectType.AIRBASE then + + if Target.Status==TARGET.ObjectStatus.ALIVE then + N=N+1 + end + + elseif Target.Type==TARGET.ObjectType.COORDINATE then + + -- No target we can check! + + else + self:E(self.lid.."ERROR unknown target type") + end + + end + + return N +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index b4167feb6..693428891 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -17,6 +17,7 @@ -- @field #table CategoryName Names of airbase categories. -- @field #string AirbaseName Name of the airbase. -- @field #number AirbaseID Airbase ID. +-- @field Core.Zone#ZONE AirbaseZone Circular zone around the airbase with a radius of 2500 meters. For ships this is a ZONE_UNIT object. -- @field #number category Airbase category. -- @field #table descriptors DCS descriptors. -- @field #boolean isAirdrome Airbase is an airdrome. @@ -493,8 +494,14 @@ function AIRBASE:Register(AirbaseName) self:GetCoordinate() if vec2 then - -- TODO: For ships we need a moving zone. - self.AirbaseZone=ZONE_RADIUS:New( AirbaseName, vec2, 2500 ) + if self.isShip then + local unit=UNIT:FindByName(AirbaseName) + if unit then + self.AirbaseZone=ZONE_UNIT:New(AirbaseName, unit, 2500) + end + else + self.AirbaseZone=ZONE_RADIUS:New(AirbaseName, vec2, 2500) + end else self:E(string.format("ERROR: Cound not get position Vec2 of airbase %s", AirbaseName)) end diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index f7632d687..1b305e6e9 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -814,7 +814,8 @@ end -- @return #number The velocity in knots. function POSITIONABLE:GetVelocityKNOTS() self:F2( self.PositionableName ) - return UTILS.MpsToKnots(self:GetVelocityMPS()) + local velmps=self:GetVelocityMPS() + return UTILS.MpsToKnots(velmps) end --- Returns the Angle of Attack of a positionable.