diff --git a/Moose Development/Moose/Core/Event.lua b/Moose Development/Moose/Core/Event.lua index b5122c04e..d3a105631 100644 --- a/Moose Development/Moose/Core/Event.lua +++ b/Moose Development/Moose/Core/Event.lua @@ -1348,7 +1348,8 @@ function EVENT:onEvent( Event ) Event.Weapon = Event.weapon Event.WeaponName = Event.Weapon:getTypeName() Event.WeaponUNIT = CLIENT:Find( Event.Weapon, '', true ) -- Sometimes, the weapon is a player unit! - Event.WeaponPlayerName = Event.WeaponUNIT and Event.Weapon:getPlayerName() + Event.WeaponPlayerName = Event.WeaponUNIT and Event.Weapon.getPlayerName and Event.Weapon:getPlayerName() + --Event.WeaponPlayerName = Event.WeaponUNIT and Event.Weapon:getPlayerName() Event.WeaponCoalition = Event.WeaponUNIT and Event.Weapon:getCoalition() Event.WeaponCategory = Event.WeaponUNIT and Event.Weapon:getDesc().category Event.WeaponTypeName = Event.WeaponUNIT and Event.Weapon:getTypeName() diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index 8a811e19f..3911a52f3 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -8,22 +8,6 @@ -- -- === -- --- # Demo Missions --- --- ### [POINT_VEC Demo Missions source code]() --- --- ### [POINT_VEC Demo Missions, only for beta testers]() --- --- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) --- --- === --- --- # YouTube Channel --- --- ### [POINT_VEC YouTube Channel]() --- --- === --- -- ### Authors: -- -- * FlightControl (Design & Programming) @@ -920,7 +904,7 @@ do -- COORDINATE end - --- Return an angle in radians from the COORDINATE using a direction vector in Vec3 format. + --- Return an angle in radians from the COORDINATE using a **direction vector in Vec3 format**. -- @param #COORDINATE self -- @param DCS#Vec3 DirectionVec3 The direction vector in Vec3 format. -- @return #number DirectionRadians The angle in radians. @@ -933,10 +917,12 @@ do -- COORDINATE return DirectionRadians end - --- Return an angle in degrees from the COORDINATE using a direction vector in Vec3 format. + --- Return an angle in degrees from the COORDINATE using a **direction vector in Vec3 format**. -- @param #COORDINATE self -- @param DCS#Vec3 DirectionVec3 The direction vector in Vec3 format. -- @return #number DirectionRadians The angle in degrees. + -- @usage + -- local directionAngle = currentCoordinate:GetAngleDegrees(currentCoordinate:GetDirectionVec3(sourceCoordinate:GetVec3())) function COORDINATE:GetAngleDegrees( DirectionVec3 ) local AngleRadians = self:GetAngleRadians( DirectionVec3 ) local Angle = UTILS.ToDegree( AngleRadians ) @@ -3021,6 +3007,16 @@ do -- COORDINATE return BRAANATO end + --- Return the BULLSEYE as COORDINATE Object + -- @param #number Coalition Coalition of the bulls eye to return, e.g. coalition.side.BLUE + -- @return #COORDINATE self + -- @usage + -- -- note the dot (.) here,not using the colon (:) + -- local redbulls = COORDINATE.GetBullseyeCoordinate(coalition.side.RED) + function COORDINATE.GetBullseyeCoordinate(Coalition) + return COORDINATE:NewFromVec3( coalition.getMainRefPoint( Coalition ) ) + end + --- Return a BULLS string out of the BULLS of the coalition to the COORDINATE. -- @param #COORDINATE self -- @param DCS#coalition.side Coalition The coalition. diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index ee0917406..fec517906 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -1065,8 +1065,15 @@ do self:FilterActive( false ) return self + + --- Filter the set once + -- @function [parent=#SET_GROUP] FilterOnce + -- @param #SET_GROUP self + -- @return #SET_GROUP self + + end - + --- Get a *new* set that only contains alive groups. -- @param #SET_GROUP self -- @return #SET_GROUP Set of alive groups. @@ -1976,6 +1983,7 @@ do --- Get the closest group of the set with respect to a given reference coordinate. Optionally, only groups of given coalitions are considered in the search. -- @param #SET_GROUP self -- @param Core.Point#COORDINATE Coordinate Reference Coordinate from which the closest group is determined. + -- @param #table Coalitions (Optional) Table of coalition #number entries to filter for. -- @return Wrapper.Group#GROUP The closest group (if any). -- @return #number Distance in meters to the closest group. function SET_GROUP:GetClosestGroup(Coordinate, Coalitions) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 3e33aea63..f80dbd41b 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -3324,7 +3324,7 @@ function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) -- R2.2 -- UTILS.PrintTableToLog(SpawnTemplate.units[UnitID].callsign,1) end else - -- Ruskis + -- Russkis for UnitID = 1, #SpawnTemplate.units do SpawnTemplate.units[UnitID].callsign = math.random(1,999) end @@ -3402,8 +3402,25 @@ function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) -- R2.2 -- UTILS.PrintTableToLog(SpawnTemplate.units[UnitID].datalinks,1) end end + -- Link16 team members + for UnitID = 1, #SpawnTemplate.units do + if SpawnTemplate.units[UnitID].datalinks and SpawnTemplate.units[UnitID].datalinks.Link16 and SpawnTemplate.units[UnitID].datalinks.Link16.network then + local team = {} + local isF16 = string.find(SpawnTemplate.units[UnitID].type,"F-16",1,true) and true or false + for ID = 1, #SpawnTemplate.units do + local member = {} + member.missionUnitId = ID + if isF16 then + member.TDOA = true + end + table.insert(team,member) + end + SpawnTemplate.units[UnitID].datalinks.Link16.network.teamMembers = team + end + end self:T3( { "Template:", SpawnTemplate } ) + --UTILS.PrintTableToLog(SpawnTemplate,1) return SpawnTemplate end diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index aaff86ce7..229356455 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -42,11 +42,12 @@ -- * @{#ZONE_UNIT}: The ZONE_UNIT class defines by a zone around a @{Wrapper.Unit#UNIT} with a radius. -- * @{#ZONE_GROUP}: The ZONE_GROUP class defines by a zone around a @{Wrapper.Group#GROUP} with a radius. -- * @{#ZONE_POLYGON}: The ZONE_POLYGON class defines by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon. +-- * @{#ZONE_OVAL}: The ZONE_OVAL class isdefined by a center point, major axis, minor axis, and angle. -- -- === -- -- ### Author: **FlightControl** --- ### Contributions: **Applevangelist**, **FunkyFranky** +-- ### Contributions: **Applevangelist**, **FunkyFranky**, **coconutcockpit** -- -- === -- @@ -111,7 +112,7 @@ -- ## A zone might have additional Properties created in the DCS Mission Editor, which can be accessed: -- -- *@{#ZONE_BASE.GetProperty}(): Returns the Value of the zone with the given PropertyName, or nil if no matching property exists. --- *@{#ZONE_BASE.GetAllProperties}(): Returns the zone Properties table. +-- *@{#ZONE_BASE.GetAllProperties}(): Returns the zone Properties table. -- -- @field #ZONE_BASE ZONE_BASE = { @@ -313,7 +314,7 @@ function ZONE_BASE:Get2DDistance(Coordinate) else b.x=Coordinate.x b.y=Coordinate.y - end + end local dist=UTILS.VecDist2D(a,b) return dist end @@ -480,7 +481,13 @@ function ZONE_BASE:UndrawZone(Delay) self:ScheduleOnce(Delay, ZONE_BASE.UndrawZone, self) else if self.DrawID then - UTILS.RemoveMark(self.DrawID) + if type(self.DrawID) ~= "table" then + UTILS.RemoveMark(self.DrawID) + else -- DrawID is a table with a collections of mark ids, as used in ZONE_POLYGON + for _, mark_id in pairs(self.DrawID) do + UTILS.RemoveMark(mark_id) + end + end end end return self @@ -575,17 +582,17 @@ end -- @usage -- -- Create a new zone and start watching it every 5 secs for a defined GROUP entering or leaving -- local triggerzone = ZONE:New("ZonetoWatch"):Trigger(GROUP:FindByName("Aerial-1")) --- +-- -- -- This FSM function will be called when the group enters the zone -- function triggerzone:OnAfterEnteredZone(From,Event,To,Group) -- MESSAGE:New("Group has entered zone!",15):ToAll() -- end --- +-- -- -- This FSM function will be called when the group leaves the zone -- function triggerzone:OnAfterLeftZone(From,Event,To,Group) -- MESSAGE:New("Group has left zone!",15):ToAll() -- end --- +-- -- -- Stop watching the zone after 1 hour -- triggerzone:__TriggerStop(3600) function ZONE_BASE:Trigger(Objects) @@ -606,20 +613,20 @@ function ZONE_BASE:Trigger(Objects) self:_TriggerCheck(true) self:__TriggerRunCheck(self.Checktime) return self - + ------------------------ --- Pseudo Functions --- ------------------------ - + --- Triggers the FSM event "TriggerStop". Stops the ZONE_BASE Trigger. -- @function [parent=#ZONE_BASE] TriggerStop -- @param #ZONE_BASE self - --- Triggers the FSM event "TriggerStop" after a delay. + --- Triggers the FSM event "TriggerStop" after a delay. -- @function [parent=#ZONE_BASE] __TriggerStop -- @param #ZONE_BASE self -- @param #number delay Delay in seconds. - + --- On After "EnteredZone" event. An observed object has entered the zone. -- @function [parent=#ZONE_BASE] OnAfterEnteredZone -- @param #ZONE_BASE self @@ -662,12 +669,12 @@ function ZONE_BASE:_TriggerCheck(fromstart) local obj = _object -- Wrapper.Controllable#CONTROLLABLE if obj and obj:IsAlive() then if not obj.TriggerInZone then - -- has not been tagged previously - wasn't in set! + -- has not been tagged previously - wasn't in set! obj.TriggerInZone = {} end if not obj.TriggerInZone[self.ZoneName] then - -- has not been tagged previously - wasn't in set! - obj.TriggerInZone[self.ZoneName] = false + -- has not been tagged previously - wasn't in set! + obj.TriggerInZone[self.ZoneName] = false end -- is obj in zone? local inzone = self:IsCoordinateInZone(obj:GetCoordinate()) @@ -687,7 +694,7 @@ function ZONE_BASE:_TriggerCheck(fromstart) end end end - end + end return self end @@ -712,7 +719,7 @@ end -- @param #string PropertyName The name of a the TriggerZone Property to be retrieved. -- @return #string The Value of the TriggerZone Property with the given PropertyName, or nil if absent. -- @usage --- +-- -- local PropertiesZone = ZONE:FindByName("Properties Zone") -- local Property = "ExampleProperty" -- local PropertyValue = PropertiesZone:GetProperty(Property) @@ -788,7 +795,7 @@ function ZONE_RADIUS:New( ZoneName, Vec2, Radius, DoNotRegisterZone ) if not DoNotRegisterZone then _EVENTDISPATCHER:CreateEventNewZone(self) end - + --self.Coordinate=COORDINATE:NewFromVec2(Vec2) return self @@ -1426,7 +1433,7 @@ end function ZONE_RADIUS:IsVec2InZone( Vec2 ) self:F2( Vec2 ) - if not Vec2 then return false end + if not Vec2 then return false end local ZoneVec2 = self:GetVec2() @@ -1445,7 +1452,7 @@ end -- @return #boolean true if the point is within the zone. function ZONE_RADIUS:IsVec3InZone( Vec3 ) self:F2( Vec3 ) - if not Vec3 then return false end + if not Vec3 then return false end local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } ) return InZone @@ -1565,7 +1572,7 @@ function ZONE_RADIUS:GetRandomCoordinate(inner, outer, surfacetypes) return Coordinate end ---- Returns a @{Core.Point#COORDINATE} object reflecting a random location within the zone where there are no **map objects** of type "Building". +--- Returns a @{Core.Point#COORDINATE} object reflecting a random location within the zone where there are no **map objects** of type "Building". -- Does not find statics you might have placed there. **Note** This might be quite CPU intensive, use with care. -- @param #ZONE_RADIUS self -- @param #number inner (Optional) Minimal distance from the center of the zone in meters. Default is 0m. @@ -1592,7 +1599,7 @@ function ZONE_RADIUS:GetRandomCoordinateWithoutBuildings(inner,outer,distance,ma local buildings = {} local buildingzones = {} - + if self.ScanData and self.ScanData.BuildingCoordinates then buildings = self.ScanData.BuildingCoordinates buildingzones = self.ScanData.BuildingZones @@ -1619,7 +1626,7 @@ function ZONE_RADIUS:GetRandomCoordinateWithoutBuildings(inner,outer,distance,ma end -- max 1000 tries - local rcoord = nil + local rcoord = nil local found = true local iterations = 0 @@ -1635,21 +1642,21 @@ function ZONE_RADIUS:GetRandomCoordinateWithoutBuildings(inner,outer,distance,ma break end end - if found then + if found then -- we have a winner! if markfinal then MARKER:New(rcoord,"FREE"):ToAll() end - break + break end end - + if not found then -- max 1000 tries - local rcoord = nil + local rcoord = nil local found = true local iterations = 0 - + for i=1,1000 do iterations = iterations + 1 rcoord = self:GetRandomCoordinate(inner,outer) @@ -1661,22 +1668,22 @@ function ZONE_RADIUS:GetRandomCoordinateWithoutBuildings(inner,outer,distance,ma found = false end end - if found then + if found then -- we have a winner! if markfinal then MARKER:New(rcoord,"FREE"):ToAll() end - break + break end end end - + T1=timer.getTime() - + self:T(string.format("Found a coordinate: %s | Iterations: %d | Time: %.3f",tostring(found),iterations,T1-T0)) - + if found then return rcoord else return nil end - + end --- @@ -1994,6 +2001,323 @@ function ZONE_GROUP:GetRandomPointVec2( inner, outer ) end +--- ZONE_OVAL created from a center point, major axis, minor axis, and angle. +-- Ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Oval.lua +-- @type ZONE_OVAL +-- @extends Core.Zone#ZONE_BASE + +--- ## ZONE_OVAL class, extends @{#ZONE_BASE} +-- +-- The ZONE_OVAL class is defined by a center point, major axis, minor axis, and angle. +-- This class implements the inherited functions from @{#ZONE_BASE} taking into account the own zone format and properties. +-- +-- @field #ZONE_OVAL +ZONE_OVAL = { + ClassName = "OVAL", + ZoneName="", + MajorAxis = nil, + MinorAxis = nil, + Angle = 0, + DrawPoly = nil -- let's just use a ZONE_POLYGON to draw the ZONE_OVAL on the map +} + +--- Creates a new ZONE_OVAL from a center point, major axis, minor axis, and angle. +--- ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Oval.lua +-- @param #table vec2 The center point of the oval +-- @param #number major_axis The major axis of the oval +-- @param #number minor_axis The minor axis of the oval +-- @param #number angle The angle of the oval +-- @return #ZONE_OVAL The new oval +function ZONE_OVAL:New(name, vec2, major_axis, minor_axis, angle) + self = BASE:Inherit(self, ZONE_BASE:New()) + self.ZoneName = name + self.CenterVec2 = vec2 + self.MajorAxis = major_axis + self.MinorAxis = minor_axis + self.Angle = angle or 0 + + _DATABASE:AddZone(name, self) + + return self +end + +--- Constructor to create a ZONE_OVAL instance, taking the name of a drawing made with the draw tool in the Mission Editor. +--- ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Oval.lua +-- @param #ZONE_OVAL self +-- @param #string DrawingName The name of the drawing in the Mission Editor +-- @return #ZONE_OVAL self +function ZONE_OVAL:NewFromDrawing(DrawingName) + self = BASE:Inherit(self, ZONE_BASE:New(DrawingName)) + for _, layer in pairs(env.mission.drawings.layers) do + for _, object in pairs(layer["objects"]) do + if string.find(object["name"], DrawingName, 1, true) then + if object["polygonMode"] == "oval" then + self.CenterVec2 = { x = object["mapX"], y = object["mapY"] } + self.MajorAxis = object["r1"] + self.MinorAxis = object["r2"] + self.Angle = object["angle"] + + end + end + end + end + + _DATABASE:AddZone(DrawingName, self) + + return self +end + +--- Gets the major axis of the oval. +-- @param #ZONE_OVAL self +-- @return #number The major axis of the oval +function ZONE_OVAL:GetMajorAxis() + return self.MajorAxis +end + +--- Gets the minor axis of the oval. +-- @param #ZONE_OVAL self +-- @return #number The minor axis of the oval +function ZONE_OVAL:GetMinorAxis() + return self.MinorAxis +end + +--- Gets the angle of the oval. +-- @param #ZONE_OVAL self +-- @return #number The angle of the oval +function ZONE_OVAL:GetAngle() + return self.Angle +end + +--- Returns a the center point of the oval +-- @param #ZONE_OVAL self +-- @return #table The center Vec2 +function ZONE_OVAL:GetVec2() + return self.CenterVec2 +end + +--- Checks if a point is contained within the oval. +-- @param #ZONE_OVAL self +-- @param #table point The point to check +-- @return #bool True if the point is contained, false otherwise +function ZONE_OVAL:IsVec2InZone(vec2) + local cos, sin = math.cos, math.sin + local dx = vec2.x - self.CenterVec2.x + local dy = vec2.y - self.CenterVec2.y + local rx = dx * cos(self.Angle) + dy * sin(self.Angle) + local ry = -dx * sin(self.Angle) + dy * cos(self.Angle) + return rx * rx / (self.MajorAxis * self.MajorAxis) + ry * ry / (self.MinorAxis * self.MinorAxis) <= 1 +end + +--- Calculates the bounding box of the oval. The bounding box is the smallest rectangle that contains the oval. +-- @param #ZONE_OVAL self +-- @return #table The bounding box of the oval +function ZONE_OVAL:GetBoundingSquare() + local min_x = self.CenterVec2.x - self.MajorAxis + local min_y = self.CenterVec2.y - self.MinorAxis + local max_x = self.CenterVec2.x + self.MajorAxis + local max_y = self.CenterVec2.y + self.MinorAxis + + return { + {x=min_x, y=min_x}, {x=max_x, y=min_y}, {x=max_x, y=max_y}, {x=min_x, y=max_y} + } +end + +--- Find points on the edge of the oval +-- @param #ZONE_OVAL self +-- @param #number num_points How many points should be found. More = smoother shape +-- @return #table Points on he edge +function ZONE_OVAL:PointsOnEdge(num_points) + num_points = num_points or 40 + local points = {} + local dtheta = 2 * math.pi / num_points + + for i = 0, num_points - 1 do + local theta = i * dtheta + local x = self.CenterVec2.x + self.MajorAxis * math.cos(theta) * math.cos(self.Angle) - self.MinorAxis * math.sin(theta) * math.sin(self.Angle) + local y = self.CenterVec2.y + self.MajorAxis * math.cos(theta) * math.sin(self.Angle) + self.MinorAxis * math.sin(theta) * math.cos(self.Angle) + table.insert(points, {x = x, y = y}) + end + + return points +end + +--- Returns a random Vec2 within the oval. +-- @param #ZONE_OVAL self +-- @return #table The random Vec2 +function ZONE_OVAL:GetRandomVec2() + local theta = math.rad(self.Angle) + + local random_point = math.sqrt(math.random()) --> uniformly + --local random_point = math.random() --> more clumped around center + local phi = math.random() * 2 * math.pi + local x_c = random_point * math.cos(phi) + local y_c = random_point * math.sin(phi) + local x_e = x_c * self.MajorAxis + local y_e = y_c * self.MinorAxis + local rx = (x_e * math.cos(theta) - y_e * math.sin(theta)) + self.CenterVec2.x + local ry = (x_e * math.sin(theta) + y_e * math.cos(theta)) + self.CenterVec2.y + + return {x=rx, y=ry} +end + +--- Define a random @{Core.Point#POINT_VEC2} within the zone. +-- @param #ZONE_OVAL self +-- @return Core.Point#POINT_VEC2 The PointVec2 coordinates. +function ZONE_OVAL:GetRandomPointVec2() + return POINT_VEC2:NewFromVec2(self:GetRandomVec2()) +end + +--- Define a random @{Core.Point#POINT_VEC2} within the zone. +-- @param #ZONE_OVAL self +-- @return Core.Point#POINT_VEC2 The PointVec2 coordinates. +function ZONE_OVAL:GetRandomPointVec3() + return POINT_VEC2:NewFromVec3(self:GetRandomVec2()) +end + +--- Draw the zone on the F10 map. +--- ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Oval.lua +-- @param #ZONE_OVAL self +-- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. +-- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red. +-- @param #number Alpha Transparency [0,1]. Default 1. +-- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. -- doesn't seem to work +-- @param #number FillAlpha Transparency [0,1]. Default 0.15. -- doesn't seem to work +-- @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=Solid. +-- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. +-- @return #ZONE_OVAL self +function ZONE_OVAL:DrawZone(Coalition, Color, Alpha, FillColor, FillAlpha, LineType) + Coalition = Coalition or self:GetDrawCoalition() + + -- Set draw coalition. + self:SetDrawCoalition(Coalition) + + Color = Color or self:GetColorRGB() + Alpha = Alpha or 1 + + -- Set color. + self:SetColor(Color, Alpha) + + FillColor = FillColor or self:GetFillColorRGB() + if not FillColor then + UTILS.DeepCopy(Color) + end + FillAlpha = FillAlpha or self:GetFillColorAlpha() + if not FillAlpha then + FillAlpha = 0.15 + end + + LineType = LineType or 1 + + -- Set fill color -----------> has fill color worked in recent versions of DCS? + -- doing something like + -- + -- trigger.action.markupToAll(7, -1, 501, p.Coords[1]:GetVec3(), p.Coords[2]:GetVec3(),p.Coords[3]:GetVec3(),p.Coords[4]:GetVec3(),{1,0,0, 1}, {1,0,0, 1}, 4, false, Text or "") + -- + -- doesn't seem to fill in the shape for an n-sided polygon + self:SetFillColor(FillColor, FillAlpha) + + self.DrawPoly = ZONE_POLYGON:NewFromPointsArray(self.ZoneName, self:PointsOnEdge(80)) + self.DrawPoly:DrawZone(Coalition, Color, Alpha, FillColor, FillAlpha, LineType) +end + +--- Remove drawing from F10 map +-- @param #ZONE_OVAL self +function ZONE_OVAL:UndrawZone() + if self.DrawPoly then + self.DrawPoly:UndrawZone() + end +end + + +--- Ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Triangle.lua +--- This triangle "zone" is not really to be used on its own, it only serves as building blocks for +--- ZONE_POLYGON to accurately find a point inside a polygon; as well as getting the correct surface area of +--- a polygon. +-- @type _ZONE_TRIANGLE +-- @extends #BASE +_ZONE_TRIANGLE = { + ClassName="ZONE_TRIANGLE", + Points={}, + Coords={}, + CenterVec2={x=0, y=0}, + SurfaceArea=0, + DrawIDs={} +} + +function _ZONE_TRIANGLE:New(p1, p2, p3) + local self = BASE:Inherit(self, ZONE_BASE:New()) + self.Points = {p1, p2, p3} + + local center_x = (p1.x + p2.x + p3.x) / 3 + local center_y = (p1.y + p2.y + p3.y) / 3 + self.CenterVec2 = {x=center_x, y=center_y} + + for _, pt in pairs({p1, p2, p3}) do + table.add(self.Coords, COORDINATE:NewFromVec2(pt)) + end + + self.SurfaceArea = math.abs((p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y)) * 0.5 + + return self +end + +--- Checks if a point is contained within the triangle. +-- @param #table pt The point to check +-- @param #table points (optional) The points of the triangle, or 3 other points if you're just using the TRIANGLE class without an object of it +-- @return #bool True if the point is contained, false otherwise +function _ZONE_TRIANGLE:ContainsPoint(pt, points) + points = points or self.Points + + local function sign(p1, p2, p3) + return (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y) + end + + local d1 = sign(pt, self.Points[1], self.Points[2]) + local d2 = sign(pt, self.Points[2], self.Points[3]) + local d3 = sign(pt, self.Points[3], self.Points[1]) + + local has_neg = (d1 < 0) or (d2 < 0) or (d3 < 0) + local has_pos = (d1 > 0) or (d2 > 0) or (d3 > 0) + + return not (has_neg and has_pos) +end + +--- Returns a random Vec2 within the triangle. +-- @param #table points The points of the triangle, or 3 other points if you're just using the TRIANGLE class without an object of it +-- @return #table The random Vec2 +function _ZONE_TRIANGLE:GetRandomVec2(points) + points = points or self.Points + local pt = {math.random(), math.random()} + table.sort(pt) + local s = pt[1] + local t = pt[2] - pt[1] + local u = 1 - pt[2] + + return {x = s * points[1].x + t * points[2].x + u * points[3].x, + y = s * points[1].y + t * points[2].y + u * points[3].y} +end + +--- Draw the triangle +function _ZONE_TRIANGLE:Draw(Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) + Coalition=Coalition or -1 + + Color=Color or {1, 0, 0 } + Alpha=Alpha or 1 + + FillColor=FillColor or Color + if not FillColor then UTILS.DeepCopy(Color) end + FillAlpha=FillAlpha or Alpha + if not FillAlpha then FillAlpha=1 end + + for i=1, #self.Coords do + local c1 = self.Coords[i] + local c2 = self.Coords[i % #self.Coords + 1] + table.add(self.DrawIDs, c1:LineToAll(c2, Coalition, Color, Alpha, LineType, ReadOnly)) + end + return self.DrawIDs +end + + --- -- @type ZONE_POLYGON_BASE -- @field #ZONE_POLYGON_BASE.ListVec2 Polygon The polygon defined by an array of @{DCS#Vec2}. @@ -2021,7 +2345,10 @@ end -- @field #ZONE_POLYGON_BASE ZONE_POLYGON_BASE = { ClassName="ZONE_POLYGON_BASE", - } + _Triangles={}, -- _ZONE_TRIANGLES + SurfaceArea=0, + DrawID={} -- making a table out of the MarkID so its easier to draw an n-sided polygon, see ZONE_POLYGON_BASE:Draw() +} --- A 2D points array. -- @type ZONE_POLYGON_BASE.ListVec2 @@ -2055,9 +2382,101 @@ function ZONE_POLYGON_BASE:New( ZoneName, PointsArray ) end + -- triangulate the polygon so we can work with it + self._Triangles = self:_Triangulate() + -- set the polygon's surface area + self.SurfaceArea = self:_CalculateSurfaceArea() + return self end +--- Triangulates the polygon. +--- ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Polygon.lua +-- @return #table The #_TRIANGLE list that make up +function ZONE_POLYGON_BASE:_Triangulate() + local points = self._.Polygon + local triangles = {} + + local function get_orientation(shape_points) + local sum = 0 + for i = 1, #shape_points do + local j = i % #shape_points + 1 + sum = sum + (shape_points[j].x - shape_points[i].x) * (shape_points[j].y + shape_points[i].y) + end + return sum >= 0 and "clockwise" or "counter-clockwise" -- sum >= 0, return "clockwise", else return "counter-clockwise" + end + + local function ensure_clockwise(shape_points) + local orientation = get_orientation(shape_points) + if orientation == "counter-clockwise" then + -- Reverse the order of shape_points so they're clockwise + local reversed = {} + for i = #shape_points, 1, -1 do + table.insert(reversed, shape_points[i]) + end + return reversed + end + return shape_points + end + + local function is_clockwise(p1, p2, p3) + local cross_product = (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x) + return cross_product < 0 + end + + local function divide_recursively(shape_points) + if #shape_points == 3 then + table.insert(triangles, _ZONE_TRIANGLE:New(shape_points[1], shape_points[2], shape_points[3])) + elseif #shape_points > 3 then -- find an ear -> a triangle with no other points inside it + for i, p1 in ipairs(shape_points) do + local p2 = shape_points[(i % #shape_points) + 1] + local p3 = shape_points[(i + 1) % #shape_points + 1] + local triangle = _ZONE_TRIANGLE:New(p1, p2, p3) + local is_ear = true + + if not is_clockwise(p1, p2, p3) then + is_ear = false + else + for _, point in ipairs(shape_points) do + if point ~= p1 and point ~= p2 and point ~= p3 and triangle:ContainsPoint(point) then + is_ear = false + break + end + end + end + + if is_ear then + -- Check if any point in the original polygon is inside the ear triangle + local is_valid_triangle = true + for _, point in ipairs(points) do + if point ~= p1 and point ~= p2 and point ~= p3 and triangle:ContainsPoint(point) then + is_valid_triangle = false + break + end + end + if is_valid_triangle then + table.insert(triangles, triangle) + local remaining_points = {} + for j, point in ipairs(shape_points) do + if point ~= p2 then + table.insert(remaining_points, point) + end + end + divide_recursively(remaining_points) + break + end + else + + end + end + end + end + + points = ensure_clockwise(points) + divide_recursively(points) + return triangles +end + --- Update polygon points with an array of @{DCS#Vec2}. -- @param #ZONE_POLYGON_BASE self -- @param #ZONE_POLYGON_BASE.ListVec2 Vec2Array An array of @{DCS#Vec2}, forming a polygon. @@ -2072,6 +2491,10 @@ function ZONE_POLYGON_BASE:UpdateFromVec2(Vec2Array) self._.Polygon[i].y=Vec2Array[i].y end + -- triangulate the polygon so we can work with it + self._Triangles = self:_Triangulate() + -- set the polygon's surface area + self.SurfaceArea = self:_CalculateSurfaceArea() return self end @@ -2089,9 +2512,24 @@ function ZONE_POLYGON_BASE:UpdateFromVec3(Vec3Array) self._.Polygon[i].y=Vec3Array[i].z end + -- triangulate the polygon so we can work with it + self._Triangles = self:_Triangulate() + -- set the polygon's surface area + self.SurfaceArea = self:_CalculateSurfaceArea() return self end +--- Calculates the surface area of the polygon. The surface area is the sum of the areas of the triangles that make up the polygon. +--- ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Polygon.lua +-- @return #number The surface area of the polygon +function ZONE_POLYGON_BASE:_CalculateSurfaceArea() + local area = 0 + for _, triangle in pairs(self._Triangles) do + area = area + triangle.SurfaceArea + end + return area +end + --- Returns the center location of the polygon. -- @param #ZONE_POLYGON_BASE self -- @return DCS#Vec2 The location of the zone based on the @{Wrapper.Group} location. @@ -2233,64 +2671,78 @@ function ZONE_POLYGON_BASE:BoundZone( UnBound ) return self end ---- Draw the zone on the F10 map. **NOTE** Currently, only polygons **up to ten points** are supported! +--- Draw the zone on the F10 map. Infinite number of points supported +--- ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Polygon.lua -- @param #ZONE_POLYGON_BASE self -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red. -- @param #number Alpha Transparency [0,1]. Default 1. --- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. --- @param #number FillAlpha Transparency [0,1]. Default 0.15. +-- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. -- doesn't seem to work +-- @param #number FillAlpha Transparency [0,1]. Default 0.15. -- doesn't seem to work -- @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=Solid. -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. -- @return #ZONE_POLYGON_BASE self -function ZONE_POLYGON_BASE:DrawZone(Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) +function ZONE_POLYGON_BASE:DrawZone(Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, IncludeTriangles) + if self._.Polygon and #self._.Polygon >= 3 then + Coalition = Coalition or self:GetDrawCoalition() - if self._.Polygon and #self._.Polygon>=3 then + -- Set draw coalition. + self:SetDrawCoalition(Coalition) - local coordinate=COORDINATE:NewFromVec2(self._.Polygon[1]) + Color = Color or self:GetColorRGB() + Alpha = Alpha or 1 - Coalition=Coalition or self:GetDrawCoalition() + -- Set color. + self:SetColor(Color, Alpha) - -- Set draw coalition. - self:SetDrawCoalition(Coalition) + FillColor = FillColor or self:GetFillColorRGB() + if not FillColor then + UTILS.DeepCopy(Color) + end + FillAlpha = FillAlpha or self:GetFillColorAlpha() + if not FillAlpha then + FillAlpha = 0.15 + end - Color=Color or self:GetColorRGB() - Alpha=Alpha or 1 + -- Set fill color -----------> has fill color worked in recent versions of DCS? + -- doing something like + -- + -- trigger.action.markupToAll(7, -1, 501, p.Coords[1]:GetVec3(), p.Coords[2]:GetVec3(),p.Coords[3]:GetVec3(),p.Coords[4]:GetVec3(),{1,0,0, 1}, {1,0,0, 1}, 4, false, Text or "") + -- + -- doesn't seem to fill in the shape for an n-sided polygon + self:SetFillColor(FillColor, FillAlpha) - -- Set color. - self:SetColor(Color, Alpha) - - FillColor=FillColor or self:GetFillColorRGB() - if not FillColor then UTILS.DeepCopy(Color) end - FillAlpha=FillAlpha or self:GetFillColorAlpha() - if not FillAlpha then FillAlpha=0.15 end - - -- Set fill color. - self:SetFillColor(FillColor, FillAlpha) - - if #self._.Polygon==4 then - - local Coord2=COORDINATE:NewFromVec2(self._.Polygon[2]) - local Coord3=COORDINATE:NewFromVec2(self._.Polygon[3]) - local Coord4=COORDINATE:NewFromVec2(self._.Polygon[4]) - - self.DrawID=coordinate:QuadToAll(Coord2, Coord3, Coord4, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) - - else - - local Coordinates=self:GetVerticiesCoordinates() - table.remove(Coordinates, 1) - - self.DrawID=coordinate:MarkupToAllFreeForm(Coordinates, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) + IncludeTriangles = IncludeTriangles or false + -- just draw the triangles, we get the outline for free + if IncludeTriangles then + for _, triangle in pairs(self._Triangles) do + local draw_ids = triangle:Draw() + table.combine(self.DrawID, draw_ids) + end + -- draw outline only + else + local coords = self:GetVerticiesCoordinates() + for i = 1, #coords do + local c1 = coords[i] + local c2 = coords[i % #coords + 1] + table.add(self.DrawID, c1:LineToAll(c2, Coalition, Color, Alpha, LineType, ReadOnly)) + end + end end - - end - - return self + return self end ---- Get the smallest radius encompassing all points of the polygon zone. +--- Get the surface area of this polygon +-- @param #ZONE_POLYGON_BASE self +-- @return #number Surface area +function ZONE_POLYGON_BASE:GetSurfaceArea() + return self.SurfaceArea +end + + + +--- Get the smallest radius encompassing all points of the polygon zone. -- @param #ZONE_POLYGON_BASE self -- @return #number Radius of the zone in meters. function ZONE_POLYGON_BASE:GetRadius() @@ -2298,22 +2750,22 @@ function ZONE_POLYGON_BASE:GetRadius() local center=self:GetVec2() local radius=0 - + for _,_vec2 in pairs(self._.Polygon) do local vec2=_vec2 --DCS#Vec2 - + local r=UTILS.VecDist2D(center, vec2) - + if r>radius then radius=r end - + end return radius end ---- Get the smallest circular zone encompassing all points of the polygon zone. +--- Get the smallest circular zone encompassing all points of the polygon zone. -- @param #ZONE_POLYGON_BASE self -- @param #string ZoneName (Optional) Name of the zone. Default is the name of the polygon zone. -- @param #boolean DoNotRegisterZone (Optional) If `true`, zone is not registered. @@ -2323,25 +2775,25 @@ function ZONE_POLYGON_BASE:GetZoneRadius(ZoneName, DoNotRegisterZone) local center=self:GetVec2() local radius=self:GetRadius() - + local zone=ZONE_RADIUS:New(ZoneName or self.ZoneName, center, radius, DoNotRegisterZone) return zone end ---- Get the smallest rectangular zone encompassing all points points of the polygon zone. +--- Get the smallest rectangular zone encompassing all points points of the polygon zone. -- @param #ZONE_POLYGON_BASE self -- @param #string ZoneName (Optional) Name of the zone. Default is the name of the polygon zone. -- @param #boolean DoNotRegisterZone (Optional) If `true`, zone is not registered. -- @return #ZONE_POLYGON The rectangular zone. function ZONE_POLYGON_BASE:GetZoneQuad(ZoneName, DoNotRegisterZone) - + local vec1, vec3=self:GetBoundingVec2() - + local vec2={x=vec1.x, y=vec3.y} local vec4={x=vec3.x, y=vec1.y} - + local zone=ZONE_POLYGON_BASE:New(ZoneName or self.ZoneName, {vec1, vec2, vec3, vec4}) return zone @@ -2354,15 +2806,15 @@ end function ZONE_POLYGON_BASE:RemoveJunk(Height) Height=Height or 1000 - + local vec2SW, vec2NE=self:GetBoundingVec2() local vec3SW={x=vec2SW.x, y=-Height, z=vec2SW.y} --DCS#Vec3 local vec3NE={x=vec2NE.x, y= Height, z=vec2NE.y} --DCS#Vec3 - + --local coord1=COORDINATE:NewFromVec3(vec3SW):MarkToAll("SW") --local coord1=COORDINATE:NewFromVec3(vec3NE):MarkToAll("NE") - + local volume = { id = world.VolumeType.BOX, params = { @@ -2371,7 +2823,7 @@ function ZONE_POLYGON_BASE:RemoveJunk(Height) } } - local n=world.removeJunk(volume) + local n=world.removeJunk(volume) return n end @@ -2449,7 +2901,7 @@ end -- @return #boolean true if the location is within the zone. function ZONE_POLYGON_BASE:IsVec2InZone( Vec2 ) self:F2( Vec2 ) - if not Vec2 then return false end + if not Vec2 then return false end local Next local Prev local InPolygon = false @@ -2479,40 +2931,34 @@ end -- @return #boolean true if the point is within the zone. function ZONE_POLYGON_BASE:IsVec3InZone( Vec3 ) self:F2( Vec3 ) - - if not Vec3 then return false end - + + if not Vec3 then return false end + local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } ) return InZone end --- Define a random @{DCS#Vec2} within the zone. +--- ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Polygon.lua -- @param #ZONE_POLYGON_BASE self -- @return DCS#Vec2 The Vec2 coordinate. function ZONE_POLYGON_BASE:GetRandomVec2() - - -- It is a bit tricky to find a random point within a polygon. Right now i am doing it the dirty and inefficient way... - - -- Get the bounding square. - local BS = self:GetBoundingSquare() - - local Nmax=1000 ; local n=0 - while n= random_weight then + return triangle:GetRandomVec2() + end + end end --- Return a @{Core.Point#POINT_VEC2} object representing a random 2D point at landheight within the zone. @@ -2597,7 +3043,7 @@ function ZONE_POLYGON_BASE:GetBoundingVec2() y2 = ( y2 < self._.Polygon[i].y ) and self._.Polygon[i].y or y2 end - + local vec1={x=x1, y=y1} local vec2={x=x2, y=y2} @@ -2649,7 +3095,8 @@ end -- @extends #ZONE_POLYGON_BASE ---- The ZONE_POLYGON class defined by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon. +--- The ZONE_POLYGON class defined by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon, OR by drawings made with the Draw tool +--- in the Mission Editor -- This class implements the inherited functions from @{#ZONE_RADIUS} taking into account the own zone format and properties. -- -- ## Declare a ZONE_POLYGON directly in the DCS mission editor! @@ -2672,6 +3119,13 @@ end -- then SetZone would contain the ZONE_POLYGON object `DefenseZone` as part of the zone collection, -- without much scripting overhead! -- +-- This class now also supports drawings made with the Draw tool in the Mission Editor. Any drawing made with Line > Segments > Closed, Polygon > Rect or Polygon > Free can be +-- made into a ZONE_POLYGON. +-- +-- This class has been updated to use a accurate way of generating random points inside the polygon without having to use trial and error guesses. +-- You can also get the surface area of the polygon now, handy if you want measure which coalition has the largest captured area, for example. + + -- @field #ZONE_POLYGON ZONE_POLYGON = { ClassName="ZONE_POLYGON", @@ -2732,6 +3186,49 @@ function ZONE_POLYGON:NewFromGroupName( GroupName ) return self end +--- Constructor to create a ZONE_POLYGON instance, taking the name of a drawing made with the draw tool in the Mission Editor. +-- @param #ZONE_POLYGON self +-- @param #string DrawingName The name of the drawing in the Mission Editor +-- @return #ZONE_POLYGON self +function ZONE_POLYGON:NewFromDrawing(DrawingName) + local points = {} + for _, layer in pairs(env.mission.drawings.layers) do + for _, object in pairs(layer["objects"]) do + if object["name"] == DrawingName then + if (object["primitiveType"] == "Line" and object["closed"] == true) or (object["polygonMode"] == "free") then + -- points for the drawings are saved in local space, so add the object's map x and y coordinates to get + -- world space points we can use + for _, point in UTILS.spairs(object["points"]) do + local p = {x = object["mapX"] + point["x"], + y = object["mapY"] + point["y"] } + table.add(points, p) + end + elseif object["polygonMode"] == "rect" then + -- the points for a rect are saved as local coordinates with an angle. To get the world space points from this + -- we need to rotate the points around the center of the rects by an angle. UTILS.RotatePointAroundPivot was + -- committed in an earlier commit + local angle = object["angle"] + local half_width = object["width"] / 2 + local half_height = object["height"] / 2 + + local center = { x = object["mapX"], y = object["mapY"] } + local p1 = UTILS.RotatePointAroundPivot({ x = center.x - half_height, y = center.y + half_width }, center, angle) + local p2 = UTILS.RotatePointAroundPivot({ x = center.x + half_height, y = center.y + half_width }, center, angle) + local p3 = UTILS.RotatePointAroundPivot({ x = center.x + half_height, y = center.y - half_width }, center, angle) + local p4 = UTILS.RotatePointAroundPivot({ x = center.x - half_height, y = center.y - half_width }, center, angle) + + points = {p1, p2, p3, p4} + else + -- something else that might be added in the future + end + end + end + end + local self = BASE:Inherit(self, ZONE_POLYGON_BASE:New(DrawingName, points)) + _EVENTDISPATCHER:CreateEventNewZone(self) + return self +end + --- Find a polygon zone in the _DATABASE using the name of the polygon zone. -- @param #ZONE_POLYGON self diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index e9005ace0..e238914a9 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -1234,7 +1234,7 @@ function RANGE:SetSRS(PathToSRS, Port, Coalition, Frequency, Modulation, Volume, return self end ---- (SRS) Set range control frequency and voice. +--- (SRS) Set range control frequency and voice. Use `RANGE:SetSRS()` once first before using this function. -- @param #RANGE self -- @param #number frequency Frequency in MHz. Default 256 MHz. -- @param #number modulation Modulation, defaults to radio.modulation.AM. @@ -1244,6 +1244,10 @@ end -- @param #string relayunitname Name of the unit used for transmission location. -- @return #RANGE self function RANGE:SetSRSRangeControl( frequency, modulation, voice, culture, gender, relayunitname ) + if not self.instructmsrs then + self:E(self.lid.."Use myrange:SetSRS() once first before using myrange:SetSRSRangeControl!") + return self + end self.rangecontrolfreq = frequency or 256 self.controlmsrs:SetFrequencies(self.rangecontrolfreq) self.controlmsrs:SetModulations(modulation or radio.modulation.AM) @@ -1259,7 +1263,7 @@ function RANGE:SetSRSRangeControl( frequency, modulation, voice, culture, gender return self end ---- (SRS) Set range instructor frequency and voice. +--- (SRS) Set range instructor frequency and voice. Use `RANGE:SetSRS()` once first before using this function. -- @param #RANGE self -- @param #number frequency Frequency in MHz. Default 305 MHz. -- @param #number modulation Modulation, defaults to radio.modulation.AM. @@ -1269,6 +1273,10 @@ end -- @param #string relayunitname Name of the unit used for transmission location. -- @return #RANGE self function RANGE:SetSRSRangeInstructor( frequency, modulation, voice, culture, gender, relayunitname ) + if not self.instructmsrs then + self:E(self.lid.."Use myrange:SetSRS() once first before using myrange:SetSRSRangeInstructor!") + return self + end self.instructorfreq = frequency or 305 self.instructmsrs:SetFrequencies(self.instructorfreq) self.instructmsrs:SetModulations(modulation or radio.modulation.AM) diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index beb60200f..7ac6e2839 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -7404,6 +7404,8 @@ function WAREHOUSE:_CheckRequestNow(request) -- Check if at least one (cargo) asset is available. if _nassets>0 then + + local asset=_assets[1] --#WAREHOUSE.Assetitem -- Get the attibute of the requested asset. _assetattribute=_assets[1].attribute @@ -7414,11 +7416,24 @@ function WAREHOUSE:_CheckRequestNow(request) if _assetcategory==Group.Category.AIRPLANE or _assetcategory==Group.Category.HELICOPTER then if self.airbase and self.airbase:GetCoalition()==self:GetCoalition() then + + -- Check if DCS warehouse of airbase has enough assets + if self.airbase.storage then + local nS=self.airbase.storage:GetAmount(asset.unittype) + local nA=asset.nunits*request.nasset -- Number of units requested + if nS NOT enough to spawn the requested %d asset units (%d groups)", + self.alias, nS, asset.unittype, nA, request.nasset) + self:_InfoMessage(text, 5) + return false + end + end + if self:IsRunwayOperational() or _assetairstart then if _assetairstart then - -- Airstart no need to check parking + -- Airstart no need to check parking else -- Check parking. @@ -7530,6 +7545,9 @@ function WAREHOUSE:_CheckRequestNow(request) self:_InfoMessage(text, 5) return false end + + elseif _assetcategory==Group.Category.AIRPLANE or _assetcategory==Group.Category.HELICOPTER then + end diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index 143364103..6819ee36c 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -312,10 +312,16 @@ -- -- atis=ATIS:New("Batumi", 305, radio.modulation.AM) -- atis:SetSRS("D:\\DCS\\_SRS\\", "male", "en-US") --- atis:Start() +-- atis:Start() -- -- This uses a male voice with US accent. It requires SRS to be installed in the `D:\DCS\_SRS\` directory. Note that backslashes need to be escaped or simply use slashes (as in linux). -- +-- ### SRS can use multiple frequencies: +-- +-- atis=ATIS:New("Batumi", {305,103.85}, {radio.modulation.AM,radio.modulation.FM}) +-- atis:SetSRS("D:\\DCS\\_SRS\\", "male", "en-US") +-- atis:Start() +-- -- ### SRS Localization -- -- You can localize the SRS output, all you need is to provide a table of translations and set the `locale` of your instance. You need to provide the translations in your script **before you instantiate your ATIS**. @@ -884,13 +890,14 @@ _ATIS = {} --- ATIS class version. -- @field #string version -ATIS.version = "0.10.3" +ATIS.version = "0.10.4" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Correct fog for elevation. +-- DONE: Option to add multiple frequencies for SRS -- DONE: Zulu time --> Zulu in output. -- DONE: Fix for AB not having a runway - Helopost like Naqoura -- DONE: Add new Normandy airfields. @@ -899,7 +906,7 @@ ATIS.version = "0.10.3" -- DONE: Visibility reported twice over SRS -- DONE: Add text report for output. -- DONE: Add stop FMS functions. --- NOGO: Use local time. Not realisitc! +-- NOGO: Use local time. Not realistic! -- DONE: Dew point. Approx. done. -- DONE: Metric units. -- DONE: Set UTC correction. @@ -915,8 +922,8 @@ ATIS.version = "0.10.3" --- Create a new ATIS class object for a specific airbase. -- @param #ATIS self -- @param #string AirbaseName Name of the airbase. --- @param #number Frequency Radio frequency in MHz. Default 143.00 MHz. --- @param #number Modulation Radio modulation: 0=AM, 1=FM. Default 0=AM. See `radio.modulation.AM` and `radio.modulation.FM` enumerators. +-- @param #number Frequency Radio frequency in MHz. Default 143.00 MHz. When using **SRS** this can be passed as a table of multiple frequencies. +-- @param #number Modulation Radio modulation: 0=AM, 1=FM. Default 0=AM. See `radio.modulation.AM` and `radio.modulation.FM` enumerators. When using **SRS** this can be passed as a table of multiple modulations. -- @return #ATIS self function ATIS:New(AirbaseName, Frequency, Modulation) @@ -1594,8 +1601,16 @@ function ATIS:onafterStart( From, Event, To ) end -- Info. - self:I( self.lid .. string.format( "Starting ATIS v%s for airbase %s on %.3f MHz Modulation=%d", ATIS.version, self.airbasename, self.frequency, self.modulation ) ) - + if type(self.frequency) == "table" then + local frequency = table.concat(self.frequency,"/") + local modulation = self.modulation + if type(self.modulation) == "table" then + modulation = table.concat(self.modulation,"/") + end + self:I( self.lid .. string.format( "Starting ATIS v%s for airbase %s on %s MHz Modulation=%s", ATIS.version, self.airbasename, frequency, modulation ) ) + else + self:I( self.lid .. string.format( "Starting ATIS v%s for airbase %s on %.3f MHz Modulation=%d", ATIS.version, self.airbasename, self.frequency, self.modulation ) ) + end -- Start radio queue. if not self.useSRS then self.radioqueue = RADIOQUEUE:New( self.frequency, self.modulation, string.format( "ATIS %s", self.airbasename ) ) @@ -1653,7 +1668,17 @@ function ATIS:onafterStatus( From, Event, To ) end -- Info text. - local text = string.format( "State %s: Freq=%.3f MHz %s", fsmstate, self.frequency, UTILS.GetModulationName( self.modulation ) ) + local text = "" + if type(self.frequency) == "table" then + local frequency = table.concat(self.frequency,"/") + local modulation = self.modulation + if type(self.modulation) == "table" then + modulation = table.concat(self.modulation,"/") + end + text = string.format( "State %s: Freq=%s MHz %s", fsmstate, frequency, modulation ) + else + text = string.format( "State %s: Freq=%.3f MHz %s", fsmstate, self.frequency, UTILS.GetModulationName( self.modulation ) ) + end if self.useSRS then text = text .. string.format( ", SRS path=%s (%s), gender=%s, culture=%s, voice=%s", tostring( self.msrs.path ), tostring( self.msrs.port ), tostring( self.msrs.gender ), tostring( self.msrs.culture ), tostring( self.msrs.voice ) ) else @@ -2919,8 +2944,17 @@ function ATIS:UpdateMarker( information, runact, wind, altimeter, temperature ) if self.markerid then self.airbase:GetCoordinate():RemoveMark( self.markerid ) end - - local text = string.format( "ATIS on %.3f %s, %s:\n", self.frequency, UTILS.GetModulationName( self.modulation ), tostring( information ) ) + local text = "" + if type(self.frequency) == "table" then + local frequency = table.concat(self.frequency,"/") + local modulation = self.modulation + if type(modulation) == "table" then + modulation = table.concat(self.modulation,"/") + end + text = string.format( "ATIS on %s %s, %s:\n", tostring(frequency), tostring(modulation), tostring( information ) ) + else + text = string.format( "ATIS on %.3f %s, %s:\n", self.frequency, UTILS.GetModulationName( self.modulation ), tostring( information ) ) + end text = text .. string.format( "%s\n", tostring( runact ) ) text = text .. string.format( "%s\n", tostring( wind ) ) text = text .. string.format( "%s\n", tostring( altimeter ) ) diff --git a/Moose Development/Moose/Ops/FlightControl.lua b/Moose Development/Moose/Ops/FlightControl.lua index bab56059a..8403e0c24 100644 --- a/Moose Development/Moose/Ops/FlightControl.lua +++ b/Moose Development/Moose/Ops/FlightControl.lua @@ -2811,7 +2811,11 @@ function FLIGHTCONTROL:_PlayerInfoATIS(groupname) -- Radio message. self:TransmissionPilot(rtext, flight) - self:TransmissionTower(srstxt,flight,10) + if self.atis then + self:TransmissionTower(srstxt,flight,10) + else + self:TransmissionTower(text,flight,10) + end else self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua index ad115cdc2..25a6b8ef2 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -2863,8 +2863,11 @@ function FLIGHTGROUP:_CheckGroupDone(delay, waittime) end else + -- Check if not parking (could be on ALERT5 and just spawned (current mission=nil) + if not self:IsParking() then self:T(self.lid..string.format("Passed Final WP but Tasks=%d or Missions=%d left in the queue. Wait!", nTasks, nMissions)) self:__Wait(-1) + end end else self:T(self.lid..string.format("Passed Final WP but still have current Task (#%s) or Mission (#%s) left to do", tostring(self.taskcurrent), tostring(self.currentmission))) diff --git a/Moose Development/Moose/Ops/OpsZone.lua b/Moose Development/Moose/Ops/OpsZone.lua index 01b7a3b19..780b389c0 100644 --- a/Moose Development/Moose/Ops/OpsZone.lua +++ b/Moose Development/Moose/Ops/OpsZone.lua @@ -1483,11 +1483,11 @@ function OPSZONE:_GetZoneColor() local color={0,0,0} if self.ownerCurrent==coalition.side.NEUTRAL then - color={1, 1, 1} + color=self.ZoneOwnerNeutral or {1, 1, 1} elseif self.ownerCurrent==coalition.side.BLUE then - color={0, 0, 1} + color=self.ZoneOwnerBlue or {0, 0, 1} elseif self.ownerCurrent==coalition.side.RED then - color={1, 0, 0} + color=self.ZoneOwnerRed or {1, 0, 0} else end @@ -1495,6 +1495,21 @@ function OPSZONE:_GetZoneColor() return color end +--- Set custom RGB color of zone depending on current owner. +-- @param #OPSZONE self +-- @param #table Neutral Color is a table of RGB values 0..1 for Red, Green, and Blue respectively, e.g. {1,0,0} for red. +-- @param #table Blue Color is a table of RGB values 0..1 for Red, Green, and Blue respectively, e.g. {0,1,0} for green. +-- @param #table Red Color is a table of RGB values 0..1 for Red, Green, and Blue respectively, e.g. {0,0,1} for blue. +-- @return #OPSZONE self +function OPSZONE:SetZoneColor(Neutral, Blue, Red) + + self.ZoneOwnerNeutral = Neutral or {1, 1, 1} + self.ZoneOwnerBlue = Blue or {0, 0, 1} + self.ZoneOwnerRed = Red or {1, 0, 0} + + return self +end + --- Update marker on the F10 map. -- @param #OPSZONE self function OPSZONE:_UpdateMarker() diff --git a/Moose Development/Moose/Ops/PlayerRecce.lua b/Moose Development/Moose/Ops/PlayerRecce.lua index c8cad0bdd..ec3a57954 100644 --- a/Moose Development/Moose/Ops/PlayerRecce.lua +++ b/Moose Development/Moose/Ops/PlayerRecce.lua @@ -104,7 +104,7 @@ PLAYERRECCE = { ClassName = "PLAYERRECCE", verbose = true, lid = nil, - version = "0.0.21", + version = "0.0.22", ViewZone = {}, ViewZoneVisual = {}, ViewZoneLaser = {}, @@ -469,8 +469,10 @@ function PLAYERRECCE:_GetClockDirection(unit, target) local _playerPosition = unit:GetCoordinate() -- get position of helicopter local _targetpostions = target:GetCoordinate() -- get position of downed pilot local _heading = unit:GetHeading() -- heading + --self:I("Heading = ".._heading) local DirectionVec3 = _playerPosition:GetDirectionVec3( _targetpostions ) local Angle = _playerPosition:GetAngleDegrees( DirectionVec3 ) + --self:I("Angle = "..Angle) local clock = 12 local hours = 0 if _heading and Angle then @@ -478,10 +480,13 @@ function PLAYERRECCE:_GetClockDirection(unit, target) --if angle == 0 then angle = 360 end clock = _heading-Angle hours = (clock/30)*-1 + --self:I("hours = "..hours) clock = 12+hours clock = UTILS.Round(clock,0) if clock > 12 then clock = clock-12 end - end + if clock == 0 then clock = 12 end + end + --self:I("Clock ="..clock) return clock end @@ -709,8 +714,8 @@ function PLAYERRECCE:_GetViewZone(unit, vheading, minview, maxview, angle, camon local heading2 = (vheading-90)%360 self:T({heading1,heading2}) local startpos = startp:Translate(minview,vheading) - local pos1 = startpos:Translate(10,heading1) - local pos2 = startpos:Translate(10,heading2) + local pos1 = startpos:Translate(12.5,heading1) + local pos2 = startpos:Translate(12.5,heading2) local pos3 = pos1:Translate(maxview,vheading) local pos4 = pos2:Translate(maxview,vheading) local array = {} @@ -912,32 +917,41 @@ function PLAYERRECCE:_LaseTarget(client,targetset) else laser = self.LaserSpots[playername] end + -- old target if self.LaserTarget[playername] then -- still looking at target? local target=self.LaserTarget[playername] -- Ops.Target#TARGET local oldtarget = target:GetObject() --or laser.Target - --self:I("Targetstate: "..target:GetState()) - --self:I("Laser State: "..tostring(laser:IsLasing())) - if not oldtarget or targetset:IsNotInSet(oldtarget) or target:IsDead() or target:IsDestroyed() then + self:T("Targetstate: "..target:GetState()) + self:T("Laser State: "..tostring(laser:IsLasing())) + if (not oldtarget) or targetset:IsNotInSet(oldtarget) or target:IsDead() or target:IsDestroyed() then -- lost LOS or dead laser:LaseOff() if target:IsDead() or target:IsDestroyed() or target:GetLife() < 2 then self:__Shack(-1,client,oldtarget) - self.LaserTarget[playername] = nil + --self.LaserTarget[playername] = nil else self:__TargetLOSLost(-1,client,oldtarget) - self.LaserTarget[playername] = nil + --self.LaserTarget[playername] = nil end - end - if oldtarget and (not laser:IsLasing()) then - --self:I("Switching laser back on ..") + self.LaserTarget[playername] = nil + oldtarget = nil + self.LaserSpots[playername] = nil + elseif oldtarget and laser and (not laser:IsLasing()) then + --laser:LaseOff() + self:T("Switching laser back on ..") local lasercode = self.UnitLaserCodes[playername] or laser.LaserCode or 1688 local lasingtime = self.lasingtime or 60 --local targettype = target:GetTypeName() laser:LaseOn(oldtarget,lasercode,lasingtime) --self:__TargetLasing(-1,client,oldtarget,lasercode,lasingtime) + else + -- we should not be here... + self:T("Target alive and laser is on!") + --self.LaserSpots[playername] = nil end - elseif not laser:IsLasing() and target then + -- new target + elseif (not laser:IsLasing()) and target then local relativecam = self.LaserRelativePos[client:GetTypeName()] laser:SetRelativeStartPosition(relativecam) local lasercode = self.UnitLaserCodes[playername] or laser.LaserCode or 1688 @@ -945,7 +959,7 @@ function PLAYERRECCE:_LaseTarget(client,targetset) --local targettype = target:GetTypeName() laser:LaseOn(target,lasercode,lasingtime) self.LaserTarget[playername] = TARGET:New(target) - self.LaserTarget[playername].TStatus = 9 + --self.LaserTarget[playername].TStatus = 9 self:__TargetLasing(-1,client,target,lasercode,lasingtime) end return self @@ -1027,6 +1041,13 @@ function PLAYERRECCE:_SwitchLasing(client,group,playername) MESSAGE:New("Lasing is now ON",10,self.Name or "FACA"):ToClient(client) else self.AutoLase[playername] = false + if self.LaserSpots[playername] then + local laser = self.LaserSpots[playername] -- Core.Spot#SPOT + if laser:IsLasing() then + laser:LaseOff() + end + self.LaserSpots[playername] = nil + end MESSAGE:New("Lasing is now OFF",10,self.Name or "FACA"):ToClient(client) end if self.ClientMenus[playername] then @@ -1681,7 +1702,7 @@ function PLAYERRECCE:onafterRecceOnStation(From, Event, To, Client, Playername) local text2tts = string.format("All stations, FACA %s on station at %s!",callsign, coordtext) text2tts = self:_GetTextForSpeech(text2tts) if self.debug then - self:I(text2.."\n"..text2tts) + self:T(text2.."\n"..text2tts) end if self.UseSRS then local grp = Client:GetGroup() @@ -1720,7 +1741,7 @@ function PLAYERRECCE:onafterRecceOffStation(From, Event, To, Client, Playername) local texttts = string.format("All stations, FACA %s leaving station at %s, good bye!",callsign, coordtext) texttts = self:_GetTextForSpeech(texttts) if self.debug then - self:I(text.."\n"..texttts) + self:T(text.."\n"..texttts) end local text1 = "Going home!" if self.UseSRS then diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index b877fa004..76da83c94 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -239,6 +239,13 @@ AIRBASE.Nevada = { -- * AIRBASE.Normandy.Broglie -- * AIRBASE.Normandy.Bernay_Saint_Martin -- * AIRBASE.Normandy.Saint_Andre_de_lEure +-- * AIRBASE.Normandy.Biggin_Hill +-- * AIRBASE.Normandy.Manston +-- * AIRBASE.Normandy.Detling +-- * AIRBASE.Normandy.Lympne +-- * AIRBASE.Normandy.Abbeville_Drucat +-- * AIRBASE.Normandy.Merville_Calonne +-- * AIRBASE.Normandy.Saint_Omer_Wizernes -- -- @field Normandy AIRBASE.Normandy = { @@ -311,7 +318,14 @@ AIRBASE.Normandy = { ["Beaumont_le_Roger"] = "Beaumont-le-Roger", ["Broglie"] = "Broglie", ["Bernay_Saint_Martin"] = "Bernay Saint Martin", - ["Saint_Andre_de_lEure"] = "Saint-Andre-de-lEure", + ["Saint_Andre_de_lEure"] = "Saint-Andre-de-lEure", + ["Biggin_Hill"] = "Biggin Hill", + ["Manston"] = "Manston", + ["Detling"] = "Detling", + ["Lympne"] = "Lympne", + ["Abbeville_Drucat"] = "Abbeville Drucat", + ["Merville_Calonne"] = "Merville Calonne", + ["Saint_Omer_Wizernes"] = "Saint-Omer Wizernes", } --- Airbases of the Persion Gulf Map: diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index d603a3511..e013df17b 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -3974,7 +3974,7 @@ function CONTROLLABLE:OptionAAAttackRange( range ) local Controller = self:_GetController() if Controller then if self:IsAir() then - self:SetOption( AI.Option.Air.val.MISSILE_ATTACK, range ) + self:SetOption( AI.Option.Air.id.MISSILE_ATTACK, range ) end end return self diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index 593e9561f..b77214b99 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -2924,7 +2924,7 @@ function GROUP:GetCustomCallSign(ShortCallsign,Keepnumber,CallsignTranslations) return callsign end ---- +--- Set a GROUP to act as recovery tanker -- @param #GROUP self -- @param Wrapper.Group#GROUP CarrierGroup. -- @param #number Speed Speed in knots. @@ -2950,3 +2950,37 @@ function GROUP:SetAsRecoveryTanker(CarrierGroup,Speed,ToKIAS,Altitude,Delay,Last return self end + +--- Get a list of Link16 S/TN data from a GROUP. Can (as of Nov 2023) be obtained from F-18, F-16, F-15E (not the user flyable one) and A-10C-II groups. +-- @param #GROUP self +-- @return #table Table of data entries, indexed by unit name, each entry is a table containing STN, VCL (voice call label), VCN (voice call number), and Lead (#boolean, if true it's the flight lead) +-- @return #string Report Formatted report of all data +function GROUP:GetGroupSTN() + local tSTN = {} -- table + local units = self:GetUnits() + local gname = self:GetName() + gname = string.gsub(gname,"(#%d+)$","") + local report = REPORT:New() + report:Add("Link16 S/TN Report") + report:Add("Group: "..gname) + report:Add("==================") + for _,_unit in pairs(units) do + local unit = _unit -- Wrapper.Unit#UNIT + if unit and unit:IsAlive() then + local STN, VCL, VCN, Lead = unit:GetSTN() + local name = unit:GetName() + tSTN[name] = { + STN=STN, + VCL=VCL, + VCN=VCN, + Lead=Lead, + } + local lead = Lead == true and "(*)" or "" + report:Add(string.format("| %s%s %s %s",tostring(VCL),tostring(VCN),tostring(STN),lead)) + end + end + report:Add("==================") + local text = report:Text() + return tSTN,text +end + diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index 7ad0998ac..0b3e3300f 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -1659,3 +1659,36 @@ function UNIT:GetSkill() local skill = _DATABASE.Templates.Units[name].Template.skill or "Random" return skill end + +--- Get Link16 STN or SADL TN and other datalink info from Unit, if any. +-- @param #UNIT self +-- @return #string STN STN or TN Octal as string, or nil if not set/capable. +-- @return #string VCL Voice Callsign Label or nil if not set/capable. +-- @return #string VCN Voice Callsign Number or nil if not set/capable. +-- @return #string Lead If true, unit is Flight Lead, else false or nil. +function UNIT:GetSTN() + self:F2(self.UnitName) + local STN = nil -- STN/TN + local VCL = nil -- VoiceCallsignLabel + local VCN = nil -- VoiceCallsignNumber + local FGL = false -- FlightGroupLeader + local template = self:GetTemplate() + if template.AddPropAircraft then + if template.AddPropAircraft.STN_L16 then + STN = template.AddPropAircraft.STN_L16 + elseif template.AddPropAircraft.SADL_TN then + STN = template.AddPropAircraft.SADL_TN + end + VCN = template.AddPropAircraft.VoiceCallsignNumber + VCL = template.AddPropAircraft.VoiceCallsignLabel + end + if template.datalinks and template.datalinks.Link16 and template.datalinks.Link16.settings then + FGL = template.datalinks.Link16.settings.flightLead + end + -- A10CII + if template.datalinks and template.datalinks.SADL and template.datalinks.SADL.settings then + FGL = template.datalinks.SADL.settings.flightLead + end + + return STN, VCL, VCN, FGL +end