Compare commits

..

106 Commits

Author SHA1 Message Date
Frank 6eb1d78ecb Merge branch 'FF/Ops' into FF/OpsDev 2025-11-23 20:47:49 +01:00
Frank 88a8a3318f Merge branch 'develop' into FF/Ops 2025-11-23 20:42:00 +01:00
Thomas bc24d75ad7 Merge pull request #2448 from FlightControl-Master/master
Merge from master
2025-11-22 15:21:28 +01:00
Frank ed091c4f3e Merge branch 'develop' into FF/Ops 2025-11-22 11:21:25 +01:00
Frank ff86cc9f11 Flightplan 2025-11-15 21:39:30 +01:00
Frank 1e78c6dea2 Merge branch 'FF/Ops' into FF/OpsDev 2025-11-15 10:14:15 +01:00
Frank d37cd1613d Merge branch 'develop' into FF/Ops 2025-11-14 23:44:40 +01:00
Frank 96a923b5a8 Merge branch 'master' into FF/Ops 2025-11-14 23:44:25 +01:00
Frank 56d7638617 Update Utils.lua 2025-10-26 20:58:31 +01:00
Frank 386be19b8b Merge branch 'FF/Ops' into FF/OpsDev 2025-10-26 15:20:48 +01:00
Frank 0e4eb9a263 Merge branch 'develop' into FF/OpsDev 2025-10-25 21:18:04 +02:00
Frank 1f20b830b7 Merge branch 'develop' into FF/OpsDev 2025-04-17 21:40:41 +02:00
Frank 0531b8f57e Merge branch 'FF/Ops' into FF/OpsDev 2025-04-09 22:47:34 +02:00
Frank 98e2997e26 Merge branch 'develop' into FF/OpsDev 2025-04-09 22:43:24 +02:00
Frank bdec6eb82d Merge branch 'FF/Ops' into FF/OpsDev 2024-07-07 11:33:44 +02:00
Frank 9bdacfbcf7 Merge branch 'develop' into FF/OpsDev 2024-07-01 16:46:22 +02:00
Frank be2eda25df Merge branch 'develop' into FF/OpsDev 2024-06-27 21:31:24 +02:00
Frank fa8e1d2a56 Merge branch 'FF/Ops' into FF/OpsDev 2024-06-19 07:53:54 +02:00
Frank 5242754c60 Merge branch 'FF/Ops' into FF/OpsDev 2024-04-26 14:33:05 +02:00
Frank 3dbafd7a19 Update Astar.lua 2024-04-20 14:34:15 +02:00
Frank 56e751193a Merge branch 'FF/Ops' into FF/OpsDev 2024-04-04 09:35:53 +02:00
Frank b2b258cff0 Merge branch 'FF/Ops' into FF/OpsDev 2024-03-30 22:32:42 +01:00
Frank 75068a2a26 Merge branch 'FF/Ops' into FF/OpsDev 2024-03-24 17:25:02 +01:00
Frank d82df93eff Merge branch 'FF/Ops' into FF/OpsDev 2024-01-23 11:05:50 +01:00
Frank 5168380eac Update Modules.lua 2024-01-21 16:41:09 +01:00
Frank a7ffbab850 Update Modules.lua 2024-01-16 19:44:08 +01:00
Frank a238294079 Merge branch 'FF/Ops' into FF/OpsDev 2024-01-16 17:37:41 +01:00
Frank accb4b45fb Merge branch 'FF/Ops' into FF/OpsDev 2024-01-07 21:17:17 +01:00
Frank ba4ff5c9e9 Update Procedure.lua 2024-01-07 10:29:31 +01:00
Frank 171c450576 Merge branch 'FF/Ops' into FF/OpsDev 2024-01-04 15:14:25 +01:00
Frank f35078b426 Merge branch 'FF/Ops' into FF/OpsDev 2024-01-02 17:07:15 +01:00
Frank 8921007805 Update Point.lua 2024-01-02 17:04:04 +01:00
Frank 8fd3034503 Merge branch 'FF/Ops' into FF/OpsDev 2024-01-01 21:55:52 +01:00
Frank a20b6e8a62 Merge branch 'FF/Ops' into FF/OpsDev 2023-12-03 21:10:24 +01:00
Frank 4ce6cc7776 Merge branch 'FF/Ops' into FF/OpsDev 2023-11-17 18:06:17 +01:00
Frank 434296ab11 Merge branch 'FF/Ops' into FF/OpsDev 2023-11-07 00:06:35 +01:00
Frank a5232f49c0 Navigation 2023-11-06 21:35:20 +01:00
Frank befc8207f5 Update Nav 2023-11-02 23:42:59 +01:00
Frank 23947b7c30 Merge branch 'FF/Ops' into FF/OpsDev 2023-11-02 20:33:53 +01:00
Frank da91b710a9 Merge branch 'FF/Ops' into FF/OpsDev 2023-10-31 20:17:10 +01:00
Frank d656bbc014 Update Vector.lua 2023-10-19 21:49:45 +02:00
Frank ed23f11e4a Merge branch 'FF/Ops' into FF/OpsDev 2023-10-13 22:00:57 +02:00
Frank 068465a8f2 Merge branch 'FF/Ops' into FF/OpsDev 2023-10-13 21:33:33 +02:00
Frank 4e24a7bf80 Navigation 2023-10-08 21:49:15 +02:00
Frank 1ece7238dc NAv 2023-10-05 16:52:41 +02:00
Frank db53f427e3 Update Point.lua 2023-10-02 17:02:21 +02:00
Frank d2e2c51275 Update FlightPlan.lua 2023-09-29 13:27:59 +02:00
Frank 691748082b Merge branch 'FF/Ops' into FF/OpsDev 2023-09-27 22:29:38 +02:00
Frank b294ef10c8 Update NavyGroup.lua 2023-09-27 10:54:37 +02:00
Frank 5d40091947 NAVYGROUP
- Improved heading into wind
2023-09-27 10:53:19 +02:00
Frank 7453a6c55d navygroup
- into wind
2023-09-26 17:02:13 +02:00
Frank 153ef7cd08 utils 2023-09-26 01:44:30 +02:00
Frank b6a550a247 Update Vector.lua 2023-09-25 16:59:41 +02:00
Frank 3568f27150 Vector 2023-09-24 22:46:50 +02:00
Frank c46061466c VECTOR
- Added functions
2023-09-24 22:09:49 +02:00
Frank 9c7b5e8506 VECTOR
**VECTOR**
- New core class for vector algebra
2023-09-23 01:15:08 +02:00
Frank b11df6b523 Navigation
Refactored names etc.
2023-09-21 23:37:25 +02:00
Frank 66dcd44fb8 Merge branch 'FF/OpsDev' of https://github.com/FlightControl-Master/MOOSE into FF/OpsDev 2023-09-21 17:48:12 +02:00
Frank f35237f86f Update FlightControl.lua 2023-09-21 17:48:04 +02:00
Frank f19c877a11 Navigation 2023-09-21 17:01:32 +02:00
Frank 792a487eeb Merge branch 'FF/Ops' into FF/OpsDev 2023-09-20 22:32:16 +02:00
Frank f5e8bd0ffc Merge branch 'FF/Ops' into FF/OpsDev 2023-09-20 14:39:41 +02:00
Frank cbdbf36f32 FC
- Improved taxi phrases
2023-09-20 11:52:14 +02:00
Frank 1116b19b00 FC 2023-09-19 17:04:37 +02:00
Frank edb490fa97 Update Pathline.lua
- json
2023-09-19 03:01:47 +02:00
Frank 9fa16c6385 Update FlightControl.lua 2023-09-18 16:52:13 +02:00
Frank a0ee957493 Update FlightControl.lua 2023-09-17 23:07:59 +02:00
Frank f32c303c0a pathline 2023-09-17 22:58:53 +02:00
Frank 6febbbc8e6 FC 2023-09-17 21:55:29 +02:00
Frank d5aa9eaf0f Merge branch 'FF/Ops' into FF/OpsDev 2023-09-17 16:26:23 +02:00
Frank 321b2d761d FLIGHTCONTROL
- Improved Taxi comms
2023-09-17 16:10:59 +02:00
Frank 18581e4a78 Merge branch 'FF/OpsDev' of https://github.com/FlightControl-Master/MOOSE into FF/OpsDev 2023-09-14 17:40:11 +02:00
Frank b03905154d ASTAR Pathlines 2023-09-14 17:40:02 +02:00
Frank 2fda1709bc Update FlightControl.lua 2023-09-14 16:46:08 +02:00
Frank 39f626390a Merge branch 'FF/Ops' into FF/OpsDev 2023-09-13 11:14:55 +02:00
Frank aa1e12163d ASTAR/PATHLINE 2023-09-12 23:57:50 +02:00
Frank abb7f860ae Update Astar.lua 2023-09-12 03:21:23 +02:00
Frank cffada1a1e AIRBASE
- Taxiways
2023-09-11 16:54:10 +02:00
Frank f8d91798e3 Update FlightControl.lua 2023-09-10 23:55:40 +02:00
Frank 548aa8d5a9 Merge branch 'FF/Ops' into FF/OpsDev 2023-09-10 22:43:13 +02:00
Frank 1e929d1d19 ASTAR Taxi 2023-09-10 22:38:00 +02:00
Frank d5f215505e Merge branch 'FF/Ops' into FF/OpsDev 2023-09-09 00:32:29 +02:00
Frank 851660c793 NAV 2023-09-08 16:11:11 +02:00
Frank ee57b46c14 Update Astar.lua 2023-09-07 23:32:07 +02:00
Frank 36eac9fccc PATH 2023-09-06 22:50:24 +02:00
Frank 1f630ab490 Astar Pathline 2023-09-05 23:29:27 +02:00
Frank 8f0c6948ac Merge branch 'FF/Ops' into FF/OpsDev 2023-09-05 20:53:45 +02:00
Frank eb2380c3f6 Merge branch 'FF/Ops' into FF/OpsDev 2023-09-04 23:35:05 +02:00
Frank df3e182de4 Merge branch 'FF/OpsDev' of https://github.com/FlightControl-Master/MOOSE into FF/OpsDev 2023-09-04 23:34:26 +02:00
Frank bda7988118 Astar 2023-09-04 23:34:23 +02:00
Frank 72deb9ee17 Merge branch 'FF/OpsDev' of https://github.com/FlightControl-Master/MOOSE into FF/OpsDev 2023-08-30 15:53:49 +02:00
Frank ced1f30f3f Update DCS.lua 2023-08-30 15:53:35 +02:00
Frank 47c0006537 ASTAR & PATHLINE 2023-08-29 22:43:36 +02:00
Frank 21e5c124c0 Astar 2023-08-28 22:38:37 +02:00
Frank 3ff374e1b9 Merge branch 'FF/Ops' into FF/OpsDev 2023-08-27 20:39:51 +02:00
Frank 02000be9af Merge branch 'FF/Ops' into FF/OpsDev 2023-08-17 19:15:12 +02:00
Frank 663c6cead0 Update FlightControl.lua 2023-08-16 20:46:53 +02:00
Frank 6867df58c4 Merge branch 'FF/Ops' into FF/OpsDev 2023-08-13 18:22:12 +02:00
Frank 639625d3d5 Update FlightControl.lua 2023-08-13 14:16:40 +02:00
Frank 0ace200e5a Nav
- Small stuff
2023-08-04 00:08:16 +02:00
Frank e76c26ff59 FLIGHTPLAN 2023-08-03 00:41:34 +02:00
Frank cb11de6f9c Navigation
- Added NavFix
2023-08-02 10:53:28 +02:00
Frank cdd78e5163 Update FlightPlan.lua 2023-08-01 00:25:08 +02:00
Frank a8da02774a Update FlightPlan.lua 2023-08-01 00:24:49 +02:00
Frank 04258a69c4 Navigation
- Init stuff
2023-07-31 21:58:01 +02:00
Frank 49882f03d9 Update FlightControl.lua 2023-07-26 16:05:15 +02:00
48 changed files with 4180 additions and 4960 deletions
+2 -2
View File
@@ -77,11 +77,11 @@ jobs:
- name: Install markdown (prereq for LuaDocumentor)
run: |
sudo luarocks install markdown 0.33-1
sudo luarocks install markdown 0.32-2
- name: Install penlight (prereq for LuaDocumentor)
run: |
sudo luarocks install penlight 1.14.0-3
sudo luarocks install penlight 1.11.0-1
- name: Install metalua-compiler (prereq for LuaDocumentor)
run: |
+1 -1
View File
@@ -105,7 +105,7 @@ jobs:
if: ${{ vars.SKIP_LUACHECK != true }}
continue-on-error: true
run: |
luacheck --std=lua51c --config=.luacheckrc -guras "Moose Development/Moose"
luacheck --std=lua51c --config=.luacheckrc -gurasqq "Moose Development/Moose"
- name: Run LuaSrcDiet
run: |
+2 -2
View File
@@ -11,8 +11,8 @@
-- @module AI.AI_A2G_BAI
-- @image AI_Air_To_Ground_Engage.JPG
--- @type AI_A2G_BAI
-- @extends AI.AI_Air_Engage#AI_A2A_Engage -- TODO: Documentation. This class does not exist, unable to determine what it extends.
-- @type AI_A2G_BAI
-- @extends AI.AI_A2A_Engage#AI_A2A_Engage -- TODO: Documentation. This class does not exist, unable to determine what it extends.
--- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders.
--
+1 -1
View File
@@ -31,7 +31,7 @@
-- @module AI.AI_CAP
-- @image AI_Combat_Air_Patrol.JPG
--- @type AI_CAP_ZONE
-- @type AI_CAP_ZONE
-- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Wrapper.Controllable} patrolling.
-- @field Core.Zone#ZONE_BASE TargetZone The @{Core.Zone} where the patrol needs to be executed.
-- @extends AI.AI_Patrol#AI_PATROL_ZONE
+401 -40
View File
@@ -157,6 +157,8 @@ ASTAR = {
-- @field #number surfacetype Surface type.
-- @field #table valid Cached valid/invalid nodes.
-- @field #table cost Cached cost.
-- @field Core.Pathline#PATHLINE pathline Pathline that node is part of.
-- @field Core.Pathline#PATHLINE.Point pathpoint Pathline point.
--- ASTAR infinity.
-- @field #number INF
@@ -164,7 +166,7 @@ ASTAR.INF=1/0
--- ASTAR class version.
-- @field #string version
ASTAR.version="0.4.0"
ASTAR.version="0.5.0"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
@@ -172,6 +174,7 @@ ASTAR.version="0.4.0"
-- TODO: Add more valid neighbour functions.
-- TODO: Write docs.
-- DONE: Add pathlines for seach/valid neighbours.
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
@@ -244,7 +247,10 @@ end
function ASTAR:AddNode(Node)
self.nodes[Node.id]=Node
self.Nnodes=self.Nnodes+1
self.Nnodes=self.Nnodes+1
self:T3(self.lid..string.format("Adding node UID=%d", Node.id))
--Node.coordinate:MarkToAll(string.format("Node ID=%d", Node.id))
return self
end
@@ -262,6 +268,47 @@ function ASTAR:AddNodeFromCoordinate(Coordinate)
return node
end
--- Adds nodes to the table of grid nodes from a PATHLINE.
-- @param #ASTAR self
-- @param Core.Pathline#PATHLINE Pathline Pathline or name of pathline. Has to exist.
-- @return #ASTAR self
function ASTAR:AddNodeFromPathlineName(Pathline)
if type(Pathline)=="string" then
Pathline=PATHLINE:FindByName(Pathline)
end
if Pathline then
for i,_point in pairs(Pathline.points) do
local point=_point --Core.Pathline#PATHLINE.Point
-- Create node from point coordinate.
local node=self:AddNodeFromCoordinate(COORDINATE:NewFromVec3(point.vec3))
-- Add pathline parameters.
node.pathline=Pathline
node.pathpoint=point
-- Debug.
local name=node.pathline and node.pathline.name or "N/A"
local idx=node.pathline and node.pathline:_GetPointIndex(node.pathpoint) or "N/A"
-- Debug message.
self:T(self.lid..string.format("Adding node UID=%d pathline=%s [%s]", node.id, name, tostring(idx)))
-- Debug mark
--node.coordinate:MarkToAll(string.format("Node ID=%d\npathline=%s [%s]", node.id, name, tostring(idx)))
end
else
env.error("FF error pathline")
end
return self
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.
@@ -340,6 +387,18 @@ function ASTAR:SetValidNeighbourRoad(MaxDistance)
return self
end
--- Set valid neighbours to be on the same pathline or not further apart than 10 meters to jump from one pathline to another.
-- @param #ASTAR self
-- @param #number MaxDistance Max allowed distance between nodes of different pathlines in meters. Default is 10 m.
-- @return #ASTAR self
function ASTAR:SetValidNeighbourPathline(MaxDistance)
self:SetValidNeighbourFunction(ASTAR.Pathline, 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.
@@ -384,7 +443,7 @@ end
-- @return #ASTAR self
function ASTAR:SetCostRoad()
self:SetCostFunction(ASTAR)
self:SetCostFunction(ASTAR.Road)
return self
end
@@ -544,6 +603,55 @@ function ASTAR.Road(nodeA, nodeB)
end
--- Function to check if two nodes are on the same pathline or if nodes are less than 10 meters apart.
-- @param #ASTAR.Node nodeA First node.
-- @param #ASTAR.Node nodeB Other node.
-- @param #number distmax Max distance in meters. Default is 10 m.
-- @return #boolean If true, two nodes are connected.
function ASTAR.Pathline(nodeA, nodeB, distmax)
distmax=distmax or 10
if nodeA.pathline.name==nodeB.pathline.name then
-- Nodes are on the same pathline. We use the index to check if they are neighbours.
local pathline=nodeA.pathline
local idxA=pathline:_GetPointIndex(nodeA.pathpoint)
local idxB=pathline:_GetPointIndex(nodeB.pathpoint)
if math.abs(idxA-idxB)<=1 then
return true
end
else
-- Check if nodeB is close to pathline of nodeA.
local c, dist, segA=nodeA.pathline:GetClosestPoint3D(nodeB.coordinate)
local seg=segA --Core.Pathline#PATHLINE.Segment
if dist<distmax and (nodeA.pathpoint.uid==seg.p1.uid or nodeA.pathpoint.uid==seg.p2.uid) then
--env.info(string.format("FF NodeB=%d [pathline=%s] is close to NodeA=%d [pathline=%s] ==> valid neighbour", nodeB.id, nodeB.pathline.name, nodeA.id, nodeA.pathline.name))
return true
end
-- Check if nodeA is close to pathline of nodeB.
local c, dist, segB=nodeB.pathline:GetClosestPoint3D(nodeA.coordinate)
local seg=segB --Core.Pathline#PATHLINE.Segment
if dist<distmax and (nodeB.pathpoint.uid==seg.p1.uid or nodeB.pathpoint.uid==seg.p2.uid) then
--env.info(string.format("FF NodeA=%d [pathline=%s] is close to NodeB=%d [pathline=%s] ==> valid neighbour", nodeA.id, nodeA.pathline.name, nodeB.id, nodeB.pathline.name))
return true
end
end
return false
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.
@@ -567,7 +675,9 @@ end
-- @param #ASTAR.Node nodeB Other node.
-- @return #number Distance between the two nodes.
function ASTAR.Dist2D(nodeA, nodeB)
local dist=nodeA.coordinate:Get2DDistance(nodeB)
local dist=nodeA.coordinate:Get2DDistance(nodeB.coordinate)
--local text=string.format("FF Cost Dist2D NodeA=%d-->NodeB=%d = %.1f", nodeA.id, nodeB.id, dist)
--env.info(text)
return dist
end
@@ -594,7 +704,7 @@ function ASTAR.DistRoad(nodeA, nodeB)
local dist=0
for i=2,#path do
local b=path[i] --DCS#Vec2
local b=path[i] --DCS#Vec2
local a=path[i-1] --DCS#Vec2
dist=dist+UTILS.VecDist2D(a,b)
@@ -604,7 +714,6 @@ function ASTAR.DistRoad(nodeA, nodeB)
return dist
end
return math.huge
end
@@ -614,10 +723,11 @@ end
--- Find the closest node from a given coordinate.
-- @param #ASTAR self
-- @param Core.Point#COORDINATE Coordinate.
-- @return #ASTAR.Node Cloest node to the coordinate.
-- @param Core.Point#COORDINATE Coordinate Reference coordinate.
-- @param #table ExcludeNodes Table of nodes that are excluded.
-- @return #ASTAR.Node Closest node to the coordinate.
-- @return #number Distance to closest node in meters.
function ASTAR:FindClosestNode(Coordinate)
function ASTAR:FindClosestNode(Coordinate, ExcludeNodes)
local distMin=math.huge
local closeNode=nil
@@ -625,11 +735,15 @@ function ASTAR:FindClosestNode(Coordinate)
for _,_node in pairs(self.nodes) do
local node=_node --#ASTAR.Node
local dist=node.coordinate:Get2DDistance(Coordinate)
if ExcludeNodes==nil or self:_IsNodeNotInTable(ExcludeNodes, node) then
if dist<distMin then
distMin=dist
closeNode=node
local dist=node.coordinate:Get2DDistance(Coordinate)
if dist<distMin then
distMin=dist
closeNode=node
end
end
end
@@ -637,38 +751,162 @@ function ASTAR:FindClosestNode(Coordinate)
return closeNode, distMin
end
--- Find the closest pathline to a given reference coordinate.
-- @param #ASTAR self
-- @param Core.Point#COORDINATE Coordinate Reference coordinate.
-- @return Core.Pathline#PATHLINE Closest pathline
-- @return #number Distance in meters.
-- @return DCS#Vec3 Closest point on pathline to the ref coordinate.
-- @return Core.Pathline#PATHLINE.Segment Segment.
function ASTAR:FindClosestPathline(Coordinate)
local pathline=nil --Core.Pathline#PATHLINE
local dist=math.huge
local vec3=nil
local S=nil
for _,_node in pairs(self.nodes) do
local node=_node --#ASTAR.Node
if node.pathline then
local vec, d, s=node.pathline:GetClosestPoint3D(Coordinate)
if d<dist then
pathline=node.pathline
dist=d
vec3=vec
S=s
end
end
end
if pathline then
-- Debug info.
self:T(self.lid..string.format("Closest pathline %s: dist=%.1f", pathline.name, dist))
end
return pathline, dist, vec3, S
end
--- Find the closest node to the given coordinate.
-- @param #ASTAR self
-- @param Core.Point#COORDINATE Coord Reference coordinate.
-- @param #table ExcludeNodes Nodes that are excluded.
-- @return #ASTAR.Node The node that was fround
function ASTAR:_FindClosestTerminalNode(Coord, ExcludeNodes)
-- Find the closest pathline to the ref coordinate.
local pathline, dist, vec3, s=self:FindClosestPathline(Coord)
-- Find the closest node to the given start coordinate.
local node, dist2=self:FindClosestNode(Coord)
if pathline and vec3 and dist and dist2>dist then
-- Create a node on the closest pathline so we first go straight there and then along the pathline.
local node=self:AddNodeFromCoordinate(COORDINATE:NewFromVec3(vec3))
-- We also need the pathline point.
local point=pathline:AddPointFromVec3(vec3, nil, s.p1)
node.pathline=pathline
node.pathpoint=point
self:T2(self.lid..string.format("Added new node=%d, which is closest to start coord. dist=%.1f m", node.id, dist))
end
-- Find the closest node to the given start coordinate.
local Node, dist3=self:FindClosestNode(Coord, ExcludeNodes)
-- Debug info.
self:T(self.lid..string.format("CLOSEST node ID=%d, distance=%.1f", Node.id, dist3))
return Node, dist3
end
--- Find the start node.
-- @param #ASTAR self
-- @param #ASTAR.Node Node The node to be added to the nodes table.
-- @return #ASTAR self
function ASTAR:FindStartNode()
-- Find the closest pathline to the
local pathline, dist, vec3, s=self:FindClosestPathline(self.startCoord)
local node, dist=self:FindClosestNode(self.startCoord)
-- Find the closest node to the given start coordinate.
local node, dist2=self:FindClosestNode(self.startCoord)
self.startNode=node
if dist>1000 then
self:T(self.lid.."Adding start node to node grid!")
self:AddNode(node)
if pathline and vec3 and dist and dist2>dist then
-- Create a node on the closest pathline so we first go straight there and then along the pathline.
local node=self:AddNodeFromCoordinate(COORDINATE:NewFromVec3(vec3))
-- We also need the pathline point.
local point=pathline:AddPointFromVec3(vec3, nil, s.p1)
node.pathline=pathline
node.pathpoint=point
self:T2(self.lid..string.format("Added new node=%d, which is closest to start coord. dist=%.1f m", node.id, dist))
end
-- Find the closest node to the given start coordinate.
self.startNode, dist2=self:FindClosestNode(self.startCoord)
--self.startNode.coordinate:MarkToAll("Start Node")
-- Debug info.
self:T(self.lid..string.format("START node ID=%d", self.startNode.id))
-- Not sure why I did this. The node does not need to be added again as it is already contained in self.nodes!
-- if dist>1000 then
-- self:T(self.lid.."Adding start node to node grid!")
-- self:AddNode(node)
-- end
return self
end
--- Add a node.
--- Find the end 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:T(self.lid.."Adding end node to node grid!")
self:AddNode(node)
local pathline, dist, vec3, s=self:FindClosestPathline(self.endCoord)
-- Find the closest node to the given start coordinate.
local node, dist2=self:FindClosestNode(self.endCoord)
if pathline and vec3 and dist and dist2>dist then
-- Create a node on the closest pathline so we first go straight there and then along the pathline.
local node=self:AddNodeFromCoordinate(COORDINATE:NewFromVec3(vec3))
-- We also need the point.
local point=pathline:AddPointFromVec3(vec3, nil, s.p1)
-- Add pathline parameters to node.
node.pathline=pathline
node.pathpoint=point
self:T2(self.lid..string.format("Added new node=%d, which is closest to END coord: dist=%.1f m", node.id, dist))
end
-- Find closest node to the end coordinate (exclude the start coordinate.
self.endNode, dist=self:FindClosestNode(self.endCoord, {self.startNode})
--self.endNode.coordinate:MarkToAll("End Node")
-- Debug info.
self:T(self.lid..string.format("END node ID=%d", self.endNode.id))
-- Not sure why I did this. The node does not need to be added again as it is already contained in self.nodes!
-- if dist>1000 then
-- self:T(self.lid.."Adding end node to node grid!")
-- self:AddNode(node)
-- end
return self
end
@@ -684,12 +922,21 @@ end
-- @return #table Table of nodes from start to finish.
function ASTAR:GetPath(ExcludeStartNode, ExcludeEndNode)
self:FindStartNode()
self:FindEndNode()
-- self:FindStartNode()
-- self:FindEndNode()
-- Find start Node (closest node to start coordinate).
self.startNode=self:_FindClosestTerminalNode(self.startCoord)
-- Find end node, which is not the start node (excluded).
self.endNode=self:_FindClosestTerminalNode(self.endCoord, {self.startNode})
local nodes=self.nodes
local start=self.startNode
local goal=self.endNode
-- Debug info.
self:T(self.lid..string.format("GetPath Start Node=%d, End Node=%d", start.id, goal.id))
-- Sets.
local openset = {}
@@ -746,7 +993,12 @@ function ASTAR:GetPath(ExcludeStartNode, ExcludeEndNode)
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)
text=text..string.format(", Ncost=%d [%d cached]", self.ncost, self.ncostcache)
text=text..string.format("\nNodes:")
for i,_node in ipairs(path) do
local node=_node --#ASTAR.Node
text=text..string.format("\n[%d] Node ID=%d", i, node.id)
end
self:T(self.lid..text)
return path
@@ -759,13 +1011,16 @@ function ASTAR:GetPath(ExcludeStartNode, ExcludeEndNode)
-- Get neighbour nodes.
local neighbors=self:_NeighbourNodes(current, nodes)
-- Loop over neighbours.
for _,neighbor in pairs(neighbors) do
-- Node is not in closed set.
if self:_NotIn(closedset, neighbor.id) then
local tentative_g_score=g_score[current.id]+self:_DistNodes(current, neighbor)
-- Calculate tentative_g_score.
--local tentative_g_score=g_score[current.id] + self:_DistNodes(current, neighbor)
local tentative_g_score=g_score[current.id] + self:_HeuristicCost(current, neighbor)
if self:_NotIn(openset, neighbor.id) or tentative_g_score < g_score[neighbor.id] then
@@ -793,6 +1048,73 @@ function ASTAR:GetPath(ExcludeStartNode, ExcludeEndNode)
return nil -- no valid path
end
--- A* pathfinding function. This seaches the path along nodes between start and end nodes/coordinates.
-- It automatically creates a PATHLINE object that is returned in combination with the nodes of the optimal path.
-- @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 Core.Pathline#PATHLINE Pathline.
-- @return #table Nodes of path.
function ASTAR:GetPathline(ExcludeStartNode, ExcludeEndNode)
local nodes=self:GetPath(ExcludeStartNode, ExcludeEndNode)
local pathline=nil --Core.Pathline#PATHLINE
if nodes then
pathline=PATHLINE:New("Astar")
for _,_node in pairs(nodes) do
local node=_node --#ASTAR.Node
local point=pathline:AddPointFromVec3(node.coordinate)
point.name=node.pathline.name
end
end
return pathline, nodes
end
--- Get pathlines from nodes.
-- @param #ASTAR self
-- @param #table Nodes Given nodes.
-- @return #table Table of PATHLINES used in the path.
function ASTAR:GetPathlinesFromNodes(Nodes)
local pathlines={}
--for _,_node in pairs(Nodes or {}) do
for i=1,#Nodes do
local node=Nodes[i] --#ASTAR.Node
-- Pathline.
local pathline=node.pathline
if pathline and i>1 and i<#Nodes then
-- Previous and next nodes.
local n=Nodes[i-1] --#ASTAR.Node
local N=Nodes[i+1] --#ASTAR.Node
-- Check if previous and next nodes are on the same pathline.
-- If only one point in beteen is of another pathline, this is a junction and we dont actually switch to the other pathline.
if n.pathline and N.pathline and n.pathline.name==N.pathline.name and n.pathline.name~=pathline.name then
pathline=n.pathline
end
end
-- We do not want to add the same pathline two times in a row.
if #pathlines==0 or (#pathlines>0 and pathlines[#pathlines].name~=pathline.name) then
table.insert(pathlines, pathline)
end
end
return pathlines
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- A* pathfinding helper functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@@ -811,16 +1133,18 @@ function ASTAR:_HeuristicCost(nodeA, nodeB)
local cost=nodeA.cost[nodeB.id]
if cost~=nil then
self.ncostcache=self.ncostcache+1
self:T(self.lid..string.format("Cost nodeA=%d --> nodeB=%d = %.1f (Cashed!)", nodeA.id, nodeB.id, cost))
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
self:T(self.lid..string.format("Cost nodeA=%d --> nodeB=%d = %.1f", nodeA.id, nodeB.id, cost))
nodeA.cost[nodeB.id]=cost
nodeB.cost[nodeA.id]=cost -- Symmetric problem.
@@ -834,9 +1158,10 @@ end
-- @return #boolean If true, transition between nodes is possible.
function ASTAR:_IsValidNeighbour(node, neighbor)
-- Counter.
-- Counter of function calls.
self.nvalid=self.nvalid+1
-- Check if neighbour is in cached set.
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))
@@ -844,13 +1169,16 @@ function ASTAR:_IsValidNeighbour(node, neighbor)
return valid
end
-- Check if this is a valid neighbour.
local valid=nil
if self.ValidNeighbourFunc then
valid=self.ValidNeighbourFunc(node, neighbor, unpack(self.ValidNeighbourArg))
else
-- If no valid neighbour function is defined, we assume all nodes are valid neighbours.
valid=true
end
-- Cache valid neighbour.
node.valid[neighbor.id]=valid
neighbor.valid[node.id]=valid -- Symmetric problem.
@@ -884,6 +1212,9 @@ function ASTAR:_LowestFscore(set, f_score)
end
end
-- Debug info.
self:T(self.lid..string.format("Lowest Fscore=%.1f, Node=%s", lowest, tostring(bestNode)))
return self.nodes[bestNode]
end
@@ -928,16 +1259,46 @@ end
-- @param #table map Map.
-- @param #ASTAR.Node current_node The current node.
-- @return #table Unwinded path.
function ASTAR:_UnwindPath( flat_path, map, current_node )
function ASTAR:_UnwindPath(flat_path, map, current_node)
if map [current_node] then
table.insert (flat_path, 1, map[current_node])
return self:_UnwindPath(flat_path, map, map[current_node])
local previous_node=map[current_node]
if previous_node then
table.insert(flat_path, 1, previous_node)
return self:_UnwindPath(flat_path, map, previous_node)
else
-- No previous node ==> return path.
return flat_path
end
end
--- Function to check if a certain node is in a given table.
-- @param #ASTAR self
-- @param #table Nodes Nodes table.
-- @param #ASTAR.Node Node The node to check.
-- @return #boolean If true, the node is not in the set.
function ASTAR:_IsNodeInTable(Nodes, Node)
for _,_node in pairs(Nodes) do
local node=_node --#ASTAR.Node
if node.id==Node.id then
return true
end
end
return false
end
--- Function to check if a certain node is **not** in a given table.
-- @param #ASTAR self
-- @param #table Nodes Nodes table.
-- @param #ASTAR.Node Node The node to check.
-- @return #boolean If true, the node is not in the set.
function ASTAR:_IsNodeNotInTable(Nodes, Node)
local is=self:_IsNodeInTable(Nodes, Node)
return not is
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+1 -1
View File
@@ -1820,7 +1820,7 @@ function DATABASE:_EventOnPlayerEnterUnit( Event )
if Event.IniDCSUnit then
-- Player entering a CA slot
if Event.IniObjectCategory == 1 and Event.IniUnit and Event.IniUnit:IsGround() then
if Event.IniObjectCategory == 1 and Event.IniGroup and Event.IniGroup:IsGround() then
local IsPlayer = Event.IniDCSUnit:getPlayerName()
if IsPlayer then
+13 -26
View File
@@ -50,7 +50,7 @@ MARKEROPS_BASE = {
ClassName = "MARKEROPS",
Tag = "mytag",
Keywords = {},
version = "0.1.5",
version = "0.1.4",
debug = false,
Casesensitive = true,
}
@@ -59,8 +59,9 @@ MARKEROPS_BASE = {
-- @param #MARKEROPS_BASE self
-- @param #string Tagname Name to identify us from the event text.
-- @param #table Keywords Table of keywords recognized from the event text.
-- @param #boolean Casesensitive (Optional) Switch case sensitive identification of Tagname. Defaults to true.
-- @return #MARKEROPS_BASE self
function MARKEROPS_BASE:New(Tagname,Keywords)
function MARKEROPS_BASE:New(Tagname,Keywords,Casesensitive)
-- Inherit FSM
local self=BASE:Inherit(self, FSM:New()) -- #MARKEROPS_BASE
@@ -71,7 +72,11 @@ function MARKEROPS_BASE:New(Tagname,Keywords)
self.Keywords = Keywords or {} -- #table - might want to use lua regex here, too
self.debug = false
self.Casesensitive = true
if Casesensitive and Casesensitive == false then
self.Casesensitive = false
end
-----------------------
--- FSM Transitions ---
-----------------------
@@ -140,7 +145,7 @@ function MARKEROPS_BASE:New(Tagname,Keywords)
end
--- (Internal) Handle events.
--- (internal) Handle events.
-- @param #MARKEROPS_BASE self
-- @param Core.Event#EVENTDATA Event
function MARKEROPS_BASE:OnEventMark(Event)
@@ -196,17 +201,15 @@ function MARKEROPS_BASE:OnEventMark(Event)
end
end
--- (Internal) Match tag.
--- (internal) Match tag.
-- @param #MARKEROPS_BASE self
-- @param #string Eventtext Text added to the marker.
-- @return #boolean
function MARKEROPS_BASE:_MatchTag(Eventtext)
local matches = false
--self:I(self.lid .. "Casesensitive "..tostring(self.Casesensitive))
if self.Casesensitive == false then
--self:I(self.lid .. "Marker non-casesensitive "..Eventtext)
if not self.Casesensitive then
local type = string.lower(self.Tag) -- #string
if string.find(string.lower(Eventtext),type,1,true) then
if string.find(string.lower(Eventtext),type) then
matches = true --event text contains tag
end
else
@@ -218,7 +221,7 @@ function MARKEROPS_BASE:_MatchTag(Eventtext)
return matches
end
--- (Internal) Match keywords table.
--- (internal) Match keywords table.
-- @param #MARKEROPS_BASE self
-- @param #string Eventtext Text added to the marker.
-- @return #table
@@ -283,22 +286,6 @@ function MARKEROPS_BASE:onenterStopped(From,Event,To)
self:UnHandleEvent(EVENTS.MarkRemoved)
end
--- Switch off case sensitive matching
-- @param #MARKEROPS_BASE self
-- @return self
function MARKEROPS_BASE:SwitchCaseSensitiveOff()
self.Casesensitive = false
return self
end
--- Switch on case sensitive matching
-- @param #MARKEROPS_BASE self
-- @return self
function MARKEROPS_BASE:SwitchCaseSensitiveOn()
self.Casesensitive = true
return self
end
--------------------------------------------------------------------------
-- MARKEROPS_BASE Class Definition End.
--------------------------------------------------------------------------
+413 -49
View File
@@ -5,11 +5,13 @@
-- * Path from A to B
-- * Arbitrary number of points
-- * Automatically from lines drawtool
-- * Draw line or mark points on F10 map
-- * Find closest points to path
--
-- ===
--
-- ### Author: **funkyfranky**
--
--
-- ===
-- @module Core.Pathline
-- @image CORE_Pathline.png
@@ -21,6 +23,7 @@
-- @field #string lid Class id string for output to DCS log file.
-- @field #string name Name of the path line.
-- @field #table points List of 3D points defining the path.
-- @field #number counter Running number counting the point IDs.
-- @extends Core.Base#BASE
--- *The shortest distance between two points is a straight line.* -- Archimedes
@@ -28,30 +31,30 @@
-- ===
--
-- # The PATHLINE Concept
--
--
-- List of points defining a path from A to B. The pathline can consist of multiple points. Each point holds the information of its position, the surface type, the land height
-- and the water depth (if over sea).
--
--
-- Line drawings created in the mission editor are automatically registered as pathlines and stored in the MOOSE database.
-- They can be accessed with the @{#PATHLINE.FindByName) function.
--
--
-- # Constructor
--
--
-- The @{PATHLINE.New) function creates a new PATHLINE object. This does not hold any points. Points can be added with the @{#PATHLINE.AddPointFromVec2} and @{#PATHLINE.AddPointFromVec3}
--
--
-- For a given table of 2D or 3D positions, a new PATHLINE object can be created with the @{#PATHLINE.NewFromVec2Array} or @{#PATHLINE.NewFromVec3Array}, respectively.
--
--
-- # Line Drawings
--
--
-- The most convenient way to create a pathline is the draw panel feature in the DCS mission editor. You can select "Line" and then "Segments", "Segment" or "Free" to draw your lines.
-- These line drawings are then automatically added to the MOOSE database as PATHLINE objects and can be retrieved with the @{#PATHLINE.FindByName) function, where the name is the one
-- you specify in the draw panel.
--
--
-- # Mark on F10 map
--
-- The points of the PATHLINE can be marked on the F10 map with the @{#PATHLINE.MarkPoints}(`true`) function. The mark points contain information of the surface type, land height and
-- water depth.
--
--
-- To remove the marks, use @{#PATHLINE.MarkPoints}(`false`).
--
-- @field #PATHLINE
@@ -59,10 +62,14 @@ PATHLINE = {
ClassName = "PATHLINE",
lid = nil,
points = {},
counter = 0,
}
--- Point of line.
-- @type PATHLINE.Point
-- @field #number uid Unique ID of this point.
-- @field #string mother Name of the pathline this point belongs to.
-- @field #string name Name of this point.
-- @field DCS#Vec3 vec3 3D position.
-- @field DCS#Vec2 vec2 2D position.
-- @field #number surfaceType Surface type.
@@ -80,7 +87,9 @@ PATHLINE.version="0.2.0"
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO: A lot...
-- TODO: Read/write to JSON file
-- TODO: Translate/rotate pathline
-- TODO: Add color.
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
@@ -94,10 +103,10 @@ function PATHLINE:New(Name)
-- Inherit everything from INTEL class.
local self=BASE:Inherit(self, BASE:New()) --#PATHLINE
self.name=Name or "Unknown Path"
self.lid=string.format("PATHLINE %s | ", Name)
self.lid=string.format("PATHLINE %s | ", self.name)
return self
end
@@ -152,41 +161,70 @@ end
--- Add a point to the path from a given 2D position. The third dimension is determined from the land height.
-- @param #PATHLINE self
-- @param DCS#Vec2 Vec2 The 2D vector (x,y) to add.
-- @param #number Index Index to add this point, *e.g.* 1 for first point or 2 for second point. Default is at the end.
-- @param #PATHLINE.Point Point Add point after given point. Default is at the end or at given index.
-- @return #PATHLINE self
function PATHLINE:AddPointFromVec2(Vec2)
function PATHLINE:AddPointFromVec2(Vec2, Index, Point)
if Vec2 then
-- Create a new point.
local point=self:_CreatePoint(Vec2)
table.insert(self.points, point)
if Index then
-- Add at given index.
table.insert(self.points, Index, point)
else
if Point then
-- Get index of given point.
local i=self:_GetPointIndex(Point)
-- Add new point after given point.
table.insert(self.points, i+1, point)
else
-- Add add the end.
table.insert(self.points, point)
end
end
end
return self
end
--- Add a point to the path from a given 3D position.
-- @param #PATHLINE self
-- @param DCS#Vec3 Vec3 The 3D vector (x,y) to add.
-- @return #PATHLINE self
function PATHLINE:AddPointFromVec3(Vec3)
-- @param #number Index Index to add this point, *e.g.* 1 for first point or 2 for second point. Default is at the end.
-- @param #PATHLINE.Point Point Add point after given point. Default is at the end or at given index.
-- @return #PATHLINE.Point Point that was added.
function PATHLINE:AddPointFromVec3(Vec3, Index, Point)
if Vec3 then
local point=self:_CreatePoint(Vec3)
table.insert(self.points, point)
if Index then
-- Add add given index.
table.insert(self.points, Index, point)
else
if Point then
local i=self:_GetPointIndex(Point)
table.insert(self.points, i+1, point)
else
-- Add add the end.
table.insert(self.points, point)
end
end
return point
end
return self
return nil
end
--- Get name of pathline.
-- @param #PATHLINE self
-- @return #string Name of the pathline.
function PATHLINE:GetName()
function PATHLINE:GetName()
return self.name
end
@@ -200,18 +238,35 @@ end
--- Get points of pathline. Not that points are tables, that contain more information as just the 2D or 3D position but also the surface type etc.
-- @param #PATHLINE self
-- @return #list <#PATHLINE.Point> List of points.
function PATHLINE:GetPoints()
-- @return #list <Core.Pathline#PATHLINE.Point> List of points.
function PATHLINE:GetPoints()
return self.points
end
--- Get segments of pathline.
-- @param #PATHLINE self
-- @return #list <Core.Pathline#PATHLINE.Segment> List of points.
function PATHLINE:GetSetments()
local segments={}
for i=1,#self.points-1 do
local segment={} --#PATHLINE.Segment
segment.p1=self.points[i]
segment.p2=self.points[i+1]
table.insert(segments, segment)
end
return segments
end
--- Get 3D points of pathline.
-- @param #PATHLINE self
-- @return <DCS#Vec3> List of DCS#Vec3 points.
-- @return #list <DCS#Vec3> List of DCS#Vec3 points.
function PATHLINE:GetPoints3D()
local vecs={}
for _,_point in pairs(self.points) do
local point=_point --#PATHLINE.Point
table.insert(vecs, point.vec3)
@@ -222,11 +277,11 @@ end
--- Get 2D points of pathline.
-- @param #PATHLINE self
-- @return <DCS#Vec2> List of DCS#Vec2 points.
-- @return #list <DCS#Vec2> List of DCS#Vec2 points.
function PATHLINE:GetPoints2D()
local vecs={}
for _,_point in pairs(self.points) do
local point=_point --#PATHLINE.Point
table.insert(vecs, point.vec2)
@@ -241,11 +296,11 @@ end
function PATHLINE:GetCoordinates()
local vecs={}
for _,_point in pairs(self.points) do
local point=_point --#PATHLINE.Point
local coord=COORDINATE:NewFromVec3(point.vec3)
table.insert(vecs,coord)
table.insert(vecs, coord)
end
return vecs
@@ -258,11 +313,11 @@ end
function PATHLINE:GetPointFromIndex(n)
local N=self:GetNumberOfPoints()
n=n or 1
local point=nil --#PATHLINE.Point
if n>=1 and n<=N then
point=self.points[n]
else
@@ -279,11 +334,11 @@ end
function PATHLINE:GetPoint3DFromIndex(n)
local point=self:GetPointFromIndex(n)
if point then
return point.vec3
end
return nil
end
@@ -294,11 +349,11 @@ end
function PATHLINE:GetPoint2DFromIndex(n)
local point=self:GetPointFromIndex(n)
if point then
return point.vec2
end
return nil
end
@@ -332,8 +387,10 @@ end
-- @param #boolean Switch If `true` or nil, set marks. If `false`, remove marks.
-- @return #PATHLINE self
function PATHLINE:MarkPoints(Switch)
for i,_point in pairs(self.points) do
local point=_point --#PATHLINE.Point
if Switch==false then
if point.markerID then
@@ -345,13 +402,19 @@ function PATHLINE:MarkPoints(Switch)
if point.markerID then
UTILS.RemoveMark(point.markerID)
end
else
if point.markerID then
UTILS.RemoveMark(point.markerID)
end
point.markerID=UTILS.GetMarkID()
local text=string.format("Pathline %s: Point #%d\nSurface Type=%d\nHeight=%.1f m\nDepth=%.1f m", self.name, i, point.surfaceType, point.landHeight, point.depth)
local text=string.format("Pathline %s: Point #%d [UID=%d]\nSurface Type=%d\nHeight=%.1f m\nDepth=%.1f m", self.name, i, point.uid, point.surfaceType, point.landHeight, point.depth)
trigger.action.markToAll(point.markerID, text, point.vec3, "")
end
end
return self
@@ -407,6 +470,283 @@ function PATHLINE:UnDrawLine(Delay)
end
--- Draw line on F10 map.
-- @param #PATHLINE self
-- @param #boolean Switch If `true` or nil, draw pathline. If `false`, remove drawing.
-- @param #number Coalition Coalition side. Default -1 for all.
-- @param #table Color RGB color and alpha `{r, g, b, a}`. Default {0, 1, 0, 0.5}.
-- @param #number LineType Line type. Default 1=solid.
-- @return #PATHLINE self
function PATHLINE:Draw(Switch, Coalition, Color, LineType)
Coalition=Coalition or -1
Color=Color or {0, 1, 0, 0.5}
LineType=LineType or 1
if Switch==false then
for i,_point in pairs(self.points) do
local point=_point --#PATHLINE.Point
if point.lineID then
UTILS.RemoveMark(point.lineID)
end
end
else
for i=2,#self.points do
local p1=self.points[i-1] --#PATHLINE.Point
local p2=self.points[i] --#PATHLINE.Point
if p2.lineID then
UTILS.RemoveMark(p2.lineID)
end
p2.lineID=UTILS.GetMarkID()
trigger.action.lineToAll(Coalition, p2.lineID, p1.vec3, p2.vec3, Color, LineType)
end
end
return self
end
--- Get the closest point on the pathline for a given reference point.
-- @param #PATHLINE self
-- @param DCS#Vec2 Vec2 Reference Point in 2D.
-- @return DCS#Vec2 Cloest point on pathline.
-- @return #number Distance from closest point to ref point in meters.
-- @return #PATHLINE.Segment Closest segment of ref point.
function PATHLINE:GetClosestPoint2D(Vec2)
local P=nil --DCS#Vec2
local D=math.huge
local S={} --#PATHLINE.Segment
for i=2,#self.points do
local A=self.points[i-1] --#PATHLINE.Point
local B=self.points[i] --#PATHLINE.Point
local a=A.vec2
local b=B.vec2
local ab=UTILS.Vec2Substract(b, a)
local ap=UTILS.Vec2Substract(Vec2, a)
local proj=UTILS.Vec2Dot(ap, ab)
local lab=UTILS.Vec2Norm(ab)
local f=proj/lab/lab
-- Debug info.
local text=string.format("FF Proj=%.1f, |ab|=%.1f, f=%.1f", proj, lab, f)
self:T(self.lid..text)
-- Cases for finite segment.
local p=nil --DCS#Vec2
if f<0 then
p=a
elseif f>1 then
p=b
else
local r=UTILS.Vec2Mult(ab, f)
p=UTILS.Vec2Add(a, r)
end
-- Distance.
local d=UTILS.VecDist2D(p, Vec2)
if d<=D then
D=d
P=p
S.p1=A
S.p2=B
end
end
return P, D, S
end
--- Get the closest point on the pathline for a given reference point.
-- This point does not necessarily is a node of the pathline. In general it will be somewhere in between the nodes defining the pathline.
-- @param #PATHLINE self
-- @param DCS#Vec3 Vec3 Reference Point in 3D. Can also be a `COORDINATE`.
-- @return DCS#Vec3 Closest point on pathline.
-- @return #number Distance from closest point to ref point in meters.
-- @return #PATHLINE.Segment Closest segment of ref point.
function PATHLINE:GetClosestPoint3D(Vec3)
local P=nil --DCS#Vec3
local D=math.huge
local S={} --#PATHLINE.Segment
if not Vec3 then
self:E(self.lid.."ERROR: input Vec3 is nil!")
return nil, nil, nil
end
for i=2,#self.points do
local A=self.points[i-1] --#PATHLINE.Point
local B=self.points[i] --#PATHLINE.Point
local a=A.vec3
local b=B.vec3
local ab=UTILS.VecSubstract(b, a)
local ap=UTILS.VecSubstract(Vec3, a)
local proj=UTILS.VecDot(ap, ab)
local lab=UTILS.VecNorm(ab)
local f=proj/lab/lab
-- Debug info.
self:T(self.lid..string.format("Proj=%.1f, |ab|=%.1f, f=%.1f", proj, lab, f))
-- Cases for finite segment.
local p=nil --DCS#Vec2
if f<0 then
p=a
elseif f>1 then
p=b
else
local r=UTILS.VecMult(ab, f)
p=UTILS.VecAdd(a, r)
end
-- Distance.
local d=UTILS.VecDist3D(p, Vec3)
if d<=D then
D=d
P=p
S.p1=A
S.p2=B
end
end
return P, D, S
end
--- Write PATHLINE to JSON file.
-- **NOTE**: Requires `io` and `lfs` to be de-sanitized!
-- @param #PATHLINE self
-- @param #string FileName Name of the file. Default is the name of the pathline.
-- @return #PATHLINE self
function PATHLINE:WriteJSON(FileName)
if io and lfs then
-- JSON script.
local json=loadfile("Scripts\\JSON.lua")()
local data={}
-- We store the name and the points.
data.name=self.name
data.points=self.points
for i,_point in pairs(self.points) do
local point=_point --#PATHLINE.Point
--point.markerID=nil
end
-- Encode data to raw JSON. Encode converts a lua table into JSON string that can be written to file.
local raw_json=json:encode(data)
-- Debug data.
self:T(data)
-- Write in "User/Saved Games/" Folder.
local filepath=lfs.writedir() .. FileName
-- Open file for writing.
local f = io.open(filepath, "wb")
if f then
f:write(raw_json)
f:close()
self:T(self.lid .. string.format("Saving PATHLINE %s file %s", self.name, tostring(filepath)))
else
self:E(self.lid .. string.format( "ERROR: Could not save PATHLINE to file %s", tostring(filepath)))
end
else
self:E(self.lid .. string.format( "ERROR: Could not save results because IO and/or LFS are not de-sanitized!"))
end
end
--- Read PATHLINE from JSON file.
-- **NOTE**: Requires `io` and `lfs` to be de-sanitized!
-- @param #PATHLINE self
-- @param #string FileName Name of the file.
-- @return #PATHLINE self
function PATHLINE:NewFromJSON(FileName)
if io and lfs then
-- JSON script.
local json=loadfile("Scripts\\JSON.lua")()
local data={}
-- Write in "User/Saved Games/" Folder.
local filepath=lfs.writedir() .. FileName
--env.info(filepath)
-- Open file in binary mode for reading.
local f = io.open(filepath, "rb")
if f then
data = f:read("*all")
f:close()
else
env.info(string.format("WARNING: Could not load PATHLINE from file %s!", tostring(filepath)))
return nil
end
-- Decode JSON data to get a lua table.
local data=json:decode(data)
if data and data.name then
-- Create a new pathline instance.
local self=PATHLINE:New(data.name)
for i=1,#data.points do
local point=data.points[i] --#PATHLINE.Point
-- Create new point from data.
local p=self:AddPointFromVec3(point.vec3)
-- Set name.
p.name=point.name
-- Remove marker ID.
p.markerID=nil
end
return self
else
BASE:E("ERROR: Cannot find pathline name in data from JSON file. File may be corrupted!")
end
else
BASE:E("ERROR: IO and/or LFS not de-sanitized! Cannot read file.")
end
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Private functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@@ -414,32 +754,56 @@ end
--- Get 3D points of pathline.
-- @param #PATHLINE self
-- @param DCS#Vec3 Vec Position vector. Can also be a DCS#Vec2 in which case the altitude at landheight is taken.
-- @return #PATHLINE.Point
-- @return #PATHLINE.Point Pathline Point.
function PATHLINE:_CreatePoint(Vec)
local point={} --#PATHLINE.Point
self.counter=self.counter+1
point.uid=self.counter
point.mother=self.name
point.name=string.format("%s #%d", self.name, point.uid)
if Vec.z then
-- Given vec is 3D
point.vec3=UTILS.DeepCopy(Vec)
point.vec2={x=Vec.x, y=Vec.z}
else
-- Given vec is 2D
-- Given vec is 2D
point.vec2=UTILS.DeepCopy(Vec)
point.vec3={x=Vec.x, y=land.getHeight(Vec), z=Vec.y}
end
-- Get surface type.
point.surfaceType=land.getSurfaceType(point.vec2)
-- Get land height and depth.
point.landHeight, point.depth=land.getSurfaceHeightWithSeabed(point.vec2)
point.markerID=nil
return point
end
--- Get index of point in the lua table.
-- @param #PATHLINE self
-- @param #PATHLINE.Point Point Given point.
-- @return #number index
function PATHLINE:_GetPointIndex(Point)
for i,_point in pairs(self.points) do
local point=_point --#PATHLINE.Point
if point.uid==Point.uid then
return i
end
end
return nil
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+18 -1
View File
@@ -455,6 +455,23 @@ do -- COORDINATE
end
--- Returns the coordinate from the latitude and longitude given in degrees, minutes and seconds (DMS).
-- @param #COORDINATE self
-- @param #string Latitude Latitude in DMS as string, e.g. "`42° 24' 14.3"`". Not that the characters `°`, `'` and `"` are important.
-- @param #string Longitude Longitude in DMS as string, e.g. "`42° 24' 14.3"`". Not that the characters `°`, `'` and `"` are important.
-- @param #number Altitude (Optional) Altitude in meters. Default is the land height at the coordinate.
-- @return #COORDINATE
function COORDINATE:NewFromLLDMS(Latitude, Longitude, Altitude)
local lat=UTILS.LLDMSstringToDD(Latitude)
local lon=UTILS.LLDMSstringToDD(Longitude)
self=COORDINATE:NewFromLLDD(lat, lon, Altitude)
return self
end
--- Returns if the 2 coordinates are at the same 2D position.
-- @param #COORDINATE self
-- @param #COORDINATE Coordinate
@@ -663,7 +680,7 @@ do -- COORDINATE
local _,_,_,_,_,scenerys=self:ScanObjects(radius, false, false, true)
local set={}
for _,_scenery in pairs(scenerys) do
local scenery=_scenery --DCS#Object
+1
View File
@@ -89,6 +89,7 @@ do -- SET_BASE
Index = {},
Database = nil,
CallScheduler = nil,
Filter = {},
FilterCoalitionNumbers = {
[coalition.side.RED+1] = "red",
[coalition.side.BLUE+1] = "blue",
-8
View File
@@ -1667,9 +1667,6 @@ function SPAWN:SpawnWithIndex( SpawnIndex, NoBirth )
-- If RandomizeUnits, then Randomize the formation at the start point.
if self.SpawnRandomizeUnits then
if self.SpawnRandomizePosition then
PointVec3 = COORDINATE:New( SpawnTemplate.x, SpawnTemplate.route.points[1].alt, SpawnTemplate.y )
end
for UnitID = 1, #SpawnTemplate.units do
local RandomVec2 = PointVec3:GetRandomVec2InRadius( self.SpawnOuterRadius, self.SpawnInnerRadius )
if (SpawnZone) then
@@ -3521,11 +3518,6 @@ function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) -- R2.2
max = 18
ctable = CALLSIGN.F15E
end
if SpawnTemplate.units[1].type == "A6E" then
min = 4
max = 18
ctable = CALLSIGN.Intruder
end
local callsignnr = math.random(min,max)
local callsignname = "Enfield"
for name, value in pairs(ctable) do
@@ -334,31 +334,6 @@ function SPAWNSTATIC:InitCargo(IsCargo)
return self
end
--- Hide the static on the map view (visible to game master slots!).
-- @param #SPAWN self
-- @param #boolean OnOff Defaults to true
-- @return #SPAWN The SPAWN object
function SPAWNSTATIC:InitHiddenOnMap(OnOff)
self.SpawnHiddenOnMap = OnOff == false and false or true
return self
end
--- Hide the static on MFDs (visible to game master slots!).
-- @param #SPAWN self
-- @return #SPAWN The SPAWN object
function SPAWNSTATIC:InitHiddenOnMFD()
self.SpawnHiddenOnMFD = true
return self
end
--- Hide the static on planner (visible to game master slots!).
-- @param #SPAWN self
-- @return #SPAWN The SPAWN object
function SPAWNSTATIC:InitHiddenOnPlanner()
self.SpawnHiddenOnPlanner = true
return self
end
--- Initialize as dead.
-- @param #SPAWNSTATIC self
-- @param #boolean IsDead If true, this static is dead.
@@ -558,19 +533,6 @@ function SPAWNSTATIC:_SpawnStatic(Template, CountryID)
Template.mass=self.InitStaticCargoMass
end
-- hiding options
if self.SpawnHiddenOnPlanner then
Template.hiddenOnPlanner=true
end
if self.SpawnHiddenOnMFD then
Template.hiddenOnMFD=true
end
if self.SpawnHiddenOnMap then
Template.hidden=self.SpawnHiddenOnMap
end
if self.InitLinkUnit then
Template.linkUnit=self.InitLinkUnit:GetID()
Template.linkOffset=true
+28 -30
View File
@@ -306,6 +306,20 @@ function ZONE_BASE:GetCoordinate( Height ) --R2.1
return self.Coordinate
end
--- Returns the @{Core.Vector#VECTOR} of the zone.
-- @param #ZONE_BASE self
-- @param DCS#Distance Height The height in meters to add to the land height where the center of the zone is located.
-- @return Core.Vector#VECTOR The vector of the zone.
function ZONE_BASE:GetVector( Height )
self:F2(self.ZoneName)
local Vec3 = self:GetVec3( Height )
local vector=VECTOR:NewFromVec(Vec3)
return vector
end
--- Get 2D distance to a coordinate.
-- @param #ZONE_BASE self
-- @param Core.Point#COORDINATE Coordinate Reference coordinate. Can also be a DCS#Vec2 or DCS#Vec3 object.
@@ -783,7 +797,7 @@ end
-- @param #ZONE_BASE self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param #string to
-- @return #ZONE_BASE self
function ZONE_BASE:onafterTriggerRunCheck(From,Event,To)
if self:GetState() ~= "TriggerStopped" then
@@ -1158,7 +1172,7 @@ end
-- myzone:Scan({Object.Category.UNIT},{Unit.Category.GROUND_UNIT})
-- local IsAttacked = myzone:IsSomeInZoneOfCoalition( self.Coalition )
function ZONE_RADIUS:Scan( ObjectCategories, UnitCategories )
self.ScanData = {}
self.ScanData.Coalitions = {}
self.ScanData.Scenery = {}
@@ -1168,7 +1182,7 @@ function ZONE_RADIUS:Scan( ObjectCategories, UnitCategories )
local ZoneCoord = self:GetCoordinate():SetAlt()
local ZoneRadius = self:GetRadius()
--self:I({x = ZoneCoord.x, y=ZoneCoord.y, z=ZoneCoord.z, ZoneRadius = ZoneRadius})
--self:F({ZoneCoord = ZoneCoord, ZoneRadius = ZoneRadius, ZoneCoordLL = ZoneCoord:ToStringLLDMS()})
local SphereSearch = {
id = world.VolumeType.SPHERE,
@@ -1180,7 +1194,6 @@ function ZONE_RADIUS:Scan( ObjectCategories, UnitCategories )
local function EvaluateZone( ZoneObject )
--if ZoneObject:isExist() then --FF: isExist always returns false for SCENERY objects since DCS 2.2 and still in DCS 2.5
if ZoneObject and self:IsVec3InZone(ZoneObject:getPoint()) then
-- Get object category.
@@ -1266,48 +1279,38 @@ end
--- Get a set of scanned units.
-- @param #ZONE_RADIUS self
-- @param #number Coalition (optional) Filter for this coalition only.
-- @return Core.Set#SET_UNIT Set of units and statics inside the zone.
function ZONE_RADIUS:GetScannedSetUnit(Coalition)
function ZONE_RADIUS:GetScannedSetUnit()
local SetUnit = SET_UNIT:New()
self.SetUnit = self.SetUnit or SET_UNIT:New()
self.SetUnit:Clear(false)
self.SetUnit.Set={}
if self.ScanData then
for ObjectID, UnitObject in pairs( self.ScanData.Units ) do
local UnitObject = UnitObject -- DCS#Unit
if UnitObject:isExist() then
local FoundUnit = UNIT:FindByName( UnitObject:getName() )
local FoundCoalition = FoundUnit and FoundUnit:GetCoalition() or nil
local includeoncoalition = true
if Coalition ~= nil and FoundCoalition==Coalition then includeoncoalition = true else includeoncoalition = false end
if Coalition == nil then includeoncoalition = true end
--self:I(string.format("Unit name %s coalition %s filter coalition = %s include = %s",FoundUnit:GetName(),tostring(FoundCoalition),tostring(Coalition),tostring(includeoncoalition)))
if FoundUnit and includeoncoalition then
self.SetUnit:AddUnit( FoundUnit )
if FoundUnit then
SetUnit:AddUnit( FoundUnit )
else
local FoundStatic = STATIC:FindByName( UnitObject:getName(), false )
if FoundStatic then
self.SetUnit:AddUnit( FoundStatic )
SetUnit:AddUnit( FoundStatic )
end
end
end
end
end
return self.SetUnit
return SetUnit
end
--- Get a set of scanned groups.
-- @param #ZONE_RADIUS self
-- @param #number Coalition (optional) Filter for this coalition only.
-- @return Core.Set#SET_GROUP Set of groups.
function ZONE_RADIUS:GetScannedSetGroup(Coalition)
function ZONE_RADIUS:GetScannedSetGroup()
self.ScanSetGroup=self.ScanSetGroup or SET_GROUP:New() --Core.Set#SET_GROUP
self.ScanSetGroup:Clear(false)
self.ScanSetGroup.Set={}
if self.ScanData then
@@ -1316,12 +1319,7 @@ function ZONE_RADIUS:GetScannedSetGroup(Coalition)
if UnitObject:isExist() then
local FoundUnit=UNIT:FindByName(UnitObject:getName())
local FoundCoalition = FoundUnit and FoundUnit:GetCoalition() or nil
local includeoncoalition = true
if Coalition ~= nil and FoundCoalition==Coalition then includeoncoalition = true else includeoncoalition = false end
if Coalition == nil then includeoncoalition = true end
--self:I(string.format("Unit name %s coalition %s filter coalition = %s include = %s",FoundUnit:GetName(),tostring(FoundCoalition),tostring(Coalition),tostring(includeoncoalition)))
if FoundUnit and includeoncoalition then
if FoundUnit then
local group=FoundUnit:GetGroup()
self.ScanSetGroup:AddGroup(group)
end
@@ -3396,7 +3394,7 @@ function ZONE_POLYGON:Scan( ObjectCategories, UnitCategories )
self.ScanData.Scenery[SceneryType] = self.ScanData.Scenery[SceneryType] or {}
self.ScanData.Scenery[SceneryType][SceneryName] = SCENERY:Register( SceneryName, ZoneObject )
table.insert(self.ScanData.SceneryTable,self.ScanData.Scenery[SceneryType][SceneryName])
--self:I( { SCENERY = self.ScanData.Scenery[SceneryType][SceneryName] } )
--self:T( { SCENERY = self.ScanData.Scenery[SceneryType][SceneryName] } )
end
end
@@ -27,7 +27,7 @@
--
-- ### Author: **funkyfranky**
--
-- ### Contributions: FlightControl, Applevangelist
-- ### Contributions: FlightControl
--
-- ====
-- @module Functional.Artillery
@@ -780,7 +780,7 @@ ARTY.db={
--- Arty script version.
-- @field #string version
ARTY.version="1.3.4"
ARTY.version="1.3.3"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@@ -2268,7 +2268,7 @@ function ARTY._FuncTrack(weapon, self, target)
if _dist<target.radius then
-- Get random coordinate within a certain radius.
local _cr=_coord:GetRandomCoordinateInRadius(target.radius)
local _cr=_coord:GetRandomCoordinateInRadius(_data.target.radius)
-- Fire smoke at this coordinate.
_cr:Smoke(self.smokeColor)
+39 -346
View File
@@ -22,7 +22,7 @@
-- @module Functional.Mantis
-- @image Functional.Mantis.jpg
--
-- Last Update: December 2025
-- Last Update: August 2025
-------------------------------------------------------------------------
--- **MANTIS** class, extends Core.Base#BASE
@@ -61,13 +61,10 @@
-- @field #boolean checkforfriendlies If true, do not activate a SAM installation if a friendly aircraft is in firing range.
-- @field #table FilterZones Table of Core.Zone#ZONE Zones Consider SAM groups in this zone(s) only for this MANTIS instance, must be handed as #table of Zone objects.
-- @field #boolean SmokeDecoy If true, smoke short range SAM units as decoy if a plane is in firing range.
-- @field #number SmokeDecoyColor Color to use, defaults to SMOKECOLOR.White.
-- @field #number SmokeDecoyColor Color to use, defaults to SMOKECOLOR.White
-- @field #number checkcounter Counter for SAM Table refreshes.
-- @field #number DLinkCacheTime Seconds after which cached contacts in DLink will decay.
-- @field #boolean logsamstatus Log SAM status in dcs.log every cycle if true.
-- @field #boolean DetectAccoustic Set if we can also detect units accousticly.
-- @field #number DetectAccousticRadius We can hear in this range.
-- @field #table DetectAccousticCategories We can hear these categories.
-- @field #boolean logsamstatus Log SAM status in dcs.log every cycle if true
-- @extends Core.Base#BASE
@@ -116,7 +113,7 @@
-- * TOR M2
-- * C-RAM
-- * Silkworm (though strictly speaking this is a surface to ship missile)
-- * SA-2, SA-3, SA-5, SA-6, SA-7, SA-8, SA-9, SA-10, SA-11, SA-13, SA-15, SA-19, SA-21, S-300VM, S-300V4, S-400
-- * SA-2, SA-3, SA-5, SA-6, SA-7, SA-8, SA-9, SA-10, SA-11, SA-13, SA-15, SA-19
-- * From IDF mod: STUNNER IDFA, TAMIR IDFA (Note all caps!)
-- * From HDS (see note on HDS below): SA-2, SA-3, SA-10B, SA-10C, SA-12, SA-17, SA-20A, SA-20B, SA-23, HQ-2, SAMP/T Block 1, SAMP/T Block 1INT, SAMP/T Block2
-- * Other Mods: Nike
@@ -283,7 +280,7 @@
MANTIS = {
ClassName = "MANTIS",
name = "mymantis",
version = "0.9.42",
version = "0.9.34",
SAM_Templates_Prefix = "",
SAM_Group = nil,
EWR_Templates_Prefix = "",
@@ -334,9 +331,6 @@ MANTIS = {
checkcounter = 1,
DLinkCacheTime = 120,
logsamstatus = false,
DetectAccoustic = false,
DetectAccousticRadius = 2000,
DetectAccousticCategories = {Unit.Category.HELICOPTER},
}
--- Advanced state enumerator
@@ -401,14 +395,10 @@ MANTIS.SamData = {
["SA-17"] = { Range=50, Blindspot=3, Height=50, Type="Medium", Radar="SA-17" },
["SA-20A"] = { Range=150, Blindspot=5, Height=27, Type="Long" , Radar="S-300PMU1"},
["SA-20B"] = { Range=200, Blindspot=4, Height=27, Type="Long" , Radar="S-300PMU2"},
["SA-21"] = { Range=380, Blindspot=5, Height=30, Type="Long" , Radar="92N6E"},
["S-300VM"] = { Range=200, Blindspot=5, Height=30, Type="Long" , Radar="9S32M"},
["S-300V4"] = { Range=380, Blindspot=5, Height=30, Type="Long" , Radar="9S32M"},
["S-400"] = { Range=250, Blindspot=5, Height=27, Type="Long" , Radar="92N6E"},
["HQ-2"] = { Range=50, Blindspot=6, Height=35, Type="Medium", Radar="HQ_2_Guideline_LN" },
["TAMIR IDFA"] = { Range=20, Blindspot=0.6, Height=12.3, Type="Short", Radar="IRON_DOME_LN" },
["STUNNER IDFA"] = { Range=250, Blindspot=1, Height=45, Type="Long", Radar="DAVID_SLING_LN" },
["Nike"] = { Range=155, Blindspot=6, Height=30, Type="Long", Radar="HIPAR" },
["NIKE"] = { Range=155, Blindspot=6, Height=30, Type="Long", Radar="HIPAR" },
["Dog Ear"] = { Range=11, Blindspot=0, Height=9, Type="Point", Radar="Dog Ear", Point="true" },
-- CH Added to DCS core 2.9.19.x
["Pantsir S1"] = { Range=20, Blindspot=1.2, Height=15, Type="Point", Radar="PantsirS1" , Point="true" },
@@ -534,7 +524,7 @@ do
--@param #string samprefix Prefixes for the SAM groups from the ME, e.g. all groups starting with "Red Sam..."
--@param #string ewrprefix Prefixes for the EWR groups from the ME, e.g. all groups starting with "Red EWR..."
--@param #string hq Group name of your HQ (optional)
--@param #string Coalition Coalition side of your setup, e.g. "blue", "red" or "neutral"
--@param #string coalition Coalition side of your setup, e.g. "blue", "red" or "neutral"
--@param #boolean dynamic Use constant (true) filtering or just filter once (false, default) (optional)
--@param #string awacs Group name of your Awacs (optional)
--@param #boolean EmOnOff Make MANTIS switch Emissions on and off instead of changing the alarm state between RED and GREEN (optional)
@@ -558,7 +548,7 @@ do
-- mybluemantis = MANTIS:New("bluemantis","Blue SAM","Blue EWR",nil,"blue",false,"Blue Awacs")
-- mybluemantis:Start()
--
function MANTIS:New(name,samprefix,ewrprefix,hq,Coalition,dynamic,awacs, EmOnOff, Padding, Zones)
function MANTIS:New(name,samprefix,ewrprefix,hq,coalition,dynamic,awacs, EmOnOff, Padding, Zones)
-- Inherit everything from BASE class.
@@ -571,14 +561,12 @@ do
-- DONE: Treat Awacs separately, since they might be >80km off site
-- DONE: Allow tables of prefixes for the setup
-- DONE: Auto-Mode with range setups for various known SAM types.
-- DONE: Added reaction on HIT and UNIT LOST events.
self.name = name or "mymantis"
self.SAM_Templates_Prefix = samprefix or "Red SAM"
self.EWR_Templates_Prefix = ewrprefix or "Red EWR"
self.HQ_Template_CC = hq or nil
self.Coalition = Coalition or "red"
self.coalition = Coalition == "blue" and coalition.side.BLUE or coalition.side.RED
self.Coalition = coalition or "red"
self.SAM_Table = {}
self.SAM_Table_Long = {}
self.SAM_Table_Medium = {}
@@ -728,8 +716,6 @@ do
self:AddTransition("*", "SeadSuppressionStart", "*") -- SEAD has switched off one group.
self:AddTransition("*", "SeadSuppressionEnd", "*") -- SEAD has switched on one group.
self:AddTransition("*", "SeadSuppressionPlanned", "*") -- SEAD has planned a suppression.
self:AddTransition("*", "SAMUnitHit", "*") -- A SAM unit was hit
self:AddTransition("*", "SAMUnitLost", "*") -- A SAM Unit was lost
self:AddTransition("*", "Stop", "Stopped") -- Stop FSM.
------------------------
@@ -840,24 +826,6 @@ do
-- @param Wrapper.Group#GROUP Group The suppressed GROUP object
-- @param #string Name Name of the suppressed group
--- On After "SAMUnitHit" event. A SAM Unit was hit.
-- @function [parent=#MANTIS] OnAfterSeadSuppressionEnd
-- @param #MANTIS self
-- @param #string From The From State
-- @param #string Event The Event
-- @param #string To The To State
-- @param Wrapper.Group#GROUP Group The GROUP of the hit UNIT object
-- @param #string Name Name of the suppressed group
--- On After "SAMUnitLoast" event. A SAM Unit was lost.
-- @function [parent=#MANTIS] OnAfterSeadSuppressionEnd
-- @param #MANTIS self
-- @param #string From The From State
-- @param #string Event The Event
-- @param #string To The To State
-- @param Wrapper.Group#GROUP Group The GROUP of the lost UNIT object
-- @param #string Name Name of the suppressed group
return self
end
@@ -865,138 +833,6 @@ do
-- MANTIS helper functions
-----------------------------------------------------------------------
--- Set to accept accoustic detection. Set this *before* MANTIS starts!
-- @param #MANTIS self
-- @param #number Radius Radius in which we can "hear" units. Defaults to 2000 meters.
-- @param #table UnitCategories Set what Unit Categories we can "hear". Defaults to `{Unit.Category.HELICOPTER}`
-- @return #MANTIS self
function MANTIS:SetAccousticDetectionOn(Radius,UnitCategories)
self.DetectAccoustic = true
self.DetectAccousticRadius = Radius or 2000
self.DetectAccousticCategories = UnitCategories or {Unit.Category.HELICOPTER}
return self
end
--- Switch off accoustic detection.
-- @param #MANTIS self
-- @return #MANTIS self
function MANTIS:SetAccousticDetectionOff()
self.DetectAccoustic = false
return self
end
--- [Internal] Function to manage hits on SAM units
-- @param #MANTIS self
-- @param Core.Event#EVENTDATA EventData The EVENT data
-- @return #MANTIS self
function MANTIS:_EventHandler(EventData)
self:T(self.lid .. "_EventHandler")
local function IsOneOfOurs(name)
for _,_name in pairs(self.ewr_templates) do
if string.find(name,_name,1,true) then
return true
end
end
return false
end
local function SwitchSAMOn(Name,Group)
local suppressed = self.SuppressedGroups[Name] or false
if not suppressed and self.SamStateTracker[Name] == "GREEN" then
self.SamStateTracker[Name] = "RED"
if self.UseEmOnOff then
-- DONE: add emissions on/off
Group:EnableEmission(true)
elseif (not self.UseEmOnOff) then
Group:OptionAlarmStateRed()
end
self:__RedState(1,Group)
if self.SmokeDecoy == true then
self:_SmokeUnits(Group)
end
end
end
local coordinate -- Core.Point#COORDINATE
local Name -- #string
local Group -- Wrapper.Group#GROUP
local lasthit = 0
local firsthit = false
local alerton = false
-- Check if we can get a location
local data = EventData -- Core.Event#EVENTDATA
if data.id == EVENTS.Hit then
-- Unit hit, one of ours?
if data.TgtGroupName and IsOneOfOurs(data.TgtGroupName) then
self:T("Unit hit in group: "..data.TgtGroupName)
if data.TgtGroup then
lasthit = data.TgtGroup:GetProperty("MANTIS_LASTHIT")
firsthit = (lasthit==nil) and true or false
if firsthit == true then alerton = true end
if lasthit ~= nil and timer.getTime()-lasthit > self.ShoradTime then alerton = true end
coordinate = data.TgtGroup:GetCoordinate()
Name = data.TgtGroupName
Group = data.TgtGroup
if alerton == true then
self:__SAMUnitHit(1,Group,Name)
SwitchSAMOn(Name,Group)
end
if coordinate and self.debug then
local text = coordinate:ToStringMGRS()
self:I("Location: "..text)
end
end
end
end
if data.id == EVENTS.UnitLost then
if data.IniGroupName and IsOneOfOurs(data.IniGroupName) then
self:T("Unit lost in group: "..data.IniGroupName)
if data.IniGroup then
lasthit = data.IniGroup:GetProperty("MANTIS_LASTHIT")
firsthit = (lasthit==nil) and true or false
if firsthit == true then alerton = true end
if lasthit ~= nil and timer.getTime()-lasthit > self.ShoradTime then alerton = true end
coordinate = data.IniGroup:GetCoordinate()
Name = data.IniGroupName
Group = data.IniGroup
alerton = true
SwitchSAMOn(Name,Group)
self:__SAMUnitLost(1,Group,Name)
if coordinate and self.debug then
local text = coordinate:ToStringMGRS()
self:I("Location: "..text)
end
end
end
end
if firsthit == true or alerton == true then
Group:SetProperty("MANTIS_LASTHIT",timer.getTime())
end
if coordinate ~= nil and Name ~= nil and Group ~=nil and alerton == true then
if self.ShoradLink then
self:T("Shorad activated for: "..Name)
local Shorad = self.Shorad -- Functional.Shorad#SHORAD
local radius = self.checkradius
local ontime = self.ShoradTime
Shorad:WakeUpShorad(Name, radius, ontime, nil, true)
self:__ShoradActivated(1,Name, radius, ontime)
end
if self.autorelocate and Group then
Group:RelocateGroundRandomInRadius(20,500,true,true,nil,true)
end
end
return self
end
--- [Internal] Function to get the self.SAM_Table
-- @param #MANTIS self
-- @return #table table
@@ -1067,111 +903,13 @@ do
return self
end
--- Add a single reject zone to MANTIS.
-- @param #MANTIS self
-- @param Core.Zone#ZONE Zone The zone to be added
-- @return #MANTIS self
function MANTIS:AddRejectZone(Zone)
if Zone and Zone:IsInstanceOf("ZONE_BASE") then
table.insert(self.RejectZones,Zone)
self.usezones = true
end
return self
end
--- Add a single accept zone to MANTIS.
-- @param #MANTIS self
-- @param Core.Zone#ZONE Zone The zone to be added
-- @return #MANTIS self
function MANTIS:AddAcceptZone(Zone)
if Zone and Zone:IsInstanceOf("ZONE_BASE") then
table.insert(self.AcceptZones,Zone)
self.usezones = true
end
return self
end
--- Add a single conflict zone to MANTIS.
-- @param #MANTIS self
-- @param Core.Zone#ZONE Zone The zone to be added
-- @return #MANTIS self
function MANTIS:AddConflictZone(Zone)
if Zone and Zone:IsInstanceOf("ZONE_BASE") then
table.insert(self.ConflictZones,Zone)
self.usezones = true
end
return self
end
--- Function to set corridor zones.
-- @param #MANTIS self
-- @param Core.Set#SET_ZONE CorridorZones Can be handed in as SET\_ZONE or single ZONE object.
-- @return #MANTIS self
function MANTIS:SetCorridorZones(CorridorZones)
self:T(self.lid .. "SetCorridorZones")
if CorridorZones and CorridorZones:IsInstanceOf("SET_ZONE") then
self.corridorzones = CorridorZones
self.usecorridors = true
elseif CorridorZones and CorridorZones:IsInstanceOf("ZONE_BASE") then
if not self.corridorzones then self.corridorzones = SET_ZONE:New() end
self.corridorzones:AddZone(CorridorZones)
self.usecorridors = true
end
if self.intelset then
for _,_intel in pairs(self.intelset) do
_intel:SetCorridorZones(self.corridorzones)
end
end
return self
end
--- Function to add one corridor zone.
-- @param #MANTIS self
-- @param Core.Zone#ZONE CorridorZone The ZONE object to be added.
-- @return #MANTIS self
function MANTIS:AddCorridorZone(CorridorZone)
self:T(self.lid .. "AddCorridorZone")
self:SetCorridorZones(CorridorZone)
return self
end
--- Function to set corridor zone floor and ceiling in FEET.
-- @param #MANTIS self
-- @param #number Floor Floor altitude ASL in feet.
-- @param #number Ceiling Ceiling altitude ASL in feet.
-- @return #MANTIS self
function MANTIS:SetCorridorZoneFloorAndCeiling(Floor,Ceiling)
self.corridorfloor = UTILS.FeetToMeters(Floor)
self.corridorceiling = UTILS.FeetToMeters(Ceiling)
if self.intelset then
for _,_intel in pairs(self.intelset) do
_intel:SetCorridorLimits(self.corridorfloor,self.corridorceiling)
end
end
return self
end
--- Function to set corridor zone floor and ceiling in METERS.
-- @param #MANTIS self
-- @param #number Floor Floor altitude ASL in meters.
-- @param #number Ceiling Ceiling altitude ASL in meters.
-- @return #MANTIS self
function MANTIS:SetCorridorZoneFloorAndCeilingMeters(Floor,Ceiling)
self.corridorfloor = Floor
self.corridorceiling = Ceiling
if self.intelset then
for _,_intel in pairs(self.intelset) do
_intel:SetCorridorLimits(self.corridorfloor,self.corridorceiling)
end
end
return self
end
--- Function to set the detection radius of the EWR in meters. (Deprecated, SAM range is used)
-- @param #MANTIS self
-- @param #number radius Radius of the EWR detection zone
function MANTIS:SetEWRRange(radius)
self:T(self.lid .. "SetEWRRange")
--local radius = radius or 80000
-- self.acceptrange = radius
return self
end
@@ -1587,12 +1325,11 @@ do
return inzone
end
--- [Internal] Function to prefilter height based and check for Helo activity.
--- [Internal] Function to prefilter height based
-- @param #MANTIS self
-- @param #number height
-- @param Core.Point#COORDINATE SamCoordinate
-- @return #table set
function MANTIS:_PreFilterHeight(height,SamCoordinate)
function MANTIS:_PreFilterHeight(height)
self:T(self.lid.."_PreFilterHeight")
local set = {}
local dlink = self.Detection -- Ops.Intel#INTEL_DLINK
@@ -1601,22 +1338,8 @@ do
local contact = _contact -- Ops.Intel#INTEL.Contact
local grp = contact.group -- Wrapper.Group#GROUP
if grp:IsAlive() then
local coord = grp:GetCoordinate()
local dist = 0
local include = true
if grp:IsGround() then include = false end
if grp:GetCoalition() == self.coalition then include = false end
if coord and SamCoordinate and grp:IsHelicopter() then
dist = coord:Get2DDistance(SamCoordinate) or 0
if dist > self.ShoradActDistance then include = false end -- we do not want long range shooting at helos
end
if self.debug then
local text = "Looking at Group: "..grp:GetName() or "N/A"
text = text .. " Include = "..tostring(include)
MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.verbose):ToLog()
end
local grpalt = grp:GetHeight(true)
if grpalt < height and grpalt > 10 and include == true then
if grp:GetHeight(true) < height then
local coord = grp:GetCoordinate()
table.insert(set,coord)
end
end
@@ -1640,7 +1363,7 @@ do
local set = dectset
if dlink then
-- DEBUG
set = self:_PreFilterHeight(height,samcoordinate)
set = self:_PreFilterHeight(height)
end
--self.friendlyset -- Core.Set#SET_GROUP
if self.checkforfriendlies == true and self.friendlyset == nil then
@@ -1721,30 +1444,14 @@ do
self.intelset = {}
local IntelOne = INTEL:New(groupset,self.coalition,self.name.." IntelOne")
IntelOne.DetectAccoustic = self.DetectAccoustic
IntelOne.DetectAccousticRadius = self.DetectAccousticRadius or 2000
IntelOne.DetectAccousticUnitTypes = self.DetectAccousticCategories or {Unit.Category.HELICOPTER}
--IntelOne:SetClusterAnalysis(true,true,true)
if self.usecorridors == true then
IntelOne:SetCorridorZones(self.corridorzones)
if self.corridorfloor or self.corridorceiling then
IntelOne:SetCorridorLimits(self.corridorfloor,self.corridorceiling)
end
end
local IntelOne = INTEL:New(groupset,self.Coalition,self.name.." IntelOne")
--IntelOne:SetClusterAnalysis(true,true)
--IntelOne:SetClusterRadius(5000)
IntelOne:Start()
local IntelTwo = INTEL:New(samset,self.coalition,self.name.." IntelTwo")
IntelTwo.DetectAccoustic = self.DetectAccoustic
IntelTwo.DetectAccousticRadius = self.DetectAccousticRadius or 2000
IntelTwo.DetectAccousticUnitTypes = self.DetectAccousticCategories or {Unit.Category.HELICOPTER}
--IntelTwo:SetClusterAnalysis(true,true,true)
if self.usecorridors == true then
IntelTwo:SetCorridorZones(self.corridorzones)
if self.corridorfloor or self.corridorceiling then
IntelTwo:SetCorridorLimits(self.corridorfloor,self.corridorceiling)
end
end
local IntelTwo = INTEL:New(samset,self.Coalition,self.name.." IntelTwo")
--IntelTwo:SetClusterAnalysis(true,true)
--IntelTwo:SetClusterRadius(5000)
IntelTwo:Start()
local CacheTime = self.DLinkCacheTime or 120
@@ -2073,27 +1780,6 @@ do
self.ShoradLink = false
return self
end
--- [Internal] Function to smoke a group in decoy.
-- @param #MANTIS self
-- @param Wrapper.Group#GROUP Group
-- @return #MANTIS self
function MANTIS:_SmokeUnits(Group)
self:T("Smoking")
local LastSmoketime=Group:GetProperty("MANTIS_LASTSMOKE_TIME") or 0
local TNow = timer.getTime()
if TNow - LastSmoketime > 290 then -- Smoking lasts 5 minutes
Group:SetProperty("MANTIS_LASTSMOKE_TIME",TNow)
local units = Group:GetUnits() or {}
local smoke = self.SmokeDecoyColor or SMOKECOLOR.White
for _,unit in pairs(units) do
if unit and unit:IsAlive() then
unit:GetCoordinate():Smoke(smoke)
end
end
end
return self
end
-----------------------------------------------------------------------
-- MANTIS main functions
@@ -2121,7 +1807,7 @@ do
local radius = _data[3]
local height = _data[4]
local blind = _data[5] * 1.25 + 1
local shortsam = (_data[6] ~= MANTIS.SamType.LONG) and true or false
local shortsam = (_data[6] == MANTIS.SamType.SHORT) and true or false
if not shortsam then
shortsam = (_data[6] == MANTIS.SamType.POINT) and true or false
end
@@ -2148,11 +1834,18 @@ do
end
if self.SamStateTracker[name] ~= "RED" and switch then
self:__RedState(1,samgroup)
self.SamStateTracker[name] = "RED"
end
-- DONE Restrict on Distance
if shortsam == true and self.SmokeDecoy == true and Distance < self.DetectAccousticRadius*1.5 then
-- TODO doesn't work
if shortsam == true and self.SmokeDecoy == true then
self:T("Smoking")
self:_SmokeUnits(samgroup)
local units = samgroup:GetUnits() or {}
local smoke = self.SmokeDecoyColor or SMOKECOLOR.White
for _,unit in pairs(units) do
if unit and unit:IsAlive() then
unit:GetCoordinate():Smoke(smoke)
end
end
end
-- link in to SHORAD if available
-- DONE: Test integration fully
@@ -2337,9 +2030,13 @@ do
else
self.Detection = self:StartIntelDetection()
end
--[[
if self.advAwacs and not self.automode then
self.AWACS_Detection = self:StartAwacsDetection()
end
--]]
if self.autoshorad then
self.Shorad = SHORAD:New(self.name.."-SHORAD","SHORAD",self.SAM_Group,self.ShoradActDistance,self.ShoradTime,self.Coalition,self.UseEmOnOff,self.SmokeDecoy,self.SmokeDecoyColor)
self.Shorad = SHORAD:New(self.name.."-SHORAD","SHORAD",self.SAM_Group,self.ShoradActDistance,self.ShoradTime,self.coalition,self.UseEmOnOff)
self.Shorad:SetDefenseLimits(80,95)
self.ShoradLink = true
self.Shorad.Groupset=self.ShoradGroupSet
@@ -2348,10 +2045,6 @@ do
if self.shootandscoot and self.SkateZones and self.Shorad then
self.Shorad:AddScootZones(self.SkateZones,self.SkateNumber or 3,self.ScootRandom,self.ScootFormation)
end
self:HandleEvent(EVENTS.Hit,self._EventHandler)
self:HandleEvent(EVENTS.UnitLost,self._EventHandler)
self:__Status(-math.random(1,10))
return self
end
@@ -100,7 +100,7 @@ PSEUDOATC.id="PseudoATC | "
--- PSEUDOATC version.
-- @field #number version
PSEUDOATC.version="0.10.6"
PSEUDOATC.version="0.10.5"
-----------------------------------------------------------------------------------------------------------------------------------------
@@ -579,9 +579,6 @@ function PSEUDOATC:MenuAirports(GID,UID)
-- Create menu reporting commands
missionCommands.addCommandForGroup(GID, "Weather Report", submenu, self.ReportWeather, self, GID, UID, pos, name)
missionCommands.addCommandForGroup(GID, "Request BR", submenu, self.ReportBR, self, GID, UID, pos, name)
if self.radios then
missionCommands.addCommandForGroup(GID, "Radios", submenu, self.ReportRadios, self, GID, UID, pos, name)
end
-- Debug message.
self:T(string.format(PSEUDOATC.id.."Creating airport menu item %s for ID %d", name, GID))
@@ -708,30 +705,7 @@ function PSEUDOATC:ReportWeather(GID, UID, position, location)
end
--- Report airport radio information.
-- @param #PSEUDOATC self
-- @param #number GID Group id of player unit.
-- @param #number UID Unit id of player.
-- @param Core.Point#COORDINATE position Coordinate of the airport.
-- @param #string location Name of the airport.
function PSEUDOATC:ReportRadios(GID, UID, position, location)
self:F({GID=GID, UID=UID, position=position, location=location})
if self.radios then
local Text=""
local radio = self.radios:GetClosestRadio(position,9) --Navigation.Radios#RADIOS.Radio
if radio then
Text=self.radios:_GetMarkerText(radio)
else
Text=self.group[GID].player[UID].playername..", no radio information found!"
end
-- Send message
self:_DisplayMessageToGroup(self.group[GID].player[UID].unit, Text, self.mdur, true)
end
return self
end
--- Report absolute bearing and range from player unit to airport.
--- Report absolute bearing and range form player unit to airport.
-- @param #PSEUDOATC self
-- @param #number GID Group id of player unit.
-- @param #number UID Unit id of player.
@@ -1005,22 +979,3 @@ function PSEUDOATC:_myname(unitname)
return string.format("%s (%s)", csign, pname)
end
--- Returns a string which consits of this callsign and the player name.
-- @param #PSEUDOATC self
-- @param #string path Path to map data, e.g. `[[<DCS_Install_Directory>\Mods\terrains\<Map_Name>\Radio.lua]]` (replace with correct path).
-- Needs `lfs` and `io` to be desanitized in the `MissionScripting.lua` in `<DCS_Install_Directory>\Scripts`
-- @return #PSEUDOATC self
-- @usage
--
-- mypseudoatc:SetUsingRadioInformationFromMap([[C:\Program Files\Eagle Dynamics\DCS World.Openbeta\Mods\terrains\Caucasus\Radio.lua]])
--
function PSEUDOATC:SetUsingRadioInformationFromMap(path)
if RADIOS and lfs and io then
self.radios = RADIOS:NewFromFile(path)
else
self:E("PSEUDOATC:SetUsingRadioInformationFromMap Needs `lfs`and `io` to be desanitized in the `MissionScripting.lua` in `<DCS_Install_Directory>/Scripts`")
end
return self
end
+14 -16
View File
@@ -5584,7 +5584,7 @@ end
-- @param #string dest Name of the destination airport.
function RAT:_ATCAddFlight(name, dest)
-- Debug info
BASE:T(RAT.id..string.format("ATC %s: Adding flight %s with destination %s.", dest, name, dest))
BASE:I(RAT.id..string.format("ATC %s: Adding flight %s with destination %s.", dest, name, dest))
-- Create new flight
local flight={} --#RAT.AtcFlight
@@ -5603,7 +5603,7 @@ end
function RAT._ATCDelFlight(t,entry)
for k,_ in pairs(t) do
if k==entry then
BASE:T(RAT.id..string.format("Removing flight %s from queue", entry))
BASE:I(RAT.id..string.format("Removing flight %s from queue", entry))
t[entry]=nil
end
end
@@ -5614,7 +5614,7 @@ end
-- @param #string name Group name of the flight.
-- @param #number time Time the fight first registered.
function RAT._ATCRegisterFlight(name, time)
BASE:T(RAT.id..string.format("Flight %s registered at ATC for landing clearance.", name))
BASE:I(RAT.id..string.format("Flight %s registered at ATC for landing clearance.", name))
RAT.ATC.flight[name].Tarrive=time
RAT.ATC.flight[name].holding=0
end
@@ -5649,16 +5649,15 @@ function RAT._ATCStatus()
-- Aircraft is holding.
local text=string.format("ATC %s: Flight %s is holding for %i:%02d. %s.", dest, name, hold/60, hold%60, busy)
BASE:T(RAT.id..text)
BASE:I(RAT.id..text)
elseif hold==RAT.ATC.onfinal then
-- Aircarft is on final approach for landing.
local Tonfinal = flight.Tonfinal or timer.getTime()-1
local Tfinal=Tnow-Tonfinal
local Tfinal=Tnow-flight.Tonfinal
local text=string.format("ATC %s: Flight %s is on final. Waiting %i:%02d for landing event.", dest, name, Tfinal/60, Tfinal%60)
BASE:T(RAT.id..text)
BASE:I(RAT.id..text)
elseif hold==RAT.ATC.unregistered then
@@ -5712,13 +5711,13 @@ function RAT._ATCCheck()
-- Debug message.
local text=string.format("ATC %s: Flight %s runway is busy. You are #%d of %d in landing queue. Your holding time is %i:%02d.",
airportname, flightname, qID, nqueue, flight.holding/60, flight.holding%60)
BASE:T(RAT.id..text)
BASE:I(RAT.id..text)
else
local text=string.format("ATC %s: Flight %s was cleared for landing. Your holding time was %i:%02d.",
airportname, flightname, flight.holding/60, flight.holding%60)
BASE:T(RAT.id..text)
BASE:I(RAT.id..text)
-- Clear flight for landing.
RAT._ATCClearForLanding(airportname, flightname)
@@ -5773,7 +5772,7 @@ function RAT._ATCClearForLanding(airportname, flightname)
-- Debug message.
BASE:T(RAT.id..string.format("ATC %s: Flight %s cleared for landing", airportname, flightname))
BASE:I(RAT.id..string.format("ATC %s: Flight %s cleared for landing", airportname, flightname))
if string.find(flightname,"#") then
flightname = string.match(flightname,"^(.+)#")
@@ -5800,9 +5799,8 @@ function RAT._ATCFlightLanded(name)
-- Times for holding and final approach.
local Tnow=timer.getTime()
local Tonfinal = flight.Tonfinal or timer.getTime()-1
local Tfinal=Tnow-Tonfinal
local Thold=Tonfinal-flight.Tarrive
local Tfinal=Tnow-flight.Tonfinal
local Thold=flight.Tonfinal-flight.Tarrive
local airport=RAT.ATC.airport[dest] --#RAT.AtcAirport
@@ -5825,9 +5823,9 @@ function RAT._ATCFlightLanded(name)
local TrafficPerHour=airport.traffic/(timer.getTime()-RAT.ATC.T0)*3600
-- Debug info
BASE:T(RAT.id..string.format("ATC %s: Flight %s landed. Tholding = %i:%02d, Tfinal = %i:%02d.", dest, name, Thold/60, Thold%60, Tfinal/60, Tfinal%60))
BASE:T(RAT.id..string.format("ATC %s: Number of flights still on final %d.", dest, airport.Nonfinal))
BASE:T(RAT.id..string.format("ATC %s: Traffic report: Number of planes landed in total %d. Flights/hour = %3.2f.", dest, airport.traffic, TrafficPerHour))
BASE:I(RAT.id..string.format("ATC %s: Flight %s landed. Tholding = %i:%02d, Tfinal = %i:%02d.", dest, name, Thold/60, Thold%60, Tfinal/60, Tfinal%60))
BASE:I(RAT.id..string.format("ATC %s: Number of flights still on final %d.", dest, airport.Nonfinal))
BASE:I(RAT.id..string.format("ATC %s: Traffic report: Number of planes landed in total %d. Flights/hour = %3.2f.", dest, airport.traffic, TrafficPerHour))
if string.find(name,"#") then
name = string.match(name,"^(.+)#")
+6 -20
View File
@@ -109,6 +109,7 @@
-- @field Core.Menu#MENU_MISSION menuF10root Specific user defined root F10 menu.
-- @field #number ceilingaltitude Range ceiling altitude in ft MSL. Aircraft above this altitude are not considered to be in the range. Default is 20000 ft.
-- @field #boolean ceilingenabled Range has a ceiling and is not unlimited. Default is false.
-- @extends Core.Fsm#FSM
--- *Don't only practice your art, but force your way into its secrets; art deserves that, for it and knowledge can raise man to the Divine.* - Ludwig van Beethoven
@@ -1270,9 +1271,6 @@ function RANGE:SetSRS(PathToSRS, Port, Coalition, Frequency, Modulation, Volume,
self.controlmsrs:SetCoalition(Coalition or coalition.side.BLUE)
self.controlmsrs:SetLabel("RANGEC")
self.controlmsrs:SetVolume(Volume or 1.0)
if self.rangezone then
self.controlmsrs:SetCoordinate(self.rangezone:GetCoordinate())
end
self.controlsrsQ = MSRSQUEUE:New("CONTROL")
self.instructmsrs=MSRS:New(PathToSRS or MSRS.path, Frequency or 305, Modulation or radio.modulation.AM)
@@ -1280,9 +1278,6 @@ function RANGE:SetSRS(PathToSRS, Port, Coalition, Frequency, Modulation, Volume,
self.instructmsrs:SetCoalition(Coalition or coalition.side.BLUE)
self.instructmsrs:SetLabel("RANGEI")
self.instructmsrs:SetVolume(Volume or 1.0)
if self.rangezone then
self.instructmsrs:SetCoordinate(self.rangezone:GetCoordinate())
end
self.instructsrsQ = MSRSQUEUE:New("INSTRUCT")
if PathToGoogleKey then
@@ -1321,13 +1316,8 @@ function RANGE:SetSRSRangeControl( frequency, modulation, voice, culture, gender
self.rangecontrol = true
if relayunitname then
local unit = UNIT:FindByName(relayunitname)
if unit then
local Coordinate = unit:GetCoordinate()
self.rangecontrolrelayname = relayunitname
self.controlmsrs:SetCoordinate(Coordinate)
else
MESSAGE:New("RANGE: Control Relay Unit "..relayunitname.." not found!",15,"ERROR"):ToAllIf(self.Debug):ToLog()
end
local Coordinate = unit:GetCoordinate()
self.rangecontrolrelayname = relayunitname
end
return self
end
@@ -1355,13 +1345,9 @@ function RANGE:SetSRSRangeInstructor( frequency, modulation, voice, culture, gen
self.instructor = true
if relayunitname then
local unit = UNIT:FindByName(relayunitname)
if unit then
local Coordinate = unit:GetCoordinate()
self.instructmsrs:SetCoordinate(Coordinate)
self.instructorrelayname = relayunitname
else
MESSAGE:New("RANGE: Instructor Relay Unit "..relayunitname.." not found!",15,"ERROR"):ToAllIf(self.Debug):ToLog()
end
local Coordinate = unit:GetCoordinate()
self.instructmsrs:SetCoordinate(Coordinate)
self.instructorrelayname = relayunitname
end
return self
end
+45 -161
View File
@@ -230,10 +230,8 @@ SCORING = {
ClassID = 0,
Players = {},
AutoSave = true,
version = "1.18.5",
version = "1.18.4",
ScoringScenery = nil, -- Core.Set#SET_SCENERY
SceneryHitsInZone = false,
LoadSave = false,
}
local _SCORINGCoalition = {
@@ -252,16 +250,15 @@ local _SCORINGCategory = {
--- Creates a new SCORING object to administer the scoring achieved by players.
-- @param #SCORING self
-- @param #string GameName The name of the game. This name is also logged in the CSV score file.
-- @param #string SavePath (Optional) Path where to save the CSV files, defaults to your **<User>\\Saved Games\\DCS\\Logs** folder. See next two options.
-- @param #boolean AutoSave (Optional) If passed as `false`, then swith autosave off. This stores a detailed table which will never be loaded again by SCORING (for e.g. Discord purposes).
-- @param #boolean LoadSave (Optional) If passed as `true` save summary scores per player, and load at restart of the mission.
-- @param #string SavePath (Optional) Path where to save the CSV file, defaults to your **<User>\\Saved Games\\DCS\\Logs** folder.
-- @param #boolean AutoSave (Optional) If passed as `false`, then swith autosave off.
-- @return #SCORING self
-- @usage
--
-- -- Define a new scoring object for the mission Gori Valley.
-- ScoringObject = SCORING:New( "Gori Valley" )
--
function SCORING:New( GameName, SavePath, AutoSave, LoadSave )
function SCORING:New( GameName, SavePath, AutoSave )
-- Inherits from BASE
local self = BASE:Inherit( self, BASE:New() ) -- #SCORING
@@ -269,7 +266,7 @@ function SCORING:New( GameName, SavePath, AutoSave, LoadSave )
if GameName then
self.GameName = GameName
else
error( "A game name must be given to register the scoring results!" )
error( "A game name must be given to register the scoring results" )
end
-- Additional Object scores
@@ -304,12 +301,6 @@ function SCORING:New( GameName, SavePath, AutoSave, LoadSave )
self.penaltyoncoalitionchange = true
self:SetDisplayMessagePrefix()
self.SceneryHitsInZone = false
if LoadSave then
self.LoadSave = LoadSave
end
-- Event handlers
self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash )
@@ -337,61 +328,11 @@ function SCORING:New( GameName, SavePath, AutoSave, LoadSave )
end
self:I("SCORING "..tostring(GameName).." started! v"..self.version)
if LoadSave == true then
self:_LoadPlayerSummaryScore()
end
return self
end
--- [Internal] Helper to load scores from disk at scoring start
-- @param #SCORING self
-- @return #SCORING self
function SCORING:_LoadPlayerSummaryScore()
if lfs and io and self.LoadSave == true then
local path = self.AutoSavePath or lfs.writedir() .. [[Logs\]]
local filename = self.GameName or "PlayerScoresSummary"
filename = filename..".csv"
if UTILS.CheckFileExists(path,filename) then
local ok, data = UTILS.LoadFromFile(path,filename)
-- Playername;;Score;;Penalty
table.remove(data,1)
for _,_data in pairs(data) do
local line = UTILS.Split(_data,";;")
local playername = tostring(line[1])
local score = tonumber(line[2])
local penalty = tonumber(line[3])
self:I(string.format("Player %s Score %d Penalty %d",playername,score,penalty))
local PlayerData = self.Players[playername]
if not PlayerData then
PlayerData = {}
PlayerData.Hit = {}
PlayerData.Destroy = {}
PlayerData.Goals = {}
PlayerData.Goals[self.GameName] = {Score = score, Penalty = penalty}
PlayerData.Mission = {}
PlayerData.HitPlayers = {}
PlayerData.Score = score
PlayerData.Penalty = penalty
PlayerData.PenaltyCoalition = 0
PlayerData.PenaltyWarning = 0
self.Players[playername] = PlayerData
else
PlayerData.Score = score
PlayerData.Penalty =penalty
self.Players[playername] = PlayerData
PlayerData.Goals[self.GameName] = {Score = score, Penalty = penalty}
end
end
end
end
return self
end
--- Set a prefix string that will be displayed at each scoring message sent.
-- @param #SCORING self
-- @param #string DisplayMessagePrefix (Default="Scoring: ") The scoring prefix string.
@@ -638,22 +579,6 @@ function SCORING:AddZoneScore( ScoreZone, Score )
return self
end
--- Allow Scenery hits in Zones to count (no specific(!) scenery targets). NOTE - Allowing this can spam your scoring display!
-- @param #SCORING self
-- @return #SCORING self
function SCORING:EnableSceneryHitsinZones()
self.SceneryHitsInZone = true
return self
end
--- Disallow Scenery hits in Zones to count (no specific(!) scenery targets).
-- @param #SCORING self
-- @return #SCORING self
function SCORING:DisableSceneryHitsinZones()
self.SceneryHitsInZone = false
return self
end
--- Add a @{Core.Set#SET_ZONE} to define additional scoring when any object is destroyed in that zone.
-- Note that if a @{Core.Zone} with the same name is already within the scoring added, the @{Core.Zone} (type) and Score will be replaced!
-- This allows for a dynamic destruction zone evolution within your mission.
@@ -952,20 +877,6 @@ function SCORING:AddGoalScorePlayer( PlayerName, GoalTag, Text, Score )
-- PlayerName can be nil, if the Unit with the player crashed or due to another reason.
if PlayerName then
local PlayerData = self.Players[PlayerName]
if not PlayerData then
PlayerData = {}
PlayerData.Goals = {}
PlayerData.Hit = {}
PlayerData.Destroy = {}
PlayerData.Goals = {}
PlayerData.Mission = {}
PlayerData.HitPlayers = {}
PlayerData.Score = 0
PlayerData.Penalty = 0
PlayerData.PenaltyCoalition = 0
PlayerData.PenaltyWarning = 0
self.Players[PlayerName] = PlayerData
end
PlayerData.Goals[GoalTag] = PlayerData.Goals[GoalTag] or { Score = 0 }
PlayerData.Goals[GoalTag].Score = PlayerData.Goals[GoalTag].Score + Score
@@ -1242,7 +1153,7 @@ function SCORING:_EventOnHit( Event )
TargetType = "Scenery"
TargetSceneryObject = TargetUNIT
self:T("***** Target is Scenery and TargetUNIT is SCENERY object!")
--UTILS.PrintTableToLog(TargetSceneryObject)
UTILS.PrintTableToLog(TargetSceneryObject)
end
TargetUnitCoalition = _SCORINGCoalition[TargetCoalition]
@@ -1463,18 +1374,17 @@ function SCORING:_EventOnHit( Event )
-- Player contains the score data from self.Players[WeaponPlayerName]
local PlayerName = Event.IniPlayerName or "Ghost"
local Player = self.Players[PlayerName]
if Player then
Player.Score = Player.Score + Score
Player.Score = Player.Score + self.ScoreIncrementOnHit
MESSAGE:NewType( self.DisplayMessagePrefix .. "hit in zone '" .. ScoreZone:GetName() .. "'." ..
"Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. "Total: " .. Player.Score - Player.Penalty,
MESSAGE.Type.Information )
:ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() )
:ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() )
self:ScoreCSV( PlayerName, "", "HIT_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Zone", TargetUnitType )
end
end
Player.Score = Player.Score + Score
Player.Score = Player.Score + self.ScoreIncrementOnHit
MESSAGE:NewType( self.DisplayMessagePrefix .. "hit in zone '" .. ScoreZone:GetName() .. "'." ..
"Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. "Total: " .. Player.Score - Player.Penalty,
MESSAGE.Type.Information )
:ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() )
:ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() )
self:ScoreCSV( PlayerName, "", "HIT_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Zone", TargetUnitType )
end
end
end
@@ -1657,24 +1567,22 @@ function SCORING:_EventOnDeadOrCrash( Event )
end
else
if self.SceneryHitsInZone == true then
-- Check if there are Zones where the destruction happened.
for ZoneName, ScoreZoneData in pairs( self.ScoringZones ) do
self:F( { ScoringZone = ScoreZoneData } )
local ScoreZone = ScoreZoneData.ScoreZone -- Core.Zone#ZONE_BASE
local Score = ScoreZoneData.Score
if ScoreZone:IsVec2InZone( TargetUnit:GetVec2() ) then
Player.Score = Player.Score + Score
TargetDestroy.Score = TargetDestroy.Score + Score
MESSAGE:NewType( self.DisplayMessagePrefix .. "Scenery destroyed in zone '" .. ScoreZone:GetName() .. "'." ..
"Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. "Total: " .. Player.Score - Player.Penalty,
MESSAGE.Type.Information )
:ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() )
:ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() )
self:ScoreCSV( PlayerName, "", "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Scenery", TargetUnitType )
Destroyed = true
end
-- Check if there are Zones where the destruction happened.
for ZoneName, ScoreZoneData in pairs( self.ScoringZones ) do
self:F( { ScoringZone = ScoreZoneData } )
local ScoreZone = ScoreZoneData.ScoreZone -- Core.Zone#ZONE_BASE
local Score = ScoreZoneData.Score
if ScoreZone:IsVec2InZone( TargetUnit:GetVec2() ) then
Player.Score = Player.Score + Score
TargetDestroy.Score = TargetDestroy.Score + Score
MESSAGE:NewType( self.DisplayMessagePrefix .. "Scenery destroyed in zone '" .. ScoreZone:GetName() .. "'." ..
"Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. "Total: " .. Player.Score - Player.Penalty,
MESSAGE.Type.Information )
:ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() )
:ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() )
self:ScoreCSV( PlayerName, "", "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Scenery", TargetUnitType )
Destroyed = true
end
end
end
@@ -2018,12 +1926,9 @@ end
--- Report all players score
-- @param #SCORING self
-- @param Wrapper.Group#GROUP PlayerGroup The player group.
-- @param #boolean JustScore If this is true, return just a table with playernames and overall scores.
-- @return #table ReportTable Table returned if JustScore is true.
function SCORING:ReportScoreAllSummary( PlayerGroup, JustScore )
function SCORING:ReportScoreAllSummary( PlayerGroup )
local PlayerMessage = ""
local ReportTable = {}
self:T( { "Summary Score Report of All Players", Players = self.Players } )
@@ -2055,28 +1960,20 @@ function SCORING:ReportScoreAllSummary( PlayerGroup, JustScore )
local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions
local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + PenaltyGoals + PenaltyMissions
if JustScore~=true then
PlayerMessage =
string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )",
PlayerName,
PlayerScore - PlayerPenalty,
PlayerScore,
PlayerPenalty
)
MESSAGE:NewType( PlayerMessage, MESSAGE.Type.Overview ):ToGroup( PlayerGroup )
else
ReportTable[PlayerName] = {["Score"]=PlayerScore,["Penalty"]=PlayerPenalty}
end
PlayerMessage =
string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )",
PlayerName,
PlayerScore - PlayerPenalty,
PlayerScore,
PlayerPenalty
)
MESSAGE:NewType( PlayerMessage, MESSAGE.Type.Overview ):ToGroup( PlayerGroup )
end
end
return ReportTable
end
--- Opens a score CSV file to log the scores.
-- @param #SCORING self
-- @param #number sSeconds
-- @return #string ClockString
function SCORING:SecondsToClock( sSeconds )
local nSeconds = sSeconds
if nSeconds == 0 then
@@ -2204,19 +2101,6 @@ function SCORING:ScoreCSV( PlayerName, TargetPlayerName, ScoreType, ScoreTimes,
self.CSVFile:write( "\n" )
end
if lfs and io and self.LoadSave == true then
local path = self.AutoSavePath or lfs.writedir() .. [[Logs\]]
local filename = self.GameName or "PlayerScoresSummary"
filename = filename..".csv"
local data = self:ReportScoreAllSummary("",true)
local text = "-- Playername;;Score;;Penalty\n"
for _playername,_data in pairs(data or {}) do
text = text..string.format("%s;;%d;;%d\n")
end
UTILS.SaveToFile(path,filename,text)
end
end
--- Close CSV file
+5 -47
View File
@@ -48,9 +48,7 @@
-- @field #number minscootdist Min distance of the next zone
-- @field #number maxscootdist Max distance of the next zone
-- @field #boolean scootrandomcoord If true, use a random coordinate in the zone and not the center
-- @field #string scootformation Formation to take for scooting, e.g. "Vee" or "Cone"
-- @field #boolean SmokeDecoy = false,
-- @field #number SmokeDecoyColor = SMOKECOLOR.White
-- @field #string scootformation Formation to take for scooting, e.g. "Vee" or "Cone"
-- @extends Core.Base#BASE
@@ -116,9 +114,7 @@ SHORAD = {
SkateZones = nil,
minscootdist = 100,
maxscootdist = 3000,
scootrandomcoord = false,
SmokeDecoy = false,
SmokeDecoyColor = SMOKECOLOR.White
scootrandomcoord = false,
}
-----------------------------------------------------------------------
@@ -165,10 +161,8 @@ do
-- @param #number ActiveTimer Determines how many seconds the systems stay on red alert after wake-up call
-- @param #string Coalition Coalition, i.e. "blue", "red", or "neutral"
-- @param #boolean UseEmOnOff Use Emissions On/Off rather than Alarm State Red/Green (default: use Emissions switch)
-- @param #boolean SmokeDecoy Throw smoke decoy when getting activated. Defaults to false.
-- @param #number SmokeDecoyColor SMOLECOLOR to use. Defaults to SMOLECOLOR.White
-- @return #SHORAD self
function SHORAD:New(Name, ShoradPrefix, Samset, Radius, ActiveTimer, Coalition, UseEmOnOff, SmokeDecoy, SmokeDecoyColor)
function SHORAD:New(Name, ShoradPrefix, Samset, Radius, ActiveTimer, Coalition, UseEmOnOff)
local self = BASE:Inherit( self, FSM:New() )
self:T({Name, ShoradPrefix, Samset, Radius, ActiveTimer, Coalition})
@@ -177,7 +171,6 @@ do
self.name = Name or "MyShorad"
self.Prefixes = ShoradPrefix or "SAM SHORAD"
self.Radius = Radius or 20000
if type(Coalition) == "number" then Coalition = string.lower(UTILS.GetCoalitionName(Coalition)) end
self.Coalition = Coalition or "blue"
self.Samset = Samset or GroupSet
self.ActiveTimer = ActiveTimer or 600
@@ -188,15 +181,8 @@ do
self.DefenseLowProb = 70 -- probability to detect a missile shot, low margin
self.DefenseHighProb = 90 -- probability to detect a missile shot, high margin
self.UseEmOnOff = true -- Decide if we are using Emission on/off (default) or AlarmState red/green
if UseEmOnOff == false then self.UseEmOnOff = UseEmOnOff end
if SmokeDecoy then
self.SmokeDecoy = SmokeDecoy
self.SmokeDecoyColor = SmokeDecoyColor or SMOKECOLOR.White
end
self:I("*** SHORAD - Started Version 0.3.5")
self:I("*** SHORAD - Started Version 0.3.4")
-- Set the string id for output to DCS.log file.
self.lid=string.format("SHORAD %s | ", self.name)
self:_InitState()
@@ -465,32 +451,6 @@ do
return returnname
end
--- Smoke a SHORAD Group
-- @param #SHORAD self
-- @param Wrapper.Group#GROUP Group The Shorad Group to Smoke
-- @return self
function SHORAD:_SmokeUnits(Group)
if self.SmokeDecoy == true then
if Group and Group:IsAlive() then
local units = Group:GetUnits()
for _,_unit in pairs(units) do
local unit = _unit -- Wrapper.Unit#UNIT
if unit and unit:IsAlive() then
local coordinate = unit:GetCoordinate()
if coordinate then
coordinate:SwitchSmokeOffsetOn()
coordinate:Smoke(self.SmokeDecoyColor,Duration,nil,Name,true,1,20)
coordinate:Smoke(self.SmokeDecoyColor,Duration,nil,Name,true,180,20)
coordinate:Smoke(self.SmokeDecoyColor,Duration,nil,Name,true,270,20)
coordinate:Smoke(self.SmokeDecoyColor,Duration,nil,Name,true,90,20)
end
end
end
end
end
return self
end
--- Calculate if the missile shot is detected
-- @param #SHORAD self
-- @return #boolean Returns true for a detection, else false
@@ -577,7 +537,7 @@ do
local text = string.format("Shot at SHORAD %s! Evading!", _group:GetName())
self:T(text)
local m = MESSAGE:New(text,10,"SHORAD"):ToAllIf(self.debug)
self:_SmokeUnits(_group)
--Shoot and Scoot
if self.shootandscoot then
self:__ShootAndScoot(1,_group)
@@ -592,7 +552,6 @@ do
_group:EnableEmission(true)
end
_group:OptionAlarmStateRed()
self:_SmokeUnits(_group)
if self.ActiveGroups[groupname] == nil then -- no timer yet for this group
self.ActiveGroups[groupname] = { Timing = ActiveTimer }
local endtime = timer.getTime() + (ActiveTimer * math.random(75,100) / 100 ) -- randomize wakeup a bit
@@ -725,7 +684,6 @@ do
local ShootingWeapon = EventData.Weapon -- Identify the weapon fired
local ShootingWeaponName = EventData.WeaponName -- return weapon type
-- get firing coalition
if not EventData.IniGroup then return self end
local weaponcoalition = EventData.IniGroup:GetCoalition()
-- get detection probability
if self:_CheckCoalition(weaponcoalition) then --avoid overhead on friendly fire
@@ -4112,10 +4112,10 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay,
-- Get name of template group.
local templategroupname=group:GetName()
local unit = group:GetUnit(1)
local Descriptors= (unit and unit:IsAlive()) and unit:GetDesc() or {}
local Descriptors=group:GetUnit(1):GetDesc()
local Category=group:GetCategory()
local TypeName=group:GetTypeName() or "none"
local TypeName=group:GetTypeName()
local SpeedMax=group:GetSpeedMax()
local RangeMin=group:GetRange()
local smax,sx,sy,sz=_GetObjectSize(Descriptors)
@@ -6132,7 +6132,6 @@ function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrol
unit.onboard_num=asset.modex[i]
end
if asset.callsign then
--UTILS.PrintTableToLog(asset.callsign)
unit.callsign=asset.callsign[i]
end
+3 -2
View File
@@ -118,7 +118,6 @@ __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/PlayerRecce.lua' )
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/Squadron.lua' )
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/Target.lua' )
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/EasyGCICAP.lua' )
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/EasyA2G.lua' )
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/AI/AI_Balancer.lua' )
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/AI/AI_Air.lua' )
@@ -189,8 +188,10 @@ __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Tasking/Task_Cargo_Dispatcher
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Tasking/Task_Capture_Zone.lua' )
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Tasking/Task_Capture_Dispatcher.lua' )
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Navigation/Point.lua' )
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Navigation/Beacons.lua' )
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Navigation/Point.lua' )
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Navigation/Procedure.lua' )
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Navigation/FlightPlan.lua' )
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Navigation/Radios.lua' )
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Navigation/Towns.lua' )
@@ -116,7 +116,6 @@ __Moose.Include( 'Ops\\Operation.lua' )
__Moose.Include( 'Ops\\FlightControl.lua' )
__Moose.Include( 'Ops\\PlayerRecce.lua' )
__Moose.Include( 'Ops\\EasyGCICAP.lua' )
__Moose.Include( 'Ops\\EasyA2G.lua' )
__Moose.Include( 'AI\\AI_Balancer.lua' )
__Moose.Include( 'AI\\AI_Air.lua' )
@@ -0,0 +1,481 @@
--- **NAVIGATION** - Flight Plan.
--
-- **Main Features:**
--
-- * Manage navigation aids
-- * VOR, NDB
--
-- ===
--
-- ## Example Missions:
--
-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Ops%20-%20FlightPlan).
--
-- ===
--
-- ### Author: **funkyfranky**
--
-- ===
-- @module Navigation.FlightPlan
-- @image NAVIGATION_FlightPlan.png
--- FLIGHTPLAN class.
-- @type FLIGHTPLAN
-- @field #string ClassName Name of the class.
-- @field #number verbose Verbosity of output.
-- @field #table fixes Navigation fixes.
-- @field Core.Pathline#PATHLINE pathline Pathline of the plan.
-- @field Wrapper.Airbase#AIRBASE departureAirbase Departure airbase.
-- @field Wrapper.Airbase#AIRBASE destinationAirbase Destination airbase.
-- @field #number altitudeCruiseMin Minimum cruise altitude in feet MSL.
-- @field #number altitudeCruiseMax Maximum cruise altitude in feet MSL.
-- @extends Core.Pathline#PATHLINE
--- *Life is what happens to us while we are making other plans.* -- Allen Saunders
--
-- ===
--
-- # The FLIGHTPLAN Concept
--
-- This class has a great concept!
--
-- # Basic Setup
--
-- A new `FLIGHTPLAN` object can be created with the @{#FLIGHTPLAN.New}() function.
--
-- myFlightplan=FLIGHTPLAN:New("Plan A")
-- myFleet:SetPortZone(ZonePort1stFleet)
-- myFleet:Start()
--
-- A fleet needs a *port zone*, which is set via the @{#FLIGHTPLAN.SetPortZone}(`PortZone`) function. This is the zone where the naval assets are spawned and return to.
--
-- Finally, the fleet needs to be started using the @{#FLIGHTPLAN.Start}() function. If the fleet is not started, it will not process any requests.
--
-- @field #FLIGHTPLAN
FLIGHTPLAN = {
ClassName = "FLIGHTPLAN",
verbose = 0,
fixes = {}
}
--- Type of flightplan.
-- @type FLIGHTPLAN.Type
-- @field #string IFRH Instrument Flying Rules High Altitude.
-- @field #string IFRL Instrument Flying Rules Low Altitude.
-- @field #string VFR Visual Flight Rules.
FLIGHTPLAN.Type={
IFRH = "IFR High",
IFRL = "IFR Low",
VFR = "VFR",
}
--- FLIGHTPLAN class version.
-- @field #string version
FLIGHTPLAN.version="0.0.1"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- ToDo list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO: How to connect SID, STAR, ENROUTE, TRANSITION, APPROACH. Typical flightplan SID --> ENROUTE --> STAR --> APPROACH
-- TODO: Add approach.
-- DONE: How to handle the FLIGHTGROUP:_LandAtAirBase
-- TODO: Do we always need a holding pattern? https://www.faa.gov/air_traffic/publications/atpubs/aip_html/part2_enr_section_1.5.html#:~:text=If%20no%20holding%20pattern%20is,than%20that%20desired%20by%20ATC.
-- DOEN: Read from MSFS file.
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Create a new FLIGHTPLAN instance.
-- @param #FLIGHTPLAN self
-- @param #string Name Name of this flight plan.
-- @return #FLIGHTPLAN self
function FLIGHTPLAN:New(Name)
-- Inherit everything from BASE class.
self=BASE:Inherit(self, PATHLINE:New(Name)) -- #FLIGHTPLAN
-- Set alias.
self.alias=tostring(Name)
-- Set some string id for output to DCS.log file.
self.lid=string.format("FLIGHTPLAN %s | ", self.alias)
--self.pathline=PATHLINE:New(Name)
-- Debug info.
self:I(self.lid..string.format("Created FLIGHTPLAN!"))
return self
end
--- Create a new FLIGHTPLAN instance from another FLIGHTPLAN acting as blue print.
-- The newly created flight plan is deep copied from the given one.
-- @param #FLIGHTPLAN self
-- @param #FLIGHTPLAN FlightPlan Blue print of the flight plan to copy.
-- @return #FLIGHTPLAN self
function FLIGHTPLAN:NewFromFlightPlan(FlightPlan)
self=UTILS.DeepCopy(FlightPlan)
return self
end
--- Create a new FLIGHTPLAN instance from a given file.
-- Currently, the file has to be an MSFS 2020 .pln file as, *e.g.*, exported from [Navigraph](https://navigraph.com/).
--
-- **Note** that the flight plan does only cover the departure, enroute and arrival portions but **not the approach** part!
-- @param #FLIGHTPLAN self
-- @param #string FileName Full path to file.
-- @return #FLIGHTPLAN self
function FLIGHTPLAN:NewFromFile(FileName)
if UTILS.FileExists(FileName) then
self=FLIGHTPLAN._ReadFileMSFS(FileName)
else
error(string.format("ERROR: File not found! File name=%s", tostring(FileName)))
end
return self
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Add navigation fix to the flight plan.
-- @param #FLIGHTPLAN self
-- @param Navigation.Point#NAVPOINT NavFix The nav fix.
-- @return #FLIGHTPLAN self
function FLIGHTPLAN:AddNavFix(NavFix)
table.insert(self.fixes, NavFix)
local point=self:AddPointFromVec3(NavFix.vector:GetVec3(true))
point.navpoint=NavFix
return self
end
--- Set departure airbase.
-- @param #FLIGHTPLAN self
-- @param #string AirbaseName Name of the airbase or AIRBASE object.
-- @return #FLIGHTPLAN self
function FLIGHTPLAN:SetDepartureAirbase(AirbaseName)
self.departureAirbase=AIRBASE:FindByName(AirbaseName)
return self
end
--- Set destination airbase.
-- @param #FLIGHTPLAN self
-- @param #string AirbaseName Name of the airbase or AIRBASE object.
-- @return #FLIGHTPLAN self
function FLIGHTPLAN:SetDestinationAirbase(AirbaseName)
self.destinationAirbase=AIRBASE:FindByName(AirbaseName)
return self
end
--- Set cruise altitude.
-- @param #FLIGHTPLAN self
-- @param #number AltMin Minimum altitude in feet MSL.
-- @param #number AltMax Maximum altitude in feet MSL. Default is `AltMin`.
-- @return #FLIGHTPLAN self
function FLIGHTPLAN:SetCruiseAltitude(AltMin, AltMax)
self.altitudeCruiseMin=AltMin
self.altitudeCruiseMax=AltMax or self.altitudeCruiseMin
return self
end
--- Set cruise speed.
-- @param #FLIGHTPLAN self
-- @param #number SpeedMin Minimum speed in knots.
-- @param #number SpeedMax Maximum speed in knots. Default is `SpeedMin`.
-- @return #FLIGHTPLAN self
function FLIGHTPLAN:SetCruiseSpeed(SpeedMin, SpeedMax)
self.speedCruiseMin=SpeedMin
self.speedCruiseMax=SpeedMax or self.speedCruiseMin
return self
end
--- Get the name of this flight plan.
-- @param #FLIGHTPLAN self
-- @return #string The name.
function FLIGHTPLAN:GetName()
return self.alias
end
--- Get cruise altitude. This returns a random altitude between the set min/max cruise altitudes.
-- @param #FLIGHTPLAN self
-- @return #number Cruise altitude in feet MSL.
function FLIGHTPLAN:GetCruiseAltitude()
local alt=10000
if self.altitudeCruiseMin and self.altitudeCruiseMax then
alt=math.random(self.altitudeCruiseMin, self.altitudeCruiseMax)
elseif self.altitudeCruiseMin then
alt=self.altitudeCruiseMin
elseif self.altitudeCruiseMax then
alt=self.altitudeCruiseMax
end
return alt
end
--- Get cruise speed. This returns a random speed between the set min/max cruise speeds.
-- @param #FLIGHTPLAN self
-- @return #number Cruise speed in knots.
function FLIGHTPLAN:GetCruiseSpeed()
local speed=250
if self.speedCruiseMin and self.speedCruiseMax then
speed=math.random(self.speedCruiseMin, self.speedCruiseMax)
elseif self.speedCruiseMin then
speed=self.speedCruiseMin
elseif self.altitudeCruiseMax then
speed=self.speedCruiseMax
end
return speed
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Private Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Read flight plan from a given MSFS 2020 .plt file.
-- @param #string FileName Name of the file.
-- @return #FLIGHTPLAN The flight plan.
function FLIGHTPLAN._ReadFileMSFS(FileName)
local function readfile(filename)
local lines = {}
-- Open file in read binary mode.
local file=assert(io.open(filename, "rb"), string.format("File not found! File name = %s", tostring(filename)))
for line in file:lines() do
lines[#lines+1] = line
end
-- Close file.
file:close()
-- Return data
return lines
end
--- This function returns an XML element, i.e. the string between <...> and </...>.
local function getXMLelement(line)
local element=string.match(line, ">(.+)<")
return element
end
--- This function returns Latitude and Longitude
local function getLatLong(line)
local latlong=getXMLelement(line)
-- The format is "N41° 38' 20.00",E41° 33' 19.00",+000000.00" so we still need to process that.
local lat,long=string.match(latlong, "(.+),(.+),")
return lat,long
end
-- Read data from file.
local data=readfile(FileName)
local flightplan={}
local waypoints={}
local wp=nil
local gotwaypoint=false
for i,line in pairs(data) do
--print(line)
-- Title
if string.find(line, "<Title>") then
flightplan.title=getXMLelement(line)
end
-- Departure ICAO
if string.find(line, "<DepartureID>") then
flightplan.departureICAO=getXMLelement(line)
end
-- Destination ICAO
if string.find(line, "<DestinationID>") then
flightplan.destinationICAO=getXMLelement(line)
end
-- FPType
if string.find(line, "<FPType>") then
flightplan.plantype=getXMLelement(line)
end
-- Route type
if string.find(line, "<RouteType>") then
flightplan.routetype=getXMLelement(line)
end
-- Cruise alt in feet
if string.find(line, "<CruisingAlt>") then
flightplan.altCruise=getXMLelement(line)
end
-- Departure LLA
if string.find(line, "<DepartureLLA>") then
local lat,long=getLatLong(line)
end
-- Destination LLA
if string.find(line, "<DestinationLLA>") then
local lat,long=getLatLong(line)
end
-- Departure Name
if string.find(line, "<DepartureName>") then
local DepartureName=getXMLelement(line)
end
-- DestinationName
if string.find(line, "<DestinationName>") then
local DestinationName=getXMLelement(line)
end
---
-- Waypoint stuff
---
-- New waypoint starts.
if string.find(line, "ATCWaypoint id") then
--Get string inside quotes " and ".
local wpid=string.match(line, [["(.+)"]])
-- Create a new wp table.
wp={}
-- Set waypoint name.
wp.name=wpid
end
-- Waypoint info ends.
if string.find(line, "</ATCWaypoint>") then
-- This is the end of the waypoint.
-- Add info to waypoints table.
table.insert(waypoints, wp)
-- Set waypoint to nil. We create an empty table if the next wp starts.
wp=nil
end
-- Waypoint type (Airport, Intersection, NDB, VORTAC)
if string.find(line, "<ATCWaypointType>") then
local wptype=getXMLelement(line)
wp.type=wptype
end
-- Waypoint position.
if string.find(line, "<WorldPosition>") then
wp.lat, wp.long=getLatLong(line)
end
-- Runway should exist for initial and final WP if it is an airport.
if string.find(line, "RunwayNumberFP") then
wp.runway=getXMLelement(line)
end
-- Runway designator: LEFT, RIGHT, CENTER
if string.find(line, "RunwayDesignatorFP") then
wp.runwayDesignator=getXMLelement(line)
end
-- Segment is Departure
if string.find(line, "<DepartureFP>") then
wp.segment="Departure"
end
-- Segment is Arrival
if string.find(line, "<ArrivalFP>") then
wp.segment="Arrival"
end
-- Segment is Enroute
if string.find(line, "<ATCAirway>") then
wp.segment="Enroute"
end
-- Approach type: VORDME, LOCALIZER
if string.find(line, "ApproachTypeFP") then
flightplan.approachtype=getXMLelement(line)
end
-- Approach type suffic: Z
if string.find(line, "SuffixFP") then
local SuffixFP=getXMLelement(line)
end
end
for key, value in pairs(flightplan) do
env.info(string.format("Flightplan %s=%s", key, tostring(value)))
end
env.info(string.format("Number of waypoints=%d", #waypoints))
for i,wp in pairs(waypoints) do
env.info(string.format("Waypoint name=%s type=%s segment=%s runway=%s lat=%s long=%s", wp.name, wp.type, tostring(wp.segment), tostring(wp.runway)..tostring(wp.runwayDesignator or ""), wp.lat, wp.long))
end
-- Create a new flightplan.
local fp=FLIGHTPLAN:New(flightplan.title)
-- Set cruise altitude.
fp:SetCruiseAltitude(flightplan.altCruise)
-- Set departure and destination airports.
fp:SetDepartureAirbase(flightplan.departureICAO)
fp:SetDestinationAirbase(flightplan.destinationICAO)
--TODO: Remove first and last waypoint if they are identical to the departure/destination airport!
for i,wp in pairs(waypoints) do
-- Create a navpoint.
local navpoint=NAVPOINT:NewFromLLDMS(wp.name, wp.type, wp.lat, wp.long)
navpoint:SetAltMin(flightplan.altCruise)
-- Add point to flightplan.
-- TODO: section departure, enroute, arrival.
fp:AddNavFix(navpoint)
end
return fp
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@@ -0,0 +1,333 @@
--- **NAVIGATION** - Prodedures for Departure (*e.g.* SID), Enroute, Arrival (*e.g.* STAR) and Approach.
--
-- **Main Features:**
--
-- * Stuff
-- * More Stuff
--
-- ===
--
-- ## Example Missions:
--
-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Navigation%20-%20Template).
--
-- ===
--
-- ### Author: **funkyfranky**
--
-- ===
-- @module Navigation.Procedure
-- @image NAVIGATION_Procedure.png
--- APPROACH class.
-- @type APPROACH
-- @field #string ClassName Name of the class.
-- @field #number verbose Verbosity of output.
-- @field #string apptype Approach type (ILS, VOR, LOC).
-- @field Wrapper.Airbase#AIRBASE airbase Airbase of this approach.
-- @field Wrapper.Airbase#AIRBASE.Runway runway Runway of this approach.
-- @field Navigation.Point#NAVAID navaid Primary navigation aid.
-- @field #number wpcounter Running number counting the waypoints to generate its UID.
-- @list <#APPROACH.Waypoint> path Path of approach consisting of waypoints.
-- @extends Core.Base#BASE
--- *A fleet of British ships at war are the best negotiators.* -- Horatio Nelson
--
-- ===
--
-- # The APPROACH Concept
--
-- A typical approach has (up to) four segments. It starts with the initial approach segment, followed by the intermediate approach segment, followed
-- by the final approach segment. In case something goes wrong during the final approach, the missed approach segment kicks in.
--
-- The initial approach segment starts at the initial approach fix (IAF). The segment can contain multiple other fixes, that need to be passed.
-- Note, that an approach procedure can have more than one intitial approach segment and IAF.
--
-- The intermediate approach segment starts at the intermediate fix (IF). The intermediate approach segment blends the initial approach segment into the final approach segment.
-- It is the segment in which aircraft configuration, speed, and positioning adjustments are made for entry into the final approach segment.
--
--
-- https://en.wikipedia.org/wiki/Visual_approach
-- https://en.wikipedia.org/wiki/Instrument_approach
--
-- # Basic Setup
--
-- A new `APPROACH` object can be created with the @{#APPROACH.New}() function.
--
-- myTemplate=APPROACH:New()
-- myTemplate:SetXYZ(X, Y, Z)
--
-- This is how it works.
--
-- @field #APPROACH
APPROACH = {
ClassName = "APPROACH",
verbose = 0,
wpcounter = 0,
}
--- Type of approach.
-- @type APPROACH.Type
-- @field #string VFR Visual Flight Rules.
-- @field #string VOR VOR
-- @field #string NDB NDB
APPROACH.Type={
VFR="VFR",
VOR="VOR",
ILS="ILS",
}
--- Setments of approach.
-- @type APPROACH.Segment
-- @field #string INITIAL Initial approach segment.
-- @field #string INTERMEDIATE Intermediate approach segment.
-- @field #string FINAL Final approach segment.
-- @field #string MISSED Missed approach segment.
APPROACH.Segment={
INITIAL="Initial",
INTERMEDIATE="Intermediate",
FINAL="Final",
MISSED="Missed",
}
--- Waypoint of the approach.
-- @type APPROACH.Waypoint
-- @field #number uid Unique ID of the point.
-- @field #string segment The segment this point belongs to.
-- @field Navigation.Point#NAVFIX navfix The navigation fix that determines the coordinates of this point.
--- APPROACH class version.
-- @field #string version
APPROACH.version="0.0.1"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- ToDo list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO: A lot...
-- Initial approach segment --> Intermediate approach segment: starts at IF --> Final approach segment
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Create a new APPROACH class instance.
-- @param #APPROACH self
-- @param #string Type Type of approach (ILS, VOR, LOC).
-- @param Wrapper.Airbase#AIRBASE Airbase The airbase or name of the airbase.
-- @param Wrapper.Airbase#AIRBASE.Runway Runway The runway or name of the runway.
-- @return #APPROACH self
function APPROACH:New(Type, Airbase, Runway)
-- Inherit everything from BASE class.
self=BASE:Inherit(self, BASE:New()) -- #APPROACH
-- Set approach type.
-- TODO: Check if this is a valid/known approach type.
self.apptype=Type
if type(Airbase)=="string" then
self.airbase=AIRBASE:FindByName(Airbase)
else
self.airbase=Airbase
end
if type(Runway)=="string" then
self.runway=self.airbase:GetRunwayByName(Runway)
else
self.runway=Runway
end
-- Debug info.
self:I("Created new approach for airbase %s: type=%s, runway=%s", self.airbase:GetName(), self.apptype, self.runway.name)
return self
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Set the primary navigation aid used in the approach.
-- @param #APPROACH self
-- @param Navigation.Point#NAVAID NavAid The NAVAID.
-- @return #APPROACH self
function APPROACH:SetNavAid(NavAid)
self.navaid=NavAid
return self
end
--- Add a waypoint to the path of the approach.
-- @param #APPROACH self
-- @param Navigation.Point#NAVFIX NavFix The navigation fix.
-- @param #string Segment The approach segment this fix belongs to.
-- @return #APPROACH.Waypoint The waypoint data table.
function APPROACH:AddNavFix(NavFix, Segment)
self.wpcounter=self.wpcounter+1
local point={} --#APPROACH.Waypoint
point.uid=self.wpcounter
point.segment=Segment
point.navfix=NavFix
table.insert(self.path, point)
return point
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Private Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Add private functions here.
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- DEPARTURE class.
-- @type DEPARTURE
-- @field #string ClassName Name of the class.
-- @field #number verbose Verbosity of output.
-- @field #string apptype DEPARTURE type (ILS, VOR, LOC).
-- @field Wrapper.Airbase#AIRBASE airbase Airbase of this DEPARTURE.
-- @field Wrapper.Airbase#AIRBASE.Runway runway Runway of this DEPARTURE.
-- @field Navigation.Point#NAVAID navaid Primary navigation aid.
-- @field #number wpcounter Running number counting the waypoints to generate its UID.
-- @list <#DEPARTURE.Waypoint> path Path of DEPARTURE consisting of waypoints.
-- @extends Core.Base#BASE
--- *A fleet of British ships at war are the best negotiators.* -- Horatio Nelson
--
-- ===
--
-- # The DEPARTURE Concept
--
-- Bla.
--
-- @field #DEPARTURE
DEPARTURE = {
ClassName = "DEPARTURE",
verbose = 0,
wpcounter = 0,
}
--- Type of DEPARTURE.
-- @type DEPARTURE.Type
-- @field #string VOR VOR
-- @field #string NDB NDB
DEPARTURE.Type={
VOR="VOR",
ILS="ILS",
}
--- Setments of DEPARTURE.
-- @type DEPARTURE.Segment
-- @field #string INITIAL Initial DEPARTURE segment.
-- @field #string INTERMEDIATE Intermediate DEPARTURE segment.
-- @field #string FINAL Final DEPARTURE segment.
-- @field #string MISSED Missed DEPARTURE segment.
DEPARTURE.Segment={
INITIAL="Initial",
INTERMEDIATE="Intermediate",
FINAL="Final",
MISSED="Missed",
}
--- Waypoint of the DEPARTURE.
-- @type DEPARTURE.Waypoint
-- @field #number uid Unique ID of the point.
-- @field #string segment The segment this point belongs to.
-- @field Navigation.Point#NAVFIX navfix The navigation fix that determines the coordinates of this point.
--- DEPARTURE class version.
-- @field #string version
DEPARTURE.version="0.0.1"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- ToDo list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO: A lot...
-- Initial DEPARTURE segment --> Intermediate DEPARTURE segment: starts at IF --> Final DEPARTURE segment
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Create a new DEPARTURE class instance.
-- @param #DEPARTURE self
-- @param Wrapper.Airbase#AIRBASE Airbase The airbase or name of the airbase.
-- @param Wrapper.Airbase#AIRBASE.Runway Runway The runway or name of the runway.
-- @param #string Type Type of DEPARTURE (ILS, VOR, LOC).
-- @return #DEPARTURE self
function DEPARTURE:New(Airbase, Runway)
-- Inherit everything from BASE class.
self=BASE:Inherit(self, BASE:New()) -- #DEPARTURE
if type(Airbase)=="string" then
self.airbase=AIRBASE:FindByName(Airbase)
else
self.airbase=Airbase
end
if type(Runway)=="string" then
self.runway=self.airbase:GetRunwayByName(Runway)
else
self.runway=Runway
end
return self
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Set the primary navigation aid used in the DEPARTURE.
-- @param #DEPARTURE self
-- @param Navigation.Point#NAVAID NavAid The NAVAID.
-- @return #DEPARTURE self
function DEPARTURE:SetNavAid(NavAid)
self.navaid=NavAid
return self
end
--- Add a waypoint to the path of the DEPARTURE.
-- @param #DEPARTURE self
-- @param Navigation.Point#NAVFIX NavFix The navigation fix.
-- @param #string Segment The DEPARTURE segment this fix belongs to.
-- @return #DEPARTURE.Waypoint The waypoint data.
function DEPARTURE:AddWaypoint(NavFix, Segment)
self.wpcounter=self.wpcounter+1
local point={} --#DEPARTURE.Waypoint
point.uid=self.wpcounter
point.segment=Segment
point.navfix=NavFix
table.insert(self.path, point)
return point
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Private Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Add DEPARTURE private functions here.
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@@ -49,7 +49,7 @@
--
-- A new `RADIOS` object can be created with the @{#RADIOS.NewFromFile}(*radio_lua_file*) function.
--
-- local radios=RADIOS:NewFromFile("<DCS_Install_Directory>\Mods\terrains\<Map_Name>\Radio.lua")
-- local radios=RADIOS:NewFromFile("<DCS_Install_Directory>\Mods\terrains\<Map_Name>\radio.lua")
-- radios:MarkerShow()
--
-- This will load the radios from the `<DCS_Install_Directory>` for the specific map and place markers on the F10 map. This is the first step you should do to ensure that the file
@@ -0,0 +1,108 @@
--- **NAVIGATION** - Template.
--
-- **Main Features:**
--
-- * Stuff
-- * More Stuff
--
-- ===
--
-- ## Example Missions:
--
-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Navigation%20-%20Template).
--
-- ===
--
-- ### Author: **funkyfranky**
--
-- ===
-- @module Navigation.Template
-- @image NAVIGATION_Template.png
--- TEMPLATE class.
-- @type TEMPLATE
-- @field #string ClassName Name of the class.
-- @field #number verbose Verbosity of output.
-- @extends Core.Base#BASE
--- *A fleet of British ships at war are the best negotiators.* -- Horatio Nelson
--
-- ===
--
-- # The TEMPLATE Concept
--
-- The TEMPLATE class has a great concept!
--
-- # Basic Setup
--
-- A new `TEMPLATE` object can be created with the @{#TEMPLATE.New}() function.
--
-- myTemplate=TEMPLATE:New()
-- myTemplate:SetXYZ(X, Y, Z)
--
-- This is how it works.
--
-- @field #TEMPLATE
TEMPLATE = {
ClassName = "TEMPLATE",
verbose = 0,
}
--- Type of navaid
-- @type TEMPLATE.Type
-- @field #string VOR VOR
-- @field #string NDB NDB
TEMPLATE.TYPE={
VOR="VOR",
NDB="NDB",
}
--- TEMPLATE class version.
-- @field #string version
TEMPLATE.version="0.0.1"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- ToDo list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO: A lot...
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Create a new TEMPLATE class instance.
-- @param #TEMPLATE self
-- @return #TEMPLATE self
function TEMPLATE:New()
-- Inherit everything from SCENERY class.
self=BASE:Inherit(self, BASE:New()) -- #TEMPLATE
return self
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Set frequency.
-- @param #TEMPLATE self
-- @param #number Frequency Frequency in Hz.
-- @return #TEMPLATE self
function TEMPLATE:SetFrequency(Frequency)
self.frequency=Frequency
return self
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Private Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-21
View File
@@ -326,27 +326,6 @@ function AIRWING:AddSquadron(Squadron)
if Squadron:IsStopped() then
Squadron:Start()
end
-- if storage is limited, add the amount of aircraft needed
local airbasename = self:GetAirbaseName()
if airbasename then
local group = GROUP:FindByName(Squadron.templategroup)
local Nunits = 1
local units
if group then units = group:GetUnits() end
if units then Nunits = #units end
local typename = Squadron.aircrafttype or "none"
local NAssets = Squadron.Ngroups * Nunits
local storage = STORAGE:New(airbasename)
--self:T(self.lid.."Adding "..typename.." #"..NAssets)
if storage and storage.warehouse and storage:IsLimitedAircraft() and typename ~= "none" then
local NInStore = storage:GetItemAmount(typename) or 0
if NAssets > NInStore then
storage:AddItem(typename,NAssets)
end
end
end
return self
end
+1 -1
View File
@@ -3060,7 +3060,7 @@ end
--- **[LEGION, COMMANDER, CHIEF]** Set the repeat delay in seconds after a mission is successful/failed. Only valid if the mission is handled by a LEGION (AIRWING, BRIGADE, FLEET) or higher level.
-- @param #AUFTRAG self
-- @param #number RepeatDelay Repeat delay in seconds. Default 1.
-- @param #number Nrepeat Repeat delay in seconds. Default 1.
-- @return #AUFTRAG self
function AUFTRAG:SetRepeatDelay(RepeatDelay)
self.repeatDelay = RepeatDelay
+1 -58
View File
@@ -509,7 +509,7 @@ do
-- @field #AWACS
AWACS = {
ClassName = "AWACS", -- #string
version = "0.2.74", -- #string
version = "0.2.73", -- #string
lid = "", -- #string
coalition = coalition.side.BLUE, -- #number
coalitiontxt = "blue", -- #string
@@ -2060,55 +2060,6 @@ function AWACS:SetRejectionZone(Zone,Draw)
return self
end
--- Function to set corridor zones.
-- @param #AWACS self
-- @param Core.Set#SET_ZONE CorridorZones Can be handed in as SET\_ZONE or single ZONE object.
-- @return #AWACS self
function AWACS:SetCorridorZones(CorridorZones)
self:T(self.lid .. "SetCorridorZones")
if CorridorZones and CorridorZones:IsInstanceOf("SET_ZONE") then
self.corridorzones = CorridorZones
self.usecorridors = true
elseif CorridorZones and CorridorZones:IsInstanceOf("ZONE_BASE") then
if not self.corridorzones then self.corridorzones = SET_ZONE:New() end
self.corridorzones:AddZone(CorridorZones)
self.usecorridors = true
end
return self
end
--- Function to add one corridor zone.
-- @param #AWACS self
-- @param Core.Zone#ZONE CorridorZone The ZONE object to be added.
-- @return #AWACS self
function AWACS:AddCorridorZone(CorridorZone)
self:T(self.lid .. "AddCorridorZone")
self:SetCorridorZones(CorridorZone)
return self
end
--- Function to set corridor zone floor and ceiling in FEET.
-- @param #AWACS self
-- @param #number Floor Floor altitude ASL in feet.
-- @param #number Ceiling Ceiling altitude ASL in feet.
-- @return #AWACS self
function AWACS:SetCorridorZoneFloorAndCeiling(Floor,Ceiling)
self.corridorfloor = UTILS.FeetToMeters(Floor)
self.corridorceiling = UTILS.FeetToMeters(Ceiling)
return self
end
--- Function to set corridor zone floor and ceiling in METERS.
-- @param #AWACS self
-- @param #number Floor Floor altitude ASL in meters.
-- @param #number Ceiling Ceiling altitude ASL in meters.
-- @return #AWACS self
function AWACS:SetCorridorZoneFloorAndCeilingMeters(Floor,Ceiling)
self.corridorfloor = Floor
self.corridorceiling = Ceiling
return self
end
--- [User] Draw a line around the FEZ on the F10 map.
-- @param #AWACS self
-- @return #AWACS self
@@ -4385,14 +4336,6 @@ function AWACS:_StartIntel(awacs)
intel:SetFilterCategory({Unit.Category.AIRPLANE,Unit.Category.HELICOPTER})
end
-- Corridors
if self.usecorridors == true then
intel:SetCorridorZones(self.corridorzones)
if self.corridorceiling or self.corridorfloor then
intel:SetCorridorLimits(self.corridorfloor,self.corridorceiling)
end
end
-- Callbacks
local function NewCluster(Cluster)
self:__NewCluster(5,Cluster)
+32 -83
View File
@@ -31,7 +31,7 @@
-- @image OPS_CSAR.jpg
---
-- Last Update Dec 2025
-- Last Update Oct 2025
-------------------------------------------------------------------------
--- **CSAR** class, extends Core.Base#BASE, Core.Fsm#FSM
@@ -81,7 +81,7 @@
--
-- mycsar.allowDownedPilotCAcontrol = false -- Set to false if you don\'t want to allow control by Combined Arms.
-- mycsar.allowFARPRescue = true -- allows pilots to be rescued by landing at a FARP or Airbase. Else MASH only!
-- mycsar.FARPRescueDistance = 500 -- you need to be this close to a FARP or Airport for the pilot to be rescued.
-- mycsar.FARPRescueDistance = 1000 -- you need to be this close to a FARP or Airport for the pilot to be rescued.
-- mycsar.autosmoke = false -- automatically smoke a downed pilot\'s location when a heli is near.
-- mycsar.autosmokedistance = 1000 -- distance for autosmoke
-- mycsar.coordtype = 1 -- Use Lat/Long DDM (0), Lat/Long DMS (1), MGRS (2), Bullseye imperial (3) or Bullseye metric (4) for coordinates.
@@ -119,7 +119,6 @@
-- mycsar.PilotWeight = 80 -- Loaded pilots weigh 80kgs each
-- mycsar.AllowIRStrobe = false -- Allow a menu item to request an IR strobe to find a downed pilot at night (requires NVGs to see it).
-- mycsar.IRStrobeRuntime = 300 -- If an IR Strobe is activated, it runs for 300 seconds (5 mins).
-- mycsar.EnableMenuSmokeMASH = true -- Allow a menu item to request smoke at the closest MASH/AFB for rescue.
--
-- ## 2.1 Create own SET_GROUP to manage CTLD Pilot groups
--
@@ -273,8 +272,6 @@ CSAR = {
UserSetGroup = nil,
AllowIRStrobe = false,
IRStrobeRuntime = 300,
FARPRescueDistance = 500,
EnableMenuSmokeMASH = true,
}
--- Downed pilots info.
@@ -318,7 +315,7 @@ CSAR.AircraftType["CH-47Fbl1"] = 31
--- CSAR class version.
-- @field #string version
CSAR.version="1.0.35"
CSAR.version="1.0.34"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- ToDo list
@@ -434,13 +431,12 @@ function CSAR:New(Coalition, Template, Alias)
self.radioSound = "beacon.ogg" -- the name of the sound file to use for the Pilot radio beacons. If this isnt added to the mission BEACONS WONT WORK!
self.beaconRefresher = 29 -- seconds
self.allowFARPRescue = true --allows pilot to be rescued by landing at a FARP or Airbase
self.FARPRescueDistance = 500 -- you need to be this close to a FARP or Airport for the pilot to be rescued.
self.FARPRescueDistance = 1000 -- you need to be this close to a FARP or Airport for the pilot to be rescued.
self.max_units = 6 --max number of pilots that can be carried
self.useprefix = true -- Use the Prefixed defined below, Requires Unit have the Prefix defined below
self.csarPrefix = { "helicargo", "MEDEVAC"} -- prefixes used for useprefix=true - DON\'T use # in names!
self.template = Template or "generic" -- template for downed pilot
self.mashprefix = {"MASH"} -- prefixes used to find MASHes
self.EnableMenuSmokeMASH = true
self.autosmoke = false -- automatically smoke location when heli is near
self.autosmokedistance = 2000 -- distance for autosmoke
@@ -677,7 +673,7 @@ function CSAR:_CreateDownedPilotTrack(Group,Groupname,Side,OriginalUnit,Descript
DownedPilot.alive = true
DownedPilot.wetfeet = Wetfeet or false
DownedPilot.BeaconName = BeaconName
-- Add Pilot
local PilotTable = self.downedPilots
local counter = self.downedpilotcounter
@@ -855,10 +851,6 @@ function CSAR:_AddCsar(_coalition , _country, _point, _typeName, _unitName, _pla
else
BeaconName = "Ghost-1-1"..math.random(1,10000)
end
if _playerName == nil or _playerName == "" then
_playerName = "AI MIA"
end
if (_freq and _freq ~= 0) then --shagrat only add beacon if _freq is NOT 0
self:_AddBeaconToGroup(_spawnedGroup, _freq, BeaconName)
@@ -1240,7 +1232,7 @@ function CSAR:_EventHandler(EventData)
local _place = _event.Place -- Wrapper.Airbase#AIRBASE
if _place == nil then
self:T(self.lid .. " Landing Place nil")
self:T(self.lid .. " Landing Place Nil")
return self -- error!
end
@@ -1275,7 +1267,7 @@ function CSAR:_EventHandler(EventData)
self:T("Country = ".._country.." Coalition = ".._coalition)
if _coalition == self.coalition then
local _freq = self:_GenerateADFFrequency()
self:T({coalition=_coalition,country= _country, coord=_LandingPos, name=_unitname, player=_event.IniPlayerName, freq=_freq})
self:I({coalition=_coalition,country= _country, coord=_LandingPos, name=_unitname, player=_event.IniPlayerName, freq=_freq})
self:_AddCsar(_coalition, _country, _LandingPos, nil, _unitname, _event.IniPlayerName, _freq, self.suppressmessages, "none")--shagrat add CSAR at Parachute location.
Unit.destroy(_event.initiator) -- shagrat remove static Pilot model
@@ -1769,14 +1761,7 @@ function CSAR:_ScheduledSARFlight(heliname,groupname, isairport, noreschedule, I
end
self:T(self.lid.."[Drop off debug] Check distance to MASH for "..heliname.." Distance km: "..math.floor(_dist/1000))
if self.verbose>0 then
local debugtext = string.format("Distance %dm | Rescuedist %dm | IsAirport %s | IsInAir %s | IsHeloBase %s\n",_dist,self.FARPRescueDistance,tostring(isairport),tostring(_heliUnit:InAir()),tostring(IsHeloBase))
self:T("*******************************")
self:T(debugtext)
self:T("*******************************")
end
if ( _dist < self.FARPRescueDistance or isairport ) and ((_heliUnit:InAir() == false) or (IsHeloBase == true)) then
self:T(self.lid.."[Drop off debug] Distance ok, door check")
if self.pilotmustopendoors and self:_IsLoadingDoorOpen(heliname) == false then
@@ -2153,52 +2138,15 @@ function CSAR:_Reqsmoke( _unitName )
return self
end
---(Internal) Request smoke at closest MASH/AFB.
--@param #CSAR self
--@param #string _unitName Name of the helicopter
function CSAR:_ReqsmokeMash( _unitName )
self:T(self.lid .. " _ReqsmokeMash")
local _heli = self:_GetSARHeli(_unitName)
if _heli == nil then
return
end
local smokedist = 8000
if smokedist < self.approachdist_far then smokedist = self.approachdist_far end
local distance, name, coordinate = self:_GetClosestMASH(_heli)
if coordinate and distance then
local disttext
if _SETTINGS:IsImperial() then
disttext = string.format("%.1fnm",UTILS.MetersToNM(distance))
else
disttext = string.format("%.1fkm",distance/1000)
end
local _msg = string.format("%s - Popping smoke at the closest rescue point: %s", self:_GetCustomCallSign(_unitName), disttext)
self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true, true)
local color = self.smokecolor
coordinate:Smoke(color)
else
local _distance = string.format("%.1fkm",smokedist/1000)
if _SETTINGS:IsImperial() then
_distance = string.format("%.1fnm",UTILS.MetersToNM(smokedist))
else
_distance = string.format("%.1fkm",smokedist/1000)
end
self:_DisplayMessageToSAR(_heli, string.format("No rescue point within %s",_distance), self.messageTime, false, false, true)
end
return self
end
--- (Internal) Determine distance to closest MASH.
-- @param #CSAR self
-- @param Wrapper.Unit#UNIT _heli Helicopter #UNIT
-- @return #number Distance in meters
-- @return #string MASH Name as string
-- @return Core.Point#COORDINATE Coordinate The MASH/AFB Coordinate (for smoke)
function CSAR:_GetClosestMASH(_heli)
self:T(self.lid .. " _GetClosestMASH")
local _mashset = self.mash -- Core.Set#SET_GROUP
local MashSets = {}
local MashSets = {}
--local _mashes = _mashset.Set-- #table
table.insert(MashSets,_mashset.Set)
table.insert(MashSets,self.zonemashes.Set)
@@ -2207,21 +2155,12 @@ function CSAR:_GetClosestMASH(_heli)
local _distance = 0
local _helicoord = _heli:GetCoordinate()
local MashName = nil
local Coordinate = nil -- Core.Point#COORDINATE
if self.allowFARPRescue then
local afb,distance = _helicoord:GetClosestAirbase(nil,self.coalition)
local position = _heli:GetCoordinate()
local afb,distance = position:GetClosestAirbase(nil,self.coalition)
_shortestDistance = distance
MashName = (afb ~= nil) and afb:GetName() or "Unknown"
Coordinate = (afb ~= nil) and afb:GetCoordinate()
if afb then
local afbzone = afb:GetMinimumBoundingCircleFromParkingSpots()
if afbzone then
if afbzone:IsCoordinateInZone(_helicoord) and distance > self.FARPRescueDistance*1.1 then
_shortestDistance = 100
end
end
end
end
for _,_mashes in pairs(MashSets) do
@@ -2236,13 +2175,12 @@ function CSAR:_GetClosestMASH(_heli)
if _distance ~= nil and (_shortestDistance == -1 or _distance < _shortestDistance) then
_shortestDistance = _distance
MashName = _mashUnit:GetName() or "Unknown"
Coordinate = _mashcoord
end
end
end
if _shortestDistance ~= -1 then
return _shortestDistance, MashName, Coordinate
return _shortestDistance, MashName
else
return -1
end
@@ -2311,9 +2249,6 @@ function CSAR:_AddMedevacMenuItem()
local _rootMenu4 = MENU_GROUP_COMMAND:New(_group,"Request Smoke",_rootPath, self._Reqsmoke,self,_unitName)
if self.AllowIRStrobe then
local _rootMenu5 = MENU_GROUP_COMMAND:New(_group,"Request IR Strobe",_rootPath, self._ReqIRStrobe,self,_unitName):Refresh()
end
if self.EnableMenuSmokeMASH then
local _rootMenu6 = MENU_GROUP_COMMAND:New(_group,"Smoke Closest MASH",_rootPath, self._ReqsmokeMash,self,_unitName)
else
_rootMenu4:Refresh()
end
@@ -2334,7 +2269,6 @@ function CSAR:_GetDistance(_point1, _point2)
if _point1 and _point2 then
local distance1 = _point1:Get2DDistance(_point2)
local distance2 = _point1:DistanceFromPointVec2(_point2)
MESSAGE:New(string.format("_GetDistance: d1 = %dm | d2 = %dm",distance1,distance2)):ToAllIf(self.verbose>1):ToLogIf(self.verbose>1)
if distance1 and type(distance1) == "number" then
return distance1
elseif distance2 and type(distance2) == "number" then
@@ -2434,7 +2368,7 @@ function CSAR:_AddBeaconToGroup(_group, _freq, BeaconName)
--local name = _radioUnit:GetName()
local Sound = "l10n/DEFAULT/"..self.radioSound
local vec3 = _radioUnit:GetVec3() or _radioUnit:GetPositionVec3() or {x=0,y=0,z=0}
self:T(self.lid..string.format("Added Radio Beacon %d Hertz | Name %s | Position {%d,%d,%d}",Frequency,BeaconName,vec3.x,vec3.y,vec3.z))
self:I(self.lid..string.format("Added Radio Beacon %d Hertz | Name %s | Position {%d,%d,%d}",Frequency,BeaconName,vec3.x,vec3.y,vec3.z))
trigger.action.radioTransmission(Sound, vec3, 0, true, Frequency, self.ADFRadioPwr or 500,BeaconName) -- Beacon in MP only runs for exactly 30secs straight
end
end
@@ -2562,6 +2496,22 @@ function CSAR:onafterStart(From, Event, To)
self.staticmashes = SET_STATIC:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(self.mashprefix):FilterStart()
self.zonemashes = SET_ZONE:New():FilterPrefixes(self.mashprefix):FilterStart()
--[[
if staticmashes:Count() > 0 then
for _,_mash in pairs(staticmashes.Set) do
self.mash:AddObject(_mash)
end
end
if zonemashes:Count() > 0 then
self:T("Adding zones to self.mash SET")
for _,_mash in pairs(zonemashes.Set) do
self.mash:AddObject(_mash)
end
self:T("Objects in SET: "..self.mash:Count())
end
--]]
if not self.coordinate then
local csarhq = self.mash:GetRandom()
if csarhq then
@@ -2904,7 +2854,6 @@ function CSAR:onafterSave(From, Event, To, path, filename)
if DownedPilot and DownedPilot.alive then
-- get downed pilot data for saving
local playerName = DownedPilot.player
if playerName == nil or playerName == "" then playerName = "AI MIA" end
local group = DownedPilot.group
local coalition = group:GetCoalition()
local country = group:GetCountry()
@@ -2915,7 +2864,7 @@ function CSAR:onafterSave(From, Event, To, path, filename)
local unitName = DownedPilot.originalUnit
local txt = string.format("%s,%d,%d,%d,%s,%s,%s,%s,%s,%d\n",playerName,location.x,location.y,location.z,coalition,country,description,typeName,unitName,freq)
self:T(self.lid.."Saving to CSAR File: " .. txt)
self:I(self.lid.."Saving to CSAR File: " .. txt)
data = data .. txt
end
@@ -3030,8 +2979,8 @@ function CSAR:onafterLoad(From, Event, To, path, filename)
-- Info message.
local text=string.format("Loading CSAR state from file %s", filename)
MESSAGE:New(text,10):ToAllIf(self.verbose>0)
self:T(self.lid..text)
MESSAGE:New(text,10):ToAllIf(self.Debug)
self:I(self.lid..text)
local file=assert(io.open(filename, "rb"))
File diff suppressed because it is too large Load Diff
-948
View File
@@ -1,948 +0,0 @@
--- **Ops** - Create your A2G Defenses.
--
-- **Main Features:**
--
-- * Automatically create and manage A2G defenses using an AirWing and Squadrons for one coalition
-- * Easy set-up
--
-- ===
--
-------------------------------------------------------------------------
-- Easy A2G Engagement Class, based on OPS classes
-------------------------------------------------------------------------
--
-- ## Documentation:
--
-- https://flightcontrol-master.github.io/MOOSE_DOCS_DEVELOP/Documentation/Ops.EasyAG.html
--
-- ## Example Missions:
--
-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Ops/EasyAG).
--
-------------------------------------------------------------------------
-- Date: Dec 2025
-- Last Update: Dec 2025
-------------------------------------------------------------------------
--
-- ===
--
-- **Main Features:**
--
-- * Automatically create and manage A2G defenses using an AirWing and Squadrons for one coalition
-- * Easy set-up
-- * Add additional AirWings on other airbases
-- * Each wing can have more than one Squadron - tasking to Squadrons is done on a random basis per AirWing
-- * Create borders and zones of engagement
-- * Detection can be ground based and/or via AWACS
--
-- ===
--
-- ### AUTHOR: **applevangelist**
--
-- @module Ops.EasyAG
-- @image AI_Air_To_Ground_Dispatching.JPG
--- EASYA2G Class
-- @type EASYA2G
-- @field #string ClassName
-- @field #number overhead
-- @field #number engagerange
-- @field #number capgrouping
-- @field #string airbasename
-- @field Wrapper.Airbase#AIRBASE airbase
-- @field #number coalition
-- @field #string alias
-- @field #table wings
-- @field Ops.Intel#INTEL Intel
-- @field #number resurrection
-- @field #number capspeed
-- @field #number capalt
-- @field #number capdir
-- @field #number capleg
-- @field #number maxinterceptsize
-- @field #number missionrange
-- @field #number noalert5
-- @field #table ManagedAW
-- @field #table ManagedSQ
-- @field #table ManagedCP
-- @field #table ManagedTK
-- @field #table ManagedEWR
-- @field #table ManagedREC
-- @field #number MaxAliveMissions
-- @field #boolean debug
-- @field #number repeatsonfailure
-- @field Core.Set#SET_ZONE GoZoneSet
-- @field Core.Set#SET_ZONE NoGoZoneSet
-- @field Core.Set#SET_ZONE ConflictZoneSet
-- @field #boolean Monitor
-- @field #boolean TankerInvisible
-- @field #number CapFormation
-- @field #table ReadyFlightGroups
-- @field #boolean DespawnAfterLanding
-- @field #boolean DespawnAfterHolding
-- @field #list<Ops.Auftrag#AUFTRAG> ListOfAuftrag
-- @field #string defaulttakeofftype Take off type
-- @field #number FuelLowThreshold
-- @field #number FuelCriticalThreshold
-- @field #boolean showpatrolpointmarks
-- @field #table EngageTargetTypes
-- @extends Ops.EasyGCICAP#EASYGCICAP
--- *“High-Threat Close-Air-Support is a Myth.”* -- Mike “Starbaby” Pietrucha.
--
-- ===
--
-- # The EasyAG Concept
--
-- The idea of this class is partially to make the OPS classes easier operational for an A2G defense network, and to replace the legacy AI_A2G_Dispatcher system - not to it's
-- full extent, but make a basic system work very quickly.
--
-- # Setup
--
-- ## Basic understanding
--
-- The basics are, there is **one** and only **one** AirWing per airbase. Each AirWing has **at least** one Squadron, who will do A2G tasks. Squadrons will be randomly chosen for the task at hand.
-- Each AirWing has **at least** one Conflict Zone that it manages. COnflict Zones will be covered by the AirWing automatically as long as airframes are available. Detected enemy ground forces will be assigned to **one**
-- AirWing based on proximity (that is, if you have more than one).
--
-- ## Assignment of tasks for enemies
--
-- An exisiting plane or a newly spawned plane will take care of the intruders. Standard overhead is 0.1, i.e. a group of 10 intrudes will
-- be managed by one planes from the assigned AirWing. There is an maximum missions limitation per AirWing, so we do not spam the skies.
--
-- ## Basic set-up code
--
-- ### Prerequisites
--
-- You have to put a **STATIC WAREHOUSE** object on the airbase with the UNIT name according to the name of the airbase. **Do not put any other static type or it creates a conflict with the airbase name!**
-- E.g. for Kutaisi this has to have the unit name Kutaisi. This object symbolizes the AirWing HQ.
-- Next put a late activated template group for your A2G Squadron on the map. Last, put a zone on the map for the Defense operations, let's name it "Blue Zone 1". Size of the zone plays no role.
-- Put a scout system on the map and name it aptly, like "Blue SCOUT".
--
-- ### Zones
--
-- For our example, you create a RED and a BLUE border, as a closed polygonal zone representing the borderlines. You can also have conflict zone, where - for our example - BLUE will attack
-- RED groups, despite being on or close to RED territory. Think of a no-fly zone or an limited area of engagement. Conflict zones take precedence over borders, i.e. they can overlap all borders.
--
-- ### Code it
--
-- -- Set up a basic system for the blue side, we'll reside on Kutaisi, and use GROUP objects with "Blue SCOUT" in the name as Detecting Systems.
-- local mywing = EASYA2G:New("A2G",AIRBASE.Caucasus.Kutaisi,"blue","SCOUT")
--
-- -- Add a holding/ingress point belonging to our airbase, we'll be at 5k ft doing 250 kn, initial direction 225 degrees (West), leg 5NM
-- -- This will effectively be the ingress coordinate into the cnflict zone
-- local Coordinate = ZONE:New("A2G Loitering"):GetCoordinate()
-- mywing:AddHoldingPointA2G(AIRBASE.Caucasus.Kutaisi,Coordinate,5000,250,225,5)
--
-- -- Add a recon point over the conflict zone, we'll use a reaper for recon
-- local Coordinate2 = ZONE:New("A2G Recon"):GetCoordinate()
-- mywing:AddPatrolPointRecon(AIRBASE.Caucasus.Kutaisi,Coordinate2,15000,225,225,5)
--
-- -- Add three Squadrons with templates "Hero 1" and "Hero 2", 20 airframes, skill as set
-- mywing:AddSquadron("A2G Flight", "Hero 1", AIRBASE.Caucasus.Kutaisi, 5, AI.Skill.GOOD, Modex, Livery)
-- mywing:AddSquadron("A2G Helo", "Hero 2", AIRBASE.Caucasus.Kutaisi, 5, AI.Skill.HIGH, Modex, Livery)
-- mywing:AddReconSquadron("Recon Drone", "SpyInTheSky SCOUT", AIRBASE.Caucasus.Kutaisi, 5, AI.Skill.EXCELLENT, Modex, Livery)
--
-- -- Ensure our reaper doesn't get immediately killed
-- mywing:SetTankerAndScoutsInvisible(true)
--
-- -- Add a couple of zones
-- -- We'll defend our own border
-- mywing:AddAcceptZone(ZONE_POLYGON:New( "Blue Border", GROUP:FindByName( "Blue Border" ) ))
-- -- We'll attack intruders also here - conflictzones can overlap borders(!) - limited zone of engagement
-- mywing:AddConflictZone(ZONE_POLYGON:New("Red Defense Zone", GROUP:FindByName( "Red Defense Zone" )))
-- -- We'll leave the reds alone on their turf
-- mywing:AddRejectZone(ZONE_POLYGON:New( "Red Border", GROUP:FindByName( "Red Border" ) ))
--
-- -- Optional - Draw the borders on the map so we see what's going on
-- -- Set up borders on map
-- local BlueBorder = ZONE_POLYGON:New( "Blue Border", GROUP:FindByName( "Blue Border" ) )
-- BlueBorder:DrawZone(-1,{0,0,1},1,FillColor,FillAlpha,1,true)
-- local ConflictZone = ZONE_POLYGON:New("Red Defense Zone", GROUP:FindByName( "Red Defense Zone" ))
-- ConflictZone:DrawZone(-1,{1,1,0},1,FillColor,FillAlpha,2,true)
-- local BlueNoGoZone = ZONE_POLYGON:New( "Red Border", GROUP:FindByName( "Red Border" ) )
-- BlueNoGoZone:DrawZone(-1,{1,0,0},1,FillColor,FillAlpha,4,true)
--
-- ### Add a second airwing with squads and own patrol point (optional)
--
-- -- Set this up at Sukhumi
-- mywing:AddAirwing(AIRBASE.Caucasus.Sukhumi_Babushara,"Blue A2G Sukhumi")
-- -- A2G Point "Blue Zone 2"
-- mywing:AddPatrolPointA2G(AIRBASE.Caucasus.Sukhumi_Babushara,ZONE:FindByName("Blue Zone 2"):GetCoordinate(),30000,400,90,20)
--
-- -- This one has two squadrons to choose from
-- mywing:AddSquadron("Blue Sq3 F16","A2G Sukhumi II",AIRBASE.Caucasus.Sukhumi_Babushara,20,AI.Skill.GOOD,402,"JASDF 6th TFS 43-8526 Skull Riders")
-- mywing:AddSquadron("Blue Sq2 F15","A2G Sukhumi I",AIRBASE.Caucasus.Sukhumi_Babushara,20,AI.Skill.GOOD,202,"390th Fighter SQN")
--
-- ### Add a tanker (optional)
--
-- -- **Note** If you need different tanker types, i.e. Boom and Drogue, set them up at different AirWings!
-- -- Add a tanker point
-- mywing:AddPatrolPointTanker(AIRBASE.Caucasus.Kutaisi,ZONE:FindByName("Blue Zone Tanker"):GetCoordinate(),20000,280,270,50)
-- -- Add a tanker squad - Radio 251 AM, TACAN 51Y
-- mywing:AddTankerSquadron("Blue Tanker","Tanker Ops Kutaisi",AIRBASE.Caucasus.Kutaisi,20,AI.Skill.EXCELLENT,602,nil,251,radio.modulation.AM,51)
--
-- ### Add an AWACS (optional)
--
-- -- Add an AWACS point
-- mywing:AddPatrolPointAwacs(AIRBASE.Caucasus.Kutaisi,ZONE:FindByName("Blue Zone AWACS"):GetCoordinate(),25000,300,270,50)
-- -- Add an AWACS squad - Radio 251 AM, TACAN 51Y
-- mywing:AddAWACSSquadron("Blue AWACS","AWACS Ops Kutaisi",AIRBASE.Caucasus.Kutaisi,20,AI.Skill.AVERAGE,702,nil,271,radio.modulation.AM)
--
-- # Fine-Tuning
--
-- ## Change Defaults
--
-- * @{#EASYA2G.SetDefaultResurrection}: Set how many seconds the AirWing stays inoperable after the AirWing STATIC HQ ist destroyed, default 900 secs.
-- * @{#EASYA2G.SetDefaultA2GSpeed}: Set how many knots the A2G flights should do (will be altitude corrected), default 225 kn.
-- * @{#EASYA2G.SetDefaultA2GAlt}: Set at which altitude (ASL) the A2G planes will fly, default 10,000 ft.
-- * @{#EASYA2G.SetDefaultA2GDirection}: Set the initial direction from the A2G point the planes will fly in degrees, default is 90°.
-- * @{#EASYA2G.SetDefaultA2GLeg}: Set the length of the A2G leg, default is 5 NM.
-- * @{#EASYA2G.SetDefaultA2GGrouping}: Set how many planes will be spawned per mission (CVAP/GCI), defaults to 1.
-- * @{#EASYA2G.SetDefaultMissionRange}: Set how many NM the planes can go from the home base, defaults to 100.
-- * @{#EASYA2G.SetDefaultNumberAlert5Standby}: Set how many planes will be spawned on cold standby (Alert5), default 2.
-- * @{#EASYA2G.SetDefaultEngageRange}: Set max engage range for A2G flights if they detect intruders, defaults to 50.
-- * @{#EASYA2G.SetMaxAliveMissions}: Set max parallel missions can be done (A2G+GCI+Alert5+Tanker+AWACS), defaults to 8.
-- * @{#EASYA2G.SetDefaultRepeatOnFailure}: Set max repeats on failure for intercepting/killing intruders, defaults to 3.
-- * @{#EASYA2G.SetTankerAndScoutsInvisible}: Set Tanker and Scouts to be invisible to enemy AI eyes. Is set to `true` by default.
--
-- ## Debug and Monitor
--
-- mywing.debug = true -- log information
-- mywing.Monitor = true -- show some statistics on screen
--
--
-- @field #EASYA2G
EASYA2G = {
ClassName = "EASYA2G",
overhead = 0.2,
capgrouping = 1,
airbasename = nil,
airbase = nil,
coalition = "blue",
alias = nil,
wings = {},
Intel = nil,
resurrection = 900,
capspeed = 300,
capalt = 25000,
capdir = 45,
capleg = 5,
maxinterceptsize = 2,
missionrange = 100,
noalert5 = 2,
ManagedAW = {},
ManagedSQ = {},
ManagedCP = {},
ManagedTK = {},
ManagedEWR = {},
ManagedREC = {},
MaxAliveMissions = 8,
debug = false,
engagerange = 50,
repeatsonfailure = 3,
GoZoneSet = nil,
NoGoZoneSet = nil,
ConflictZoneSet = nil,
Monitor = false,
TankerInvisible = true,
CapFormation = nil,
ReadyFlightGroups = {},
DespawnAfterLanding = false,
DespawnAfterHolding = true,
ListOfAuftrag = {},
defaulttakeofftype = "hot",
FuelLowThreshold = 25,
FuelCriticalThreshold = 10,
showpatrolpointmarks = false,
EngageTargetTypes = {"Ground"},
}
--- Internal Squadron data type
-- @type EASYA2G.Squad
-- @field #string TemplateName
-- @field #string SquadName
-- @field #string AirbaseName
-- @field #number AirFrames
-- @field #string Skill
-- @field #string Modex
-- @field #string Livery
-- @field #boolean Tanker
-- @field #boolean AWACS
-- @field #boolean RECON
-- @field #number Frequency
-- @field #number Modulation
-- @field #number TACAN
--- Internal Wing data type
-- @type EASYA2G.Wing
-- @field #string AirbaseName
-- @field #string Alias
-- @field #string CapZoneName
--- Internal CapPoint data type
-- @type EASYA2G.CapPoint
-- @field #string AirbaseName
-- @field Core.Point#COORDINATE Coordinate
-- @field #number Altitude
-- @field #number Speed
-- @field #number Heading
-- @field #number LegLength
-- @field Core.Zone#ZONE_BASE Zone
--- EASYA2G class version.
-- @field #string version
EASYA2G.version="0.1.3"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO: TBD
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Create a new GCIA2G Manager
-- @param #EASYA2G self
-- @param #string Alias A Name for this A2G Setup
-- @param #string AirbaseName Name of the Home Airbase
-- @param #string Coalition Coalition, e.g. "blue" or "red"
-- @param #string ScoutName (Partial) group name of the detection system of the coalition, e.g. "Red SCOUT", can be handed in as table of names, e.g.{"SCOUT","DRONE","SAM"}
-- @return #EASYA2G self
function EASYA2G:New(Alias, AirbaseName, Coalition, ScoutName)
-- Inherit everything from FSM class.
local self=BASE:Inherit(self, EASYGCICAP:New(Alias, AirbaseName, Coalition, ScoutName)) -- #EASYA2G
-- defaults
self.alias = Alias or AirbaseName.." A2G Wing"
self.coalitionname = string.lower(Coalition) or "blue"
self.coalition = self.coalitionname == "blue" and coalition.side.BLUE or coalition.side.RED
self.wings = {}
if type(ScoutName) == "string" then ScoutName = {ScoutName} end
self.EWRName = ScoutName --or self.coalitionname.." EWR"
--self.CapZoneName = CapZoneName
self.airbasename = AirbaseName
self.airbase = AIRBASE:FindByName(self.airbasename)
self.GoZoneSet = SET_ZONE:New()
self.NoGoZoneSet = SET_ZONE:New()
self.ConflictZoneSet = SET_ZONE:New()
self.resurrection = 900
self.capspeed = 225
self.capalt = 5000
self.capdir = 90
self.capleg = 5
self.capgrouping = 2
self.missionrange = 100
self.noalert5 = 2
self.MaxAliveMissions = 8
self.engagerange = 50
self.repeatsonfailure = 3
self.Monitor = false
self.TankerInvisible = true
self.CapFormation = ENUMS.Formation.FixedWing.FingerFour.Group
self.DespawnAfterLanding = false
self.DespawnAfterHolding = true
self.ListOfAuftrag = {}
self.defaulttakeofftype = "hot"
self.FuelLowThreshold = 25
self.FuelCriticalThreshold = 10
self.showpatrolpointmarks = false
self.EngageTargetTypes = {"Ground"}
-- Set some string id for output to DCS.log file.
self.lid=string.format("EASYA2G %s | ", self.alias)
-- Add FSM transitions.
-- From State --> Event --> To State
self:SetStartState("Stopped")
self:AddTransition("Stopped", "Start", "Running")
self:AddTransition("Running", "Stop", "Stopped")
self:AddTransition("*", "Status", "*")
--- On Before "Start" event.
-- @function [parent=#EASYA2G] OnBeforeStart
-- @param #EASYA2G self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
--- On After "Start" event.
-- @function [parent=#EASYA2G] OnAfterStart
-- @param #EASYA2G self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
--- On Before "Status" event.
-- @function [parent=#EASYA2G] OnBeforeStatus
-- @param #EASYA2G self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
--- On After "Status" event.
-- @function [parent=#EASYA2G] OnAfterStatus
-- @param #EASYA2G self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
self:AddAirwing(self.airbasename,self.alias,self.CapZoneName)
self:I(self.lid.."Created new instance (v"..self.version..")")
self:__Start(math.random(6,12))
return self
end
-------------------------------------------------------------------------
-- Functions
-------------------------------------------------------------------------
--- Set Tanker and Scouts to be invisible to enemy AI eyes
-- @param #EASYA2G self
-- @param #boolean Switch Set to true or false, by default this is set to true already
-- @return #EASYA2G self
function EASYA2G:SetTankerAndScoutsInvisible(Switch)
self:T(self.lid.."SetTankerAndScoutsInvisible")
self.TankerInvisible = Switch
return self
end
--- Set default A2G Speed in knots
-- @param #EASYA2G self
-- @param #number Speed Speed defaults to 300
-- @return #EASYA2G self
function EASYA2G:SetDefaultA2GSpeed(Speed)
self:T(self.lid.."SetDefaultSpeed")
self.capspeed = Speed or 300
return self
end
--- Set A2G Flight formation.
-- @param #EASYA2G self
-- @param #number Formation Formation to fly, defaults to ENUMS.Formation.FixedWing.FingerFour.Group
-- @return #EASYA2G self
function EASYA2G:SetA2GFormation(Formation)
self:T(self.lid.."SetA2GFormation")
self.CapFormation = Formation
return self
end
--- Set default A2G Altitude in feet
-- @param #EASYA2G self
-- @param #number Altitude Altitude defaults to 25000
-- @return #EASYA2G self
function EASYA2G:SetDefaultA2GAlt(Altitude)
self:T(self.lid.."SetDefaultAltitude")
self.capalt = Altitude or 25000
return self
end
--- Set default A2G lieg initial direction in degrees
-- @param #EASYA2G self
-- @param #number Direction Direction defaults to 90 (East)
-- @return #EASYA2G self
function EASYA2G:SetDefaultA2GDirection(Direction)
self:T(self.lid.."SetDefaultDirection")
self.capdir = Direction or 90
return self
end
--- Set default leg length in NM
-- @param #EASYA2G self
-- @param #number Leg Leg defaults to 5
-- @return #EASYA2G self
function EASYA2G:SetDefaultA2GLeg(Leg)
self:T(self.lid.."SetDefaultLeg")
self.capleg = Leg or 5
return self
end
--- Set default grouping, i.e. how many airplanes per A2G point
-- @param #EASYA2G self
-- @param #number Grouping Grouping defaults to 2
-- @return #EASYA2G self
function EASYA2G:SetDefaultA2GGrouping(Grouping)
self:T(self.lid.."SetDefaultA2GGrouping")
self.capgrouping = Grouping or 2
return self
end
--- Set A2G mission start to vary randomly between Start end End seconds.
-- @param #EASYA2G self
-- @param #number Start
-- @param #number End
-- @return #EASYA2G self
function EASYA2G:SetA2GStartTimeVariation(Start, End)
self.capOptionVaryStartTime = Start or 5
self.capOptionVaryEndTime = End or 60
return self
end
--- Set which target types A2G flights will prefer to engage, defaults to {"Ground"}
-- @param #EASYA2G self
-- @param #table types Table of comma separated #string entries, defaults to {"Ground"} (everything that is ground and is not a weapon). Useful other options are e.g. {"Armored vehicles"}, {"Tanks"},
-- or {"APC"} or combinations like {"APC", "Tanks", "Artillery"}. See [Hoggit Wiki](https://wiki.hoggitworld.com/view/DCS_enum_attributes).
-- @return #EASYA2G self
function EASYA2G:SetA2GEngageTargetTypes(types)
self.EngageTargetTypes = types or {"Ground"}
return self
end
--- Add a A2G patrol/holding point to a Wing
-- @param #EASYA2G self
-- @param #string AirbaseName Name of the Wing's airbase
-- @param Core.Point#COORDINATE Coordinate. Can be handed as a Core.Zone#ZONE object (e.g. in case you want the point to align with a moving zone).
-- @param #number Altitude Defaults to 25000 feet ASL.
-- @param #number Speed Defaults to 300 knots TAS.
-- @param #number Heading Defaults to 90 degrees (East).
-- @param #number LegLength Defaults to 15 NM.
-- @return #EASYA2G self
function EASYA2G:AddHoldingPointA2G(AirbaseName,Coordinate,Altitude,Speed,Heading,LegLength)
self:T(self.lid.."AddHoldingPointA2G")--..Coordinate:ToStringLLDDM())
local coordinate = Coordinate
local EntryCAP = {} -- #EASYGCICAP.CapPoint
if Coordinate:IsInstanceOf("ZONE_BASE") then
-- adjust coordinate and get the coordinate from the zone
coordinate = Coordinate:GetCoordinate()
EntryCAP.Zone = Coordinate
end
EntryCAP.AirbaseName = AirbaseName
EntryCAP.Coordinate = coordinate
EntryCAP.Altitude = Altitude or 25000
EntryCAP.Speed = Speed or 300
EntryCAP.Heading = Heading or 90
EntryCAP.LegLength = LegLength or 5
self.ManagedCP[#self.ManagedCP+1] = EntryCAP
if self.debug then
local mark = MARKER:New(coordinate,self.lid.."Holding Point"):ToAll()
end
return self
end
--- (Internal) Add a Squadron to an Airwing of the manager
-- @param #EASYA2G self
-- @param #string TemplateName Name of the group template.
-- @param #string SquadName Squadron name - must be unique!
-- @param #string AirbaseName Name of the airbase the airwing resides on, e.g. AIRBASE.Caucasus.Kutaisi
-- @param #number AirFrames Number of available airframes, e.g. 20.
-- @param #string Skill(optional) Skill level, e.g. AI.Skill.AVERAGE
-- @param #string Modex (optional) Modex to be used,e.g. 402.
-- @param #string Livery (optional) Livery name to be used.
-- @param #number Frequency (optional) Radio Frequency to be used.
-- @param #number Modulation (optional) Radio Modulation to be used, e.g. radio.modulation.AM or radio.modulation.FM
-- @return #EASYA2G self
function EASYA2G:_AddSquadron(TemplateName, SquadName, AirbaseName, AirFrames, Skill, Modex, Livery, Frequency, Modulation)
self:T(self.lid.."_AddSquadron "..SquadName)
-- Add Squadrons
local Squadron_One = SQUADRON:New(TemplateName,AirFrames,SquadName)
Squadron_One:AddMissionCapability({AUFTRAG.Type.CAS, AUFTRAG.Type.CASENHANCED, AUFTRAG.Type.BAI, AUFTRAG.Type.ALERT5, AUFTRAG.Type.BOMBING, AUFTRAG.Type.STRIKE})
--Squadron_One:SetFuelLowRefuel(true)
Squadron_One:SetFuelLowThreshold(0.3)
Squadron_One:SetTurnoverTime(10,20)
Squadron_One:SetModex(Modex)
Squadron_One:SetLivery(Livery)
Squadron_One:SetSkill(Skill or AI.Skill.AVERAGE)
Squadron_One:SetMissionRange(self.missionrange)
local wing = self.wings[AirbaseName][1] -- Ops.Airwing#AIRWING
wing:AddSquadron(Squadron_One)
wing:NewPayload(TemplateName,-1,{AUFTRAG.Type.CAS, AUFTRAG.Type.CASENHANCED, AUFTRAG.Type.BAI, AUFTRAG.Type.ALERT5, AUFTRAG.Type.BOMBING, AUFTRAG.Type.STRIKE},75)
return self
end
--- (Internal) Try to assign the intercept to a FlightGroup already in air and ready.
-- @param #EASYA2G self
-- @param #table ReadyFlightGroups ReadyFlightGroups
-- @param Ops.Auftrag#AUFTRAG Auftrag The Auftrag
-- @param Wrapper.Group#GROUP Group The Target
-- @param #number WingSize Calculated number of Flights
-- @return #boolean assigned
-- @return #number leftover
function EASYA2G:_TryAssignMission(ReadyFlightGroups,Auftrag,Group,WingSize)
self:T("_TryAssignMission for size "..WingSize or 1)
local assigned = false
local wingsize = WingSize or 1
local mindist = 0
local disttable = {}
if Group and Group:IsAlive() then
local gcoord = Group:GetCoordinate() or COORDINATE:New(0,0,0)
self:T(self.lid..string.format("Assignment for %s",Group:GetName()))
for _name,_FG in pairs(ReadyFlightGroups or {}) do
local FG = _FG -- Ops.FlightGroup#FLIGHTGROUP
local fcoord = FG:GetCoordinate()
local dist = math.floor(UTILS.Round(fcoord:Get2DDistance(gcoord)/1000,1))
self:T(self.lid..string.format("FG %s Distance %dkm",_name,dist))
disttable[#disttable+1] = { FG=FG, dist=dist}
if dist>mindist then mindist=dist end
end
local function sortDistance(a, b)
return a.dist < b.dist
end
table.sort(disttable, sortDistance)
for _,_entry in ipairs(disttable) do
local FG = _entry.FG -- Ops.FlightGroup#FLIGHTGROUP
FG:AddMission(Auftrag)
local cm = FG:GetMissionCurrent()
if cm then cm:Cancel() end
wingsize = wingsize - 1
self:T(self.lid..string.format("Assigned to FG %s Distance %dkm",FG:GetName(),_entry.dist))
if wingsize == 0 then
assigned = true
break
end
end
end
return assigned, wingsize
end
--- Find a holding point closest to the group to be attacked (if any set)
-- @param #EASYA2G self
-- @param Wrapper.Group#GROUP Group
-- @return Core.Point#COORDINATE Point (can be nil!)
function EASYA2G:_GetClosestHoldingPoint(Group)
local point = nil
local mindist = 0
if Group and Group:IsAlive() then
local gcoord = Group:GetCoordinate() or COORDINATE:New(0,0,0)
for _,_data in pairs(self.ManagedCP or {}) do
local data = _data -- #EASYGCICAP.CapPoint
--data.Coordinate
local dist = math.floor(UTILS.Round(data.Coordinate:Get2DDistance(gcoord)/1000,1))
self:T(self.lid..string.format("Holding Point Distance %dkm",dist))
if dist>mindist then
mindist=dist
point=data.Coordinate
end
end
end
return point
end
--- Here, we'll decide if we need to launch an attacking flight, and from where
-- @param #EASYA2G self
-- @param Ops.Intel#INTEL.Cluster Cluster
-- @return #EASYA2G self
function EASYA2G:_AssignMission(Cluster)
self:I(self.lid.."_AssignMission")
-- Here, we'll decide if we need to launch an attacking flight, and from where
local overhead = self.overhead
local capspeed = self.capspeed + 100
local capalt = self.capalt or 5000
local maxsize = self.maxinterceptsize
local repeatsonfailure = self.repeatsonfailure
local wings = self.wings
local ctlpts = self.ManagedCP
local MaxAliveMissions = self.MaxAliveMissions --* self.capgrouping
local nogozoneset = self.NoGoZoneSet
local conflictzoneset = self.ConflictZoneSet
local ReadyFlightGroups = self.ReadyFlightGroups
-- Aircraft?
if Cluster.ctype == INTEL.Ctype.AIRCRAFT then return end
-- Threatlevel 0..10
local contact = self.Intel:GetHighestThreatContact(Cluster)
local name = contact.groupname --#string
local threat = contact.threatlevel --#number
local position = self.Intel:CalcClusterFuturePosition(Cluster,300)
-- calculate closest zone
local bestdistance = 2000*1000 -- 2000km
local targetairwing = nil -- Ops.Airwing#AIRWING
local targetawname = "" -- #string
local clustersize = self.Intel:ClusterCountUnits(Cluster) or 1
local wingsize = math.abs(overhead * (clustersize+1))
if wingsize > maxsize then wingsize = maxsize end
-- existing mission, and if so - done?
local retrymission = true
if Cluster.mission and (not Cluster.mission:IsOver()) then
retrymission = false
end
if (retrymission) and (wingsize >= 1) then
MESSAGE:New(string.format("**** %s Attackers need wingsize %d", UTILS.GetCoalitionName(self.coalition), wingsize),15,"A2G"):ToAllIf(self.debug):ToLog()
for _,_data in pairs (wings) do
local airwing = _data[1] -- Ops.Airwing#AIRWING
local zone = _data[2] -- Core.Zone#ZONE
local zonecoord = zone:GetCoordinate()
local name = _data[3] -- #string
local coa = AIRBASE:FindByName(name):GetCoalition()
local distance = position:DistanceFromPointVec2(zonecoord)
local airframes = airwing:CountAssets(true)
local samecoalitionab = coa == self.coalition and true or false
if distance < bestdistance and airframes >= wingsize and samecoalitionab == true then
bestdistance = distance
targetairwing = airwing
targetawname = name
end
end
for _,_data in pairs (ctlpts) do
--local airwing = _data[1] -- Ops.Airwing#AIRWING
--local zone = _data[2] -- Core.Zone#ZONE
--local zonecoord = zone:GetCoordinate()
--local name = _data[3] -- #string
local data = _data -- #EASYGCICAP.CapPoint
local name = data.AirbaseName
local zonecoord = data.Coordinate
if data.Zone then
-- refresh coordinate in case we have a (moving) zone
zonecoord = data.Zone:GetCoordinate()
end
local airwing = wings[name][1]
local coa = AIRBASE:FindByName(name):GetCoalition()
local samecoalitionab = coa == self.coalition and true or false
local distance = position:DistanceFromPointVec2(zonecoord)
local airframes = airwing:CountAssets(true)
if distance < bestdistance and airframes >= wingsize and samecoalitionab == true then
bestdistance = distance
targetairwing = airwing -- Ops.Airwing#AIRWING
targetawname = name
end
end
local text = string.format("Closest Airwing is %s", targetawname)
local m = MESSAGE:New(text,10,"EasyA2G"):ToAllIf(self.debug):ToLog()
-- Do we have a matching airwing?
if targetairwing then
local AssetCount = targetairwing:CountAssetsOnMission(MissionTypes,Cohort)
local missioncount = self:_CountAliveAuftrags()
-- Enough airframes on mission already?
self:T(self.lid.." Assets on Mission "..AssetCount)
if missioncount < MaxAliveMissions then
local repeats = repeatsonfailure
local Vec1 = contact.group:GetVec2()
local Vec2 = targetairwing:GetVec2()
--local HoldingVec2 = UTILS.FindNearestPointOnCircle(Vec1,UTILS.NMToMeters(10),Vec2)
local IngressCoordinate = self:_GetClosestHoldingPoint(contact.group)
if IngressCoordinate == nil then
local IngressVec2 = UTILS.FindNearestPointOnCircle(Vec1,UTILS.NMToMeters(10),Vec2)
IngressCoordinate = COORDINATE:NewFromVec2(IngressVec2)
end
local InterceptAuftrag = AUFTRAG:NewBAI(contact.group,capalt)
:SetMissionRange(150)
:SetPriority(1,true,1)
:SetRepeatDelay(300)
--:SetRequiredAssets(wingsize)
:SetRepeatOnFailure(repeats)
:SetMissionSpeed(UTILS.KnotsToAltKIAS(capspeed,capalt))
:SetMissionAltitude(capalt)
-- TODO: Refine this
--:SetMissionHoldingCoord(COORDINATE:NewFromVec2(HoldingVec2),capalt,capspeed,120)
:SetMissionIngressCoord(IngressCoordinate,capalt,capspeed)
--:SetMissionEgressCoord(COORDINATE:NewFromVec2(HoldingVec2),capalt,capspeed)
if nogozoneset:Count() > 0 then
InterceptAuftrag:AddConditionSuccess(
function(group,zoneset,conflictset)
local success = false
if group and group:IsAlive() then
local coord = group:GetCoordinate()
if coord and zoneset:Count() > 0 and zoneset:IsCoordinateInZone(coord) then
success = true
end
if coord and conflictset:Count() > 0 and conflictset:IsCoordinateInZone(coord) then
success = false
end
else
success = true -- target dead
end
return success
end,
contact.group,
nogozoneset,
conflictzoneset
)
end
InterceptAuftrag:AddConditionFailure(
function()
local failure = false
if InterceptAuftrag:CountOpsGroups()==0 and InterceptAuftrag:IsExecuting() then failure = true end
return failure
end
)
table.insert(self.ListOfAuftrag,InterceptAuftrag)
local assigned, rest = self:_TryAssignMission(ReadyFlightGroups,InterceptAuftrag,contact.group,wingsize)
if not assigned then
InterceptAuftrag:SetRequiredAssets(rest)
targetairwing:AddMission(InterceptAuftrag)
end
Cluster.mission = InterceptAuftrag
end
else
MESSAGE:New("**** Not enough airframes available or max mission limit reached!",15,"EasyA2G"):ToAllIf(self.debug):ToLog()
end
end
end
--- (Internal) Start detection.
-- @param #EASYA2G self
-- @return #EASYA2G self
function EASYA2G:_StartIntel()
self:T(self.lid.."_StartIntel")
-- Border GCI Detection
local BlueAir_DetectionSetGroup = SET_GROUP:New()
BlueAir_DetectionSetGroup:FilterPrefixes( self.EWRName )
BlueAir_DetectionSetGroup:FilterStart()
-- Intel type detection
local BlueIntel = INTEL:New(BlueAir_DetectionSetGroup,self.coalitionname, self.alias)
BlueIntel:SetClusterAnalysis(true,false,false)
BlueIntel:SetForgetTime(300)
BlueIntel:SetAcceptZones(self.GoZoneSet)
BlueIntel:SetRejectZones(self.NoGoZoneSet)
BlueIntel:SetConflictZones(self.ConflictZoneSet)
BlueIntel:SetVerbosity(0)
if self.usecorridors == true then
BlueIntel:SetCorridorZones(self.corridorzones)
if self.corridorfloor or self.corridorceiling then
BlueIntel:SetCorridorLimitsFeet(self.corridorfloor,self.corridorceiling)
end
end
BlueIntel:Start()
if self.debug then
BlueIntel.debug = true
end
local function AssignCluster(Cluster)
self:_AssignMission(Cluster)
end
function BlueIntel:onbeforeNewCluster(From,Event,To,Cluster)
AssignCluster(Cluster)
end
self.Intel = BlueIntel
return self
end
-------------------------------------------------------------------------
-- TODO FSM Functions
-------------------------------------------------------------------------
--- (Internal) FSM Function onafterStart
-- @param #EASYA2G self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @return #EASYA2G self
function EASYA2G:onafterStart(From,Event,To)
self:T({From,Event,To})
self:_StartIntel()
self:_CreateAirwings()
self:_CreateSquads()
--self:_SetCAPPatrolPoints()
self:_SetTankerPatrolPoints()
self:_SetAwacsPatrolPoints()
self:_SetReconPatrolPoints()
self:__Status(-10)
return self
end
--- (Internal) FSM Function onafterStatus
-- @param #EASYA2G self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @return #EASYA2G self
function EASYA2G:onafterStatus(From,Event,To)
self:T({From,Event,To})
-- cleanup
local cleaned = false
local cleanlist = {}
for _,_auftrag in pairs(self.ListOfAuftrag) do
local auftrag = _auftrag -- Ops.Auftrag#AUFTRAG
if auftrag and (not (auftrag:IsCancelled() or auftrag:IsDone() or auftrag:IsOver())) then
table.insert(cleanlist,auftrag)
cleaned = true
end
end
if cleaned == true then
self.ListOfAuftrag = nil
self.ListOfAuftrag = cleanlist
end
-- Gather Some Stats
local function counttable(tbl)
local count = 0
for _,_data in pairs(tbl) do
count = count + 1
end
return count
end
local wings = counttable(self.ManagedAW)
local squads = counttable(self.ManagedSQ)
local caps = counttable(self.ManagedCP)
local assets = 0
local instock = 0
local capmission = 0
local interceptmission = 0
local reconmission = 0
local awacsmission = 0
local tankermission = 0
local alert5mission = 0
for _,_wing in pairs(self.wings) do
local count = _wing[1]:CountAssetsOnMission(MissionTypes,Cohort)
local count2 = _wing[1]:CountAssets(true,MissionTypes,Attributes)
--capmission = capmission + _wing[1]:CountMissionsInQueue({AUFTRAG.Type.PATROLRACETRACK})
interceptmission = interceptmission + _wing[1]:CountMissionsInQueue({AUFTRAG.Type.BAI})
reconmission = reconmission + _wing[1]:CountMissionsInQueue({AUFTRAG.Type.RECON})
awacsmission = awacsmission + _wing[1]:CountMissionsInQueue({AUFTRAG.Type.AWACS})
tankermission = tankermission + _wing[1]:CountMissionsInQueue({AUFTRAG.Type.TANKER})
alert5mission = alert5mission + _wing[1]:CountMissionsInQueue({AUFTRAG.Type.ALERT5})
assets = assets + count
instock = instock + count2
local assetsonmission = _wing[1]:GetAssetsOnMission({AUFTRAG.Type.BAI,AUFTRAG.Type.ALERT5})
-- update ready groups
self.ReadyFlightGroups = nil
self.ReadyFlightGroups = {}
for _,_asset in pairs(assetsonmission or {}) do
local asset = _asset -- Functional.Warehouse#WAREHOUSE.Assetitem
local FG = asset.flightgroup -- Ops.FlightGroup#FLIGHTGROUP
if FG then
local name = FG:GetName()
local engage = FG:IsEngaging()
local hasmissiles = FG:CanAirToGround()
--self:T("Is Alert5? "..tostring(FG:GetMissionCurrent().type))
local isalert5 = (FG:GetMissionCurrent() ~= nil and FG:GetMissionCurrent().type == AUFTRAG.Type.ALERT5) and true or false
local ready = hasmissiles and FG:IsFuelGood() and (FG:IsAirborne() or isalert5)
self:T(string.format("Flightgroup %s Engaging = %s Ready = %s (HasAmmo = %s HasFuel = %s Alert5 = %s)",tostring(name),tostring(engage),tostring(ready),tostring(hasmissiles),tostring(FG:IsFuelGood()), tostring(isalert5)))
if ready then
self.ReadyFlightGroups[name] = FG
end
end
end
end
if self.Monitor then
local threatcount = #self.Intel.Clusters or 0
local text = self.alias
text = text.."\nWings: "..wings.."\nSquads: "..squads.."\nHoldPoints: "..caps.."\nAssets on Mission: "..assets.."\nAssets in Stock: "..instock
text = text.."\nThreats: "..threatcount
text = text.."\nAirWing alive Missions: "..capmission+awacsmission+tankermission+reconmission+interceptmission+alert5mission
--text = text.."\n - A2G Holding: "..capmission
text = text.."\n - A2G Attack: "..interceptmission
text = text.."\n - AWACS: "..awacsmission
text = text.."\n - TANKER: "..tankermission
text = text.."\n - Recon: "..reconmission
text = text.."\n - Alert5 "..alert5mission
text = text.."\nMission Limit: "..self.MaxAliveMissions
MESSAGE:New(text,15,"A2G"):ToAll():ToLogIf(self.debug)
end
self:__Status(30)
return self
end
+10 -141
View File
@@ -1,12 +1,3 @@
--- **Ops** - Create your A2A Defenses.
--
-- **Main Features:**
--
-- * Automatically create and manage A2A defenses using an AirWing and Squadrons for one coalition
-- * Easy set-up
--
-- ===
--
-------------------------------------------------------------------------
-- Easy CAP/GCI Class, based on OPS classes
-------------------------------------------------------------------------
@@ -118,7 +109,7 @@
-- ### Prerequisites
--
-- You have to put a **STATIC WAREHOUSE** object on the airbase with the UNIT name according to the name of the airbase. **Do not put any other static type or it creates a conflict with the airbase name!**
-- E.g. for Kutaisi this has to have the unit name Kutaisi. This object symbolizes the AirWing HQ.
-- E.g. for Kuitaisi this has to have the unit name Kutaisi. This object symbolizes the AirWing HQ.
-- Next put a late activated template group for your CAP/GCI Squadron on the map. Last, put a zone on the map for the CAP operations, let's name it "Blue Zone 1". Size of the zone plays no role.
-- Put an EW radar system on the map and name it aptly, like "Blue EWR".
--
@@ -214,7 +205,7 @@ EASYGCICAP = {
coalition = "blue",
alias = nil,
wings = {},
Intel = nil, -- Ops.Intel#INTEL
Intel = nil,
resurrection = 900,
capspeed = 300,
capalt = 25000,
@@ -284,11 +275,10 @@ EASYGCICAP = {
--- EASYGCICAP class version.
-- @field #string version
EASYGCICAP.version="0.1.33"
EASYGCICAP.version="0.1.30"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO: TBD
@@ -360,34 +350,6 @@ function EASYGCICAP:New(Alias, AirbaseName, Coalition, EWRName)
self:__Start(math.random(6,12))
--- On Before "Start" event.
-- @function [parent=#EASYGCICAP] OnBeforeStart
-- @param #EASYGCICAP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
--- On After "Start" event.
-- @function [parent=#EASYGCICAP] OnAfterStart
-- @param #EASYGCICAP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
--- On Before "Status" event.
-- @function [parent=#EASYGCICAP] OnBeforeStatus
-- @param #EASYGCICAP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
--- On After "Status" event.
-- @function [parent=#EASYGCICAP] OnAfterStatus
-- @param #EASYGCICAP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
return self
end
@@ -407,28 +369,6 @@ function EASYGCICAP:GetAirwing(AirbaseName)
return nil
end
--- Add an agent to the underlying INTEL detection - caution, we need to be started first for this to work!
-- Normally this isn't necessary when the Group name is correctly filled (see EWRName in `New()`).
-- @param #EASYGCICAP self
-- @param Wrapper.Group#GROUP Group The group object to be added as Intel Agent.
-- @return #EASYGCICAP self
function EASYGCICAP:AddAgent(Group)
self:T(self.lid.."AddAgent")
if Group:IsInstanceOf("GROUP") and self.Intel ~= nil then
self.Intel:AddAgent(Group)
if self.TankerInvisible == true then
Group:SetCommandInvisible(true)
Group:OptionROEHoldFire()
if Group:IsAir() then
Group:OptionROTEvadeFire()
else
Group:OptionDisperseOnAttack(30)
end
end
end
return self
end
--- Get a table of all managed AirWings
-- @param #EASYGCICAP self
-- @return #table Table of Ops.AirWing#AIRWING Airwings
@@ -671,6 +611,7 @@ function EASYGCICAP:SetCapStartTimeVariation(Start, End)
return self
end
--- Set which target types CAP flights will prefer to engage, defaults to {"Air"}
-- @param #EASYGCICAP self
-- @param #table types Table of comma separated #string entries, defaults to {"Air"} (everything that flies and is not a weapon). Useful other options are e.g. {"Bombers"}, {"Fighters"},
@@ -715,7 +656,7 @@ function EASYGCICAP:_CreateAirwings()
return self
end
--- (Internal) Create and add another AirWing to the manager
--- (internal) Create and add another AirWing to the manager
-- @param #EASYGCICAP self
-- @param #string Airbasename
-- @param #string Alias
@@ -822,13 +763,8 @@ function EASYGCICAP:_AddAirwing(Airbasename, Alias)
end
end
if self.noalert5 > 0 then
local alert
if self.ClassName == "EASYGCICAP" then
alert = AUFTRAG:NewALERT5(AUFTRAG.Type.INTERCEPT)
elseif self.ClassName == "EASYA2G" then
alert = AUFTRAG:NewALERT5(AUFTRAG.Type.BAI)
end
if self.noalert5 > 0 then
local alert = AUFTRAG:NewALERT5(AUFTRAG.Type.INTERCEPT)
alert:SetRequiredAssets(self.noalert5)
alert:SetRepeat(99)
CAP_Wing:AddMission(alert)
@@ -1364,54 +1300,6 @@ function EASYGCICAP:AddConflictZone(Zone)
return self
end
--- Function to set corridor zones.
-- @param #EASYGCICAP self
-- @param Core.Set#SET_ZONE CorridorZones Can be handed in as SET\_ZONE or single ZONE object.
-- @return #EASYGCICAP self
function EASYGCICAP:SetCorridorZones(CorridorZones)
self:T(self.lid .. "SetCorridorZones")
if CorridorZones and CorridorZones:IsInstanceOf("SET_ZONE") then
self.corridorzones = CorridorZones
self.usecorridors = true
elseif CorridorZones and CorridorZones:IsInstanceOf("ZONE_BASE") then
if not self.corridorzones then self.corridorzones = SET_ZONE:New() end
self.corridorzones:AddZone(CorridorZones)
self.usecorridors = true
end
return self
end
--- Function to add one corridor zone.
-- @param #EASYGCICAP self
-- @param Core.Zone#ZONE CorridorZone The ZONE object to be added.
-- @return #EASYGCICAP self
function EASYGCICAP:AddCorridorZone(CorridorZone)
self:T(self.lid .. "AddCorridorZone")
self:SetCorridorZones(CorridorZone)
return self
end
--- Function to set corridor zone floor and ceiling in FEET.
-- @param #EASYGCICAP self
-- @param #number Floor Floor altitude ASL in feet.
-- @param #number Ceiling Ceiling altitude ASL in feet.
-- @return #EASYGCICAP self
function EASYGCICAP:SetCorridorZoneFloorAndCeiling(Floor,Ceiling)
self.corridorfloor = UTILS.FeetToMeters(Floor)
self.corridorceiling = UTILS.FeetToMeters(Ceiling)
return self
end
--- Function to set corridor zone floor and ceiling in METERS.
-- @param #EASYGCICAP self
-- @param #number Floor Floor altitude ASL in meters.
-- @param #number Ceiling Ceiling altitude ASL in meters.
-- @return #EASYGCICAP self
function EASYGCICAP:SetCorridorZoneFloorAndCeilingMeters(Floor,Ceiling)
self.corridorfloor = Floor
self.corridorceiling = Ceiling
return self
end
--- (Internal) Try to assign the intercept to a FlightGroup already in air and ready.
-- @param #EASYGCICAP self
@@ -1571,8 +1459,6 @@ function EASYGCICAP:_AssignIntercept(Cluster)
if coord and conflictset:Count() > 0 and conflictset:IsCoordinateInZone(coord) then
success = false
end
else
success = true
end
return success
end,
@@ -1582,14 +1468,6 @@ function EASYGCICAP:_AssignIntercept(Cluster)
)
end
InterceptAuftrag:AddConditionFailure(
function()
local failure = false
if InterceptAuftrag:CountOpsGroups()==0 and InterceptAuftrag:IsExecuting() then failure = true end
return failure
end
)
table.insert(self.ListOfAuftrag,InterceptAuftrag)
local assigned, rest = self:_TryAssignIntercept(ReadyFlightGroups,InterceptAuftrag,contact.group,wingsize)
if not assigned then
@@ -1622,14 +1500,6 @@ function EASYGCICAP:_StartIntel()
BlueIntel:SetRejectZones(self.NoGoZoneSet)
BlueIntel:SetConflictZones(self.ConflictZoneSet)
BlueIntel:SetVerbosity(0)
if self.usecorridors == true then
BlueIntel:SetCorridorZones(self.corridorzones)
if self.corridorfloor or self.corridorceiling then
BlueIntel:SetCorridorLimitsFeet(self.corridorfloor,self.corridorceiling)
end
end
BlueIntel:Start()
if self.debug then
@@ -1733,7 +1603,7 @@ function EASYGCICAP:onafterStatus(From,Event,To)
tankermission = tankermission + _wing[1]:CountMissionsInQueue({AUFTRAG.Type.TANKER})
assets = assets + count
instock = instock + count2
local assetsonmission = _wing[1]:GetAssetsOnMission({AUFTRAG.Type.ALERT5, AUFTRAG.Type.GCICAP,AUFTRAG.Type.PATROLRACETRACK})
local assetsonmission = _wing[1]:GetAssetsOnMission({AUFTRAG.Type.GCICAP,AUFTRAG.Type.PATROLRACETRACK})
-- update ready groups
self.ReadyFlightGroups = nil
self.ReadyFlightGroups = {}
@@ -1744,8 +1614,7 @@ function EASYGCICAP:onafterStatus(From,Event,To)
local name = FG:GetName()
local engage = FG:IsEngaging()
local hasmissiles = FG:IsOutOfMissiles() == nil and true or false
local isalert5 = (FG:GetMissionCurrent() ~= nil and FG:GetMissionCurrent().type == AUFTRAG.Type.ALERT5) and true or false
local ready = hasmissiles and FG:IsFuelGood() and (FG:IsAirborne() or isalert5)
local ready = hasmissiles and FG:IsFuelGood() and FG:IsAirborne()
--self:T(string.format("Flightgroup %s Engaging = %s Ready = %s",tostring(name),tostring(engage),tostring(ready)))
if ready then
self.ReadyFlightGroups[name] = FG
File diff suppressed because it is too large Load Diff
+69 -1
View File
@@ -60,6 +60,8 @@
-- @field #boolean jettisonEmptyTanks Allow (true) or disallow (false) AI to jettison empty fuel tanks.
-- @field #boolean jettisonWeapons Allow (true) or disallow (false) AI to jettison weapons if in danger.
-- @field #number holdtime Time [s] flight is holding before going on final. Set to nil for indefinitely.
-- @field #table flightplans Flight plans for this group.
-- @field Navigation.FlightPlan#FLIGHTPLAN flightplan Currently active flight plan.
--
-- @extends Ops.OpsGroup#OPSGROUP
@@ -151,7 +153,8 @@ FLIGHTGROUP = {
playerWarnings = {},
prohibitAB = false,
jettisonEmptyTanks = true,
jettisonWeapons = true, -- that's actually a negative option like prohibitAB
jettisonWeapons = true, -- that's actually a negative option like prohibitAB
flightplans = {},
}
@@ -885,6 +888,17 @@ function FLIGHTGROUP:GetFlightControl()
return self.flightcontrol
end
--- Add flightplan.
-- @param #FLIGHTGROUP self
-- @param Navigation.FlightPlan#FLIGHTPLAN FlightPlan The flight plan.
-- @return #FLIGHTGROUP self
function FLIGHTGROUP:AddFlightPlan(FlightPlan)
self:T(self.lid..string.format("Adding flight plan %s", FlightPlan:GetName() ))
table.insert(self.flightplans, FlightPlan)
return self
end
--- Set the homebase.
-- @param #FLIGHTGROUP self
@@ -1709,6 +1723,10 @@ function FLIGHTGROUP:Status()
-- TODO: Not sure why I introduced this here.
self:_CheckGroupDone()
end
if not (self.cargoTransport or mission) then
self:_CheckFlightPlans()
end
end
@@ -3799,6 +3817,56 @@ function FLIGHTGROUP:onafterFuelCritical(From, Event, To)
end
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Flightplan Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Check flightplans.
-- @param #FLIGHTGROUP self
-- @param Navigation.FlightPlan#FLIGHTPLAN FlightPlan Flight plan.
-- @return #FLIGHTGROUP self
function FLIGHTGROUP:_SetFlightPlan(FlightPlan)
self.flightplan=FlightPlan
for i,_fix in pairs(FlightPlan.fixes) do
local fix=_fix --Navigation.NavFix#NAVFIX
--fix.coordinate
local speed=fix.altMin
local altitude=(fix.altMin or fix.altMax)~=nil and fix:GetAltitude() or FlightPlan:GetCruiseAltitude()
local wp=self:AddWaypoint(fix.coordinate, Speed, AfterWaypointWithID, altitude or 10000, false)
end
end
--- Check flightplans.
-- @param #FLIGHTGROUP self
-- @return #FLIGHTGROUP self
function FLIGHTGROUP:_CheckFlightPlans()
if self.flightplan then
return
end
for i,_flightplan in pairs(self.flightplans) do
local flightplan=_flightplan --Navigation.FlightPlan#FLIGHTPLAN
if flightplan then
self:_SetFlightPlan(flightplan)
break
end
end
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Task functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+5 -167
View File
@@ -27,9 +27,6 @@
-- @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 Core.Set#SET_ZONE conflictzoneset Set of conflict zones. Contacts in these zones are considered, even if they are not in accept zones or if they are in reject zones.
-- @field Core.Set#SET_ZONE corridorzoneset Set of corridor zones. Contacts in these zones are never considered. Also see corridorfloorheight and corridorfloorceiling.
-- @field #number corridorfloor [Air] Contacts below this height (ASL!) are considered, even if they are in a corridor zone.
-- @field #number corridorceiling [Air] Contacts above this height (ASL!) are considered, even if they are in a corridor zone.
-- @field #table Contacts Table of detected items.
-- @field #table ContactsLost Table of lost detected items.
-- @field #table ContactsUnknown Table of new detected items.
@@ -42,9 +39,6 @@
-- @field #number prediction Seconds default to be used with CalcClusterFuturePosition.
-- @field #boolean detectStatics If `true`, detect STATIC objects. Default `false`.
-- @field #number statusupdate Time interval in seconds after which the status is refreshed. Default 60 sec. Should be negative.
-- @field #boolean DetectAccoustic If true, also detect by sound (ie proximity).
-- @field #number DetectAccousticRadius Radius dfor accoustic detection, defaults to 2000 meters.
-- @field #table DetectAccousticUnitTypes Types of units we can detect accousticly. Defaults to {Unit.Category.HELICOPTER}
-- @extends Core.Fsm#FSM
--- Top Secret!
@@ -108,9 +102,6 @@ INTEL = {
clusterarrows = false,
prediction = 300,
detectStatics = false,
DetectAccoustic = false,
DetectAccousticRadius = 1000,
DetectAccousticUnitTypes = {Unit.Category.HELICOPTER},
}
--- Detected item info.
@@ -169,14 +160,14 @@ INTEL.Ctype={
--- INTEL class version.
-- @field #string version
INTEL.version="0.3.10"
INTEL.version="0.3.6"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- ToDo list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO: Add min cluster size. Only create new clusters if they have a certain group size.
-- NODO: process detected set asynchroniously for better performance.
-- TODO: process detected set asynchroniously for better performance.
-- DONE: Add statics.
-- DONE: Filter detection methods.
-- DONE: Accept zones.
@@ -184,7 +175,6 @@ INTEL.version="0.3.10"
-- NOGO: SetAttributeZone --> return groups of generalized attributes in a zone.
-- DONE: Loose units only if they remain undetected for a given time interval. We want to avoid fast oscillation between detected/lost states. Maybe 1-5 min would be a good time interval?!
-- DONE: Combine units to groups for all, new and lost.
-- DONE: Add corridor zones.
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
@@ -276,7 +266,6 @@ function INTEL:New(DetectionSet, Coalition, Alias)
self:SetForgetTime()
self:SetAcceptZones()
self:SetRejectZones()
self:SetCorridorZones()
self:SetConflictZones()
------------------------
@@ -409,26 +398,6 @@ function INTEL:SetAcceptZones(AcceptZoneSet)
return self
end
--- Set to accept accoustic detection.
-- @param #INTEL self
-- @param #number Radius Radius in which we can "hear" units. Defaults to 1000 meters.
-- @param #table UnitCategories Set what Unit Categories we can "hear". Defaults to `{Unit.Category.GROUND_UNIT,Unit.Category.HELICOPTER}`
-- @return #INTEL self
function INTEL:SetAccousticDetectionOn(Radius,UnitCategories)
self.DetectAccoustic = true
self.DetectAccousticRadius = Radius or 1000
self.DetectAccousticUnitTypes = UnitCategories or {Unit.Category.HELICOPTER}
return self
end
--- Switch off accoustic detection.
-- @param #INTEL self
-- @return #INTEL self
function INTEL:SetAccousticDetectionOff()
self.DetectAccoustic = false
return self
end
--- Add an accept zone. Only contacts detected in this zone are considered.
-- @param #INTEL self
-- @param Core.Zone#ZONE AcceptZone Add a zone to the accept zone set.
@@ -506,69 +475,7 @@ function INTEL:RemoveConflictZone(ConflictZone)
return self
end
--- Set corrdidor zones. Contacts detected in this/these zone(s) are never reported by the detection.
-- Note that corrdidor zones overrule all other zones, for exceptions see corridor floor and corridor ceiling heights.
-- @param #INTEL self
-- @param Core.Set#SET_ZONE CorridorZoneSet Set of corrdidor zone(s).
-- @return #INTEL self
function INTEL:SetCorridorZones(CorridorZoneSet)
self.corridorzoneset=CorridorZoneSet or SET_ZONE:New()
return self
end
--- Add a corrdidor zone. Contacts detected in this zone are corrdidored and not reported by the detection.
-- Note that corrdidor zones overrule all other zones, for exceptions see corridor floor and corridor ceiling heights.
-- @param #INTEL self
-- @param Core.Zone#ZONE CorridorZone Add a zone to the corrdidor zone set.
-- @return #INTEL self
function INTEL:AddCorridorZone(CorridorZone)
self.corridorzoneset:AddZone(CorridorZone)
return self
end
--- Remove a corrdidor zone from the corrdidor zone set.
-- Note that corrdidor zones overrule all other zones, for exceptions see corridor floor and corridor ceiling heights.
-- @param #INTEL self
-- @param Core.Zone#ZONE CorridorZone Remove a zone from the corrdidor zone set.
-- @return #INTEL self
function INTEL:RemoveCorridorZone(CorridorZone)
self.corridorzoneset:Remove(CorridorZone:GetName(), true)
return self
end
--- [Air] Add corrdidor zone floor and height. This is generally applicable to all(!) corridor zones. Considered as ASL (above sea level or barometric) values.
-- Overrides corridor exception for objects flying outside this limitations.
-- To set an individual ceiling/floor on any Core.Zone#ZONE you wish to use, set these properties on the Core.Zone#ZONE object:
-- `mycorridorzone:SetProperty("CorridorFloor",500)` -- meters, case sensitivity matters!
-- `mycorridorzone:SetProperty("CorridorCeiling",10000)` -- meters, case sensitivity matters!
-- @param #INTEL self
-- @param #number Floor Floor altitude in meters.
-- @param #number Ceiling Ceiling altitude in meters.
-- @return #INTEL self
function INTEL:SetCorridorLimits(Floor,Ceiling)
self.corridorceiling = Ceiling or 10000
self.corridorfloor = Floor or 1
return self
end
--- [Air] Add corrdidor zone floor and height. This is generally applicable to all(!) corridor zones. Considered as ASL (above sea level or barometric) values.
-- Overrides corridor exception for objects flying outside this limitations.
-- To set an individual ceiling/floor on any Core.Zone#ZONE you wish to use, set these properties on the Core.Zone#ZONE object:
-- `mycorridorzone:SetProperty("CorridorFloor",UTILS.FeetToMeters(5000))` -- feet, case sensitivity matters!
-- `mycorridorzone:SetProperty("CorridorCeiling",UTILS.FeetToMeters(20000))` -- feet, case sensitivity matters!
-- @param #INTEL self
-- @param #number Floor Floor altitude in feet.
-- @param #number Ceiling Ceiling altitude in feet.
-- @return #INTEL self
function INTEL:SetCorridorLimitsFeet(Floor,Ceiling)
local Ceiling = Ceiling or 25000
local Floor = Floor or 15000
self.corridorceiling = UTILS.FeetToMeters(Ceiling)
self.corridorfloor = UTILS.FeetToMeters(Floor)
return self
end
--- **OBSOLETE, not functional!** Set forget contacts time interval.
--- **OBSOLETE, will be removed in next version!** Set forget contacts time interval.
-- Previously known contacts that are not detected any more, are "lost" after this time.
-- This avoids fast oscillations between a contact being detected and undetected.
-- @param #INTEL self
@@ -924,18 +831,6 @@ function INTEL:UpdateIntel()
self:GetDetectedUnits(recce, DetectedUnits, RecceDetecting, self.DetectVisual, self.DetectOptical, self.DetectRadar, self.DetectIRST, self.DetectRWR, self.DetectDLINK)
end
if self.DetectAccoustic then
local recce = group:GetFirstUnitAlive()
local detectionzone = group:GetProperty("INTEL_DETECT_ACCZONE")
if not detectionzone then
detectionzone = ZONE_GROUP:New(group.IdentifiableName.."INTEL_DETECT_ACCZONE",group,self.DetectAccousticRadius or 2000)
group:SetProperty("INTEL_DETECT_ACCZONE",detectionzone)
end
if recce and recce:IsGround() then
self:GetDetectedUnitsAccoustic(recce,DetectedUnits,RecceDetecting,detectionzone)
end
end
end
end
@@ -989,36 +884,6 @@ function INTEL:UpdateIntel()
table.insert(remove, unitname)
end
end
-- Check if unit is in any of the corridor zones.
if self.corridorzoneset:Count()>0 then
self:T("Corridorzone Check for unit "..unit:GetName())
local inzone = false
for _,_zone in pairs(self.corridorzoneset.Set) do
local zone=_zone --Core.Zone#ZONE
if unit:IsInZone(zone) then
local corridorfloor = zone:GetProperty("CorridorFloor") or self.corridorfloor
local corridorceiling = zone:GetProperty("CorridorCeiling") or self.corridorceiling
local debugtext = "Corridorzone Check for unit "..unit:GetName().."\n"
debugtext = debugtext .. string.format("IsAir %s | Alt %dft | Floor %dft | Ceil %dft",tostring(unit:IsAir()),tonumber(UTILS.MetersToFeet(unit:GetAltitude())),
tonumber(UTILS.MetersToFeet(corridorfloor)),tonumber(UTILS.MetersToFeet(corridorceiling)))
MESSAGE:New(debugtext,15,"INTEL"):ToAllIf(self.verbose>1):ToLogIf(self.verbose>1)
if unit:IsAir() and (corridorfloor ~= nil or corridorceiling ~= nil) then
local alt = unit:GetAltitude()
if corridorfloor and alt > corridorfloor then inzone = true end
if corridorceiling and (inzone == true or corridorfloor == nil) and alt < corridorceiling then inzone = true else inzone = false end
if inzone == true then break end
else
inzone=true
break
end
end
end
-- Unit is inside a corridor zone ==> remove!
if inzone then
table.insert(remove, unitname)
end
end
-- Filter unit categories. Added check that we have a UNIT and not a STATIC object because :GetUnitCategory() is only available for units.
if #self.filterCategory>0 and unit:IsInstanceOf("UNIT") then
@@ -1241,7 +1106,7 @@ function INTEL:CreateDetectedItems(DetectedGroups, DetectedStatics, RecceDetecti
return self
end
--- (Internal) Return the detected target groups of the controllable as a table.
--- (Internal) Return the detected target groups of the controllable as a @{Core.Set#SET_GROUP}.
-- The optional parameters 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
@@ -1337,33 +1202,6 @@ function INTEL:GetDetectedUnits(Unit, DetectedUnits, RecceDetecting, DetectVisua
end
end
--- (Internal) Return the detected target groups of the controllable as a @{Core.Set#SET_GROUP}.
-- @param #INTEL self
-- @param Wrapper.Unit#UNIT Recce The unit detecting.
-- @param #table DetectedUnits Table of detected units to be filled.
-- @param #table RecceDetecting Table of recce per unit to be filled.
-- @param Core.Zone#ZONE_GROUP detectionzone The zone where to look.
function INTEL:GetDetectedUnitsAccoustic(Recce,DetectedUnits,RecceDetecting,detectionzone)
local othercoalition = self.coalition == coalition.side.BLUE and coalition.side.RED or coalition.side.BLUE
self:T("Other coalition = "..othercoalition)
if detectionzone then
-- Get detected units
local reccename = Recce:GetName()
local DetectAccousticUnitTypes = self.DetectAccousticUnitTypes or {Unit.Category.HELICOPTER}
detectionzone:Scan({Object.Category.UNIT},DetectAccousticUnitTypes)
local unitset = detectionzone:GetScannedSetUnit(othercoalition) -- Core.Set#SET_UNIT
self:T("Accoustic detection found #Units "..unitset:CountAlive())
for _,_unit in pairs(unitset.Set or {}) do
if _unit and _unit:IsAlive() and _unit:GetCoalition() ~= self.coalition then
local name = _unit:GetName() or "none"
DetectedUnits[name]=_unit
RecceDetecting[name]=reccename
self:T("Unit name = "..name)
end
end
end
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM Events
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@@ -2306,7 +2144,7 @@ function INTEL:GetClusterCoordinate(Cluster, Update)
return Cluster.coordinate
end
--- Check if the coordindate of the cluster changed.
--- Check if the coorindate of the cluster changed.
-- @param #INTEL self
-- @param #INTEL.Cluster Cluster The cluster.
-- @param #number Threshold in meters. Default 100 m.
+17 -16
View File
@@ -447,6 +447,7 @@ OPSGROUP.TaskType={
-- @field Wrapper.Marker#MARKER marker Marker on the F10 map.
-- @field #string formation Ground formation. Similar to action but on/off road.
-- @field #number missionUID Mission UID (Auftragsnr) this waypoint belongs to.
-- @field Navigation.FlightPlan#FLIGHTPLAN flightplan Flightplan this waypoint belongs to.
--- Cargo Carrier status.
-- @type OPSGROUP.CarrierStatus
@@ -575,6 +576,11 @@ function OPSGROUP:New(group)
-- Set DCS group and controller.
self.dcsgroup=self:GetDCSGroup()
if not self.dcsgroup then
return
end
self.controller=self.dcsgroup:getController()
-- Category.
@@ -2741,15 +2747,6 @@ function OPSGROUP:IsOutOfTorpedos()
return self.outofTorpedos
end
--- Check if the group is out of A2G Ammo
-- @param #OPSGROUP self
-- @return #boolean If `true`, group is out of torpedos.
function OPSGROUP:IsOutOfA2GAmmo()
if (self.outofMissilesAG and self.outofBombs and self.outofGuns) or self.outofAmmo then
return true
end
return false
end
--- Check if the group has currently switched a LASER on.
-- @param #OPSGROUP self
@@ -11420,9 +11417,9 @@ function OPSGROUP:_SimpleTaskFunction(Function, uid)
return DCSTask
end
--- Enhance waypoint table.
--- Enhanced waypoint table.
-- @param #OPSGROUP self
-- @param #OPSGROUP.Waypoint Waypoint data.
-- @param #OPSGROUP.Waypoint waypoint Waypoint data.
-- @return #OPSGROUP.Waypoint Modified waypoint data.
function OPSGROUP:_CreateWaypoint(waypoint)
@@ -12850,7 +12847,7 @@ function OPSGROUP:_UpdatePosition()
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()
self.velocityLast=self.velocity or self.group:GetVelocityMPS()
-- Current state.
self.position=self:GetVec3()
@@ -13696,20 +13693,24 @@ end
-- @return Core.Point#COORDINATE The coordinate of the object.
function OPSGROUP:_CoordinateFromObject(Object)
env.info("FF coordfrom object")
if Object then
if Object:IsInstanceOf("COORDINATE") then
if VECTOR._IsVector(Object) then
env.info("FF VECTOR")
return Object:GetCoordinate()
elseif Object:IsInstanceOf("COORDINATE") then
return Object
else
if Object:IsInstanceOf("POSITIONABLE") or Object:IsInstanceOf("ZONE_BASE") then
self:T(self.lid.."WARNING: Coordinate is not a COORDINATE but a POSITIONABLE or ZONE. Trying to get coordinate")
self:E(self.lid.."WARNING: Coordinate is not a COORDINATE but a POSITIONABLE or ZONE. Trying to get coordinate")
local coord=Object:GetCoordinate()
return coord
else
self:T(self.lid.."ERROR: Coordinate is neither a COORDINATE nor any POSITIONABLE or ZONE!")
self:E(self.lid.."ERROR: Coordinate is neither a COORDINATE nor any POSITIONABLE or ZONE!")
end
end
else
self:T(self.lid.."ERROR: Object passed is nil!")
self:E(self.lid.."ERROR: Object passed is nil!")
end
return nil
+28 -313
View File
@@ -21,7 +21,7 @@
-- ===
-- @module Ops.PlayerTask
-- @image OPS_PlayerTask.jpg
-- @date Last Update Dec 2025
-- @date Last Update Oct 2025
do
@@ -61,7 +61,6 @@ do
-- @field #number PreviousCount
-- @field #boolean CanSmoke
-- @field #boolean ShowThreatDetails
-- @field #boolean PersistMe
-- @extends Core.Fsm#FSM
@@ -99,12 +98,11 @@ PLAYERTASK = {
PreviousCount = 0,
CanSmoke = true,
ShowThreatDetails = true,
PersistMe = false,
}
--- PLAYERTASK class version.
-- @field #string version
PLAYERTASK.version="0.1.31"
PLAYERTASK.version="0.1.29"
--- Generic task condition.
-- @type PLAYERTASK.Condition
@@ -403,17 +401,8 @@ function PLAYERTASK:CanJoinTask(Group, Client)
return true
end
--- [User] Set this task for persistance, if persistance is enabled on the PLAYERTASKCONTROLLER instance.
-- @param #PLAYERTASK self
-- @return #PLAYERTASK self
function PLAYERTASK:EnablePersistance()
self.PersistMe = true
return self
end
--- [Internal] Add a PLAYERTASKCONTROLLER for this task
-- @param #PLAYERTASK self
--
-- @param Ops.PlayerTask#PLAYERTASKCONTROLLER Controller
-- @return #PLAYERTASK self
function PLAYERTASK:_SetController(Controller)
@@ -1099,6 +1088,7 @@ function PLAYERTASK:onafterStatus(From, Event, To)
return self
end
--- [Internal] On after progress call
-- @param #PLAYERTASK self
-- @param #string From
@@ -1373,12 +1363,6 @@ do
-- @field Core.ClientMenu#CLIENTMENU MenuNoTask
-- @field #boolean InformationMenu Show Radio Info Menu
-- @field #number TaskInfoDuration How long to show the briefing info on the screen
-- @field #table TaskPersistance Table for persistance data
-- @field #boolean TaskPersistanceSwitch Switch for persisting tasks
-- @field #string TaskPersistancePath File path for persisting tasks
-- @field #string TaskPersistanceFilename File name for persisting tasks
-- @field #table TasksPersistable List of persistable tasks
-- @field #number SceneryExplosivesAmount Kgs of TNT to explode scenery on task persistance loading
-- @extends Core.Fsm#FSM
---
@@ -1685,28 +1669,8 @@ do
--
-- Set a marker on the map and add the following text to create targets from it: "TARGET". This is effectively the same as adding a COORDINATE object as target.
-- The marker can be deleted any time.
--
-- ## 9 Single Task Persistence for mission designer added tasks
--
-- The class can persist the state of single tasks of type BOMBING, PRECISIONBOMBING, ARTY and SEAD, i.e. tasks which have a GROUND(!) GROUP, UNIT, STATIC or SCENERY as target.
-- This requires the task to have a unique(!) menu name set, a TARGET which already exists on the map at mission start(!), and a flag that this task is actually to be persisted.
-- Also, you need to desanitize the mission scripting environment, i.e. "lfs" and "io" must be available so we can write to disk.
--
-- -- First, we need to enable on the PLAYERTASKCONTROLLER itself
-- taskmanager:EnableTaskPersistance([[C:\Users\myname\Saved Games\DCS\Missions\MyMisionFolder\]],"Mission Tasks.csv") -- Path and Filename
--
-- -- Then, we can design a task marking mission progress that we want to persist
-- local RussianRadios = SET_STATIC:New():FilterPrefixes("Comms Tower Russia"):FilterOnce()
--
-- local RadioTask = PLAYERTASK:New(AUFTRAG.Type.BOMBING,RussianRadios,true,5,"Bombing")
-- RadioTask:SetMenuName("Neutralize Comms Towers") -- UNIQUE menu name so we can find the task later!
-- RadioTask:AddFreetext("Find and neutralize the two communication towers near NB70 East of Fulda on Streufelsberg!")
-- RadioTask:AddFreetextTTS("Find and neutralize the two communication towers naer N;B;7;zero; East of Fulda on Streufelsberg!")
-- RadioTask:EnablePersistance() -- Enable persistence for this task
--
-- taskmanager:AddPlayerTaskToQueue(RadioTask,true,false)
--
-- ## 10 Discussion
--
-- ## 9 Discussion
--
-- If you have questions or suggestions, please visit the [MOOSE Discord](https://discord.gg/AeYAkHP) #ops-playertask channel.
--
@@ -1756,12 +1720,6 @@ PLAYERTASKCONTROLLER = {
MenuNoTask = nil,
InformationMenu = false,
TaskInfoDuration = 30,
TaskPersistance = {},
TaskPersistanceSwitch = false,
TaskPersistancePath = nil,
TaskPersistanceFilename = nil,
TasksPersistable = {},
SceneryExplosivesAmount = 300,
}
---
@@ -1782,7 +1740,6 @@ AUFTRAG.Type.PRECISIONBOMBING = "Precision Bombing"
AUFTRAG.Type.CTLD = "Combat Transport"
AUFTRAG.Type.CSAR = "Combat Rescue"
AUFTRAG.Type.CONQUER = "Conquer"
---
-- @type Scores
PLAYERTASKCONTROLLER.Scores = {
@@ -1802,24 +1759,6 @@ PLAYERTASKCONTROLLER.Scores = {
[AUFTRAG.Type.CAP] = 100,
[AUFTRAG.Type.CAPTUREZONE] = 100,
}
---
-- @type TasksPersistable
PLAYERTASKCONTROLLER.TasksPersistable = {
[AUFTRAG.Type.PRECISIONBOMBING] = true,
[AUFTRAG.Type.BOMBING] = true,
[AUFTRAG.Type.ARTY] = true,
[AUFTRAG.Type.SEAD] = true,
}
---
-- @type PersistenceData
-- @field #number ID
-- @field #string Name
-- @field #string Type
-- @field #number InitialTargets
-- @field #number Targetsleft
-- @field #boolean updated
---
-- @type SeadAttributes
@@ -2120,7 +2059,7 @@ function PLAYERTASKCONTROLLER:New(Name, Coalition, Type, ClientFilter)
self:AddTransition("*", "Stop", "Stopped")
self:__Start(2)
local starttime = math.random(10,15)
local starttime = math.random(5,10)
self:__Status(starttime)
self:I(self.lid..self.version.." Started.")
@@ -2235,30 +2174,6 @@ function PLAYERTASKCONTROLLER:New(Name, Coalition, Type, ClientFilter)
end
--- [User] Enable Task persistance (for specific gound target tasks)
-- @param #PLAYERTASKCONTROLLER self
-- @param #string Path Path where to save the task data
-- @param #string Filename File name under which to save the task data
-- @param #number KgsOfTNT (Optional) Explosives kgs used to remove scenery for persistence, defaults to 300
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:EnableTaskPersistance(Path,Filename,KgsOfTNT)
self.TaskPersistanceSwitch = true
self.TaskPersistancePath = Path
self.TaskPersistanceFilename = Filename
self.SceneryExplosivesAmount = KgsOfTNT or 300
return self
end
--- [User] Disable Task persistance (for specific gound target tasks)
-- @param #PLAYERTASKCONTROLLER self
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:DisableTaskPersistance()
self.TaskPersistanceSwitch = false
self.TaskPersistancePath = nil
self.TaskPersistanceFilename = nil
return self
end
--- [User] Set or create a SCORING object for this taskcontroller
-- @param #PLAYERTASKCONTROLLER self
-- @param Functional.Scoring#SCORING Scoring (optional) the Scoring object
@@ -2946,6 +2861,15 @@ function PLAYERTASKCONTROLLER:_GetTasksPerType()
end
end
--[[
for _type,_data in pairs(tasktypes) do
self:I("Task Type: ".._type)
for _id,_task in pairs(_data) do
self:I("Task Name: ".._task.Target:GetName())
end
end
--]]
return tasktypes
end
@@ -3642,9 +3566,9 @@ function PLAYERTASKCONTROLLER:AddPlayerTaskToQueue(PlayerTask,Silent,TaskFilter)
PlayerTask:_SetController(self)
PlayerTask:SetCoalition(self.Coalition)
self.TaskQueue:Push(PlayerTask)
--if not Silent then
self:__TaskAdded(10,PlayerTask,Silent)
--end
if not Silent then
self:__TaskAdded(10,PlayerTask)
end
else
self:E(self.lid.."***** NO valid PAYERTASK object sent!")
end
@@ -3787,7 +3711,7 @@ function PLAYERTASKCONTROLLER:_FlashInfo()
local task = self.TasksPerPlayer:ReadByID(_playername) -- Ops.PlayerTask#PLAYERTASK
local Coordinate = task.Target:GetCoordinate()
local CoordText = ""
if self.Type ~= PLAYERTASKCONTROLLER.Type.A2A and task.Type~=AUFTRAG.Type.INTERCEPT then
if self.Type ~= PLAYERTASKCONTROLLER.Type.A2A then
CoordText = Coordinate:ToStringA2G(_client, nil, self.ShowMagnetic)
else
CoordText = Coordinate:ToStringA2A(_client, nil, self.ShowMagnetic)
@@ -3846,7 +3770,7 @@ function PLAYERTASKCONTROLLER:_ActiveTaskInfo(Task, Group, Client)
local CoordTextLLDM = nil
local ShowThreatInfo = task.ShowThreatDetails
local LasingDrone = self:_FindLasingDroneForTaskID(task.PlayerTaskNr)
if self.Type ~= PLAYERTASKCONTROLLER.Type.A2A and task.Type~=AUFTRAG.Type.INTERCEPT then
if self.Type ~= PLAYERTASKCONTROLLER.Type.A2A then
CoordText = Coordinate:ToStringA2G(Client,nil,self.ShowMagnetic)
else
CoordText = Coordinate:ToStringA2A(Client,nil,self.ShowMagnetic)
@@ -4660,76 +4584,6 @@ function PLAYERTASKCONTROLLER:RemoveConflictZone(ConflictZone)
return self
end
--- [User] Add an corridor zone to INTEL detection. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this.
-- @param #PLAYERTASKCONTROLLER self
-- @param Core.Zone#ZONE CorridorZone Add a zone to the corridor zone set.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:AddCorridorZone(CorridorZone)
self:T(self.lid.."AddCorridorZone")
if self.Intel then
self.Intel:AddCorridorZone(CorridorZone)
else
self:E(self.lid.."*****NO detection has been set up (yet)!")
end
return self
end
--- [User] Add an corridor SET_ZONE to INTEL detection. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this.
-- @param #PLAYERTASKCONTROLLER self
-- @param Core.Set#SET_ZONE CorridorZoneSet Add a SET_ZONE to the corridor zone set.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:AddCorridorZoneSet(CorridorZoneSet)
self:T(self.lid.."AddCorridorZoneSet")
if self.Intel then
self.Intel.corridorzoneset:AddSet(CorridorZoneSet)
else
self:E(self.lid.."*****NO detection has been set up (yet)!")
end
return self
end
--- [User] Remove an corridor zone from INTEL detection. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this.
-- @param #PLAYERTASKCONTROLLER self
-- @param Core.Zone#ZONE CorridorZone Remove this zone from the corridor zone set.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:RemoveCorridorZone(CorridorZone)
self:T(self.lid.."RemoveCorridorZone")
if self.Intel then
self.Intel:RemoveCorridorZone(CorridorZone)
else
self:E(self.lid.."*****NO detection has been set up (yet)!")
end
return self
end
--- Function to set corridor zone floor and ceiling in FEET.
-- @param #PLAYERTASKCONTROLLER self
-- @param #number Floor Floor altitude ASL in feet.
-- @param #number Ceiling Ceiling altitude ASL in feet.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:SetCorridorZoneFloorAndCeiling(Floor,Ceiling)
if self.Intel then
self.Intel:SetCorridorLimitsFeet(Floor,Ceiling)
else
self:E(self.lid.."*****NO detection has been set up (yet)!")
end
return self
end
--- Function to set corridor zone floor and ceiling in METERS.
-- @param #PLAYERTASKCONTROLLER self
-- @param #number Floor Floor altitude ASL in meters.
-- @param #number Ceiling Ceiling altitude ASL in meters.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:SetCorridorZoneFloorAndCeilingMeters(Floor,Ceiling)
if self.Intel then
self.Intel:SetCorridorLimits(Floor,Ceiling)
else
self:E(self.lid.."*****NO detection has been set up (yet)!")
end
return self
end
--- [User] Set the top menu name to a custom string.
-- @param #PLAYERTASKCONTROLLER self
-- @param #string Name The name to use as the top menu designation.
@@ -4911,128 +4765,6 @@ function PLAYERTASKCONTROLLER:SetSRSBroadcast(Frequency,Modulation)
return self
end
---
-- @param #PLAYERTASKCONTROLLER self
-- @param Ops.PlayerTask#PLASERTASK Task
-- @param #number TargetsLeft
function PLAYERTASKCONTROLLER:_UpdateTargetsAlive(Task,TargetsLeft)
self:T(self.lid.."_UpdateTargetsAlive")
local delta = Task.Target:CountTargets() - TargetsLeft
if delta > 0 then
self:T("Delta targets to be removed: "..delta)
local count = 0
local targets = Task.Target:GetObjects()
for _,_object in pairs(targets or {}) do
if _object and _object.ClassName and (_object:IsInstanceOf("GROUP") or _object:IsInstanceOf("UNIT") or _object:IsInstanceOf("STATIC") or _object:IsInstanceOf("SCENERY")) then
if count < delta then
count = count + 1
if not _object:IsInstanceOf("SCENERY") then
_object:Destroy(true)
else
_object:Explode(self.SceneryExplosivesAmount)
end
end
end
end
end
return self
end
---
-- @param #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:_LoadTasksPersisted()
self:T(self.lid.."_LoadTasksPersisted")
local function MatchTask(Type,Name)
local foundtask
self.TaskQueue:ForEach(
function(_task)
local task = _task -- #PLAYERTASK
if task.Type == Type and task.Target.name and task.Target.name == Name then
foundtask = task
end
end
)
return foundtask
end
if lfs and io then
local ok,data = UTILS.LoadFromFile(self.TaskPersistancePath,self.TaskPersistanceFilename)
if ok == true then
table.remove(data, 1)
for _,_entry in pairs(data) do
-- "--ID;;Name;;InitialTargets;;Targetsleft;;Type\n"
local dataset = UTILS.Split(_entry,";;")
local Taskdata = {} -- #PersistenceData
Taskdata.ID = tonumber(dataset[1])
Taskdata.Name = tostring(dataset[2])
Taskdata.InitialTargets = tonumber(dataset[3])
Taskdata.Targetsleft = tonumber(dataset[4])
Taskdata.Type = tostring(dataset[5])
Taskdata.Task = MatchTask(Taskdata.Type,Taskdata.Name)
if Taskdata.Task == nil then
self:E(self.lid.."No actual task found for "..Taskdata.Name)
else
self:T(self.lid.."Task loaded and match found for "..Taskdata.Name)
end
Taskdata.updated = Taskdata.InitialTargets == Taskdata.Targetsleft and true or false
if Taskdata.Task and Taskdata.updated == false then
self:_UpdateTargetsAlive(Taskdata.Task,Taskdata.Targetsleft)
Taskdata.updated = true
end
self.TaskPersistance[Taskdata.ID] = Taskdata
end
end
end
return self
end
--- [User] Clear persisted data on disk.
-- @param #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:ClearPersistedData()
if lfs and io then
local text = "-- Data Cleared\n"
UTILS.SaveToFile(self.TaskPersistancePath,self.TaskPersistanceFilename,text)
end
return self
end
---
-- @param #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:_SaveTasksPersisted()
if lfs and io then
local text = "--ID;;Name;;InitialTargets;;Targetsleft;;Type\n"
for _,_data in pairs(self.TaskPersistance) do
local data = _data -- #PersistenceData
data.Targetsleft = data.Task.Target:CountTargets() -- recount
if data.Task and data.Task:IsDone() then data.Targetsleft = 0 end
local tasktext = string.format("%d;;%s;;%d;;%d;;%s\n",data.ID,data.Name,data.InitialTargets,data.Targetsleft,data.Type)
text = text..tasktext
end
UTILS.SaveToFile(self.TaskPersistancePath,self.TaskPersistanceFilename,text)
end
return self
end
---
-- @param #PLAYERTASKCONTROLLER self
-- @param #PLAYERTASK Task
function PLAYERTASKCONTROLLER:_AddPersistenceData(Task)
local Taskdata = {} -- #PersistenceData
if not self.TaskPersistance[Task.PlayerTaskNr] then
Taskdata.ID = Task.PlayerTaskNr
Taskdata.Name = Task.Target.name or "none"
Taskdata.InitialTargets = Task.Target:CountTargets()
Taskdata.Targetsleft = Taskdata.InitialTargets
Taskdata.Type = Task.Type
Taskdata.updated = true
Taskdata.Task = Task
self.TaskPersistance[Task.PlayerTaskNr] = Taskdata
end
return self
end
-------------------------------------------------------------------------------------------------------------------
-- FSM Functions PLAYERTASKCONTROLLER
-- TODO: FSM Functions PLAYERTASKCONTROLLER
@@ -5056,12 +4788,7 @@ function PLAYERTASKCONTROLLER:onafterStart(From, Event, To)
self:HandleEvent(EVENTS.PilotDead, self._EventHandler)
self:HandleEvent(EVENTS.PlayerEnterAircraft, self._EventHandler)
self:HandleEvent(EVENTS.UnitLost, self._EventHandler)
self:SetEventPriority(5)
-- Persistence
if self.TaskPersistanceSwitch == true then
self:ScheduleOnce(5,self._LoadTasksPersisted,self)
--self:_LoadTasksPersisted()
end
self:SetEventPriority(5)
return self
end
@@ -5101,11 +4828,6 @@ function PLAYERTASKCONTROLLER:onafterStatus(From, Event, To)
self:I(text)
end
-- Persistence
if self.TaskPersistanceSwitch == true then
self:_SaveTasksPersisted()
end
if self:GetState() ~= "Stopped" then
self:__Status(-30)
end
@@ -5261,26 +4983,19 @@ end
-- @param #string Event
-- @param #string To
-- @param Ops.PlayerTask#PLAYERTASK Task
-- @param #boolean Silent
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:onafterTaskAdded(From, Event, To, Task, Silent)
function PLAYERTASKCONTROLLER:onafterTaskAdded(From, Event, To, Task)
self:T({From, Event, To})
self:T(self.lid.."TaskAdded")
local addtxt = self.gettext:GetEntry("TASKADDED",self.locale)
local taskname = string.format(addtxt, self.MenuName or self.Name, tostring(Task.Type))
if not Silent then
if not self.NoScreenOutput then
self:_SendMessageToClients(taskname,15)
--local m = MESSAGE:New(taskname,15,"Tasking"):ToCoalition(self.Coalition)
end
if self.UseSRS then
taskname = string.format(addtxt, self.MenuName or self.Name, tostring(Task.TTSType))
self.SRSQueue:NewTransmission(taskname,nil,self.SRS,nil,2)
end
if not self.NoScreenOutput then
self:_SendMessageToClients(taskname,15)
--local m = MESSAGE:New(taskname,15,"Tasking"):ToCoalition(self.Coalition)
end
self:T(self.lid..string.format("Pers = %s | Type = %s | TypePers = %s | TaskFlag = %s",tostring(self.TaskPersistanceSwitch),tostring(Task.Type),tostring(self.TasksPersistable[Task.Type]),tostring(Task.PersistMe)))
if self.TaskPersistanceSwitch == true and self.TasksPersistable[Task.Type] == true and Task.PersistMe == true then
self:_AddPersistenceData(Task)
if self.UseSRS then
taskname = string.format(addtxt, self.MenuName or self.Name, tostring(Task.TTSType))
self.SRSQueue:NewTransmission(taskname,nil,self.SRS,nil,2)
end
return self
end
+5 -4
View File
@@ -884,7 +884,7 @@ function RESCUEHELO:onafterStart(From, Event, To)
else
-- Spawn helo. We need to introduce an alias in case this class is used twice. This would confuse the spawn routine.
Spawn=SPAWN:NewWithAlias(self.helogroupname, self.alias)
local Spawn=SPAWN:NewWithAlias(self.helogroupname, self.alias)
-- Set modex for spawn.
Spawn:InitModex(self.modex)
@@ -933,8 +933,8 @@ function RESCUEHELO:onafterStart(From, Event, To)
self:E(string.format("ERROR: No uncontrolled (alive) rescue helo group with name %s could be found!", self.helogroupname))
return
end
end
elseif UsesAliveGroup==false then
elseif UsesAliveGroup==false then
-- Spawn at airbase.
self.helo=Spawn:SpawnAtAirbase(self.airbase, self.takeoff, nil, AIRBASE.TerminalType.HelicopterUsable)
@@ -948,8 +948,9 @@ function RESCUEHELO:onafterStart(From, Event, To)
delay=60
end
end
end
end
-- Set of group(s) to follow Mother.
self.followset=SET_GROUP:New()
+1 -27
View File
@@ -515,8 +515,7 @@ ENUMS.ReportingName =
Atlas = "A400",
Lancer = "B1-B",
Stratofortress = "B-52H",
Herc = "C-130",
Hercules = "C-130J-30",
Hercules = "C-130",
Super_Hercules = "Hercules",
Globemaster = "C-17",
Greyhound = "C-2A",
@@ -529,7 +528,6 @@ ENUMS.ReportingName =
Orion = "P-3C",
Viking = "S-3B",
Osprey = "V-22",
Intruder = "A6E",
-- Bomber Rus
Badger = "H6-J",
Bear_J = "Tu-142", -- also Tu-95
@@ -1774,30 +1772,6 @@ ENUMS.Storage.weapons.adapters.lau105 = 'weapons.adapters.lau-105'
ENUMS.Storage.weapons.containers.US_M10_SMOKE_TANK_WHITE = 'weapons.containers.{US_M10_SMOKE_TANK_WHITE}'
ENUMS.Storage.weapons.bombs.Mk_82 = 'weapons.bombs.Mk_82'
ENUMS.Storage.weapons.adapters.BRU42_LS_SUU25 = 'weapons.adapters.BRU-42_LS_(SUU-25)'
ENUMS.Storage.weapons.missiles.Aster_30_Blk_1 = 'weapons.missiles.Aster 30 Blk 1'
ENUMS.Storage.weapons.missiles.Aster_30_Blk_1NT = 'weapons.missiles.Aster 30 Blk 1NT'
ENUMS.Storage.weapons.missiles.Aster_30_Blk_2 = 'weapons.missiles.Aster 30 Blk 2'
ENUMS.Storage.weapons.missiles.SA9M83M = 'weapons.missiles.SA9M83M'
ENUMS.Storage.weapons.gunmounts.C130_M4_Rifle = 'weapons.gunmounts.C130_M4_Rifle'
ENUMS.Storage.weapons.gunmounts.C130_M18_Sidearm_ = 'weapons.gunmounts.{C130-M18-Sidearm}'
ENUMS.Storage.weapons.gunmounts.C130_Cargo_Bay_M4 = 'weapons.gunmounts.{C130-Cargo-Bay-M4}'
ENUMS.Storage.weapons.gunmounts.C130_M18_Sidearm = 'weapons.gunmounts.C130_M18_Sidearm'
ENUMS.Storage.weapons.droptanks.C130J_Ext_Tank_R = 'weapons.droptanks.C130J_Ext_Tank_R'
ENUMS.Storage.weapons.droptanks.C130J_Ext_Tank_L = 'weapons.droptanks.C130J_Ext_Tank_L'
ENUMS.Storage.weapons.missiles.SAHQ2 = 'weapons.missiles.SAHQ2'
ENUMS.Storage.weapons.missiles.Strela_2 = 'weapons.missiles.Strela-2'
ENUMS.Storage.weapons.missiles.Strela_2M = 'weapons.missiles.Strela-2M'
ENUMS.Storage.weapons.missiles.Strela_3 = 'weapons.missiles.Strela-3'
ENUMS.Storage.weapons.missiles.SA9M83 = 'weapons.missiles.SA9M83'
ENUMS.Storage.weapons.missiles.SAV601P = 'weapons.missiles.SAV601P'
ENUMS.Storage.weapons.missiles.SA2V759 = 'weapons.missiles.SA2V759'
ENUMS.Storage.weapons.missiles.SA9M317 = 'weapons.missiles.SA9M317'
ENUMS.Storage.weapons.missiles.SA9M82M = 'weapons.missiles.SA9M82M'
ENUMS.Storage.weapons.missiles.SA9M82 = 'weapons.missiles.SA9M82'
ENUMS.Storage.weapons.missiles.Igla_S = 'weapons.missiles.Igla_S'
ENUMS.Storage.weapons.gunmounts.AKAN_NO_TRC = 'weapons.gunmounts.{AKAN_NO_TRC}'
ENUMS.Storage.weapons.gunmounts.AKAN = 'weapons.gunmounts.{AKAN}'
ENUMS.Storage.weapons.shells.M882_9x19 = 'weapons.shells.9x19_m882'
-- UH-60L Mod
ENUMS.Storage.weapons.gunmounts.UH60LGAU19 = 'weapons.gunmounts.UH-60L GAU-19'
+98 -205
View File
@@ -249,23 +249,7 @@ CALLSIGN={
Stetson = 22,
Wrath = 23,
},
Intruder = {
Raygun = 4,
Heartless = 5,
Viceroy = 6,
Cupcake = 7,
["Flying Tiger"] = 8,
["Flying Ace"] = 9,
Buckeye = 10,
Goldplate = 11,
Phoenix = 12,
Electron = 13,
Rustler = 14,
Vixen = 15,
Jackal = 16,
Milestone = 17,
Devil = 18,
},
} --#CALLSIGN
--- Utilities static class.
@@ -821,6 +805,33 @@ UTILS.kg2lbs = function( kg )
return kg * 2.20462
end
--- Convert latitude or longitude from degrees, minutes, seconds (DMS) to decimal degrees (DD).
-- @param #number Degrees Degrees in grad.
-- @param #number Minutes Minutes.
-- @param #number Seconds Seconds.
-- @return #number Latitude or Longitude in decimal degrees.
UTILS.LLDMSToDD = function(Degrees, Minutes, Seconds)
local dd=tonumber(Degrees or 0) + tonumber(Minutes or 0)/60 + tonumber(Seconds or 0)/3600
return dd
end
--- Convert latitude or longitude from degrees, minutes, seconds (DMS) given in text form to decimal degrees (DD).
-- @param #string LatOrLongString Latitude or longitude passed as ttring in format `DD°MM'SS.SS"`.
-- @return #number Latitude or Longitude in decimal degrees.
UTILS.LLDMSstringToDD = function(LatOrLongString)
local hem=string.match(LatOrLongString, "(%a)")
local Degrees=string.match(LatOrLongString, "(%d+)°")
local Minutes=string.match(LatOrLongString, "(%d+)'")
local Seconds=string.match(LatOrLongString, "([%d\.]+)\"")
local dd=UTILS.LLDMSToDD(Degrees, Minutes, Seconds)
return dd
end
--[[acc:
in DM: decimal point of minutes.
In DMS: decimal point of seconds.
@@ -1541,6 +1552,22 @@ function UTILS.Vec2Add(a, b)
return {x=a.x+b.x, y=a.y+b.y}
end
--- Multiply 2D vector by a scalar value.
-- @param DCS#Vec2 a Vector in 2D with x, y components.
-- @param #number c Scalar value.
-- @return DCS#Vec2 Vector
function UTILS.Vec2Mult(a, c)
return {x=c*a.x, y=c*a.y}
end
--- Multiply 3D vector by a scalar value.
-- @param DCS#Vec3 a Vector in 3D with x, y, z components.
-- @param #number c Scalar value.
-- @return DCS#Vec3 Vector
function UTILS.VecMult(a, c)
return {x=c*a.x, y=c*a.y, z=c*a.z}
end
--- Calculate the angle between two 3D vectors.
-- @param DCS#Vec3 a Vector in 3D with x, y, z components.
-- @param DCS#Vec3 b Vector in 3D with x, y, z components.
@@ -1561,6 +1588,24 @@ function UTILS.VecAngle(a, b)
return math.deg(alpha)
end
--- Calculate the angle between two 3D vectors.
-- @param DCS#Vec3 a Vector in 3D with x, y, z components.
-- @param DCS#Vec3 b Vector in 3D with x, y, z components.
-- @return #number Angle alpha between and b in degrees.
function UTILS.VecAngleSigned(a, b)
local a=UTILS.VecSubstract(s1.p2.vec3, s1.p1.vec3)
local b=UTILS.VecSubstract(s2.p2.vec3, s2.p1.vec3)
local h1=UTILS.VecHdg(a)
local h2=UTILS.VecHdg(b)
local angle=h1-h2 --UTILS.VecAngle(a, b)
end
--- Calculate "heading" of a 3D vector in the X-Z plane.
-- @param DCS#Vec3 a Vector in 3D with x, y, z components.
-- @return #number Heading in degrees in [0,360).
@@ -1583,6 +1628,20 @@ function UTILS.Vec2Hdg(a)
return h
end
--- Calculate the difference between two "heading", i.e. angles in [0,360) deg.
-- @param DCS#Vec3 a Vector a.
-- @param DCS#Vec3 a Vector b.
-- @return #number Heading difference in degrees.
function UTILS.VecHdgDiff(a, b)
local ha=math.deg(math.atan2(a.z, a.x))
local hb=math.deg(math.atan2(b.z, b.x))
local angle=ha-hb
return angle
end
--- Calculate the difference between two "heading", i.e. angles in [0,360) deg.
-- @param #number h1 Heading one.
-- @param #number h2 Heading two.
@@ -2030,12 +2089,6 @@ function UTILS.GetCallsignName(Callsign)
end
end
for name, value in pairs(CALLSIGN.Intruder) do
if value==Callsign then
return name
end
end
return "Ghostrider"
end
@@ -2322,21 +2375,6 @@ function UTILS.IsLoadingDoorOpen( unit_name )
return true
end
if type_name=="C-130J-30" and (unit:getDrawArgumentValue(86)==1)then
BASE:T(unit_name.." rear doors are open")
return true
end
if type_name=="C-130J-30" and (unit:getDrawArgumentValue(87)==1)then
BASE:T(unit_name.." Side door(s) are open")
return true
end
if type_name=="C-130J-30" and (unit:getDrawArgumentValue(88)==1)then
BASE:T(unit_name.." Paratroop door(s) are open")
return true
end
if string.find(type_name, "Hercules") and (unit:getDrawArgumentValue(1215) == 1 and unit:getDrawArgumentValue(1216) == 1) then
BASE:T(unit_name .. " rear doors are open")
return true
@@ -4259,27 +4297,25 @@ function UTILS.SpawnFARPAndFunctionalStatics(Name,Coordinate,FARPType,Coalition,
local function PopulateStorage(Name,liquids,equip,airframes)
local newWH = STORAGE:New(Name)
if newWH then
if liquids and liquids > 0 then
-- Storage fill-up
newWH:SetLiquid(STORAGE.Liquid.DIESEL,liquids) -- kgs to tons
newWH:SetLiquid(STORAGE.Liquid.GASOLINE,liquids)
newWH:SetLiquid(STORAGE.Liquid.JETFUEL,liquids)
newWH:SetLiquid(STORAGE.Liquid.MW50,liquids)
end
if equip and equip > 0 then
for cat,nitem in pairs(ENUMS.Storage.weapons) do
for name,item in pairs(nitem) do
newWH:SetItem(item,equip)
end
if liquids and liquids > 0 then
-- Storage fill-up
newWH:SetLiquid(STORAGE.Liquid.DIESEL,liquids) -- kgs to tons
newWH:SetLiquid(STORAGE.Liquid.GASOLINE,liquids)
newWH:SetLiquid(STORAGE.Liquid.JETFUEL,liquids)
newWH:SetLiquid(STORAGE.Liquid.MW50,liquids)
end
if equip and equip > 0 then
for cat,nitem in pairs(ENUMS.Storage.weapons) do
for name,item in pairs(nitem) do
newWH:SetItem(item,equip)
end
end
if airframes and airframes > 0 then
for typename in pairs (CSAR.AircraftType) do
newWH:SetItem(typename,airframes)
end
end
if airframes and airframes > 0 then
for typename in pairs (CSAR.AircraftType) do
newWH:SetItem(typename,airframes)
end
end
end
@@ -4358,6 +4394,8 @@ function UTILS.SpawnFARPAndFunctionalStatics(Name,Coordinate,FARPType,Coalition,
}
-- Create BIRTH event.
world.onEvent(Event)
PopulateStorage(Name.."-1",liquids,equip,airframes)
else
-- Spawn FARP
local newfarp = SPAWNSTATIC:NewFromType(STypeName,"Heliports",Country) -- "Invisible FARP" "FARP"
@@ -4365,11 +4403,10 @@ function UTILS.SpawnFARPAndFunctionalStatics(Name,Coordinate,FARPType,Coalition,
newfarp:InitFARP(callsign,freq,mod,DynamicSpawns,HotStart)
local spawnedfarp = newfarp:SpawnFromCoordinate(farplocation,0,Name)
table.insert(ReturnObjects,spawnedfarp)
end
PopulateStorage(Name,liquids,equip,airframes)
PopulateStorage(Name,liquids,equip,airframes)
end
-- Spawn Objects
local FARPStaticObjectsNato = {
["FUEL"] = { TypeName = "FARP Fuel Depot", ShapeName = "GSM Rus", Category = "Fortifications"},
@@ -5312,147 +5349,3 @@ function UTILS.CreateAirbaseEnum()
_savefile(string.format("%s-enums.txt", env.mission.theatre), text)
--env.info(text)
end
--- Calculate then center and radius of a circle enclosing a list if DCS#Vec2 points.
-- @param #table points Table of DCS#Vec2 entries
-- @return DCS#Vec2 center DCS#Vec2
-- @return #number radius
function UTILS.GetCenterAndRadius(points)
if #points == 0 then
return nil, nil
end
-- Calculate centroid (average of all points)
local sumX, sumY = 0, 0
for _, p in ipairs(points) do
sumX = sumX + p.x
sumY = sumY + p.y
end
local center = {
x = sumX / #points,
y = sumY / #points
}
-- Find maximum distance from center to any point
local maxDist = 0
for _, p in ipairs(points) do
local dx = p.x - center.x
local dy = p.y - center.y
local dist = math.sqrt(dx*dx + dy*dy)
if dist > maxDist then
maxDist = dist
end
end
return center, maxDist
end
--- More accurate: Minimum bounding circle (Welzl's algorithm), calculate then center and radius of a circle enclosing a list if DCS#Vec2 points.
-- @param #table points Table of DCS#Vec2 entries
-- @return DCS#Vec2 center DCS#Vec2
-- @return #number radius
function UTILS.GetMinimumBoundingCircle(points)
if #points == 0 then
return nil, nil
end
-- Calculate distance between two points
local function distance(p1, p2)
local dx = p2.x - p1.x
local dy = p2.y - p1.y
return math.sqrt(dx*dx + dy*dy)
end
-- Circle from 2 points (diameter)
local function circleFrom2Points(p1, p2)
local center = {
x = (p1.x + p2.x) / 2,
y = (p1.y + p2.y) / 2
}
local radius = distance(p1, p2) / 2
return center, radius
end
-- Circle from 3 points (circumcircle)
local function circleFrom3Points(p1, p2, p3)
local ax, ay = p1.x, p1.y
local bx, by = p2.x, p2.y
local cx, cy = p3.x, p3.y
local d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by))
if math.abs(d) < 0.0001 then
-- Points are collinear, use 2-point circle
return circleFrom2Points(p1, p3)
end
local aSq = ax*ax + ay*ay
local bSq = bx*bx + by*by
local cSq = cx*cx + cy*cy
local ux = (aSq * (by - cy) + bSq * (cy - ay) + cSq * (ay - by)) / d
local uy = (aSq * (cx - bx) + bSq * (ax - cx) + cSq * (bx - ax)) / d
local center = {x = ux, y = uy}
local radius = distance(center, p1)
return center, radius
end
-- Check if point is inside circle
local function isInside(center, radius, point, tolerance)
tolerance = tolerance or 0.0001
return distance(center, point) <= radius + tolerance
end
-- Welzl's algorithm (recursive)
local function welzlHelper(pts, n, boundary)
-- Base cases
if n == 0 or #boundary == 3 then
if #boundary == 0 then
return {x = 0, y = 0}, 0
elseif #boundary == 1 then
return {x = boundary[1].x, y = boundary[1].y}, 0
elseif #boundary == 2 then
return circleFrom2Points(boundary[1], boundary[2])
else
return circleFrom3Points(boundary[1], boundary[2], boundary[3])
end
end
-- Pick a random point
local p = pts[n]
-- Get circle without this point
local center, radius = welzlHelper(pts, n - 1, boundary)
-- If point is inside, we're done
if isInside(center, radius, p) then
return center, radius
end
-- Otherwise, point must be on the boundary
local newBoundary = {}
for i = 1, #boundary do
newBoundary[i] = boundary[i]
end
table.insert(newBoundary, p)
return welzlHelper(pts, n - 1, newBoundary)
end
-- Shuffle points for better average performance
local pts = {}
for i, p in ipairs(points) do
pts[i] = {x = p.x, y = p.y}
end
-- Simple shuffle
for i = #pts, 2, -1 do
local j = math.random(1, i)
pts[i], pts[j] = pts[j], pts[i]
end
return welzlHelper(pts, #pts, {})
end
+186 -56
View File
@@ -27,11 +27,11 @@
-- @field #table parkingByID Parking spot data table with ID as key.
-- @field #table parkingWhitelist List of parking spot terminal IDs considered for spawning.
-- @field #table parkingBlacklist List of parking spot terminal IDs **not** considered for spawning.
-- @field Core.Zone#ZONE_RADIUS parkingCircle Minimum bounding circle enclosing all parking spots.
-- @field #table runways Runways of airdromes.
-- @field #AIRBASE.Runway runwayLanding Runway used for landing.
-- @field #AIRBASE.Runway runwayTakeoff Runway used for takeoff.
-- @field Wrapper.Storage#STORAGE storage The DCS warehouse storage.
-- @field #table taxiways Taxiways stored as PATHLINEs.
-- @extends Wrapper.Positionable#POSITIONABLE
--- Wrapper class to handle the DCS Airbase objects:
@@ -79,6 +79,7 @@ AIRBASE = {
[Airbase.Category.SHIP] = "Ship",
},
activerwyno = nil,
taxiways = {},
}
--- Airbases of the Caucasus map
@@ -1550,10 +1551,36 @@ AIRBASE.SpotStatus = {
RESERVED="Reserved",
}
--- ICAO Codes.
-- @type AIRBASE.ICAO
AIRBASE.ICAO = {
UGTB=AIRBASE.Caucasus.Tbilisi_Lochini,
UGKO=AIRBASE.Caucasus.Kutaisi,
UG5X=AIRBASE.Caucasus.Kobuleti,
UG24=AIRBASE.Caucasus.Soganlug,
UG27=AIRBASE.Caucasus.Vaziani,
UGSS=AIRBASE.Caucasus.Sukhumi_Babushara,
UG23=AIRBASE.Caucasus.Gudauta,
URSS=AIRBASE.Caucasus.Sochi_Adler,
URMO=AIRBASE.Caucasus.Beslan,
URMN=AIRBASE.Caucasus.Nalchik,
XRMF=AIRBASE.Caucasus.Mozdok,
URMM=AIRBASE.Caucasus.Mineralnye_Vody,
URKH=AIRBASE.Caucasus.Maykop_Khanskaya,
URKK=AIRBASE.Caucasus.Krasnodar_Pashkovsky,
URKL=AIRBASE.Caucasus.Krasnodar_Center,
URKG=AIRBASE.Caucasus.Gelendzhik,
URKN=AIRBASE.Caucasus.Novorossiysk,
URKW=AIRBASE.Caucasus.Krymsk,
URKA=AIRBASE.Caucasus.Anapa_Vityazevo,
}
--- Runway data.
-- @type AIRBASE.Runway
-- @field #string name Runway name.
-- @field #string idx Runway ID: heading 070° ==> idx="07". Mostly same as magheading.
-- @field #string idx Runway ID: heading 070° ==> idx="07".
-- @field #number heading True heading of the runway in degrees.
-- @field #number magheading Magnetic heading of the runway in degrees. This is what is marked on the runway.
-- @field #number length Length of runway in meters.
@@ -1663,10 +1690,6 @@ end
else
self:E(string.format("ERROR: Cound not get position Vec2 of airbase %s", AirbaseName))
end
if Nrunways>0 then
self:GetMinimumBoundingCircleFromParkingSpots()
end
-- Debug info.
self:T2(string.format("Registered airbase %s", tostring(self.AirbaseName)))
@@ -1742,14 +1765,47 @@ end
--- Find a AIRBASE in the _DATABASE using the name of an existing DCS Airbase.
-- @param #AIRBASE self
-- @param #string AirbaseName The Airbase Name.
-- @param #string AirbaseName The name of the airbase. Can also be the ICAO code or the airbase ID.
-- @return #AIRBASE self
function AIRBASE:FindByName( AirbaseName )
local AirbaseFound = _DATABASE:FindAirbase( AirbaseName )
if not AirbaseFound then
AirbaseFound=self:FindByICAO(AirbaseName)
end
if not AirbaseFound then
AirbaseFound=self:FindByID(AirbaseName)
end
return AirbaseFound
end
--- Find an AIRBASE in the _DATABASE using the [International Civil Aviation Organization](https://en.wikipedia.org/wiki/ICAO_airport_code) (ICAO) airport code.
-- The code consists of four characters. Typically, the first one or two letters of the ICAO code indicate the country and the remaining letters identify the airport.
--
-- **NOTE** that the ICAO code cannot be retrieved via the DCS API and has to be hard coded into the MOOSE code. Therefore, it is a rare occasion where
-- @param #AIRBASE self
-- @param #string AirbaseICAO The Airbase ICAO code.
-- @return #AIRBASE self
function AIRBASE:FindByICAO( AirbaseICAO )
if AirbaseICAO then
local name=AIRBASE.ICAO[AirbaseICAO]
env.info(string.format("FF ICAO=%s, Name=%s", tostring(AirbaseICAO), tostring(name)))
local Airbase=self:FindByName(name)
return Airbase
end
return nil
end
--- Find a AIRBASE in the _DATABASE by its ID.
-- @param #AIRBASE self
-- @param #number id Airbase ID.
@@ -2062,6 +2118,129 @@ function AIRBASE:IsShip()
return self.isShip
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Taxi ways
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Add a taxiway from a given PATHLINE.
-- @param #AIRBASE self
-- @param Core.Pathline#PATHLINE TaxiPathline Pathline of the taxi way or name of pathline as #string.
-- @param #string Name Name of the taxi way, *e.g.* "Alpha", or "Alpha-Kilo". Default is name of pathline.
-- @return #AIRBASE self
function AIRBASE:AddTaxiway(TaxiPathline, Name)
-- If passed as string, get pathline.
if type(TaxiPathline)=="string" then
TaxiPathline=PATHLINE:FindByName(TaxiPathline)
end
-- Set name.
Name=Name or TaxiPathline:GetName()
-- Create a deep copy.
local taxiway=UTILS.DeepCopy(TaxiPathline) --Core.Pathline#PATHLINE
-- Set name.
taxiway.name=Name
-- Add to taxiways.
self.taxiways[Name]=taxiway
--self:I(self.taxiways)
return self
end
--- Find the shortest path using taxiways to get from given coodinates A to B on the airbase.
-- Note that the taxi ways have to be manually added with the `AIRBASE:AddTaxiway()` function.
-- @param #AIRBASE self
-- @param Core.Point#COORDINATE StartCoord Start coordinate.
-- @param Core.Point#COORDINATE EndCoord End coordinate.
-- @return Core.Pathline#PATHLINE Shortest path on taxiways from `StartCoord` to `EndCoord`.
-- @return #table Table of used taxi way pathlines.
function AIRBASE:FindTaxiwaysFromAtoB(StartCoord, EndCoord)
-- Create A* pathfinding.
local astar=ASTAR:New()
-- Add pathlines of taxiways of airport.
for _,_taxiway in pairs(self.taxiways) do
local taxiway=_taxiway --Core.Pathline#PATHLINE
astar:AddNodeFromPathlineName(taxiway)
end
-- Set cost function.
astar:SetCostDist2D()
-- Set valid neighbours to be on the same pathline or at most 10 meters between nodes to jump from one pathline/taxiway to another.
astar:SetValidNeighbourPathline(10)
-- Set start and end coordinates.
astar:SetStartCoordinate(StartCoord)
astar:SetEndCoordinate(EndCoord)
-- Get pathline.
local taxipath, nodes=astar:GetPathline()
local taxiways=astar:GetPathlinesFromNodes(nodes)
return taxipath, taxiways
end
--- Get closest taxiway from a given reference coordinate.
-- @param #AIRBASE self
-- @param Core.Point#COORDINATE Coord Reference coordinate.
-- @return Core.Pathline#PATHLINE Taxiway.
-- @return #number Distance to taxiway in meters.
-- @return Core.Point#COORDINATE Coordinate on taxiway closest to reference coordinate.
-- @return Core.Pathline#PATHLINE.Segment Segment of the taxiway closest to the reference coordinate.
function AIRBASE:GetClosestTaxiway(Coord)
local taxipath=nil
local distmin=math.huge
local coordmin=nil
local segmin=nil
for name,_pathline in pairs(self.taxiways) do
local pathline=_pathline --Core.Pathline#PATHLINE
local coord, dist, segment=pathline:GetClosestPoint3D(Coord)
if dist<distmin then
taxipath=pathline
coordmin=coord
distmin=dist
segmin=segment
end
end
return taxipath, distmin, coordmin, segmin
end
--- Find the shortest path using taxiways to get from given parking spot to the starting point of a runway.
-- Note that the taxi ways have to be manually added with the `AIRBASE:AddTaxiway()` function.
-- @param #AIRBASE self
-- @param #AIRBASE.ParkingSpot ParkingSpot Parking spot.
-- @param #AIRBASE.Runway Runway The runway. If none is given, we take the active runway for takeoff.
-- @return Core.Pathline#PATHLINE Shortest path on taxiways from `StartCoord` to `EndCoord`.
-- @return #table Table of used taxi way pathlines.
function AIRBASE:FindTaxiwaysParkingToRunway(ParkingSpot, Runway)
Runway=Runway or self:GetActiveRunwayTakeoff()
local StartCoord=ParkingSpot.Coordinate
local EndCoord=Runway.position
local taxipath, taxiways=self:FindTaxiwaysFromAtoB(StartCoord,EndCoord)
return taxipath, taxiways
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Parking
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@@ -2196,55 +2375,6 @@ function AIRBASE:GetParkingSpotsCoordinates(termtype)
return spots
end
--- Get the DCS#Vec2s of all parking spots at an airbase. Optionally only those of a specific terminal type. Spots on runways are excluded if not explicitly requested by terminal type.
-- @param #AIRBASE self
-- @param #AIRBASE.TerminalType termtype (Optional) Terminal type. Default all.
-- @return #table Table of DCS#Vec2 of parking spots.
function AIRBASE:GetParkingSpotsVec2s(termtype)
-- Get all parking spots data.
local parkingdata=self:GetParkingData(false)
-- Put coordinates of free spots into table.
local spots={}
for _,parkingspot in ipairs(parkingdata) do
-- Coordinates on runway are not returned unless explicitly requested.
if AIRBASE._CheckTerminalType(parkingspot.Term_Type, termtype) then
-- Get coordinate from Vec3 terminal position.
local vec2 = { x = parkingspot.vTerminalPos.x, y = parkingspot.vTerminalPos.z }
-- Add to table.
table.insert(spots, vec2)
end
end
return spots
end
--- Get the the bounding circular zone around all parking spots of an airbase.
-- @param #AIRBASE self
-- @param #boolean mark (Optional) Draw zone on map on first call of this function.
-- @return Core.Zone#ZONE_RADIUS BoundingZone
function AIRBASE:GetMinimumBoundingCircleFromParkingSpots(mark)
if self.isAirdrome then
if not self.parkingCircle then
local spots = self:GetParkingSpotsVec2s()
if #spots == 0 then return self.AirbaseZone end
local center, radius = UTILS.GetMinimumBoundingCircle(spots)
self.parkingCircle = ZONE_RADIUS:New(self.AirbaseName.." ParkingCircle",center,radius+50)
if mark == true then
self.parkingCircle:DrawZone(-1,{1,0,0},1,{0,1,0},0.2,3)
end
end
return self.parkingCircle
else
return self.AirbaseZone
end
end
--- Get a table containing the coordinates, terminal index and terminal type of free parking spots at an airbase.
-- @param #AIRBASE self
-- @return#AIRBASE self
+3 -13
View File
@@ -2439,7 +2439,7 @@ end
-- @return #table The mission route defined by points.
function GROUP:GetTaskRoute()
--self:F2( self.GroupName )
if _DATABASE.Templates.Groups[self.GroupName] and _DATABASE.Templates.Groups[self.GroupName].Template and _DATABASE.Templates.Groups[self.GroupName].Template.route and _DATABASE.Templates.Groups[self.GroupName].Template.route.points then
if _DATABASE.Templates.Groups[self.GroupName].Template and _DATABASE.Templates.Groups[self.GroupName].Template.route and _DATABASE.Templates.Groups[self.GroupName].Template.route.points then
return UTILS.DeepCopy( _DATABASE.Templates.Groups[self.GroupName].Template.route.points )
else
return {}
@@ -2467,7 +2467,7 @@ function GROUP:CopyRoute( Begin, End, Randomize, Radius )
--self:T3( { GroupName } )
local Template = _DATABASE.Templates.Groups[GroupName] and _DATABASE.Templates.Groups[GroupName].Template or nil
local Template = _DATABASE.Templates.Groups[GroupName].Template
if Template then
if not Begin then
@@ -2491,7 +2491,7 @@ function GROUP:CopyRoute( Begin, End, Randomize, Radius )
end
return Points
else
BASE:E( "Template not found for Group : " .. GroupName )
error( "Template not found for Group : " .. GroupName )
end
return nil
@@ -2944,16 +2944,6 @@ function GROUP:EnableEmission(switch)
return self
end
--- Set a formation for this group.
-- @param #GROUP self
-- @param #number Formation See. ENUMS.Formation or [Formations](https://wiki.hoggitworld.com/view/DCS_enum_formation) for options.
-- @return #GROUP self
function GROUP:SetFormation(Formation)
self:SetOption(AI.Option.Air.id.FORMATION,Formation)
return self
end
--- Switch on/off invisible flag for the group.
-- @param #GROUP self
-- @param #boolean switch If true, Invisible is enabled. If false, Invisible is disabled.
+1 -1
View File
@@ -485,7 +485,7 @@ function NET:GetPlayerIDByName(Name)
if not Name then return nil end
local playerList = net.get_player_list()
for i=1,#playerList do
local playerName = net.get_name(playerList[i])
local playerName = net.get_name(i)
if playerName == Name then
return playerList[i]
end
@@ -729,23 +729,16 @@ end
--- Returns the altitude above sea level of the POSITIONABLE.
-- @param #POSITIONABLE self
-- @param #boolean FromGround Measure from the ground or from sea level (ASL). Provide **true** for measuring from the ground (AGL). **false** or **nil** if you measure from sea level.
-- @return DCS#Distance The altitude of the POSITIONABLE.
-- @return #nil The POSITIONABLE is not existing or alive.
function POSITIONABLE:GetAltitude(FromGround)
function POSITIONABLE:GetAltitude()
self:F2()
local DCSPositionable = self:GetDCSObject()
if DCSPositionable then
local altitude = 0
local point = DCSPositionable:getPoint() --DCS#Vec3
altitude = point.y
if FromGround then
local land = land.getHeight({ x = point.x, y = point.z }) or 0
altitude = altitude - land
end
return altitude
local PositionablePointVec3 = DCSPositionable:getPoint() -- DCS#Vec3
return PositionablePointVec3.y
end
BASE:E( { "Cannot GetAltitude", Positionable = self, Alive = self:IsAlive() } )
+1 -1
View File
@@ -75,7 +75,7 @@ function SCENERY:Register( SceneryName, SceneryObject, SceneryZone )
self.Vector = (self.Vec3 and VECTOR) and VECTOR:NewFromVec(self.Vec3) or nil
end
if SceneryObject and SceneryObject.getPoint then
if SceneryObject then
local vec3 = SceneryObject:getPoint()
self.Vec3 = { x = vec3.x, y = vec3.y, z = vec3.z }
self.Vec2 = { x = vec3.x, y = vec3.z }
-1
View File
@@ -104,7 +104,6 @@ Ops/FlightControl.lua
Ops/PlayerTask.lua
Ops/PlayerRecce.lua
Ops/EasyGCICAP.lua
Ops/EasyA2G.lua
Ops/OpsZone.lua
Ops/ArmyGroup.lua
Ops/OpsTransport.lua