diff --git a/Moose Development/Moose/Core/Fsm.lua b/Moose Development/Moose/Core/Fsm.lua index af2e971fe..e86b88267 100644 --- a/Moose Development/Moose/Core/Fsm.lua +++ b/Moose Development/Moose/Core/Fsm.lua @@ -79,6 +79,7 @@ do -- FSM + --- FSM class -- @type FSM -- @field #string ClassName Name of the class. -- @field Core.Scheduler#SCHEDULER CallScheduler Call scheduler. diff --git a/Moose Development/Moose/Core/Pathline.lua b/Moose Development/Moose/Core/Pathline.lua index 8fdda2bc6..bc6e9305e 100644 --- a/Moose Development/Moose/Core/Pathline.lua +++ b/Moose Development/Moose/Core/Pathline.lua @@ -49,7 +49,7 @@ -- -- # Mark on F10 map -- --- The ponints 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 +-- 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`). @@ -69,11 +69,12 @@ PATHLINE = { -- @field #number landHeight Land height in meters. -- @field #number depth Water depth in meters. -- @field #number markerID Marker ID. +-- @field #number lineID Line marker ID. --- PATHLINE class version. -- @field #string version -PATHLINE.version="0.1.1" +PATHLINE.version="0.2.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -301,18 +302,42 @@ function PATHLINE:GetPoint2DFromIndex(n) return nil end +--- Calculate the length of the line. +-- @param #PATHLINE self +-- @param #boolean Project2D Calculate 2D distance between points. +-- @return #number Length in meters. +function PATHLINE:GetLength(Project2D) + local l=0 + + local np=#self.points + + for i=1,np-1 do + local p1=self.points[i] --#PATHLINE.Point + local p2=self.points[i+1] --#PATHLINE.Point + + if Project2D then + l=l+UTILS.VecDist2D(p1.vec2, p2.vec2) + else + l=l+UTILS.VecDist3D(p1.vec3, p2.vec3) + end + + end + + return l +end + --- Mark points on F10 map. -- @param #PATHLINE self -- @param #boolean Switch If `true` or nil, set marks. If `false`, remove marks. --- @return List of DCS#Vec3 points. +-- @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 - UTILS.RemoveMark(point.markerID, Delay) + UTILS.RemoveMark(point.markerID) end else @@ -329,6 +354,56 @@ function PATHLINE:MarkPoints(Switch) end end + return self +end + +--- Draw line on F10 map. +-- @param #PATHLINE self +-- @param #number Recipient Recipent of the line: -1=All. +-- @param #table Color Color as RGB table plus alpha value. Default {1, 0, 0, 1.0}. +-- @param #number LineType Line type: 1=Solid (default). +-- @return #PATHLINE self +function PATHLINE:DrawLine(Recipient, Color, LineType) + + -- Input + Recipient= Recipient or -1 + Color= Color or {1,0,0, 1.0} + LineType=LineType or 1 + local ReadOnly=false + + + local np=#self.points + + for i=1,np-1 do + local p1=self.points[i] --#PATHLINE.Point + local p2=self.points[i+1] --#PATHLINE.Point + + p1.lineID = UTILS.GetMarkID() + + trigger.action.lineToAll(Recipient, p1.lineID, p1.vec3, p2.vec3, Color, LineType, ReadOnly, "") + + end + + return self +end + +--- Remove line on F10 map. +-- @param #PATHLINE self +-- @param #number Delay Delay in seconds before line is removed. +-- @return #PATHLINE self +function PATHLINE:UnDrawLine(Delay) + + + local np=#self.points + + for _,_point in pairs(self.points) do + local p=_point --#PATHLINE.Point + if p.lineID then + UTILS.RemoveMark(p.lineID, Delay) + end + end + + return self end diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index fcf67ca02..729a5116d 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -2060,6 +2060,40 @@ do -- COORDINATE return Path, Way, GotPath end + + --- Returns a table of coordinates to a destination using only roads or railroads. + -- The first point is the closest point on road of the given coordinate. + -- By default, the last point is the closest point on road of the ToCoord. Hence, the coordinate itself and the final ToCoord are not necessarily included in the path. + -- @param #COORDINATE self + -- @param #COORDINATE ToCoord Coordinate of destination. + -- @param #boolean IncludeEndpoints (Optional) Include the coordinate itself and the ToCoordinate in the path. + -- @param #boolean Railroad (Optional) If true, path on railroad is returned. Default false. + -- @return Core.Pathline#PATHLINE Pathline containing the points on road. If no path on road can be found, nil is returned or just the endpoints. + function COORDINATE:GetPathlineOnRoad(ToCoord, IncludeEndpoints, Railroad) + + -- Set road type. + local RoadType="roads" + if Railroad==true then + RoadType="railroads" + end + + -- DCS API function returning a table of vec2. + local path = land.findPathOnRoads(RoadType, self.x, self.z, ToCoord.x, ToCoord.z) + + if IncludeEndpoints then + path=path or {} + table.insert(path, 1, self:GetVec2()) + table.insert(path, ToCoord:GetVec2()) + end + + local pathline=nil + if path then + pathline=PATHLINE:NewFromVec2Array(RoadType, path) + end + + return pathline + end + --- Gets the surface type at the coordinate. -- @param #COORDINATE self diff --git a/Moose Development/Moose/Core/Vector.lua b/Moose Development/Moose/Core/Vector.lua new file mode 100644 index 000000000..ce8ab3631 --- /dev/null +++ b/Moose Development/Moose/Core/Vector.lua @@ -0,0 +1,1381 @@ +--- **CORE** - Vector algebra. +-- +-- **Main Features:** +-- +-- * Easy vector algebra function +-- * Redefinition of `+`, `-`, `*`, `/`, `%` operators to be compatible with vectors +-- * Interface to DCS API functions +-- * Reduced confusion of DCS coordinate system +-- * Better performance than related classes (COORDINATE, POINT_VEC) +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Core%20-%20Vector). +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- @module Core.Vector +-- @image CORE_Vector.png + + +--- VECTOR class. +-- @type VECTOR +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity of output. +-- @field #number x Component pointing North if > 0 and South if < 0. +-- @field #number y Component pointing up if >0 and down if < 0. This describes the altitude above main sea level. +-- @field #number z Component pointing East if > 0 and West if < 0. + +--- *Mathematics knows no races or geographic boundaries; for mathematics, the cultural world is one country.* --David Hilbert +-- +-- === +-- +-- # The VECTOR Concept +-- +-- The VECTOR class has a great concept! +-- +-- https://github.com/automattf/vector.lua/blob/master/vector.lua +-- +-- # The DCS Coordinate System +-- +-- DCS has a rather unconventional way to define the coordinate system. The definition even depends whether you work with a 2D vector or a 3D vector. +-- The good think is, that you usually do not need to worry about this unless you directly call the DCS API functions. Plus, this class tries to +-- hide the differences between 2D and 3D conventions as good as possible with internal if-cases. +-- +-- Still, it is important to unterstand the differences. So let us explain: +-- +-- Usually, we draw a coordinate system in 2D and label the horizonal axis (pointing right) as the x-axis. The vertical axis (poining up on a piece of paper) +-- +-- ## 3D +-- +-- The x-axis points North. The z-axis points East. The y-axis points upwards and defines the altitue with respect to the mean sea level. +-- +-- ## 2D +-- +-- The x-axis points North (just like in the 3D) case. The y-axis points East. +-- +-- # Constructors +-- +-- There are different ways to create a new instance of a VECTOR. All methods start with `New`. +-- +-- ## From Components +-- +-- ## From 2D or 3D Vectors +-- +-- ## From Polar Coordinates +-- +-- ## From Spherical Coordinates +-- +-- +-- # Operators +-- +-- ## Addition [MATH] `+` +-- +-- ## Subtraction [MATH] `-` +-- +-- ## Multiplication [MATH] `*` +-- +-- ## Devision [MATH] `/` +-- +-- ## Modulo [MATH] `%` +-- +-- +-- # DCS API Interface +-- +-- This class offers easy and convenient ways to access all vector related DCS API functions. +-- +-- ## Land and Surface +-- +-- ## Atmosphere +-- +-- ## Effects and Actions +-- +-- ## Map Markings +-- +-- ## Coordinates +-- +-- # Inferface to other MOOSE Classes +-- +-- Of course, this class is interfaced with other MOOSE classes in the sense that you can obtain and work with VECTOR instances from MOOSE objects, from which a position or direction +-- vector can be derived. +-- +-- ## GROUP +-- +-- ## UNIT +-- +-- ## STATIC +-- +-- ## SCENERY +-- +-- ## ZONE +-- +-- # Examples +-- +-- A new `VECTOR` object can be created with the @{#VECTOR.New} function. +-- Here we create two vectors, a and b, and add them to create a new vector c. +-- +-- local a=VECTOR:New(1, 0) +-- local b=VECTOR:New(0, 1) +-- local c=a+b +-- +-- This is how it works. +-- +-- @field #VECTOR +VECTOR = { + ClassName = "VECTOR", + verbose = 0, +} + +--- VECTOR class version. +-- @field #string version +VECTOR.version="0.1.0" + +--- VECTOR unique ID +_VECTORID=0 + +--- VECTOR private index. +-- @field #VECTOR __index +VECTOR.__index = VECTOR + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: 3D rotation +-- DONE: Markers +-- TODO: Documentation + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new VECTOR class instance from given cartesian coordinates `x`, `y` and `z`, where `z` is optional. +-- **Note** that whether the `z` component is passed or nil has great impact on the interpretation of the `y` component. +-- If `z` is not given (`nil`), then `y` is used as `z` (the coordinate pointing East) because we always constuct a 3D vector. +-- If `z` is given (not `nil`), then `y` is used as coordinate pointing up (out of plane) as for all 3D DCS vectors. +-- @param #VECTOR self +-- @param #number x Component of vector along x-axis (pointing North in both 2D and 3D). +-- @param #number y Component of vector along y-axis (pointing East in 2D and Up in 3D). +-- @param #number z (Optional) Component of the z-axis (pointing East in 3D). +-- @return #VECTOR self +function VECTOR:New(x, y, z) + + if z==nil then + -- z-component is not given ==> 2D ==> we take y as z and set y=0. + self=setmetatable({x=x or 0, y=0, z=y or 0}, VECTOR) + else + self=setmetatable({x=x or 0, y=y or 0, z=z or 0}, VECTOR) + end + + _VECTORID=_VECTORID+1 + + self.uid=_VECTORID + + return self +end + +--- Create a new VECTOR class instance from given 2D or 3D vector object. +-- @param #VECTOR self +-- @param DCS#Vec3 Vec Vector object with `x`, `y` and optionally `z` components. Can be a DCS#Vec2, DCS#Vec3, `COORDINATE`, `VECTOR` object. +-- @return #VECTOR self +function VECTOR:NewFromVec(Vec) + + -- The :New function takes care whether this is a 2D or 3D vector. + local vector=VECTOR:New(Vec.x, Vec.y, Vec.z) + + return vector +end + +--- Create a new VECTOR class instance from polar coordinates (r, phi). +-- @param #VECTOR self +-- @param #number r Distance. +-- @param #number phi Angle in Degrees. Note that 0° corresponds to North, 90° East etc. +-- @return #VECTOR self +function VECTOR:NewFromPolar(r, phi) + + -- Convert deg to rad. + local Phi=math.rad(phi) + + -- Polar coordinates. What we want in DCS is: + -- North: Phi=0 ==> x= 1, y= 0 + -- East : Phi=90 ==> x= 0, y= 1 + -- South: Phi=180 ==> x=-1, y= 0 + -- West : Phi=270 ==> x= 0, y=-1 + + -- sin(0)=0 sin(90)=1, sin(180)=0, sin(270)=-1 ==> y + -- cos(0)=1 cos(90)=0, cos(180)=-1, cos(270)=0 ==> x + + local x=r*math.cos(phi) + local y=r*math.sin(phi) + + -- Create new vector. As z is nil, the 2D character is taken care of. + local v=VECTOR:New(x, y) + + return self +end + +--- Create a new VECTOR class instance from spherical coordinates (r, theta, phi). +-- @param #VECTOR self +-- @param #number r Distance in meters with r>=0. +-- @param #number theta Polar angle in Degrees measured from a fixed polar axis or zenith direction. This angle is in [0°, 180°]. +-- @param #number phi Azimuthal angle in Degrees. This angle is in [0°, 360°). +-- @return #VECTOR self +function VECTOR:NewFromSpherical(r, theta, phi) + + local sinPhi=math.sin(math.rad(phi)) + local cosPhi=math.cos(math.rad(phi)) + local sinTheta=math.sin(math.rad(theta)) + local cosTheta=math.cos(math.rad(theta)) + + --TODO: Check x,y,z convention for DCS. + local x=r*sinTheta*cosPhi + local y=r*sinTheta*sinPhi + local z=r*cosTheta + + local v=VECTOR:New(x, y, z) + + return self +end + +--- Get the directional vector that points from a given vector `a` to another given vector `b`. +-- The vector is `c=-a+b=b-a`. +-- @param #VECTOR self +-- @param #VECTOR a Vector a. This can also be given as any table with x, y and z components (z optional). +-- @param #VECTOR b Vector b. This can also be given as any table with x, y and z components (z optional). +-- @return #VECTOR Directional vector from a to b. +function VECTOR:NewDirectionalVector(a, b) + + local x=b.x-a.x + local y + local z + + if a.z and b.z then + -- Both given vectors are 3D + y=b.y-a.y + z=b.z-a.z + elseif b.z then + -- a is 2D and b is 3D + y=b.y-0 + z=b.z-a.y + elseif a.z then + -- a is 3D and b is 2D + y=0-a.y + z=b.y-a.z + else + -- a is 2D and b is 2D + y=b.y-a.y + z=nil --We leave z=nil, so the New function takes care of the 2D character. + end + + local c=VECTOR:New(x, y, z) + + return c +end + + +--- Creates a new VECTOR instance from given the latitude and longitude in decimal degrees (DD). +-- @param #VECTOR self +-- @param #number Latitude Latitude in decimal degrees. +-- @param #number Longitude Longitude in decimal degrees. +-- @param #number Altitude (Optional) Altitude in meters. Default is the land height at the 2D position. +-- @return #VECTOR self +function VECTOR:NewFromLLDD(Latitude, Longitude, Altitude) + + -- Returns a point from latitude and longitude in the vec3 format. + local vec3=coord.LLtoLO(Latitude, Longitude) + + -- Convert vec3 to coordinate object. + self=VECTOR:NewFromVec(vec3) + + -- Adjust height + if Altitude then + self.y=Altitude + end + + return self +end + +--- Creates a new VECTOR instance from given latitude and longitude in degrees, minutes and seconds (DMS). +-- **Note** that latitude and longitude are passed as strings and the characters `°`, `'` and `"` are important. +-- @param #VECTOR self +-- @param #string Latitude Latitude in DMS as string, e.g. "`42° 24' 14.3"`". +-- @param #string Longitude Longitude in DMS as string, e.g. "`42° 24' 14.3"`". +-- @param #number Altitude (Optional) Altitude in meters. Default is the land height at the coordinate. +-- @return #VECTOR +function VECTOR:NewFromLLDMS(Latitude, Longitude, Altitude) + + local lat=UTILS.LLDMSstringToDD(Latitude) + local lon=UTILS.LLDMSstringToDD(Longitude) + + self=VECTOR:NewFromLLDD(lat, lon, Altitude) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get 2D vector as simple table. +-- @param #VECTOR self +-- @return DCS#Vec2 2D array {x, y}. +function VECTOR:GetVec2() + + local vec={x=self.x, y=self.z} + + return vec +end + +--- Get 3D vector as simple table. +-- @param #VECTOR self +-- @param #boolean OnSurface If `true`, the `y` component is set the land height [m] of the 2D position. +-- @return DCS#Vec3 3D array {x=x, y=y, z=z}. +function VECTOR:GetVec3(OnSurface) + + local x=self.x + local y=OnSurface and land.getHeight({x=self.x, y=self.z}) or self.y + local z=self.z + + local vec={x=x, y=y, z=z} --DCS#Vec3 + + return vec +end + +--- Get a COORDINATE object. +-- @param #VECTOR self +-- @param #boolean OnSurface If `true`, the `y` component is set the land height [m] of the 2D position. +-- @return Core.Point#COORDINATE The COORDINATE object. +function VECTOR:GetCoordinate(OnSurface) + + local vec3=self:GetVec3(OnSurface) + + local coordinate=COORDINATE:NewFromVec3(vec3) + + return coordinate +end + +--- Get the distance from this vector to another vector. +-- @param #VECTOR self +-- @param #VECTOR Vector Vector to which the distance is requested. +-- @param #boolean Only2D If `true`, calculate only the projected 2D distance. +-- @return #number Distance in meters. +function VECTOR:GetDistance(Vector, Only2D) + + local dx=self.x-Vector.x + local dy=0 + local dz=0 + + if Vector.z then + if not Only2D then + dy=self.y-Vector.y + end + dz=self.z-Vector.z + else + -- Given vector is 2D. + dy=0 + dz=self.z-Vector.y + end + + -- Calculate the distance. + local dist=math.sqrt( dx*dx + dy*dy + dz*dz ) + + return dist +end + +--- Get the directional vector that points from this VECTOR to another VECTOR `a`. +-- @param #VECTOR self +-- @param #VECTOR a Vector `a`. This can also be given as any table with `x`, `y` and `z` components, where `z` is optional. +-- @return #VECTOR Directional vector from this vector to `a`. +function VECTOR:GetDirectionalVectorTo(a) + + local x=a.x-self.x + local y=0 + local z=nil + + if a.z then + -- a is 3D + y=a.y-self.y + z=a.z-self.z + else + -- a is 2D + y=a.y-self.z + z=nil --We leave z=nil, so the New function takes care of the 2D character. + end + + local c=VECTOR:New(x, y, z) + + return c +end + +--- Get the directional vector that points from another VECTOR `a` to this VECTOR. +-- @param #VECTOR self +-- @param #VECTOR a Vector a. This can also be given as any table with x,y and z components (z optional). +-- @return #VECTOR Directional vector from self to a. +function VECTOR:GetDirectionalVectorFrom(a) + + local x=self.x-a.x + local y + local z + + if a.z then + -- a is 3D + y=self.y-a.y + z=self.z-a.z + else + -- a is 2D ==> we work in 2D and take z component of self + y=self.z-a.y + z=nil --We leave z=nil, so the New function takes care of the 2D character. + end + + local c=VECTOR:New(x, y, z) + + return c +end + +--- Get length/norm/magnitude of this vector. +-- @param #VECTOR self +-- @return #number Length of vector. +function VECTOR:GetLength() + + local l=math.sqrt(self.x*self.x+self.y*self.y+self.z*self.z) + + return l +end + +--- Get heading of vector. +-- Note that a heading of +-- +-- * 000° = North +-- * 090° = East +-- * 180° = South +-- * 270° = West. +-- +-- @param #VECTOR self +-- @param #boolean To360 If `true` or `nil`, adjust heading to [0,360) range. If `false`, headings not in this range can occur. +-- @return #number Heading in degrees. +function VECTOR:GetHeading(To360) + + -- Get heading. + local heading=math.atan2(self.z, self.x) + + -- Convert to degrees. + heading=math.deg(heading) + + + if To360==nil or To360==true then + -- Adjust heading so it is in [0,360). + heading=UTILS.AdjustHeading360(heading) + else + -- Just make sure 360 is returned a 0. + if heading==360.0 then + heading=0.0 + end + end + + return heading +end + +--- Get the heading from this vector to another given vector. +-- @param #VECTOR self +-- @param #VECTOR Vector Vector to which the heading is requested. +-- @return #number Heading from this vector to the other vector in degrees. +function VECTOR:GetHeadingTo(Vector) + + -- Get directional vector from given vector to this vector. + local a=self:GetDirectionalVectorTo(Vector) + + -- Get heading of directional vector. + local heading=math.deg(math.atan2(a.z, a.x)) + + -- Adjust heading so it is in [0,360). + heading=UTILS.AdjustHeading360(heading) + + return heading +end + +--- Get the heading from a given vector to this vector. +-- @param #VECTOR self +-- @param #VECTOR Vector Vector from which the heading is requested. +-- @return #number Heading from the other vector to this vector in degrees. +function VECTOR:GetHeadingFrom(Vector) + + -- Get directional vector from given vector to this vector. + local a=self:GetDirectionalVectorFrom(Vector) + + -- Get heading of directional vector. + local heading=math.deg(math.atan2(a.z, a.x)) + + -- Adjust heading so it is in [0,360). + heading=UTILS.AdjustHeading360(heading) + + return heading +end + + +--- Get latitude and longitude of this vector. +-- @param #VECTOR self +-- @return #number Latitude in decimal degrees (DD). +-- @return #number Longitude in decimal degrees (DD). +function VECTOR:GetLatitudeLongitude() + + local vec3=self:GetVec3() + + local latitude, longitude, altitude=coord.LOtoLL(vec3) + + return latitude, longitude +end + + +--- Get MGRS information of this vector. +-- +-- `MGRS = {UTMZone = string, MGRSDigraph = string, Easting = number, Northing = number}` +-- +-- @param #VECTOR self +-- @return #table MGRS table with `UTMZone`, `MGRSDiGraph`, `Easting` and `Northing` keys. +function VECTOR:GetMGRS() + + local lat, long=self:GetLatitudeLongitude() + + local mgrs=coord.LLtoMGRS(lat, long) + + -- Example table returned by coord.LLtoMGRS + --[[ + MGRS = { + UTMZone = string, + MGRSDigraph = string, + Easting = number, + Northing = number + } + ]] + + return mgrs +end + +--- Get the difference of the heading of this vector w. +-- +-- **Example 1:** +-- This vector has a heading of 90° (pointing East) and the other vector has a heading of 225° (pointing South-West), +-- we would optain a delta of 225°-90°=135°. +-- +-- **Example 2:** +-- This vector has a heading of 180 (pointing South) and the other vector has a heading of 90° (pointing East), +-- we would optain a delta of 90°-180°=-90°. +-- +-- @param #VECTOR self +-- @param #VECTOR Vector Vector to which the heading is requested. +-- @return #number Heading from this vector to the other vector in degrees. +function VECTOR:GetHeadingDelta(Vector) + + local h1=self:GetHeading(false) + + local h2=Vector:GetHeading(false) + + local delta=h2-h1 + + return delta +end + +--- Return an intermediate VECTOR between this and another given vector. +-- @param #VECTOR self +-- @param #VECTOR Vector The destination vector. +-- @param #number Fraction The fraction (0,1) where the new vector is created. Default 0.5, *i.e.* in the middle. +-- @return #VECTOR Vector between this and the other vector. +function VECTOR:GetIntermediateVector(Vector, Fraction) + + local f=Fraction or 0.5 + + -- Get the directional vector to the given Vector. + local vec=self:GetDirectionalVectorTo(Vector) + + -- Get the length of the vector. + local length=vec:GetLength() + + -- Set/scale the length. + vec:SetLength(f*length) + + -- Get to the desired position. + vec=self+vec + + return vec +end + + + +--- Set length/norm/magnitude of this vector. +-- @param #VECTOR self +-- @param #number Length Desired length of vector. +function VECTOR:SetLength(Length) + + -- Normalize this vector to a length of 1. + self:Normalize() + + -- Scale the vector to the desired length. + local v=self*Length + + -- Replace with scaled version. + self:Replace(v) + + return self +end + +--- Set x-component of vector. The x-axis points to the North. +-- @param #VECTOR self +-- @param #number x Value of x. Default 0. +-- @return #VECTOR self +function VECTOR:SetX(x) + self.x=x or 0 + return self +end + +--- Set y-component of vector. The y-axis points to the upwards and describes the altitude above mean sea level. +-- @param #VECTOR self +-- @param #number y Value of y. Default land/surface height at this point. +-- @return #VECTOR self +function VECTOR:SetY(y) + + if y==nil then + y=self:GetSurfaceHeight() + end + self.y=y + return self +end + +--- Set z-component of vector. The z-axis points to the East. +-- @param #VECTOR self +-- @param #number z Value of z. Default 0. +-- @return #VECTOR self +function VECTOR:SetZ(z) + self.z=z or 0 + return self +end + + +--- Add a vector to this. This function works for DCS#Vec2, DCS#Vec3, VECTOR, COORDINATE objects. +-- Note that if you want to add a VECTOR, you can also simply use the `+` operator. +-- @param #VECTOR self +-- @param DCS#Vec3 Vec Vector to add. Can also be a DCS#Vec2, DCS#Vec3, COORDINATE or VECTOR object. +-- @return #VECTOR self +function VECTOR:AddVec(Vec) + + self.x=self.x+Vec.x + + if Vec.z then + self.y=self.y+Vec.y + self.z=self.z+Vec.z + else + -- Vec is 2D ==> we take its y-component. + self.z=self.z+Vec.y + end + + return self +end + +--- Subtract a vector from this one. This function works for DCS#Vec2, DCS#Vec3, VECTOR, COORDINATE objects. +-- +-- **Note** that if you want to add a VECTOR, you can also simply use the `-` operator. +-- @param #VECTOR self +-- @param DCS#Vec3 Vec Vector to substract. Can also be a DCS#Vec2, DCS#Vec3, COORDINATE or VECTOR object. +-- @return #VECTOR self +function VECTOR:SubVec(Vec) + + self.x=self.x-Vec.x + + if Vec.z then + self.y=self.y-Vec.y + self.z=self.z-Vec.z + else + -- Vec is 2D ==> we take its y-component. + self.z=self.z-Vec.y + end + + return self +end + +--- Calculate the dot product of this VECTOR with another vector. This function works for DCS#Vec2, DCS#Vec3, VECTOR, COORDINATE objects. +-- @param #VECTOR self +-- @param DCS#Vec3 Vec The other vector. Can also be a DCS#Vec2, DCS#Vec3, COORDINATE or VECTOR object. +-- @return #number Dot product Sum_i(a[i]*b[i]). Note that this is a **scalar** and not a vector any more! +function VECTOR:Dot(Vec) + + local dot=self.x*Vec.x + if Vec.z then + dot=dot+self.y*Vec.y+self.z*Vec.z + else + -- Vec is 2D ==> we take its y-component for z. + dot=dot+self.z*Vec.y + end + + return dot +end + +--- Calculate the rotation or cross product of this VECTOR with another vector. This function works for DCS#Vec2, DCS#Vec3, VECTOR, COORDINATE objects. +-- @param #VECTOR self +-- @param DCS#Vec3 Vec The other vector. Can also be a DCS#Vec2, DCS#Vec3, COORDINATE or VECTOR object. +-- @return #VECTOR The cross product vector. +function VECTOR:Rot(Vec) + + -- TODO: + local dot=self.x*Vec.x + if Vec.z then + dot=dot+self.y*Vec.y+self.z*Vec.z + else + -- Vec is 2D ==> we take its y-component for z. + dot=dot+self.z*Vec.y + end + + return dot +end + + +--- Get a clone (deep copy) of this vector. +-- @param #VECTOR self +-- @return #VECTOR Copy of the vector. +function VECTOR:Copy() + + local c=VECTOR:New(self.x, self.y, self.z) + + return c +end + +--- Replace this vector with another one. +-- If the given vector is 2D, we +-- @param #VECTOR self +-- @param #VECTOR Vector The vector that is used to replace this vector. +-- @param #boolean Project2D If `true` and the given vector is 2D, we project the updated vector to 2D (`y=0`). Otherwise, we leave `y` untouched. +-- @return #VECTOR self updated +function VECTOR:Replace(Vector, Project2D) + + self.x=Vector.x + + if Vector.z then + -- Given vector is 3D + self.y=Vector.y + self.z=Vector.z + else + -- Given vector is 2D + if Project2D then + self.y=0 + end + self.z=Vector.y + end + + return self +end + +--- Normalize this vector, so that has a length of 1. +-- @param #VECTOR self +-- @return #VECTOR self +function VECTOR:Normalize() + + local l=self:GetLength() + + if l~=0 then + self:Replace(self/l) + end + + return self +end + +--- Translate the vector by a given distance and angle. +-- @param #VECTOR self +-- @param #number Distance Distance in meters. Default 1000 meters. +-- @param #number Heading Heading angle in degrees. Default 0° = North. +-- @param #boolean Copy Create a copy of the VECTOR so the original stays unchanged. +-- @return #VECTOR The translated vector or a copy of it. +function VECTOR:Translate(Distance, Heading, Copy) + + -- Set default distance if not passed. + Distance=Distance or 1000 + + -- Angle in rad. + local alpha = math.rad(Heading or 0) + + -- Create a copy if requested. + local vector=Copy and self:Copy() or self + + -- Set new coordinates. + vector.x = Distance * math.cos(alpha) + vector.x -- New x + vector.z = Distance * math.sin(alpha) + vector.z -- New z + + return vector +end + +--- Rotate the VECTOR clockwise in the 2D (x,z) plane. +-- @param #VECTOR self +-- @param #number Angle Rotation angle in degrees). Default 0. +-- @param #boolean Copy Create a copy of the VECTOR so the original stays unchanged. +-- @return #VECTOR The translated vector or a copy of it. +function VECTOR:Rotate2D(Angle, Copy) + + -- Angle in rad. + local phi = -math.rad(Angle or 0) + + -- Sin/Cos of angle. + local sinPhi = math.sin(phi) + local cosPhi = math.cos(phi) + + -- Get more convenient notation. + local X=self.z + local Y=self.x + + -- Apply rotation matrix. + local z = X*cosPhi - Y*sinPhi + local x = X*sinPhi + Y*cosPhi + + -- Create new vector. + if Copy then + local vector=VECTOR:New(x, self.y, z) + return vector + else + self:SetX(x) + self:SetZ(z) + return self + end +end + + + +--- Provides an MGRS string. +-- @param #VECTOR self +-- @param Core.Settings#SETTINGS Settings (Optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. +-- @return #string The MGRS text. +function VECTOR:ToStringMGRS(Settings) + + -- Get Accuracy. + local MGRS_Accuracy = Settings and Settings.MGRS_Accuracy or _SETTINGS.MGRS_Accuracy + + local lat, lon = coord.LOtoLL( self:GetVec3() ) + + local MGRS = coord.LLtoMGRS( lat, lon ) + + local text="MGRS " .. UTILS.tostringMGRS( MGRS, MGRS_Accuracy ) + + return text +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- DCS API Wrapper Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get the surface type at the vector. +-- +-- * LAND = 1 +-- * SHALLOW_WATER = 2 +-- * WATER = 3 +-- * ROAD = 4 +-- * RUNWAY = 5 +-- +-- @param #VECTOR self +-- @return #number Surface type +function VECTOR:GetSurfaceType() + + local vec2=self:GetVec2() + + local s=land.getSurfaceType(vec2) + + return s +end + + +--- Get the name of surface type at the vector. +-- +-- * LAND = 1 +-- * SHALLOW_WATER = 2 +-- * WATER = 3 +-- * ROAD = 4 +-- * RUNWAY = 5 +-- +-- @param #VECTOR self +-- @return #string Surface type name +function VECTOR:GetSurfaceTypeName() + + local vec2=self:GetVec2() + + local s=land.getSurfaceType(vec2) + + for name,id in land.SurfaceType() do + if id==s then + return name + end + end + + return "unknown" +end + +--- Check if a given vector has line of sight with this vector. +-- @param #VECTOR self +-- @param #VECTOR Vec The other vector. +-- @return #number Surface Type +function VECTOR:IsVisible(Vec) + + local vec1=self:GetVec3() + + local vec2={x=Vec.x, Vec.y, Vec.z} + + local los=land.isVisible(vec1, vec2) + + return los +end + +--- Get a vector on the closest road. +-- @param #VECTOR self +-- @return #VECTOR Closest vector to a road. +function VECTOR:GetClosestRoad() + + local vec2=self:GetVec2() + + local x,y=land.getClosestPointOnRoads('roads', vec2.x, vec2.y) + + local road=nil + if x and y then + road=VECTOR:New(x, y) + end + + return road +end + +--- Get a vector on the closest railroad. +-- @param #VECTOR self +-- @return #VECTOR Closest vector to a railroad. +function VECTOR:GetClosestRailroad() + + local vec2=self:GetVec2() + + local x,y=land.getClosestPointOnRoads('railroads', vec2.x, vec2.y) + + local road=nil + if x and y then + road=VECTOR:New(x, y) + end + + return road +end + + +--- Get the path on road from this vector to a given other vector. +-- @param #VECTOR self +-- @param #VECTOR Vec The destination vector. +-- @return Core.Pathline#PATHLINE Pathline with points on road. +function VECTOR:GetPathOnRoad(Vec) + + local vec1=self:GetVec2() + local vec2=Vec:GetVec2() + + local vec2points=land.findPathOnRoads("roads", vec1.x , vec1.y, vec2.x, vec2.y) + + local path=nil + if vec2points then + path=PATHLINE:NewFromVec2Array("Road", vec2points) + end + + return path +end + + +--- Get profile of the land between the two passed points. +-- @param #VECTOR self +-- @param #VECTOR Vec3 The 3D destination vector. If a 2D vector is passed, `y` is set to the land height. +-- @return Core.Pathline#PATHLINE Pathline with points of the profile. +function VECTOR:GetProfile(Vec3) + + local vec3=self:GetVec3() + + -- Get profile + local vec3s=land.profile(vec3, Vec3) + + local profile=nil + if vec3s then + + profile=PATHLINE:NewFromVec3Array("Profile", vec3s) + + end + + return profile +end + +--- Returns an intercept point at which a ray drawn from the this vector in the passed normalized direction for a specified distance. +-- @param #VECTOR self +-- @param DCS#Vec3 DirectionVector Directional vector. +-- @param #number Distance Distance in meters. Default 1000 m. +-- @return #VECTOR Intercept vector. Can be `nil` if no intercept point is found. +function VECTOR:GetInterceptPoint(DirectionVector, Distance) + + local vec3=self:GetVec3() + + local ip3=land.getIP(vec3, DirectionVector, Distance or 1000) --DCS#Vec3 + + local ipvector=nil + + if ip3 then + ipvector=VECTOR:New(ip3.x, ip3.y , ip3.z) + end + + return ipvector +end + +--- Returns the distance from sea level at this vector. +-- @param #VECTOR self +-- @return #number Distance above sea leavel in meters. +function VECTOR:GetSurfaceHeight() + + local vec2=self:GetVec2() + + local h=land.getHeight(vec2) + + return h +end + + +--- Returns the distance from sea level at this vector. +-- @param #VECTOR self +-- @return #number Heigh above sea leavel in meters. +-- @return #number Depth (positive) at this point in meters. +function VECTOR:GetSurfaceHeightAndDepth() + + local vec2=self:GetVec2() + + local h,d=land.getSurfaceHeightWithSeabed(vec2) + + return h,d +end + +--- Returns a velocity vector of the wind at this vector. Turbolences can be optionally be included. +-- @param #VECTOR self +-- @param #boolean WithTurbulence If `true`, return wind including turbulence. +-- @return #VECTOR Velocity 3D vector [m/s] the wind is blowing to. +function VECTOR:GetWindVector(WithTurbulence) + + local vec3=self:GetVec3() + + local wind=nil + if WithTurbulence then + wind=atmosphere.getWindWithTurbulence(vec3) + + else + wind=atmosphere.getWind(vec3) + end + + local vector=VECTOR:New(wind) + + return vector +end + +--- Returns a temperature and pressure at this vector. +-- @param #VECTOR self +-- @return #number Temperatur in Kelvin. +-- @return #number Pressure in Pascals. +function VECTOR:GetTemperaturAndPressure() + + local vec3=self:GetVec3() + + local t,p=atmosphere.getTemperatureAndPressure(vec3) + + return t,p +end + +--- Creates a smoke at this vector. +-- @param #VECTOR self +-- @param #number Color Color of the smoke: 0=Green, 1=Red, 2=White, 3=Orange, 4=Blue. Default 0. +-- @param #number Duration (Optional) Duration of the smoke in seconds. Default nil. +-- @return #string Name of the smoke object. Can be used to stop it. +function VECTOR:Smoke(Color, Duration) + + local vec3=self:GetVec3() + + Color=Color or 0 + + -- Create a name for the smoke object + local name=string.format("Vector-Smoke-%d", self.uid) + + -- Create smoke at this position + trigger.action.smoke(vec3, Color, name) + + if Duration and Duration>0 then + self:StopSmoke(name, Duration) + end + + return name +end + + +--- Creates a large smoke and fire effect of a specified type and density at this vector. +-- +-- * 1 = small smoke and fire +-- * 2 = medium smoke and fire +-- * 3 = large smoke and fire +-- * 4 = huge smoke and fire +-- * 5 = small smoke +-- * 6 = medium smoke +-- * 7 = large smoke +-- * 8 = huge smoke +-- +-- @param #VECTOR self +-- @param #number Preset Preset of smoke. Default `BIGSMOKEPRESET.LargeSmokeAndFire`. +-- @param #number Density Density between [0,1]. Default 0.5. +-- @param #number Duration (Optional) Duration of the smoke and fire in seconds. +-- @return #string Name of the smoke. Can be used to stop it. +function VECTOR:SmokeAndFire(Preset, Density, Duration) + + Preset=Preset or BIGSMOKEPRESET.LargeSmokeAndFire + Density=Density or 0.5 + + local vec3=self:GetVec3() + + -- Get a name of this smoke & fire object + local name=string.format("Vector-Fire-%d", self.uid) + + trigger.action.effectSmokeBig(vec3, Preset, Density, name) + + if Duration and Duration>0 then + self:StopSmoke(name, Duration) + end + + return name +end + +--- Stop smoke or fire effect. +-- @param #VECTOR self +-- @param #string Name Name of the smoke object. +-- @param #number Delay Delay in seconds before the smoke is stopped. +-- @return #VECTOR self +function VECTOR:StopSmoke(Name, Delay) + + if Delay and Delay>0 then + TIMER:New(VECTOR.StopSmoke, self, Name):Start(Delay) + else + if Name then + trigger.action.effectSmokeStop(Name) + else + env.error(string.format("No name provided in VECTOR.StopSmoke function!")) + end + end + + return self +end + + +--- Creates an illumination bomb at the specified point. +-- @param #VECTOR self +-- @param #number Power The power in Candela (cd). Should be between 1 and 1000000. Default 1000 cd. +-- @param #number Altitude (Optional) Altitude [m] at which the illumination bomb is created. +-- @return #VECTOR self +function VECTOR:IlluminationBomb(Power, Altitude) + + local vec3=self:GetVec3() + + if Altitude then + vec3.y=Altitude + end + + -- Create an illumination bomb. + trigger.action.illuminationBomb(vec3, Power or 1000) + + return self +end + +--- Creates an explosion at a given point at the specified power. +-- @param #VECTOR self +-- @param #number Power The power in kg TNT. Default 100 kg. +-- @return #VECTOR self +function VECTOR:Explosion(Power) + + local vec3=self:GetVec3() + + trigger.action.explosion(vec3, Power or 100) + + return self +end + +--- Creates a signal flare at the given point in the specified color. The flare will be launched in the direction of the azimuth angle. +-- @param #VECTOR self +-- @param #number Color Color of flare. Default Green. +-- @param #number Azimuth Azimuth angle in degrees. Default 0. +-- @return #VECTOR self +function VECTOR:Flare(Color, Azimuth) + + local vec3=self:GetVec3() + + trigger.action.signalFlare(vec3, Color or 0, math.rad(Azimuth or 0)) + + return self +end + + +--- Creates a arrow from this VECTOR to another vector on the F10 map. +-- @param #VECTOR self +-- @param #VECTOR Vector The vector defining the endpoint. +-- @param #number Coalition Coalition Id: -1=All, 0=Neutral, 1=Red, 2=Blue. Default -1. +-- @param #table Color RGB color with alpha {r, g, b, alpha}. Default {1, 0, 0, 0.7}. +-- @param #table FillColor RGB color with alpha {r, g, b, alpha}. Default {1, 0, 0, 0.5}. +-- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot Dash, 5=Long Dash, 6=Two Dash. Default 1. +-- @return #number Marker ID. Can be used to remove the drawing. +function VECTOR:ArrowTo(Vector, Coalition, Color, FillColor, LineType) + + local vec3End=self:GetVec3() + local vec3Start=Vector:GetVec3() + + local id=UTILS.GetMarkID() + + Coalition=Coalition or -1 + Color=Color or {1,0,0,0.7} + FillColor=FillColor or {1,0,0,0.5} + LineType=LineType or 1 + + local readOnly=false + + trigger.action.arrowToAll(Coalition , id, vec3Start, vec3End, Color, FillColor , LineType, readOnly, "") + + return id +end + + +--- Create mark on F10 map. +-- @param #VECTOR self +-- @param #string MarkText Free format text that shows the marking clarification. +-- @param #number Recipient Recipient of the mark: -1=All (default), 0=Neutral, 1=Red, 2=Blue. Can also be a `GROUP` object. +-- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. +-- @return #number Mark ID. +function VECTOR:Mark(MarkText, Recipient, ReadOnly) + + Recipient=Recipient or -1 + + if type(Recipient)=="number" then + + local MarkID = UTILS.GetMarkID() + + if Recipient==-1 then + trigger.action.markToAll(MarkID, MarkText, self:GetVec3(), ReadOnly, "") + elseif Recipient==0 then + trigger.action.markToCoalition(MarkID, MarkText, self:GetVec3(), coalition.side.NEUTRAL, ReadOnly, "") + elseif Recipient==1 then + trigger.action.markToCoalition(MarkID, MarkText, self:GetVec3(), coalition.side.RED, ReadOnly, "") + elseif Recipient==2 then + trigger.action.markToCoalition(MarkID, MarkText, self:GetVec3(), coalition.side.BLUE, ReadOnly, "") + end + + return MarkID + + elseif type(Recipient)=="table" then + + local MarkID = UTILS.GetMarkID() + + local group=Recipient --Wrapper.Group#GROUP + + trigger.action.markToGroup( MarkID, MarkText, self:GetVec3(), group:GetID(), ReadOnly, "") + + return MarkID + + end + + return nil +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Private Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check if passed object is a Vector. +-- @param #table t The object to be tested. +-- @return #boolean Returns `true` if `t` is a #VECTOR and `false` otherwise. +function VECTOR._IsVector(t) + return getmetatable(t) == VECTOR +end + +--- Meta function to add vectors together. +-- @param #VECTOR a Vector a. +-- @param #VECTOR b Vector b. +-- @return #VECTOR Returns a new VECTOR c with c[i]=a[i]+b[i] for i=x,y,z. +function VECTOR.__add(a,b) + + assert(VECTOR._IsVector(a) and VECTOR._IsVector(b), "ERROR in VECTOR.__add: wrong argument types! (expected and )") + + local c=VECTOR:New(a.x+b.x, a.y+b.y, a.z+b.z) + + return c +end + +--- Meta function to subtract vectors. +-- @param #VECTOR a Vector a. +-- @param #VECTOR b Vector b. +-- @return #VECTOR Returns a new VECTOR c with c[i]=a[i]-b[i] for i=x,y,z. +function VECTOR.__sub(a,b) + + assert(VECTOR._IsVector(a) and VECTOR._IsVector(b), "ERROR in VECTOR.__sub: wrong argument types: (expected and )") + + local c=VECTOR:New(a.x-b.x, a.y-b.y, a.z-b.z) + + return c +end + + +--- Meta function to multiplicate vector by another vector or a scalar. +-- @param #VECTOR a Vector a. Can also be a #number. +-- @param #VECTOR b Vector b. Can also be a #number. +-- @return #VECTOR Returns a new VECTOR c with c[i]=a[i]*b[i] for i=x,y,z. +function VECTOR.__mul(a, b) + + local c=nil --#VECTOR + + if type(a)=='number' then + c=VECTOR:New(a*b.x, a*b.y, a*b.z) + elseif type(b)=='number' then + c=VECTOR:New(b*a.x, b*a.y, b*a.z) + else + c=VECTOR:New(a.x*b.x, a.y*b.y, a.z*b.z) + end + + return c +end + +--- Meta function for dividing a vector by a scalar or by another vector. +-- @param #VECTOR a Vector a. +-- @param #VECTOR b Vector b. Can also be a #number. +-- @return #VECTOR Returns a new VECTOR c with c[i]=a[i]/b or c[i]=a[i]/b[i] for i=x,y,z. +function VECTOR.__div(a, b) + + assert(VECTOR._IsVector(a) and (type(b) == "number" or VECTOR._IsVector(b)), "div: wrong argument types (expected and ( or ))") + + local c=nil --#VECTOR + + if type(b) == "number" then + c=VECTOR:New(a.x/b, a.y/b, a.z/b) + else + c=VECTOR:New(a.x/b.x, a.y/b.y, a.z/b.z) + end + + return c +end + +--- Meta function to make vectors negative. +-- @param #VECTOR v Vector v. +function VECTOR.__unm(v) + local c=VECTOR:New(-v.x, -v.y, -v.z) + return c +end + +--- Meta function to check if two vectors are equal. +-- @param #VECTOR a Vector a. +-- @param #VECTOR b Vector b. +-- @return #boolean If `true`, both vectors are equal +function VECTOR.__eq(a, b) + return a.x==b.x and a.y==b.y and a.z==b.z +end + + +--- Meta function to change how vectors appear as string. +-- @param #VECTOR self +-- @return #string String representation of vector. +function VECTOR:__tostring() + local text=string.format("VECTOR: x=%.1f, y=%.1f, z=%.1f |v|=%.1f Phi=%4.1f°", self.x, self.y, self.z, self:GetLength(), self:GetHeading(false)) + return text +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 807f57f98..d5dbaec92 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -3340,7 +3340,6 @@ function WAREHOUSE:FindAssetInDB(group) if aid~=nil then local asset=_WAREHOUSEDB.Assets[aid] - self:T2({asset=asset}) if asset==nil then self:_ErrorMessage(string.format("ERROR: Asset for group %s not found in the data base!", group:GetName()), 0) end @@ -3918,7 +3917,7 @@ end -- @param #string assignment A free to choose string specifying an assignment for the asset. This can be used with the @{#WAREHOUSE.OnAfterNewAsset} function. -- @param #table other (Optional) Table of other useful data. Can be collected via WAREHOUSE.OnAfterNewAsset() function for example function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribute, forcecargobay, forceweight, loadradius, skill, liveries, assignment, other) - self:T({group=group, ngroups=ngroups, forceattribute=forceattribute, forcecargobay=forcecargobay, forceweight=forceweight}) + --self:T({group=group:GetName(), ngroups=ngroups, forceattribute=forceattribute, forcecargobay=forcecargobay, forceweight=forceweight}) -- Set default. local n=ngroups or 1 @@ -4446,7 +4445,6 @@ end -- @param #WAREHOUSE.Queueitem Request Information table of the request. -- @return #boolean If true, request is granted. function WAREHOUSE:onbeforeRequest(From, Event, To, Request) - self:T3({warehouse=self.alias, request=Request}) -- Distance from warehouse to requesting warehouse. local distance=self:GetCoordinate():Get2DDistance(Request.warehouse:GetCoordinate()) @@ -6153,9 +6151,6 @@ function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrol -- Uncontrolled spawning. template.uncontrolled=uncontrolled - -- Debug info. - self:T2({airtemplate=template}) - -- Spawn group. local group=_DATABASE:Spawn(template) --Wrapper.Group#GROUP @@ -8601,6 +8596,8 @@ function WAREHOUSE:_DeleteStockItem(stockitem) local item=self.stock[i] --#WAREHOUSE.Assetitem if item.uid==stockitem.uid then table.remove(self.stock,i) + -- remove also from warehouse DB + _WAREHOUSEDB.Assets[stockitem.uid]=nil break end end diff --git a/Moose Development/Moose/Globals.lua b/Moose Development/Moose/Globals.lua index 771547060..5108254d1 100644 --- a/Moose Development/Moose/Globals.lua +++ b/Moose Development/Moose/Globals.lua @@ -20,6 +20,13 @@ _DATABASE:_RegisterCargos() _DATABASE:_RegisterZones() _DATABASE:_RegisterAirbases() +--- Function that writes to DCS log file +-- @param #string text Formatted text. +-- @param ... Format passed to string.format(). +function printf(text, ...) + env.info(string.format(text, ...)) +end + --- Check if os etc is available. BASE:I("Checking de-sanitization of os, io and lfs:") local __na = false diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index 37c6ac45c..13f30e38f 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -32,6 +32,7 @@ __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Core/MarkerOps_Base.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Core/TextAndSound.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Core/Pathline.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Core/ClientMenu.lua') +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Core/Vector.lua') __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Wrapper/Object.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Wrapper/Identifiable.lua' ) @@ -186,4 +187,9 @@ __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/Radios.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Navigation/Towns.lua' ) + __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Globals.lua' ) diff --git a/Moose Development/Moose/Modules_local.lua b/Moose Development/Moose/Modules_local.lua index 5a6b6c0e9..356464f50 100644 --- a/Moose Development/Moose/Modules_local.lua +++ b/Moose Development/Moose/Modules_local.lua @@ -32,6 +32,7 @@ __Moose.Include( 'Core\\MarkerOps_Base.lua' ) __Moose.Include( 'Core\\TextAndSound.lua' ) __Moose.Include( 'Core\\Condition.lua' ) __Moose.Include( 'Core\\ClientMenu.lua' ) +__Moose.Include( 'Core\\Vector.lua' ) __Moose.Include( 'Wrapper\\Object.lua' ) __Moose.Include( 'Wrapper\\Identifiable.lua' ) @@ -177,4 +178,9 @@ __Moose.Include( 'Tasking\\Task_Cargo_Dispatcher.lua' ) __Moose.Include( 'Tasking\\Task_Capture_Zone.lua' ) __Moose.Include( 'Tasking\\Task_Capture_Dispatcher.lua' ) +__Moose.Include( 'Navigation\\Point.lua' ) +__Moose.Include( 'Navigation\\Beacons.lua' ) +__Moose.Include( 'Navigation\\Radios.lua' ) +__Moose.Include( 'Navigation\\Towns.lua' ) + __Moose.Include( 'Globals.lua' ) diff --git a/Moose Development/Moose/Navigation/Beacons.lua b/Moose Development/Moose/Navigation/Beacons.lua new file mode 100644 index 000000000..9c7acba8b --- /dev/null +++ b/Moose Development/Moose/Navigation/Beacons.lua @@ -0,0 +1,422 @@ +--- **NAVIGATION** - Beacons of the map/theatre. +-- +-- **Main Features:** +-- +-- * Access beacons of the map +-- * Find closest beacon +-- * Get frequencies and channels +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Navigation%20-%20Beacons). +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- @module Navigation.Beacons +-- @image NAVIGATION_Beacons.png + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- BEACONS class. +-- @type BEACONS +-- +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity of output. +-- @field #table beacons Beacons. +-- +-- @extends Core.Base#BASE + +--- *Hope is the beacon that guides lost ships back to the shore.* +-- +-- === +-- +-- # The BEACONS Concept +-- +-- This class is desinged to make information about beacons of a map/theatre easier accessible. The information contains location, type and frequencies of all or specific beacons of the map. +-- +-- **Note** that try to avoid hard coding stuff in Moose since DCS is updated frequently and things change. Therefore, the main source of information is either a file `beacons.lua` that can be +-- found in the installation directory of DCS for each map or a table that the user needs to provide. +-- +-- # Basic Setup +-- +-- A new `BEACONS` object can be created with the @{#BEACONS.NewFromFile}(*beacons_lua_file*) function. +-- +-- local beacons=BEACONS:NewFromFile("\Mods\terrains\\beacons.lua") +-- beacons:MarkerShow() +-- +-- This will load the beacons from the `` for the specific map and place markers on the F10 map. This is the first step you should do to ensure that the file +-- you provided is correct and all relevant beacons are present. +-- +-- # User Functions +-- +-- ## F10 Map Markers +-- +-- ## Position +-- +-- ## Get Closest Beacon +-- +-- +-- @field #BEACONS +BEACONS = { + ClassName = "BEACONS", + verbose = 1, + beacons = {}, +} + +--- Mission capability. +-- @type BEACONS.Beacon +-- @field #function display_name Function that returns the localized name. +-- @field #number type Beacon type. +-- @field #string beaconId Beacon ID. +-- @field #string callsign Call sign. +-- @field #number frequency Frequency in Hz. +-- @field #number channel TACAN, RSBN or PRMG channel depending on type. +-- @field #table position Position table. +-- @field #number direction Direction in degrees. +-- @field #table positionGeo Table with latitude and longitude. +-- @field #table sceneObjects Table with scenery objects, e.g. `{t:393396742}`. +-- @field #number chartOffsetX No idea what this offset is?! +-- @field DCS#Vec3 vec3 Position vector 3D. +-- @field #number markerID ID for the F10 marker. +-- @field #string typeName Name of becon type. +-- @field Wrapper.Scenery#SCENERY scenery The scenery object. + +--- BEACONS class version. +-- @field #string version +BEACONS.version="0.1.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: A lot... +-- DONE: TACAN channel from frequency (was already in beacon.lua as channel) +-- DONE: Scenery object + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor(s) +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new BECAONS class instance from a given table. +-- @param #BEACONS self +-- @param #table BeaconTable Table with beacon info. +-- @return #BEACONS self +function BEACONS:NewFromTable(BeaconTable) + + -- Inherit everything from BASE class. + self=BASE:Inherit(self, BASE:New()) -- #BEACONS + + for _,_beacon in pairs(BeaconTable) do + local beacon=_beacon --#BEACONS.Beacon + + -- Get 3D vector + beacon.vec3={x=beacon.position[1], y=beacon.position[2], z=beacon.position[3]} + + -- Get coordinate + beacon.coordinate=COORDINATE:NewFromVec3(beacon.vec3) + + -- Get type name + beacon.typeName=self:_GetTypeName(beacon.type) + + -- Find closest scenery object from scan + beacon.scenery=beacon.coordinate:FindClosestScenery(20) + + -- Debug stuff for scenery object + if false then + if beacon.scenery then + env.info(string.format("FF Beacon %s %s %s got scenery object %s, %s", beacon.callsign, beacon.beaconId, beacon.typeName, beacon.scenery:GetName(), beacon.scenery:GetTypeName() )) + UTILS.PrintTableToLog(beacon.scenery.SceneryObject) + UTILS.PrintTableToLog(beacon.sceneObjects) + else + env.info(string.format("FF NO scenery object %s %s %s ", beacon.callsign, beacon.beaconId, beacon.typeName)) + end + end + + -- Add to table + table.insert(self.beacons, beacon) + end + + -- Debug output + self:I(string.format("Added %d beacons", #self.beacons)) + + if self.verbose > 0 then + local text="Beacon types:" + for typeName,typeID in pairs(BEACON.Type) do + local n=self:CountBeacons(typeID) + text=text..string.format("\n%s = %d", typeName, n) + end + self:I(text) + end + + return self +end + + +--- Create a new BECAONS class instance from a given file. +-- @param #BEACONS self +-- @param #string FileName Full path to the file containing the map beacons. +-- @return #BEACONS self +function BEACONS:NewFromFile(FileName) + + -- Inherit everything from BASE class. + self=BASE:Inherit(self, BASE:New()) -- #BEACONS + + local exists=UTILS.FileExists(FileName) + + if exists==false then + self:E(string.format("ERROR: file with beacon info does not exist!")) + return nil + end + + -- This will create a global table `beacons` + dofile(FileName) + + -- Get beacons from table. + self=self:NewFromTable(beacons) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get 3D position vector of a specific beacon. +-- @param #BEACONS self +-- @param #BEACONS.Beacon beacon The beacon data structure. +-- @return DCS#Vec3 Position vector. +function BEACONS:GetVec3(beacon) + return beacon.vec3 +end + +--- Get COORDINATE of a specific beacon. +-- @param #BEACONS self +-- @param #BEACONS.Beacon beacon The beacon data structure. +-- @return Core.Point#COORDINATE The coordinate. +function BEACONS:GetCoordinate(beacon) + local coordinate=COORDINATE:NewFromVec3(beacon.vec3) + return coordinate +end + +--- Find closest beacon to a given coordinate. +-- @param #BEACONS self +-- @param Core.Point#COORDINATE Coordinate The reference coordinate. +-- @param #number TypeID (Optional) Only search for specific beacon types, *e.g.* `BEACON.Type.TACAN`. +-- @param #number DistMax (Optional) Max search distance in meters. +-- @param #table ExcludeList (Optional) List of beacons to exclude. +-- @return #BEACONS.Beacon The closest beacon. +function BEACONS:GetClosestBeacon(Coordinate, TypeID, DistMax, ExcludeList) + + local beacon=nil --#BEACONS.Beacon + local distmin=math.huge + + ExcludeList=ExcludeList or {} + + for _,_beacon in pairs(self.beacons) do + local bc=_beacon --#BEACONS.Beacon + + if (TypeID==nil or TypeID==bc.type) and (not UTILS.IsInTable(ExcludeList, bc, "beaconId")) then + + local dist=Coordinate:Get2DDistance(bc.vec3) + + if dist=1e6 then + freq=freq/1e6 + unit="MHz" + elseif freq>=1e3 then + freq=freq/1e3 + unit="kHz" + end + + return freq, unit +end + +--- Get name of beacon type. +-- @param #BEACONS self +-- @param #number typeID Beacon type number. +-- @return #string Type name. +function BEACONS:_GetTypeName(typeID) + + if typeID~=nil then + for typeName,_typeID in pairs(BEACON.Type) do + if _typeID==typeID then + return typeName + end + end + end + + return "Unknown" +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Navigation/Point.lua b/Moose Development/Moose/Navigation/Point.lua new file mode 100644 index 000000000..2c8d1e378 --- /dev/null +++ b/Moose Development/Moose/Navigation/Point.lua @@ -0,0 +1,587 @@ +--- **NAVIGATION** - Navigation Airspace Points, Fixes and Aids. +-- +-- **Main Features:** +-- +-- * Navigation Fixes +-- * Navigation Aids +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Navigation%20-%20NavFix). +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- @module Navigation.Point +-- @image NAVIGATION_Point.png + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- NAVFIX class. +-- @type NAVFIX +-- +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity of output. +-- @field #string name Name of the point. +-- @field #string typePoint Type of the point, *e.g. "Intersection", "VOR", "Airport". +-- @field Core.Vector#VECTOR vector Position vector of the fix. +-- @field Wrapper.Marker#MARKER marker Marker on F10 map. +-- @field #number altMin Minimum altitude in meters. +-- @field #number altMax Maximum altitude in meters. +-- @field #number speedMin Minimum speed in knots. +-- @field #number speedMax Maximum speed in knots. +-- +-- @field #boolean isCompulsory Is this a compulsory fix. +-- @field #boolean isFlyover Is this a flyover fix (`true`) or turning point otherwise. +-- @field #boolean isFAF Is this a final approach fix. +-- @field #boolean isIAF Is this an initial approach fix. +-- @field #boolean isIF Is this an initial fix. +-- @field #boolean isMAF Is this an initial fix. +-- +-- @extends Core.Base#BASE + +--- *A fleet of British ships at war are the best negotiators.* -- Horatio Nelson +-- +-- === +-- +-- # The NAVFIX Concept +-- +-- The NAVFIX class has a great concept! +-- +-- A NAVFIX describes a geo position and can, *e.g.*, be part of a FLIGHTPLAN. It has a unique name and is of a certain type, *e.g.* "Intersection", "VOR", "Airbase" etc. +-- It can also have further properties as min/max altitudes and speeds that aircraft need to obey when they pass the point. +-- +-- # Basic Setup +-- +-- A new `NAVFIX` object can be created with the @{#NAVFIX.New}() function. +-- +-- myNavPoint=NAVFIX:New() +-- myTemplate:SetXYZ(X, Y, Z) +-- +-- This is how it works. +-- +-- @field #NAVFIX +NAVFIX = { + ClassName = "NAVFIX", + verbose = 0, +} + +--- Type of point. +-- @type NAVFIX.Type +-- @field #string POINT Waypoint. +-- @field #string INTERSECTION Intersection of airway. +-- @field #string AIRPORT Airport. +-- @field #string VOR Very High Frequency Omnidirectional Range Station. +-- @field #string DME Distance Measuring Equipment. +-- @field #string NDB Non-Directional Beacon. +-- @field #string VORDME Combined VHF omnidirectional range (VOR) with a distance-measuring equipment (DME). +-- @field #string LOC Localizer. +-- @field #string ILS Instrument Landing System. +-- @field #string TACAN TACtical Air Navigation System (TACAN). +NAVFIX.Type={ + POINT="Point", + INTERSECTION="Intersection", + AIRPORT="Airport", + NDB="NDB", + VOR="VOR", + DME="DME", + VORDME="VOR/DME", + LOC="Localizer", + ILS="ILS", + TACAN="TACAN" +} + +--- NAVFIX class version. +-- @field #string version +NAVFIX.version="0.1.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: A lot... + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor(s) +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new NAVFIX class instance from a given VECTOR. +-- @param #NAVFIX self +-- @param #string Name Name/ident of the point. Should be unique! +-- @param #string Type Type of the point. Default `NAVFIX.Type.POINT`. +-- @param Core.Vector#VECTOR Vector Position vector of the navpoint. +-- @return #NAVFIX self +function NAVFIX:NewFromVector(Name, Type, Vector) + + -- Inherit everything from BASE class. + self=BASE:Inherit(self, BASE:New()) -- #NAVFIX + + -- Vector of point. + self.vector=Vector + + -- Name of point. + self.name=Name + + -- Type of the point. + self.typePoint=Type or NAVFIX.Type.POINT + + local coord=COORDINATE:NewFromVec3(self.vector) + + -- Marker on F10. + self.marker=MARKER:New(coord, self:_GetMarkerText()) + + -- Log ID string. + self.lid=string.format("NAVFIX %s [%s] | ", tostring(self.name), tostring(self.typePoint)) + + -- Debug info. + self:I(self.lid..string.format("Created NAVFIX")) + + return self +end + + +--- Create a new NAVFIX class instance from a given COORDINATE. +-- @param #NAVFIX self +-- @param #string Name Name of the fix. Should be unique! +-- @param #string Type Type of the point. Default `NAVFIX.Type.POINT`. +-- @param Core.Point#COORDINATE Coordinate Coordinate of the point. +-- @return #NAVFIX self +function NAVFIX:NewFromCoordinate(Name, Type, Coordinate) + + -- Create a VECTOR from the coordinate. + local Vector=VECTOR:NewFromVec(Coordinate) + + -- Create NAVFIX. + self=NAVFIX:NewFromVector(Name, Type, Vector) + + return self +end + + +--- Create a new NAVFIX instance from given latitude and longitude in degrees, minutes and seconds (DMS). +-- @param #NAVFIX self +-- @param #string Name Name of the fix. Should be unique! +-- @param #string Type Type of the point. Default `NAVFIX.Type.POINT`. +-- @param #string Latitude Latitude in DMS as string. +-- @param #string Longitude Longitude in DMS as string. +-- @return #NAVFIX self +function NAVFIX:NewFromLLDMS(Name, Type, Latitude, Longitude) + + -- Create a VECTOR from the coordinate. + local Vector=VECTOR:NewFromLLDMS(Latitude, Longitude) + + -- Create NAVFIX. + self=NAVFIX:NewFromVector(Name, Type, Vector) + + return self +end + +--- Create a new NAVFIX instance from given latitude and longitude in decimal degrees (DD). +-- @param #NAVFIX self +-- @param #string Name Name of the fix. Should be unique! +-- @param #string Type Type of the point. Default `NAVFIX.Type.POINT`. +-- @param #number Latitude Latitude in DD. +-- @param #number Longitude Longitude in DD. +-- @return #NAVFIX self +function NAVFIX:NewFromLLDD(Name, Type, Latitude, Longitude) + + -- Create a VECTOR from the coordinate. + local Vector=VECTOR:NewFromLLDD(Latitude, Longitude) + + -- Create NAVFIX. + self=NAVFIX:NewFromVector(Name, Type, Vector) + + return self +end + + +--- Create a new NAVFIX class instance relative to a given other NAVFIX. +-- You have to specify the distance and bearing from the new point to the given point. *E.g.*, for a distance of 5 NM and a bearing of 090° (West), the +-- new nav point is created 5 NM East of the given nav point. The reason is that this corresponts to convention used in most maps. +-- You can, however, use the `Reciprocal` switch to create the new point in the direction you specify. +-- @param #NAVFIX self +-- @param #string Name Name of the fix. Should be unique! +-- @param #string Type Type of navfix. +-- @param #NAVFIX NavFix The given/existing navigation fix relative to which the new fix is created. +-- @param #number Distance Distance from the given to the new point in nautical miles. +-- @param #number Bearing Bearing [Deg] from the new point to the given one. +-- @param #boolean Reciprocal If `true` the reciprocal `Bearing` is taken so it specifies the direction from the given point to the new one. +-- @return #NAVFIX self +function NAVFIX:NewFromNavFix(Name, Type, NavFix, Distance, Bearing, Reciprocal) + + -- Convert magnetic to true bearing by adding magnetic declination, e.g. mag. bearing 10°M ==> true bearing 16°M (for 6° variation on Caucasus map) + Bearing=Bearing+UTILS.GetMagneticDeclination() + + if Reciprocal then + Bearing=Bearing-180 + end + + -- Translate. + local Vector=NavFix.vector:Translate(UTILS.NMToMeters(Distance), Bearing, true) + + self=NAVFIX:NewFromVector(Name, Type, Vector) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set whether this is the intermediate fix (IF). +-- @param #NAVFIX self +-- @return #NAVFIX self +function NAVFIX:SetIntermediateFix(IntermediateFix) + self.isIF=IntermediateFix + return self +end + +--- Set whether this is an initial approach fix (IAF). +-- The IAF is the point where the initial approach segment of an instrument approach begins. +-- It is usually a designated intersection, VHF omidirectional range (VOR) non-directional beacon (NDB) +-- or distance measuring equipment (DME) fix. +-- The IAF may be collocated with the intermediate fix (IF) of the instrument apprach an in such case they designate the +-- beginning of the intermediate segment of the approach. When the IAF and the IF are combined, there is no inital approach segment. +-- @param #NAVFIX self +-- @param #boolean IntermediateFix If `true`, this is an intermediate fix. +-- @return #NAVFIX self +function NAVFIX:SetInitialApproachFix(IntermediateFix) + self.isIAF=IntermediateFix + return self +end + + +--- Set whether this is the final approach fix (FAF). +-- @param #NAVFIX self +-- @param #boolean FinalApproachFix If `true`, this is a final approach fix. +-- @return #NAVFIX self +function NAVFIX:SetFinalApproachFix(FinalApproachFix) + self.isFAF=FinalApproachFix + return self +end + +--- Set whether this is the final approach fix (FAF). +-- @param #NAVFIX self +-- @param #boolean FinalApproachFix If `true`, this is a final approach fix. +-- @return #NAVFIX self +function NAVFIX:SetMissedApproachFix(MissedApproachFix) + self.isMAF=MissedApproachFix + return self +end + + +--- Set minimum altitude. +-- @param #NAVFIX self +-- @param #number Altitude Min altitude in feet. +-- @return #NAVFIX self +function NAVFIX:SetAltMin(Altitude) + + self.altMin=Altitude + + return self +end + +--- Set maximum altitude. +-- @param #NAVFIX self +-- @param #number Altitude Max altitude in feet. +-- @return #NAVFIX self +function NAVFIX:SetAltMax(Altitude) + + self.altMax=Altitude + + return self +end + +--- Set mandatory altitude (min alt = max alt). +-- @param #NAVFIX self +-- @param #number Altitude Altitude in feet. +-- @return #NAVFIX self +function NAVFIX:SetAltMandatory(Altitude) + + self.altMin=Altitude + self.altMax=Altitude + + return self +end + +--- Set minimum allowed speed at this fix. +-- @param #NAVFIX self +-- @param #number Speed Min speed in knots. +-- @return #NAVFIX self +function NAVFIX:SetSpeedMin(Speed) + + self.speedMin=Speed + + return self +end + +--- Set maximum allowed speed at this fix. +-- @param #NAVFIX self +-- @param #number Speed Max speed in knots. +-- @return #NAVFIX self +function NAVFIX:SetSpeedMax(Speed) + + self.speedMax=Speed + + return self +end + +--- Set mandatory speed (min speed = max speed) at this fix. +-- @param #NAVFIX self +-- @param #number Speed Mandatory speed in knots. +-- @return #NAVFIX self +function NAVFIX:SetSpeedMandatory(Speed) + + self.speedMin=Speed + self.speedMax=Speed + + return self +end + + +--- Set whether this fix is compulsory. +-- @param #NAVFIX self +-- @param #boolean Compulsory If `true`, this is a compusory fix. If `false` or nil, it is non-compulsory. +-- @return #NAVFIX self +function NAVFIX:SetCompulsory(Compulsory) + self.isCompulsory=Compulsory + return self +end + +--- Set whether this is a fly-over fix fix. +-- @param #NAVFIX self +-- @param #boolean FlyOver If `true`, this is a fly over fix. If `false` or nil, it is not. +-- @return #NAVFIX self +function NAVFIX:SetFlyOver(FlyOver) + self.isFlyover=FlyOver + return self +end + + +--- Get the altitude in feet MSL. If min and max altitudes are set, it will return a random altitude between min and max. +-- @param #NAVFIX self +-- @return #number Altitude in feet MSL. Can be `nil`, if neither min nor max altitudes have beeen set. +function NAVFIX:GetAltitude() + + local alt=nil + if self.altMin and self.altMax and self.altMin~=self.altMax then + alt=math.random(self.altMin, self.altMax) + elseif self.altMin then + alt=self.altMin + elseif self.altMax then + alt=self.altMax + end + + return alt +end + + +--- Get the speed. If min and max speeds are set, it will return a random speed between min and max. +-- @param #NAVFIX self +-- @return #number Speed in knots. Can be `nil`, if neither min nor max speeds have beeen set. +function NAVFIX:GetSpeed() + + local speed=nil + if self.speedMin and self.speedMax and self.speedMin~=self.speedMax then + speed=math.random(self.speedMin, self.speedMax) + elseif self.speedMin then + speed=self.speedMin + elseif self.speedMax then + speed=self.speedMax + end + + return speed +end + + + +--- Add marker the NAVFIX on the F10 map. +-- @param #NAVFIX self +-- @return #NAVFIX self +function NAVFIX:MarkerShow() + + self.marker:ToAll() + + return self +end + +--- Remove marker of the NAVFIX from the F10 map. +-- @param #NAVFIX self +-- @return #NAVFIX self +function NAVFIX:MarkerRemove() + + self.marker:Remove() + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Private Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get text displayed in the F10 marker. +-- @param #NAVFIX self +-- @return #string Marker text. +function NAVFIX:_GetMarkerText() + + local altmin=self.altMin and tostring(self.altMin) or "" + local altmax=self.altMax and tostring(self.altMax) or "" + local speedmin=self.speedMin and tostring(self.speedMin) or "" + local speedmax=self.speedMax and tostring(self.speedMax) or "" + + + local text=string.format("NAVFIX %s", self.name) + if self.isIAF then + text=text..string.format(" (IAF)") + end + if self.isIF then + text=text..string.format(" (IF)") + end + text=text..string.format("\nAltitude [ft]: %s - %s", altmin, altmax) + text=text..string.format("\nSpeed [knots]: %s - %s", speedmin, speedmax) + text=text..string.format("\nCompulsory: %s", tostring(self.isCompulsory)) + text=text..string.format("\nFly Over: %s", tostring(self.isFlyover)) + + return text +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- NAVAID class. +-- @type NAVAID +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity of output. +-- @extends Navigation.Point#NAVFIX + +--- *A fleet of British ships at war are the best negotiators.* -- Horatio Nelson +-- +-- === +-- +-- # The NAVAID Concept +-- +-- A NAVAID consists of one or multiple FLOTILLAs. These flotillas "live" in a WAREHOUSE that has a phyiscal struction (STATIC or UNIT) and can be captured or destroyed. +-- +-- # Basic Setup +-- +-- A new `NAVAID` object can be created with the @{#NAVAID.New}(`WarehouseName`, `FleetName`) function, where `WarehouseName` is the name of the static or unit object hosting the fleet +-- and `FleetName` is the name you want to give the fleet. This must be *unique*! +-- +-- myFleet=NAVAID:New("myWarehouseName", "1st Fleet") +-- myFleet:SetPortZone(ZonePort1stFleet) +-- myFleet:Start() +-- +-- A fleet needs a *port zone*, which is set via the @{#NAVAID.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 @{#NAVAID.Start}() function. If the fleet is not started, it will not process any requests. +-- +-- @field #NAVAID +NAVAID = { + ClassName = "NAVAID", + verbose = 0, +} + +--- NAVAID class version. +-- @field #string version +NAVAID.version="0.1.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Add frequencies. Which unit MHz, kHz, Hz? +-- TODO: Add radial function + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new NAVAID class instance. +-- @param #NAVAID self +-- @param #string Name Name/ident of this navaid. +-- @param #string Type Type of the point. Default `NAVFIX.Type.POINT`. +-- @param #string ZoneName Name of the zone to scan the scenery. +-- @param #string SceneryName Name of the scenery object. +-- @return #NAVAID self +function NAVAID:NewFromScenery(Name, Type, ZoneName, SceneryName) + + -- Get the zone. + local zone=ZONE:FindByName(ZoneName) + + -- Get coordinate. + local Coordinate=zone:GetCoordinate() + + -- Inherit everything from NAVFIX class. + self=BASE:Inherit(self, NAVFIX:NewFromCoordinate(Name, Type, Coordinate)) -- #NAVAID + + -- Set zone. + self.zone=ZONE:FindByName(ZoneName) + + -- Try to get the scenery object. Note not all can be found unfortunately. + if SceneryName then + self.scenery=SCENERY:FindByNameInZone(SceneryName, ZoneName) + if not self.scenery then + self:E(string.format("ERROR: Could not find scenery object %s in zone %s", SceneryName, ZoneName)) + end + end + + -- Alias. + self.alias=string.format("%s %s %s", tostring(ZoneName), tostring(SceneryName), tostring(Type)) + + -- Set some string id for output to DCS.log file. + self.lid=string.format("NAVAID %s | ", self.alias) + + -- Debug info. + self:I(self.lid..string.format("Created NAVAID!")) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set frequency the beacon transmits on. +-- @param #NAVAID self +-- @param #number Frequency Frequency in Hz. +-- @return #NAVAID self +function NAVAID:SetFrequency(Frequency) + + self.frequency=Frequency + + return self +end + +--- Set channel of, *e.g.*, TACAN beacons. +-- @param #NAVAID self +-- @param #number Channel The channel. +-- @param #string Band The band either `"X"` (default) or `"Y"`. +-- @return #NAVAID self +function NAVAID:SetChannel(Channel, Band) + + self.channel=Channel + self.band=Band or "X" + + return self +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Private Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- Add private CLASS functions here. +-- No private NAVAID functions yet. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Navigation/Radios.lua b/Moose Development/Moose/Navigation/Radios.lua new file mode 100644 index 000000000..910235109 --- /dev/null +++ b/Moose Development/Moose/Navigation/Radios.lua @@ -0,0 +1,422 @@ +--- **NAVIGATION** - Airbase radios. +-- +-- **Main Features:** +-- +-- * Get radio frequencies of airbases +-- * Find closest airbase radios +-- * Mark radio frequencies on F10 map +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Navigation%20-%20Radios). +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- @module Navigation.Radios +-- @image NAVIGATION_Radios.png + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- RADIOS class. +-- @type RADIOS +-- +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity of output. +-- @field #table radios Radios. +-- +-- @extends Core.Base#BASE + +--- *It's not true I had nothing on, I had the radio on.* -- *Marilyn Monroe* +-- +-- === +-- +-- # The RADIOS Concept +-- +-- This class is desinged to make information about radios of a map/theatre easier accessible. The information contains mostly the frequencies of airbases of the map. +-- +-- **Note** that try to avoid hard coding stuff in Moose since DCS is updated frequently and things change. Therefore, the main source of information is either a file `radio.lua` that can be +-- found in the installation directory of DCS for each map or a table that the user needs to provide. +-- +-- # Basic Setup +-- +-- A new `RADIOS` object can be created with the @{#RADIOS.NewFromFile}(*radio_lua_file*) function. +-- +-- local radios=RADIOS:NewFromFile("\Mods\terrains\\radios.lua") +-- radios:MarkerShow() +-- +-- This will load the radios from the `` for the specific map and place markers on the F10 map. This is the first step you should do to ensure that the file +-- you provided is correct and all relevant radios are present. +-- +-- # User Functions +-- +-- ## F10 Map Markers +-- +-- ## Position +-- +-- ## Closest Radio +-- +-- +-- @field #RADIOS +RADIOS = { + ClassName = "RADIOS", + verbose = 0, + radios = {}, +} + +--- Radio item data structure. +-- @type RADIOS.Radio +-- @field #string radioId Radio ID. +-- @field #table role Roles of the radio (usually {"ground", "tower", "approach"}). +-- @field #table callsign Callsigns of the radio (usually the airbase name). +-- @field #table frequency Frequencies of the radios. +-- @field #table position Position table. +-- @field #table sceneObjects Scenery objects. +-- @field #string name Name of the airbase. +-- @field Wrapper.Airbase#AIRBASE airbase Airbase. +-- @field Core.Point#COORDINATE coordinate The COORDINATE of the radio. +-- @field DCS#Vec3 vec3 3D vector. +-- @field #number markerID Marker ID. + +--- Radio item data structure. +-- @type RADIOS.Frequency +-- @field #number modu Modulation type. +-- @field #number freq Frequency in Hz. + + +--- RADIOS class version. +-- @field #string version +RADIOS.version="0.1.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: A lot... + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor(s) +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new RADIOS class instance from a given table. +-- @param #RADIOS self +-- @param #table RadioTable Table with radios info. +-- @return #RADIOS self +function RADIOS:NewFromTable(RadioTable) + + -- Inherit everything from BASE class. + self=BASE:Inherit(self, BASE:New()) -- #RADIOS + + --local airbasenames=AIRBASE.GetAllAirbaseNames() + + -- Get all airdromes + local airdromes=AIRBASE.GetAllAirbases(nil, Airbase.Category.AIRDROME) + + for _,_radio in pairs(RadioTable) do + local radio=_radio --#RADIOS.Radio + + -- The table structure of callsign is a bit awkward. We need to get the airbase name. + -- Note that unfortunately, the callsign does not always correspond to the airbase name. + if false then + local cs=radio.callsign[1] + if cs and cs.common then + radio.name=cs.common[1] + elseif cs and cs.nato then + radio.name=cs.nato[1] + else + radio.name="Unknown" + end + radio.name=self:_GetAirbaseName(airbasenames, radio.name) + radio.airbase=AIRBASE:FindByName(radio.name) + end + + -- Each radio item has a key radioId = 'airfield106_0', where 106 is the UID of the airbase. + -- So we can use that to get the airbase. + local aid = tonumber(string.match(radio.radioId, "airfield(%d+)_")) + + -- Get airbase + radio.airbase=self:_GetAirbaseByID(airdromes, aid) + + -- Set other stuff + if radio.airbase then + radio.coordinate=radio.airbase:GetCoordinate() + radio.vec3=radio.airbase:GetVec3() + radio.name=radio.airbase:GetName() + end + + -- Add to table + table.insert(self.radios, radio) + end + + -- Debug output + self:I(string.format("Added %d radios", #self.radios)) + + return self +end + + +--- Create a new RADIOS class instance from a given file. +-- @param #RADIOS self +-- @param #string FileName Full path to the file containing the map radios. +-- @return #RADIOS self +function RADIOS:NewFromFile(FileName) + + -- Inherit everything from BASE class. + self=BASE:Inherit(self, BASE:New()) -- #RADIOS + + local exists=UTILS.FileExists(FileName) + + if exists==false then + self:E(string.format("ERROR: file with radios info does not exist! File=%s", tostring(FileName))) + return nil + end + + -- Backup DCS radio table + local radiobak=UTILS.DeepCopy(radio) + + -- This will create a global table `radio` + dofile(FileName) + + -- Get radios from table. + self=self:NewFromTable(radio) + + -- Restore DCS radio table + radio=UTILS.DeepCopy(radiobak) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get 3D position vector of a specific radio. +-- @param #RADIOS self +-- @param #RADIOS.Radio radio The radio data structure. +-- @return DCS#Vec3 Position vector. +function RADIOS:GetVec3(radio) + return radio.vec3 +end + +--- Get COORDINATE of a specific radio. +-- @param #RADIOS self +-- @param #RADIOS.Radio radio The radio data structure. +-- @return Core.Point#COORDINATE The coordinate. +function RADIOS:GetCoordinate(radio) + return radio.coordinate +end + + +--- Find closest radio to a given coordinate. +-- @param #RADIOS self +-- @param Core.Point#COORDINATE Coordinate The reference coordinate. +-- @param #number DistMax (Optional) Max search distance in meters. +-- @param #table ExcludeList (Optional) List of radios to exclude. +-- @return #RADIOS.Radio The closest radio. +function RADIOS:GetClosestRadio(Coordinate, DistMax, ExcludeList) + + local radio=nil --#RADIOS.Radio + local distmin=math.huge + + ExcludeList=ExcludeList or {} + + for _,_radio in pairs(self.radios) do + local ra=_radio --#RADIOS.Radio + + if (not UTILS.IsInTable(ExcludeList, ra, "radioId")) then + + local dist=Coordinate:Get2DDistance(ra.coordinate) + + if dist=1e6 then + freq=freq/1e6 + unit="MHz" + elseif freq>=1e3 then + freq=freq/1e3 + unit="kHz" + end + + return freq, unit +end + +--- Get name of frequency band. +-- @param #RADIOS self +-- @param #number BandNumber Band as number. +-- @return #string Band name. +function RADIOS:_GetBandName(BandNumber) + + if BandNumber~=nil then + for bandName,bandNumber in pairs(ENUMS.FrequencyBand) do + if bandNumber==BandNumber then + return bandName + end + end + end + + return "Unknown" +end + +--- Get name of frequency band. +-- @param #RADIOS self +-- @param #table airbasenames Names of all airbases. +-- @param #string name Name of airbase. +-- @return #string Name of airbase +function RADIOS:_GetAirbaseName(airbasenames, name) + + local airbase=AIRBASE:FindByName(name) + + if airbase then + return name + else + for _,airbasename in pairs(airbasenames) do + if string.find(airbasename, name) then + return airbasename + end + end + end + + return "Unknown" +end + +--- Get name of frequency band. +-- @param #RADIOS self +-- @param #table airbases Table of airbases. +-- @param #number aid Airbase ID. +-- @return Wrapper.Airbase#AIRBASE Airbase matching the ID or nil. +function RADIOS:_GetAirbaseByID(airbases, aid) + + for _,_airbase in pairs(airbases) do + local airbase=_airbase --Wrapper.Airbase#AIRBASE + local id=airbase:GetID(true) + if id==aid then + return airbase + end + end + + return nil +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Navigation/Towns.lua b/Moose Development/Moose/Navigation/Towns.lua new file mode 100644 index 000000000..c5f67efbd --- /dev/null +++ b/Moose Development/Moose/Navigation/Towns.lua @@ -0,0 +1,331 @@ +--- **NAVIGATION** - Towns of the map/theatre. +-- +-- **Main Features:** +-- +-- * Find towns of map +-- * Road and rail connections +-- * Find closest town to a given coordinate +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Navigation%20-%20Towns). +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- @module Navigation.Towns +-- @image NAVIGATION_Towns.png + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- TOWNS class. +-- @type TOWNS +-- +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity of output. +-- @field #table towns Towns. +-- +-- @extends Core.Base#BASE + +--- *Hope is the beacon that guides lost ships back to the shore.* +-- +-- === +-- +-- # The TOWNS Concept +-- +-- This class is desinged to make information about towns of a map/theatre easier accessible. The information contains location and road/rail connections of the towns. +-- +-- **Note** that try to avoid hard coding stuff in Moose since DCS is updated frequently and things change. Therefore, the main source of information is either a file `towns.lua` that can be +-- found in the installation directory of DCS for each map or a table that the user needs to provide. +-- +-- # Basic Setup +-- +-- A new `TOWNS` object can be created with the @{#TOWNS.NewFromFile}(*towns_lua_file*) function. +-- +-- local towns=TOWNS:NewFromFile("\Mods\terrains\\towns.lua") +-- towns:MarkerShow() +-- +-- This will load the towns from the `` for the specific map and place markers on the F10 map. This is the first step you should do to ensure that the file +-- you provided is correct and all relevant towns are present. +-- +-- # User Functions +-- +-- ## F10 Map Markers +-- +-- ## Position +-- +-- ## Get Closest Town +-- +-- +-- @field #TOWNS +TOWNS = { + ClassName = "TOWNS", + verbose = 0, + towns = {}, +} + +--- Town data. +-- @type TOWNS.Town +-- @field #string display_name Displayed name. +-- @field #string name Name of the town. +-- @field #number latitude Latitude. +-- @field #number longitude Longitude +-- @field DCS#Vec3 vec3 Position vector 3D. +-- @field Core.Point#COORDINATE coordinate The coordinate. +-- @field Core.Point#COORDINATE coordRoad The coordinate of the closest road. +-- @field Core.Point#COORDINATE coordRail The coordinate of the closest railway. +-- @field #number markerID ID for the F10 marker. + +--- TOWNS class version. +-- @field #string version +TOWNS.version="0.1.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: A lot... +-- DONE: Road connection +-- DONE: Rail connection +-- DONE: Connection between towns + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor(s) +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new TOWNS class instance from a given table. +-- @param #TOWNS self +-- @param #table TownTable Table with all towns data. +-- @return #TOWNS self +function TOWNS:NewFromTable(TownTable) + + -- Inherit everything from BASE class. + self=BASE:Inherit(self, BASE:New()) -- #TOWNS + + for TownName,_town in pairs(TownTable) do + local town=_town --#TOWNS.Town + + town.name=TownName + + -- Get coordinate + town.coordinate=COORDINATE:NewFromLLDD(town.latitude, town.longitude) + + -- Get coordinate of closest road + town.coordRoad=town.coordinate:GetClosestPointToRoad() + + -- Get coordinate of closest rail + town.coordRail=town.coordinate:GetClosestPointToRoad(true) + + -- Add to table + table.insert(self.towns, town) + end + + -- Debug output + self:I(string.format("Added %d towns", #self.towns)) + + return self +end + + +--- Create a new TOWNS class instance from a given file. +-- @param #TOWNS self +-- @param #string FileName Full path to the file containing the towns data. +-- @return #TOWNS self +function TOWNS:NewFromFile(FileName) + + -- Inherit everything from BASE class. + self=BASE:Inherit(self, BASE:New()) -- #TOWNS + + local exists=UTILS.FileExists(FileName) + + if exists==false then + self:E(string.format("ERROR: file with towns info does not exist!")) + return nil + end + + -- This will create a global table `towns` + dofile(FileName) + + -- Get towns from table. + self=self:NewFromTable(towns) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get 3D position vector of a specific town. +-- @param #TOWNS self +-- @param #TOWNS.Town town The town data structure. +-- @return DCS#Vec3 Position vector. +function TOWNS:GetVec3(town) + return town.vec3 +end + +--- Get COORDINATE of a specific town. +-- @param #TOWNS self +-- @param #TOWNS.Town town The town data structure. +-- @return Core.Point#COORDINATE The coordinate. +function TOWNS:GetCoordinate(town) + return town.coordinate +end + +--- Get closest road coordinate of a town. +-- @param #TOWNS self +-- @param #TOWNS.Town town The town data structure. +-- @return Core.Point#COORDINATE The closest road coordinate. +function TOWNS:GetCoordRoad(town) + return town.coordRoad +end + +--- Get closest rail coordinate of a town. +-- @param #TOWNS self +-- @param #TOWNS.Town town The town data structure. +-- @return Core.Point#COORDINATE The closest rail coordinate. +function TOWNS:GetCoordRail(town) + return town.coordRail +end + +--- Get road or rail connection between two towns. +-- @param #TOWNS self +-- @param #TOWNS.Town townA The town data structure. +-- @param #TOWNS.Town townB The town data structure. +-- @param #boolean Railroad If `true`, find rail road connection +-- @return Core.Pathline#PATHLINE Pathline connecting the two towns on road. +function TOWNS:GetConnectionRoad(townA, townB, Railroad) + + local path=townA.coordRoad:GetPathlineOnRoad(townB.coordRoad, false, Railroad) + + return path +end + +--- Find closest town to a given coordinate. +-- @param #TOWNS self +-- @param Core.Point#COORDINATE Coordinate The reference coordinate. +-- @param #number DistMax (Optional) Max search distance in meters. +-- @param #table ExcludeList (Optional) List of towns excluded from the search. +-- @return #TOWNS.Town The closest town. +function TOWNS:GetClosestTown(Coordinate, DistMax, ExcludeList) + + local Town=nil --#TOWNS.Town + local distmin=math.huge + ExcludeList=ExcludeList or {} + + for _,_town in pairs(self.towns) do + local town=_town --#TOWNS.Town + + if (not UTILS.IsInTable(ExcludeList, town, "name")) then + + local dist=Coordinate:Get2DDistance(town.coordinate) + + if dist100 or los==false then - --env.info("FF Update Engage Target Moved "..self.engage.Target:GetName()) - -- Update new position. self.engage.Coordinate:UpdateFromVec3(vec3) @@ -1852,13 +1850,14 @@ function ARMYGROUP:_UpdateEngageTarget() -- Remove current waypoint self:RemoveWaypointByID(self.engage.Waypoint.uid) - local intercoord=self:GetCoordinate():GetIntermediateCoordinate(self.engage.Coordinate, 0.9) + -- Get new coordinate where to go. + local intercoord=self:GetCoordinate():GetIntermediateCoordinate(self.engage.Coordinate, 0.95) -- Add waypoint after current. self.engage.Waypoint=self:AddWaypoint(intercoord, self.engage.Speed, uid, self.engage.Formation, true) -- Set if we want to resume route after reaching the detour waypoint. - self.engage.Waypoint.detour=0 + self.engage.Waypoint.detour=1 end diff --git a/Moose Development/Moose/Ops/Auftrag.lua b/Moose Development/Moose/Ops/Auftrag.lua index a425eacae..cdad223f1 100644 --- a/Moose Development/Moose/Ops/Auftrag.lua +++ b/Moose Development/Moose/Ops/Auftrag.lua @@ -324,6 +324,10 @@ -- -- A ground attack mission can be created with the @{#AUFTRAG.NewGROUNDATTACK}() function. -- +-- ## NAVALENGAGEMENT +-- +-- A naval engagement mission can be created with the @{#AUFTRAG.NewNAVALENGAGEMENT}() function. +-- -- # Assigning Missions -- -- An AUFTRAG can be assigned to groups (FLIGHTGROUP, ARMYGROUP, NAVYGROUP), legions (AIRWING, BRIGADE, FLEET) or to a COMMANDER. @@ -443,6 +447,7 @@ _AUFTRAGSNR=0 -- @field #string HOVER Hover. -- @field #string LANDATCOORDINATE Land at coordinate. -- @field #string GROUNDATTACK Ground attack. +-- @field #string NAVALENGAGEMENT Naval engagement (similar to GROUNDATTACK). -- @field #string CARGOTRANSPORT Cargo transport. -- @field #string RELOCATECOHORT Relocate a cohort from one legion to another. -- @field #string AIRDEFENSE Air defense. @@ -491,6 +496,7 @@ AUFTRAG.Type={ HOVER="Hover", LANDATCOORDINATE="Land at Coordinate", GROUNDATTACK="Ground Attack", + NAVALENGAGEMENT="Naval Engagement", CARGOTRANSPORT="Cargo Transport", RELOCATECOHORT="Relocate Cohort", AIRDEFENSE="Air Defence", @@ -515,6 +521,7 @@ AUFTRAG.Type={ -- @field #string BARRAGE Barrage. -- @field #string HOVER Hover. -- @field #string GROUNDATTACK Ground attack. +-- @field #string NAVALENGAGEMENT Naval engagement. -- @field #string FERRY Ferry mission. -- @field #string RELOCATECOHORT Relocate cohort. -- @field #string AIRDEFENSE Air defense. @@ -537,6 +544,7 @@ AUFTRAG.SpecialTask={ ARMORATTACK="AmorAttack", HOVER="Hover", GROUNDATTACK="Ground Attack", + NAVALENGAGEMENT="Naval Engagement", FERRY="Ferry", RELOCATECOHORT="Relocate Cohort", AIRDEFENSE="Air Defense", @@ -666,7 +674,7 @@ AUFTRAG.Category={ --- AUFTRAG class version. -- @field #string version -AUFTRAG.version="1.2.1" +AUFTRAG.version="1.3.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -2210,7 +2218,7 @@ end -- **Note** that it is recommended to set the weapon range via the `OPSGROUP:AddWeaponRange()` function as this cannot be retrieved from the DCS API. -- @param #AUFTRAG self -- @param Core.Point#COORDINATE Target Center of the firing solution. --- @param #number Nshots Number of shots to be fired. Default `#nil`. +-- @param #number Nshots Number of shots to be fired. Default `#nil`. If value is in (0,1), it is interpreted as per cent of available ammo. -- @param #number Radius Radius of the shells in meters. Default 100 meters. -- @param #number Altitude Altitude in meters. Can be used to setup a Barrage. Default `#nil`. -- @return #AUFTRAG self @@ -2382,11 +2390,13 @@ function AUFTRAG:NewARMORATTACK(Target, Speed, Formation) return mission end ---- **[GROUND]** Create a GROUNDATTACK mission. Ground group(s) will go to a target object and attack. +--- **[GROUND]** Create a GROUNDATTACK mission. Ground group(s) will go to a target object and attack at their own discretion. +-- Unfortunately, the "Attack Group" and "Attack Unit" tasks do not work for ground and naval groups (only for aircraft). +-- Therefore, we resort to this workaround, which guides the attacking group to the vicinity of the target. Then they start shooting on their own, once they detect the target. -- @param #AUFTRAG self -- @param Wrapper.Positionable#POSITIONABLE Target The target to attack. Can be a GROUP, UNIT or STATIC object. -- @param #number Speed Speed in knots. Default max. --- @param #string Formation The attack formation, e.g. "Wedge", "Vee" etc. Default `ENUMS.Formation.Vehicle.Vee`. +-- @param #string Formation The attack formation, e.g. "Wedge", "Vee" etc. Default `ENUMS.Formation.Vehicle.Vee`. Only working for ground, not naval! -- @return #AUFTRAG self function AUFTRAG:NewGROUNDATTACK(Target, Speed, Formation) @@ -2413,6 +2423,38 @@ function AUFTRAG:NewGROUNDATTACK(Target, Speed, Formation) return mission end +--- **[NAVAL]** Create a NAVALENGAGEMENT mission. Naval group(s) will go to a target object and attack at their own discretion. +-- Unfortunately, the "Attack Group" and "Attack Unit" tasks do not work for ground and naval groups (only for aircraft). +-- Therefore, we resort to this workaround, which guides the attacking group to the vicinity of the target. Then they start shooting on their own, once they detect the target. +-- @param #AUFTRAG self +-- @param Wrapper.Positionable#POSITIONABLE Target The target to attack. Can be a GROUP, UNIT or STATIC object. +-- @param #number Speed Speed in knots. Default max. +-- @param #number Depth The attack depth in meters. Only for submarines! +-- @return #AUFTRAG self +function AUFTRAG:NewNAVALENGAGEMENT(Target, Speed, Depth) + + local mission=AUFTRAG:New(AUFTRAG.Type.NAVALENGAGEMENT) + + mission:_TargetFromObject(Target) + + mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.NAVALENGAGEMENT) + + -- Defaults. + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionAlarm=ENUMS.AlarmState.Auto + mission.missionFraction=0.70 + mission.missionSpeed=Speed and UTILS.KnotsToKmph(Speed) or nil + mission.missionAltitude=Depth or 0 + + mission.categories={AUFTRAG.Category.NAVAL} + + mission.DCStask=mission:GetDCSMissionTask() + + mission.DCStask.params.speed=mission.missionSpeed and UTILS.KmphToMps(mission.missionSpeed) or nil + + return mission +end + --- **[AIR, GROUND, NAVAL]** Create a RECON mission. -- @param #AUFTRAG self -- @param Core.Set#SET_ZONE ZoneSet The recon zones. @@ -2705,33 +2747,35 @@ function AUFTRAG:NewFromTarget(Target, MissionType) local mission=nil --#AUFTRAG if MissionType==AUFTRAG.Type.ANTISHIP then - mission=self:NewANTISHIP(Target, Altitude) + mission=self:NewANTISHIP(Target) elseif MissionType==AUFTRAG.Type.ARTY then - mission=self:NewARTY(Target, Nshots, Radius) + mission=self:NewARTY(Target, 0.3) -- use 30% of the available ammo elseif MissionType==AUFTRAG.Type.BAI then - mission=self:NewBAI(Target, Altitude) + mission=self:NewBAI(Target) elseif MissionType==AUFTRAG.Type.BOMBCARPET then - mission=self:NewBOMBCARPET(Target, Altitude, CarpetLength) + mission=self:NewBOMBCARPET(Target) elseif MissionType==AUFTRAG.Type.BOMBING then - mission=self:NewBOMBING(Target, Altitude) + mission=self:NewBOMBING(Target) elseif MissionType==AUFTRAG.Type.BOMBRUNWAY then - mission=self:NewBOMBRUNWAY(Target, Altitude) + mission=self:NewBOMBRUNWAY(Target) elseif MissionType==AUFTRAG.Type.STRAFING then - mission=self:NewSTRAFING(Target, Altitude) + mission=self:NewSTRAFING(Target) elseif MissionType==AUFTRAG.Type.CAS then - mission=self:NewCAS(ZONE_RADIUS:New(Target:GetName(),Target:GetVec2(),1000), Altitude, Speed, Target:GetAverageCoordinate(), Heading, Leg, TargetTypes) + mission=self:NewCAS(ZONE_RADIUS:New(Target:GetName(),Target:GetVec2(),1000), nil, nil, Target:GetAverageCoordinate()) elseif MissionType==AUFTRAG.Type.CASENHANCED then - mission=self:NewCASENHANCED(ZONE_RADIUS:New(Target:GetName(),Target:GetVec2(),1000), Altitude, Speed, RangeMax, NoEngageZoneSet, TargetTypes) + mission=self:NewCASENHANCED(ZONE_RADIUS:New(Target:GetName(),Target:GetVec2(),1000)) elseif MissionType==AUFTRAG.Type.INTERCEPT then mission=self:NewINTERCEPT(Target) elseif MissionType==AUFTRAG.Type.SEAD then - mission=self:NewSEAD(Target, Altitude) + mission=self:NewSEAD(Target) elseif MissionType==AUFTRAG.Type.STRIKE then - mission=self:NewSTRIKE(Target, Altitude) + mission=self:NewSTRIKE(Target) elseif MissionType==AUFTRAG.Type.ARMORATTACK then - mission=self:NewARMORATTACK(Target, Speed) + mission=self:NewARMORATTACK(Target) elseif MissionType==AUFTRAG.Type.GROUNDATTACK then - mission=self:NewGROUNDATTACK(Target, Speed, Formation) + mission=self:NewGROUNDATTACK(Target) + elseif MissionType==AUFTRAG.Type.NAVALENGAGEMENT then + mission=self:NewNAVALENGAGEMENT(Target) else return nil end @@ -2848,7 +2892,7 @@ function AUFTRAG:NewAUTO(EngageGroup) if auftrag==AUFTRAG.Type.ANTISHIP then mission=AUFTRAG:NewANTISHIP(Target) elseif auftrag==AUFTRAG.Type.ARTY then - mission=AUFTRAG:NewARTY(Target) + mission=AUFTRAG:NewARTY(Target, 0.2) elseif auftrag==AUFTRAG.Type.AWACS then mission=AUFTRAG:NewAWACS(Coordinate, Altitude,Speed,Heading,Leg) elseif auftrag==AUFTRAG.Type.BAI then @@ -4399,7 +4443,7 @@ function AUFTRAG:onafterStatus(From, Event, To) -- Group info. if self.verbose>=3 then -- Data on assigned groups. - local text=string.format("Assets [N=%d,Nassigned=%s, Ndead=%s]:", self.Nassets or 0, self.Nassigned or 0, self.Ndead or 0) + local text=string.format("Assets [N=%d, Nassigned=%s, Ndead=%s]:", self.Nassets or 0, self.Nassigned or 0, self.Ndead or 0) for i,_asset in pairs(self.assets or {}) do local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem text=text..string.format("\n[%d] %s: spawned=%s, requested=%s, reserved=%s", i, asset.spawngroupname, tostring(asset.spawned), tostring(asset.requested), tostring(asset.reserved)) @@ -4440,7 +4484,7 @@ function AUFTRAG:Evaluate() local owndamage=self.Ncasualties/self.Nelements*100 -- Current number of mission targets. - local Ntargets=self:CountMissionTargets() + local Ntargets=self:CountMissionTargets(true) local Ntargets0=self:GetTargetInitialNumber() local Life=self:GetTargetLife() @@ -4891,16 +4935,25 @@ function AUFTRAG:CheckGroupsDone() self:T2(self.lid..string.format("CheckGroupsDone: Mission is still in state %s [FSM=%s] and reinfoce=%d. Mission NOT DONE!", self.status, self:GetState(), self.reinforce)) return false end + + local NopsgroupsAlive=self:CountOpsGroups() + local NopsgroupsDone=self:CountOpsGroupsInStatus(AUFTRAG.GroupStatus.DONE)+self:CountOpsGroupsInStatus(AUFTRAG.GroupStatus.CANCELLED) -- It could be that all groups were destroyed on the way to the mission execution waypoint. -- TODO: would be better to check if everybody is dead by now. - if self:IsStarted() and self:CountOpsGroups()==0 then + if self:IsStarted() and NopsgroupsAlive==0 then self:T(self.lid..string.format("CheckGroupsDone: Mission is STARTED state %s [FSM=%s] but count of alive OPSGROUP is zero. Mission DONE!", self.status, self:GetState())) return true end - if (self:IsStarted() or self:IsExecuting()) and (fsmState == AUFTRAG.Status.STARTED or fsmState == AUFTRAG.Status.EXECUTING) and self:CountOpsGroups()>0 then - self:T(self.lid..string.format("CheckGroupsDone: Mission is STARTED state %s [FSM=%s] and count of alive OPSGROUP > zero. Mission NOT DONE!", self.status, self:GetState())) + -- Every group alive is done or cancelled + if NopsgroupsAlive==NopsgroupsDone then + self:T(self.lid..string.format("CheckGroupsDone: Mission is in state %s [FSM=%s] but all groups [=%d] are done or cancelled. Mission DONE!", self.status, self:GetState(), NopsgroupsAlive)) + return true + end + + if (self:IsStarted() or self:IsExecuting()) and (fsmState == AUFTRAG.Status.STARTED or fsmState == AUFTRAG.Status.EXECUTING) and NopsgroupsAlive>0 then + self:T(self.lid..string.format("CheckGroupsDone: Mission is in state %s [FSM=%s] and count of alive OPSGROUP > zero. Mission NOT DONE!", self.status, self:GetState())) return false end @@ -5494,8 +5547,9 @@ end --- Count alive mission targets. -- @param #AUFTRAG self +-- @param #boolean OnlyReallyAlive (Optional) If `true`, count only really alive targets (units, groups) but not coordinates or zones. -- @return #number Number of alive target units. -function AUFTRAG:CountMissionTargets() +function AUFTRAG:CountMissionTargets(OnlyReallyAlive) local N=0 @@ -5503,7 +5557,7 @@ function AUFTRAG:CountMissionTargets() local Coalitions=self.coalition and UTILS.GetCoalitionEnemy(self.coalition, true) or nil if self.engageTarget then - N=self.engageTarget:CountTargets(Coalitions) + N=self.engageTarget:CountTargets(Coalitions, OnlyReallyAlive) end return N @@ -6642,6 +6696,26 @@ function AUFTRAG:GetDCSMissionTask() table.insert(DCStasks, DCStask) + elseif self.type==AUFTRAG.Type.NAVALENGAGEMENT then + + --------------------------- + -- NAVAL ENGAGEMENT Mission -- + --------------------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.NAVALENGAGEMENT + + -- We create a "fake" DCS task and pass the parameters to the NAVYGROUP. + local param={} + param.target=self:GetTargetData() + param.speed=self.missionSpeed and UTILS.KmphToMps(self.missionSpeed) or nil + param.altitude=self.missionAltitude or 0 + + DCStask.params=param + + table.insert(DCStasks, DCStask) + elseif self.type==AUFTRAG.Type.AMMOSUPPLY then ------------------------- diff --git a/Moose Development/Moose/Ops/Brigade.lua b/Moose Development/Moose/Ops/Brigade.lua index b8382f136..5734e0f9c 100644 --- a/Moose Development/Moose/Ops/Brigade.lua +++ b/Moose Development/Moose/Ops/Brigade.lua @@ -599,7 +599,80 @@ function BRIGADE:onafterStatus(From, Event, To) text=text..string.format("\n* %s: spawned=%s", asset.spawngroupname, tostring(asset.spawned)) end self:I(self.lid..text) - end + end + + if self.verbose>=3 then + + -- Count numbers + local Ntotal=0 + local Nspawned=0 + local Nrequested=0 + local Nreserved=0 + local Nstock=0 + + local text="\n===========================================\n" + text=text.."Assets:" + local legion=self --Ops.Legion#LEGION + + for _,_cohort in pairs(legion.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + for _,_asset in pairs(cohort.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + local state="In Stock" + if asset.flightgroup then + state=asset.flightgroup:GetState() + local mission=legion:GetAssetCurrentMission(asset) + if mission then + state=state..string.format(", Mission \"%s\" [%s]", mission:GetName(), mission:GetType()) + end + else + if asset.spawned then + env.info("FF ERROR: asset has opsgroup but is NOT spawned!") + end + if asset.requested and asset.isReserved then + env.info("FF ERROR: asset is requested and reserved. Should not be both!") + state="Reserved+Requested!" + elseif asset.isReserved then + state="Reserved" + elseif asset.requested then + state="Requested" + end + end + + -- Text. + text=text..string.format("\n[UID=%03d] %s Legion=%s [%s]: State=%s [RID=%s]", + asset.uid, asset.spawngroupname, legion.alias, cohort.name, state, tostring(asset.rid)) + + + if asset.spawned then + Nspawned=Nspawned+1 + end + if asset.requested then + Nrequested=Nrequested+1 + end + if asset.isReserved then + Nreserved=Nreserved+1 + end + if not (asset.spawned or asset.requested or asset.isReserved) then + Nstock=Nstock+1 + end + + Ntotal=Ntotal+1 + + end + + end + text=text.."\n-------------------------------------------" + text=text..string.format("\nNstock = %d", Nstock) + text=text..string.format("\nNreserved = %d", Nreserved) + text=text..string.format("\nNrequested = %d", Nrequested) + text=text..string.format("\nNspawned = %d", Nspawned) + text=text..string.format("\nNtotal = %d (=%d)", Ntotal, Nstock+Nspawned+Nrequested+Nreserved) + text=text.."\n===========================================" + self:I(self.lid..text) + end end diff --git a/Moose Development/Moose/Ops/Chief.lua b/Moose Development/Moose/Ops/Chief.lua index 3e9f46561..9500ae313 100644 --- a/Moose Development/Moose/Ops/Chief.lua +++ b/Moose Development/Moose/Ops/Chief.lua @@ -35,7 +35,7 @@ -- @field #number Nsuccess Number of successful missions. -- @field #number Nfailure Number of failed mission. -- @field #table assetNumbers Asset numbers. Each entry is a table of data type `#CHIEF.AssetNumber`. --- @extends Ops.Intel#INTEL +-- @extends Ops.Intelligence#INTEL --- *In preparing for battle I have always found that plans are useless, but planning is indispensable* -- Dwight D Eisenhower -- @@ -48,7 +48,7 @@ -- -- # Territory -- --- The chief class allows you to define boarder zones, conflict zones and attack zones. +-- The chief class allows you to define border zones, conflict zones and attack zones. -- -- ## Border Zones -- @@ -332,7 +332,7 @@ CHIEF.Strategy = { --- CHIEF class version. -- @field #string version -CHIEF.version="0.6.1" +CHIEF.version="0.7.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -2897,6 +2897,8 @@ function CHIEF:_GetMissionPerformanceFromTarget(Target) table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY, 30)) else + + -- Everything else table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI, 100)) table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.GROUNDATTACK, 50)) @@ -2912,6 +2914,8 @@ function CHIEF:_GetMissionPerformanceFromTarget(Target) --- table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ANTISHIP, 100)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.NAVALENGAGEMENT, 50)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY, 30)) else self:E(self.lid.."ERROR: Unknown Group category!") diff --git a/Moose Development/Moose/Ops/Cohort.lua b/Moose Development/Moose/Ops/Cohort.lua index c18d838d6..48f63112c 100644 --- a/Moose Development/Moose/Ops/Cohort.lua +++ b/Moose Development/Moose/Ops/Cohort.lua @@ -629,32 +629,42 @@ end --- Remove assets from pool. Not that assets must not be spawned or already reserved or requested. -- @param #COHORT self -- @param #number N Number of assets to be removed. Default 1. +-- @param #number Delay Delay in seconds before assets are removed. -- @return #COHORT self -function COHORT:RemoveAssets(N) +function COHORT:RemoveAssets(N, Delay) self:T2(self.lid..string.format("Remove %d assets of Cohort", N)) - - N=N or 1 - local n=0 - for i=#self.assets,1,-1 do - local asset=self.assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + if Delay and Delay>0 then + -- Delayed call + self:ScheduleOnce(Delay, COHORT.RemoveAssets, self, N, 0) + else - self:T2(self.lid..string.format("Checking removing asset %s", asset.spawngroupname)) - if not (asset.requested or asset.spawned or asset.isReserved) then - self:T2(self.lid..string.format("Removing asset %s", asset.spawngroupname)) - table.remove(self.assets, i) - n=n+1 - else - self:T2(self.lid..string.format("Could NOT Remove asset %s", asset.spawngroupname)) + N=N or 1 + + local n=0 + for i=#self.assets,1,-1 do + local asset=self.assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + + self:T2(self.lid..string.format("Checking removing asset %s", asset.spawngroupname)) + if not (asset.requested or asset.spawned or asset.isReserved) then + self:T2(self.lid..string.format("Removing asset %s", asset.spawngroupname)) + -- Remove from warehouse and warehouse DB + asset.legion:_DeleteStockItem(asset) + table.remove(self.assets, i) + n=n+1 + else + self:T2(self.lid..string.format("Could NOT Remove asset %s", asset.spawngroupname)) + end + + if n>=N then + break + end + end - if n>=N then - break - end + self:T(self.lid..string.format("Removed %d/%d assets. New asset count=%d", n, N, #self.assets)) end - - self:T(self.lid..string.format("Removed %d/%d assets. New asset count=%d", n, N, #self.assets)) return self end diff --git a/Moose Development/Moose/Ops/Legion.lua b/Moose Development/Moose/Ops/Legion.lua index 8e3a3f10c..513ed07f0 100644 --- a/Moose Development/Moose/Ops/Legion.lua +++ b/Moose Development/Moose/Ops/Legion.lua @@ -1901,7 +1901,7 @@ function LEGION:_TacticalOverview() for _,mtype in pairs(AUFTRAG.Type) do local n=self:CountMissionsInQueue(mtype) if n>0 then - local N=self:CountMissionsInQueue(mtype) + local N=self:CountMissionsInQueue(mtype, true) text=text..string.format(" - %s: %d [Running=%d]\n", mtype, n, N) end end @@ -2057,18 +2057,23 @@ end --- Count missions in mission queue. -- @param #LEGION self -- @param #table MissionTypes Types on mission to be checked. Default *all* possible types `AUFTRAG.Type`. +-- @param #boolean OnlyRunning If `true`, only count running missions. -- @return #number Number of missions that are not over yet. -function LEGION:CountMissionsInQueue(MissionTypes) +function LEGION:CountMissionsInQueue(MissionTypes, OnlyRunning) MissionTypes=MissionTypes or AUFTRAG.Type local N=0 for _,_mission in pairs(self.missionqueue) do local mission=_mission --Ops.Auftrag#AUFTRAG + + if (not OnlyRunning) or (mission.statusLegion~=AUFTRAG.Status.PLANNED) then - -- Check if this mission type is requested. - if mission:IsNotOver() and AUFTRAG.CheckMissionType(mission.type, MissionTypes) then - N=N+1 + -- Check if this mission type is requested. + if mission:IsNotOver() and AUFTRAG.CheckMissionType(mission.type, MissionTypes) then + N=N+1 + end + end end diff --git a/Moose Development/Moose/Ops/NavyGroup.lua b/Moose Development/Moose/Ops/NavyGroup.lua index 85f03853d..5f2869d03 100644 --- a/Moose Development/Moose/Ops/NavyGroup.lua +++ b/Moose Development/Moose/Ops/NavyGroup.lua @@ -86,12 +86,14 @@ NAVYGROUP = { -- @field Ops.Target#TARGET Target The target. -- @field Core.Point#COORDINATE Coordinate Last known coordinate of the target. -- @field Ops.OpsGroup#OPSGROUP.Waypoint Waypoint the waypoint created to go to the target. +-- @field #number Speed Speed in knots. +-- @field #number Depth Depth of the engagement (submarines). -- @field #number roe ROE backup. -- @field #number alarmstate Alarm state backup. --- NavyGroup version. -- @field #string version -NAVYGROUP.version="1.0.3" +NAVYGROUP.version="1.0.4" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1081,7 +1083,7 @@ function NAVYGROUP:onafterSpawned(From, Event, To) text=text..string.format("Elements = %d\n", #self.elements) text=text..string.format("Waypoints = %d\n", #self.waypoints) text=text..string.format("Radio = %.1f MHz %s %s\n", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu), tostring(self.radio.On)) - text=text..string.format("Ammo = %d (G=%d/R=%d/M=%d/T=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Missiles, self.ammo.Torpedos) + text=text..string.format("Ammo = %d (G=%d/C=%d/R=%d/M=%d/T=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Cannons, self.ammo.Rockets, self.ammo.Missiles, self.ammo.Torpedos) text=text..string.format("FSM state = %s\n", self:GetState()) text=text..string.format("Is alive = %s\n", tostring(self:IsAlive())) text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated())) @@ -1602,8 +1604,10 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param Wrapper.Group#GROUP Group the group to be engaged. -function NAVYGROUP:onafterEngageTarget(From, Event, To, Target) +-- @param Ops.Target#TARGET Target The target to be engaged. Can also be a GROUP or UNIT object. +-- @param #number Speed Attack speed in knots. +-- @param #number Depth The depth in meters. Only for submarins. +function NAVYGROUP:onafterEngageTarget(From, Event, To, Target, Speed, Depth) self:T(self.lid.."Engaging Target") if Target:IsInstanceOf("TARGET") then @@ -1615,11 +1619,9 @@ function NAVYGROUP:onafterEngageTarget(From, Event, To, Target) -- Target coordinate. self.engage.Coordinate=UTILS.DeepCopy(self.engage.Target:GetCoordinate()) - - local intercoord=self:GetCoordinate():GetIntermediateCoordinate(self.engage.Coordinate, 0.9) + -- Get a coordinate close to the target. + local intercoord=self:GetCoordinate():GetIntermediateCoordinate(self.engage.Coordinate, 0.8) - - -- Backup ROE and alarm state. self.engage.roe=self:GetROE() self.engage.alarmstate=self:GetAlarmstate() @@ -1629,11 +1631,17 @@ function NAVYGROUP:onafterEngageTarget(From, Event, To, Target) self:SwitchROE(ENUMS.ROE.OpenFire) -- ID of current waypoint. - local uid=self:GetWaypointCurrent().uid + local uid=self:GetWaypointCurrentUID() + + -- Set formation. + self.engage.Depth=Depth or 0 + + -- Set speed. + self.engage.Speed=Speed -- Add waypoint after current. - self.engage.Waypoint=self:AddWaypoint(intercoord, nil, uid, Formation, true) - + self.engage.Waypoint=self:AddWaypoint(intercoord, Speed, uid, Depth, true) + -- Set if we want to resume route after reaching the detour waypoint. self.engage.Waypoint.detour=1 @@ -1656,24 +1664,22 @@ function NAVYGROUP:_UpdateEngageTarget() -- Check if target moved more than 100 meters. if dist>100 then - --env.info("FF Update Engage Target Moved "..self.engage.Target:GetName()) - -- Update new position. self.engage.Coordinate:UpdateFromVec3(vec3) -- ID of current waypoint. - local uid=self:GetWaypointCurrent().uid + local uid=self:GetWaypointCurrentUID() -- Remove current waypoint self:RemoveWaypointByID(self.engage.Waypoint.uid) - local intercoord=self:GetCoordinate():GetIntermediateCoordinate(self.engage.Coordinate, 0.9) + local intercoord=self:GetCoordinate():GetIntermediateCoordinate(self.engage.Coordinate, 0.8) -- Add waypoint after current. - self.engage.Waypoint=self:AddWaypoint(intercoord, nil, uid, Formation, true) + self.engage.Waypoint=self:AddWaypoint(intercoord, self.engage.Speed, uid, self.engage.Depth, true) -- Set if we want to resume route after reaching the detour waypoint. - self.engage.Waypoint.detour=0 + self.engage.Waypoint.detour=1 end @@ -1709,8 +1715,8 @@ function NAVYGROUP:onafterDisengage(From, Event, To) local task=self:GetTaskCurrent() -- Get if current task is ground attack. - if task and task.dcstask.id==AUFTRAG.SpecialTask.GROUNDATTACK then - self:T(self.lid.."Disengage with current task GROUNDATTACK ==> Task Done!") + if task and (task.dcstask.id==AUFTRAG.SpecialTask.GROUNDATTACK or task.dcstask.id==AUFTRAG.SpecialTask.NAVALENGAGEMENT) then + self:T(self.lid.."Disengage with current task GROUNDATTACK/NAVALENGAGEMENT ==> Task Done!") self:TaskDone(task) end diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index 24fc98f2d..08539aa5c 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -405,7 +405,9 @@ OPSGROUP.TaskType={ --- Ammo data. -- @type OPSGROUP.Ammo -- @field #number Total Total amount of ammo. --- @field #number Guns Amount of gun shells. +-- @field #number Shells Amount of shells (guns + cannons). +-- @field #number Guns Amount of gun shells (caliber < 25). +-- @field #number Cannons Amount of cannon shells (caliber >= 25). -- @field #number Bombs Amount of bombs. -- @field #number Rockets Amount of rockets. -- @field #number Torpedos Amount of torpedos. @@ -1189,8 +1191,8 @@ function OPSGROUP:GetDCSObject() return self.dcsgroup end ---- Set detection on or off. --- If detection is on, detected targets of the group will be evaluated and FSM events triggered. +--- Make a target (unit, group, opsgroup) known to this group. +-- This is useing the DCS function `controller.knowTarget`. -- @param #OPSGROUP self -- @param Wrapper.Positionable#POSITIONABLE TargetObject The target object. -- @param #boolean KnowType Make type known. @@ -4513,6 +4515,25 @@ function OPSGROUP:_UpdateTask(Task, Mission) if target then self:EngageTarget(target, speed, Task.dcstask.params.formation) end + + elseif Task.dcstask.id==AUFTRAG.SpecialTask.NAVALENGAGEMENT then + + --- + -- Task "Naval Engagement" Mission. + --- + + -- Engage target. + local target=Task.dcstask.params.target --Ops.Target#TARGET + + -- Set speed. Default max. + local speed=self.speedMax and UTILS.KmphToKnots(self.speedMax) or nil + if Task.dcstask.params.speed then + speed=UTILS.MpsToKnots(Task.dcstask.params.speed) + end + + if target then + self:EngageTarget(target, speed, Task.dcstask.params.altitude) + end elseif Task.dcstask.id==AUFTRAG.SpecialTask.PATROLRACETRACK then @@ -4739,7 +4760,7 @@ function OPSGROUP:_UpdateTask(Task, Mission) elseif weaponType==ENUMS.WeaponFlag.AnyRocket then nAmmo=ammo.Rockets elseif weaponType==ENUMS.WeaponFlag.Cannons then - nAmmo=ammo.Guns + nAmmo=ammo.Cannons end --TODO: Update target location while we're at it anyway. @@ -4886,7 +4907,7 @@ function OPSGROUP:onafterTaskCancel(From, Event, To, Task) done=true elseif Task.dcstask.id==AUFTRAG.SpecialTask.ONGUARD or Task.dcstask.id==AUFTRAG.SpecialTask.ARMOREDGUARD then done=true - elseif Task.dcstask.id==AUFTRAG.SpecialTask.GROUNDATTACK or Task.dcstask.id==AUFTRAG.SpecialTask.ARMORATTACK then + elseif Task.dcstask.id==AUFTRAG.SpecialTask.GROUNDATTACK or Task.dcstask.id==AUFTRAG.SpecialTask.ARMORATTACK or Task.dcstask.id==AUFTRAG.SpecialTask.NAVALENGAGEMENT then done=true elseif Task.dcstask.id==AUFTRAG.SpecialTask.NOTHING then done=true @@ -5718,7 +5739,7 @@ end function OPSGROUP:onafterMissionDone(From, Event, To, Mission) -- Debug info. - local text=string.format("Mission %s DONE!", Mission.name) + local text=string.format("Mission DONE %s!", Mission.name) self:T(self.lid..text) -- Set group status. @@ -6231,11 +6252,12 @@ function OPSGROUP:RouteToMission(mission, delay) self:T(self.lid.."Already in mission zone ==> TaskExecute()") self:TaskExecute(waypointtask) -- TODO: Calling PassingWaypoint here is probably better as it marks the mission waypoint as passed! - --self:PassingWaypoint(waypoint) + self:PassingWaypoint(waypoint) return elseif d<25 then self:T(self.lid.."Already within 25 meters of mission waypoint ==> TaskExecute()") self:TaskExecute(waypointtask) + self:PassingWaypoint(waypoint) return end @@ -7850,7 +7872,7 @@ function OPSGROUP:_Spawn(Delay, Template) self:ScheduleOnce(Delay, OPSGROUP._Spawn, self, 0, Template) else -- Debug output. - self:T2({Template=Template}) + --self:T2({Template=Template}) if self:IsArmygroup() and self.ValidateAndRepositionGroundUnits then UTILS.ValidateAndRepositionGroundUnits(Template.units) @@ -11190,11 +11212,11 @@ function OPSGROUP:_CheckAmmoStatus() self:OutOfAmmo() end - -- Guns. - if self.outofGuns and ammo.Guns>0 then + -- Guns (changed to shells) + if self.outofGuns and ammo.Shells>0 then self.outofGuns=false end - if ammo.Guns==0 and self.ammo.Guns>0 and not self.outofGuns then + if ammo.Shells==0 and self.ammo.Shells>0 and not self.outofGuns then self.outofGuns=true self:OutOfGuns() end @@ -13370,7 +13392,9 @@ function OPSGROUP:GetAmmoTot() local Ammo={} --#OPSGROUP.Ammo Ammo.Total=0 + Ammo.Shells=0 Ammo.Guns=0 + Ammo.Cannons=0 Ammo.Rockets=0 Ammo.Bombs=0 Ammo.Torpedos=0 @@ -13391,7 +13415,9 @@ function OPSGROUP:GetAmmoTot() -- Add up total. Ammo.Total=Ammo.Total+ammo.Total + Ammo.Shells=Ammo.Shells+ammo.Shells Ammo.Guns=Ammo.Guns+ammo.Guns + Ammo.Cannons=Ammo.Cannons+ammo.Cannons Ammo.Rockets=Ammo.Rockets+ammo.Rockets Ammo.Bombs=Ammo.Bombs+ammo.Bombs Ammo.Torpedos=Ammo.Torpedos+ammo.Torpedos @@ -13424,6 +13450,8 @@ function OPSGROUP:GetAmmoUnit(unit, display) -- Init counter. local nammo=0 local nshells=0 + local nguns=0 + local ncannons=0 local nrockets=0 local nmissiles=0 local nmissilesAA=0 @@ -13446,10 +13474,10 @@ function OPSGROUP:GetAmmoUnit(unit, display) local ammotable=unit:GetAmmo() if ammotable then - local weapons=#ammotable - --self:I(ammotable) + --self:I(ammotable) + --UTILS.PrintTableToLog(ammotable) -- Loop over all weapons. for w=1,weapons do @@ -13457,9 +13485,9 @@ function OPSGROUP:GetAmmoUnit(unit, display) -- Number of current weapon. local Nammo=ammotable[w]["count"] - -- Range in meters. Seems only to exist for missiles (not shells). - local rmin=ammotable[w]["desc"]["rangeMin"] or 0 - local rmax=ammotable[w]["desc"]["rangeMaxAltMin"] or 0 + -- Range in meters. Seems only to exist for missiles (not shells). + local rmin=ammotable[w]["desc"]["rangeMin"] or 0 + local rmax=ammotable[w]["desc"]["rangeMaxAltMin"] or 0 -- Type name of current weapon. local Tammo=ammotable[w]["desc"]["typeName"] @@ -13481,6 +13509,16 @@ function OPSGROUP:GetAmmoUnit(unit, display) -- Add up all shells. nshells=nshells+Nammo + + -- Add small and large caliber shells for guns and cannons + if ammotable[w]["desc"]["warhead"] and ammotable[w]["desc"]["warhead"]["caliber"] then + local caliber=ammotable[w]["desc"]["warhead"]["caliber"] + if caliber<25 then + nguns=nguns+Nammo + else + ncannons=ncannons+Nammo + end + end -- Debug info. text=text..string.format("- %d shells of type %s, range=%d - %d meters\n", Nammo, _weaponName, rmin, rmax) @@ -13559,7 +13597,9 @@ function OPSGROUP:GetAmmoUnit(unit, display) local ammo={} --#OPSGROUP.Ammo ammo.Total=nammo - ammo.Guns=nshells + ammo.Shells=nshells + ammo.Guns=nguns + ammo.Cannons=ncannons ammo.Rockets=nrockets ammo.Bombs=nbombs ammo.Torpedos=ntorps diff --git a/Moose Development/Moose/Ops/Target.lua b/Moose Development/Moose/Ops/Target.lua index 4863f5202..5a3b75464 100644 --- a/Moose Development/Moose/Ops/Target.lua +++ b/Moose Development/Moose/Ops/Target.lua @@ -2006,9 +2006,10 @@ end --- Count alive objects. -- @param #TARGET self -- @param #TARGET.Object Target Target objective. --- @param #table Coalitions (Optional) Only count targets of the given coalition(s). +-- @param #table Coalitions (Optional) Only count targets of the given coalition(s). +-- @param #boolean OnlyReallyAlive (Optional) If `true`, count only really alive targets (units, groups) but not coordinates or zones. -- @return #number Number of alive target objects. -function TARGET:CountObjectives(Target, Coalitions) +function TARGET:CountObjectives(Target, Coalitions, OnlyReallyAlive) local N=0 @@ -2067,13 +2068,17 @@ function TARGET:CountObjectives(Target, Coalitions) -- No target, where we can check the alive status, so we assume it is alive. Changed this because otherwise target count is 0 if we pass a coordinate. -- This is also more consitent with the life and is alive status. - N=N+1 + if not OnlyReallyAlive then + N=N+1 + end elseif Target.Type==TARGET.ObjectType.ZONE then -- No target, where we can check the alive status, so we assume it is alive. Changed this because otherwise target count is 0 if we pass a coordinate. -- This is also more consitent with the life and is alive status. - N=N+1 + if not OnlyReallyAlive then + N=N+1 + end elseif Target.Type==TARGET.ObjectType.OPSZONE then @@ -2093,15 +2098,16 @@ end --- Count alive targets. -- @param #TARGET self -- @param #table Coalitions (Optional) Only count targets of the given coalition(s). +-- @param #boolean OnlyReallyAlive (Optional) If `true`, count only really alive targets (units, groups) but not coordinates or zones. -- @return #number Number of alive target objects. -function TARGET:CountTargets(Coalitions) +function TARGET:CountTargets(Coalitions, OnlyReallyAlive) local N=0 for _,_target in pairs(self.targets) do local Target=_target --#TARGET.Object - N=N+self:CountObjectives(Target, Coalitions) + N=N+self:CountObjectives(Target, Coalitions, OnlyReallyAlive) end diff --git a/Moose Development/Moose/Utilities/Enums.lua b/Moose Development/Moose/Utilities/Enums.lua index 17e70099d..adcb5f862 100644 --- a/Moose Development/Moose/Utilities/Enums.lua +++ b/Moose Development/Moose/Utilities/Enums.lua @@ -1816,3 +1816,28 @@ ENUMS.FARPObjectTypeNamesAndShape ={ [ENUMS.FARPType.PADSINGLE] = { TypeName="FARP_SINGLE_01", ShapeName="FARP_SINGLE_01"}, } +--- Radio frequency bands (HF, VHF, UHF) +-- @type ENUMS.FrequencyBand +-- @field #number HF High frequency +-- @field #number VHF_LOW Very high frequency +-- @field #number VHF_HI Very high frequency +-- @field #number UHF Ultra high frequency +ENUMS.FrequencyBand = { + HF = 0, + VHF_LOW = 1, + VHF_HI = 2, + UHF = 3, +} + +--- Radio modulation types (AM, FM) +-- @type ENUMS.ModulationType +-- @field #number AM Amplitude modulation +-- @field #number FM Frequency modulation +-- @field #number AMFM Amplitude and frequency modulation +-- @field #number DISCARD Discard modulation +ENUMS.ModulationType = { + AM = 0, + FM = 1, + AMFM = 2, + DISCARD = -1, +} diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index fa4ba7233..3b9e62e69 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -3227,6 +3227,22 @@ function UTILS.BearingToCardinal(Heading) end end +--- Adjust given heading so that is is in [0, 360). +-- @param #number Heading The heading in degrees. +-- @return #number Adjust heading in [0,360). +function UTILS.AdjustHeading360(Heading) + + while Heading>=360 or Heading<0 do + if Heading>=360 then + Heading=Heading-360 + elseif Heading<0 then + Heading=Heading+360 + end + end + + return Heading +end + --- Create a BRAA NATO call string BRAA between two GROUP objects -- @param Wrapper.Group#GROUP FromGrp GROUP object -- @param Wrapper.Group#GROUP ToGrp GROUP object diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index 0109a9719..ad7afeef1 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -1164,6 +1164,23 @@ function GROUP:GetVec3() return nil end +--- Returns the current {@Core.Vector#VECTOR} of the first Unit in the GROUP. +-- @param #GROUP self +-- @return Core.Vector#VECTOR Vector of the first Unit of the GROUP or nil if cannot be found. +function GROUP:GetVector() + + -- Get first unit. + local unit=self:GetUnit(1) + + if unit then + local vector=unit:GetVector() + return vector + end + + self:E("ERROR: Cannot get Vector of group "..tostring(self.GroupName)) + return nil +end + --- Returns the average Vec3 vector of the Units in the GROUP. -- @param #GROUP self -- @return DCS#Vec3 Current Vec3 of the GROUP or nil if cannot be found. diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index 7be7ed135..69c949429 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -219,6 +219,25 @@ function POSITIONABLE:GetOrientationZ() end end +--- Returns the vectors of the orientation of the object: +-- X is the orientation parallel to the movement of the object, Z perpendicular and Y vertical orientation. +-- @param #POSITIONABLE self +-- @return Core.Vector#VECTOR X orientation, i.e. parallel to the direction of movement. +-- @return Core.Vector#VECTOR Y orientation, i.e. vertical. +-- @return Core.Vector#VECTOR Z orientation, i.e. perpendicular to the direction of movement. +function POSITIONABLE:GetOrientationVectors() + local position = self:GetPosition() + if position then + local vecx=VECTOR:NewFromVec(position.x) + local vecy=VECTOR:NewFromVec(position.y) + local vecz=VECTOR:NewFromVec(position.z) + return vecx, vecy, vecz + else + BASE:E( { "Cannot GetOrientation", Positionable = self, Alive = self:IsAlive() } ) + return nil, nil, nil + end +end + --- Returns the @{DCS#Position3} position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission. -- @param #POSITIONABLE self -- @return DCS#Position The 3D position vectors of the POSITIONABLE. @@ -286,6 +305,27 @@ function POSITIONABLE:GetVec2() return nil end +--- Returns the @{Core.Vector#VECTOR} indicating the 3D position of the object on the map. +-- @param #POSITIONABLE self +-- @return Core.Vector#VECTOR The 3D vector. +function POSITIONABLE:GetVector() + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + + local Vec3 = DCSPositionable:getPoint() -- DCS#Vec3 + + local vector=VECTOR:NewFromVec(Vec3) + + return vector + end + + self:E( { "Cannot GetVec2", Positionable = self, Alive = self:IsAlive() } ) + + return nil +end + --- Returns a COORDINATE object indicating the point in 2D of the POSITIONABLE within the mission. -- @param #POSITIONABLE self -- @return Core.Point#COORDINATE The 3D point vector of the POSITIONABLE. @@ -914,6 +954,24 @@ function POSITIONABLE:GetVelocityVec3() return nil end +--- Returns the velocity vector. +-- @param #POSITIONABLE self +-- @return Core.Vector#VECTOR The velocity vector or `nil` if the object does not exist. +function POSITIONABLE:GetVelocityVector() + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable and DCSPositionable:isExist() then + local vec3 = DCSPositionable:getVelocity() + local vector=VECTOR:NewFromVec(vec3) + return vector + end + + BASE:E( { "Cannot GetVelocityVector", Positionable = self, Alive = self:IsAlive() } ) + + return nil +end + --- Get relative velocity with respect to another POSITIONABLE. -- @param #POSITIONABLE self -- @param #POSITIONABLE Positionable Other POSITIONABLE.