diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index 2d985a021..e1a1e732d 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -577,13 +577,19 @@ do -- Zones and Pathlines -- For a rectangular polygon drawing, we have the width (y) and height (x). local w=objectData.width local h=objectData.height + local rotation = UTILS.ToRadian(objectData.angle or 0) - -- Create points from center using with and height (width for y and height for x is a bit confusing, but this is how ED implemented it). - local points={} - points[1]={x=vec2.x-h/2, y=vec2.y+w/2} --Upper left - points[2]={x=vec2.x+h/2, y=vec2.y+w/2} --Upper right - points[3]={x=vec2.x+h/2, y=vec2.y-w/2} --Lower right - points[4]={x=vec2.x-h/2, y=vec2.y-w/2} --Lower left + local sinRot = math.sin(rotation) + local cosRot = math.cos(rotation) + local dx = h / 2 + local dy = w / 2 + + local points = { + { x = -dx * cosRot - (-dy * sinRot) + vec2.x, y = -dx * sinRot + (-dy * cosRot) + vec2.y }, + { x = dx * cosRot - (-dy * sinRot) + vec2.x, y = dx * sinRot + (-dy * cosRot) + vec2.y }, + { x = dx * cosRot - (dy * sinRot) + vec2.x, y = dx * sinRot + (dy * cosRot) + vec2.y }, + { x = -dx * cosRot - (dy * sinRot) + vec2.x, y = -dx * sinRot + (dy * cosRot) + vec2.y }, + } --local coord=COORDINATE:NewFromVec2(vec2):MarkToAll("MapX, MapY") @@ -1112,7 +1118,7 @@ function DATABASE:_RegisterGroupTemplate( GroupTemplate, CoalitionSide, Category self:E("WARNING: Invalid STN "..tostring(UnitTemplate.AddPropAircraft.STN_L16).." for ".. UnitTemplate.name) else self.STNS[stn] = UnitTemplate.name - self:I("Register STN "..tostring(UnitTemplate.AddPropAircraft.STN_L16).." for ".. UnitTemplate.name) + self:T("Register STN "..tostring(UnitTemplate.AddPropAircraft.STN_L16).." for ".. UnitTemplate.name) end end if UnitTemplate.AddPropAircraft.SADL_TN then @@ -1121,7 +1127,7 @@ function DATABASE:_RegisterGroupTemplate( GroupTemplate, CoalitionSide, Category self:E("WARNING: Invalid SADL "..tostring(UnitTemplate.AddPropAircraft.SADL_TN).." for ".. UnitTemplate.name) else self.SADL[sadl] = UnitTemplate.name - self:I("Register SADL "..tostring(UnitTemplate.AddPropAircraft.SADL_TN).." for ".. UnitTemplate.name) + self:T("Register SADL "..tostring(UnitTemplate.AddPropAircraft.SADL_TN).." for ".. UnitTemplate.name) end end end @@ -1382,7 +1388,7 @@ function DATABASE:GetCoalitionFromClientTemplate( ClientName ) if self.Templates.ClientsByName[ClientName] then return self.Templates.ClientsByName[ClientName].CoalitionID end - self:E("WARNING: Template does not exist for client "..tostring(ClientName)) + self:T("WARNING: Template does not exist for client "..tostring(ClientName)) return nil end @@ -1394,7 +1400,7 @@ function DATABASE:GetCategoryFromClientTemplate( ClientName ) if self.Templates.ClientsByName[ClientName] then return self.Templates.ClientsByName[ClientName].CategoryID end - self:E("WARNING: Template does not exist for client "..tostring(ClientName)) + self:T("WARNING: Template does not exist for client "..tostring(ClientName)) return nil end @@ -1406,7 +1412,7 @@ function DATABASE:GetCountryFromClientTemplate( ClientName ) if self.Templates.ClientsByName[ClientName] then return self.Templates.ClientsByName[ClientName].CountryID end - self:E("WARNING: Template does not exist for client "..tostring(ClientName)) + self:T("WARNING: Template does not exist for client "..tostring(ClientName)) return nil end diff --git a/Moose Development/Moose/Core/Event.lua b/Moose Development/Moose/Core/Event.lua index c5893325b..7cdec63ac 100644 --- a/Moose Development/Moose/Core/Event.lua +++ b/Moose Development/Moose/Core/Event.lua @@ -1508,7 +1508,9 @@ function EVENT:onEvent( Event ) else if Event.place:isExist() and Object.getCategory(Event.place) ~= Object.Category.SCENERY then Event.Place=AIRBASE:Find(Event.place) - Event.PlaceName=Event.Place:GetName() + if Event.Place then + Event.PlaceName=Event.Place:GetName() + end end end end diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index a74356985..b17e40f27 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -777,7 +777,9 @@ do -- COORDINATE -- @return DCS#Vec2 Vec2 function COORDINATE:GetRandomVec2InRadius( OuterRadius, InnerRadius ) self:F2( { OuterRadius, InnerRadius } ) - + math.random() + math.random() + math.random() local Theta = 2 * math.pi * math.random() local Radials = math.random() + math.random() if Radials > 1 then @@ -837,6 +839,26 @@ do -- COORDINATE return land.getHeight( Vec2 ) end + --- Returns a table of DCS#Vec3 points representing the terrain profile between two points. + -- @param #COORDINATE self + -- @param Destination DCS#Vec3 Ending point of the profile. + -- @return #table DCS#Vec3 table of the profile + function COORDINATE:GetLandProfileVec3(Destination) + return land.profile(self:GetVec3(), Destination) + end + + --- Returns a table of #COORDINATE representing the terrain profile between two points. + -- @param #COORDINATE self + -- @param Destination #COORDINATE Ending coordinate of the profile. + -- @return #table #COORDINATE table of the profile + function COORDINATE:GetLandProfileCoordinates(Destination) + local points = self:GetLandProfileVec3(Destination:GetVec3()) + local coords = {} + for _, point in ipairs(points) do + table.insert(coords, COORDINATE:NewFromVec3(point)) + end + return coords + end --- Set the heading of the coordinate, if applicable. -- @param #COORDINATE self @@ -3797,7 +3819,26 @@ do -- COORDINATE function COORDINATE:GetRandomPointVec3InRadius( OuterRadius, InnerRadius ) return COORDINATE:NewFromVec3( self:GetRandomVec3InRadius( OuterRadius, InnerRadius ) ) end - + + +--- Search for clear zones in a given area. A powerful and efficient function using Disposition to find clear areas for spawning ground units avoiding trees, water and map scenery. +-- @param #number SearchRadius Radius of the search area. +-- @param #number PosRadius Required clear radius around each position. +-- @param #number NumPositions Number of positions to find. +-- @return #table A table of Core.Point#COORDINATE that are clear of map objects within the given PosRadius. nil if no positions are found. + function COORDINATE:GetSimpleZones(SearchRadius, PosRadius, NumPositions) + local clearPositions = UTILS.GetSimpleZones(self:GetVec3(), SearchRadius, PosRadius, NumPositions) + if clearPositions and #clearPositions > 0 then + local coords = {} + for _, pos in pairs(clearPositions) do + local coord = COORDINATE:NewFromVec2(pos) + table.insert(coords, coord) + end + return coords + end + return nil + end + end do diff --git a/Moose Development/Moose/Core/ScheduleDispatcher.lua b/Moose Development/Moose/Core/ScheduleDispatcher.lua index 023c20c95..8e062d82b 100644 --- a/Moose Development/Moose/Core/ScheduleDispatcher.lua +++ b/Moose Development/Moose/Core/ScheduleDispatcher.lua @@ -175,7 +175,7 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr local Name = Info.name or "?" local ErrorHandler = function( errmsg ) - env.info( "Error in timer function: " .. errmsg ) + env.info( "Error in timer function: " .. errmsg or "" ) if BASE.Debug ~= nil then env.info( BASE.Debug.traceback() ) end diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index 7f863abf2..5ee275fd5 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -6691,6 +6691,8 @@ do -- SET_ZONE -- -- -- Stop watching after 1 hour -- zoneset:__TriggerStop(3600) + -- -- Call :SetPartlyInside() on any zone (or SET_ZONE) if you want GROUPs to count as inside when any of their units enters even if they are far apart. + -- -- Make sure to call :SetPartlyInside() before :Trigger()!. function SET_ZONE:Trigger(Objects) --self:I("Added Set_Zone Trigger") self:AddTransition("*","TriggerStart","TriggerRunning") @@ -6741,6 +6743,20 @@ do -- SET_ZONE -- @param Core.Zone#ZONE_BASE Zone The zone left. end + --- Toggle “partly-inside” handling for every zone in the set when those zones are used with :Trigger(). + -- * Call with no argument or **true** → enable for all. + -- * Call with **false** → disable again (handy if it was enabled before). + -- @param #SET_ZONE self + -- @return #SET_ZONE self + function SET_ZONE:SetPartlyInside(state) + for _,Zone in pairs(self.Set) do + if Zone.SetPartlyInside then + Zone:SetPartlyInside(state) + end + end + return self + end + --- (Internal) Check the assigned objects for being in/out of the zone -- @param #SET_ZONE self -- @param #boolean fromstart If true, do the init of the objects @@ -6776,8 +6792,13 @@ do -- SET_ZONE -- has not been tagged previously - wasn't in set! obj.TriggerInZone[_zone.ZoneName] = false end - -- is obj in zone? - local inzone = _zone:IsCoordinateInZone(obj:GetCoordinate()) + -- is obj in this zone? + local inzone + if _zone.PartlyInside and obj.ClassName == "GROUP" then + inzone = obj:IsAnyInZone(_zone) -- TRUE as soon as any unit is inside + else + inzone = _zone:IsCoordinateInZone(obj:GetCoordinate()) -- original centroid test + end --self:I("Object "..obj:GetName().." is in zone: "..tostring(inzone)) if inzone and not obj.TriggerInZone[_zone.ZoneName] then -- wasn't in zone before @@ -7844,6 +7865,28 @@ do -- SET_OPSGROUP return self end + --- Iterate the SET_OPSGROUP and count how many GROUPs and UNITs are alive. + -- @param #SET_GROUP self + -- @return #number The number of GROUPs alive. + -- @return #number The number of UNITs alive. + function SET_OPSGROUP:CountAlive() + local CountG = 0 + local CountU = 0 + + local Set = self:GetSet() + + for GroupID, GroupData in pairs( Set ) do -- For each GROUP in SET_GROUP + if GroupData and GroupData:IsAlive() then + CountG = CountG + 1 + -- Count Units. + CountU = CountU + GroupData:GetGroup():CountAliveUnits() + end + + end + + return CountG, CountU + end + --- Finds an OPSGROUP based on the group name. -- @param #SET_OPSGROUP self -- @param #string GroupName Name of the group. diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index c81aea59d..f58937439 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -1049,6 +1049,23 @@ function SPAWN:InitSetUnitAbsolutePositions(Positions) return self end + +--- Uses Disposition and other fallback logic to find better ground positions for ground units. +--- NOTE: This is not a spawn randomizer. +--- It will try to find clear ground locations avoiding trees, water, roads, runways, map scenery, statics and other units in the area. +--- Maintains the original layout and unit positions as close as possible by searching for the next closest valid position to each unit. +-- @param #SPAWN self +-- @param #boolean OnOff Enable/disable the feature. +-- @param #number MaxRadius (Optional) Max radius to search for valid ground locations in meters. Default is double the max radius of the units. +-- @param #number Spacing (Optional) Minimum spacing between units in meters. Default is 5% of the search radius or 5 meters, whichever is larger. +-- @return #SPAWN +function SPAWN:InitValidateAndRepositionGroundUnits(OnOff, MaxRadius, Spacing) + self.SpawnValidateAndRepositionGroundUnits = OnOff + self.SpawnValidateAndRepositionGroundUnitsRadius = MaxRadius + self.SpawnValidateAndRepositionGroundUnitsSpacing = Spacing + return self +end + --- This method is rather complicated to understand. But I'll try to explain. -- This method becomes useful when you need to spawn groups with random templates of groups defined within the mission editor, -- but they will all follow the same Template route and have the same prefix name. @@ -1829,7 +1846,13 @@ function SPAWN:SpawnWithIndex( SpawnIndex, NoBirth ) if self.SpawnHiddenOnMap then SpawnTemplate.hidden=self.SpawnHiddenOnMap end - + + if self.SpawnValidateAndRepositionGroundUnits then + local units = SpawnTemplate.units + local gPos = { x = SpawnTemplate.x, y = SpawnTemplate.y } + UTILS.ValidateAndRepositionGroundUnits(units, gPos, self.SpawnValidateAndRepositionGroundUnitsRadius, self.SpawnValidateAndRepositionGroundUnitsSpacing) + end + -- Set country, coalition and category. SpawnTemplate.CategoryID = self.SpawnInitCategory or SpawnTemplate.CategoryID SpawnTemplate.CountryID = self.SpawnInitCountry or SpawnTemplate.CountryID diff --git a/Moose Development/Moose/Core/SpawnStatic.lua b/Moose Development/Moose/Core/SpawnStatic.lua index 344d1a5d7..06a9179e1 100644 --- a/Moose Development/Moose/Core/SpawnStatic.lua +++ b/Moose Development/Moose/Core/SpawnStatic.lua @@ -378,6 +378,20 @@ function SPAWNSTATIC:InitLinkToUnit(Unit, OffsetX, OffsetY, OffsetAngle) return self end +--- Uses Disposition and other fallback logic to find a better and valid ground spawn position. +--- NOTE: This is not a spawn randomizer. +--- It will try to a find clear ground location avoiding trees, water, roads, runways, map scenery, other statics and other units in the area. +--- Uses the initial position if it's a valid location. +-- @param #SPAWNSTATIC self +-- @param #boolean OnOff Enable/disable the feature. +-- @param #number MaxRadius (Optional) Max radius to search for a valid ground location in meters. Default is 10 times the max radius of the static. +-- @return #SPAWNSTATIC self +function SPAWNSTATIC:InitValidateAndRepositionStatic(OnOff, MaxRadius) + self.ValidateAndRepositionStatic = OnOff + self.ValidateAndRepositionStaticMaxRadius = MaxRadius + return self +end + --- Allows to place a CallFunction hook when a new static spawns. -- The provided method will be called when a new group is spawned, including its given parameters. -- The first parameter of the SpawnFunction is the @{Wrapper.Static#STATIC} that was spawned. @@ -544,6 +558,14 @@ function SPAWNSTATIC:_SpawnStatic(Template, CountryID) -- Add static to the game. local Static=nil --DCS#StaticObject + if self.ValidateAndRepositionStatic then + local validPos = UTILS.ValidateAndRepositionStatic(CountryID, Template.category, Template.type, Template, Template.shape_name, self.ValidateAndRepositionStaticMaxRadius) + if validPos then + Template.x = validPos.x + Template.y = validPos.y + end + end + if self.InitFarp then local TemplateGroup={} @@ -611,16 +633,15 @@ function SPAWNSTATIC:_SpawnStatic(Template, CountryID) if self.StaticCopyFrom ~= nil then mystatic.StaticCopyFrom = self.StaticCopyFrom - if not _DATABASE.Templates.Statics[Template.name] then - local TemplateGroup={} - TemplateGroup.units={} - TemplateGroup.units[1]=Template - TemplateGroup.x=Template.x - TemplateGroup.y=Template.y - TemplateGroup.name=Template.name - _DATABASE:_RegisterStaticTemplate( TemplateGroup, self.CoalitionID, self.CategoryID, CountryID ) - end end + + local TemplateGroup={} + TemplateGroup.units={} + TemplateGroup.units[1]=Template + TemplateGroup.x=Template.x + TemplateGroup.y=Template.y + TemplateGroup.name=Template.name + _DATABASE:_RegisterStaticTemplate( TemplateGroup, self.CoalitionID, self.CategoryID, CountryID ) return mystatic end diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 6664364ed..c2deb6823 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -70,6 +70,7 @@ -- @field #table Table of any trigger zone properties from the ME. The key is the Name of the property, and the value is the property's Value. -- @field #number Surface Type of surface. Only determined at the center of the zone! -- @field #number Checktime Check every Checktime seconds, used for ZONE:Trigger() +-- @field #boolean PartlyInside When called, a GROUP is considered inside as soon as any of its units enters the zone even if they are far apart. -- @extends Core.Fsm#FSM @@ -534,6 +535,19 @@ function ZONE_BASE:GetZoneProbability() return self.ZoneProbability end +--- Get the coordinate on the radius of the zone nearest to Outsidecoordinate. Useto e.g. find an ingress point. +-- @param #ZONE_BASE self +-- @param Core.Point#COORDINATE Outsidecoordinate The coordinate outside of the zone from where to look. +-- @return Core.Point#COORDINATE CoordinateOnRadius +function ZONE_BASE:FindNearestCoordinateOnRadius(Outsidecoordinate) + local Vec1 = self:GetVec2() + local Radius = self:GetRadius() + local Vec2 = Outsidecoordinate:GetVec2() + local Point = UTILS.FindNearestPointOnCircle(Vec1,Radius,Vec2) + local rc = COORDINATE:NewFromVec2(Point) + return rc +end + --- Get the zone taking into account the randomization probability of a zone to be selected. -- @param #ZONE_BASE self -- @return #ZONE_BASE The zone is selected taking into account the randomization probability factor. @@ -599,6 +613,8 @@ end -- -- -- Stop watching the zone after 1 hour -- triggerzone:__TriggerStop(3600) +-- -- Call :SetPartlyInside() if you use SET_GROUP to count as inside when any of their units enters even when they are far apart. +-- -- Make sure to call :SetPartlyInside() before :Trigger()! function ZONE_BASE:Trigger(Objects) --self:I("Added Zone Trigger") self:SetStartState("TriggerStopped") @@ -667,6 +683,16 @@ function ZONE_BASE:Trigger(Objects) end + --- Toggle “partly-inside” handling for this zone. To be used before :Trigger(). + -- * Default:* flag is **false** until you call the method. + -- * Call with no argument or with **true** → enable. + -- * Call with **false** → disable again (handy if it was enabled before). + -- @param #ZONE_BASE self + -- @return #ZONE_BASE self + function ZONE_BASE:SetPartlyInside(state) + self.PartlyInside = state or not ( state == false ) + return self + end --- (Internal) Check the assigned objects for being in/out of the zone -- @param #ZONE_BASE self -- @param #boolean fromstart If true, do the init of the objects @@ -705,7 +731,12 @@ function ZONE_BASE:_TriggerCheck(fromstart) obj.TriggerInZone[self.ZoneName] = false end -- is obj in zone? - local inzone = self:IsCoordinateInZone(obj:GetCoordinate()) + local inzone + if self.PartlyInside and obj.ClassName == "GROUP" then + inzone = obj:IsAnyInZone(self) -- TRUE if any unit is inside + else + inzone = self:IsCoordinateInZone(obj:GetCoordinate()) -- original barycentre test + end --self:I("Object "..obj:GetName().." is in zone: "..tostring(inzone)) if inzone and obj.TriggerInZone[self.ZoneName] then -- just count @@ -1149,15 +1180,13 @@ function ZONE_RADIUS:Scan( ObjectCategories, UnitCategories ) local function EvaluateZone( ZoneObject ) --if ZoneObject:isExist() then --FF: isExist always returns false for SCENERY objects since DCS 2.2 and still in DCS 2.5 - if ZoneObject then + if ZoneObject and self:IsVec3InZone(ZoneObject:getPoint()) then -- Get object category. local ObjectCategory = Object.getCategory(ZoneObject) if ( ObjectCategory == Object.Category.UNIT and ZoneObject:isExist() and ZoneObject:isActive() ) or (ObjectCategory == Object.Category.STATIC and ZoneObject:isExist()) then - local CoalitionDCSUnit = ZoneObject:getCoalition() - local Include = false if not UnitCategories then -- Anything found is included. @@ -1509,6 +1538,26 @@ function ZONE_RADIUS:IsVec3InZone( Vec3 ) return InZone end +--- Search for clear ground spawn zones within this zone. A powerful and efficient function using Disposition to find clear areas for spawning ground units avoiding trees, water and map scenery. +-- @param #ZONE_RADIUS self +-- @param #number PosRadius Required clear radius around each position. +-- @param #number NumPositions Number of positions to find. +-- @return #table A table of DCS#Vec2 positions that are clear of map objects within the given PosRadius. nil if no clear positions are found. +function ZONE_RADIUS:GetClearZonePositions(PosRadius, NumPositions) + return UTILS.GetClearZonePositions(self, PosRadius, NumPositions) +end + + +--- Search for a random clear ground spawn coordinate within this zone. A powerful and efficient function using Disposition to find clear areas for spawning ground units avoiding trees, water and map scenery. +-- @param #ZONE_RADIUS self +-- @param #number PosRadius (Optional) Required clear radius around each position. (Default is math.min(Radius/10, 200)) +-- @param #number NumPositions (Optional) Number of positions to find. (Default 50) +-- @return Core.Point#COORDINATE A random coordinate for a clear zone. nil if no clear positions are found. +-- @return #number Assigned radius for the found zones. nil if no clear positions are found. +function ZONE_RADIUS:GetRandomClearZoneCoordinate(PosRadius, NumPositions) + return UTILS.GetRandomClearZoneCoordinate(self, PosRadius, NumPositions) +end + --- Returns a random Vec2 location within the zone. -- @param #ZONE_RADIUS self -- @param #number inner (Optional) Minimal distance from the center of the zone. Default is 0. @@ -1520,6 +1569,10 @@ function ZONE_RADIUS:GetRandomVec2(inner, outer, surfacetypes) local Vec2 = self:GetVec2() local _inner = inner or 0 local _outer = outer or self:GetRadius() + + math.random() + math.random() + math.random() if surfacetypes and type(surfacetypes)~="table" then surfacetypes={surfacetypes} @@ -2487,6 +2540,26 @@ function ZONE_POLYGON_BASE:Flush() return self end +--- Search for clear ground spawn zones within this zone. A powerful and efficient function using Disposition to find clear areas for spawning ground units avoiding trees, water and map scenery. +-- @param #ZONE_POLYGON_BASE self +-- @param #number PosRadius Required clear radius around each position. +-- @param #number NumPositions Number of positions to find. +-- @return #table A table of DCS#Vec2 positions that are clear of map objects within the given PosRadius. nil if no clear positions are found. +function ZONE_POLYGON_BASE:GetClearZonePositions(PosRadius, NumPositions) + return UTILS.GetClearZonePositions(self, PosRadius, NumPositions) +end + + +--- Search for a random clear ground spawn coordinate within this zone. A powerful and efficient function using Disposition to find clear areas for spawning ground units avoiding trees, water and map scenery. +-- @param #ZONE_POLYGON_BASE self +-- @param #number PosRadius (Optional) Required clear radius around each position. (Default is math.min(Radius/10, 200)) +-- @param #number NumPositions (Optional) Number of positions to find. (Default 50) +-- @return Core.Point#COORDINATE A random coordinate for a clear zone. nil if no clear positions are found. +-- @return #number Assigned radius for the found zones. nil if no clear positions are found. +function ZONE_POLYGON_BASE:GetRandomClearZoneCoordinate(PosRadius, NumPositions) + return UTILS.GetRandomClearZoneCoordinate(self, PosRadius, NumPositions) +end + --- Smokes the zone boundaries in a color. -- @param #ZONE_POLYGON_BASE self -- @param #boolean UnBound If true, the tyres will be destroyed. @@ -2865,6 +2938,11 @@ end function ZONE_POLYGON_BASE:GetRandomVec2() -- make sure we assign weights to the triangles based on their surface area, otherwise -- we'll be more likely to generate random points in smaller triangles + + math.random() + math.random() + math.random() + local weights = {} for _, triangle in pairs(self._Triangles) do weights[triangle] = triangle.SurfaceArea / self.SurfaceArea @@ -3228,14 +3306,12 @@ function ZONE_POLYGON:Scan( ObjectCategories, UnitCategories ) local function EvaluateZone( ZoneObject ) - if ZoneObject then + if ZoneObject and self:IsVec3InZone(ZoneObject:getPoint()) then local ObjectCategory = Object.getCategory(ZoneObject) if ( ObjectCategory == Object.Category.UNIT and ZoneObject:isExist() and ZoneObject:isActive() ) or (ObjectCategory == Object.Category.STATIC and ZoneObject:isExist()) then - local CoalitionDCSUnit = ZoneObject:getCoalition() - local Include = false if not UnitCategories then -- Anything found is included. @@ -3267,7 +3343,7 @@ function ZONE_POLYGON:Scan( ObjectCategories, UnitCategories ) end -- trying with box search - if ObjectCategory == Object.Category.SCENERY and self:IsVec3InZone(ZoneObject:getPoint()) then + if ObjectCategory == Object.Category.SCENERY then local SceneryType = ZoneObject:getTypeName() local SceneryName = ZoneObject:getName() self.ScanData.Scenery[SceneryType] = self.ScanData.Scenery[SceneryType] or {} diff --git a/Moose Development/Moose/Functional/Mantis.lua b/Moose Development/Moose/Functional/Mantis.lua index 4b761e99f..2e1ff3b18 100644 --- a/Moose Development/Moose/Functional/Mantis.lua +++ b/Moose Development/Moose/Functional/Mantis.lua @@ -22,7 +22,7 @@ -- @module Functional.Mantis -- @image Functional.Mantis.jpg -- --- Last Update: July 2025 +-- Last Update: August 2025 ------------------------------------------------------------------------- --- **MANTIS** class, extends Core.Base#BASE @@ -108,10 +108,15 @@ -- * Patriot -- * Rapier -- * Roland +-- * IRIS-T SLM +-- * Pantsir S1 +-- * TOR M2 +-- * C-RAM -- * Silkworm (though strictly speaking this is a surface to ship missile) -- * SA-2, SA-3, SA-5, SA-6, SA-7, SA-8, SA-9, SA-10, SA-11, SA-13, SA-15, SA-19 -- * From IDF mod: STUNNER IDFA, TAMIR IDFA (Note all caps!) -- * From HDS (see note on HDS below): SA-2, SA-3, SA-10B, SA-10C, SA-12, SA-17, SA-20A, SA-20B, SA-23, HQ-2, SAMP/T Block 1, SAMP/T Block 1INT, SAMP/T Block2 +-- * Other Mods: Nike -- -- * From SMA: RBS98M, RBS70, RBS90, RBS90M, RBS103A, RBS103B, RBS103AM, RBS103BM, Lvkv9040M -- **NOTE** If you are using the Swedish Military Assets (SMA), please note that the **group name** for RBS-SAM types also needs to contain the keyword "SMA" @@ -275,7 +280,7 @@ MANTIS = { ClassName = "MANTIS", name = "mymantis", - version = "0.9.32", + version = "0.9.34", SAM_Templates_Prefix = "", SAM_Group = nil, EWR_Templates_Prefix = "", @@ -384,7 +389,7 @@ MANTIS.SamData = { ["Chaparral"] = { Range=8, Blindspot=0, Height=3, Type="Short", Radar="Chaparral" }, ["Linebacker"] = { Range=4, Blindspot=0, Height=3, Type="Point", Radar="Linebacker", Point="true" }, ["Silkworm"] = { Range=90, Blindspot=1, Height=0.2, Type="Long", Radar="Silkworm" }, - ["HEMTT_C-RAM_Phalanx"] = { Range=2, Blindspot=0, Height=2, Type="Point", Radar="HEMTT_C-RAM_Phalanx", Point="true" }, + ["C-RAM"] = { Range=2, Blindspot=0, Height=2, Type="Point", Radar="HEMTT_C-RAM_Phalanx", Point="true" }, -- units from HDS Mod, multi launcher options is tricky ["SA-10B"] = { Range=75, Blindspot=0, Height=18, Type="Medium" , Radar="SA-10B"}, ["SA-17"] = { Range=50, Blindspot=3, Height=50, Type="Medium", Radar="SA-17" }, @@ -392,7 +397,13 @@ MANTIS.SamData = { ["SA-20B"] = { Range=200, Blindspot=4, Height=27, Type="Long" , Radar="S-300PMU2"}, ["HQ-2"] = { Range=50, Blindspot=6, Height=35, Type="Medium", Radar="HQ_2_Guideline_LN" }, ["TAMIR IDFA"] = { Range=20, Blindspot=0.6, Height=12.3, Type="Short", Radar="IRON_DOME_LN" }, - ["STUNNER IDFA"] = { Range=250, Blindspot=1, Height=45, Type="Long", Radar="DAVID_SLING_LN" }, + ["STUNNER IDFA"] = { Range=250, Blindspot=1, Height=45, Type="Long", Radar="DAVID_SLING_LN" }, + ["NIKE"] = { Range=155, Blindspot=6, Height=30, Type="Long", Radar="HIPAR" }, + ["Dog Ear"] = { Range=11, Blindspot=0, Height=9, Type="Point", Radar="Dog Ear", Point="true" }, + -- CH Added to DCS core 2.9.19.x + ["Pantsir S1"] = { Range=20, Blindspot=1.2, Height=15, Type="Point", Radar="PantsirS1" , Point="true" }, + ["Tor M2"] = { Range=12, Blindspot=1, Height=10, Type="Point", Radar="TorM2", Point="true" }, + ["IRIS-T SLM"] = { Range=40, Blindspot=0.5, Height=20, Type="Medium", Radar="CH_IRIST_SLM" }, } --- SAM data HDS @@ -458,15 +469,15 @@ MANTIS.SamDataCH = { -- https://www.currenthill.com/ -- group name MUST contain CHM to ID launcher type correctly! ["2S38 CHM"] = { Range=6, Blindspot=0.1, Height=4.5, Type="Short", Radar="2S38" }, - ["PantsirS1 CHM"] = { Range=20, Blindspot=1.2, Height=15, Type="Short", Radar="PantsirS1" }, + ["PantsirS1 CHM"] = { Range=20, Blindspot=1.2, Height=15, Type="Point", Radar="PantsirS1", Point="true" }, ["PantsirS2 CHM"] = { Range=30, Blindspot=1.2, Height=18, Type="Medium", Radar="PantsirS2" }, ["PGL-625 CHM"] = { Range=10, Blindspot=1, Height=5, Type="Short", Radar="PGL_625" }, ["HQ-17A CHM"] = { Range=15, Blindspot=1.5, Height=10, Type="Short", Radar="HQ17A" }, ["M903PAC2 CHM"] = { Range=120, Blindspot=3, Height=24.5, Type="Long", Radar="MIM104_M903_PAC2" }, ["M903PAC3 CHM"] = { Range=160, Blindspot=1, Height=40, Type="Long", Radar="MIM104_M903_PAC3" }, - ["TorM2 CHM"] = { Range=12, Blindspot=1, Height=10, Type="Short", Radar="TorM2" }, - ["TorM2K CHM"] = { Range=12, Blindspot=1, Height=10, Type="Short", Radar="TorM2K" }, - ["TorM2M CHM"] = { Range=16, Blindspot=1, Height=10, Type="Short", Radar="TorM2M" }, + ["TorM2 CHM"] = { Range=12, Blindspot=1, Height=10, Type="Point", Radar="TorM2", Point="true" }, + ["TorM2K CHM"] = { Range=12, Blindspot=1, Height=10, Type="Point", Radar="TorM2K", Point="true" }, + ["TorM2M CHM"] = { Range=16, Blindspot=1, Height=10, Type="Point", Radar="TorM2M", Point="true" }, ["NASAMS3-AMRAAMER CHM"] = { Range=50, Blindspot=2, Height=35.7, Type="Medium", Radar="CH_NASAMS3_LN_AMRAAM_ER" }, ["NASAMS3-AIM9X2 CHM"] = { Range=20, Blindspot=0.2, Height=18, Type="Short", Radar="CH_NASAMS3_LN_AIM9X2" }, ["C-RAM CHM"] = { Range=2, Blindspot=0, Height=2, Type="Point", Radar="CH_Centurion_C_RAM", Point="true" }, @@ -882,7 +893,11 @@ do self.AcceptZones = AcceptZones or {} self.RejectZones = RejectZones or {} self.ConflictZones = ConflictZones or {} - if #self.AcceptZones > 0 or #self.RejectZones > 0 or #self.ConflictZones > 0 then + self.AcceptZonesNo = UTILS.TableLength(self.AcceptZones) + self.RejectZonesNo = UTILS.TableLength(self.RejectZones) + self.ConflictZonesNo = UTILS.TableLength(self.ConflictZones) + self:T(string.format("AcceptZonesNo = %d | RejectZonesNo = %d | ConflictZonesNo = %d",self.AcceptZonesNo,self.RejectZonesNo,self.ConflictZonesNo)) + if self.AcceptZonesNo > 0 or self.RejectZonesNo > 0 or self.ConflictZonesNo > 0 then self.usezones = true end return self @@ -1274,7 +1289,8 @@ do self:T(self.lid.."_CheckCoordinateInZones") local inzone = false -- acceptzones - if #self.AcceptZones > 0 then + self:T(string.format("AcceptZonesNo = %d | RejectZonesNo = %d | ConflictZonesNo = %d",self.AcceptZonesNo,self.RejectZonesNo,self.ConflictZonesNo)) + if self.AcceptZonesNo > 0 then for _,_zone in pairs(self.AcceptZones) do local zone = _zone -- Core.Zone#ZONE if zone:IsCoordinateInZone(coord) then @@ -1285,7 +1301,7 @@ do end end -- rejectzones - if #self.RejectZones > 0 and inzone then -- maybe in accept zone, but check the overlaps + if self.RejectZonesNo > 0 then for _,_zone in pairs(self.RejectZones) do local zone = _zone -- Core.Zone#ZONE if zone:IsCoordinateInZone(coord) then @@ -1296,7 +1312,7 @@ do end end -- conflictzones - if #self.ConflictZones > 0 and not inzone then -- if not already accepted, might be in conflict zones + if self.ConflictZonesNo > 0 then for _,_zone in pairs(self.ConflictZones) do local zone = _zone -- Core.Zone#ZONE if zone:IsCoordinateInZone(coord) then @@ -1362,6 +1378,7 @@ do end -- check accept/reject zones local zonecheck = true + self:T("self.usezones = "..tostring(self.usezones)) if self.usezones then -- DONE zonecheck = self:_CheckCoordinateInZones(coord) diff --git a/Moose Development/Moose/Functional/Scoring.lua b/Moose Development/Moose/Functional/Scoring.lua index c4a0c8e20..2c49243f8 100644 --- a/Moose Development/Moose/Functional/Scoring.lua +++ b/Moose Development/Moose/Functional/Scoring.lua @@ -321,7 +321,9 @@ function SCORING:New( GameName, SavePath, AutoSave ) -- Create the CSV file. self.AutoSavePath = SavePath self.AutoSave = AutoSave or true - self:OpenCSV( GameName ) + if self.AutoSave == true then + self:OpenCSV( GameName ) + end return self @@ -985,6 +987,7 @@ function SCORING:_EventOnHit( Event ) local TargetUnitCoalition = nil local TargetUnitCategory = nil local TargetUnitType = nil + local TargetIsScenery = false if Event.IniDCSUnit then @@ -1025,6 +1028,12 @@ function SCORING:_EventOnHit( Event ) TargetCategory = Event.TgtCategory TargetType = Event.TgtTypeName + -- Scenery hit + if (not TargetCategory) and TargetUNIT ~= nil and TargetUnit:IsInstanceOf("SCENERY") then + TargetCategory = Unit.Category.STRUCTURE + TargetIsScenery = true + end + TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] TargetUnitCategory = _SCORINGCategory[TargetCategory] TargetUnitType = TargetType @@ -1117,17 +1126,22 @@ function SCORING:_EventOnHit( Event ) MESSAGE.Type.Update ) :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - else + elseif TargetIsScenery ~= true then MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. InitPlayerName .. "' hit enemy target " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.ScoreHit .. " times. " .. "Score: " .. PlayerHit.Score .. ". Score Total:" .. Player.Score - Player.Penalty, MESSAGE.Type.Update ) :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) + elseif TargetIsScenery == true then + MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. InitPlayerName .. "' hit scenery object." .. " Score: " .. PlayerHit.Score .. ". Score Total:" .. Player.Score - Player.Penalty, + MESSAGE.Type.Update ) + :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) end self:ScoreCSV( InitPlayerName, TargetPlayerName, "HIT_SCORE", 1, 1, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) end else -- A scenery object was hit. - MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. InitPlayerName .. "' hit scenery object.", + MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. InitPlayerName .. "' hit nothing special.", MESSAGE.Type.Update ) :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) @@ -1923,7 +1937,7 @@ function SCORING:ScoreCSV( PlayerName, TargetPlayerName, ScoreType, ScoreTimes, TargetUnitType = TargetUnitType or "" TargetUnitName = TargetUnitName or "" - if lfs and io and os and self.AutoSave then + if lfs and io and os and self.AutoSave == true and self.CSVFile ~= nil then self.CSVFile:write( '"' .. self.GameName .. '"' .. ',' .. '"' .. self.RunTime .. '"' .. ',' .. diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 2486e74b6..807f57f98 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -4247,6 +4247,16 @@ function WAREHOUSE:_AssetItemInfo(asset) self:T3({Template=asset.template}) end +--- This function uses Disposition and other fallback logic to find better ground positions for ground units. +--- NOTE: This is not a spawn randomizer. +--- It will try to find clear ground locations avoiding trees, water, roads, runways, map scenery, statics and other units in the area and modifies the provided positions table. +--- Maintains the original layout and unit positions as close as possible by searching for the next closest valid position to each unit. +--- Uses UTILS.ValidateAndRepositionGroundUnits. +-- @param #boolean Enabled Enable/disable the feature. +function WAREHOUSE:SetValidateAndRepositionGroundUnits(Enabled) + self.ValidateAndRepositionGroundUnits = Enabled +end + --- On after "NewAsset" event. A new asset has been added to the warehouse stock. -- @param #WAREHOUSE self -- @param #string From From state. @@ -5965,6 +5975,10 @@ function WAREHOUSE:_SpawnAssetGroundNaval(alias, asset, request, spawnzone, late template.y = coord.z template.alt = coord.y + if self.ValidateAndRepositionGroundUnits then + UTILS.ValidateAndRepositionGroundUnits(template.units) + end + -- Spawn group. local group=_DATABASE:Spawn(template) --Wrapper.Group#GROUP diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index f50802de2..d602c9018 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -39,6 +39,8 @@ -- * [USS America](https://en.wikipedia.org/wiki/USS_America_\(LHA-6\)) (LHA-6) -- * [Juan Carlos I](https://en.wikipedia.org/wiki/Spanish_amphibious_assault_ship_Juan_Carlos_I) (L61) -- * [HMAS Canberra](https://en.wikipedia.org/wiki/HMAS_Canberra_\(L02\)) (L02) +-- * BONHOMMERICHARD [VWV Mod] +-- * ENTERPRISE66 [VWV Mod] -- -- **Supported Aircraft:** -- @@ -1317,6 +1319,10 @@ AIRBOSS.AircraftCarrier={ -- @field #string FORRESTAL USS Forrestal (CV-59) [Heatblur Carrier Module] -- @field #string VINSON USS Carl Vinson (CVN-70) [Deprecated!] -- @field #string ESSEX Essex class carrier (e.g. USS Yorktown (CV-10)) [Magnitude 3 Carrier Module] +-- @field #string BONHOMMERICHARD USS Bon Homme Richard carrier [VWV Mod] +-- @field #string ESSEXSCB125 Generic Essex class carrier with angled deck (SCB-125 upgrade) [VWV Mod] +-- @field #string ENTERPRISE66 USS Enterprise in the 1966 configuration [VWV Mod] +-- @field #string ENTERPRISEMODERN USS Enterprise in a modern configuration [Derived VWV Mod] -- @field #string HERMES HMS Hermes (R12) [V/STOL Carrier] -- @field #string INVINCIBLE HMS Invincible (R05) [V/STOL Carrier] -- @field #string TARAWA USS Tarawa (LHA-1) [V/STOL Carrier] @@ -1331,8 +1337,12 @@ AIRBOSS.CarrierType = { TRUMAN = "CVN_75", STENNIS = "Stennis", FORRESTAL = "Forrestal", + ENTERPRISE66 = "USS Enterprise 1966", + ENTERPRISEMODERN = "cvn-65", VINSON = "VINSON", ESSEX = "Essex", + BONHOMMERICHARD = "USS Bon Homme Richard", + ESSEXSCB125 = "essex_scb125", HERMES = "HERMES81", INVINCIBLE = "hms_invincible", TARAWA = "LHA_Tarawa", @@ -1756,7 +1766,7 @@ AIRBOSS.MenuF10Root = nil --- Airboss class version. -- @field #string version -AIRBOSS.version = "1.4.0" +AIRBOSS.version = "1.4.2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2019,11 +2029,19 @@ function AIRBOSS:New( carriername, alias ) self:_InitNimitz() elseif self.carriertype == AIRBOSS.CarrierType.FORRESTAL then self:_InitForrestal() + elseif self.carriertype == AIRBOSS.CarrierType.ENTERPRISE66 then + self:_InitEnterprise() + elseif self.carriertype == AIRBOSS.CarrierType.ENTERPRISEMODERN then + self:_InitEnterprise() elseif self.carriertype == AIRBOSS.CarrierType.VINSON then -- Carl Vinson is legacy now. self:_InitStennis() elseif self.carriertype == AIRBOSS.CarrierType.ESSEX then self:_InitEssex() + elseif self.carriertype == AIRBOSS.CarrierType.BONHOMMERICHARD then + self:_InitBonHommeRichard() + elseif self.carriertype == AIRBOSS.CarrierType.ESSEXSCB125 then + self:_InitEssexSCB125() elseif self.carriertype == AIRBOSS.CarrierType.HERMES then -- Hermes parameters. self:_InitHermes() @@ -2412,6 +2430,16 @@ end -- USER API Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Set the carrier illumination mode. +-- @param #AIRBOSS self +-- @param #number Mode Options are: -2: OFF, -1: AUTO, 0: NAVIGATION, 1: AC LAUNCH, 2: AC RECOVERY +-- @return #AIRBOSS self +function AIRBOSS:SetCarrierIllumination(Mode) + self.carrier:SetCarrierIlluminationMode(Mode) + return self +end + + --- Set welcome messages for players. -- @param #AIRBOSS self -- @param #boolean Switch If true, display welcome message to player. @@ -3081,7 +3109,7 @@ end --- Set up SRS for usage without sound files -- @param #AIRBOSS self --- @param #string PathToSRS Path to SRS folder, e.g. "C:\\Program Files\\DCS-SimpleRadio-Standalone". +-- @param #string PathToSRS Path to SRS folder, e.g. "C:\\Program Files\\DCS-SimpleRadio\\ExternalAudio". -- @param #number Port Port of the SRS server, defaults to 5002. -- @param #string Culture (Optional, Airboss Culture) Culture, defaults to "en-US". -- @param #string Gender (Optional, Airboss Gender) Gender, e.g. "male" or "female". Defaults to "male". @@ -3100,8 +3128,8 @@ function AIRBOSS:EnableSRS(PathToSRS,Port,Culture,Gender,Voice,GoogleCreds,Volum self.SRS:SetCulture(Culture or "en-US") --self.SRS:SetFrequencies(Frequencies) self.SRS:SetGender(Gender or "male") - self.SRS:SetPath(PathToSRS) - self.SRS:SetPort(Port or 5002) + --self.SRS:SetPath(PathToSRS) + self.SRS:SetPort(Port or MSRS.port or 5002) self.SRS:SetLabel(self.AirbossRadio.alias or "AIRBOSS") self.SRS:SetCoordinate(self.carrier:GetCoordinate()) self.SRS:SetVolume(Volume or 1) @@ -3112,7 +3140,10 @@ function AIRBOSS:EnableSRS(PathToSRS,Port,Culture,Gender,Voice,GoogleCreds,Volum if Voice then self.SRS:SetVoice(Voice) end - self.SRS:SetVolume(Volume or 1.0) + if (not Voice) and self.SRS and self.SRS:GetProvider() == MSRS.Provider.GOOGLE then + self.SRS.voice = MSRS.poptions["gcloud"].voice or MSRS.Voices.Google.Standard.en_US_Standard_B + end + --self.SRS:SetVolume(Volume or 1.0) -- SRSQUEUE self.SRSQ = MSRSQUEUE:New("AIRBOSS") self.SRSQ:SetTransmitOnlyWithPlayers(true) @@ -4643,6 +4674,26 @@ function AIRBOSS:_InitForrestal() end +--- Init parameters for Enterprise carrier. +-- @param #AIRBOSS self +function AIRBOSS:_InitEnterprise() + -- Using Forrestal as template + self:_InitForrestal() + + self.carrierparam.sterndist = -164.30 + self.carrierparam.deckheight = 19.52 + + self.carrierparam.totlength = 335 + self.carrierparam.rwylength = 223 + + -- Wires. + self.carrierparam.wire1 = 57.7 + self.carrierparam.wire2 = 69.6 + self.carrierparam.wire3 = 79.5 + self.carrierparam.wire4 = 90.0 + +end + --- Init parameters for Essec class carriers. -- @param #AIRBOSS self function AIRBOSS:_InitEssex() @@ -4688,6 +4739,35 @@ function AIRBOSS:_InitEssex() end +--- Init parameters for CVA-31 Bon Homme Richard carriers. +-- @param #AIRBOSS self +function AIRBOSS:_InitBonHommeRichard() + -- Init Essex as default + self:_InitEssex() + + self.carrierparam.deckheight = 16.95 + + -- Landing runway. + -- from BHR EssexRunwayAndRoutes.lua + self.carrierparam.rwyangle = -11.4 + self.carrierparam.rwylength = 97 + self.carrierparam.rwywidth = 20 + + -- Wires. + self.carrierparam.wire1 = 40.4 -- Distance from stern to first wire. Original from Frank - 42 + self.carrierparam.wire2 = 45 + self.carrierparam.wire3 = 51 + self.carrierparam.wire4 = 58.1 +end + +--- Init parameters for Generic Essex SC125 class carriers. +-- @param #AIRBOSS self +function AIRBOSS:_InitEssexSCB125() + -- Init Bon Homme Richard as default + self:_InitBonHommeRichard() + +end + --- Init parameters for R12 HMS Hermes carrier. -- @param #AIRBOSS self function AIRBOSS:_InitHermes() diff --git a/Moose Development/Moose/Ops/CSAR.lua b/Moose Development/Moose/Ops/CSAR.lua index 8dede4270..9ba328b5c 100644 --- a/Moose Development/Moose/Ops/CSAR.lua +++ b/Moose Development/Moose/Ops/CSAR.lua @@ -1,25 +1,25 @@ --- **Ops** - Combat Search and Rescue. -- -- === --- +-- -- **CSAR** - MOOSE based Helicopter CSAR Operations. --- +-- -- === --- +-- -- ## Missions:--- **Ops** -- Combat Search and Rescue. -- -- === --- +-- -- **CSAR** - MOOSE based Helicopter CSAR Operations. --- +-- -- === --- +-- -- ## Missions: -- -- ### [CSAR - Combat Search & Rescue](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Ops/CSAR) --- +-- -- === --- +-- -- **Main Features:** -- -- * MOOSE-based Helicopter CSAR Operations for Players. @@ -31,7 +31,7 @@ -- @image OPS_CSAR.jpg --- --- Last Update May 2025 +-- Last Update Oct 2025 ------------------------------------------------------------------------- --- **CSAR** class, extends Core.Base#BASE, Core.Fsm#FSM @@ -49,24 +49,24 @@ -- === -- -- # CSAR Concept --- +-- -- * MOOSE-based Helicopter CSAR Operations for Players. -- * Object oriented refactoring of Ciribob\'s fantastic CSAR script. --- * No need for extra MIST loading. +-- * No need for extra MIST loading. -- * Additional events to tailor your mission. -- * Optional SpawnCASEVAC to create casualties without beacon (e.g. handling dead ground vehicles and create CASVAC requests). --- +-- -- ## 0. Prerequisites --- +-- -- You need to load an .ogg soundfile for the pilot\'s beacons into the mission, e.g. "beacon.ogg", use a once trigger, "sound to country" for that. -- Create a late-activated single infantry unit as template in the mission editor and name it e.g. "Downed Pilot". --- +-- -- Example sound files are here: [Moose Sound](https://github.com/FlightControl-Master/MOOSE_SOUND/tree/master/CTLD%20CSAR) --- +-- -- ## 1. Basic Setup --- +-- -- A basic setup example is the following: --- +-- -- -- Instantiate and start a CSAR for the blue side, with template "Downed Pilot" and alias "Luftrettung" -- local my_csar = CSAR:New(coalition.side.BLUE,"Downed Pilot","Luftrettung") -- -- options @@ -74,9 +74,9 @@ -- my_csar.invisiblecrew = false -- downed pilot spawn is visible -- -- start the FSM -- my_csar:__Start(5) --- +-- -- ## 2. Options --- +-- -- The following options are available (with their defaults). Only set the ones you want changed: -- -- mycsar.allowDownedPilotCAcontrol = false -- Set to false if you don\'t want to allow control by Combined Arms. @@ -87,7 +87,7 @@ -- mycsar.coordtype = 1 -- Use Lat/Long DDM (0), Lat/Long DMS (1), MGRS (2), Bullseye imperial (3) or Bullseye metric (4) for coordinates. -- mycsar.csarOncrash = false -- (WIP) If set to true, will generate a downed pilot when a plane crashes as well. -- mycsar.enableForAI = false -- set to false to disable AI pilots from being rescued. --- mycsar.pilotRuntoExtractPoint = true -- Downed pilot will run to the rescue helicopter up to mycsar.extractDistance in meters. +-- mycsar.pilotRuntoExtractPoint = true -- Downed pilot will run to the rescue helicopter up to mycsar.extractDistance in meters. -- mycsar.extractDistance = 500 -- Distance the downed pilot will start to run to the rescue helicopter. -- mycsar.immortalcrew = true -- Set to true to make wounded crew immortal. -- mycsar.invisiblecrew = false -- Set to true to make wounded crew insvisible. @@ -95,14 +95,14 @@ -- mycsar.mashprefix = {"MASH"} -- prefixes of #GROUP objects used as MASHes. Will also try to add ZONE and STATIC objects with this prefix once at startup. -- mycsar.max_units = 6 -- max number of pilots that can be carried if #CSAR.AircraftType is undefined. -- mycsar.messageTime = 15 -- Time to show messages for in seconds. Doubled for long messages. --- mycsar.radioSound = "beacon.ogg" -- the name of the sound file to use for the pilots\' radio beacons. +-- mycsar.radioSound = "beacon.ogg" -- the name of the sound file to use for the pilots\' radio beacons. -- mycsar.smokecolor = 4 -- Color of smokemarker, 0 is green, 1 is red, 2 is white, 3 is orange and 4 is blue. -- mycsar.useprefix = true -- Requires CSAR helicopter #GROUP names to have the prefix(es) defined below. --- mycsar.csarPrefix = { "helicargo", "MEDEVAC"} -- #GROUP name prefixes used for useprefix=true - DO NOT use # in helicopter names in the Mission Editor! +-- mycsar.csarPrefix = { "helicargo", "MEDEVAC"} -- #GROUP name prefixes used for useprefix=true - DO NOT use # in helicopter names in the Mission Editor! -- mycsar.verbose = 0 -- set to > 1 for stats output for debugging. -- -- limit amount of downed pilots spawned by **ejection** events -- mycsar.limitmaxdownedpilots = true --- mycsar.maxdownedpilots = 10 +-- mycsar.maxdownedpilots = 10 -- -- allow to set far/near distance for approach and optionally pilot must open doors -- mycsar.approachdist_far = 5000 -- switch do 10 sec interval approach mode, meters -- mycsar.approachdist_near = 3000 -- switch to 5 sec interval approach mode, meters @@ -119,16 +119,16 @@ -- mycsar.PilotWeight = 80 -- Loaded pilots weigh 80kgs each -- mycsar.AllowIRStrobe = false -- Allow a menu item to request an IR strobe to find a downed pilot at night (requires NVGs to see it). -- mycsar.IRStrobeRuntime = 300 -- If an IR Strobe is activated, it runs for 300 seconds (5 mins). --- +-- -- ## 2.1 Create own SET_GROUP to manage CTLD Pilot groups --- +-- -- -- Parameter: Set The SET_GROUP object created by the mission designer/user to represent the CSAR pilot groups. -- -- Needs to be set before starting the CSAR instance. -- local myset = SET_GROUP:New():FilterPrefixes("Helikopter"):FilterCoalitions("red"):FilterStart() -- mycsar:SetOwnSetPilotGroups(myset) --- +-- -- ## 2.2 SRS Features and Other Features --- +-- -- mycsar.useSRS = false -- Set true to use FF\'s SRS integration -- mycsar.SRSPath = "C:\\Progra~1\\DCS-SimpleRadio-Standalone\\ExternalAudio\\" -- adjust your own path in your SRS installation -- server(!) -- mycsar.SRSchannel = 300 -- radio channel @@ -147,88 +147,88 @@ -- mycsar.wetfeettemplate = "man in floating thingy" -- if you use a mod to have a pilot in a rescue float, put the template name in here for wet feet spawns. Note: in conjunction with csarUsePara this might create dual ejected pilots in edge cases. -- mycsar.allowbronco = false -- set to true to use the Bronco mod as a CSAR plane -- mycsar.CreateRadioBeacons = true -- set to false to disallow creating ADF radio beacons. --- +-- -- ## 3. Results --- +-- -- Number of successful landings with save pilots and aggregated number of saved pilots is stored in these variables in the object: --- +-- -- mycsar.rescues -- number of successful landings *with* saved pilots -- mycsar.rescuedpilots -- aggregated number of pilots rescued from the field (of *all* players) --- +-- -- ## 4. Events -- -- The class comes with a number of FSM-based events that missions designers can use to shape their mission. -- These are: --- --- ### 4.1. PilotDown. --- +-- +-- ### 4.1. PilotDown. +-- -- The event is triggered when a new downed pilot is detected. Use e.g. `function my_csar:OnAfterPilotDown(...)` to link into this event: --- +-- -- function my_csar:OnAfterPilotDown(from, event, to, spawnedgroup, frequency, groupname, coordinates_text) -- ... your code here ... -- end --- --- ### 4.2. Approach. --- +-- +-- ### 4.2. Approach. +-- -- A CSAR helicpoter is closing in on a downed pilot. Use e.g. `function my_csar:OnAfterApproach(...)` to link into this event: --- +-- -- function my_csar:OnAfterApproach(from, event, to, heliname, groupname) -- ... your code here ... -- end --- --- ### 4.3. Boarded. --- +-- +-- ### 4.3. Boarded. +-- -- The pilot has been boarded to the helicopter. Use e.g. `function my_csar:OnAfterBoarded(...)` to link into this event: --- +-- -- function my_csar:OnAfterBoarded(from, event, to, heliname, groupname, description) -- ... your code here ... -- end --- --- ### 4.4. Returning. --- +-- +-- ### 4.4. Returning. +-- -- The CSAR helicopter is ready to return to an Airbase, FARP or MASH. Use e.g. `function my_csar:OnAfterReturning(...)` to link into this event: --- +-- -- function my_csar:OnAfterReturning(from, event, to, heliname, groupname) -- ... your code here ... -- end --- --- ### 4.5. Rescued. --- +-- +-- ### 4.5. Rescued. +-- -- The CSAR helicopter has landed close to an Airbase/MASH/FARP and the pilots are safe. Use e.g. `function my_csar:OnAfterRescued(...)` to link into this event: --- +-- -- function my_csar:OnAfterRescued(from, event, to, heliunit, heliname, pilotssaved) -- ... your code here ... --- end +-- end -- -- ## 5. Spawn downed pilots at location to be picked up. --- +-- -- If missions designers want to spawn downed pilots into the field, e.g. at mission begin to give the helicopter guys works, they can do this like so: --- +-- -- -- Create downed "Pilot Wagner" in #ZONE "CSAR_Start_1" at a random point for the blue coalition -- my_csar:SpawnCSARAtZone( "CSAR_Start_1", coalition.side.BLUE, "Pilot Wagner", true ) -- -- --Create a casualty and CASEVAC request from a "Point" (VEC2) for the blue coalition --shagrat --- my_csar:SpawnCASEVAC(Point, coalition.side.BLUE) --- +-- my_csar:SpawnCASEVAC(Point, coalition.side.BLUE) +-- -- ## 6. Save and load downed pilots - Persistance --- +-- -- You can save and later load back downed pilots to make your mission persistent. -- For this to work, you need to de-sanitize **io** and **lfs** in your MissionScripting.lua, which is located in your DCS installtion folder under Scripts. -- There is a risk involved in doing that; if you do not know what that means, this is possibly not for you. --- +-- -- Use the following options to manage your saves: --- +-- -- mycsar.enableLoadSave = true -- allow auto-saving and loading of files -- mycsar.saveinterval = 600 -- save every 10 minutes -- mycsar.filename = "missionsave.csv" -- example filename -- mycsar.filepath = "C:\\Users\\myname\\Saved Games\\DCS\Missions\\MyMission" -- example path --- +-- -- Then use an initial load at the beginning of your mission: --- +-- -- mycsar:__Load(10) --- +-- -- **Caveat:** --- Dropped troop noMessage and forcedesc parameters aren't saved. +-- Dropped troop noMessage and forcedesc parameters aren't saved. -- -- @field #CSAR CSAR = { @@ -254,7 +254,7 @@ CSAR = { hoverStatus = {}, -- tracks status of a helis hover above a downed pilot pilotDisabled = {}, -- tracks what aircraft a pilot is disabled for pilotLives = {}, -- tracks how many lives a pilot has - useprefix = true, -- Use the Prefixed defined below, Requires Unit have the Prefix defined below + useprefix = true, -- Use the Prefixed defined below, Requires Unit have the Prefix defined below csarPrefix = {}, template = nil, mash = {}, @@ -300,11 +300,12 @@ CSAR.AircraftType["SA342L"] = 4 CSAR.AircraftType["SA342M"] = 4 CSAR.AircraftType["UH-1H"] = 8 CSAR.AircraftType["Mi-8MTV2"] = 12 -CSAR.AircraftType["Mi-8MT"] = 12 -CSAR.AircraftType["Mi-24P"] = 8 +CSAR.AircraftType["Mi-8MT"] = 12 +CSAR.AircraftType["Mi-24P"] = 8 CSAR.AircraftType["Mi-24V"] = 8 -CSAR.AircraftType["Bell-47"] = 2 +CSAR.AircraftType["Bell-47"] = 2 CSAR.AircraftType["UH-60L"] = 10 +CSAR.AircraftType["UH-60L_DAP"] = 2 CSAR.AircraftType["AH-64D_BLK_II"] = 2 CSAR.AircraftType["Bronco-OV-10A"] = 2 CSAR.AircraftType["MH-60R"] = 10 @@ -314,7 +315,7 @@ CSAR.AircraftType["CH-47Fbl1"] = 31 --- CSAR class version. -- @field #string version -CSAR.version="1.0.33" +CSAR.version="1.0.34" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list @@ -334,12 +335,12 @@ CSAR.version="1.0.33" -- @param #string Alias An *optional* alias how this object is called in the logs etc. -- @return #CSAR self function CSAR:New(Coalition, Template, Alias) - + -- Inherit everything from FSM class. local self=BASE:Inherit(self, FSM:New()) -- #CSAR - + BASE:T({Coalition, Template, Alias}) - + --set Coalition if Coalition and type(Coalition)=="string" then if Coalition=="blue" then @@ -358,12 +359,12 @@ function CSAR:New(Coalition, Template, Alias) self.coalition = Coalition self.coalitiontxt = string.lower(UTILS.GetCoalitionName(self.coalition)) end - + -- Set alias. if Alias then self.alias=tostring(Alias) else - self.alias="Red Cross" + self.alias="Red Cross" if self.coalition then if self.coalition==coalition.side.RED then self.alias="IFRC" @@ -372,10 +373,10 @@ function CSAR:New(Coalition, Template, Alias) end end end - + -- Set some string id for output to DCS.log file. self.lid=string.format("%s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown") - + -- Start State. self:SetStartState("Stopped") @@ -385,12 +386,12 @@ function CSAR:New(Coalition, Template, Alias) self:AddTransition("*", "Status", "*") -- CSAR status update. self:AddTransition("*", "PilotDown", "*") -- Downed Pilot added self:AddTransition("*", "Approach", "*") -- CSAR heli closing in. - self:AddTransition("*", "Landed", "*") -- CSAR heli landed + self:AddTransition("*", "Landed", "*") -- CSAR heli landed self:AddTransition("*", "Boarded", "*") -- Pilot boarded. self:AddTransition("*", "Returning", "*") -- CSAR able to return to base. self:AddTransition("*", "Rescued", "*") -- Pilot at MASH. self:AddTransition("*", "KIA", "*") -- Pilot killed in action. - self:AddTransition("*", "Load", "*") -- CSAR load event. + self:AddTransition("*", "Load", "*") -- CSAR load event. self:AddTransition("*", "Save", "*") -- CSAR save event. self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. @@ -411,7 +412,7 @@ function CSAR:New(Coalition, Template, Alias) self.woundedGroups = {} -- contains the new group of units self.downedPilots = {} -- Replacement woundedGroups self.downedpilotcounter = 1 - + -- settings, counters etc self.rescues = 0 -- counter for successful rescue landings at FARP/AFB/MASH self.rescuedpilots = 0 -- counter for saved pilots @@ -421,9 +422,9 @@ function CSAR:New(Coalition, Template, Alias) self.smokecolor = 4 -- Color of smokemarker for blue side, 0 is green, 1 is red, 2 is white, 3 is orange and 4 is blue self.coordtype = 2 -- Use Lat/Long DDM (0), Lat/Long DMS (1), MGRS (2), Bullseye imperial (3) or Bullseye metric (4) for coordinates. self.immortalcrew = true -- Set to true to make wounded crew immortal - self.invisiblecrew = false -- Set to true to make wounded crew insvisible - self.messageTime = 15 -- Time to show longer messages for in seconds - self.pilotRuntoExtractPoint = true -- Downed Pilot will run to the rescue helicopter up to self.extractDistance METERS + self.invisiblecrew = false -- Set to true to make wounded crew insvisible + self.messageTime = 15 -- Time to show longer messages for in seconds + self.pilotRuntoExtractPoint = true -- Downed Pilot will run to the rescue helicopter up to self.extractDistance METERS self.loadDistance = 75 -- configure distance for pilot to get in helicopter in meters. self.extractDistance = 500 -- Distance the Downed pilot will run to the rescue helicopter self.loadtimemax = 135 -- seconds @@ -432,11 +433,11 @@ function CSAR:New(Coalition, Template, Alias) self.allowFARPRescue = true --allows pilot to be rescued by landing at a FARP or Airbase self.FARPRescueDistance = 1000 -- you need to be this close to a FARP or Airport for the pilot to be rescued. self.max_units = 6 --max number of pilots that can be carried - self.useprefix = true -- Use the Prefixed defined below, Requires Unit have the Prefix defined below + self.useprefix = true -- Use the Prefixed defined below, Requires Unit have the Prefix defined below self.csarPrefix = { "helicargo", "MEDEVAC"} -- prefixes used for useprefix=true - DON\'T use # in names! self.template = Template or "generic" -- template for downed pilot self.mashprefix = {"MASH"} -- prefixes used to find MASHes - + self.autosmoke = false -- automatically smoke location when heli is near self.autosmokedistance = 2000 -- distance for autosmoke -- added 0.1.4 @@ -449,34 +450,34 @@ function CSAR:New(Coalition, Template, Alias) self.approachdist_near = 3000 -- switch to 5 sec interval approach mode, meters self.pilotmustopendoors = false -- switch to true to enable check on open doors self.suppressmessages = false - + -- added 0.1.11r1 self.rescuehoverheight = 20 self.rescuehoverdistance = 10 - + -- added 0.1.12 self.countryblue= country.id.USA self.countryred = country.id.RUSSIA self.countryneutral = country.id.UN_PEACEKEEPERS - + -- added 0.1.3 self.csarUsePara = false -- shagrat set to true, will use the LandingAfterEjection Event instead of Ejection - + -- added 0.1.4 self.wetfeettemplate = nil self.usewetfeet = false - + -- added 1.0.15 self.allowbronco = false -- set to true to use the Bronco mod as a CSAR plane - + self.ADFRadioPwr = 500 - + -- added 1.0.16 self.PilotWeight = 80 - + -- Own SET_GROUP if any self.UserSetGroup = nil - + -- WARNING - here\'ll be dragons -- for this to work you need to de-sanitize your mission environment in \Scripts\MissionScripting.lua -- needs SRS => 1.9.6 to work (works on the *server* side) @@ -493,20 +494,20 @@ function CSAR:New(Coalition, Template, Alias) self.CSARVoice = MSRS.Voices.Google.Standard.en_US_Standard_A self.CSARVoiceMS = MSRS.Voices.Microsoft.Hedda self.coordinate = nil -- Core.Point#COORDINATE - + local AliaS = string.gsub(self.alias," ","_") self.filename = string.format("CSAR_%s_Persist.csv",AliaS) - + -- load and save downed pilots self.enableLoadSave = false self.filepath = nil self.saveinterval = 600 - + ------------------------ --- Pseudo Functions --- ------------------------ - - --- Triggers the FSM event "Start". Starts the CSAR. Initializes parameters and starts event handlers. + + --- Triggers the FSM event "Start". Starts the CSAR. Initializes parameters and starts event handlers. -- @function [parent=#CSAR] Start -- @param #CSAR self @@ -531,7 +532,7 @@ function CSAR:New(Coalition, Template, Alias) -- @function [parent=#CSAR] __Status -- @param #CSAR self -- @param #number delay Delay in seconds. - -- + -- -- --- Triggers the FSM event "Load". -- @function [parent=#CSAR] Load -- @param #CSAR self @@ -540,7 +541,7 @@ function CSAR:New(Coalition, Template, Alias) -- @function [parent=#CSAR] __Load -- @param #CSAR self -- @param #number delay Delay in seconds. - + --- Triggers the FSM event "Save". -- @function [parent=#CSAR] Load -- @param #CSAR self @@ -549,7 +550,7 @@ function CSAR:New(Coalition, Template, Alias) -- @function [parent=#CSAR] __Save -- @param #CSAR self -- @param #number delay Delay in seconds. - + --- On After "PilotDown" event. Downed Pilot detected. -- @function [parent=#CSAR] OnAfterPilotDown -- @param #CSAR self @@ -561,7 +562,7 @@ function CSAR:New(Coalition, Template, Alias) -- @param #string Leadername Name of the #UNIT of the downed pilot. -- @param #string CoordinatesText String of the position of the pilot. Format determined by self.coordtype. -- @param #string Playername Player name if any given. Might be nil! - + --- On After "Aproach" event. Heli close to downed Pilot. -- @function [parent=#CSAR] OnAfterApproach -- @param #CSAR self @@ -570,8 +571,8 @@ function CSAR:New(Coalition, Template, Alias) -- @param #string To To state. -- @param #string Heliname Name of the helicopter group. -- @param #string Woundedgroupname Name of the downed pilot\'s group. - - --- On After "Landed" event. Heli landed at an airbase. + + --- On After "Landed" event. Heli landed at an airbase. -- @function [parent=#CSAR] OnAfterLanded -- @param #CSAR self -- @param #string From From state. @@ -579,8 +580,8 @@ function CSAR:New(Coalition, Template, Alias) -- @param #string To To state. -- @param #string HeliName Name of the #UNIT which has landed. -- @param Wrapper.Airbase#AIRBASE Airbase Airbase where the heli landed. - - --- On After "Boarded" event. Downed pilot boarded heli. + + --- On After "Boarded" event. Downed pilot boarded heli. -- @function [parent=#CSAR] OnAfterBoarded -- @param #CSAR self -- @param #string From From state. @@ -590,7 +591,7 @@ function CSAR:New(Coalition, Template, Alias) -- @param #string Woundedgroupname Name of the downed pilot\'s group. -- @param #string Description Descriptive name of the group. - --- On After "Returning" event. Heli can return home with downed pilot(s). + --- On After "Returning" event. Heli can return home with downed pilot(s). -- @function [parent=#CSAR] OnAfterReturning -- @param #CSAR self -- @param #string From From state. @@ -598,8 +599,8 @@ function CSAR:New(Coalition, Template, Alias) -- @param #string To To state. -- @param #string Heliname Name of the helicopter group. -- @param #string Woundedgroupname Name of the downed pilot\'s group. - - --- On After "Rescued" event. Pilot(s) have been brought to the MASH/FARP/AFB. + + --- On After "Rescued" event. Pilot(s) have been brought to the MASH/FARP/AFB. -- @function [parent=#CSAR] OnAfterRescued -- @param #CSAR self -- @param #string From From state. @@ -608,7 +609,7 @@ function CSAR:New(Coalition, Template, Alias) -- @param Wrapper.Unit#UNIT HeliUnit Unit of the helicopter. -- @param #string HeliName Name of the helicopter group. -- @param #number PilotsSaved Number of the saved pilots on board when landing. - + --- On After "KIA" event. Pilot is dead. -- @function [parent=#CSAR] OnAfterKIA -- @param #CSAR self @@ -616,7 +617,7 @@ function CSAR:New(Coalition, Template, Alias) -- @param #string Event Event. -- @param #string To To state. -- @param #string Pilotname Name of the pilot KIA. - + --- FSM Function OnAfterLoad. -- @function [parent=#CSAR] OnAfterLoad -- @param #CSAR self @@ -625,7 +626,7 @@ function CSAR:New(Coalition, Template, Alias) -- @param #string To To state. -- @param #string path (Optional) Path where the file is located. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized. -- @param #string filename (Optional) File name for loading. Default is "CSAR__Persist.csv". - + --- FSM Function OnAfterSave. -- @function [parent=#CSAR] OnAfterSave -- @param #CSAR self @@ -634,7 +635,7 @@ function CSAR:New(Coalition, Template, Alias) -- @param #string To To state. -- @param #string path (Optional) Path where the file is saved. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized. -- @param #string filename (Optional) File name for saving. Default is "CSAR__Persist.csv". - + return self end @@ -656,7 +657,7 @@ end -- @return #CSAR self. function CSAR:_CreateDownedPilotTrack(Group,Groupname,Side,OriginalUnit,Description,Typename,Frequency,Playername,Wetfeet,BeaconName) self:T({"_CreateDownedPilotTrack",Groupname,Side,OriginalUnit,Description,Typename,Frequency,Playername}) - + -- create new entry local DownedPilot = {} -- #CSAR.DownedPilot DownedPilot.desc = Description or "" @@ -672,7 +673,7 @@ function CSAR:_CreateDownedPilotTrack(Group,Groupname,Side,OriginalUnit,Descript DownedPilot.alive = true DownedPilot.wetfeet = Wetfeet or false DownedPilot.BeaconName = BeaconName - + -- Add Pilot local PilotTable = self.downedPilots local counter = self.downedpilotcounter @@ -688,14 +689,14 @@ end --- (Internal) Count pilots on board. -- @param #CSAR self -- @param #string _heliName --- @return #number count +-- @return #number count function CSAR:_PilotsOnboard(_heliName) self:T(self.lid .. " _PilotsOnboard") - local count = 0 + local count = 0 if self.inTransitGroups[_heliName] then - for _, _group in pairs(self.inTransitGroups[_heliName]) do - count = count + 1 - end + for _, _group in pairs(self.inTransitGroups[_heliName]) do + count = count + 1 + end end return count end @@ -705,15 +706,15 @@ end -- @param #string _unitname Name of unit. -- @return #boolean Outcome function CSAR:_DoubleEjection(_unitname) - if self.lastCrash[_unitname] then - local _time = self.lastCrash[_unitname] - if timer.getTime() - _time < 10 then - self:E(self.lid.."Caught double ejection!") - return true - end + if self.lastCrash[_unitname] then + local _time = self.lastCrash[_unitname] + if timer.getTime() - _time < 10 then + self:E(self.lid.."Caught double ejection!") + return true end - self.lastCrash[_unitname] = timer.getTime() - return false + end + self.lastCrash[_unitname] = timer.getTime() + return false end --- (User) Add a PLAYERTASK - FSM events will check success @@ -744,8 +745,8 @@ function CSAR:_SpawnPilotInField(country,point,frequency,wetfeet) for i=1,10 do math.random(i,10000) end - if point:IsSurfaceTypeWater() or wetfeet then - point.y = 0 + if point:IsSurfaceTypeWater() or wetfeet then + point.y = 0 end local template = self.template if self.usewetfeet and wetfeet then @@ -770,29 +771,29 @@ end function CSAR:_AddSpecialOptions(group) self:T(self.lid.." _AddSpecialOptions") self:T({group}) - + local immortalcrew = self.immortalcrew local invisiblecrew = self.invisiblecrew if immortalcrew then local _setImmortal = { - id = 'SetImmortal', - params = { - value = true - } + id = 'SetImmortal', + params = { + value = true + } } group:SetCommand(_setImmortal) end if invisiblecrew then local _setInvisible = { - id = 'SetInvisible', - params = { - value = true - } + id = 'SetInvisible', + params = { + value = true + } } - group:SetCommand(_setInvisible) + group:SetCommand(_setInvisible) end - + group:OptionAlarmStateGreen() group:OptionROEHoldFire() return self @@ -807,7 +808,7 @@ end -- @param #string _unitName Unitname -- @param #string _playerName Playername -- @param #number _freq Frequency --- @param #boolean noMessage +-- @param #boolean noMessage -- @param #string _description Description -- @param #boolean forcedesc Use the description only for the pilot track entry -- @return Wrapper.Group#GROUP PilotInField Pilot GROUP object @@ -818,31 +819,31 @@ function CSAR:_AddCsar(_coalition , _country, _point, _typeName, _unitName, _pla local template = self.template local wetfeet = false - + local surface = _point:GetSurfaceType() if surface == land.SurfaceType.WATER then wetfeet = true end - + if not _freq then _freq = self:_GenerateADFFrequency() if not _freq then _freq = 333000 end --noob catch - end - + end + local _spawnedGroup, _alias = self:_SpawnPilotInField(_country,_point,_freq,wetfeet) - + local _typeName = _typeName or "Pilot" - + if not noMessage then if _freq ~= 0 then --shagrat different CASEVAC msg - self:_DisplayToAllSAR("MAYDAY MAYDAY! " .. _typeName .. " is down. ", self.coalition, self.messageTime) - else - self:_DisplayToAllSAR("Troops In Contact. " .. _typeName .. " requests CASEVAC. ", self.coalition, self.messageTime) + self:_DisplayToAllSAR("MAYDAY MAYDAY! " .. _typeName .. " is down. ", self.coalition, self.messageTime) + else + self:_DisplayToAllSAR("Troops In Contact. " .. _typeName .. " requests CASEVAC. ", self.coalition, self.messageTime) + end end - end - + local BeaconName - + if _playerName then BeaconName = _playerName..math.random(1,10000) elseif _unitName then @@ -850,37 +851,37 @@ function CSAR:_AddCsar(_coalition , _country, _point, _typeName, _unitName, _pla else BeaconName = "Ghost-1-1"..math.random(1,10000) end - - if (_freq and _freq ~= 0) then --shagrat only add beacon if _freq is NOT 0 + + if (_freq and _freq ~= 0) then --shagrat only add beacon if _freq is NOT 0 self:_AddBeaconToGroup(_spawnedGroup, _freq, BeaconName) end - + self:_AddSpecialOptions(_spawnedGroup) local _text = _description if not forcedesc then if _playerName ~= nil then - if _freq ~= 0 then --shagrat - _text = "Pilot " .. _playerName - else - _text = "TIC - " .. _playerName - end + if _freq ~= 0 then --shagrat + _text = "Pilot " .. _playerName + else + _text = "TIC - " .. _playerName + end elseif _unitName ~= nil then - if _freq ~= 0 then --shagrat - _text = "AI Pilot of " .. _unitName - else - _text = "TIC - " .. _unitName + if _freq ~= 0 then --shagrat + _text = "AI Pilot of " .. _unitName + else + _text = "TIC - " .. _unitName + end end end - end self:T({_spawnedGroup, _alias}) - + local _GroupName = _spawnedGroup:GetName() or _alias self:_CreateDownedPilotTrack(_spawnedGroup,_GroupName,_coalition,_unitName,_text,_typeName,_freq,_playerName,wetfeet,BeaconName) self:_InitSARForPilot(_spawnedGroup, _unitName, _freq, noMessage, _playerName) --shagrat use unitName to have the aircraft callsign / descriptive "name" etc. - + return _spawnedGroup, _alias end @@ -897,7 +898,7 @@ end function CSAR:_SpawnCsarAtZone( _zone, _coalition, _description, _randomPoint, _nomessage, unitname, typename, forcedesc) self:T(self.lid .. " _SpawnCsarAtZone") local freq = self:_GenerateADFFrequency() - + local _triggerZone = nil if type(_zone) == "string" then _triggerZone = ZONE:New(_zone) -- trigger to use as reference position @@ -906,16 +907,16 @@ function CSAR:_SpawnCsarAtZone( _zone, _coalition, _description, _randomPoint, _ _triggerZone = _zone -- is already a zone end end - + if _triggerZone == nil then self:E(self.lid.."ERROR: Can\'t find zone called " .. _zone, 10) return end - + local _description = _description or "PoW" local unitname = unitname or "Old Rusty" local typename = typename or "Phantom II" - + local pos = {} if _randomPoint then local _pos = _triggerZone:GetRandomPointVec3() @@ -923,7 +924,7 @@ function CSAR:_SpawnCsarAtZone( _zone, _coalition, _description, _randomPoint, _ else pos = _triggerZone:GetCoordinate() end - + local _country = 0 if _coalition == coalition.side.BLUE then _country = self.countryblue @@ -932,9 +933,9 @@ function CSAR:_SpawnCsarAtZone( _zone, _coalition, _description, _randomPoint, _ else _country = self.countryneutral end - + self:_AddCsar(_coalition, _country, pos, typename, unitname, _description, freq, _nomessage, _description, forcedesc) - + return self end @@ -949,7 +950,7 @@ end -- @param #string Typename (optional) Type of plane. -- @param #boolean Forcedesc (optional) Force to use the **description passed only** for the pilot track entry. Use to have fully custom names. -- @usage If missions designers want to spawn downed pilots into the field, e.g. at mission begin, to give the helicopter guys work, they can do this like so: --- +-- -- -- Create downed "Pilot Wagner" in #ZONE "CSAR_Start_1" at a random point for the blue coalition -- my_csar:SpawnCSARAtZone( "CSAR_Start_1", coalition.side.BLUE, "Wagner", true, false, "Charly-1-1", "F5E" ) function CSAR:SpawnCSARAtZone(Zone, Coalition, Description, RandomPoint, Nomessage, Unitname, Typename, Forcedesc) @@ -968,14 +969,14 @@ end -- @param #boolean forcedesc (optional) Force to use the description passed only for the pilot track entry. Use to have fully custom names. function CSAR:_SpawnCASEVAC( _Point, _coalition, _description, _nomessage, unitname, typename, forcedesc) --shagrat added internal Function _SpawnCASEVAC self:T(self.lid .. " _SpawnCASEVAC") - + local _description = _description or "CASEVAC" local unitname = unitname or "CASEVAC" local typename = typename or "Ground Commander" - + local pos = {} pos = _Point - + local _country = 0 if _coalition == coalition.side.BLUE then _country = self.countryblue @@ -986,7 +987,7 @@ function CSAR:_SpawnCASEVAC( _Point, _coalition, _description, _nomessage, unitn end --shagrat set frequency to 0 as "flag" for no beacon self:_AddCsar(_coalition, _country, pos, typename, unitname, _description, 0, _nomessage, _description, forcedesc) - + return self end @@ -1000,10 +1001,10 @@ end -- @param #string Typename (optional) Type of plane. -- @param #boolean Forcedesc (optional) Force to use the **description passed only** for the pilot track entry. Use to have fully custom names. -- @usage If missions designers want to spawn downed pilots into the field, e.g. at mission begin, to give the helicopter guys work, they can do this like so: --- +-- -- -- Create casualty "CASEVAC" at coordinate Core.Point#COORDINATE for the blue coalition. -- my_csar:SpawnCASEVAC( coordinate, coalition.side.BLUE ) -function CSAR:SpawnCASEVAC(Point, Coalition, Description, Nomessage, Unitname, Typename, Forcedesc) +function CSAR:SpawnCASEVAC(Point, Coalition, Description, Nomessage, Unitname, Typename, Forcedesc) self:_SpawnCASEVAC(Point, Coalition, Description, Nomessage, Unitname, Typename, Forcedesc) return self end --shagrat end added CASEVAC @@ -1013,57 +1014,57 @@ end --shagrat end added CASEVAC function CSAR:_EventHandler(EventData) self:T(self.lid .. " _EventHandler") self:T({Event = EventData.id}) - + local _event = EventData -- Core.Event#EVENTDATA - - -- no Player + + -- no Player if self.enableForAI == false and _event.IniPlayerName == nil then - return self - end - - -- no event + return self + end + + -- no event if _event == nil or _event.initiator == nil then return self - - -- take off + + -- take off elseif _event.id == EVENTS.Takeoff then -- taken off self:T(self.lid .. " Event unit - Takeoff") - + local _coalition = _event.IniCoalition if _coalition ~= self.coalition then - return self --ignore! + return self --ignore! end - + if _event.IniGroupName then - self.takenOff[_event.IniUnitName] = true + self.takenOff[_event.IniUnitName] = true end - + return self - - -- player enter unit + + -- player enter unit elseif _event.id == EVENTS.PlayerEnterAircraft or _event.id == EVENTS.PlayerEnterUnit then --player entered unit self:T(self.lid .. " Event unit - Player Enter") - + local _coalition = _event.IniCoalition self:T("Coalition = "..UTILS.GetCoalitionName(_coalition)) if _coalition ~= self.coalition then - return self --ignore! + return self --ignore! end - + if _event.IniPlayerName then - self.takenOff[_event.IniPlayerName] = nil + self.takenOff[_event.IniPlayerName] = nil end - + -- jumped into flying plane? self:T("Taken Off: "..tostring(_event.IniUnit:InAir(true))) - + if _event.IniUnit:InAir(true) then self.takenOff[_event.IniPlayerName] = true end - + local _unit = _event.IniUnit local _group = _event.IniGroup - + local function IsBronco(Group) local grp = Group -- Wrapper.Group#GROUP local typename = grp:GetTypeName() @@ -1071,81 +1072,81 @@ function CSAR:_EventHandler(EventData) if typename == "Bronco-OV-10A" then return true end return false end - + if _unit:IsHelicopter() or _group:IsHelicopter() or IsBronco(_group) then self:_AddMedevacMenuItem() - end - - return self - - elseif (_event.id == EVENTS.PilotDead and self.csarOncrash == false) then - -- Pilot dead - - self:T(self.lid .. " Event unit - Pilot Dead") - - local _unit = _event.IniUnit - local _unitname = _event.IniUnitName - local _group = _event.IniGroup - - if _unit == nil then - return self -- error! - end - - local _coalition = _event.IniCoalition - if _coalition ~= self.coalition then - return self --ignore! - end - - -- Catch multiple events here? - if self.takenOff[_event.IniUnitName] == true or _group:IsAirborne() then - if self:_DoubleEjection(_unitname) then - return self - end + end - else - self:T(self.lid .. " Pilot has not taken off, ignore") - end - - return self - - elseif _event.id == EVENTS.PilotDead or _event.id == EVENTS.Ejection then - if _event.id == EVENTS.PilotDead and self.csarOncrash == false then - return self - end - self:T(self.lid .. " Event unit - Pilot Ejected") - - local _unit = _event.IniUnit - local _unitname = _event.IniUnitName - local _group = _event.IniGroup - - self:T({_unit.UnitName, _unitname, _group.GroupName}) - - if _unit == nil then - self:T("Unit NIL!") - return self -- error! - end - - --local _coalition = _unit:GetCoalition() -- nil now for some reason - local _coalition = _group:GetCoalition() - if _coalition ~= self.coalition then - self:T("Wrong coalition! Coalition = "..UTILS.GetCoalitionName(_coalition)) - return self --ignore! - end - - - self:T("Airborne: "..tostring(_group:IsAirborne())) - self:T("Taken Off: "..tostring(self.takenOff[_event.IniUnitName])) - - if not self.takenOff[_event.IniUnitName] and not _group:IsAirborne() then - self:T(self.lid .. " Pilot has not taken off, ignore") - -- return self -- give up, pilot hasnt taken off - end - + return self + + elseif (_event.id == EVENTS.PilotDead and self.csarOncrash == false) then + -- Pilot dead + + self:T(self.lid .. " Event unit - Pilot Dead") + + local _unit = _event.IniUnit + local _unitname = _event.IniUnitName + local _group = _event.IniGroup + + if _unit == nil then + return self -- error! + end + + local _coalition = _event.IniCoalition + if _coalition ~= self.coalition then + return self --ignore! + end + + -- Catch multiple events here? + if self.takenOff[_event.IniUnitName] == true or _group:IsAirborne() then if self:_DoubleEjection(_unitname) then - self:T("Double Ejection!") return self end + else + self:T(self.lid .. " Pilot has not taken off, ignore") + end + + return self + + elseif _event.id == EVENTS.PilotDead or _event.id == EVENTS.Ejection then + if _event.id == EVENTS.PilotDead and self.csarOncrash == false then + return self + end + self:T(self.lid .. " Event unit - Pilot Ejected") + + local _unit = _event.IniUnit + local _unitname = _event.IniUnitName + local _group = _event.IniGroup + + self:T({_unit.UnitName, _unitname, _group.GroupName}) + + if _unit == nil then + self:T("Unit NIL!") + return self -- error! + end + + --local _coalition = _unit:GetCoalition() -- nil now for some reason + local _coalition = _group:GetCoalition() + if _coalition ~= self.coalition then + self:T("Wrong coalition! Coalition = "..UTILS.GetCoalitionName(_coalition)) + return self --ignore! + end + + + self:T("Airborne: "..tostring(_group:IsAirborne())) + self:T("Taken Off: "..tostring(self.takenOff[_event.IniUnitName])) + + if not self.takenOff[_event.IniUnitName] and not _group:IsAirborne() then + self:T(self.lid .. " Pilot has not taken off, ignore") + -- return self -- give up, pilot hasnt taken off + end + + if self:_DoubleEjection(_unitname) then + self:T("Double Ejection!") + return self + end + local initdcscoord = nil local initcoord = nil @@ -1159,117 +1160,120 @@ function CSAR:_EventHandler(EventData) self:T({initdcscoord}) end - -- Remove downed pilot if already exists to replace with new one. - if _event.IniPlayerName then - local PilotTable = self.downedPilots --#CSAR.DownedPilot - local _foundPilot = nil - for _,_pilot in pairs(PilotTable) do - if _pilot.player == _event.IniPlayerName and _pilot.alive == true then - _foundPilot = _pilot - break - end - end - if _foundPilot then - self:T("Downed pilot already exists!") - _foundPilot.group:Destroy(false) - self:_RemoveNameFromDownedPilots(_foundPilot.name) - self:_CheckDownedPilotTable() - end + -- Remove downed pilot if already exists to replace with new one. + if _event.IniPlayerName then + local PilotTable = self.downedPilots --#CSAR.DownedPilot + local _foundPilot = nil + for _,_pilot in pairs(PilotTable) do + if _pilot.player == _event.IniPlayerName and _pilot.alive == true then + _foundPilot = _pilot + break + end end + if _foundPilot then + self:T("Downed pilot already exists!") + _foundPilot.group:Destroy(false) + self:_RemoveNameFromDownedPilots(_foundPilot.name) + self:_CheckDownedPilotTable() + end + end + + -- limit no of pilots in the field. + if self.limitmaxdownedpilots and self:_ReachedPilotLimit() then + self:T("Maxed Downed Pilot!") + return self + end + - -- limit no of pilots in the field. - if self.limitmaxdownedpilots and self:_ReachedPilotLimit() then - self:T("Maxed Downed Pilot!") - return self - end - - -- TODO: Over water check --- EVENTS.LandingAfterEjection NOT triggered by DCS, so handle csarUsePara = true case -- might create dual pilots in edge cases - + local wetfeet = false - + --local surface = _unit:GetCoordinate():GetSurfaceType() local surface = initcoord:GetSurfaceType() - + if surface == land.SurfaceType.WATER then self:T("Wet feet!") wetfeet = true - end - -- all checks passed, get going. + end + -- all checks passed, get going. if self.csarUsePara == false or (self.csarUsePara and wetfeet ) then --shagrat check parameter LandingAfterEjection, if true don't spawn a Pilot from EJECTION event, wait for the Chute to land local _freq = self:_GenerateADFFrequency() - self:_AddCsar(_coalition, _unit:GetCountry(), initcoord , _unit:GetTypeName(), _unit:GetName(), _event.IniPlayerName, _freq, false, "none") + self:_AddCsar(_coalition, _unit:GetCountry(), initcoord , _unit:GetTypeName(), _unit:GetName(), _event.IniPlayerName, _freq, self.suppressmessages, "none") return self end - + elseif _event.id == EVENTS.Land then - self:T(self.lid .. " Landing") - - if _event.IniUnitName then - self.takenOff[_event.IniUnitName] = nil + self:T(self.lid .. " Landing") + + if _event.IniUnitName then + self.takenOff[_event.IniUnitName] = nil + end + + if self.allowFARPRescue then + + local _unit = _event.IniUnit -- Wrapper.Unit#UNIT + + if _unit == nil then + self:T(self.lid .. " Unit nil on landing") + return self -- error! end - - if self.allowFARPRescue then - - local _unit = _event.IniUnit -- Wrapper.Unit#UNIT - - if _unit == nil then - self:T(self.lid .. " Unit nil on landing") - return self -- error! - end - - --local _coalition = _event.IniCoalition - local _coalition = _event.IniGroup:GetCoalition() - if _coalition ~= self.coalition then - self:T(self.lid .. " Wrong coalition") - return self --ignore! - end - - self.takenOff[_event.IniUnitName] = nil - - local _place = _event.Place -- Wrapper.Airbase#AIRBASE - - if _place == nil then - self:T(self.lid .. " Landing Place Nil") - return self -- error! - end - - -- anyone on board? - if self.inTransitGroups[_event.IniUnitName] == nil then - -- ignore - return self - end - - if _place:GetCoalition() == self.coalition or _place:GetCoalition() == coalition.side.NEUTRAL then - self:__Landed(2,_event.IniUnitName, _place) - self:_ScheduledSARFlight(_event.IniUnitName,_event.IniGroupName,true,true) - else - self:T(string.format("Airfield %d, Unit %d", _place:GetCoalition(), _unit:GetCoalition())) - end - end - - return self + + --local _coalition = _event.IniCoalition + local _coalition = _event.IniGroup:GetCoalition() + if _coalition ~= self.coalition then + self:T(self.lid .. " Wrong coalition") + return self --ignore! end - + + self.takenOff[_event.IniUnitName] = nil + + local _place = _event.Place -- Wrapper.Airbase#AIRBASE + + if _place == nil then + self:T(self.lid .. " Landing Place Nil") + return self -- error! + end + + -- anyone on board? + if self.inTransitGroups[_event.IniUnitName] == nil then + -- ignore + return self + end + + if _place:GetCoalition() == self.coalition or _place:GetCoalition() == coalition.side.NEUTRAL then + self:__Landed(2,_event.IniUnitName, _place) + local IsHeloBase = false + local ABName = _place:GetName() + if ABName and string.find(ABName,"^H") then IsHeloBase = true end -- if name starts with an H it's an (possibly elevated) helo base on current maps + self:_ScheduledSARFlight(_event.IniUnitName,_event.IniGroupName,true,true,IsHeloBase) + else + self:T(string.format("Airfield %d, Unit %d", _place:GetCoalition(), _unit:GetCoalition())) + end + end + + return self + end + ---- shagrat on event LANDING_AFTER_EJECTION spawn pilot at parachute location if (_event.id == EVENTS.LandingAfterEjection and self.csarUsePara == true) then self:T("LANDING_AFTER_EJECTION") local _LandingPos = COORDINATE:NewFromVec3(_event.initiator:getPosition().p) local _unitname = "Aircraft" --_event.initiator:getName() or "Aircraft" --shagrat Optional use of Object name which is unfortunately 'f15_Pilot_Parachute' - local _typename = "Ejected Pilot" --_event.Initiator.getTypeName() or "Ejected Pilot" + local _typename = "Ejected Pilot" --_event.Initiator.getTypeName() or "Ejected Pilot" local _country = _event.initiator:getCountry() local _coalition = coalition.getCountryCoalition( _country ) self:T("Country = ".._country.." Coalition = ".._coalition) if _coalition == self.coalition then local _freq = self:_GenerateADFFrequency() self:I({coalition=_coalition,country= _country, coord=_LandingPos, name=_unitname, player=_event.IniPlayerName, freq=_freq}) - self:_AddCsar(_coalition, _country, _LandingPos, nil, _unitname, _event.IniPlayerName, _freq, false, "none")--shagrat add CSAR at Parachute location. + self:_AddCsar(_coalition, _country, _LandingPos, nil, _unitname, _event.IniPlayerName, _freq, self.suppressmessages, "none")--shagrat add CSAR at Parachute location. Unit.destroy(_event.initiator) -- shagrat remove static Pilot model - end + end end - + return self end @@ -1287,38 +1291,38 @@ function CSAR:_InitSARForPilot(_downedGroup, _GroupName, _freq, _nomessage, _pla local _freqk = _freq / 1000 local _coordinatesText = self:_GetPositionOfWounded(_downedGroup) local _leadername = _leader:GetName() - + if not _nomessage then - if _freq ~= 0 then --shagrat - local _text = string.format("%s requests SAR at %s, beacon at %.2f KHz", _groupName, _coordinatesText, _freqk)--shagrat _groupName to prevent 'f15_Pilot_Parachute' - if self.coordtype ~= 2 then --not MGRS - self:_DisplayToAllSAR(_text,self.coalition,self.messageTime) - else - self:_DisplayToAllSAR(_text,self.coalition,self.messageTime,false,true) - local coordtext = UTILS.MGRSStringToSRSFriendly(_coordinatesText,true) - local _text = string.format("%s requests SAR at %s, beacon at %.2f kilo hertz", _groupName, coordtext, _freqk) - self:_DisplayToAllSAR(_text,self.coalition,self.messageTime,true,false) + if _freq ~= 0 then --shagrat + local _text = string.format("%s requests SAR at %s, beacon at %.2f KHz", _groupName, _coordinatesText, _freqk)--shagrat _groupName to prevent 'f15_Pilot_Parachute' + if self.coordtype ~= 2 then --not MGRS + self:_DisplayToAllSAR(_text,self.coalition,self.messageTime) + else + self:_DisplayToAllSAR(_text,self.coalition,self.messageTime,false,true) + local coordtext = UTILS.MGRSStringToSRSFriendly(_coordinatesText,true) + local _text = string.format("%s requests SAR at %s, beacon at %.2f kilo hertz", _groupName, coordtext, _freqk) + self:_DisplayToAllSAR(_text,self.coalition,self.messageTime,true,false) + end + else --shagrat CASEVAC msg + local _text = string.format("Pickup Zone at %s.", _coordinatesText ) + if self.coordtype ~= 2 then --not MGRS + self:_DisplayToAllSAR(_text,self.coalition,self.messageTime) + else + self:_DisplayToAllSAR(_text,self.coalition,self.messageTime,false,true) + local coordtext = UTILS.MGRSStringToSRSFriendly(_coordinatesText,true) + local _text = string.format("Pickup Zone at %s.", coordtext ) + self:_DisplayToAllSAR(_text,self.coalition,self.messageTime,true,false) + end end - else --shagrat CASEVAC msg - local _text = string.format("Pickup Zone at %s.", _coordinatesText ) - if self.coordtype ~= 2 then --not MGRS - self:_DisplayToAllSAR(_text,self.coalition,self.messageTime) - else - self:_DisplayToAllSAR(_text,self.coalition,self.messageTime,false,true) - local coordtext = UTILS.MGRSStringToSRSFriendly(_coordinatesText,true) - local _text = string.format("Pickup Zone at %s.", coordtext ) - self:_DisplayToAllSAR(_text,self.coalition,self.messageTime,true,false) - end - end end - + for _,_heliName in pairs(self.csarUnits) do self:_CheckWoundedGroupStatus(_heliName, _groupName) end - -- trigger FSM event + -- trigger FSM event self:__PilotDown(2,_downedGroup, _freqk, _groupName, _coordinatesText, _playername) - + return self end @@ -1336,7 +1340,7 @@ function CSAR:_CheckNameInDownedPilots(name) found = true table = _pilot break - end + end end return found, table end @@ -1366,13 +1370,13 @@ end -- @return #CSAR self function CSAR:SetCallSignOptions(ShortCallsign,Keepnumber,CallsignTranslations) if not ShortCallsign or ShortCallsign == false then - self.ShortCallsign = false + self.ShortCallsign = false else - self.ShortCallsign = true + self.ShortCallsign = true end self.Keepnumber = Keepnumber or false self.CallsignTranslations = CallsignTranslations - return self + return self end --- (Internal) Check if a name is in downed pilot table and remove it. @@ -1405,13 +1409,13 @@ function CSAR:_CheckWoundedGroupStatus(heliname,woundedgroupname) self:T("...not found in list!") return end - + local _woundedGroup = _downedpilot.group - if _woundedGroup ~= nil and _woundedGroup:IsAlive() then + if _woundedGroup ~= nil and _woundedGroup:IsAlive() then local _heliUnit = self:_GetSARHeli(_heliName) -- Wrapper.Unit#UNIT - + local _lookupKeyHeli = _heliName .. "_" .. _woundedGroupName --lookup key for message state tracking - + if _heliUnit == nil then self.heliVisibleMessage[_lookupKeyHeli] = nil self.heliCloseMessage[_lookupKeyHeli] = nil @@ -1419,15 +1423,15 @@ function CSAR:_CheckWoundedGroupStatus(heliname,woundedgroupname) self:T("...heliunit nil!") return end - + local _heliCoord = _heliUnit:GetCoordinate() local _leaderCoord = _woundedGroup:GetCoordinate() local _distance = self:_GetDistance(_heliCoord,_leaderCoord) -- autosmoke if (self.autosmoke == true) and (_distance < self.autosmokedistance) and (_distance ~= -1) then - self:_PopSmokeForGroup(_woundedGroupName, _woundedGroup) + self:_PopSmokeForGroup(_woundedGroupName, _woundedGroup) end - + if _distance < self.approachdist_near and _distance > 0 then if self:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedGroup, _woundedGroupName) == true then -- we\'re close, reschedule @@ -1437,21 +1441,21 @@ function CSAR:_CheckWoundedGroupStatus(heliname,woundedgroupname) elseif _distance >= self.approachdist_near and _distance < self.approachdist_far then -- message once if self.heliVisibleMessage[_lookupKeyHeli] == nil then - local _pilotName = _downedpilot.desc - if self.autosmoke == true then - local dist = self.autosmokedistance / 1000 - local disttext = string.format("%.0fkm",dist) - if _SETTINGS:IsImperial() then - local dist = UTILS.MetersToNM(self.autosmokedistance) - disttext = string.format("%.0fnm",dist) - end - self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. I hear you! Finally, that is music in my ears!\nI'll pop a smoke when you are %s away.\nLand or hover by the smoke.", self:_GetCustomCallSign(_heliName), _pilotName, disttext), self.messageTime,false,true) - else - self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. I hear you! Finally, that is music in my ears!\nRequest a flare or smoke if you need.", self:_GetCustomCallSign(_heliName), _pilotName), self.messageTime,false,true) + local _pilotName = _downedpilot.desc + if self.autosmoke == true then + local dist = self.autosmokedistance / 1000 + local disttext = string.format("%.0fkm",dist) + if _SETTINGS:IsImperial() then + local dist = UTILS.MetersToNM(self.autosmokedistance) + disttext = string.format("%.0fnm",dist) end - --mark as shown for THIS heli and THIS group - self.heliVisibleMessage[_lookupKeyHeli] = true - end + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. I hear you! Finally, that is music in my ears!\nI'll pop a smoke when you are %s away.\nLand or hover by the smoke.", self:_GetCustomCallSign(_heliName), _pilotName, disttext), self.messageTime,false,true) + else + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. I hear you! Finally, that is music in my ears!\nRequest a flare or smoke if you need.", self:_GetCustomCallSign(_heliName), _pilotName), self.messageTime,false,true) + end + --mark as shown for THIS heli and THIS group + self.heliVisibleMessage[_lookupKeyHeli] = true + end self.heliCloseMessage[_lookupKeyHeli] = nil self.landedStatus[_lookupKeyHeli] = nil --reschedule as units aren\'t dead yet , schedule for a bit slower though as we\'re far away @@ -1459,11 +1463,11 @@ function CSAR:_CheckWoundedGroupStatus(heliname,woundedgroupname) self:__Approach(-10,heliname,woundedgroupname) end else - self:T("...Downed Pilot KIA?!") - if not _downedpilot.alive then - --self:__KIA(1,_downedpilot.name) - self:_RemoveNameFromDownedPilots(_downedpilot.name, true) - end + self:T("...Downed Pilot KIA?!") + if not _downedpilot.alive then + --self:__KIA(1,_downedpilot.name) + self:_RemoveNameFromDownedPilots(_downedpilot.name, true) + end end return self end @@ -1477,11 +1481,11 @@ function CSAR:_PopSmokeForGroup(_woundedGroupName, _woundedLeader) -- have we popped smoke already in the last 5 mins local _lastSmoke = self.smokeMarkers[_woundedGroupName] if _lastSmoke == nil or timer.getTime() > _lastSmoke then - - local _smokecolor = self.smokecolor - local _smokecoord = _woundedLeader:GetCoordinate():Translate( 6, math.random( 1, 360) ) --shagrat place smoke at a random 6 m distance, so smoke does not obscure the pilot - _smokecoord:Smoke(_smokecolor) - self.smokeMarkers[_woundedGroupName] = timer.getTime() + 300 -- next smoke time + + local _smokecolor = self.smokecolor + local _smokecoord = _woundedLeader:GetCoordinate():Translate( 6, math.random( 1, 360) ) --shagrat place smoke at a random 6 m distance, so smoke does not obscure the pilot + _smokecoord:Smoke(_smokecolor) + self.smokeMarkers[_woundedGroupName] = timer.getTime() + 300 -- next smoke time end return self end @@ -1498,43 +1502,43 @@ function CSAR:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupNam local _heliName = _heliUnit:GetName() local _groups = self.inTransitGroups[_heliName] local _unitsInHelicopter = self:_PilotsOnboard(_heliName) - + -- init table if there is none for this helicopter if not _groups then - self.inTransitGroups[_heliName] = {} - _groups = self.inTransitGroups[_heliName] + self.inTransitGroups[_heliName] = {} + _groups = self.inTransitGroups[_heliName] end - + -- if the heli can\'t pick them up, show a message and return local _maxUnits = self.AircraftType[_heliUnit:GetTypeName()] if _maxUnits == nil then _maxUnits = self.max_units end if _unitsInHelicopter + 1 > _maxUnits then - self:_DisplayMessageToSAR(_heliUnit, string.format("%s, %s. We\'re already crammed with %d guys! Sorry!", _pilotName, self:_GetCustomCallSign(_heliName), _unitsInHelicopter, _unitsInHelicopter), self.messageTime,false,false,true) - return self + self:_DisplayMessageToSAR(_heliUnit, string.format("%s, %s. We\'re already crammed with %d guys! Sorry!", _pilotName, self:_GetCustomCallSign(_heliName), _unitsInHelicopter, _unitsInHelicopter), self.messageTime,false,false,true) + return self end - + local found,downedgrouptable = self:_CheckNameInDownedPilots(_woundedGroupName) local grouptable = downedgrouptable --#CSAR.DownedPilot self.inTransitGroups[_heliName][_woundedGroupName] = - { + { originalUnit = grouptable.originalUnit, woundedGroup = _woundedGroupName, side = self.coalition, desc = grouptable.desc, player = grouptable.player, - } + } _woundedGroup:Destroy(false) self:_RemoveNameFromDownedPilots(_woundedGroupName,true) - + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s I\'m in! Get to the MASH ASAP! ", self:_GetCustomCallSign(_heliName), _pilotName), self.messageTime,true,true) - + self:_UpdateUnitCargoMass(_heliName) - + self:__Boarded(5,_heliName,_woundedGroupName,grouptable.desc) - + return self end @@ -1588,136 +1592,136 @@ function CSAR:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedG local _woundedLeader = _woundedGroup local _lookupKeyHeli = _heliUnit:GetName() .. "_" .. _woundedGroupName --lookup key for message state tracking - + local _found, _pilotable = self:_CheckNameInDownedPilots(_woundedGroupName) -- #boolean, #CSAR.DownedPilot local _pilotName = _pilotable.desc - + local _reset = true - + if (_distance < 500) then - self:T(self.lid .. "[Pickup Debug] Helo closer than 500m: ".._lookupKeyHeli) - if self.heliCloseMessage[_lookupKeyHeli] == nil then - if self.autosmoke == true then - self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. You\'re close now! Land or hover at the smoke.", self:_GetCustomCallSign(_heliName), _pilotName), self.messageTime,false,true) - else - self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. You\'re close now! Land in a safe place, I will go there ", self:_GetCustomCallSign(_heliName), _pilotName), self.messageTime,false,true) - end - self.heliCloseMessage[_lookupKeyHeli] = true + self:T(self.lid .. "[Pickup Debug] Helo closer than 500m: ".._lookupKeyHeli) + if self.heliCloseMessage[_lookupKeyHeli] == nil then + if self.autosmoke == true then + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. You\'re close now! Land or hover at the smoke.", self:_GetCustomCallSign(_heliName), _pilotName), self.messageTime,false,true) + else + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. You\'re close now! Land in a safe place, I will go there ", self:_GetCustomCallSign(_heliName), _pilotName), self.messageTime,false,true) end - self:T(self.lid .. "[Pickup Debug] Checking landed vs Hover for ".._lookupKeyHeli) - -- have we landed close enough? - if not _heliUnit:InAir() then - self:T(self.lid .. "[Pickup Debug] Helo landed: ".._lookupKeyHeli) - if self.pilotRuntoExtractPoint == true then - if (_distance < self.extractDistance) then - local _time = self.landedStatus[_lookupKeyHeli] - self:T(self.lid .. "[Pickup Debug] Check pilot running or arrived ".._lookupKeyHeli) - if _time == nil then - self:T(self.lid .. "[Pickup Debug] Pilot running not arrived yet ".._lookupKeyHeli) - self.landedStatus[_lookupKeyHeli] = math.floor( (_distance - self.loadDistance) / 3.6 ) - _time = self.landedStatus[_lookupKeyHeli] - _woundedGroup:OptionAlarmStateGreen() - self:_OrderGroupToMoveToPoint(_woundedGroup, _heliUnit:GetCoordinate()) - self:_DisplayMessageToSAR(_heliUnit, "Wait till " .. _pilotName .. " gets in. \nETA " .. _time .. " more seconds.", self.messageTime, false) - else - _time = self.landedStatus[_lookupKeyHeli] - 10 - self.landedStatus[_lookupKeyHeli] = _time - end - --if _time <= 0 or _distance < self.loadDistance then - self:T(self.lid .. "[Pickup Debug] Pilot close enough? ".._lookupKeyHeli) - if _distance < self.loadDistance + 5 or _distance <= 13 then - self:T(self.lid .. "[Pickup Debug] Pilot close enough - YES ".._lookupKeyHeli) - if self.pilotmustopendoors and (self:_IsLoadingDoorOpen(_heliName) == false) then - self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true, true) - self:T(self.lid .. "[Pickup Debug] Door closed, try again next loop ".._lookupKeyHeli) - return false - else - self:T(self.lid .. "[Pickup Debug] Pick up Pilot ".._lookupKeyHeli) - self.landedStatus[_lookupKeyHeli] = nil - self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) - return true - end - end + self.heliCloseMessage[_lookupKeyHeli] = true + end + self:T(self.lid .. "[Pickup Debug] Checking landed vs Hover for ".._lookupKeyHeli) + -- have we landed close enough? + if not _heliUnit:InAir() then + self:T(self.lid .. "[Pickup Debug] Helo landed: ".._lookupKeyHeli) + if self.pilotRuntoExtractPoint == true then + if (_distance < self.extractDistance) then + local _time = self.landedStatus[_lookupKeyHeli] + self:T(self.lid .. "[Pickup Debug] Check pilot running or arrived ".._lookupKeyHeli) + if _time == nil then + self:T(self.lid .. "[Pickup Debug] Pilot running not arrived yet ".._lookupKeyHeli) + self.landedStatus[_lookupKeyHeli] = math.floor( (_distance - self.loadDistance) / 3.6 ) + _time = self.landedStatus[_lookupKeyHeli] + _woundedGroup:OptionAlarmStateGreen() + self:_OrderGroupToMoveToPoint(_woundedGroup, _heliUnit:GetCoordinate()) + self:_DisplayMessageToSAR(_heliUnit, "Wait till " .. _pilotName .. " gets in. \nETA " .. _time .. " more seconds.", self.messageTime, false) + else + _time = self.landedStatus[_lookupKeyHeli] - 10 + self.landedStatus[_lookupKeyHeli] = _time + end + --if _time <= 0 or _distance < self.loadDistance then + self:T(self.lid .. "[Pickup Debug] Pilot close enough? ".._lookupKeyHeli) + if _distance < self.loadDistance + 5 or _distance <= 13 then + self:T(self.lid .. "[Pickup Debug] Pilot close enough - YES ".._lookupKeyHeli) + if self.pilotmustopendoors and (self:_IsLoadingDoorOpen(_heliName) == false) then + self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true, true) + self:T(self.lid .. "[Pickup Debug] Door closed, try again next loop ".._lookupKeyHeli) + return false + else + self:T(self.lid .. "[Pickup Debug] Pick up Pilot ".._lookupKeyHeli) + self.landedStatus[_lookupKeyHeli] = nil + self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) + return true end - else - self:T(self.lid .. "[Pickup Debug] Helo landed, pilot NOT set to run to helo ".._lookupKeyHeli) - if (_distance < self.loadDistance) then - self:T(self.lid .. "[Pickup Debug] Helo close enough, door check ".._lookupKeyHeli) - if self.pilotmustopendoors and (self:_IsLoadingDoorOpen(_heliName) == false) then - self:T(self.lid .. "[Pickup Debug] Door closed, try again next loop ".._lookupKeyHeli) - self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true, true) - return false - else - self:T(self.lid .. "[Pickup Debug] Pick up Pilot ".._lookupKeyHeli) - self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) - return true - end end end else - self:T(self.lid .. "[Pickup Debug] Helo hovering".._lookupKeyHeli) - local _unitsInHelicopter = self:_PilotsOnboard(_heliName) - local _maxUnits = self.AircraftType[_heliUnit:GetTypeName()] - if _maxUnits == nil then - _maxUnits = self.max_units - end - self:T(self.lid .. "[Pickup Debug] Check capacity and close enough for winching ".._lookupKeyHeli) - if _heliUnit:InAir() and _unitsInHelicopter + 1 <= _maxUnits then - -- DONE - make variable - if _distance < self.rescuehoverdistance then - self:T(self.lid .. "[Pickup Debug] Helo hovering close enough ".._lookupKeyHeli) - --check height! - local leaderheight = _woundedLeader:GetHeight() - if leaderheight < 0 then leaderheight = 0 end - local _height = _heliUnit:GetHeight() - leaderheight - - -- DONE - make variable - if _height <= self.rescuehoverheight then - self:T(self.lid .. "[Pickup Debug] Helo hovering low enough ".._lookupKeyHeli) - local _time = self.hoverStatus[_lookupKeyHeli] - - if _time == nil then - self.hoverStatus[_lookupKeyHeli] = 10 - _time = 10 - else - _time = self.hoverStatus[_lookupKeyHeli] - 10 - self.hoverStatus[_lookupKeyHeli] = _time - end - self:T(self.lid .. "[Pickup Debug] Check hover timer ".._lookupKeyHeli) - if _time > 0 then - self:T(self.lid .. "[Pickup Debug] Helo hovering not long enough ".._lookupKeyHeli) - self:_DisplayMessageToSAR(_heliUnit, "Hovering above " .. _pilotName .. ". \n\nHold hover for " .. _time .. " seconds to winch them up. \n\nIf the countdown stops you\'re too far away!", self.messageTime, true) - else - self:T(self.lid .. "[Pickup Debug] Helo hovering long enough - door check ".._lookupKeyHeli) - if self.pilotmustopendoors and (self:_IsLoadingDoorOpen(_heliName) == false) then - self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true, true) - self:T(self.lid .. "[Pickup Debug] Door closed, try again next loop ".._lookupKeyHeli) - return false - else - self.hoverStatus[_lookupKeyHeli] = nil - self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) - self:T(self.lid .. "[Pickup Debug] Pilot picked up ".._lookupKeyHeli) - return true - end - end - _reset = false - else - self:T(self.lid .. "[Pickup Debug] Helo hovering too high ".._lookupKeyHeli) - self:_DisplayMessageToSAR(_heliUnit, "Too high to winch " .. _pilotName .. " \nReduce height and hover for 10 seconds!", self.messageTime, true,true) - self:T(self.lid .. "[Pickup Debug] Hovering too high, try again next loop ".._lookupKeyHeli) - return false - end - end - + self:T(self.lid .. "[Pickup Debug] Helo landed, pilot NOT set to run to helo ".._lookupKeyHeli) + if (_distance < self.loadDistance) then + self:T(self.lid .. "[Pickup Debug] Helo close enough, door check ".._lookupKeyHeli) + if self.pilotmustopendoors and (self:_IsLoadingDoorOpen(_heliName) == false) then + self:T(self.lid .. "[Pickup Debug] Door closed, try again next loop ".._lookupKeyHeli) + self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true, true) + return false + else + self:T(self.lid .. "[Pickup Debug] Pick up Pilot ".._lookupKeyHeli) + self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) + return true end + end end + else + self:T(self.lid .. "[Pickup Debug] Helo hovering".._lookupKeyHeli) + local _unitsInHelicopter = self:_PilotsOnboard(_heliName) + local _maxUnits = self.AircraftType[_heliUnit:GetTypeName()] + if _maxUnits == nil then + _maxUnits = self.max_units + end + self:T(self.lid .. "[Pickup Debug] Check capacity and close enough for winching ".._lookupKeyHeli) + if _heliUnit:InAir() and _unitsInHelicopter + 1 <= _maxUnits then + -- DONE - make variable + if _distance < self.rescuehoverdistance then + self:T(self.lid .. "[Pickup Debug] Helo hovering close enough ".._lookupKeyHeli) + --check height! + local leaderheight = _woundedLeader:GetHeight() + if leaderheight < 0 then leaderheight = 0 end + local _height = _heliUnit:GetHeight() - leaderheight + + -- DONE - make variable + if _height <= self.rescuehoverheight then + self:T(self.lid .. "[Pickup Debug] Helo hovering low enough ".._lookupKeyHeli) + local _time = self.hoverStatus[_lookupKeyHeli] + + if _time == nil then + self.hoverStatus[_lookupKeyHeli] = 10 + _time = 10 + else + _time = self.hoverStatus[_lookupKeyHeli] - 10 + self.hoverStatus[_lookupKeyHeli] = _time + end + self:T(self.lid .. "[Pickup Debug] Check hover timer ".._lookupKeyHeli) + if _time > 0 then + self:T(self.lid .. "[Pickup Debug] Helo hovering not long enough ".._lookupKeyHeli) + self:_DisplayMessageToSAR(_heliUnit, "Hovering above " .. _pilotName .. ". \n\nHold hover for " .. _time .. " seconds to winch them up. \n\nIf the countdown stops you\'re too far away!", self.messageTime, true) + else + self:T(self.lid .. "[Pickup Debug] Helo hovering long enough - door check ".._lookupKeyHeli) + if self.pilotmustopendoors and (self:_IsLoadingDoorOpen(_heliName) == false) then + self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true, true) + self:T(self.lid .. "[Pickup Debug] Door closed, try again next loop ".._lookupKeyHeli) + return false + else + self.hoverStatus[_lookupKeyHeli] = nil + self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) + self:T(self.lid .. "[Pickup Debug] Pilot picked up ".._lookupKeyHeli) + return true + end + end + _reset = false + else + self:T(self.lid .. "[Pickup Debug] Helo hovering too high ".._lookupKeyHeli) + self:_DisplayMessageToSAR(_heliUnit, "Too high to winch " .. _pilotName .. " \nReduce height and hover for 10 seconds!", self.messageTime, true,true) + self:T(self.lid .. "[Pickup Debug] Hovering too high, try again next loop ".._lookupKeyHeli) + return false + end + end + + end + end end - + if _reset then - self.hoverStatus[_lookupKeyHeli] = nil + self.hoverStatus[_lookupKeyHeli] = nil end - + if _distance < 500 then return true else @@ -1730,22 +1734,23 @@ end -- @param #string heliname Heli name -- @param #string groupname Group name -- @param #boolean isairport If true, EVENT.Landing took place at an airport or FARP --- @param #boolean noreschedule If true, do not try to reschedule this is distances are not ok (coming from landing event) -function CSAR:_ScheduledSARFlight(heliname,groupname, isairport, noreschedule) +-- @param #boolean noreschedule If true, do not try to reschedule this if distances are not ok (coming from landing event) +-- @param #boolean IsHeloBase If true, landing took place at a Helo Base (name "H ..." on current maps) +function CSAR:_ScheduledSARFlight(heliname,groupname, isairport, noreschedule, IsHeloBase) self:T(self.lid .. " _ScheduledSARFlight") self:T({heliname,groupname}) local _heliUnit = self:_GetSARHeli(heliname) local _woundedGroupName = groupname if (_heliUnit == nil) then - --helicopter crashed? - self.inTransitGroups[heliname] = nil - return + --helicopter crashed? + self.inTransitGroups[heliname] = nil + return end if self.inTransitGroups[heliname] == nil or self.inTransitGroups[heliname][_woundedGroupName] == nil then - -- Groups already rescued - return + -- Groups already rescued + return end local _dist = self:_GetClosestMASH(_heliUnit) @@ -1754,10 +1759,10 @@ function CSAR:_ScheduledSARFlight(heliname,groupname, isairport, noreschedule) self:T(self.lid.."[Drop off debug] Check distance to MASH for "..heliname.." Distance can not be determined!") return end - + self:T(self.lid.."[Drop off debug] Check distance to MASH for "..heliname.." Distance km: "..math.floor(_dist/1000)) - - if ( _dist < self.FARPRescueDistance or isairport ) and _heliUnit:InAir() == false then + + if ( _dist < self.FARPRescueDistance or isairport ) and ((_heliUnit:InAir() == false) or (IsHeloBase == true)) then self:T(self.lid.."[Drop off debug] Distance ok, door check") if self.pilotmustopendoors and self:_IsLoadingDoorOpen(heliname) == false then self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me out!", self.messageTime, true, true) @@ -1771,8 +1776,8 @@ function CSAR:_ScheduledSARFlight(heliname,groupname, isairport, noreschedule) --queue up if not noreschedule then - self:__Returning(5,heliname,_woundedGroupName, isairport) - self:ScheduleOnce(5,self._ScheduledSARFlight,self,heliname,groupname, isairport, noreschedule) + self:__Returning(5,heliname,_woundedGroupName, isairport) + self:ScheduleOnce(5,self._ScheduledSARFlight,self,heliname,groupname, isairport, noreschedule, IsHeloBase) end return self end @@ -1784,22 +1789,22 @@ function CSAR:_RescuePilots(_heliUnit) self:T(self.lid .. " _RescuePilots") local _heliName = _heliUnit:GetName() local _rescuedGroups = self.inTransitGroups[_heliName] - + if _rescuedGroups == nil then - -- Groups already rescued - return + -- Groups already rescued + return end local PilotsSaved = self:_PilotsOnboard(_heliName) - + self.inTransitGroups[_heliName] = nil - + local _txt = string.format("%s: The %d pilot(s) have been taken to the\nmedical clinic. Good job!", self:_GetCustomCallSign(_heliName), PilotsSaved) - + self:_DisplayMessageToSAR(_heliUnit, _txt, self.messageTime) - + self:_UpdateUnitCargoMass(_heliName) - + -- trigger event self:__Rescued(-1,_heliUnit,_heliName, PilotsSaved) return self @@ -1861,9 +1866,9 @@ function CSAR:_GetPositionOfWounded(_woundedGroup,_Unit) if self.coordtype == 0 then -- Lat/Long DMTM _coordinatesText = _coordinate:ToStringLLDDM() elseif self.coordtype == 1 then -- Lat/Long DMS - _coordinatesText = _coordinate:ToStringLLDMS() + _coordinatesText = _coordinate:ToStringLLDMS() elseif self.coordtype == 2 then -- MGRS - _coordinatesText = _coordinate:ToStringMGRS() + _coordinatesText = _coordinate:ToStringMGRS() else -- Bullseye Metric --(medevac.coordtype == 4 or 3) _coordinatesText = _coordinate:ToStringBULLS(self.coalition) end @@ -1896,15 +1901,15 @@ end -- @param #string _unitName Unit to display to function CSAR:_DisplayActiveSAR(_unitName) self:T(self.lid .. " _DisplayActiveSAR") - local _msg = "Active MEDEVAC/SAR:" + local _msg = "Active MEDEVAC/SAR:" local _heli = self:_GetSARHeli(_unitName) -- Wrapper.Unit#UNIT if _heli == nil then - return + return end - + local _heliSide = self.coalition local _csarList = {} - + local _DownedPilotTable = self.downedPilots self:T({Table=_DownedPilotTable}) for _, _value in pairs(_DownedPilotTable) do @@ -1912,40 +1917,40 @@ function CSAR:_DisplayActiveSAR(_unitName) self:T(string.format("Display Active Pilot: %s", tostring(_groupName))) self:T({Table=_value}) local _woundedGroup = _value.group - if _woundedGroup and _value.alive then - local _coordinatesText = self:_GetPositionOfWounded(_woundedGroup,_heli) - local _helicoord = _heli:GetCoordinate() - local _woundcoord = _woundedGroup:GetCoordinate() - local _distance = self:_GetDistance(_helicoord, _woundcoord) - self:T({_distance = _distance}) - local distancetext = "" - local settings = _SETTINGS - if _heli:GetPlayerName() then - settings = _DATABASE:GetPlayerSettings(_heli:GetPlayerName()) or _SETTINGS - end - if settings:IsImperial() then - distancetext = string.format("%.1fnm",UTILS.MetersToNM(_distance)) - else - distancetext = string.format("%.1fkm", _distance/1000.0) - end - if _value.frequency == 0 or self.CreateRadioBeacons == false then--shagrat insert CASEVAC without Frequency - table.insert(_csarList, { dist = _distance, msg = string.format("%s at %s - %s ", _value.desc, _coordinatesText, distancetext) }) - else - table.insert(_csarList, { dist = _distance, msg = string.format("%s at %s - %.2f KHz ADF - %s ", _value.desc, _coordinatesText, _value.frequency / 1000, distancetext) }) - end + if _woundedGroup and _value.alive then + local _coordinatesText = self:_GetPositionOfWounded(_woundedGroup,_heli) + local _helicoord = _heli:GetCoordinate() + local _woundcoord = _woundedGroup:GetCoordinate() + local _distance = self:_GetDistance(_helicoord, _woundcoord) + self:T({_distance = _distance}) + local distancetext = "" + local settings = _SETTINGS + if _heli:GetPlayerName() then + settings = _DATABASE:GetPlayerSettings(_heli:GetPlayerName()) or _SETTINGS + end + if settings:IsImperial() then + distancetext = string.format("%.1fnm",UTILS.MetersToNM(_distance)) + else + distancetext = string.format("%.1fkm", _distance/1000.0) + end + if _value.frequency == 0 or self.CreateRadioBeacons == false then--shagrat insert CASEVAC without Frequency + table.insert(_csarList, { dist = _distance, msg = string.format("%s at %s - %s ", _value.desc, _coordinatesText, distancetext) }) + else + table.insert(_csarList, { dist = _distance, msg = string.format("%s at %s - %.2f KHz ADF - %s ", _value.desc, _coordinatesText, _value.frequency / 1000, distancetext) }) + end end end - + local function sortDistance(a, b) - return a.dist < b.dist + return a.dist < b.dist end - + table.sort(_csarList, sortDistance) - + for _, _line in pairs(_csarList) do - _msg = _msg .. "\n" .. _line.msg + _msg = _msg .. "\n" .. _line.msg end - + self:_DisplayMessageToSAR(_heli, _msg, self.messageTime*2, false, false, true) return self end @@ -1962,30 +1967,30 @@ function CSAR:_GetClosestDownedPilot(_heli) local _distance = 0 local _closestGroupInfo = nil local _heliCoord = _heli:GetCoordinate() or _heli:GetCoordinate() - - if _heliCoord == nil then - self:E("****Error obtaining coordinate!") - return nil - end - - local DownedPilotsTable = self.downedPilots - - for _, _groupInfo in UTILS.spairs(DownedPilotsTable) do - --for _, _groupInfo in pairs(DownedPilotsTable) do - local _woundedName = _groupInfo.name - local _tempWounded = _groupInfo.group - - -- check group exists and not moving to someone else - if _tempWounded then - local _tempCoord = _tempWounded:GetCoordinate() - _distance = self:_GetDistance(_heliCoord, _tempCoord) - if _distance ~= nil and (_shortestDistance == -1 or _distance < _shortestDistance) then - _shortestDistance = _distance - _closestGroup = _tempWounded - _closestGroupInfo = _groupInfo - end + if _heliCoord == nil then + self:E("****Error obtaining coordinate!") + return nil + end + + local DownedPilotsTable = self.downedPilots + + for _, _groupInfo in UTILS.spairs(DownedPilotsTable) do + --for _, _groupInfo in pairs(DownedPilotsTable) do + local _woundedName = _groupInfo.name + local _tempWounded = _groupInfo.group + + -- check group exists and not moving to someone else + if _tempWounded then + local _tempCoord = _tempWounded:GetCoordinate() + _distance = self:_GetDistance(_heliCoord, _tempCoord) + + if _distance ~= nil and (_shortestDistance == -1 or _distance < _shortestDistance) then + _shortestDistance = _distance + _closestGroup = _tempWounded + _closestGroupInfo = _groupInfo end + end end return { pilot = _closestGroup, distance = _shortestDistance, groupInfo = _closestGroupInfo } @@ -1998,35 +2003,35 @@ function CSAR:_SignalFlare(_unitName) self:T(self.lid .. " _SignalFlare") local _heli = self:_GetSARHeli(_unitName) if _heli == nil then - return + return end - + local _closest = self:_GetClosestDownedPilot(_heli) local smokedist = 8000 if self.approachdist_far > smokedist then smokedist = self.approachdist_far end if _closest ~= nil and _closest.pilot ~= nil and _closest.distance > 0 and _closest.distance < smokedist then - - local _clockDir = self:_GetClockDirection(_heli, _closest.pilot) - local _distance = "" - if _SETTINGS:IsImperial() then - _distance = string.format("%.1fnm",UTILS.MetersToNM(_closest.distance)) - else - _distance = string.format("%.1fkm",_closest.distance/1000) - end - local _msg = string.format("%s - Firing signal flare at your %s o\'clock. Distance %s", self:_GetCustomCallSign(_unitName), _clockDir, _distance) - self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true, true) - - local _coord = _closest.pilot:GetCoordinate() - _coord:FlareRed(_clockDir) + + local _clockDir = self:_GetClockDirection(_heli, _closest.pilot) + local _distance = "" + if _SETTINGS:IsImperial() then + _distance = string.format("%.1fnm",UTILS.MetersToNM(_closest.distance)) + else + _distance = string.format("%.1fkm",_closest.distance/1000) + end + local _msg = string.format("%s - Firing signal flare at your %s o\'clock. Distance %s", self:_GetCustomCallSign(_unitName), _clockDir, _distance) + self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true, true) + + local _coord = _closest.pilot:GetCoordinate() + _coord:FlareRed(_clockDir) else - local _distance = smokedist - local dtext = "" - if _SETTINGS:IsImperial() then - dtext = string.format("%.1fnm",UTILS.MetersToNM(smokedist)) - else - dtext = string.format("%.1fkm",smokedist/1000) - end - self:_DisplayMessageToSAR(_heli, string.format("No Pilots within %s",dtext), self.messageTime, false, false, true) + local _distance = smokedist + local dtext = "" + if _SETTINGS:IsImperial() then + dtext = string.format("%.1fnm",UTILS.MetersToNM(smokedist)) + else + dtext = string.format("%.1fkm",smokedist/1000) + end + self:_DisplayMessageToSAR(_heli, string.format("No Pilots within %s",dtext), self.messageTime, false, false, true) end return self end @@ -2034,7 +2039,7 @@ end --- (Internal) Display info to all SAR groups. -- @param #CSAR self -- @param #string _message Message to display. --- @param #number _side Coalition of message. +-- @param #number _side Coalition of message. -- @param #number _messagetime How long to show. -- @param #boolean ToSRS If true or nil, send to SRS TTS -- @param #boolean ToScreen If true or nil, send to Screen @@ -2054,7 +2059,7 @@ function CSAR:_DisplayToAllSAR(_message, _side, _messagetime,ToSRS,ToScreen) for _, _unitName in pairs(self.csarUnits) do local _unit = self:_GetSARHeli(_unitName) if _unit and not self.suppressmessages then - self:_DisplayMessageToSAR(_unit, _message, _messagetime) + self:_DisplayMessageToSAR(_unit, _message, _messagetime) end end end @@ -2068,30 +2073,30 @@ function CSAR:_ReqIRStrobe( _unitName ) self:T(self.lid .. " _ReqIRStrobe") local _heli = self:_GetSARHeli(_unitName) if _heli == nil then - return + return end local smokedist = 8000 if smokedist < self.approachdist_far then smokedist = self.approachdist_far end local _closest = self:_GetClosestDownedPilot(_heli) if _closest ~= nil and _closest.pilot ~= nil and _closest.distance > 0 and _closest.distance < smokedist then - local _clockDir = self:_GetClockDirection(_heli, _closest.pilot) - local _distance = string.format("%.1fkm",_closest.distance/1000) - if _SETTINGS:IsImperial() then - _distance = string.format("%.1fnm",UTILS.MetersToNM(_closest.distance)) - else - _distance = string.format("%.1fkm",_closest.distance/1000) - end - local _msg = string.format("%s - IR Strobe active at your %s o\'clock. Distance %s", self:_GetCustomCallSign(_unitName), _clockDir, _distance) - self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true, true) - _closest.pilot:NewIRMarker(true,self.IRStrobeRuntime or 300) + local _clockDir = self:_GetClockDirection(_heli, _closest.pilot) + local _distance = string.format("%.1fkm",_closest.distance/1000) + if _SETTINGS:IsImperial() then + _distance = string.format("%.1fnm",UTILS.MetersToNM(_closest.distance)) + else + _distance = string.format("%.1fkm",_closest.distance/1000) + end + local _msg = string.format("%s - IR Strobe active at your %s o\'clock. Distance %s", self:_GetCustomCallSign(_unitName), _clockDir, _distance) + self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true, true) + _closest.pilot:NewIRMarker(true,self.IRStrobeRuntime or 300) else - local _distance = string.format("%.1fkm",smokedist/1000) - if _SETTINGS:IsImperial() then - _distance = string.format("%.1fnm",UTILS.MetersToNM(smokedist)) - else - _distance = string.format("%.1fkm",smokedist/1000) - end - self:_DisplayMessageToSAR(_heli, string.format("No Pilots within %s",_distance), self.messageTime, false, false, true) + local _distance = string.format("%.1fkm",smokedist/1000) + if _SETTINGS:IsImperial() then + _distance = string.format("%.1fnm",UTILS.MetersToNM(smokedist)) + else + _distance = string.format("%.1fkm",smokedist/1000) + end + self:_DisplayMessageToSAR(_heli, string.format("No Pilots within %s",_distance), self.messageTime, false, false, true) end return self end @@ -2103,32 +2108,32 @@ function CSAR:_Reqsmoke( _unitName ) self:T(self.lid .. " _Reqsmoke") local _heli = self:_GetSARHeli(_unitName) if _heli == nil then - return + return end local smokedist = 8000 if smokedist < self.approachdist_far then smokedist = self.approachdist_far end local _closest = self:_GetClosestDownedPilot(_heli) if _closest ~= nil and _closest.pilot ~= nil and _closest.distance > 0 and _closest.distance < smokedist then - local _clockDir = self:_GetClockDirection(_heli, _closest.pilot) - local _distance = string.format("%.1fkm",_closest.distance/1000) - if _SETTINGS:IsImperial() then - _distance = string.format("%.1fnm",UTILS.MetersToNM(_closest.distance)) - else - _distance = string.format("%.1fkm",_closest.distance/1000) - end - local _msg = string.format("%s - Popping smoke at your %s o\'clock. Distance %s", self:_GetCustomCallSign(_unitName), _clockDir, _distance) - self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true, true) - local _coord = _closest.pilot:GetCoordinate() - local color = self.smokecolor - _coord:Smoke(color) + local _clockDir = self:_GetClockDirection(_heli, _closest.pilot) + local _distance = string.format("%.1fkm",_closest.distance/1000) + if _SETTINGS:IsImperial() then + _distance = string.format("%.1fnm",UTILS.MetersToNM(_closest.distance)) + else + _distance = string.format("%.1fkm",_closest.distance/1000) + end + local _msg = string.format("%s - Popping smoke at your %s o\'clock. Distance %s", self:_GetCustomCallSign(_unitName), _clockDir, _distance) + self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true, true) + local _coord = _closest.pilot:GetCoordinate() + local color = self.smokecolor + _coord:Smoke(color) else - local _distance = string.format("%.1fkm",smokedist/1000) - if _SETTINGS:IsImperial() then - _distance = string.format("%.1fnm",UTILS.MetersToNM(smokedist)) - else - _distance = string.format("%.1fkm",smokedist/1000) - end - self:_DisplayMessageToSAR(_heli, string.format("No Pilots within %s",_distance), self.messageTime, false, false, true) + local _distance = string.format("%.1fkm",smokedist/1000) + if _SETTINGS:IsImperial() then + _distance = string.format("%.1fnm",UTILS.MetersToNM(smokedist)) + else + _distance = string.format("%.1fkm",smokedist/1000) + end + self:_DisplayMessageToSAR(_heli, string.format("No Pilots within %s",_distance), self.messageTime, false, false, true) end return self end @@ -2150,36 +2155,36 @@ function CSAR:_GetClosestMASH(_heli) local _distance = 0 local _helicoord = _heli:GetCoordinate() local MashName = nil - + if self.allowFARPRescue then local position = _heli:GetCoordinate() local afb,distance = position:GetClosestAirbase(nil,self.coalition) _shortestDistance = distance MashName = (afb ~= nil) and afb:GetName() or "Unknown" end - + for _,_mashes in pairs(MashSets) do for _, _mashUnit in pairs(_mashes or {}) do - local _mashcoord - if _mashUnit and (not _mashUnit:IsInstanceOf("ZONE_BASE")) and _mashUnit:IsAlive() then - _mashcoord = _mashUnit:GetCoordinate() - elseif _mashUnit and _mashUnit:IsInstanceOf("ZONE_BASE") then - _mashcoord = _mashUnit:GetCoordinate() - end - _distance = self:_GetDistance(_helicoord, _mashcoord) - if _distance ~= nil and (_shortestDistance == -1 or _distance < _shortestDistance) then - _shortestDistance = _distance - MashName = _mashUnit:GetName() or "Unknown" - end + local _mashcoord + if _mashUnit and (not _mashUnit:IsInstanceOf("ZONE_BASE")) and _mashUnit:IsAlive() then + _mashcoord = _mashUnit:GetCoordinate() + elseif _mashUnit and _mashUnit:IsInstanceOf("ZONE_BASE") then + _mashcoord = _mashUnit:GetCoordinate() + end + _distance = self:_GetDistance(_helicoord, _mashcoord) + if _distance ~= nil and (_shortestDistance == -1 or _distance < _shortestDistance) then + _shortestDistance = _distance + MashName = _mashUnit:GetName() or "Unknown" + end end end - + if _shortestDistance ~= -1 then - return _shortestDistance, MashName + return _shortestDistance, MashName else - return -1 + return -1 end - + end --- (Internal) Display onboarded rescued pilots. @@ -2187,47 +2192,47 @@ end -- @param #string _unitName Name of the chopper function CSAR:_CheckOnboard(_unitName) self:T(self.lid .. " _CheckOnboard") - local _unit = self:_GetSARHeli(_unitName) - if _unit == nil then - return + local _unit = self:_GetSARHeli(_unitName) + if _unit == nil then + return + end + --list onboard pilots + local _inTransit = self.inTransitGroups[_unitName] + if _inTransit == nil then + self:_DisplayMessageToSAR(_unit, "No Rescued Pilots onboard", self.messageTime, false, false, true) + else + local _text = "Onboard - RTB to FARP/Airfield or MASH: " + for _, _onboard in pairs(self.inTransitGroups[_unitName]) do + _text = _text .. "\n" .. _onboard.desc end - --list onboard pilots - local _inTransit = self.inTransitGroups[_unitName] - if _inTransit == nil then - self:_DisplayMessageToSAR(_unit, "No Rescued Pilots onboard", self.messageTime, false, false, true) - else - local _text = "Onboard - RTB to FARP/Airfield or MASH: " - for _, _onboard in pairs(self.inTransitGroups[_unitName]) do - _text = _text .. "\n" .. _onboard.desc - end - self:_DisplayMessageToSAR(_unit, _text, self.messageTime*2, false, false, true) - end - return self + self:_DisplayMessageToSAR(_unit, _text, self.messageTime*2, false, false, true) + end + return self end --- (Internal) Populate F10 menu for CSAR players. -- @param #CSAR self function CSAR:_AddMedevacMenuItem() self:T(self.lid .. " _AddMedevacMenuItem") - + local coalition = self.coalition local allheligroupset = self.allheligroupset -- Core.Set#SET_GROUP local _allHeliGroups = allheligroupset:GetSetObjects() -- rebuild units table local _UnitList = {} - for _key, _group in pairs (_allHeliGroups) do + for _key, _group in pairs (_allHeliGroups) do local _unit = _group:GetFirstUnitAlive() -- Asume that there is only one unit in the flight for players if _unit then - --self:T("Unitname ".._unit:GetName().." IsAlive "..tostring(_unit:IsAlive()).." IsPlayer "..tostring(_unit:IsPlayer())) - if _unit:IsAlive() and _unit:IsPlayer() then + --self:T("Unitname ".._unit:GetName().." IsAlive "..tostring(_unit:IsAlive()).." IsPlayer "..tostring(_unit:IsPlayer())) + if _unit:IsAlive() and _unit:IsPlayer() then local unitName = _unit:GetName() - _UnitList[unitName] = unitName + _UnitList[unitName] = unitName end -- end isAlive end -- end if _unit end -- end for self.csarUnits = _UnitList - - -- build unit menus + + -- build unit menus for _, _unitName in pairs(self.csarUnits) do local _unit = self:_GetSARHeli(_unitName) -- Wrapper.Unit#UNIT if _unit then @@ -2250,7 +2255,7 @@ function CSAR:_AddMedevacMenuItem() end end end - end + end return self end @@ -2284,7 +2289,7 @@ end -- @param #CSAR self function CSAR:_GenerateVHFrequencies() self:T(self.lid .. " _GenerateVHFrequencies") - + local FreeVHFFrequencies = {} FreeVHFFrequencies = UTILS.GenerateVHFrequencies() self.FreeVHFFrequencies = FreeVHFFrequencies @@ -2298,8 +2303,8 @@ function CSAR:_GenerateADFFrequency() self:T(self.lid .. " _GenerateADFFrequency") -- get a free freq for a beacon if #self.FreeVHFFrequencies <= 3 then - self.FreeVHFFrequencies = self.UsedVHFFrequencies - self.UsedVHFFrequencies = {} + self.FreeVHFFrequencies = self.UsedVHFFrequencies + self.UsedVHFFrequencies = {} end local _vhf = table.remove(self.FreeVHFFrequencies, math.random(#self.FreeVHFFrequencies)) return _vhf @@ -2312,7 +2317,7 @@ end -- @return #number direction function CSAR:_GetClockDirection(_heli, _group) self:T(self.lid .. " _GetClockDirection") - + local _playerPosition = _heli:GetCoordinate() -- get position of helicopter local _targetpostions = _group:GetCoordinate() -- get position of downed pilot local _heading = _heli:GetHeading() -- heading @@ -2324,12 +2329,12 @@ function CSAR:_GetClockDirection(_heli, _group) if _heading and Angle then clock = 12 --if angle == 0 then angle = 360 end - clock = _heading-Angle + clock = _heading-Angle hours = (clock/30)*-1 clock = 12+hours clock = UTILS.Round(clock,0) if clock > 12 then clock = clock-12 end - end + end return clock end @@ -2340,62 +2345,62 @@ end -- @param #string BeaconName Beacon Name to use -- @return #CSAR self function CSAR:_AddBeaconToGroup(_group, _freq, BeaconName) - self:T(self.lid .. " _AddBeaconToGroup") - if self.CreateRadioBeacons == false then return end - local _group = _group - - if _group == nil then - --return frequency to pool of available - for _i, _current in ipairs(self.UsedVHFFrequencies) do - if _current == _freq then - table.insert(self.FreeVHFFrequencies, _freq) - table.remove(self.UsedVHFFrequencies, _i) - end - end - return - end - - if _group:IsAlive() then - local _radioUnit = _group:GetUnit(1) - if _radioUnit then - local name = _radioUnit:GetName() - local Frequency = _freq -- Freq in Hertz - --local name = _radioUnit:GetName() - local Sound = "l10n/DEFAULT/"..self.radioSound - local vec3 = _radioUnit:GetVec3() or _radioUnit:GetPositionVec3() or {x=0,y=0,z=0} - self:I(self.lid..string.format("Added Radio Beacon %d Hertz | Name %s | Position {%d,%d,%d}",Frequency,BeaconName,vec3.x,vec3.y,vec3.z)) - trigger.action.radioTransmission(Sound, vec3, 0, true, Frequency, self.ADFRadioPwr or 500,BeaconName) -- Beacon in MP only runs for exactly 30secs straight + self:T(self.lid .. " _AddBeaconToGroup") + if self.CreateRadioBeacons == false then return end + local _group = _group + + if _group == nil then + --return frequency to pool of available + for _i, _current in ipairs(self.UsedVHFFrequencies) do + if _current == _freq then + table.insert(self.FreeVHFFrequencies, _freq) + table.remove(self.UsedVHFFrequencies, _i) end end - - return self + return + end + + if _group:IsAlive() then + local _radioUnit = _group:GetUnit(1) + if _radioUnit then + local name = _radioUnit:GetName() + local Frequency = _freq -- Freq in Hertz + --local name = _radioUnit:GetName() + local Sound = "l10n/DEFAULT/"..self.radioSound + local vec3 = _radioUnit:GetVec3() or _radioUnit:GetPositionVec3() or {x=0,y=0,z=0} + self:I(self.lid..string.format("Added Radio Beacon %d Hertz | Name %s | Position {%d,%d,%d}",Frequency,BeaconName,vec3.x,vec3.y,vec3.z)) + trigger.action.radioTransmission(Sound, vec3, 0, true, Frequency, self.ADFRadioPwr or 500,BeaconName) -- Beacon in MP only runs for exactly 30secs straight + end + end + + return self end --- (Internal) Helper function to (re-)add beacon to downed pilot. -- @param #CSAR self -- @return #CSAR self function CSAR:_RefreshRadioBeacons() - self:T(self.lid .. " _RefreshRadioBeacons") - if self.CreateRadioBeacons == false then return end - if self:_CountActiveDownedPilots() > 0 then - local PilotTable = self.downedPilots - for _,_pilot in pairs (PilotTable) do - self:T({_pilot.name}) - local pilot = _pilot -- #CSAR.DownedPilot - local group = pilot.group - local frequency = pilot.frequency or 0 -- thanks to @Thrud - local bname = pilot.BeaconName or pilot.name..math.random(1,100000) - --trigger.action.stopRadioTransmission(bname) - if group and group:IsAlive() and frequency > 0 then - --self:_AddBeaconToGroup(group,frequency,bname) - else - if frequency > 0 then - trigger.action.stopRadioTransmission(bname) - end + self:T(self.lid .. " _RefreshRadioBeacons") + if self.CreateRadioBeacons == false then return end + if self:_CountActiveDownedPilots() > 0 then + local PilotTable = self.downedPilots + for _,_pilot in pairs (PilotTable) do + self:T({_pilot.name}) + local pilot = _pilot -- #CSAR.DownedPilot + local group = pilot.group + local frequency = pilot.frequency or 0 -- thanks to @Thrud + local bname = pilot.BeaconName or pilot.name..math.random(1,100000) + --trigger.action.stopRadioTransmission(bname) + if group and group:IsAlive() and frequency > 0 then + --self:_AddBeaconToGroup(group,frequency,bname) + else + if frequency > 0 then + trigger.action.stopRadioTransmission(bname) end end end - return self + end + return self end --- (Internal) Helper function to count active downed pilots. @@ -2417,45 +2422,45 @@ end -- @param #CSAR self -- @return #boolean True or false. function CSAR:_ReachedPilotLimit() - self:T(self.lid .. " _ReachedPilotLimit") - local limit = self.maxdownedpilots - local islimited = self.limitmaxdownedpilots - local count = self:_CountActiveDownedPilots() - if islimited and (count >= limit) then - if self.useFIFOLimitReplacement then - local oldIndex = -1 - local oldDownedPilot = nil - for _index, _downedpilot in pairs(self.downedPilots) do - oldIndex = _index - oldDownedPilot = _downedpilot - break - end - if oldDownedPilot then - oldDownedPilot.group:Destroy(false) - oldDownedPilot.alive = false - self:_CheckDownedPilotTable() - return false - end - end - return true - else + self:T(self.lid .. " _ReachedPilotLimit") + local limit = self.maxdownedpilots + local islimited = self.limitmaxdownedpilots + local count = self:_CountActiveDownedPilots() + if islimited and (count >= limit) then + if self.useFIFOLimitReplacement then + local oldIndex = -1 + local oldDownedPilot = nil + for _index, _downedpilot in pairs(self.downedPilots) do + oldIndex = _index + oldDownedPilot = _downedpilot + break + end + if oldDownedPilot then + oldDownedPilot.group:Destroy(false) + oldDownedPilot.alive = false + self:_CheckDownedPilotTable() return false + end end + return true + else + return false + end end - --- User - Function to add onw SET_GROUP Set-up for pilot filtering and assignment. - -- Needs to be set before starting the CSAR instance. - -- @param #CSAR self - -- @param Core.Set#SET_GROUP Set The SET_GROUP object created by the mission designer/user to represent the CSAR pilot groups. - -- @return #CSAR self - function CSAR:SetOwnSetPilotGroups(Set) - self.UserSetGroup = Set - return self - end +--- User - Function to add onw SET_GROUP Set-up for pilot filtering and assignment. +-- Needs to be set before starting the CSAR instance. +-- @param #CSAR self +-- @param Core.Set#SET_GROUP Set The SET_GROUP object created by the mission designer/user to represent the CSAR pilot groups. +-- @return #CSAR self +function CSAR:SetOwnSetPilotGroups(Set) + self.UserSetGroup = Set + return self +end - ------------------------------ - --- FSM internal Functions --- - ------------------------------ +------------------------------ +--- FSM internal Functions --- +------------------------------ --- (Internal) Function called after Start() event. -- @param #CSAR self. @@ -2473,7 +2478,7 @@ function CSAR:onafterStart(From, Event, To) self:HandleEvent(EVENTS.PlayerEnterAircraft, self._EventHandler) self:HandleEvent(EVENTS.PlayerEnterUnit, self._EventHandler) self:HandleEvent(EVENTS.PilotDead, self._EventHandler) - + if self.UserSetGroup then self.allheligroupset = self.UserSetGroup elseif self.allowbronco then @@ -2485,12 +2490,12 @@ function CSAR:onafterStart(From, Event, To) else self.allheligroupset = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterCategoryHelicopter():FilterStart() end - + self.mash = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(self.mashprefix):FilterStart() - + self.staticmashes = SET_STATIC:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(self.mashprefix):FilterStart() self.zonemashes = SET_ZONE:New():FilterPrefixes(self.mashprefix):FilterStart() - + --[[ if staticmashes:Count() > 0 then for _,_mash in pairs(staticmashes.Set) do @@ -2506,10 +2511,10 @@ function CSAR:onafterStart(From, Event, To) self:T("Objects in SET: "..self.mash:Count()) end --]] - + if not self.coordinate then local csarhq = self.mash:GetRandom() - if csarhq then + if csarhq then self.coordinate = csarhq:GetCoordinate() end end @@ -2535,16 +2540,16 @@ function CSAR:onafterStart(From, Event, To) self.msrs:SetLabel("CSAR") self.SRSQueue = MSRSQUEUE:New("CSAR") -- Sound.SRS#MSRSQUEUE end - + self:__Status(-10) - + if self.enableLoadSave then local interval = self.saveinterval local filename = self.filename local filepath = self.filepath self:__Save(interval,filepath,filename) end - + return self end @@ -2553,11 +2558,11 @@ end function CSAR:_CheckDownedPilotTable() local pilots = self.downedPilots local npilots = {} - + for _ind,_entry in pairs(pilots) do local _group = _entry.group if _group:IsAlive() then - npilots[_ind] = _entry + npilots[_ind] = _entry else if _entry.alive then self:__KIA(1,_entry.desc) @@ -2577,12 +2582,12 @@ function CSAR:onbeforeStatus(From, Event, To) self:T({From, Event, To}) -- housekeeping self:_AddMedevacMenuItem() - + if not self.BeaconTimer or (self.BeaconTimer and not self.BeaconTimer:IsRunning()) then self.BeaconTimer = TIMER:New(self._RefreshRadioBeacons,self) self.BeaconTimer:Start(2,self.beaconRefresher) end - + self:_CheckDownedPilotTable() for _,_sar in pairs (self.csarUnits) do local PilotTable = self.downedPilots @@ -2593,7 +2598,7 @@ function CSAR:onbeforeStatus(From, Event, To) local timestamp = entry.timestamp or 0 local now = timer.getAbsTime() if now - timestamp > 17 then -- only check if we\'re not in approach mode, which is iterations of 5 and 10. - self:_CheckWoundedGroupStatus(_sar,name) + self:_CheckWoundedGroupStatus(_sar,name) end end end @@ -2615,14 +2620,14 @@ function CSAR:onafterStatus(From, Event, To) end local PilotsInFieldN = self:_CountActiveDownedPilots() - + local PilotsBoarded = 0 for _, _unitName in pairs(self.inTransitGroups) do for _,_units in pairs(_unitName) do PilotsBoarded = PilotsBoarded + 1 end end - + if self.verbose > 0 then local text = string.format("%s Active SAR: %d | Downed Pilots in field: %d (max %d) | Pilots boarded: %d | Landings: %d | Pilots rescued: %d", self.lid,NumberOfSARPilots,PilotsInFieldN,self.maxdownedpilots,PilotsBoarded,self.rescues,self.rescuedpilots) @@ -2692,7 +2697,7 @@ function CSAR:onbeforeBoarded(From, Event, To, Heliname, Woundedgroupname) -- right subtype? if Event == subtype and not task:IsDone() then local targetzone = task.Target:GetObject() -- Core.Zone#ZONE should be a zone in this case .... - if (targetzone and targetzone.ClassName and string.match(targetzone.ClassName,"ZONE") and targetzone:IsVec2InZone(dropvec2)) + if (targetzone and targetzone.ClassName and string.match(targetzone.ClassName,"ZONE") and targetzone:IsVec2InZone(dropvec2)) or (string.find(task.CSARPilotName,Woundedgroupname)) then if task.Clients:HasUniqueID(playername) then -- success @@ -2706,7 +2711,7 @@ function CSAR:onbeforeBoarded(From, Event, To, Heliname, Woundedgroupname) return self end ---- (Internal) Function called before Returning() event. +--- (Internal) Function called before Returning() event. -- @param #CSAR self. -- @param #string From From state. -- @param #string Event Event triggered. @@ -2741,10 +2746,10 @@ function CSAR:onbeforeRescued(From, Event, To, HeliUnit, HeliName, PilotsSaved) local subtype = task:GetSubType() -- right subtype? if Event == subtype and not task:IsDone() then - if task.Clients:HasUniqueID(playername) then - -- success - task:__Success(-1) - end + if task.Clients:HasUniqueID(playername) then + -- success + task:__Success(-1) + end end end ) @@ -2791,7 +2796,7 @@ function CSAR:onbeforeSave(From, Event, To, path, filename) if not self.enableLoadSave then return self end - -- Thanks to @FunkyFranky + -- Thanks to @FunkyFranky -- Check io module is available. if not io then self:E(self.lid.."ERROR: io not desanitized. Can't save current state.") @@ -2815,7 +2820,7 @@ end -- @param #string filename (Optional) File name for saving. Default is Default is "CSAR__Persist.csv". function CSAR:onafterSave(From, Event, To, path, filename) self:T({From, Event, To, path, filename}) - -- Thanks to @FunkyFranky + -- Thanks to @FunkyFranky if not self.enableLoadSave then return self end @@ -2830,7 +2835,7 @@ function CSAR:onafterSave(From, Event, To, path, filename) if lfs then path=self.filepath or lfs.writedir() end - + -- Set file name. filename=filename or self.filename @@ -2838,9 +2843,9 @@ function CSAR:onafterSave(From, Event, To, path, filename) if path~=nil then filename=path.."\\"..filename end - + local pilots = self.downedPilots - + --local data = "LoadedData = {\n" local data = "playerName,x,y,z,coalition,country,description,typeName,unitName,freq\n" local n = 0 @@ -2858,15 +2863,15 @@ function CSAR:onafterSave(From, Event, To, path, filename) local location = group:GetVec3() local unitName = DownedPilot.originalUnit local txt = string.format("%s,%d,%d,%d,%s,%s,%s,%s,%s,%d\n",playerName,location.x,location.y,location.z,coalition,country,description,typeName,unitName,freq) - + self:I(self.lid.."Saving to CSAR File: " .. txt) - + data = data .. txt end end - + _savefile(filename, data) - + -- AutoSave if self.enableLoadSave then local interval = self.saveinterval @@ -2891,19 +2896,19 @@ function CSAR:onbeforeLoad(From, Event, To, path, filename) end --- Function that check if a file exists. local function _fileexists(name) - local f=io.open(name,"r") - if f~=nil then + local f=io.open(name,"r") + if f~=nil then io.close(f) return true else return false end end - + -- Set file name and path filename=filename or self.filename path = path or self.filepath - + -- Check io module is available. if not io then self:E(self.lid.."WARNING: io not desanitized. Cannot load file.") @@ -2933,7 +2938,7 @@ function CSAR:onbeforeLoad(From, Event, To, path, filename) else self:E(self.lid..string.format("WARNING: State file %s might not exist.", filename)) return false - --return self + --return self end end @@ -2957,11 +2962,11 @@ function CSAR:onafterLoad(From, Event, To, path, filename) f:close() return data end - + -- Set file name and path filename=filename or self.filename path = path or self.filepath - + -- Set path or default. if lfs then path=path or lfs.writedir() @@ -2976,29 +2981,29 @@ function CSAR:onafterLoad(From, Event, To, path, filename) local text=string.format("Loading CSAR state from file %s", filename) MESSAGE:New(text,10):ToAllIf(self.Debug) self:I(self.lid..text) - + local file=assert(io.open(filename, "rb")) - + local loadeddata = {} for line in file:lines() do - loadeddata[#loadeddata+1] = line + loadeddata[#loadeddata+1] = line end file:close() - + -- remove header table.remove(loadeddata, 1) - + for _id,_entry in pairs (loadeddata) do local dataset = UTILS.Split(_entry,",") -- 1=playerName,2=x,3=y,4=z,5=coalition,6=country,7=description,8=typeName,9=unitName,10=freq\n local playerName = dataset[1] - + local vec3 = {} vec3.x = tonumber(dataset[2]) vec3.y = tonumber(dataset[3]) vec3.z = tonumber(dataset[4]) local point = COORDINATE:NewFromVec3(vec3) - + local coalition = tonumber(dataset[5]) local country = tonumber(dataset[6]) local description = dataset[7] @@ -3006,9 +3011,9 @@ function CSAR:onafterLoad(From, Event, To, path, filename) local unitName = dataset[9] local freq = tonumber(dataset[10]) - self:_AddCsar(coalition, country, point, typeName, unitName, playerName, freq, nil, description, nil) + self:_AddCsar(coalition, country, point, typeName, unitName, playerName, freq, false, description, nil) end - + return self end diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index 75d09b498..0c354709c 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -25,7 +25,7 @@ -- @module Ops.CTLD -- @image OPS_CTLD.jpg --- Last Update May 2025 +-- Last Update July 2025 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ @@ -867,7 +867,9 @@ do -- my_ctld.TroopUnloadDistHoverHook = 5 -- When hovering, unload troops this far behind the Chinook -- my_ctld.showstockinmenuitems = false -- When set to true, the menu lines will also show the remaining items in stock (that is, if you set any), downside is that the menu for all will be build every 30 seconds anew. -- my_ctld.onestepmenu = false -- When set to true, the menu will create Drop and build, Get and load, Pack and remove, Pack and load, Pack. it will be a 1 step solution. --- +-- my_ctld.VehicleMoveFormation = AI.Task.VehicleFormation.VEE -- When a group moves to a MOVE zone, then it takes this formation. Can be a table of formations, which are then randomly chosen. Defaults to "Vee". +-- my_ctld.validateAndRepositionUnits = false -- Uses Disposition and other logic to find better ground positions for ground units avoiding trees, water, roads, runways, map scenery, statics and other units in the area. (Default is false) +-- -- ## 2.1 CH-47 Chinook support -- -- The Chinook comes with the option to use the ground crew menu to load and unload cargo into the Helicopter itself for better immersion. As well, it can sling-load cargo from ground. The cargo you can actually **create** @@ -1294,6 +1296,7 @@ CTLD = { LoadedGroupsTable = {}, keeploadtable = true, allowCATransport = false, + VehicleMoveFormation = AI.Task.VehicleFormation.VEE, } ------------------------------ @@ -1393,6 +1396,7 @@ CTLD.UnitTypeCapabilities = { ["Hercules"] = {type="Hercules", crates=true, troops=true, cratelimit = 7, trooplimit = 64, length = 25, cargoweightlimit = 19000}, -- 19t cargo, 64 paratroopers. --Actually it's longer, but the center coord is off-center of the model. ["UH-60L"] = {type="UH-60L", crates=true, troops=true, cratelimit = 2, trooplimit = 20, length = 16, cargoweightlimit = 3500}, -- 4t cargo, 20 (unsec) seats + ["UH-60L_DAP"] = {type="UH-60L_DAP", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 16, cargoweightlimit = 500}, -- UH-60L DAP is an attack helo but can do limited CSAR and CTLD ["MH-60R"] = {type="MH-60R", crates=true, troops=true, cratelimit = 2, trooplimit = 20, length = 16, cargoweightlimit = 3500}, -- 4t cargo, 20 (unsec) seats ["SH-60B"] = {type="SH-60B", crates=true, troops=true, cratelimit = 2, trooplimit = 20, length = 16, cargoweightlimit = 3500}, -- 4t cargo, 20 (unsec) seats ["AH-64D_BLK_II"] = {type="AH-64D_BLK_II", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 17, cargoweightlimit = 200}, -- 2 ppl **outside** the helo @@ -1414,7 +1418,7 @@ CTLD.FixedWingTypes = { --- CTLD class version. -- @field #string version -CTLD.version="1.3.35" +CTLD.version="1.3.38" --- Instantiate a new CTLD. -- @param #CTLD self @@ -1481,6 +1485,7 @@ function CTLD:New(Coalition, Prefixes, Alias) self:AddTransition("*", "CratesRepaired", "*") -- CTLD repair event. self:AddTransition("*", "CratesBuildStarted", "*") -- CTLD build event. self:AddTransition("*", "CratesRepairStarted", "*") -- CTLD repair event. + self:AddTransition("*", "CratesPacked", "*") -- CTLD repack event. self:AddTransition("*", "HelicopterLost", "*") -- CTLD lost event. self:AddTransition("*", "Load", "*") -- CTLD load event. self:AddTransition("*", "Loaded", "*") -- CTLD load event. @@ -1553,13 +1558,17 @@ function CTLD:New(Coalition, Prefixes, Alias) self.movetroopsdistance = 5000 self.troopdropzoneradius = 100 + self.VehicleMoveFormation = AI.Task.VehicleFormation.VEE + -- added support Hercules Mod self.enableHercules = false -- deprecated self.enableFixedWing = false self.FixedMinAngels = 165 -- for troop/cargo drop via chute self.FixedMaxAngels = 2000 -- for troop/cargo drop via chute self.FixedMaxSpeed = 77 -- 280 kph or 150kn eq 77 mps - + + self.validateAndRepositionUnits = false -- 280 kph or 150kn eq 77 mps + -- message suppression self.suppressmessages = false @@ -1759,6 +1768,17 @@ function CTLD:New(Coalition, Prefixes, Alias) -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param Wrapper.Group#GROUP Vehicle The #GROUP object of the vehicle or FOB repaired. -- @return #CTLD self + + --- FSM Function OnBeforeCratesPacked. + -- @function [parent=#CTLD] OnBeforeCratesPacked + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param #CTLD_CARGO Cargo Cargo crate that was repacked. + -- @return #CTLD self --- FSM Function OnBeforeTroopsRTB. -- @function [parent=#CTLD] OnBeforeTroopsRTB @@ -1889,6 +1909,17 @@ function CTLD:New(Coalition, Prefixes, Alias) -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param Wrapper.Group#GROUP Vehicle The #GROUP object of the vehicle or FOB repaired. -- @return #CTLD self + + --- FSM Function OnAfterCratesPacked. + -- @function [parent=#CTLD] OnAfterCratesPacked + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param #CTLD_CARGO Cargo Cargo crate that was repacked. + -- @return #CTLD self --- FSM Function OnAfterTroopsRTB. -- @function [parent=#CTLD] OnAfterTroopsRTB @@ -3298,6 +3329,7 @@ function CTLD:_LoadCratesNearby(Group, Unit) self:_RefreshLoadCratesMenu(Group, Unit) -- clean up real world crates self:_CleanupTrackedCrates(crateidsloaded) + self:__CratesPickedUp(1, Group, Unit, loaded.Cargo) end end return self @@ -3708,6 +3740,7 @@ function CTLD:_UnloadTroops(Group, Unit) self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) :InitDelayOff() :InitSetUnitAbsolutePositions(Positions) + :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits) :OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end) :SpawnFromVec2(randomcoord:GetVec2()) self:__TroopsDeployed(1, Group, Unit, self.DroppedTroops[self.TroopCounter],type) @@ -4012,6 +4045,7 @@ function CTLD:_PackCratesNearby(Group, Unit) _Group:Destroy() -- if a match is found destroy the Wrapper.Group#GROUP near the player self:_GetCrates(Group, Unit, _entry, nil, false, true) -- spawn the appropriate crates near the player self:_RefreshLoadCratesMenu(Group,Unit) -- call the refresher to show the crates in the menu + self:__CratesPacked(1,Group,Unit,_entry) return true end end @@ -4153,11 +4187,13 @@ function CTLD:_BuildObjectFromCrates(Group,Unit,Build,Repair,RepairLocation,Mult self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) --:InitRandomizeUnits(true,20,2) :InitDelayOff() + :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits) :OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end) :SpawnFromVec2(randomcoord) else -- don't random position of e.g. SAM units build as FOB self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) :InitDelayOff() + :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits) :OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end) :SpawnFromVec2(randomcoord) end @@ -4173,6 +4209,17 @@ function CTLD:_BuildObjectFromCrates(Group,Unit,Build,Repair,RepairLocation,Mult return self end +--- (Internal) Function to get a vehicle formation for a moving group +-- @param #CTLD self +-- @return #string Formation +function CTLD:_GetVehicleFormation() + local VehicleMoveFormation = self.VehicleMoveFormation or AI.Task.VehicleFormation.VEE + if type(self.VehicleMoveFormation)=="table" then + VehicleMoveFormation = self.VehicleMoveFormation[math.random(1,#self.VehicleMoveFormation)] + end + return VehicleMoveFormation +end + --- (Internal) Function to move group to WP zone. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group The Group to move. @@ -4187,18 +4234,20 @@ function CTLD:_MoveGroupToZone(Group) -- yes, we can ;) local groupname = Group:GetName() local zonecoord = zone:GetRandomCoordinate(20,125) -- Core.Point#COORDINATE - local coordinate = zonecoord:GetVec2() + local formation = self:_GetVehicleFormation() + --local coordinate = zonecoord:GetVec2() Group:SetAIOn() Group:OptionAlarmStateAuto() Group:OptionDisperseOnAttack(30) - Group:OptionROEOpenFirePossible() - Group:RouteToVec2(coordinate,5) + Group:OptionROEOpenFire() + Group:RouteGroundTo(zonecoord,25,formation) end return self end --- (Internal) Housekeeping - Cleanup crates when build -- @param #CTLD self +-- -- @param #table Crates Table of #CTLD_CARGO objects near the unit. -- @param #CTLD.Buildable Build Table build object. -- @param #number Number Number of objects in Crates (found) to limit search. @@ -5170,6 +5219,7 @@ function CTLD:_UnloadSingleTroopByID(Group, Unit, chunkID) self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template, alias) :InitDelayOff() :InitSetUnitAbsolutePositions(Positions) + :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits) :OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end) :SpawnFromVec2(randomcoord:GetVec2()) self:__TroopsDeployed(1, Group, Unit, self.DroppedTroops[self.TroopCounter], cType) @@ -6882,6 +6932,7 @@ end local alias = string.format("%s-%d", _template, math.random(1,100000)) self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) :InitRandomizeUnits(randompositions,20,2) + :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits) :InitDelayOff() :OnSpawnGroup(function(grp,TimeStamp) grp.spawntime = TimeStamp or timer.getTime() end,TimeStamp) :SpawnFromVec2(randomcoord) @@ -7035,12 +7086,14 @@ end if canmove then self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) :InitRandomizeUnits(true,20,2) + :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits) :InitDelayOff() :OnSpawnGroup(function(grp,TimeStamp) grp.spawntime = TimeStamp or timer.getTime() end,TimeStamp) :SpawnFromVec2(randomcoord) else -- don't random position of e.g. SAM units build as FOB self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) :InitDelayOff() + :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits) :OnSpawnGroup(function(grp,TimeStamp) grp.spawntime = TimeStamp or timer.getTime() end,TimeStamp) :SpawnFromVec2(randomcoord) end @@ -7110,6 +7163,16 @@ end local filepath = self.filepath self:__Save(interval,filepath,filename) end + + if type(self.VehicleMoveFormation) == "table" then + local Formations = {} + for _,_formation in pairs(self.VehicleMoveFormation) do + table.insert(Formations,_formation) + end + self.VehicleMoveFormation = nil + self.VehicleMoveFormation = Formations + end + return self end diff --git a/Moose Development/Moose/Shapes/Circle.lua b/Moose Development/Moose/Shapes/Circle.lua index 39461c522..7bdbb31c1 100644 --- a/Moose Development/Moose/Shapes/Circle.lua +++ b/Moose Development/Moose/Shapes/Circle.lua @@ -72,7 +72,7 @@ end --- Checks if a point is contained within the circle. -- @param #table point The point to check --- @return #bool True if the point is contained, false otherwise +-- @return #boolean True if the point is contained, false otherwise function CIRCLE:ContainsPoint(point) if ((point.x - self.CenterVec2.x) ^ 2 + (point.y - self.CenterVec2.y) ^ 2) ^ 0.5 <= self.Radius then return true @@ -226,6 +226,11 @@ end --- Returns a random Vec2 within the circle. -- @return #table The random Vec2 function CIRCLE:GetRandomVec2() + + math.random() + math.random() + math.random() + local angle = math.random() * 2 * math.pi local rx = math.random(0, self.Radius) * math.cos(angle) + self.CenterVec2.x @@ -237,6 +242,11 @@ end --- Returns a random Vec2 on the border of the circle. -- @return #table The random Vec2 function CIRCLE:GetRandomVec2OnBorder() + + math.random() + math.random() + math.random() + local angle = math.random() * 2 * math.pi local rx = self.Radius * math.cos(angle) + self.CenterVec2.x diff --git a/Moose Development/Moose/Shapes/Polygon.lua b/Moose Development/Moose/Shapes/Polygon.lua index d0253fa04..e126a2002 100644 --- a/Moose Development/Moose/Shapes/Polygon.lua +++ b/Moose Development/Moose/Shapes/Polygon.lua @@ -352,6 +352,7 @@ end --- Returns a random Vec2 within the polygon. The Vec2 is weighted by the areas of the triangles that make up the polygon. -- @return #table The random Vec2 function POLYGON:GetRandomVec2() + local weights = {} for _, triangle in pairs(self.Triangles) do weights[triangle] = triangle.SurfaceArea / self.SurfaceArea diff --git a/Moose Development/Moose/Shapes/Triangle.lua b/Moose Development/Moose/Shapes/Triangle.lua index 747407d2e..e9a52e866 100644 --- a/Moose Development/Moose/Shapes/Triangle.lua +++ b/Moose Development/Moose/Shapes/Triangle.lua @@ -73,6 +73,11 @@ end -- @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 TRIANGLE:GetRandomVec2(points) + + math.random() + math.random() + math.random() + points = points or self.Points local pt = {math.random(), math.random()} table.sort(pt) diff --git a/Moose Development/Moose/Sound/SRS.lua b/Moose Development/Moose/Sound/SRS.lua index 5b9358b2f..3432e0f0c 100644 --- a/Moose Development/Moose/Sound/SRS.lua +++ b/Moose Development/Moose/Sound/SRS.lua @@ -443,28 +443,32 @@ MSRS.Voices = { ["en_AU_Standard_B"] = 'en-AU-Standard-B', -- [2] MALE ["en_AU_Standard_C"] = 'en-AU-Standard-C', -- [3] FEMALE ["en_AU_Standard_D"] = 'en-AU-Standard-D', -- [4] MALE - ["en_IN_Standard_A"] = 'en-IN-Standard-A', -- [5] FEMALE - ["en_IN_Standard_B"] = 'en-IN-Standard-B', -- [6] MALE - ["en_IN_Standard_C"] = 'en-IN-Standard-C', -- [7] MALE - ["en_IN_Standard_D"] = 'en-IN-Standard-D', -- [8] FEMALE + -- IN + ["en_IN_Standard_A"] = 'en-IN-Standard-A', -- Female + ["en_IN_Standard_B"] = 'en-IN-Standard-B', -- Male + ["en_IN_Standard_C"] = 'en-IN-Standard-C', -- Male + ["en_IN_Standard_D"] = 'en-IN-Standard-D', -- Female + ["en_IN_Standard_E"] = 'en-IN-Standard-E', -- Female + ["en_IN_Standard_F"] = 'en-IN-Standard-F', -- Male -- 2025 changes - ["en_GB_Standard_A"] = 'en-GB-Standard-N', -- [9] FEMALE - ["en_GB_Standard_B"] = 'en-GB-Standard-O', -- [10] MALE - ["en_GB_Standard_C"] = 'en-GB-Standard-N', -- [11] FEMALE - ["en_GB_Standard_D"] = 'en-GB-Standard-O', -- [12] MALE - ["en_GB_Standard_F"] = 'en-GB-Standard-N', -- [13] FEMALE - ["en_GB_Standard_O"] = 'en-GB-Standard-O', -- [12] MALE - ["en_GB_Standard_N"] = 'en-GB-Standard-N', -- [13] FEMALE - ["en_US_Standard_A"] = 'en-US-Standard-A', -- [14] MALE - ["en_US_Standard_B"] = 'en-US-Standard-B', -- [15] MALE - ["en_US_Standard_C"] = 'en-US-Standard-C', -- [16] FEMALE - ["en_US_Standard_D"] = 'en-US-Standard-D', -- [17] MALE - ["en_US_Standard_E"] = 'en-US-Standard-E', -- [18] FEMALE - ["en_US_Standard_F"] = 'en-US-Standard-F', -- [19] FEMALE - ["en_US_Standard_G"] = 'en-US-Standard-G', -- [20] FEMALE - ["en_US_Standard_H"] = 'en-US-Standard-H', -- [21] FEMALE - ["en_US_Standard_I"] = 'en-US-Standard-I', -- [22] MALE - ["en_US_Standard_J"] = 'en-US-Standard-J', -- [23] MALE + ["en_GB_Standard_A"] = 'en-GB-Standard-A', -- Female + ["en_GB_Standard_B"] = 'en-GB-Standard-B', -- Male + ["en_GB_Standard_C"] = 'en-GB-Standard-C', -- Female + ["en_GB_Standard_D"] = 'en-GB-Standard-D', -- Male + ["en_GB_Standard_F"] = 'en-GB-Standard-F', -- Female + ["en_GB_Standard_N"] = 'en-GB-Standard-N', -- Female + ["en_GB_Standard_O"] = 'en-GB-Standard-O', -- Male + -- US + ["en_US_Standard_A"] = 'en-US-Standard-A', -- Male + ["en_US_Standard_B"] = 'en-US-Standard-B', -- Male + ["en_US_Standard_C"] = 'en-US-Standard-C', -- Female + ["en_US_Standard_D"] = 'en-US-Standard-D', -- Male + ["en_US_Standard_E"] = 'en-US-Standard-E', -- Female + ["en_US_Standard_F"] = 'en-US-Standard-F', -- Female + ["en_US_Standard_G"] = 'en-US-Standard-G', -- Female + ["en_US_Standard_H"] = 'en-US-Standard-H', -- Female + ["en_US_Standard_I"] = 'en-US-Standard-I', -- Male + ["en_US_Standard_J"] = 'en-US-Standard-J', -- Male -- 2025 catalog changes ["fr_FR_Standard_A"] = "fr-FR-Standard-F", -- Female ["fr_FR_Standard_B"] = "fr-FR-Standard-G", -- Male @@ -474,14 +478,15 @@ MSRS.Voices = { ["fr_FR_Standard_G"] = "fr-FR-Standard-G", -- Male ["fr_FR_Standard_F"] = "fr-FR-Standard-F", -- Female -- 2025 catalog changes - ["de_DE_Standard_A"] = "de-DE-Standard-G", -- Female - ["de_DE_Standard_B"] = "de-DE-Standard-H", -- Male - ["de_DE_Standard_C"] = "de-DE-Standard-G", -- Female - ["de_DE_Standard_D"] = "de-DE-Standard-H", -- Male - ["de_DE_Standard_E"] = "de-DE-Standard-H", -- Male - ["de_DE_Standard_F"] = "de-DE-Standard-G", -- Female - ["de_DE_Standard_H"] = "de-DE-Standard-H", -- Male - ["de_DE_Standard_G"] = "de-DE-Standard-G", -- Female + ["de_DE_Standard_A"] = 'de-DE-Standard-A', -- Female + ["de_DE_Standard_B"] = 'de-DE-Standard-B', -- Male + ["de_DE_Standard_C"] = 'de-DE-Standard-C', -- Female + ["de_DE_Standard_D"] = 'de-DE-Standard-D', -- Male + ["de_DE_Standard_E"] = 'de-DE-Standard-E', -- Male + ["de_DE_Standard_F"] = 'de-DE-Standard-F', -- Female + ["de_DE_Standard_G"] = 'de-DE-Standard-G', -- Female + ["de_DE_Standard_H"] = 'de-DE-Standard-H', -- Male + -- ES ["es_ES_Standard_A"] = "es-ES-Standard-E", -- Female ["es_ES_Standard_B"] = "es-ES-Standard-F", -- Male ["es_ES_Standard_C"] = "es-ES-Standard-E", -- Female @@ -497,32 +502,36 @@ MSRS.Voices = { ["it_IT_Standard_F"] = "it-IT-Standard-F", -- Male }, Wavenet = { - ["en_AU_Wavenet_A"] = 'en-AU-Wavenet-A', -- [1] FEMALE - ["en_AU_Wavenet_B"] = 'en-AU-Wavenet-B', -- [2] MALE - ["en_AU_Wavenet_C"] = 'en-AU-Wavenet-C', -- [3] FEMALE - ["en_AU_Wavenet_D"] = 'en-AU-Wavenet-D', -- [4] MALE - ["en_IN_Wavenet_A"] = 'en-IN-Wavenet-A', -- [5] FEMALE - ["en_IN_Wavenet_B"] = 'en-IN-Wavenet-B', -- [6] MALE - ["en_IN_Wavenet_C"] = 'en-IN-Wavenet-C', -- [7] MALE - ["en_IN_Wavenet_D"] = 'en-IN-Wavenet-D', -- [8] FEMALE + ["en_AU_Wavenet_A"] = 'en-AU-Wavenet-A', -- Female + ["en_AU_Wavenet_B"] = 'en-AU-Wavenet-B', -- Male + ["en_AU_Wavenet_C"] = 'en-AU-Wavenet-C', -- Female + ["en_AU_Wavenet_D"] = 'en-AU-Wavenet-D', -- Male + -- IN + ["en_IN_Wavenet_A"] = 'en-IN-Wavenet-A', -- Female + ["en_IN_Wavenet_B"] = 'en-IN-Wavenet-B', -- Male + ["en_IN_Wavenet_C"] = 'en-IN-Wavenet-C', -- Male + ["en_IN_Wavenet_D"] = 'en-IN-Wavenet-D', -- Female + ["en_IN_Wavenet_E"] = 'en-IN-Wavenet-E', -- Female + ["en_IN_Wavenet_F"] = 'en-IN-Wavenet-F', -- Male -- 2025 changes - ["en_GB_Wavenet_A"] = 'en-GB-Wavenet-N', -- [9] FEMALE - ["en_GB_Wavenet_B"] = 'en-GB-Wavenet-O', -- [10] MALE - ["en_GB_Wavenet_C"] = 'en-GB-Wavenet-N', -- [11] FEMALE - ["en_GB_Wavenet_D"] = 'en-GB-Wavenet-O', -- [12] MALE - ["en_GB_Wavenet_F"] = 'en-GB-Wavenet-N', -- [13] FEMALE + ["en_GB_Wavenet_A"] = 'en-GB-Wavenet-A', -- [9] FEMALE + ["en_GB_Wavenet_B"] = 'en-GB-Wavenet-B', -- [10] MALE + ["en_GB_Wavenet_C"] = 'en-GB-Wavenet-C', -- [11] FEMALE + ["en_GB_Wavenet_D"] = 'en-GB-Wavenet-D', -- [12] MALE + ["en_GB_Wavenet_F"] = 'en-GB-Wavenet-F', -- [13] FEMALE ["en_GB_Wavenet_O"] = 'en-GB-Wavenet-O', -- [12] MALE - ["en_GB_Wavenet_N"] = 'en-GB-Wavenet-N', -- [13] FEMALE - ["en_US_Wavenet_A"] = 'en-US-Wavenet-A', -- [14] MALE - ["en_US_Wavenet_B"] = 'en-US-Wavenet-B', -- [15] MALE - ["en_US_Wavenet_C"] = 'en-US-Wavenet-C', -- [16] FEMALE - ["en_US_Wavenet_D"] = 'en-US-Wavenet-D', -- [17] MALE - ["en_US_Wavenet_E"] = 'en-US-Wavenet-E', -- [18] FEMALE - ["en_US_Wavenet_F"] = 'en-US-Wavenet-F', -- [19] FEMALE - ["en_US_Wavenet_G"] = 'en-US-Wavenet-G', -- [20] FEMALE - ["en_US_Wavenet_H"] = 'en-US-Wavenet-H', -- [21] FEMALE - ["en_US_Wavenet_I"] = 'en-US-Wavenet-I', -- [22] MALE - ["en_US_Wavenet_J"] = 'en-US-Wavenet-J', -- [23] MALE + ["en_GB_Wavenet_N"] = 'en-GB-Wavenet-N', -- [13] FEMALE + -- US + ["en_US_Wavenet_A"] = 'en-US-Wavenet-A', -- Male + ["en_US_Wavenet_B"] = 'en-US-Wavenet-B', -- Male + ["en_US_Wavenet_C"] = 'en-US-Wavenet-C', -- Female + ["en_US_Wavenet_D"] = 'en-US-Wavenet-D', -- Male + ["en_US_Wavenet_E"] = 'en-US-Wavenet-E', -- Female + ["en_US_Wavenet_F"] = 'en-US-Wavenet-F', -- Female + ["en_US_Wavenet_G"] = 'en-US-Wavenet-G', -- Female + ["en_US_Wavenet_H"] = 'en-US-Wavenet-H', -- Female + ["en_US_Wavenet_I"] = 'en-US-Wavenet-I', -- Male + ["en_US_Wavenet_J"] = 'en-US-Wavenet-J', -- Male -- 2025 catalog changes ["fr_FR_Wavenet_A"] = "fr-FR-Wavenet-F", -- Female ["fr_FR_Wavenet_B"] = "fr-FR-Wavenet-G", -- Male @@ -532,14 +541,15 @@ MSRS.Voices = { ["fr_FR_Wavenet_G"] = "fr-FR-Wavenet-G", -- Male ["fr_FR_Wavenet_F"] = "fr-FR-Wavenet-F", -- Female -- 2025 catalog changes - ["de_DE_Wavenet_A"] = "de-DE-Wavenet-G", -- Female - ["de_DE_Wavenet_B"] = "de-DE-Wavenet-H", -- Male - ["de_DE_Wavenet_C"] = "de-DE-Wavenet-G", -- Female - ["de_DE_Wavenet_D"] = "de-DE-Wavenet-H", -- Male - ["de_DE_Wavenet_E"] = "de-DE-Wavenet-H", -- Male - ["de_DE_Wavenet_F"] = "de-DE-Wavenet-G", -- Female - ["de_DE_Wavenet_H"] = "de-DE-Wavenet-H", -- Male - ["de_DE_Wavenet_G"] = "de-DE-Wavenet-G", -- Female + ["de_DE_Wavenet_A"] = 'de-DE-Wavenet-A', -- Female + ["de_DE_Wavenet_B"] = 'de-DE-Wavenet-B', -- Male + ["de_DE_Wavenet_C"] = 'de-DE-Wavenet-C', -- Female + ["de_DE_Wavenet_D"] = 'de-DE-Wavenet-D', -- Male + ["de_DE_Wavenet_E"] = 'de-DE-Wavenet-E', -- Male + ["de_DE_Wavenet_F"] = 'de-DE-Wavenet-F', -- Female + ["de_DE_Wavenet_G"] = 'de-DE-Wavenet-G', -- Female + ["de_DE_Wavenet_H"] = 'de-DE-Wavenet-H', -- Male + -- ES ["es_ES_Wavenet_B"] = "es-ES-Wavenet-E", -- Male ["es_ES_Wavenet_C"] = "es-ES-Wavenet-F", -- Female ["es_ES_Wavenet_D"] = "es-ES-Wavenet-E", -- Female @@ -553,6 +563,134 @@ MSRS.Voices = { ["it_IT_Wavenet_E"] = "it-IT-Wavenet-E", -- Female ["it_IT_Wavenet_F"] = "it-IT-Wavenet-F", -- Male } , + Chirp3HD = { + ["en_GB_Chirp3_HD_Aoede"] = 'en-GB-Chirp3-HD-Aoede', -- Female + ["en_GB_Chirp3_HD_Charon"] = 'en-GB-Chirp3-HD-Charon', -- Male + ["en_GB_Chirp3_HD_Fenrir"] = 'en-GB-Chirp3-HD-Fenrir', -- Male + ["en_GB_Chirp3_HD_Kore"] = 'en-GB-Chirp3-HD-Kore', -- Female + ["en_GB_Chirp3_HD_Leda"] = 'en-GB-Chirp3-HD-Leda', -- Female + ["en_GB_Chirp3_HD_Orus"] = 'en-GB-Chirp3-HD-Orus', -- Male + ["en_GB_Chirp3_HD_Puck"] = 'en-GB-Chirp3-HD-Puck', -- Male + ["en_GB_Chirp3_HD_Zephyr"] = 'en-GB-Chirp3-HD-Zephyr', -- Female + --["de_DE_Chirp3_HD_Aoede"] = 'de-DE-Chirp3-HD-Aoede', -- Female (Datenfehler im Original) + ["en_US_Chirp3_HD_Charon"] = 'en-US-Chirp3-HD-Charon', -- Male + ["en_US_Chirp3_HD_Fenrir"] = 'en-US-Chirp3-HD-Fenrir', -- Male + ["en_US_Chirp3_HD_Kore"] = 'en-US-Chirp3-HD-Kore', -- Female + ["en_US_Chirp3_HD_Leda"] = 'en-US-Chirp3-HD-Leda', -- Female + ["en_US_Chirp3_HD_Orus"] = 'en-US-Chirp3-HD-Orus', -- Male + ["en_US_Chirp3_HD_Puck"] = 'en-US-Chirp3-HD-Puck', -- Male + --["de_DE_Chirp3_HD_Zephyr"] = 'de-DE-Chirp3-HD-Zephyr', -- Female (Datenfehler im Original) + -- DE + ["de_DE_Chirp3_HD_Aoede"] = 'de-DE-Chirp3-HD-Aoede', -- Female + ["de_DE_Chirp3_HD_Charon"] = 'de-DE-Chirp3-HD-Charon', -- Male + ["de_DE_Chirp3_HD_Fenrir"] = 'de-DE-Chirp3-HD-Fenrir', -- Male + ["de_DE_Chirp3_HD_Kore"] = 'de-DE-Chirp3-HD-Kore', -- Female + ["de_DE_Chirp3_HD_Leda"] = 'de-DE-Chirp3-HD-Leda', -- Female + ["de_DE_Chirp3_HD_Orus"] = 'de-DE-Chirp3-HD-Orus', -- Male + ["de_DE_Chirp3_HD_Puck"] = 'de-DE-Chirp3-HD-Puck', -- Male + ["de_DE_Chirp3_HD_Zephyr"] = 'de-DE-Chirp3-HD-Zephyr', -- Female + -- AU + ["en_AU_Chirp3_HD_Aoede"] = 'en-AU-Chirp3-HD-Aoede', -- Female + ["en_AU_Chirp3_HD_Charon"] = 'en-AU-Chirp3-HD-Charon', -- Male + ["en_AU_Chirp3_HD_Fenrir"] = 'en-AU-Chirp3-HD-Fenrir', -- Male + ["en_AU_Chirp3_HD_Kore"] = 'en-AU-Chirp3-HD-Kore', -- Female + ["en_AU_Chirp3_HD_Leda"] = 'en-AU-Chirp3-HD-Leda', -- Female + ["en_AU_Chirp3_HD_Orus"] = 'en-AU-Chirp3-HD-Orus', -- Male + ["en_AU_Chirp3_HD_Puck"] = 'en-AU-Chirp3-HD-Puck', -- Male + ["en_AU_Chirp3_HD_Zephyr"] = 'en-AU-Chirp3-HD-Zephyr', -- Female + -- IN + ["en_IN_Chirp3_HD_Aoede"] = 'en-IN-Chirp3-HD-Aoede', -- Female + ["en_IN_Chirp3_HD_Charon"] = 'en-IN-Chirp3-HD-Charon', -- Male + ["en_IN_Chirp3_HD_Fenrir"] = 'en-IN-Chirp3-HD-Fenrir', -- Male + ["en_IN_Chirp3_HD_Kore"] = 'en-IN-Chirp3-HD-Kore', -- Female + ["en_IN_Chirp3_HD_Leda"] = 'en-IN-Chirp3-HD-Leda', -- Female + ["en_IN_Chirp3_HD_Orus"] = 'en-IN-Chirp3-HD-Orus', -- Male + }, + ChirpHD = { + ["en_US_Chirp_HD_D"] = 'en-US-Chirp-HD-D', -- Male + ["en_US_Chirp_HD_F"] = 'en-US-Chirp-HD-F', -- Female + ["en_US_Chirp_HD_O"] = 'en-US-Chirp-HD-O', -- Female + -- DE + ["de_DE_Chirp_HD_D"] = 'de-DE-Chirp-HD-D', -- Male + ["de_DE_Chirp_HD_F"] = 'de-DE-Chirp-HD-F', -- Female + ["de_DE_Chirp_HD_O"] = 'de-DE-Chirp-HD-O', -- Female + -- AU + ["en_AU_Chirp_HD_D"] = 'en-AU-Chirp-HD-D', -- Male + ["en_AU_Chirp_HD_F"] = 'en-AU-Chirp-HD-F', -- Female + ["en_AU_Chirp_HD_O"] = 'en-AU-Chirp-HD-O', -- Female + -- IN + ["en_IN_Chirp_HD_D"] = 'en-IN-Chirp-HD-D', -- Male + ["en_IN_Chirp_HD_F"] = 'en-IN-Chirp-HD-F', -- Female + ["en_IN_Chirp_HD_O"] = 'en-IN-Chirp-HD-O', -- Female + }, + }, + Neural2 = { + ["en_GB_Neural2_A"] = 'en-GB-Neural2-A', -- Female + ["en_GB_Neural2_B"] = 'en-GB-Neural2-B', -- Male + ["en_GB_Neural2_C"] = 'en-GB-Neural2-C', -- Female + ["en_GB_Neural2_D"] = 'en-GB-Neural2-D', -- Male + ["en_GB_Neural2_F"] = 'en-GB-Neural2-F', -- Female + ["en_GB_Neural2_N"] = 'en-GB-Neural2-N', -- Female + ["en_GB_Neural2_O"] = 'en-GB-Neural2-O', -- Male + -- US + ["en_US_Neural2_A"] = 'en-US-Neural2-A', -- Male + ["en_US_Neural2_C"] = 'en-US-Neural2-C', -- Female + ["en_US_Neural2_D"] = 'en-US-Neural2-D', -- Male + ["en_US_Neural2_E"] = 'en-US-Neural2-E', -- Female + ["en_US_Neural2_F"] = 'en-US-Neural2-F', -- Female + ["en_US_Neural2_G"] = 'en-US-Neural2-G', -- Female + ["en_US_Neural2_H"] = 'en-US-Neural2-H', -- Female + ["en_US_Neural2_I"] = 'en-US-Neural2-I', -- Male + ["en_US_Neural2_J"] = 'en-US-Neural2-J', -- Male + -- DE + ["de_DE_Neural2_G"] = 'de-DE-Neural2-G', -- Female + ["de_DE_Neural2_H"] = 'de-DE-Neural2-H', -- Male + -- AU + ["en_AU_Neural2_A"] = 'en-AU-Neural2-A', -- Female + ["en_AU_Neural2_B"] = 'en-AU-Neural2-B', -- Male + ["en_AU_Neural2_C"] = 'en-AU-Neural2-C', -- Female + ["en_AU_Neural2_D"] = 'en-AU-Neural2-D', -- Male + -- IN + ["en_IN_Neural2_A"] = 'en-IN-Neural2-A', -- Female + ["en_IN_Neural2_B"] = 'en-IN-Neural2-B', -- Male + ["en_IN_Neural2_C"] = 'en-IN-Neural2-C', -- Male + ["en_IN_Neural2_D"] = 'en-IN-Neural2-D', -- Female + }, + News = { + ["en_GB_News_G"] = 'en-GB-News-G', -- Female + ["en_GB_News_H"] = 'en-GB-News-H', -- Female + ["en_GB_News_I"] = 'en-GB-News-I', -- Female + ["en_GB_News_J"] = 'en-GB-News-J', -- Male + ["en_GB_News_K"] = 'en-GB-News-K', -- Male + ["en_GB_News_L"] = 'en-GB-News-L', -- Male + ["en_GB_News_M"] = 'en-GB-News-M', -- Male + -- US + ["en_US_News_K"] = 'en-US-News-K', -- Female + ["en_US_News_L"] = 'en-US-News-L', -- Female + ["en_US_News_N"] = 'en-US-News-N', -- Male + -- AU + ["en_AU_News_E"] = 'en-AU-News-E', -- Female + ["en_AU_News_F"] = 'en-AU-News-F', -- Female + ["en_AU_News_G"] = 'en-AU-News-G', -- Male + }, + Casual = { + ["en_US_Casual_K"] = 'en-US-Casual-K', -- Male + }, + Polyglot = { + ["en_US_Polyglot_1"] = 'en-US-Polyglot-1', -- Male + ["de_DE_Polyglot_1"] = 'de-DE-Polyglot-1', -- Male + ["en_AU_Polyglot_1"] = 'en-AU-Polyglot-1', -- Male + }, + Studio = { + -- Englisch (UK) - Studio + ["en_GB_Studio_B"] = 'en-GB-Studio-B', -- Male + ["en_GB_Studio_C"] = 'en-GB-Studio-C', -- Female + -- Englisch (USA) - Studio + ["en_US_Studio_O"] = 'en-US-Studio-O', -- Female + ["en_US_Studio_Q"] = 'en-US-Studio-Q', -- Male + -- DE + ["de_DE_Studio_B"] = 'de-DE-Studio-B', -- Male + ["de_DE_Studio_C"] = 'de-DE-Studio-C', -- Female }, } diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 17f6bc039..e4d85fbff 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -1586,12 +1586,12 @@ function UTILS.HdgDiff(h1, h2) return math.abs(delta) end ---- Returns the heading from one vec3 to another vec3. --- @param DCS#Vec3 a From vec3. --- @param DCS#Vec3 b To vec3. +--- Returns the heading from one vec2/vec3 to another vec2/vec3. +-- @param DCS#Vec3 a From Vec2 or Vec3. +-- @param DCS#Vec3 b To Vec2 or Vec3. -- @return #number Heading in degrees. function UTILS.HdgTo(a, b) - local dz=b.z-a.z + local dz=(b.z or b.y) - (a.z or a.y) local dx=b.x-a.x local heading=math.deg(math.atan2(dz, dx)) if heading < 0 then @@ -4259,6 +4259,136 @@ function UTILS.SpawnFARPAndFunctionalStatics(Name,Coordinate,FARPType,Coalition, return ReturnObjects, ADFName, MarkerID end +--- Spawn a MASH at a given coordinate, optionally, add an ADF Beacon. +-- @param #string Name Unique Name of the Mash. +-- @param Core.Point#COORDINATE Coordinate Coordinate where to spawn the MASH. Can be given as a Core.Zone#ZONE object, in this case we take the center coordinate. +-- @param #number Country Country ID the MASH belongs to, e.g. country.id.USA or country.id.RUSSIA. +-- @param #number ADF (Optional) ADF Frequency in kHz (Kilohertz), if given activate an ADF Beacon at the location of the MASH. +-- @param #string Livery (Optional) The livery of the static CH-47, defaults to dark green. +-- @param #boolean DeployHelo (Optional) If true, deploy the helicopter static. +-- @param #number MASHRadio MASH Radio Frequency, defaults to 127.5. +-- @param #number MASHRadioModulation MASH Radio Modulation, defaults to radio.modulation.AM. +-- @param #number MASHCallsign Defaults to CALLSIGN.FARP.Berlin. +-- @param #table Templates (Optional) You can hand in your own template table of numbered(!) entries. Each entry consist of a relative(!) x,y position and data of a +-- static, shape_name is optional. Also, livery_id is optional, but is applied to the helicopter static only. +-- @return #table Table of Wrapper.Static#STATIC objects that were spawned. +-- @return #string ADFName Name of the ADF Beacon to remove it later. +-- @usage +-- -- MASH Template example, this one is the built in one used in the function: +-- MASHTemplates = { +-- [1]={category='Infantry',type='Soldier M4',shape_name='none',heading=0,x=0.000000,y=0.000000,}, +-- [2]={category='Infantry',type='Soldier M4',shape_name='none',heading=0,x=0.313533,y=8.778935,}, +-- [3]={category='Infantry',type='Soldier M4',shape_name='none',heading=0,x=16.303737,y=20.379671,}, +-- [4]={category='Helicopters',type='CH-47Fbl1',shape_name='none',heading=0,x=-20.047735,y=-63.166179,livery_id = "us army dark green",}, +-- [5]={category='Infantry',type='Soldier M4',shape_name='none',heading=0,x=26.650339,y=20.066138,}, +-- [6]={category='Heliports',type='FARP_SINGLE_01',shape_name='FARP_SINGLE_01',heading=0,x=-25.432292,y=9.077099,}, +-- [7]={category='Heliports',type='FARP_SINGLE_01',shape_name='FARP_SINGLE_01',heading=0,x=-12.717421,y=-3.216114,}, +-- [8]={category='Heliports',type='FARP_SINGLE_01',shape_name='FARP_SINGLE_01',heading=0,x=-25.439281,y=-3.216114,}, +-- [9]={category='Heliports',type='FARP_SINGLE_01',shape_name='FARP_SINGLE_01',heading=0,x=-12.717421,y=9.155603,}, +-- [10]={category='Fortifications',type='TACAN_beacon',shape_name='none',heading=0,x=-2.329847,y=-16.579903,}, +-- [11]={category='Fortifications',type='FARP Fuel Depot',shape_name='GSM Rus',heading=0,x=2.222011,y=4.487030,}, +-- [12]={category='Fortifications',type='APFC fuel',shape_name='M92_APFCfuel',heading=0,x=3.614927,y=0.367838,}, +-- [13]={category='Fortifications',type='Camouflage03',shape_name='M92_Camouflage03',heading=0,x=21.544148,y=21.998879,}, +-- [14]={category='Fortifications',type='Container_generator',shape_name='M92_Container_generator',heading=0,x=20.989192,y=37.314334,}, +-- [15]={category='Fortifications',type='FireExtinguisher02',shape_name='M92_FireExtinguisher02',heading=0,x=3.988003,y=8.362333,}, +-- [16]={category='Fortifications',type='FireExtinguisher02',shape_name='M92_FireExtinguisher02',heading=0,x=-3.953195,y=12.945844,}, +-- [17]={category='Fortifications',type='Windsock',shape_name='H-Windsock_RW',heading=0,x=-18.944173,y=-33.042196,}, +-- [18]={category='Fortifications',type='Tent04',shape_name='M92_Tent04',heading=0,x=21.220671,y=30.247529,}, +-- } +-- +function UTILS.SpawnMASHStatics(Name,Coordinate,Country,ADF,Livery,DeployHelo,MASHRadio,MASHRadioModulation,MASHCallsign,Templates) + + -- Basic objects table + + local MASHTemplates = { + [1]={category='Infantry',type='Soldier M4',shape_name='none',heading=0,x=0.000000,y=0.000000,}, + [2]={category='Infantry',type='Soldier M4',shape_name='none',heading=0,x=0.313533,y=8.778935,}, + [3]={category='Infantry',type='Soldier M4',shape_name='none',heading=0,x=16.303737,y=20.379671,}, + [4]={category='Helicopters',type='CH-47Fbl1',shape_name='none',heading=0,x=-20.047735,y=-63.166179,livery_id = "us army dark green",}, + [5]={category='Infantry',type='Soldier M4',shape_name='none',heading=0,x=26.650339,y=20.066138,}, + [6]={category='Heliports',type='FARP_SINGLE_01',shape_name='FARP_SINGLE_01',heading=0,x=-25.432292,y=9.077099,}, + [7]={category='Heliports',type='FARP_SINGLE_01',shape_name='FARP_SINGLE_01',heading=0,x=-12.717421,y=-3.216114,}, + [8]={category='Heliports',type='FARP_SINGLE_01',shape_name='FARP_SINGLE_01',heading=0,x=-25.439281,y=-3.216114,}, + [9]={category='Heliports',type='FARP_SINGLE_01',shape_name='FARP_SINGLE_01',heading=0,x=-12.717421,y=9.155603,}, + [10]={category='Fortifications',type='TACAN_beacon',shape_name='none',heading=0,x=-2.329847,y=-16.579903,}, + [11]={category='Fortifications',type='FARP Fuel Depot',shape_name='GSM Rus',heading=0,x=2.222011,y=4.487030,}, + [12]={category='Fortifications',type='APFC fuel',shape_name='M92_APFCfuel',heading=0,x=3.614927,y=0.367838,}, + [13]={category='Fortifications',type='Camouflage03',shape_name='M92_Camouflage03',heading=0,x=21.544148,y=21.998879,}, + [14]={category='Fortifications',type='Container_generator',shape_name='M92_Container_generator',heading=0,x=20.989192,y=37.314334,}, + [15]={category='Fortifications',type='FireExtinguisher02',shape_name='M92_FireExtinguisher02',heading=0,x=3.988003,y=8.362333,}, + [16]={category='Fortifications',type='FireExtinguisher02',shape_name='M92_FireExtinguisher02',heading=0,x=-3.953195,y=12.945844,}, + [17]={category='Fortifications',type='Windsock',shape_name='H-Windsock_RW',heading=0,x=-18.944173,y=-33.042196,}, + [18]={category='Fortifications',type='Tent04',shape_name='M92_Tent04',heading=0,x=21.220671,y=30.247529,}, + } + + if Templates then MASHTemplates=Templates end + + -- locals + local name = Name or "Florence Nightingale" + local positionVec2 + local positionVec3 + local ReturnStatics = {} + local CountryID = Country or country.id.USA + local livery = "us army dark green" + local MASHRadio = MASHRadio or 127.5 + local MASHRadioModulation = MASHRadioModulation or radio.modulation.AM + local MASHCallsign = MASHCallsign or CALLSIGN.FARP.Berlin + + -- check for coordinate or zone + if type(Coordinate) == "table" then + if Coordinate:IsInstanceOf("COORDINATE") or Coordinate:IsInstanceOf("ZONE_BASE") then + positionVec2 = Coordinate:GetVec2() + positionVec3 = Coordinate:GetVec3() + end + else + BASE:E("Spawn MASH - no ZONE or COORDINATE handed!") + return + end + + -- position + local BaseX = positionVec2.x + local BaseY = positionVec2.y + + -- Statics + for id,object in pairs(MASHTemplates) do + local NewName = string.format("%s#%3d",name,id) + local vec2 = {x=BaseX+object.x,y=BaseY+object.y} + local Coordinate=COORDINATE:NewFromVec2(vec2) + local static = SPAWNSTATIC:NewFromType(object.type,object.category,CountryID) + if object.shape_name and object.shape_name ~= "none" then + static:InitShape(object.shape_name) + end + if object.category == "Helicopters" and DeployHelo == true then + if object.livery_id ~= nil then + livery = object.livery_id + end + static:InitLivery(livery) + local newstatic = static:SpawnFromCoordinate(Coordinate,object.heading,NewName) + table.insert(ReturnStatics,newstatic) + elseif object.category == "Heliports" then + static:InitFARP(MASHCallsign,MASHRadio,MASHRadioModulation,false,false) + local newstatic = static:SpawnFromCoordinate(Coordinate,object.heading,NewName) + table.insert(ReturnStatics,newstatic) + elseif object.category ~= "Helicopters" and object.category ~= "Heliports" then + local newstatic = static:SpawnFromCoordinate(Coordinate,object.heading,NewName) + table.insert(ReturnStatics,newstatic) + end + + end + + -- Beacon + local ADFName + if ADF and type(ADF) == "number" then + local ADFFreq = ADF*1000 -- KHz to Hz + local Sound = "l10n/DEFAULT/beacon.ogg" + ADFName = Name .. " ADF "..tostring(ADF).."KHz" + --BASE:I(string.format("Adding MASH Beacon %d KHz Name %s",ADF,ADFName)) + trigger.action.radioTransmission(Sound, positionVec3, 0, true, ADFFreq, 250, ADFName) + end + + return ReturnStatics, ADFName +end + --- Converts a Vec2 to a Vec3. -- @param vec the 2D vector -- @param y optional new y axis (altitude) value. If omitted it's 0. @@ -4463,13 +4593,166 @@ function UTILS.GetEnvZone(name) end end +--- net.dostring_in +function UTILS.DoStringIn(State,DoString) + return net.dostring_in(State,DoString) +end + +--- Show a picture on the screen to all +-- @param #string FileName File name of the picture +-- @param #number Duration Duration in seconds, defaults to 10 +-- @param #boolean ClearView If true, clears the view before showing the picture, defaults to false +-- @param #number StartDelay Delay in seconds before showing the picture, defaults to 0 +-- @param #number HorizontalAlign Horizontal alignment of the picture, 0: Left, 1: Center, 2: Right +-- @param #number VerticalAlign Vertical alignment of the picture, 0: Top, 1: Center, 2: Bottom +-- @param #number Size Size of the picture in percent, defaults to 100 +-- @param #number SizeUnits Size units, 0 for % of original picture size, and 1 for % of window size +function UTILS.ShowPictureToAll(FilePath, Duration, ClearView, StartDelay, HorizontalAlign, VerticalAlign, Size, SizeUnits) + ClearView = ClearView or false + StartDelay = StartDelay or 0 + HorizontalAlign = HorizontalAlign or 1 + VerticalAlign = VerticalAlign or 1 + Size = Size or 100 + SizeUnits = SizeUnits or 0 + + if ClearView then ClearView = "true" else ClearView = "false" end + + net.dostring_in("mission", string.format("a_out_picture(\"%s\", %d, %s, %d, \"%d\", \"%d\", %d, \"%d\")", FilePath, Duration or 10, ClearView, StartDelay, HorizontalAlign, VerticalAlign, Size, SizeUnits)) +end + +--- Show a picture on the screen to Coalition +-- @param #number Coalition Coalition ID, can be coalition.side.BLUE, coalition.side.RED or coalition.side.NEUTRAL +-- @param #string FileName File name of the picture +-- @param #number Duration Duration in seconds, defaults to 10 +-- @param #boolean ClearView If true, clears the view before showing the picture, defaults to false +-- @param #number StartDelay Delay in seconds before showing the picture, defaults to 0 +-- @param #number HorizontalAlign Horizontal alignment of the picture, 0: Left, 1: Center, 2: Right +-- @param #number VerticalAlign Vertical alignment of the picture, 0: Top, 1: Center, 2: Bottom +-- @param #number Size Size of the picture in percent, defaults to 100 +-- @param #number SizeUnits Size units, 0 for % of original picture size, and 1 for % of window size +function UTILS.ShowPictureToCoalition(Coalition, FilePath, Duration, ClearView, StartDelay, HorizontalAlign, VerticalAlign, Size, SizeUnits) + ClearView = ClearView or false + StartDelay = StartDelay or 0 + HorizontalAlign = HorizontalAlign or 1 + VerticalAlign = VerticalAlign or 1 + Size = Size or 100 + SizeUnits = SizeUnits or 0 + + if ClearView then ClearView = "true" else ClearView = "false" end + + local coalName = string.lower(UTILS.GetCoalitionName(Coalition)) + + net.dostring_in("mission", string.format("a_out_picture_s(\"%s\", \"%s\", %d, %s, %d, \"%d\", \"%d\", %d, \"%d\")", coalName, FilePath, Duration or 10, ClearView, StartDelay, HorizontalAlign, VerticalAlign, Size, SizeUnits)) +end + +--- Show a picture on the screen to Country +-- @param #number Country Country ID, can be country.id.USA, country.id.RUSSIA, etc. +-- @param #string FileName File name of the picture +-- @param #number Duration Duration in seconds, defaults to 10 +-- @param #boolean ClearView If true, clears the view before showing the picture, defaults to false +-- @param #number StartDelay Delay in seconds before showing the picture, defaults to 0 +-- @param #number HorizontalAlign Horizontal alignment of the picture, 0: Left, 1: Center, 2: Right +-- @param #number VerticalAlign Vertical alignment of the picture, 0: Top, 1: Center, 2: Bottom +-- @param #number Size Size of the picture in percent, defaults to 100 +-- @param #number SizeUnits Size units, 0 for % of original picture size, and 1 for % of window size +function UTILS.ShowPictureToCountry(Country, FilePath, Duration, ClearView, StartDelay, HorizontalAlign, VerticalAlign, Size, SizeUnits) + ClearView = ClearView or false + StartDelay = StartDelay or 0 + HorizontalAlign = HorizontalAlign or 1 + VerticalAlign = VerticalAlign or 1 + Size = Size or 100 + SizeUnits = SizeUnits or 0 + + if ClearView then ClearView = "true" else ClearView = "false" end + + net.dostring_in("mission", string.format("a_out_picture_c(%d, \"%s\", %d, %s, %d, \"%d\", \"%d\", %d, \"%d\")", Country, FilePath, Duration or 10, ClearView, StartDelay, HorizontalAlign, VerticalAlign, Size, SizeUnits)) +end + +--- Show a picture on the screen to Group +-- @param Wrapper.Group#GROUP Group Group to show the picture to +-- @param #string FileName File name of the picture +-- @param #number Duration Duration in seconds, defaults to 10 +-- @param #boolean ClearView If true, clears the view before showing the picture, defaults to false +-- @param #number StartDelay Delay in seconds before showing the picture, defaults to 0 +-- @param #number HorizontalAlign Horizontal alignment of the picture, 0: Left, 1: Center, 2: Right +-- @param #number VerticalAlign Vertical alignment of the picture, 0: Top, 1: Center, 2: Bottom +-- @param #number Size Size of the picture in percent, defaults to 100 +-- @param #number SizeUnits Size units, 0 for % of original picture size, and 1 for % of window size +function UTILS.ShowPictureToGroup(Group, FilePath, Duration, ClearView, StartDelay, HorizontalAlign, VerticalAlign, Size, SizeUnits) + ClearView = ClearView or false + StartDelay = StartDelay or 0 + HorizontalAlign = HorizontalAlign or 1 + VerticalAlign = VerticalAlign or 1 + Size = Size or 100 + SizeUnits = SizeUnits or 0 + + if ClearView then ClearView = "true" else ClearView = "false" end + + net.dostring_in("mission", string.format("a_out_picture_g(%d, \"%s\", %d, %s, %d, \"%d\", \"%d\", %d, \"%d\")", Group:GetID(), FilePath, Duration or 10, ClearView, StartDelay, HorizontalAlign, VerticalAlign, Size, SizeUnits)) +end + +--- Show a picture on the screen to Unit +-- @param Wrapper.Unit#UNIT Unit Unit to show the picture to +-- @param #string FileName File name of the picture +-- @param #number Duration Duration in seconds, defaults to 10 +-- @param #boolean ClearView If true, clears the view before showing the picture, defaults to false +-- @param #number StartDelay Delay in seconds before showing the picture, defaults to 0 +-- @param #number HorizontalAlign Horizontal alignment of the picture, 0: Left, 1: Center, 2: Right +-- @param #number VerticalAlign Vertical alignment of the picture, 0: Top, 1: Center, 2: Bottom +-- @param #number Size Size of the picture in percent, defaults to 100 +-- @param #number SizeUnits Size units, 0 for % of original picture size, and 1 for % of window size +function UTILS.ShowPictureToUnit(Unit, FilePath, Duration, ClearView, StartDelay, HorizontalAlign, VerticalAlign, Size, SizeUnits) + ClearView = ClearView or false + StartDelay = StartDelay or 0 + HorizontalAlign = HorizontalAlign or 1 + VerticalAlign = VerticalAlign or 1 + Size = Size or 100 + SizeUnits = SizeUnits or 0 + + if ClearView then ClearView = "true" else ClearView = "false" end + + net.dostring_in("mission", string.format("a_out_picture_u(%d, \"%s\", %d, %s, %d, \"%d\", \"%d\", %d, \"%d\")", Unit:GetID(), FilePath, Duration or 10, ClearView, StartDelay, HorizontalAlign, VerticalAlign, Size, SizeUnits)) +end + +--- Load a mission file. This will replace the current mission with the one given carrying along the online clients. +-- @param #string FileName Mission filename +function UTILS.LoadMission(FileName) + net.dostring_in("mission", string.format("a_load_mission(\"%s\")", FileName)) +end + +--- Set the mission briefing for a coalition. +-- @param #number Coalition Briefing coalition ID, can be coalition.side.BLUE, coalition.side.RED or coalition.side.NEUTRAL +-- @param #string Text Briefing text, can contain newlines, will be converted formatted properly for DCS +-- @param #string Picture Picture file path, can be a file in the DEFAULT folder inside the .miz +function UTILS.SetMissionBriefing(Coalition, Text, Picture) + Text = Text or "" + Text = Text:gsub("\n", "\\n") + Picture = Picture or "" + local coalName = string.lower(UTILS.GetCoalitionName(Coalition)) + net.dostring_in("mission", string.format("a_set_briefing(\"%s\", \"%s\", \"%s\")", coalName, Picture, Text)) +end + --- Show a helper gate at a DCS#Vec3 position -- @param DCS#Vec3 pos The position --- @param number heading Heading in degrees, can be 0..359 degrees +-- @param #number heading Heading in degrees, can be 0..359 degrees function UTILS.ShowHelperGate(pos, heading) net.dostring_in("mission",string.format("a_show_helper_gate(%s, %s, %s, %f)", pos.x, pos.y, pos.z, math.rad(heading))) end +--- Show a helper gate for a unit. +-- @param Wrapper.Unit#UNIT Unit The unit to show the gate for +-- @param #number Flag Helper gate flag +function UTILS.ShowHelperGateForUnit(Unit, Flag) + net.dostring_in("mission",string.format("a_show_route_gates_for_unit(%d, \"%d\")", Unit:GetID(), Flag)) +end + +--- Set the carrier illumination mode. -2: OFF, -1: AUTO, 0: NAVIGATION, 1: AC LAUNCH, 2: AC RECOVERY +-- @param #number UnitID Carrier unit ID ( UNIT:GetID() ) +-- @param #number Mode Illumination mode, can be -2: OFF, -1: AUTO, 0: NAVIGATION, 1: AC LAUNCH, 2: AC RECOVERY +function UTILS.SetCarrierIlluminationMode(UnitID, Mode) + net.dostring_in("mission",string.format("a_set_carrier_illumination_mode(%d, %d)", UnitID, Mode)) +end + --- Shell a zone, zone must ME created -- @param #string name The name of the ME created zone -- @param #number power Equals kg of TNT, e.g. 75 @@ -4500,3 +4783,249 @@ function UTILS.DestroyScenery(name, level) net.dostring_in("mission",string.format("a_scenery_destruction_zone(%d, %d)", z.zoneId, level)) end end + +--- Search for clear zones in a given area. A powerful and efficient function using Disposition to find clear areas for spawning ground units avoiding trees, water and map scenery. +-- @param DCS##Vec3 Center position vector for the search area. +-- @param #number SearchRadius Radius of the search area. +-- @param #number PosRadius Required clear radius around each position. +-- @param #number NumPositions Number of positions to find. +-- @return #table A table of DCS#Vec2 positions that are clear of map objects within the given PosRadius. +function UTILS.GetSimpleZones(Vec3, SearchRadius, PosRadius, NumPositions) + return Disposition.getSimpleZones(Vec3, SearchRadius, PosRadius, NumPositions) +end + +--- Search for clear ground spawn zones within this zone. A powerful and efficient function using Disposition to find clear areas for spawning ground units avoiding trees, water and map scenery. +-- @param Core.Zone#ZONE Zone to search. +-- @param #number (Optional) PosRadius Required clear radius around each position. (Default is math.min(Radius/10, 200)) +-- @param #number (Optional) NumPositions Number of positions to find. (Default 50) +-- @return #table A table of DCS#Vec2 positions that are clear of map objects within the given PosRadius. nil if no clear positions are found. +function UTILS.GetClearZonePositions(Zone, PosRadius, NumPositions) + local radius = PosRadius or math.min(Zone:GetRadius()/10, 200) + local clearPositions = UTILS.GetSimpleZones(Zone:GetVec3(), Zone:GetRadius(), radius, NumPositions or 50) + if clearPositions and #clearPositions > 0 then + local validZones = {} + for _, vec2 in pairs(clearPositions) do + if Zone:IsVec2InZone(vec2) then + table.insert(validZones, vec2) + end + end + if #validZones > 0 then + return validZones, radius + end + end + return nil +end + + +--- Search for a random clear ground spawn coordinate within this zone. A powerful and efficient function using Disposition to find clear areas for spawning ground units avoiding trees, water and map scenery. +-- @param Core.Zone#ZONE Zone to search. +-- @param #number PosRadius (Optional) Required clear radius around each position. (Default is math.min(Radius/10, 200)) +-- @param #number NumPositions (Optional) Number of positions to find. (Default 50) +-- @return Core.Point#COORDINATE A random coordinate for a clear zone. nil if no clear positions are found. +-- @return #number Assigned radius for the found zones. nil if no clear positions are found. +function UTILS.GetRandomClearZoneCoordinate(Zone, PosRadius, NumPositions) + local clearPositions = UTILS.GetClearZonePositions(Zone, PosRadius, NumPositions) + if clearPositions and #clearPositions > 0 then + local randomPosition, radius = clearPositions[math.random(1, #clearPositions)] + return COORDINATE:NewFromVec2(randomPosition), radius + end + + return nil +end + +--- Find the point on the radius of a circle closest to a point outside of the radius. +-- @param DCS#Vec2 Vec1 Simple Vec2 marking the middle of the circle. +-- @param #number Radius The radius of the circle. +-- @param DCS#Vec2 Vec2 Simple Vec2 marking the point outside of the circle. +-- @return DCS#Vec2 Vec2 point on the radius. +function UTILS.FindNearestPointOnCircle(Vec1,Radius,Vec2) + local r = Radius + local cx = Vec1.x or 1 + local cy = Vec1.y or 1 + local px = Vec2.x or 1 + local py = Vec2.y or 1 + + -- Berechne den Vektor vom Mittelpunkt zum externen Punkt + local dx = px - cx + local dy = py - cy + + -- Berechne die Länge des Vektors + local dist = math.sqrt(dx * dx + dy * dy) + + -- Wenn der Punkt im Mittelpunkt liegt, wähle einen Punkt auf der X-Achse + if dist == 0 then + return {x=cx + r, y=cy} + end + + -- Normalisiere den Vektor (richtungsweise Vektor mit Länge 1) + local norm_dx = dx / dist + local norm_dy = dy / dist + + -- Berechne den Punkt auf dem Rand des Kreises + local qx = cx + r * norm_dx + local qy = cy + r * norm_dy + + local shift_factor = 1 + qx = qx + shift_factor * norm_dx + qy = qy + shift_factor * norm_dy + + return {x=qx, y=qy} +end + +--- This function uses Disposition and other fallback logic to find better ground positions for ground units. +--- NOTE: This is not a spawn randomizer. +--- It will try to find clear ground locations avoiding trees, water, roads, runways, map scenery, statics and other units in the area and modifies the provided positions table. +--- Maintains the original layout and unit positions as close as possible by searching for the next closest valid position to each unit. +-- @param #table Positions A table of DCS#Vec2 or DCS#Vec3, can be a units table from the group template. +-- @param DCS#Vec2 Anchor (Optional) DCS#Vec2 or DCS#Vec3 as anchor point to calculate offset of the units. +-- @param #number MaxRadius (Optional) Max radius to search for valid ground locations in meters. Default is double the max radius of the units. +-- @param #number Spacing (Optional) Minimum spacing between units in meters. Default is 5% of the search radius or 5 meters, whichever is larger. +function UTILS.ValidateAndRepositionGroundUnits(Positions, Anchor, MaxRadius, Spacing) + local units = Positions + Anchor = Anchor or UTILS.GetCenterPoint(units) + local gPos = { x = Anchor.x, y = Anchor.z or Anchor.y } + local maxRadius = 0 + local unitCount = 0 + for _, unit in pairs(units) do + local pos = { x = unit.x, y = unit.z or unit.y } + local dist = UTILS.VecDist2D(pos, gPos) + if dist > maxRadius then + maxRadius = dist + end + unitCount = unitCount + 1 + end + maxRadius = MaxRadius or math.max(maxRadius * 2, 10) + local spacing = Spacing or math.max(maxRadius * 0.05, 5) + if unitCount > 0 and maxRadius > 5 then + local spots = UTILS.GetSimpleZones(UTILS.Vec2toVec3(gPos), maxRadius, spacing, 1000) + if spots and #spots > 0 then + local validSpots = {} + for _, spot in pairs(spots) do -- Disposition sometimes returns points on roads, hence this filter. + if land.getSurfaceType(spot) == land.SurfaceType.LAND then + table.insert(validSpots, spot) + end + end + spots = validSpots + end + + local step = spacing + for _, unit in pairs(units) do + local pos = { x = unit.x, y = unit.z or unit.y } + local isOnLand = land.getSurfaceType(pos) == land.SurfaceType.LAND + local isValid = false + if spots and #spots > 0 then + local si = 1 + local sid = 0 + local closestDist = 100000000 + local closestSpot + for _, spot in pairs(spots) do + local dist = UTILS.VecDist2D(pos, spot) + if dist < closestDist then + closestDist = dist + closestSpot = spot + sid = si + end + si = si + 1 + end + if closestSpot then + if closestDist >= spacing then + pos = closestSpot + end + isValid = true + table.remove(spots, sid) + end + end + + -- Failsafe calculation + if not isValid and not isOnLand then + + local h = UTILS.HdgTo(pos, gPos) + local retries = 0 + while not isValid and retries < 500 do + + local dist = UTILS.VecDist2D(pos, gPos) + pos = UTILS.Vec2Translate(pos, step, h) + + local skip = false + for _, unit2 in pairs(units) do + if unit ~= unit2 then + local pos2 = { x = unit2.x, y = unit2.z or unit2.y } + local dist2 = UTILS.VecDist2D(pos, pos2) + if dist2 < 12 then + isValid = false + skip = true + break + end + end + end + + if not skip and dist > step and land.getSurfaceType(pos) == land.SurfaceType.LAND then + isValid = true + break + elseif dist <= step then + break + end + + retries = retries + 1 + end + end + + if isValid then + unit.x = pos.x + if unit.z then + unit.z = pos.y + else + unit.y = pos.y + end + end + end + end +end + +--- This function uses Disposition and other fallback logic to find better ground positions for statics. +--- NOTE: This is not a spawn randomizer. +--- It will try to find clear ground locations avoiding trees, water, roads, runways, map scenery, statics and other units in the area and modifies the provided positions table. +--- Maintains the original layout and unit positions as close as possible by searching for the next closest valid position to each unit. +-- @param #table Positions A table of DCS#Vec2 or DCS#Vec3, can be a units table from the group template. +-- @param DCS#Vec2 Position DCS#Vec2 or DCS#Vec3 initial spawn location. +-- @param #number MaxRadius (Optional) Max radius to search for valid ground locations in meters. Default is double the max radius of the static. +-- @return DCS#Vec2 Initial Position if it's valid, else a valid spawn position. nil if no valid position found. +function UTILS.ValidateAndRepositionStatic(Country, Category, Type, Position, ShapeName, MaxRadius) + local coord = COORDINATE:NewFromVec2(Position) + local st = SPAWNSTATIC:NewFromType(Type, Category, Country) + if ShapeName then + st:InitShape(ShapeName) + end + local sName = "s-"..timer.getTime().."-"..math.random(1,10000) + local tempStatic = st:SpawnFromCoordinate(coord, 0, sName) + if tempStatic then + local sRadius = tempStatic:GetBoundingRadius(2) or 3 + tempStatic:Destroy() + sRadius = sRadius * 0.5 + MaxRadius = MaxRadius or math.max(sRadius * 10, 100) + local positions = UTILS.GetSimpleZones(coord:GetVec3(), MaxRadius, sRadius, 20) + if positions and #positions > 0 then + local closestSpot + local closestDist = math.huge + for _, spot in pairs(positions) do -- Disposition sometimes returns points on roads, hence this filter. + if land.getSurfaceType(spot) == land.SurfaceType.LAND then + local dist = UTILS.VecDist2D(Position, spot) + if dist < closestDist then + closestDist = dist + closestSpot = spot + end + end + end + + if closestSpot then + if closestDist >= sRadius then + return closestSpot + else + return Position + end + end + end + end + + return nil +end diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 520f8b923..7b56f195b 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -703,51 +703,56 @@ AIRBASE.SouthAtlantic={ --- Airbases of the Sinai map: -- --- * AIRBASE.Sinai.Abu_Rudeis --- * AIRBASE.Sinai.Abu_Suwayr --- * AIRBASE.Sinai.Al_Bahr_al_Ahmar --- * AIRBASE.Sinai.Al_Ismailiyah --- * AIRBASE.Sinai.Al_Khatatbah --- * AIRBASE.Sinai.Al_Mansurah --- * AIRBASE.Sinai.Al_Rahmaniyah_Air_Base --- * AIRBASE.Sinai.As_Salihiyah --- * AIRBASE.Sinai.AzZaqaziq --- * AIRBASE.Sinai.Baluza --- * AIRBASE.Sinai.Ben_Gurion --- * AIRBASE.Sinai.Beni_Suef --- * AIRBASE.Sinai.Bilbeis_Air_Base --- * AIRBASE.Sinai.Bir_Hasanah --- * AIRBASE.Sinai.Birma_Air_Base --- * AIRBASE.Sinai.Borg_El_Arab_International_Airport --- * AIRBASE.Sinai.Cairo_International_Airport --- * AIRBASE.Sinai.Cairo_West --- * AIRBASE.Sinai.Difarsuwar_Airfield --- * AIRBASE.Sinai.El_Arish --- * AIRBASE.Sinai.El_Gora --- * AIRBASE.Sinai.El_Minya --- * AIRBASE.Sinai.Fayed --- * AIRBASE.Sinai.Gebel_El_Basur_Air_Base --- * AIRBASE.Sinai.Hatzerim --- * AIRBASE.Sinai.Hatzor --- * AIRBASE.Sinai.Hurghada_International_Airport --- * AIRBASE.Sinai.Inshas_Airbase --- * AIRBASE.Sinai.Jiyanklis_Air_Base --- * AIRBASE.Sinai.Kedem --- * AIRBASE.Sinai.Kibrit_Air_Base --- * AIRBASE.Sinai.Kom_Awshim --- * AIRBASE.Sinai.Melez --- * AIRBASE.Sinai.Nevatim --- * AIRBASE.Sinai.Ovda --- * AIRBASE.Sinai.Palmachim --- * AIRBASE.Sinai.Quwaysina --- * AIRBASE.Sinai.Ramon_Airbase --- * AIRBASE.Sinai.Ramon_International_Airport --- * AIRBASE.Sinai.Sde_Dov --- * AIRBASE.Sinai.Sharm_El_Sheikh_International_Airport --- * AIRBASE.Sinai.St_Catherine --- * AIRBASE.Sinai.Tel_Nof --- * AIRBASE.Sinai.Wadi_Abu_Rish --- * AIRBASE.Sinai.Wadi_al_Jandali +-- * AIRBASE.SinaiMap.Abu_Rudeis +-- * AIRBASE.SinaiMap.Abu_Suwayr +-- * AIRBASE.SinaiMap.Al_Bahr_al_Ahmar +-- * AIRBASE.SinaiMap.Al_Ismailiyah +-- * AIRBASE.SinaiMap.Al_Khatatbah +-- * AIRBASE.SinaiMap.Al_Mansurah +-- * AIRBASE.SinaiMap.Al_Rahmaniyah_Air_Base +-- * AIRBASE.SinaiMap.As_Salihiyah +-- * AIRBASE.SinaiMap.AzZaqaziq +-- * AIRBASE.SinaiMap.Baluza +-- * AIRBASE.SinaiMap.Ben_Gurion +-- * AIRBASE.SinaiMap.Beni_Suef +-- * AIRBASE.SinaiMap.Bilbeis_Air_Base +-- * AIRBASE.SinaiMap.Bir_Hasanah +-- * AIRBASE.SinaiMap.Birma_Air_Base +-- * AIRBASE.SinaiMap.Borg_El_Arab_International_Airport +-- * AIRBASE.SinaiMap.Cairo_International_Airport +-- * AIRBASE.SinaiMap.Cairo_West +-- * AIRBASE.SinaiMap.Damascus_Intl +-- * AIRBASE.SinaiMap.Difarsuwar_Airfield +-- * AIRBASE.SinaiMap.El_Arish +-- * AIRBASE.SinaiMap.El_Gora +-- * AIRBASE.SinaiMap.El_Minya +-- * AIRBASE.SinaiMap.Fayed +-- * AIRBASE.SinaiMap.Gebel_El_Basur_Air_Base +-- * AIRBASE.SinaiMap.Hatzerim +-- * AIRBASE.SinaiMap.Hatzor +-- * AIRBASE.SinaiMap.Hurghada_International_Airport +-- * AIRBASE.SinaiMap.Inshas_Airbase +-- * AIRBASE.SinaiMap.Jiyanklis_Air_Base +-- * AIRBASE.SinaiMap.Kedem +-- * AIRBASE.SinaiMap.Kibrit_Air_Base +-- * AIRBASE.SinaiMap.Kom_Awshim +-- * AIRBASE.SinaiMap.Melez +-- * AIRBASE.SinaiMap.Mezzeh_Air_Base +-- * AIRBASE.SinaiMap.Nevatim +-- * AIRBASE.SinaiMap.Ovda +-- * AIRBASE.SinaiMap.Palmachim +-- * AIRBASE.SinaiMap.Quwaysina +-- * AIRBASE.SinaiMap.Rafic_Hariri_Intl +-- * AIRBASE.SinaiMap.Ramat_David +-- * AIRBASE.SinaiMap.Ramon_Airbase +-- * AIRBASE.SinaiMap.Ramon_International_Airport +-- * AIRBASE.SinaiMap.Sde_Dov +-- * AIRBASE.SinaiMap.Sharm_El_Sheikh_International_Airport +-- * AIRBASE.SinaiMap.St_Catherine +-- * AIRBASE.SinaiMap.Tabuk +-- * AIRBASE.SinaiMap.Tel_Nof +-- * AIRBASE.SinaiMap.Wadi_Abu_Rish +-- * AIRBASE.SinaiMap.Wadi_al_Jandali -- -- @field Sinai AIRBASE.Sinai = { @@ -769,6 +774,7 @@ AIRBASE.Sinai = { ["Borg_El_Arab_International_Airport"] = "Borg El Arab International Airport", ["Cairo_International_Airport"] = "Cairo International Airport", ["Cairo_West"] = "Cairo West", + ["Damascus_Intl"] = "Damascus Intl", ["Difarsuwar_Airfield"] = "Difarsuwar Airfield", ["El_Arish"] = "El Arish", ["El_Gora"] = "El Gora", @@ -784,15 +790,19 @@ AIRBASE.Sinai = { ["Kibrit_Air_Base"] = "Kibrit Air Base", ["Kom_Awshim"] = "Kom Awshim", ["Melez"] = "Melez", + ["Mezzeh_Air_Base"] = "Mezzeh Air Base", ["Nevatim"] = "Nevatim", ["Ovda"] = "Ovda", ["Palmachim"] = "Palmachim", ["Quwaysina"] = "Quwaysina", + ["Rafic_Hariri_Intl"] = "Rafic Hariri Intl", + ["Ramat_David"] = "Ramat David", ["Ramon_Airbase"] = "Ramon Airbase", ["Ramon_International_Airport"] = "Ramon International Airport", ["Sde_Dov"] = "Sde Dov", ["Sharm_El_Sheikh_International_Airport"] = "Sharm El Sheikh International Airport", ["St_Catherine"] = "St Catherine", + ["Tabuk"] = "Tabuk", ["Tel_Nof"] = "Tel Nof", ["Wadi_Abu_Rish"] = "Wadi Abu Rish", ["Wadi_al_Jandali"] = "Wadi al Jandali", @@ -923,37 +933,51 @@ AIRBASE.Afghanistan = { --- Airbases of the Iraq map -- --- * AIRBASE.Iraq.Baghdad_International_Airport --- * AIRBASE.Iraq.Sulaimaniyah_International_Airport --- * AIRBASE.Iraq.Al_Sahra_Airport --- * AIRBASE.Iraq.Erbil_International_Airpor --- * AIRBASE.Iraq.Al_Taji_Airport -- * AIRBASE.Iraq.Al_Asad_Airbase +-- * AIRBASE.Iraq.Al_Kut_Airbase +-- * AIRBASE.Iraq.Al_Sahra_Airport -- * AIRBASE.Iraq.Al_Salam_Airbase --- * AIRBASE.Iraq.Balad_Airbase --- * AIRBASE.Iraq.Kirkuk_International_Airport --- * AIRBASE.Iraq.Bashur_Airport +-- * AIRBASE.Iraq.Al_Taji_Airport -- * AIRBASE.Iraq.Al_Taquddum_Airport --- * AIRBASE.Iraq.Qayyarah_Airfield_West +-- * AIRBASE.Iraq.Baghdad_International_Airport +-- * AIRBASE.Iraq.Balad_Airbase +-- * AIRBASE.Iraq.Bashur_Airport +-- * AIRBASE.Iraq.Erbil_International_Airport +-- * AIRBASE.Iraq.Sulaimaniyah_International_Airport +-- * AIRBASE.Iraq.H2_Airbase +-- * AIRBASE.Iraq.H3_Main_Airbase +-- * AIRBASE.Iraq.H3_Northwest_Airbase +-- * AIRBASE.Iraq.H3_Southwest_Airbase -- * AIRBASE.Iraq.K1_Base +-- * AIRBASE.Iraq.Kirkuk_International_Airport +-- * AIRBASE.Iraq.Mosul_International_Airport +-- * AIRBASE.Iraq.Qayyarah_Airfield_West +-- * AIRBASE.Iraq.Sulaimaniyah_International_Airport -- -- @field Iraq AIRBASE.Iraq = { - ["Baghdad_International_Airport"] = "Baghdad International Airport", - ["Sulaimaniyah_International_Airport"] = "Sulaimaniyah International Airport", - ["Al_Sahra_Airport"] = "Al-Sahra Airport", - ["Erbil_International_Airport"] = "Erbil International Airport", - ["Al_Taji_Airport"] = "Al-Taji Airport", ["Al_Asad_Airbase"] = "Al-Asad Airbase", + ["Al_Kut_Airport"] = "Al-Kut Airport", + ["Al_Sahra_Airport"] = "Al-Sahra Airport", ["Al_Salam_Airbase"] = "Al-Salam Airbase", - ["Balad_Airbase"] = "Balad Airbase", - ["Kirkuk_International_Airport"] = "Kirkuk International Airport", - ["Bashur_Airport"] = "Bashur Airport", + ["Al_Taji_Airport"] = "Al-Taji Airport", ["Al_Taquddum_Airport"] = "Al-Taquddum Airport", - ["Qayyarah_Airfield_West"] = "Qayyarah Airfield West", + ["Baghdad_International_Airport"] = "Baghdad International Airport", + ["Balad_Airbase"] = "Balad Airbase", + ["Bashur_Airport"] = "Bashur Airport", + ["Erbil_International_Airport"] = "Erbil International Airport", + ["H2_Airbase"] = "H-2 Airbase", + ["H3_Main_Airbase"] = "H-3 Main Airbase", + ["H3_Northwest_Airbase"] = "H-3 Northwest Airbase", + ["H3_Southwest_Airbase"] = "H-3 Southwest Airbase", ["K1_Base"] = "K1 Base", + ["Kirkuk_International_Airport"] = "Kirkuk International Airport", + ["Mosul_International_Airport"] = "Mosul International Airport", + ["Qayyarah_Airfield_West"] = "Qayyarah Airfield West", + ["Sulaimaniyah_International_Airport"] = "Sulaimaniyah International Airport", } + --- Airbases of the Germany Cold War map -- * AIRBASE.GermanyCW.Airracing_Frankfurt -- * AIRBASE.GermanyCW.Airracing_Frankfurt @@ -1467,7 +1491,7 @@ function AIRBASE:Register(AirbaseName) self.descriptors=self:GetDesc() -- Debug info. - --self:I({airbase=AirbaseName, descriptors=self.descriptors}) + --self:T({airbase=AirbaseName, descriptors=self.descriptors}) -- Category. self.category=self.descriptors and self.descriptors.category or Airbase.Category.AIRDROME @@ -2634,6 +2658,7 @@ function AIRBASE:_InitRunways(IncludeInverse) --runway.name=string.format("%02d", tonumber(name)) runway.magheading=tonumber(runway.name)*10 + runway.idx=runway.magheading runway.heading=heading runway.width=width or 0 runway.length=length or 0 @@ -2946,6 +2971,7 @@ function AIRBASE:GetRunwayData(magvar, mark) local runway={} --#AIRBASE.Runway runway.heading=hdg runway.idx=idx + runway.magheading=idx runway.length=c1:Get2DDistance(c2) runway.position=c1 runway.endpoint=c2 @@ -2961,6 +2987,57 @@ function AIRBASE:GetRunwayData(magvar, mark) -- Add runway. table.insert(runways, runway) + end + + -- Look for identical (parallel) runways, e.g. 03L and 03R at Nellis. + local rpairs={} + for i,_ri in pairs(runways) do + local ri=_ri --#AIRBASE.Runway + for j,_rj in pairs(runways) do + local rj=_rj --#AIRBASE.Runway + if i 0 + return ((b.z - a.z)*(c.x - a.x) - (b.x - a.x)*(c.z - a.z)) > 0 + end + + for i,j in pairs(rpairs) do + local ri=runways[i] --#AIRBASE.Runway + local rj=runways[j] --#AIRBASE.Runway + + -- Draw arrow. + --ri.center:ArrowToAll(rj.center) + + local c0=ri.position + + -- Vector in the direction of the runway. + local a=UTILS.VecTranslate(c0, 1000, ri.heading) + + -- Vector from runway i to runway j. + local b=UTILS.VecSubstract(rj.position, ri.position) + b=UTILS.VecAdd(ri.position, b) + + -- Check if rj is left of ri. + local left=isLeft(c0, a, b) + + --env.info(string.format("Found pair %s: i=%d, j=%d, left==%s", ri.name, i, j, tostring(left))) + + if left then + ri.isLeft=false + rj.isLeft=true + else + ri.isLeft=true + rj.isLeft=false + end + + --break end return runways diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 689d841a5..20f35ee09 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -168,16 +168,25 @@ -- * @{#CONTROLLABLE.OptionAlarmStateGreen} -- * @{#CONTROLLABLE.OptionAlarmStateRed} -- --- ## 5.4) Jettison weapons: +-- ## 5.4) [AIR] Jettison weapons: -- -- * @{#CONTROLLABLE.OptionAllowJettisonWeaponsOnThreat} -- * @{#CONTROLLABLE.OptionKeepWeaponsOnThreat} -- --- ## 5.5) Air-2-Air missile attack range: +-- ## 5.5) [AIR] Air-2-Air missile attack range: -- * @{#CONTROLLABLE.OptionAAAttackRange}(): Defines the usage of A2A missiles against possible targets. -- -- # 6) [GROUND] IR Maker Beacons for GROUPs and UNITs -- * @{#CONTROLLABLE:NewIRMarker}(): Create a blinking IR Marker on a GROUP or UNIT. +-- +-- # 7) [HELICOPTER] Units prefer vertical landing and takeoffs: +-- * @{#CONTROLLABLE.OptionPreferVerticalLanding}(): Set aircraft to prefer vertical landing and takeoff. +-- +-- # 8) [AIRCRAFT] Landing approach options +-- * @{#CONTROLLABLE.SetOptionLandingStraightIn}(): Landing approach straight in. +-- * @{#CONTROLLABLE.SetOptionLandingForcePair}(): Landing approach in pairs for groups > 1 unit. +-- * @{#CONTROLLABLE.SetOptionLandingRestrictPair}(): Landing approach single. +-- * @{#CONTROLLABLE.SetOptionLandingOverheadBreak}(): Landing approach overhead break. -- -- @field #CONTROLLABLE CONTROLLABLE = { @@ -1432,7 +1441,7 @@ end -- @param #number Speed The speed [m/s] flying when holding the position. -- @return #CONTROLLABLE self function CONTROLLABLE:TaskOrbitCircleAtVec2( Point, Altitude, Speed ) - self:F2( { self.ControllableName, Point, Altitude, Speed } ) + --self:F2( { self.ControllableName, Point, Altitude, Speed } ) local DCSTask = { id = 'Orbit', @@ -4203,6 +4212,50 @@ function CONTROLLABLE:OptionEngageRange( EngageRange ) return nil end +--- [AIR] Set how the AI lands on an airfield. Here: Straight in. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:SetOptionLandingStraightIn() + self:F2( { self.ControllableName } ) + if self:IsAir() then + self:SetOption("36","0") + end + return self +end + +--- [AIR] Set how the AI lands on an airfield. Here: In pairs (if > 1 aircraft in group) +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:SetOptionLandingForcePair() + self:F2( { self.ControllableName } ) + if self:IsAir() then + self:SetOption("36","1") + end + return self +end + +--- [AIR] Set how the AI lands on an airfield. Here: No landing in pairs. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:SetOptionLandingRestrictPair() + self:F2( { self.ControllableName } ) + if self:IsAir() then + self:SetOption("36","2") + end + return self +end + +--- [AIR] Set how the AI lands on an airfield. Here: Overhead break. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:SetOptionLandingOverheadBreak() + self:F2( { self.ControllableName } ) + if self:IsAir() then + self:SetOption("36","3") + end + return self +end + --- [AIR] Set how the AI uses the onboard radar. -- @param #CONTROLLABLE self -- @param #number Option Options are: `NEVER = 0, FOR_ATTACK_ONLY = 1,FOR_SEARCH_IF_REQUIRED = 2, FOR_CONTINUOUS_SEARCH = 3` diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index 89ff5f7c3..1c38db672 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -231,6 +231,7 @@ GROUP.Attribute = { GROUND_AAA="Ground_AAA", GROUND_SAM="Ground_SAM", GROUND_SHORAD="Ground_SHORAD", + GROUND_BALLISTICMISSILE="Ground_BallisticMissile", GROUND_OTHER="Ground_OtherGround", NAVAL_AIRCRAFTCARRIER="Naval_AircraftCarrier", NAVAL_WARSHIP="Naval_WarShip", @@ -2231,6 +2232,10 @@ function GROUP:Respawn( Template, Reset ) --UTILS.PrintTableToLog(Template) + if self.ValidateAndRepositionGroundUnits then + UTILS.ValidateAndRepositionGroundUnits(Template.units) + end + -- Spawn new group. self:ScheduleOnce(0.1,_DATABASE.Spawn,_DATABASE,Template) --_DATABASE:Spawn(Template) @@ -2639,6 +2644,8 @@ function GROUP:GetAttribute() local artillery=self:HasAttribute("Artillery") local tank=self:HasAttribute("Old Tanks") or self:HasAttribute("Modern Tanks") or self:HasAttribute("Tanks") local aaa=self:HasAttribute("AAA") and (not self:HasAttribute("SAM elements")) + local ballisticMissile=artillery and self:HasAttribute("SS_missile") + local shorad=self:HasAttribute("SR SAM") local ewr=self:HasAttribute("EWR") local ifv=self:HasAttribute("IFV") local sam=self:HasAttribute("SAM elements") or self:HasAttribute("Optical Tracker") @@ -2680,6 +2687,8 @@ function GROUP:GetAttribute() attribute=GROUP.Attribute.GROUND_SAM elseif aaa then attribute=GROUP.Attribute.GROUND_AAA + elseif artillery and ballisticMissile then + attribute=GROUP.Attribute.GROUND_BALLISTICMISSILE elseif artillery then attribute=GROUP.Attribute.GROUND_ARTILLERY elseif tank then @@ -3192,3 +3201,60 @@ function GROUP:IsAAA() end return isAAA end + +--- This function uses Disposition and other fallback logic to find better ground positions for ground units. +--- NOTE: This is not a spawn randomizer. +--- It will try to find clear ground locations avoiding trees, water, roads, runways, map scenery, statics and other units in the area and modifies the provided positions table. +--- Maintains the original layout and unit positions as close as possible by searching for the next closest valid position to each unit. +--- Uses UTILS.ValidateAndRepositionGroundUnits. +-- @param #GROUP self +-- @param #boolean Enabled Enable/disable the feature. +function GROUP:SetValidateAndRepositionGroundUnits(Enabled) + self.ValidateAndRepositionGroundUnits = Enabled +end + + +--- Get the bounding box of the group combining UNIT:GetBoundingBox() units. +-- @param #GROUP self +-- @return DCS#Box3 The bounding box of the GROUP. +-- @return #nil The GROUP does not have any alive units. +function GROUP:GetBoundingBox() + local bbox = { min = { x = math.huge, y = math.huge, z = math.huge }, + max = { x = -math.huge, y = -math.huge, z = -math.huge } + } + + local Units = self:GetUnits() or {} + if #Units == 0 then + return nil + end + + for _, unit in pairs(Units) do + if unit and unit:IsAlive() then + local ubox = unit:GetBoundingBox() + + if ubox then + if ubox.min.x < bbox.min.x then + bbox.min.x = ubox.min.x + end + if ubox.min.y < bbox.min.y then + bbox.min.y = ubox.min.y + end + if ubox.min.z < bbox.min.z then + bbox.min.z = ubox.min.z + end + + if ubox.max.x > bbox.max.x then + bbox.max.x = ubox.max.x + end + if ubox.max.y > bbox.max.y then + bbox.max.y = ubox.max.y + end + if ubox.max.z > bbox.max.z then + bbox.max.z = ubox.max.z + end + end + end + end + + return bbox +end diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index 65b145384..038eefe56 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -361,15 +361,17 @@ function POSITIONABLE:GetCoord() -- Get the current position. local PositionableVec3 = self:GetVec3() - if self.coordinate then - -- Update COORDINATE from 3D vector. - self.coordinate:UpdateFromVec3( PositionableVec3 ) - else - -- New COORDINATE. - self.coordinate = COORDINATE:NewFromVec3( PositionableVec3 ) - end + if PositionableVec3 then + if self.coordinate then + -- Update COORDINATE from 3D vector. + self.coordinate:UpdateFromVec3( PositionableVec3 ) + else + -- New COORDINATE. + self.coordinate = COORDINATE:NewFromVec3( PositionableVec3 ) + end - return self.coordinate + return self.coordinate + end end -- Error message. diff --git a/Moose Development/Moose/Wrapper/Storage.lua b/Moose Development/Moose/Wrapper/Storage.lua index 4b445613c..5b5c23b3d 100644 --- a/Moose Development/Moose/Wrapper/Storage.lua +++ b/Moose Development/Moose/Wrapper/Storage.lua @@ -753,7 +753,7 @@ function STORAGE:LoadFromFile(Path,Filename) end end else - self:E("File for Liquids could not be found: "..tostring(Path).."\\"..tostring(Filename"_Liquids.csv")) + self:E("File for Liquids could not be found: "..tostring(Path).."\\"..tostring(Filename).."_Liquids.csv") end end @@ -773,7 +773,7 @@ function STORAGE:LoadFromFile(Path,Filename) end end else - self:E("File for Aircraft could not be found: "..tostring(Path).."\\"..tostring(Filename"_Aircraft.csv")) + self:E("File for Aircraft could not be found: "..tostring(Path).."\\"..tostring(Filename).."_Aircraft.csv") end end @@ -805,7 +805,7 @@ function STORAGE:LoadFromFile(Path,Filename) end end else - self:E("File for Weapons could not be found: "..tostring(Path).."\\"..tostring(Filename"_Weapons.csv")) + self:E("File for Weapons could not be found: "..tostring(Path).."\\"..tostring(Filename).."_Weapons.csv") end end diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index 20ce4837d..9d2ab5cfd 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -377,6 +377,10 @@ function UNIT:ReSpawnAt(Coordinate, Heading) --self:T( SpawnGroupTemplate ) + if self.ValidateAndRepositionGroundUnits then + UTILS.ValidateAndRepositionGroundUnits(SpawnGroupTemplate.units) + end + _DATABASE:Spawn(SpawnGroupTemplate) end @@ -1931,3 +1935,21 @@ end function UNIT:SetLife(Percent) net.dostring_in("mission",string.format("a_unit_set_life_percentage(%d, %f)", self:GetID(), Percent)) end + +--- Set the carrier illumination mode. -2: OFF, -1: AUTO, 0: NAVIGATION, 1: AC LAUNCH, 2: AC RECOVERY +-- @param #UNIT self +-- @param #number Mode Illumination mode, can be -2: OFF, -1: AUTO, 0: NAVIGATION, 1: AC LAUNCH, 2: AC RECOVERY +function UNIT:SetCarrierIlluminationMode(Mode) + UTILS.SetCarrierIlluminationMode(self:GetID(), Mode) +end + +--- This function uses Disposition and other fallback logic to find better ground positions for ground units. +--- NOTE: This is not a spawn randomizer. +--- It will try to find clear ground locations avoiding trees, water, roads, runways, map scenery, statics and other units in the area and modifies the provided positions table. +--- Maintains the original layout and unit positions as close as possible by searching for the next closest valid position to each unit. +--- Uses UTILS.ValidateAndRepositionGroundUnits. +-- @param #UNIT self +-- @param #boolean Enabled Enable/disable the feature. +function UNIT:SetValidateAndRepositionGroundUnits(Enabled) + self.ValidateAndRepositionGroundUnits = Enabled +end