diff --git a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua index 889ebe631..d0d1a940a 100644 --- a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua @@ -3940,11 +3940,7 @@ do -- -- # Demo Missions -- - -- ### [AI\_A2A\_GCICAP for Caucasus](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-200%20-%20AI_A2A%20-%20GCICAP%20Demonstration) - -- ### [AI\_A2A\_GCICAP for NTTR](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-210%20-%20NTTR%20AI_A2A_GCICAP%20Demonstration) - -- ### [AI\_A2A\_GCICAP for Normandy](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-220%20-%20NORMANDY%20AI_A2A_GCICAP%20Demonstration) - -- - -- ### [AI\_A2A\_GCICAP for beta testers](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching) + -- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching/AID-A2A%20-%20AI%20A2A%20Dispatching) -- -- === -- diff --git a/Moose Development/Moose/Core/Condition.lua b/Moose Development/Moose/Core/Condition.lua index 8d9824a72..7e9842a7b 100644 --- a/Moose Development/Moose/Core/Condition.lua +++ b/Moose Development/Moose/Core/Condition.lua @@ -23,11 +23,15 @@ -- @type CONDITION -- @field #string ClassName Name of the class. -- @field #string lid Class id string for output to DCS log file. +-- @field #string name Name of the condition. -- @field #boolean isAny General functions are evaluated as any condition. --- @field #boolean negateResult Negeate result of evaluation. +-- @field #boolean negateResult Negate result of evaluation. +-- @field #boolean noneResult Boolean that is returned if no condition functions at all were specified. -- @field #table functionsGen General condition functions. -- @field #table functionsAny Any condition functions. -- @field #table functionsAll All condition functions. +-- @field #number functionCounter Running number to determine the unique ID of condition functions. +-- @field #boolean defaultPersist Default persistence of condition functions. -- -- @extends Core.Base#BASE @@ -41,27 +45,34 @@ -- -- @field #CONDITION CONDITION = { - ClassName = "CONDITION", - lid = nil, - functionsGen = {}, - functionsAny = {}, - functionsAll = {}, + ClassName = "CONDITION", + lid = nil, + functionsGen = {}, + functionsAny = {}, + functionsAll = {}, + functionCounter = 0, + defaultPersist = false, } --- Condition function. -- @type CONDITION.Function --- @field #function func Callback function to check for a condition. Should return a `#boolean`. +-- @field #number uid Unique ID of the condition function. +-- @field #string type Type of the condition function: "gen", "any", "all". +-- @field #boolean persistence If `true`, this is persistent. +-- @field #function func Callback function to check for a condition. Must return a `#boolean`. -- @field #table arg (Optional) Arguments passed to the condition callback function if any. --- CONDITION class version. -- @field #string version -CONDITION.version="0.1.0" +CONDITION.version="0.3.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Make FSM. +-- TODO: Make FSM. No sure if really necessary. +-- DONE: Option to remove condition functions. +-- DONE: Persistence option for condition functions. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor @@ -78,6 +89,8 @@ function CONDITION:New(Name) self.name=Name or "Condition X" + self:SetNoneResult(false) + self.lid=string.format("%s | ", self.name) return self @@ -101,6 +114,28 @@ function CONDITION:SetNegateResult(Negate) return self end +--- Set whether `true` or `false` is returned, if no conditions at all were specified. By default `false` is returned. +-- @param #CONDITION self +-- @param #boolean ReturnValue Returns this boolean. +-- @return #CONDITION self +function CONDITION:SetNoneResult(ReturnValue) + if not ReturnValue then + self.noneResult=false + else + self.noneResult=true + end + return self +end + +--- Set whether condition functions are persistent, *i.e.* are removed. +-- @param #CONDITION self +-- @param #boolean IsPersistent If `true`, condition functions are persistent. +-- @return #CONDITION self +function CONDITION:SetDefaultPersistence(IsPersistent) + self.defaultPersist=IsPersistent + return self +end + --- Add a function that is evaluated. It must return a `#boolean` value, *i.e.* either `true` or `false` (or `nil`). -- @param #CONDITION self -- @param #function Function The function to call. @@ -113,47 +148,109 @@ end -- -- myCondition:AddFunction(isAequalB, a, b) -- --- @return #CONDITION self +-- @return #CONDITION.Function Condition function table. function CONDITION:AddFunction(Function, ...) -- Condition function. - local condition=self:_CreateCondition(Function, ...) + local condition=self:_CreateCondition(0, Function, ...) -- Add to table. table.insert(self.functionsGen, condition) - return self + return condition end --- Add a function that is evaluated. It must return a `#boolean` value, *i.e.* either `true` or `false` (or `nil`). -- @param #CONDITION self -- @param #function Function The function to call. -- @param ... (Optional) Parameters passed to the function (if any). --- @return #CONDITION self +-- @return #CONDITION.Function Condition function table. function CONDITION:AddFunctionAny(Function, ...) -- Condition function. - local condition=self:_CreateCondition(Function, ...) + local condition=self:_CreateCondition(1, Function, ...) -- Add to table. table.insert(self.functionsAny, condition) - return self + return condition end --- Add a function that is evaluated. It must return a `#boolean` value, *i.e.* either `true` or `false` (or `nil`). -- @param #CONDITION self -- @param #function Function The function to call. -- @param ... (Optional) Parameters passed to the function (if any). --- @return #CONDITION self +-- @return #CONDITION.Function Condition function table. function CONDITION:AddFunctionAll(Function, ...) -- Condition function. - local condition=self:_CreateCondition(Function, ...) + local condition=self:_CreateCondition(2, Function, ...) -- Add to table. table.insert(self.functionsAll, condition) + return condition +end + +--- Remove a condition function. +-- @param #CONDITION self +-- @param #CONDITION.Function ConditionFunction The condition function to be removed. +-- @return #CONDITION self +function CONDITION:RemoveFunction(ConditionFunction) + + if ConditionFunction then + + local data=nil + if ConditionFunction.type==0 then + data=self.functionsGen + elseif ConditionFunction.type==1 then + data=self.functionsAny + elseif ConditionFunction.type==2 then + data=self.functionsAll + end + + if data then + for i=#data,1,-1 do + local cf=data[i] --#CONDITION.Function + if cf.uid==ConditionFunction.uid then + self:T(self.lid..string.format("Removed ConditionFunction UID=%d", cf.uid)) + table.remove(data, i) + return self + end + end + end + + end + + return self +end + +--- Remove all non-persistant condition functions. +-- @param #CONDITION self +-- @return #CONDITION self +function CONDITION:RemoveNonPersistant() + + for i=#self.functionsGen,1,-1 do + local cf=self.functionsGen[i] --#CONDITION.Function + if not cf.persistence then + table.remove(self.functionsGen, i) + end + end + + for i=#self.functionsAll,1,-1 do + local cf=self.functionsAll[i] --#CONDITION.Function + if not cf.persistence then + table.remove(self.functionsAll, i) + end + end + + for i=#self.functionsAny,1,-1 do + local cf=self.functionsAny[i] --#CONDITION.Function + if not cf.persistence then + table.remove(self.functionsAny, i) + end + end + return self end @@ -166,11 +263,7 @@ function CONDITION:Evaluate(AnyTrue) -- Check if at least one function was given. if #self.functionsAll + #self.functionsAny + #self.functionsAll == 0 then - if self.negateResult then - return true - else - return false - end + return self.noneResult end -- Any condition for gen. @@ -206,6 +299,10 @@ function CONDITION:Evaluate(AnyTrue) return result end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Private Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + --- Check if all given condition are true. -- @param #CONDITION self -- @param #table functions Functions to evaluate. @@ -275,13 +372,20 @@ end --- Create conditon function object. -- @param #CONDITION self +-- @param #number Ftype Function type: 0=Gen, 1=All, 2=Any. -- @param #function Function The function to call. -- @param ... (Optional) Parameters passed to the function (if any). -- @return #CONDITION.Function Condition function. -function CONDITION:_CreateCondition(Function, ...) +function CONDITION:_CreateCondition(Ftype, Function, ...) + + -- Increase counter. + self.functionCounter=self.functionCounter+1 local condition={} --#CONDITION.Function + condition.uid=self.functionCounter + condition.type=Ftype or 0 + condition.persistence=self.defaultPersist condition.func=Function condition.arg={} if arg then @@ -290,6 +394,71 @@ function CONDITION:_CreateCondition(Function, ...) return condition end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Global Condition Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Condition to check if time is greater than a given threshold time. +-- @param #number Time Time in seconds. +-- @param #boolean Absolute If `true`, abs. mission time from `timer.getAbsTime()` is checked. Default is relative mission time from `timer.getTime()`. +-- @return #boolean Returns `true` if time is greater than give the time. +function CONDITION.IsTimeGreater(Time, Absolute) + + local Tnow=nil + + if Absolute then + Tnow=timer.getAbsTime() + else + Tnow=timer.getTime() + end + + if Tnow>Time then + return true + else + return false + end + + return nil +end + +--- Function that returns `true` (success) with a certain probability. For example, if you specify `Probability=80` there is an 80% chance that `true` is returned. +-- Technically, a random number between 0 and 100 is created. If the given success probability is less then this number, `true` is returned. +-- @param #number Probability Success probability in percent. Default 50 %. +-- @return #boolean Returns `true` for success and `false` otherwise. +function CONDITION.IsRandomSuccess(Probability) + + Probability=Probability or 50 + + -- Create some randomness. + math.random() + math.random() + math.random() + + -- Number between 0 and 100. + local N=math.random()*100 + + if Nradius then + radius=r + end + + end + + local zone=ZONE_RADIUS:New(ZoneName or self.ZoneName, center, radius, DoNotRegisterZone) + + return zone +end + + +--- Get the smallest rectangular zone encompassing all points points of the polygon zone. +-- @param #ZONE_POLYGON_BASE self +-- @param #string ZoneName (Optional) Name of the zone. Default is the name of the polygon zone. +-- @param #boolean DoNotRegisterZone (Optional) If `true`, zone is not registered. +-- @return #ZONE_POLYGON The rectangular zone. +function ZONE_POLYGON_BASE:GetZoneQuad(ZoneName, DoNotRegisterZone) + + local vec1, vec3=self:GetBoundingVec2() + + local vec2={x=vec1.x, y=vec3.y} + local vec4={x=vec3.x, y=vec1.y} + + local zone=ZONE_POLYGON_BASE:New(ZoneName or self.ZoneName, {vec1, vec2, vec3, vec4}) + + return zone +end + --- Smokes the zone boundaries in a color. -- @param #ZONE_POLYGON_BASE self -- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. @@ -2286,6 +2332,32 @@ function ZONE_POLYGON_BASE:GetBoundingSquare() return { x1 = x1, y1 = y1, x2 = x2, y2 = y2 } end +--- Get the bounding 2D vectors of the polygon. +-- @param #ZONE_POLYGON_BASE self +-- @return DCS#Vec2 Coordinates of western-southern-lower vertex of the box. +-- @return DCS#Vec2 Coordinates of eastern-northern-upper vertex of the box. +function ZONE_POLYGON_BASE:GetBoundingVec2() + + local x1 = self._.Polygon[1].x + local y1 = self._.Polygon[1].y + local x2 = self._.Polygon[1].x + local y2 = self._.Polygon[1].y + + for i = 2, #self._.Polygon do + self:T2( { self._.Polygon[i], x1, y1, x2, y2 } ) + x1 = ( x1 > self._.Polygon[i].x ) and self._.Polygon[i].x or x1 + x2 = ( x2 < self._.Polygon[i].x ) and self._.Polygon[i].x or x2 + y1 = ( y1 > self._.Polygon[i].y ) and self._.Polygon[i].y or y1 + y2 = ( y2 < self._.Polygon[i].y ) and self._.Polygon[i].y or y2 + + end + + local vec1={x=x1, y=y1} + local vec2={x=x2, y=y2} + + return vec1, vec2 +end + --- Draw a frontier on the F10 map with small filled circles. -- @param #ZONE_POLYGON_BASE self -- @param #number Coalition (Optional) Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1= All. diff --git a/Moose Development/Moose/DCS.lua b/Moose Development/Moose/DCS.lua index bc5a06de1..4db719cdb 100644 --- a/Moose Development/Moose/DCS.lua +++ b/Moose Development/Moose/DCS.lua @@ -341,9 +341,23 @@ do -- coalition -- @field RED -- @field BLUE - --- @function [parent=#coalition] getCountryCoalition - -- @param #number countryId - -- @return #number coalitionId + --- Get country coalition. + -- @function [parent=#coalition] getCountryCoalition + -- @param #number countryId Country ID. + -- @return #number coalitionId Coalition ID. + + --- Dynamically spawns a group. See [hoggit](https://wiki.hoggitworld.com/view/DCS_func_addGroup) + -- @function [parent=#coalition] addGroup + -- @param #number countryId Id of the country. + -- @param #number groupCategory Group category. Set -1 for spawning FARPS. + -- @param #table groupData Group data table. + -- @return DCS#Group The spawned Group object. + + --- Dynamically spawns a static object. See [hoggit](https://wiki.hoggitworld.com/view/DCS_func_addGroup) + -- @function [parent=#coalition] addStaticObject + -- @param #number countryId Id of the country. + -- @param #table groupData Group data table. + -- @return DCS#Static The spawned static object. coalition = {} -- #coalition @@ -1294,6 +1308,42 @@ do -- Group end -- Group +do -- StaticObject + + --- Represents a static object. + -- @type StaticObject + -- @extends DCS#Object + + --- Returns the static object. + -- @function [parent=#StaticObject] getByName + -- @param #string name Name of the static object. + -- @return #StaticObject + + StaticObject = {} --#StaticObject + +end + +do --Event + + --- Event structure. Note that present fields depend on type of event. + -- @type Event + -- @field #number id Event ID. + -- @field #number time Mission time in seconds. + -- @field DCS#Unit initiator Unit initiating the event. + -- @field DCS#Unit target Target unit. + -- @field DCS#Airbase place Airbase. + -- @field number subPlace Subplace. Unknown and often just 0. + -- @field #string weapon_name Weapoin name. + -- @field #number idx Mark ID. + -- @field #number coalition Coalition ID. + -- @field #number groupID Group ID, *e.g.* of group that added mark point. + -- @field #string text Text, *e.g.* of mark point. + -- @field DCS#Vec3 pos Position vector, *e.g.* of mark point. + -- @field #string comment Comment, *e.g.* LSO score. + + Event={} --#Event + +end do -- AI diff --git a/Moose Development/Moose/Functional/PseudoATC.lua b/Moose Development/Moose/Functional/PseudoATC.lua index 0cf76281f..5ee714705 100644 --- a/Moose Development/Moose/Functional/PseudoATC.lua +++ b/Moose Development/Moose/Functional/PseudoATC.lua @@ -45,6 +45,7 @@ -- @field #number talt Interval in seconds between reporting altitude until touchdown. Default 3 sec. -- @field #boolean chatty Display some messages on events like take-off and touchdown. -- @field #boolean eventsmoose If true, events are handled by MOOSE. If false, events are handled directly by DCS eventhandler. +-- @field #boolean reportplayername If true, use playername not callsign on callouts -- @extends Core.Base#BASE --- Adds some rudimentary ATC functionality via the radio menu. @@ -88,6 +89,7 @@ PSEUDOATC={ talt=3, chatty=true, eventsmoose=true, + reportplayername = false, } ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -98,7 +100,7 @@ PSEUDOATC.id="PseudoATC | " --- PSEUDOATC version. -- @field #number version -PSEUDOATC.version="0.9.2" +PSEUDOATC.version="0.9.5" ----------------------------------------------------------------------------------------------------------------------------------------- @@ -183,6 +185,13 @@ function PSEUDOATC:SetMessageDuration(duration) self.mdur=duration or 30 end +--- Use player name, not call sign, in callouts +-- @param #PSEUDOATC self +function PSEUDOATC:SetReportPlayername() + self.reportplayername = true + return self +end + --- Set time interval after which the F10 radio menu is refreshed. -- @param #PSEUDOATC self -- @param #number interval Interval in seconds. Default is every 120 sec. @@ -441,14 +450,18 @@ function PSEUDOATC:PlayerLanded(unit, place) local group=unit:GetGroup() local GID=group:GetID() local UID=unit:GetDCSObject():getID() - local PlayerName=self.group[GID].player[UID].playername - local UnitName=self.group[GID].player[UID].unitname - local GroupName=self.group[GID].player[UID].groupname - - -- Debug message. - local text=string.format("Player %s in unit %s of group %s (id=%d) landed at %s.", PlayerName, UnitName, GroupName, GID, place) - self:T(PSEUDOATC.id..text) - MESSAGE:New(text, 30):ToAllIf(self.Debug) + --local PlayerName=self.group[GID].player[UID].playername + --local UnitName=self.group[GID].player[UID].unitname + --local GroupName=self.group[GID].player[UID].groupname + local PlayerName = unit:GetPlayerName() or "Ghost" + local UnitName = unit:GetName() or "Ghostplane" + local GroupName = group:GetName() or "Ghostgroup" + if self.Debug then + -- Debug message. + local text=string.format("Player %s in unit %s of group %s landed at %s.", PlayerName, UnitName, GroupName, place) + self:T(PSEUDOATC.id..text) + MESSAGE:New(text, 30):ToAllIf(self.Debug) + end -- Stop altitude reporting timer if its activated. self:AltitudeTimerStop(GID,UID) @@ -470,21 +483,28 @@ function PSEUDOATC:PlayerTakeOff(unit, place) -- Gather some information. local group=unit:GetGroup() - local GID=group:GetID() - local UID=unit:GetDCSObject():getID() - local PlayerName=self.group[GID].player[UID].playername - local CallSign=self.group[GID].player[UID].callsign - local UnitName=self.group[GID].player[UID].unitname - local GroupName=self.group[GID].player[UID].groupname - - -- Debug message. - local text=string.format("Player %s in unit %s of group %s (id=%d) took off at %s.", PlayerName, UnitName, GroupName, GID, place) - self:T(PSEUDOATC.id..text) - MESSAGE:New(text, 30):ToAllIf(self.Debug) - + --local GID=group:GetID() + --local UID=unit:GetDCSObject():getID() + --local PlayerName=self.group[GID].player[UID].playername + --local CallSign=self.group[GID].player[UID].callsign + --local UnitName=self.group[GID].player[UID].unitname + --local GroupName=self.group[GID].player[UID].groupname + local PlayerName = unit:GetPlayerName() or "Ghost" + local UnitName = unit:GetName() or "Ghostplane" + local GroupName = group:GetName() or "Ghostgroup" + local CallSign = unit:GetCallsign() or "Ghost11" + if self.Debug then + -- Debug message. + local text=string.format("Player %s in unit %s of group %s took off at %s.", PlayerName, UnitName, GroupName, place) + self:T(PSEUDOATC.id..text) + MESSAGE:New(text, 30):ToAllIf(self.Debug) + end -- Bye-Bye message. if place and self.chatty then local text=string.format("%s, %s, you are airborne. Have a safe trip!", place, CallSign) + if self.reportplayername then + text=string.format("%s, %s, you are airborne. Have a safe trip!", place, PlayerName) + end MESSAGE:New(text, self.mdur):ToGroup(group) end @@ -501,7 +521,7 @@ function PSEUDOATC:PlayerLeft(unit) local GID=group:GetID() local UID=unit:GetDCSObject():getID() - if self.group[GID].player[UID] then + if self.group[GID] and self.group[GID].player and self.group[GID].player[UID] then local PlayerName=self.group[GID].player[UID].playername local CallSign=self.group[GID].player[UID].callsign local UnitName=self.group[GID].player[UID].unitname @@ -687,7 +707,9 @@ function PSEUDOATC:MenuWaypoints(GID, UID) -- Position of Waypoint local pos=COORDINATE:New(wp.x, wp.alt, wp.y) local name=string.format("Waypoint %d", i-1) - + if wp.name and wp.name ~= "" then + name = string.format("Waypoint %s",wp.name) + end -- "F10/PseudoATC/Waypoints/Waypoint X" local submenu=missionCommands.addSubMenuForGroup(GID, name, self.group[GID].player[UID].menu_waypoints) @@ -844,7 +866,8 @@ function PSEUDOATC:ReportHeight(GID, UID, dt, _clear) local position=unit:GetCoordinate() local height=get_AGL(position) local callsign=unit:GetCallsign() - + local PlayerName=self.group[GID].player[UID].playername + -- Settings. local settings=_DATABASE:GetPlayerSettings(self.group[GID].player[UID].playername) or _SETTINGS --Core.Settings#SETTINGS @@ -856,7 +879,9 @@ function PSEUDOATC:ReportHeight(GID, UID, dt, _clear) -- Message text. local _text=string.format("%s, your altitude is %s AGL.", callsign, Hs) - + if self.reportplayername then + _text=string.format("%s, your altitude is %s AGL.", PlayerName, Hs) + end -- Append flight level. if _clear==false then _text=_text..string.format(" FL%03d.", position.y/30.48) diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 0643cf01b..081132413 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -3492,7 +3492,7 @@ function RAT:Status(message, forID) local fuel=group:GetFuel()*100.0 local airborne=group:InAir() local coords=group:GetCoordinate() - local alt=coords.y + local alt=coords.y or 1000 --local vel=group:GetVelocityKMH() local departure=ratcraft.departure:GetName() local destination=ratcraft.destination:GetName() @@ -5671,6 +5671,9 @@ function RAT:_ATCClearForLanding(airport, flight) -- Debug message. local text1=string.format("ATC %s: Flight %s cleared for landing (flag=%d).", airport, flight, flagvalue) + if string.find(flight,"#") then + flight = string.match(flight,"^(.+)#") + end local text2=string.format("ATC %s: Flight %s you are cleared for landing.", airport, flight) BASE:T( RAT.id..text1) MESSAGE:New(text2, 10):ToAllIf(RAT.ATC.messages) @@ -5713,6 +5716,9 @@ function RAT:_ATCFlightLanded(name) local text1=string.format("ATC %s: Flight %s landed. Tholding = %i:%02d, Tfinal = %i:%02d.", dest, name, Thold/60, Thold%60, Tfinal/60, Tfinal%60) local text2=string.format("ATC %s: Number of flights still on final %d.", dest, RAT.ATC.airport[dest].Nonfinal) local text3=string.format("ATC %s: Traffic report: Number of planes landed in total %d. Flights/hour = %3.2f.", dest, RAT.ATC.airport[dest].traffic, TrafficPerHour) + if string.find(name,"#") then + name = string.match(name,"^(.+)#") + end local text4=string.format("ATC %s: Flight %s landed. Welcome to %s.", dest, name, dest) BASE:T(RAT.id..text1) BASE:T(RAT.id..text2) diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index 20d879ee0..c3425f26b 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -578,7 +578,7 @@ RANGE.MenuF10Root = nil --- Range script version. -- @field #string version -RANGE.version = "2.5.0" +RANGE.version = "2.5.1" -- TODO list: -- TODO: Verbosity level for messages. @@ -1207,13 +1207,18 @@ function RANGE:SetSRS(PathToSRS, Port, Coalition, Frequency, Modulation, Volume, self.controlmsrs:SetCoalition(Coalition or coalition.side.BLUE) self.controlmsrs:SetLabel("RANGEC") self.controlsrsQ = MSRSQUEUE:New("CONTROL") - + self.instructmsrs=MSRS:New(PathToSRS, Frequency or 305, Modulation or radio.modulation.AM, Volume or 1.0) self.instructmsrs:SetPort(Port) self.instructmsrs:SetCoalition(Coalition or coalition.side.BLUE) self.instructmsrs:SetLabel("RANGEI") self.instructsrsQ = MSRSQUEUE:New("INSTRUCT") + if PathToGoogleKey then + self.instructmsrs:SetGoogle(PathToGoogleKey) + self.instructmsrs:SetGoogle(PathToGoogleKey) + end + else self:E(self.lid..string.format("ERROR: No SRS path specified!")) end @@ -2570,7 +2575,7 @@ function RANGE:_DisplayMyStrafePitResults( _unitName ) self:F( _unitName ) -- Get player unit and name - local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) + local _unit, _playername, _multiplayer = self:_GetPlayerUnitAndName( _unitName ) if _unit and _playername then @@ -2622,7 +2627,7 @@ function RANGE:_DisplayMyStrafePitResults( _unitName ) end -- Send message to group. - self:_DisplayMessageToGroup( _unit, _message, nil, true, true ) + self:_DisplayMessageToGroup( _unit, _message, nil, true, true, _multiplayer ) end end @@ -2633,7 +2638,7 @@ function RANGE:_DisplayStrafePitResults( _unitName ) self:F( _unitName ) -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) + local _unit, _playername, _multiplayer = self:_GetPlayerUnitAndName( _unitName ) -- Check if we have a unit which is a player. if _unit and _playername then @@ -2680,7 +2685,7 @@ function RANGE:_DisplayStrafePitResults( _unitName ) end -- Send message. - self:_DisplayMessageToGroup( _unit, _message, nil, true, true ) + self:_DisplayMessageToGroup( _unit, _message, nil, true, true, _multiplayer ) end end @@ -2691,7 +2696,7 @@ function RANGE:_DisplayMyBombingResults( _unitName ) self:F( _unitName ) -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) + local _unit, _playername, _multiplayer = self:_GetPlayerUnitAndName( _unitName ) if _unit and _playername then @@ -2737,7 +2742,7 @@ function RANGE:_DisplayMyBombingResults( _unitName ) end -- Send message. - self:_DisplayMessageToGroup( _unit, _message, nil, true, true ) + self:_DisplayMessageToGroup( _unit, _message, nil, true, true, _multiplayer ) end end @@ -2751,7 +2756,7 @@ function RANGE:_DisplayBombingResults( _unitName ) local _playerResults = {} -- Get player unit and name. - local _unit, _player = self:_GetPlayerUnitAndName( _unitName ) + local _unit, _player, _multiplayer = self:_GetPlayerUnitAndName( _unitName ) -- Check if we have a unit with a player. if _unit and _player then @@ -2795,7 +2800,7 @@ function RANGE:_DisplayBombingResults( _unitName ) end -- Send message. - self:_DisplayMessageToGroup( _unit, _message, nil, true, true ) + self:_DisplayMessageToGroup( _unit, _message, nil, true, true, _multiplayer ) end end @@ -2806,7 +2811,7 @@ function RANGE:_DisplayRangeInfo( _unitname ) self:F( _unitname ) -- Get player unit and player name. - local unit, playername = self:_GetPlayerUnitAndName( _unitname ) + local unit, playername, _multiplayer = self:_GetPlayerUnitAndName( _unitname ) -- Check if we have a player. if unit and playername then @@ -2901,7 +2906,7 @@ function RANGE:_DisplayRangeInfo( _unitname ) text = text .. textdelay -- Send message to player group. - self:_DisplayMessageToGroup( unit, text, nil, true, true ) + self:_DisplayMessageToGroup( unit, text, nil, true, true, _multiplayer ) -- Debug output. self:T2( self.id .. text ) @@ -2916,7 +2921,7 @@ function RANGE:_DisplayBombTargets( _unitname ) self:F( _unitname ) -- Get player unit and player name. - local _unit, _playername = self:_GetPlayerUnitAndName( _unitname ) + local _unit, _playername, _multiplayer = self:_GetPlayerUnitAndName( _unitname ) -- Check if we have a player. if _unit and _playername then @@ -2948,7 +2953,7 @@ function RANGE:_DisplayBombTargets( _unitname ) end end - self:_DisplayMessageToGroup( _unit, _text, 120, true, true ) + self:_DisplayMessageToGroup( _unit, _text, 120, true, true, _multiplayer ) end end @@ -2959,7 +2964,7 @@ function RANGE:_DisplayStrafePits( _unitname ) self:F( _unitname ) -- Get player unit and player name. - local _unit, _playername = self:_GetPlayerUnitAndName( _unitname ) + local _unit, _playername, _multiplayer = self:_GetPlayerUnitAndName( _unitname ) -- Check if we have a player. if _unit and _playername then @@ -2988,7 +2993,7 @@ function RANGE:_DisplayStrafePits( _unitname ) _text = _text .. string.format( "\n- %s: heading %03d°\n%s", _strafepit.name, heading, mycoord ) end - self:_DisplayMessageToGroup( _unit, _text, nil, true, true ) + self:_DisplayMessageToGroup( _unit, _text, nil, true, true, _multiplayer ) end end @@ -2999,7 +3004,7 @@ function RANGE:_DisplayRangeWeather( _unitname ) self:F( _unitname ) -- Get player unit and player name. - local unit, playername = self:_GetPlayerUnitAndName( _unitname ) + local unit, playername, _multiplayer = self:_GetPlayerUnitAndName( _unitname ) -- Check if we have a player. if unit and playername then @@ -3048,7 +3053,7 @@ function RANGE:_DisplayRangeWeather( _unitname ) end -- Send message to player group. - self:_DisplayMessageToGroup( unit, text, nil, true, true ) + self:_DisplayMessageToGroup( unit, text, nil, true, true, _multiplayer ) -- Debug output. self:T2( self.id .. text ) @@ -3666,7 +3671,8 @@ end -- @param #number _time Duration how long the message is displayed. -- @param #boolean _clear Clear up old messages. -- @param #boolean display If true, display message regardless of player setting "Messages Off". -function RANGE:_DisplayMessageToGroup( _unit, _text, _time, _clear, display ) +-- @param #boolean _togroup If true, display the message to the group in any case +function RANGE:_DisplayMessageToGroup( _unit, _text, _time, _clear, display, _togroup ) self:F( { unit = _unit, text = _text, time = _time, clear = _clear } ) -- Defaults @@ -3694,8 +3700,13 @@ function RANGE:_DisplayMessageToGroup( _unit, _text, _time, _clear, display ) local playermessage = self.PlayerSettings[playername].messages -- Send message to player if messages enabled and not only for the examiner. + if _gid and (playermessage == true or display) and (not self.examinerexclusive) then - local m = MESSAGE:New(_text,_time,nil,_clear):ToUnit(_unit) + if _togroup and _grp then + local m = MESSAGE:New(_text,_time,nil,_clear):ToGroup(_grp) + else + local m = MESSAGE:New(_text,_time,nil,_clear):ToUnit(_unit) + end end -- Send message to examiner. @@ -3715,7 +3726,7 @@ end function RANGE:_SmokeBombImpactOnOff( unitname ) self:F( unitname ) - local unit, playername = self:_GetPlayerUnitAndName( unitname ) + local unit, playername, _multiplayer = self:_GetPlayerUnitAndName( unitname ) if unit and playername then local text if self.PlayerSettings[playername].smokebombimpact == true then @@ -3736,7 +3747,7 @@ end function RANGE:_SmokeBombDelayOnOff( unitname ) self:F( unitname ) - local unit, playername = self:_GetPlayerUnitAndName( unitname ) + local unit, playername, _multiplayer = self:_GetPlayerUnitAndName( unitname ) if unit and playername then local text if self.PlayerSettings[playername].delaysmoke == true then @@ -3757,7 +3768,7 @@ end function RANGE:_MessagesToPlayerOnOff( unitname ) self:F( unitname ) - local unit, playername = self:_GetPlayerUnitAndName( unitname ) + local unit, playername, _multiplayer = self:_GetPlayerUnitAndName( unitname ) if unit and playername then local text if self.PlayerSettings[playername].messages == true then @@ -3778,7 +3789,7 @@ function RANGE:_TargetsheetOnOff( _unitname ) self:F2( _unitname ) -- Get player unit and player name. - local unit, playername = self:_GetPlayerUnitAndName( _unitname ) + local unit, playername, _multiplayer = self:_GetPlayerUnitAndName( _unitname ) -- Check if we have a player. if unit and playername then @@ -3820,7 +3831,7 @@ end function RANGE:_FlareDirectHitsOnOff( unitname ) self:F( unitname ) - local unit, playername = self:_GetPlayerUnitAndName( unitname ) + local unit, playername, _multiplayer = self:_GetPlayerUnitAndName( unitname ) if unit and playername then local text if self.PlayerSettings[playername].flaredirecthits == true then @@ -4039,12 +4050,14 @@ end -- @param #string _unitName Name of the player unit. -- @return Wrapper.Unit#UNIT Unit of player. -- @return #string Name of the player. --- @return nil If player does not exist. +-- @return #boolean If true, group has > 1 player in it function RANGE:_GetPlayerUnitAndName( _unitName ) self:F2( _unitName ) if _unitName ~= nil then - + + local multiplayer = false + -- Get DCS unit from its name. local DCSunit = Unit.getByName( _unitName ) @@ -4056,7 +4069,11 @@ function RANGE:_GetPlayerUnitAndName( _unitName ) self:T2( { DCSunit = DCSunit, unit = unit, playername = playername } ) if DCSunit and unit and playername then self:F2(playername) - return unit, playername + local grp = unit:GetGroup() + if grp and grp:CountAliveUnits() > 1 then + multiplayer = true + end + return unit, playername, multiplayer end end @@ -4064,7 +4081,7 @@ function RANGE:_GetPlayerUnitAndName( _unitName ) end -- Return nil if we could not find a player. - return nil, nil + return nil, nil, nil end --- Returns a string which consists of the player name. diff --git a/Moose Development/Moose/Functional/Suppression.lua b/Moose Development/Moose/Functional/Suppression.lua index 1698f0c0a..8c6b88a93 100644 --- a/Moose Development/Moose/Functional/Suppression.lua +++ b/Moose Development/Moose/Functional/Suppression.lua @@ -85,6 +85,7 @@ -- @field #boolean eventmoose If true, events are handled by MOOSE. If false, events are handled directly by DCS eventhandler. Default true. -- @field Core.Zone#ZONE BattleZone -- @field #boolean AutoEngage +-- @field #table waypoints Waypoints of the group as defined in the ME. -- @extends Core.Fsm#FSM_CONTROLLABLE -- @@ -265,6 +266,7 @@ SUPPRESSION={ DefaultAlarmState = "Auto", DefaultROE = "Weapon Free", eventmoose = true, + waypoints = {}, } --- Enumerator of possible rules of engagement. @@ -295,7 +297,7 @@ SUPPRESSION.MenuF10=nil --- PSEUDOATC version. -- @field #number version -SUPPRESSION.version="0.9.3" +SUPPRESSION.version="0.9.4" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -309,7 +311,7 @@ SUPPRESSION.version="0.9.3" --- Creates a new AI_suppression object. -- @param #SUPPRESSION self -- @param Wrapper.Group#GROUP group The GROUP object for which suppression should be applied. --- @return #SUPPRESSION SUPPRESSION object or *nil* if group does not exist or is not a ground group. +-- @return #SUPPRESSION self function SUPPRESSION:New(group) -- Inherits from FSM_CONTROLLABLE @@ -320,7 +322,7 @@ function SUPPRESSION:New(group) self.lid=string.format("SUPPRESSION %s | ", tostring(group:GetName())) self:T(self.lid..string.format("SUPPRESSION version %s. Activating suppressive fire for group %s", SUPPRESSION.version, group:GetName())) else - self:E(self.lid.."SUPPRESSION | Requested group does not exist! (Has to be a MOOSE group.)") + self:E("SUPPRESSION | Requested group does not exist! (Has to be a MOOSE group)") return nil end @@ -1186,6 +1188,16 @@ function SUPPRESSION:onafterFightBack(Controllable, From, Event, To) -- Set ROE and alarm state back to default. self:_SetROE() self:_SetAlarmState() + + local group=Controllable --Wrapper.Group#GROUP + + local Waypoints = group:GetTemplateRoutePoints() + +-- env.info("FF waypoints",showMessageBox) +-- self:I(Waypoints) + + group:Route(Waypoints, 5) + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1251,7 +1263,7 @@ function SUPPRESSION:onafterFallBack(Controllable, From, Event, To, AttackUnit) self:_SetROE(SUPPRESSION.ROE.Hold) -- Set alarm state to GREEN and let the unit run away. - self:_SetAlarmState(SUPPRESSION.AlarmState.Green) + self:_SetAlarmState(SUPPRESSION.AlarmState.Auto) -- Make the group run away. self:_Run(Coord, self.Speed, self.Formation, self.FallbackWait) @@ -1537,7 +1549,7 @@ end -- @param #SUPPRESSION self -- @param Core.Event#EVENTDATA EventData function SUPPRESSION:_OnEventHit(EventData) - self:F(EventData) + self:F3(EventData) local GroupNameSelf=self.Controllable:GetName() local GroupNameTgt=EventData.TgtGroupName @@ -1676,15 +1688,15 @@ end function SUPPRESSION:_Run(fin, speed, formation, wait) speed=speed or 20 - formation=formation or "Off road" + formation=formation or ENUMS.Formation.Vehicle.OffRoad wait=wait or 30 - local group=self.Controllable -- Wrapper.Controllable#CONTROLLABLE + local group=self.Controllable -- Wrapper.Group#GROUP if group and group:IsAlive() then -- Clear all tasks. - group:ClearTasks() + --group:ClearTasks() -- Current coordinates of group. local ini=group:GetCoordinate() @@ -1694,57 +1706,18 @@ function SUPPRESSION:_Run(fin, speed, formation, wait) -- Heading from ini to fin. local heading=self:_Heading(ini, fin) - - -- Number of waypoints. - local nx - if dist <= 50 then - nx=2 - elseif dist <= 100 then - nx=3 - elseif dist <= 500 then - nx=4 - else - nx=5 - end - - -- Number of intermediate waypoints. - local dx=dist/(nx-1) - + -- Waypoint and task arrays. local wp={} local tasks={} -- First waypoint is the current position of the group. wp[1]=ini:WaypointGround(speed, formation) - tasks[1]=group:TaskFunction("SUPPRESSION._Passing_Waypoint", self, 1, false) if self.Debug then local MarkerID=ini:MarkToAll(string.format("Waypoing %d of group %s (initial)", #wp, self.Controllable:GetName())) end - self:T2(self.lid..string.format("Number of waypoints %d", nx)) - for i=1,nx-2 do - - local x=dx*i - local coord=ini:Translate(x, heading) - - wp[#wp+1]=coord:WaypointGround(speed, formation) - tasks[#tasks+1]=group:TaskFunction("SUPPRESSION._Passing_Waypoint", self, #wp, false) - - self:T2(self.lid..string.format("%d x = %4.1f", i, x)) - if self.Debug then - local MarkerID=coord:MarkToAll(string.format("Waypoing %d of group %s", #wp, self.Controllable:GetName())) - end - - end - self:T2(self.lid..string.format("Total distance: %4.1f", dist)) - - -- Final waypoint. - wp[#wp+1]=fin:WaypointGround(speed, formation) - if self.Debug then - local MarkerID=fin:MarkToAll(string.format("Waypoing %d of group %s (final)", #wp, self.Controllable:GetName())) - end - -- Task to hold. local ConditionWait=group:TaskCondition(nil, nil, nil, nil, wait, nil) local TaskHold = group:TaskHold() @@ -1753,25 +1726,15 @@ function SUPPRESSION:_Run(fin, speed, formation, wait) local TaskComboFin = {} TaskComboFin[#TaskComboFin+1] = group:TaskFunction("SUPPRESSION._Passing_Waypoint", self, #wp, true) TaskComboFin[#TaskComboFin+1] = group:TaskControlled(TaskHold, ConditionWait) - - -- Add final task. - tasks[#tasks+1]=group:TaskCombo(TaskComboFin) - - -- Original waypoints of the group. - local Waypoints = group:GetTemplateRoutePoints() - -- New points are added to the default route. - for i,p in ipairs(wp) do - table.insert(Waypoints, i, wp[i]) - end - - -- Set task for all waypoints. - for i,wp in ipairs(Waypoints) do - group:SetTaskWaypoint(Waypoints[i], tasks[i]) - end + -- Final waypoint. + wp[#wp+1]=fin:WaypointGround(speed, formation, TaskComboFin) + if self.Debug then + local MarkerID=fin:MarkToAll(string.format("Waypoing %d of group %s (final)", #wp, self.Controllable:GetName())) + end -- Submit task and route group along waypoints. - group:Route(Waypoints) + group:Route(wp) else self:E(self.lid..string.format("ERROR: Group is not alive!")) @@ -1790,7 +1753,7 @@ function SUPPRESSION._Passing_Waypoint(group, Fsm, i, final) local text=string.format("Group %s passing waypoint %d (final=%s)", group:GetName(), i, tostring(final)) MESSAGE:New(text,10):ToAllIf(Fsm.Debug) if Fsm.Debug then - env.info(self.lid..text) + env.info(Fsm.lid..text) end if final then @@ -1891,7 +1854,7 @@ function SUPPRESSION:_GetLife() local groupstrength=#units/self.IniGroupStrength*100 - self.T2(self.lid..string.format("Group %s _GetLife nunits = %d", self.Controllable:GetName(), #units)) + self:T2(self.lid..string.format("Group %s _GetLife nunits = %d", self.Controllable:GetName(), #units)) for _,unit in pairs(units) do diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 26cef4db9..8a1dfcd95 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -302,8 +302,8 @@ -- -- Initial Spawn states is as follows: -- GROUND: ROE, "Return Fire" Alarm, "Green" --- AIR: ROE, "Return Fire" Reaction to Threat, "Passive Defense" --- NAVAL ROE, "Return Fire" Alarm,"N/A" +-- AIR: ROE, "Return Fire" Reaction to Threat, "Passive Defense" +-- NAVAL ROE, "Return Fire" Alarm,"N/A" -- -- A request can be added by the @{#WAREHOUSE.AddRequest}(*warehouse*, *AssetDescriptor*, *AssetDescriptorValue*, *nAsset*, *TransportType*, *nTransport*, *Prio*, *Assignment*) function. -- The parameters are @@ -2647,6 +2647,13 @@ function WAREHOUSE:SetWarehouseZone(zone) return self end +--- Get the warehouse zone. +-- @param #WAREHOUSE self +-- @return Core.Zone#ZONE The warehouse zone. +function WAREHOUSE:GetWarehouseZone() + return self.zone +end + --- Set auto defence on. When the warehouse is under attack, all ground assets are spawned automatically and will defend the warehouse zone. -- @param #WAREHOUSE self -- @return #WAREHOUSE self @@ -5810,6 +5817,7 @@ function WAREHOUSE:_SpawnAssetRequest(Request) -- Now we try to find all parking spots for all cargo groups in advance. Due to the for loop, the parking spots do not get updated while spawning. local Parking={} if Request.cargocategory==Group.Category.AIRPLANE or Request.cargocategory==Group.Category.HELICOPTER then + --TODO: Check for airstart. Should be a request property. Parking=self:_FindParkingForAssets(self.airbase, cargoassets) or {} end @@ -6069,7 +6077,9 @@ function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrol end if self.Debug then - coord:MarkToAll(string.format("Spawnplace unit %s terminal %d.", unit.name, terminal)) + local text=string.format("Spawnplace unit %s terminal %d.", unit.name, terminal) + coord:MarkToAll(text) + env.info(text) end unit.x=coord.x @@ -7374,6 +7384,7 @@ function WAREHOUSE:_CheckRequestNow(request) local _transports local _assetattribute local _assetcategory + local _assetairstart=false -- Check if at least one (cargo) asset is available. if _nassets>0 then @@ -7381,21 +7392,28 @@ function WAREHOUSE:_CheckRequestNow(request) -- Get the attibute of the requested asset. _assetattribute=_assets[1].attribute _assetcategory=_assets[1].category + _assetairstart=_assets[1].takeoffType and _assets[1].takeoffType==COORDINATE.WaypointType.TurningPoint or false -- Check available parking for air asset units. if _assetcategory==Group.Category.AIRPLANE or _assetcategory==Group.Category.HELICOPTER then if self.airbase and self.airbase:GetCoalition()==self:GetCoalition() then - if self:IsRunwayOperational() then + if self:IsRunwayOperational() or _assetairstart then - local Parking=self:_FindParkingForAssets(self.airbase,_assets) - - --if Parking==nil and not (self.category==Airbase.Category.HELIPAD) then - if Parking==nil then - local text=string.format("Warehouse %s: Request denied! Not enough free parking spots for all requested assets at the moment.", self.alias) - self:_InfoMessage(text, 5) - return false + if _assetairstart then + -- Airstart no need to check parking + else + + -- Check parking. + local Parking=self:_FindParkingForAssets(self.airbase,_assets) + + -- No parking? + if Parking==nil then + local text=string.format("Warehouse %s: Request denied! Not enough free parking spots for all requested assets at the moment.", self.alias) + self:_InfoMessage(text, 5) + return false + end end else @@ -7969,93 +7987,123 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) -- Loop over all assets that need a parking psot. for _,asset in pairs(assets) do local _asset=asset --#WAREHOUSE.Assetitem - - -- Get terminal type of this asset - local terminaltype=asset.terminalType or self:_GetTerminal(asset.attribute, self:GetAirbaseCategory()) - - -- Asset specific parking. - parking[_asset.uid]={} - - -- Loop over all units - each one needs a spot. - for i=1,_asset.nunits do - -- Asset name - local assetname=_asset.spawngroupname.."-"..tostring(i) - - -- Loop over all parking spots. - local gotit=false - for _,_parkingspot in pairs(parkingdata) do - local parkingspot=_parkingspot --Wrapper.Airbase#AIRBASE.ParkingSpot - - -- Check correct terminal type for asset. We don't want helos in shelters etc. - if AIRBASE._CheckTerminalType(parkingspot.TerminalType, terminaltype) and self:_CheckParkingValid(parkingspot) and self:_CheckParkingAsset(parkingspot, asset) and airbase:_CheckParkingLists(parkingspot.TerminalID) then - - -- Coordinate of the parking spot. - local _spot=parkingspot.Coordinate -- Core.Point#COORDINATE - local _termid=parkingspot.TerminalID - local free=true - local problem=nil - - -- Loop over all obstacles. - for _,obstacle in pairs(obstacles) do - - -- Check if aircraft overlaps with any obstacle. - local dist=_spot:Get2DDistance(obstacle.coord) - local safe=_overlap(_asset.size, obstacle.size, dist) - - -- Spot is blocked. - if not safe then - self:T3(self.lid..string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is NOT SAFE", assetname, _asset.uid, _termid, dist)) - free=false - problem=obstacle - problem.dist=dist - break - else - --env.info(string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is SAFE", assetname, _asset.uid, _termid, dist)) - end - - end - - -- Check if spot is free - if free then - - -- Add parkingspot for this asset unit. - table.insert(parking[_asset.uid], parkingspot) - - -- Debug - self:T(self.lid..string.format("Parking spot %d is free for asset %s [id=%d]!", _termid, assetname, _asset.uid)) - - -- Add the unit as obstacle so that this spot will not be available for the next unit. - table.insert(obstacles, {coord=_spot, size=_asset.size, name=assetname, type="asset"}) - - gotit=true - break + if not _asset.spawned then + -- Get terminal type of this asset + local terminaltype=asset.terminalType or self:_GetTerminal(asset.attribute, self:GetAirbaseCategory()) + + -- Asset specific parking. + parking[_asset.uid]={} + + -- Loop over all units - each one needs a spot. + for i=1,_asset.nunits do + + -- Asset name + local assetname=_asset.spawngroupname.."-"..tostring(i) + + -- Loop over all parking spots. + local gotit=false + for _,_parkingspot in pairs(parkingdata) do + local parkingspot=_parkingspot --Wrapper.Airbase#AIRBASE.ParkingSpot + + -- Parking valid? + local valid=true + + if asset.parkingIDs then + -- If asset has assigned parking spots, we take these no matter what. + valid=self:_CheckParkingAsset(parkingspot, asset) else - - -- Debug output for occupied spots. - if self.Debug then - local coord=problem.coord --Core.Point#COORDINATE - local text=string.format("Obstacle %s [type=%s] blocking spot=%d! Size=%.1f m and distance=%.1f m.", problem.name, problem.type, _termid, problem.size, problem.dist) - self:I(self.lid..text) - coord:MarkToAll(string.format(text)) - else - self:T(self.lid..string.format("Parking spot %d is occupied or not big enough!", _termid)) - end - + + -- Valid terminal type depending on attribute. + local validTerminal=AIRBASE._CheckTerminalType(parkingspot.TerminalType, terminaltype) + + -- Valid parking list. + local validParking=self:_CheckParkingValid(parkingspot) + + -- Black and white list. + local validBWlist=airbase:_CheckParkingLists(parkingspot.TerminalID) + + -- Debug info. + --env.info(string.format("FF validTerminal = %s", tostring(validTerminal))) + --env.info(string.format("FF validParking = %s", tostring(validParking))) + --env.info(string.format("FF validBWlist = %s", tostring(validBWlist))) + + -- Check if all are true + valid=validTerminal and validParking and validBWlist end - - else - self:T2(self.lid..string.format("Terminal ID=%d: type=%s not supported", parkingspot.TerminalID, parkingspot.TerminalType)) - end -- check terminal type - end -- loop over parking spots - - -- No parking spot for at least one asset :( - if not gotit then - self:I(self.lid..string.format("WARNING: No free parking spot for asset %s [id=%d]", assetname, _asset.uid)) - return nil - end - end -- loop over asset units + + + -- Check correct terminal type for asset. We don't want helos in shelters etc. + if valid then + + -- Coordinate of the parking spot. + local _spot=parkingspot.Coordinate -- Core.Point#COORDINATE + local _termid=parkingspot.TerminalID + local free=true + local problem=nil + + -- Loop over all obstacles. + for _,obstacle in pairs(obstacles) do + + -- Check if aircraft overlaps with any obstacle. + local dist=_spot:Get2DDistance(obstacle.coord) + local safe=_overlap(_asset.size, obstacle.size, dist) + + -- Spot is blocked. + if not safe then + self:T3(self.lid..string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is NOT SAFE", assetname, _asset.uid, _termid, dist)) + free=false + problem=obstacle + problem.dist=dist + break + else + --env.info(string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is SAFE", assetname, _asset.uid, _termid, dist)) + end + + end + + -- Check if spot is free + if free then + + -- Add parkingspot for this asset unit. + table.insert(parking[_asset.uid], parkingspot) + + -- Debug + self:T(self.lid..string.format("Parking spot %d is free for asset %s [id=%d]!", _termid, assetname, _asset.uid)) + + -- Add the unit as obstacle so that this spot will not be available for the next unit. + table.insert(obstacles, {coord=_spot, size=_asset.size, name=assetname, type="asset"}) + + gotit=true + break + + else + + -- Debug output for occupied spots. + if self.Debug then + local coord=problem.coord --Core.Point#COORDINATE + local text=string.format("Obstacle %s [type=%s] blocking spot=%d! Size=%.1f m and distance=%.1f m.", problem.name, problem.type, _termid, problem.size, problem.dist) + self:I(self.lid..text) + coord:MarkToAll(string.format(text)) + else + self:T(self.lid..string.format("Parking spot %d is occupied or not big enough!", _termid)) + end + + end + + else + self:T2(self.lid..string.format("Terminal ID=%d: type=%s not supported", parkingspot.TerminalID, parkingspot.TerminalType)) + end -- check terminal type + end -- loop over parking spots + + -- No parking spot for at least one asset :( + if not gotit then + self:I(self.lid..string.format("WARNING: No free parking spot for asset %s [id=%d]", assetname, _asset.uid)) + return nil + end + end -- loop over asset units + end -- Asset spawned check end -- loop over asset groups return parking diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index 083aa86a4..3780ba573 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -93,6 +93,8 @@ -- @field #number dTQueueCheck Time interval to check the radio queue. Default 5 sec or 90 sec if SRS is used. -- @field #boolean ReportmBar Report mBar/hpa even if not metric, i.e. for Mirage flights -- @field #boolean TransmitOnlyWithPlayers For SRS - If true, only transmit if there are alive Players. +-- @field #string SRSText Text of the complete SRS message (if done at least once, else nil) +-- @field #boolean ATISforFARPs Will be set to true if the base given is a FARP/Helipad -- @extends Core.Fsm#FSM --- *It is a very sad thing that nowadays there is so little useless information.* - Oscar Wilde @@ -268,6 +270,8 @@ -- Unfortunately, it is not possible to determine the duration of the complete transmission. So once the transmission is finished, there might be some radio silence before -- the next iteration begins. You can fine tune the time interval between transmissions with the @{#ATIS.SetQueueUpdateTime}() function. The default interval is 90 seconds. -- +-- An SRS Setup-Guide can be found here: [Moose TTS Setup Guide](https://github.com/FlightControl-Master/MOOSE_GUIDES/blob/master/documents/Moose%20TTS%20Setup%20Guide.pdf) +-- -- # Examples -- -- ## Caucasus: Batumi @@ -306,6 +310,19 @@ -- atis:Start() -- -- This uses a male voice with US accent. It requires SRS to be installed in the `D:\DCS\_SRS\` directory. Not that backslashes need to be escaped or simply use slashes (as in linux). +-- +-- ## FARPS +-- +-- ATIS is working with FARPS, but this requires the usage of SRS. The airbase name for the `New()-method` is the UNIT name of the FARP: +-- +-- atis = ATIS:New("FARP Gold",119,radio.modulation.AM) +-- atis:SetMetricUnits() +-- atis:SetTransmitOnlyWithPlayers(true) +-- atis:SetReportmBar(true) +-- atis:SetTowerFrequencies(127.50) +-- atis:SetSRS("D:\\DCS\\_SRS\\", "male", "en-US",nil,5002) +-- atis:SetAdditionalInformation("Welcome to the Jungle!") +-- atis:__Start(3) -- -- @field #ATIS ATIS = { @@ -348,6 +365,7 @@ ATIS = { relHumidity = nil, ReportmBar = false, TransmitOnlyWithPlayers = false, + ATISforFARPs = false, } --- NATO alphabet. @@ -590,7 +608,7 @@ _ATIS = {} --- ATIS class version. -- @field #string version -ATIS.version = "0.9.11" +ATIS.version = "0.9.14" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -616,7 +634,7 @@ ATIS.version = "0.9.11" -- Constructor ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Create a new ATIS class object for a specific aircraft carrier unit. +--- Create a new ATIS class object for a specific airbase. -- @param #ATIS self -- @param #string AirbaseName Name of the airbase. -- @param #number Frequency Radio frequency in MHz. Default 143.00 MHz. @@ -878,6 +896,13 @@ function ATIS:SetMapMarks( switch ) return self end +--- Return the complete SRS Text block, if at least generated once. Else nil. +-- @param #ATIS self +-- @return #string SRSText +function ATIS:GetSRSText() + return self.SRSText +end + --- Set magnetic runway headings as depicted on the runway, *e.g.* "13" for 130° or "25L" for the left runway with magnetic heading 250°. -- @param #ATIS self -- @param #table headings Magnetic headings. Inverse (-180°) headings are added automatically. You only need to specify one heading per runway direction. "L"eft and "R" right can also be appended. @@ -1039,7 +1064,7 @@ end -- -- * 186° on the Caucaus map -- * 192° on the Nevada map --- * 170° on the Normany map +-- * 170° on the Normandy map -- * 182° on the Persian Gulf map -- -- Likewise, to convert *true* into *magnetic* heading, one has to substract easterly and add westerly variation. @@ -1247,11 +1272,18 @@ end function ATIS:onafterStart( From, Event, To ) -- Check that this is an airdrome. - if self.airbase:GetAirbaseCategory() ~= Airbase.Category.AIRDROME then - self:E( self.lid .. string.format( "ERROR: Cannot start ATIS for airbase %s! Only AIRDROMES are supported but NOT FARPS or SHIPS.", self.airbasename ) ) + if self.airbase:GetAirbaseCategory() == Airbase.Category.SHIP then + self:E( self.lid .. string.format( "ERROR: Cannot start ATIS for airbase %s! Only AIRDROMES are supported but NOT SHIPS.", self.airbasename ) ) return end - + + -- Check that if is a Helipad. + if self.airbase:GetAirbaseCategory() == Airbase.Category.HELIPAD then + self:E( self.lid .. string.format( "EXPERIMENTAL: Starting ATIS for Helipad %s! SRS must be ON", self.airbasename ) ) + self.ATISforFARPs = true + self.useSRS = true + end + -- Info. self:I( self.lid .. string.format( "Starting ATIS v%s for airbase %s on %.3f MHz Modulation=%d", ATIS.version, self.airbasename, self.frequency, self.modulation ) ) @@ -1463,10 +1495,19 @@ function ATIS:onafterBroadcast( From, Event, To ) -------------- --- Runway --- -------------- - - local runwayLanding, rwyLandingLeft=self:GetActiveRunway() - local runwayTakeoff, rwyTakeoffLeft=self:GetActiveRunway(true) - + + + local runwayLanding, rwyLandingLeft + local runwayTakeoff, rwyTakeoffLeft + + if self.airbase:GetAirbaseCategory() == Airbase.Category.HELIPAD then + runwayLanding, rwyLandingLeft="PAD 01",false + runwayTakeoff, rwyTakeoffLeft="PAD 02",false + else + runwayLanding, rwyLandingLeft=self:GetActiveRunway() + runwayTakeoff, rwyTakeoffLeft=self:GetActiveRunway(true) + end + ------------ --- Time --- ------------ @@ -1780,7 +1821,7 @@ function ATIS:onafterBroadcast( From, Event, To ) -- Airbase name subtitle = string.format( "%s", self.airbasename ) - if self.airbasename:find( "AFB" ) == nil and self.airbasename:find( "Airport" ) == nil and self.airbasename:find( "Airstrip" ) == nil and self.airbasename:find( "airfield" ) == nil and self.airbasename:find( "AB" ) == nil then + if (not self.ATISforFARPs) and self.airbasename:find( "AFB" ) == nil and self.airbasename:find( "Airport" ) == nil and self.airbasename:find( "Airstrip" ) == nil and self.airbasename:find( "airfield" ) == nil and self.airbasename:find( "AB" ) == nil then subtitle = subtitle .. " Airport" end if not self.useSRS then @@ -1855,8 +1896,6 @@ function ATIS:onafterBroadcast( From, Event, To ) end end alltext = alltext .. ";\n" .. subtitle - --self:I("Line 1811") - --self:I(alltext) -- Visibility if self.metric then @@ -1874,8 +1913,6 @@ function ATIS:onafterBroadcast( From, Event, To ) end end alltext = alltext .. ";\n" .. subtitle - --self:I("Line 1830") - --self:I(alltext) subtitle = "" -- Weather phenomena @@ -1977,10 +2014,8 @@ function ATIS:onafterBroadcast( From, Event, To ) end end end - --self:I("Line 1932") alltext = alltext .. ";\n" .. subtitle - --self:I(alltext) subtitle = "" -- Temperature if self.TDegF then @@ -2009,9 +2044,7 @@ function ATIS:onafterBroadcast( From, Event, To ) self:Transmission( ATIS.Sound.DegreesCelsius, 0.2 ) end end - --self:I("Line 1962") alltext = alltext .. ";\n" .. subtitle - --self:I(alltext) -- Dew point if self.TDegF then @@ -2040,8 +2073,6 @@ function ATIS:onafterBroadcast( From, Event, To ) self:Transmission( ATIS.Sound.DegreesCelsius, 0.2 ) end end - --self:I("Line 1992") - --self:I(alltext) alltext = alltext .. ";\n" .. subtitle -- Altimeter QNH/QFE. @@ -2107,69 +2138,68 @@ function ATIS:onafterBroadcast( From, Event, To ) end end end - --self:I("Line 2049") - --self:I(alltext) alltext = alltext .. ";\n" .. subtitle - -- Active runway. - local subtitle=string.format("Active runway %s", runwayLanding) - if rwyLandingLeft==true then - subtitle=subtitle.." Left" - elseif rwyLandingLeft==false then - subtitle=subtitle.." Right" - end - local _RUNACT = subtitle - if not self.useSRS then - self:Transmission(ATIS.Sound.ActiveRunway, 1.0, subtitle) - self.radioqueue:Number2Transmission(runwayLanding) + if not self.ATISforFARPs then + -- Active runway. + local subtitle=string.format("Active runway %s", runwayLanding) if rwyLandingLeft==true then - self:Transmission(ATIS.Sound.Left, 0.2) + subtitle=subtitle.." Left" elseif rwyLandingLeft==false then - self:Transmission(ATIS.Sound.Right, 0.2) + subtitle=subtitle.." Right" end - end - alltext = alltext .. ";\n" .. subtitle - - -- Runway length. - if self.rwylength then - - local runact = self.airbase:GetActiveRunway( self.runwaym2t ) - local length = runact.length - if not self.metric then - length = UTILS.MetersToFeet( length ) - end - - -- Length in thousands and hundrets of ft/meters. - local L1000, L0100 = self:_GetThousandsAndHundreds( length ) - - -- Subtitle. - local subtitle = string.format( "Runway length %d", length ) - if self.metric then - subtitle = subtitle .. " meters" - else - subtitle = subtitle .. " feet" - end - - -- Transmit. + local _RUNACT = subtitle if not self.useSRS then - self:Transmission( ATIS.Sound.RunwayLength, 1.0, subtitle ) - if tonumber( L1000 ) > 0 then - self.radioqueue:Number2Transmission( L1000 ) - self:Transmission( ATIS.Sound.Thousand, 0.1 ) - end - if tonumber( L0100 ) > 0 then - self.radioqueue:Number2Transmission( L0100 ) - self:Transmission( ATIS.Sound.Hundred, 0.1 ) - end - if self.metric then - self:Transmission( ATIS.Sound.Meters, 0.1 ) - else - self:Transmission( ATIS.Sound.Feet, 0.1 ) + self:Transmission(ATIS.Sound.ActiveRunway, 1.0, subtitle) + self.radioqueue:Number2Transmission(runwayLanding) + if rwyLandingLeft==true then + self:Transmission(ATIS.Sound.Left, 0.2) + elseif rwyLandingLeft==false then + self:Transmission(ATIS.Sound.Right, 0.2) end end alltext = alltext .. ";\n" .. subtitle + + -- Runway length. + if self.rwylength then + + local runact = self.airbase:GetActiveRunway( self.runwaym2t ) + local length = runact.length + if not self.metric then + length = UTILS.MetersToFeet( length ) + end + + -- Length in thousands and hundrets of ft/meters. + local L1000, L0100 = self:_GetThousandsAndHundreds( length ) + + -- Subtitle. + local subtitle = string.format( "Runway length %d", length ) + if self.metric then + subtitle = subtitle .. " meters" + else + subtitle = subtitle .. " feet" + end + + -- Transmit. + if not self.useSRS then + self:Transmission( ATIS.Sound.RunwayLength, 1.0, subtitle ) + if tonumber( L1000 ) > 0 then + self.radioqueue:Number2Transmission( L1000 ) + self:Transmission( ATIS.Sound.Thousand, 0.1 ) + end + if tonumber( L0100 ) > 0 then + self.radioqueue:Number2Transmission( L0100 ) + self:Transmission( ATIS.Sound.Hundred, 0.1 ) + end + if self.metric then + self:Transmission( ATIS.Sound.Meters, 0.1 ) + else + self:Transmission( ATIS.Sound.Feet, 0.1 ) + end + end + alltext = alltext .. ";\n" .. subtitle + end end - -- Airfield elevation if self.elevation then @@ -2236,9 +2266,7 @@ function ATIS:onafterBroadcast( From, Event, To ) end -- ILS - --self:I({ils=self.ils}) local ils=self:GetNavPoint(self.ils, runwayLanding, rwyLandingLeft) - --self:I({ils=ils,runwayLanding=runwayLanding, rwyLandingLeft=rwyLandingLeft}) if ils then subtitle = string.format( "ILS frequency %.2f MHz", ils.frequency ) if not self.useSRS then @@ -2253,7 +2281,6 @@ function ATIS:onafterBroadcast( From, Event, To ) self:Transmission( ATIS.Sound.MegaHertz, 0.2 ) end alltext = alltext .. ";\n" .. subtitle - --self:I(alltext) end -- Outer NDB @@ -2389,6 +2416,8 @@ function ATIS:onafterReport( From, Event, To, Text ) local text = string.gsub( text, "mmHg", "millimeters of Mercury" ) local text = string.gsub( text, "hPa", "hectopascals" ) local text = string.gsub( text, "m/s", "meters per second" ) + local text = string.gsub( text, "TACAN", "tackan" ) + local text = string.gsub( text, "FARP", "farp" ) -- Replace ";" by "." local text = string.gsub( text, ";", " . " ) @@ -2400,7 +2429,8 @@ function ATIS:onafterReport( From, Event, To, Text ) local duration = STTS.getSpeechTime(text,0.95) self.msrsQ:NewTransmission(text,duration,self.msrs,nil,2) --self.msrs:PlayText( text ) - + self.SRSText = text + end end diff --git a/Moose Development/Moose/Ops/CSAR.lua b/Moose Development/Moose/Ops/CSAR.lua index da3853223..0f64b56cc 100644 --- a/Moose Development/Moose/Ops/CSAR.lua +++ b/Moose Development/Moose/Ops/CSAR.lua @@ -26,11 +26,11 @@ -- -- === -- --- ### Author: **Applevangelist** (Moose Version), ***Ciribob*** (original), Thanks to: Shadowze, Cammel (testing) +-- ### Author: **Applevangelist** (Moose Version), ***Ciribob*** (original), Thanks to: Shadowze, Cammel (testing), The Chosen One (Persistence) -- @module Ops.CSAR -- @image OPS_CSAR.jpg --- Date: November 2022 +-- Date: January 2023 ------------------------------------------------------------------------- --- **CSAR** class, extends Core.Base#BASE, Core.Fsm#FSM @@ -197,6 +197,26 @@ -- -- --Create a casualty and CASEVAC request from a "Point" (VEC2) for the blue coalition --shagrat -- 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. -- -- @field #CSAR CSAR = { @@ -272,7 +292,7 @@ CSAR.AircraftType["Bronco-OV-10A"] = 2 --- CSAR class version. -- @field #string version -CSAR.version="1.0.16" +CSAR.version="1.0.17" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list @@ -296,6 +316,8 @@ function CSAR:New(Coalition, Template, Alias) -- Inherit everything from FSM class. local self=BASE:Inherit(self, FSM:New()) -- #CSAR + BASE:T({Coalition, Prefixes, Alias}) + --set Coalition if Coalition and type(Coalition)=="string" then if Coalition=="blue" then @@ -346,6 +368,8 @@ function CSAR:New(Coalition, Template, Alias) 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("*", "Save", "*") -- CSAR save event. self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. -- tables, mainly for tracking actions @@ -442,6 +466,14 @@ function CSAR:New(Coalition, Template, Alias) self.SRSVolume = 1.0 -- volume 0.0 to 1.0 self.SRSGender = "male" -- male or female + 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 --- ------------------------ @@ -471,6 +503,24 @@ 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 + + --- Triggers the FSM event "Load" after a delay. + -- @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 + + --- Triggers the FSM event "Save" after a delay. + -- @function [parent=#CSAR] __Save + -- @param #CSAR self + -- @param #number delay Delay in seconds. --- On After "PilotDown" event. Downed Pilot detected. -- @function [parent=#CSAR] OnAfterPilotDown @@ -538,6 +588,24 @@ function CSAR:New(Coalition, Template, Alias) -- @param #string To To state. -- @param #string Pilotname Name of the pilot KIA. + --- FSM Function OnAfterLoad. + -- @function [parent=#CSAR] OnAfterLoad + -- @param #CSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @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 + -- @param #string From From state. + -- @param #string Event Event. + -- @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 @@ -850,7 +918,7 @@ end --- (Internal) Function to add a CSAR object into the scene at a Point coordinate (VEC_2). For mission designers wanting to add e.g. casualties to the scene, that don't use beacons. -- @param #CSAR self --- @param #string _Point a POINT_VEC2. +-- @param Core.Point#COORDINATE _Point -- @param #number _coalition Coalition. -- @param #string _description (optional) Description. -- @param #boolean _nomessage (optional) If true, don\'t send a message to SAR. @@ -883,7 +951,7 @@ end --- Function to add a CSAR object into the scene at a zone coordinate. For mission designers wanting to add e.g. PoWs to the scene. -- @param #CSAR self --- @param #string Point a POINT_VEC2. +-- @param Core.Point#COORDINATE Point -- @param #number Coalition Coalition. -- @param #string Description (optional) Description. -- @param #boolean addBeacon (optional) yes or no. @@ -893,8 +961,8 @@ end -- @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 Point #POINT_VEC2 for the blue coalition. --- my_csar:SpawnCASEVAC( POINT_VEC2, coalition.side.BLUE ) +-- -- 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) self:_SpawnCASEVAC(Point, Coalition, Description, Nomessage, Unitname, Typename, Forcedesc) return self @@ -2108,9 +2176,10 @@ function CSAR:_AddBeaconToGroup(_group, _freq) local _radioUnit = _group:GetUnit(1) if _radioUnit then 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} - trigger.action.radioTransmission(Sound, vec3, 0, false, Frequency, self.ADFRadioPwr or 1000) -- Beacon in MP only runs for exactly 30secs straight + trigger.action.radioTransmission(Sound, vec3, 0, false, Frequency, self.ADFRadioPwr or 1000,name..math.random(1,10000)) -- Beacon in MP only runs for exactly 30secs straight end end return self @@ -2218,7 +2287,16 @@ function CSAR:onafterStart(From, Event, To) self.msrs:SetLabel("CSAR") self.SRSQueue = MSRSQUEUE:New("CSAR") 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 @@ -2451,6 +2529,240 @@ function CSAR:onbeforeLanded(From, Event, To, HeliName, Airbase) self:T({From, Event, To, HeliName, Airbase}) return self end + +--- On before "Save" event. Checks if io and lfs are available. +-- @param #CSAR self +-- @param #string From From state. +-- @param #string Event Event. +-- @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". +function CSAR:onbeforeSave(From, Event, To, path, filename) + self:T({From, Event, To, path, filename}) + if not self.enableLoadSave then + return self + end + -- 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.") + return false + end + + -- Check default path. + if path==nil and not lfs then + self:E(self.lid.."WARNING: lfs not desanitized. State will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.") + end + + return true +end + +--- On after "Save" event. Player data is saved to file. +-- @param #CSAR self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string path Path where the file is saved. If nil, file is saved in the DCS root installtion directory or your "Saved Games" folder if lfs was desanitized. +-- @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 + if not self.enableLoadSave then + return self + end + --- Function that saves data to file + local function _savefile(filename, data) + local f = assert(io.open(filename, "wb")) + f:write(data) + f:close() + end + + -- Set path or default. + if lfs then + path=self.filepath or lfs.writedir() + end + + -- Set file name. + filename=filename or self.filename + + -- Set path. + 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 + for _,_grp in pairs(pilots) do + local DownedPilot = _grp -- Wrapper.Group#GROUP + if DownedPilot and DownedPilot.alive then + -- get downed pilot data for saving + local playerName = DownedPilot.player + local group = DownedPilot.group + local coalition = group:GetCoalition() + local country = group:GetCountry() + local description = DownedPilot.desc + local typeName = DownedPilot.typename + local freq = DownedPilot.frequency + 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 + local filename = self.filename + local filepath = self.filepath + self:__Save(interval,filepath,filename) + end + return self +end + +--- On before "Load" event. Checks if io and lfs and the file are available. +-- @param #CSAR self +-- @param #string From From state. +-- @param #string Event Event. +-- @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". +function CSAR:onbeforeLoad(From, Event, To, path, filename) + self:T({From, Event, To, path, filename}) + if not self.enableLoadSave then + return self + end + --- Function that check if a file exists. + local function _fileexists(name) + 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.") + return false + end + + -- Check default path. + if path==nil and not lfs then + self:E(self.lid.."WARNING: lfs not desanitized. State will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.") + end + + -- Set path or default. + if lfs then + path=path or lfs.writedir() + end + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + -- Check if file exists. + local exists=_fileexists(filename) + + if exists then + return true + else + self:E(self.lid..string.format("WARNING: State file %s might not exist.", filename)) + return false + --return self + end + +end + +--- On after "Load" event. Loads dropped units from file. +-- @param #CSAR self +-- @param #string From From state. +-- @param #string Event Event. +-- @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". +function CSAR:onafterLoad(From, Event, To, path, filename) + self:T({From, Event, To, path, filename}) + if not self.enableLoadSave then + return self + end + --- Function that loads data from a file. + local function _loadfile(filename) + local f=assert(io.open(filename, "rb")) + local data=f:read("*all") + 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() + end + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + -- Info message. + 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 + 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 = dataset[5] + local country = dataset[6] + local description = dataset[7] + local typeName = dataset[8] + local unitName = dataset[9] + local freq = dataset[10] + + self:_AddCsar(coalition, country, point, typeName, unitName, playerName, freq, nil, description, nil) + end + + return self +end + -------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- End Ops.CSAR -------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index 0a7c38665..e4e2fd1ed 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -22,7 +22,7 @@ -- @module Ops.CTLD -- @image OPS_CTLD.jpg --- Last Update October 2022 +-- Last Update Jan 2023 do @@ -711,6 +711,9 @@ do -- my_ctld.droppedbeacontimeout = 600 -- dropped beacon lasts 10 minutes -- my_ctld.usesubcats = false -- use sub-category names for crates, adds an extra menu layer in "Get Crates", useful if you have > 10 crate types. -- my_ctld.placeCratesAhead = false -- place crates straight ahead of the helicopter, in a random way. If true, crates are more neatly sorted. +-- my_ctld.nobuildinloadzones = true -- forbid players to build stuff in LOAD zones if set to `true` +-- my_ctld.movecratesbeforebuild = true -- crates must be moved once before they can be build. Set to false for direct builds. +-- my_ctld.surfacetypes = {land.SurfaceType.LAND,land.SurfaceType.ROAD,land.SurfaceType.RUNWAY,land.SurfaceType.SHALLOW_WATER} -- surfaces for loading back objects -- -- ## 2.1 User functions -- @@ -966,7 +969,110 @@ do -- -- **Caveat:** -- If you use units build by multiple templates, they will effectively double on loading. Dropped crates are not saved. Current stock is not saved. +-- +-- ## 7. Complex example - Build a complete FARP from a CTLD crate drop +-- +-- Prerequisites - you need to add a cargo of type FOB to your CTLD instance, for simplification reasons we call it FOB: +-- +-- my_ctld:AddCratesCargo("FARP",{"FOB"},CTLD_CARGO.Enum.FOB,2) +-- +-- Also, you need to have **all statics with the fitting names** as per the script in your mission already, as we're going to copy them, and a template +-- for FARP vehicles, so -- services are goin to work (e.g. for the blue side: an unarmed humvee, two trucks and a fuel truck. Optionally add a fire fighter). +-- +-- The following code will build a FARP at the coordinate the FOB was dropped and built: +-- +-- -- FARP Radio. First one has 130AM, next 131 and for forth +-- local FARPFreq = 130 +-- local FARPName = 1 -- numbers 1..10 -- +-- local FARPClearnames = { +-- [1]="London", +-- [2]="Dallas", +-- [3]="Paris", +-- [4]="Moscow", +-- [5]="Berlin", +-- [6]="Rome", +-- [7]="Madrid", +-- [8]="Warsaw", +-- [9]="Dublin", +-- [10]="Perth", +-- } +-- +-- function BuildAFARP(Coordinate) +-- local coord = Coordinate -- Core.Point#COORDINATE +-- +-- local FarpName = ((FARPName-1)%10)+1 +-- local FName = FARPClearnames[FarpName] +-- +-- FARPFreq = FARPFreq + 1 +-- FARPName = FARPName + 1 +-- +-- -- Create a SPAWNSTATIC object from a template static FARP object. +-- local SpawnStaticFarp=SPAWNSTATIC:NewFromStatic("Static Invisible FARP-1", country.id.USA) +-- +-- -- Spawning FARPs is special in DCS. Therefore, we need to specify that this is a FARP. We also set the callsign and the frequency. +-- SpawnStaticFarp:InitFARP(FARPName, FARPFreq, 0) +-- SpawnStaticFarp:InitDead(false) +-- +-- -- Spawn FARP +-- local ZoneSpawn = ZONE_RADIUS:New("FARP "..FName,Coordinate:GetVec2(),160,false) +-- local Heading = 0 +-- local FarpBerlin=SpawnStaticFarp:SpawnFromZone(ZoneSpawn, Heading, "FARP "..FName) +-- +-- -- ATC and services - put them 125m from the center of the zone towards North +-- local FarpVehicles = SPAWN:NewWithAlias("FARP Vehicles Template","FARP "..FName.." Technicals") +-- FarpVehicles:InitHeading(180) +-- local FarpVCoord = coord:Translate(125,0) +-- FarpVehicles:SpawnFromCoordinate(FarpVCoord) +-- +-- -- We will put the rest of the statics in a nice circle around the center +-- local base = 330 +-- local delta = 30 +-- +-- local windsock = SPAWNSTATIC:NewFromStatic("Static Windsock-1",country.id.USA) +-- local sockcoord = coord:Translate(125,base) +-- windsock:SpawnFromCoordinate(sockcoord,Heading,"Windsock "..FName) +-- base=base-delta +-- +-- local fueldepot = SPAWNSTATIC:NewFromStatic("Static FARP Fuel Depot-1",country.id.USA) +-- local fuelcoord = coord:Translate(125,base) +-- fueldepot:SpawnFromCoordinate(fuelcoord,Heading,"Fueldepot "..FName) +-- base=base-delta +-- +-- local ammodepot = SPAWNSTATIC:NewFromStatic("Static FARP Ammo Storage-2-1",country.id.USA) +-- local ammocoord = coord:Translate(125,base) +-- ammodepot:SpawnFromCoordinate(ammocoord,Heading,"Ammodepot "..FName) +-- base=base-delta +-- +-- local CommandPost = SPAWNSTATIC:NewFromStatic("Static FARP Command Post-1",country.id.USA) +-- local CommandCoord = coord:Translate(125,base) +-- CommandPost:SpawnFromCoordinate(CommandCoord,Heading,"Command Post "..FName) +-- base=base-delta +-- +-- local Tent1 = SPAWNSTATIC:NewFromStatic("Static FARP Tent-11",country.id.USA) +-- local Tent1Coord = coord:Translate(125,base) +-- Tent1:SpawnFromCoordinate(Tent1Coord,Heading,"Command Tent "..FName) +-- base=base-delta +-- +-- local Tent2 = SPAWNSTATIC:NewFromStatic("Static FARP Tent-11",country.id.USA) +-- local Tent2Coord = coord:Translate(125,base) +-- Tent2:SpawnFromCoordinate(Tent2Coord,Heading,"Command Tent2 "..FName) +-- +-- -- add a loadzone to CTLD +-- my_ctld:AddCTLDZone("FARP "..FName,CTLD.CargoZoneType.LOAD,SMOKECOLOR.Blue,true,true) +-- local m = MESSAGE:New(string.format("FARP %s in operation!",FName),15,"CTLD"):ToBlue() +-- end +-- +-- function my_ctld:OnAfterCratesBuild(From,Event,To,Group,Unit,Vehicle) +-- local name = Vehicle:GetName() +-- if string.match(name,"FOB",1,true) then +-- local Coord = Vehicle:GetCoordinate() +-- Vehicle:Destroy(false) +-- BuildAFARP(Coord) +-- end +-- end +-- +-- -- @field #CTLD CTLD = { ClassName = "CTLD", @@ -1015,7 +1121,16 @@ CTLD = { -- @type CTLD.ZoneBeacon -- @field #string name -- Name of zone for the coordinate -- @field #number frequency -- in mHz --- @field #number modulation -- i.e.radio.modulation.FM or radio.modulation.AM +-- @field #number modulation -- i.e.CTLD.RadioModulation.FM or CTLD.RadioModulation.AM + +--- Radio Modulation +-- @type CTLD.RadioModulation +-- @field #number AM +-- @field #number FM +CTLD.RadioModulation = { + AM = 0, + FM = 1, +} --- Zone Info. -- @type CTLD.CargoZone @@ -1067,6 +1182,7 @@ CTLD.UnitTypes = { ["Mi-8MTV2"] = {type="Mi-8MTV2", crates=true, troops=true, cratelimit = 2, trooplimit = 12, length = 15, cargoweightlimit = 3000}, ["Mi-8MT"] = {type="Mi-8MTV2", crates=true, troops=true, cratelimit = 2, trooplimit = 12, length = 15, cargoweightlimit = 3000}, ["Ka-50"] = {type="Ka-50", crates=false, troops=false, cratelimit = 0, trooplimit = 0, length = 15, cargoweightlimit = 0}, + ["Ka-50_3"] = {type="Ka-50_3", crates=false, troops=false, cratelimit = 0, trooplimit = 0, length = 15, cargoweightlimit = 0}, ["Mi-24P"] = {type="Mi-24P", crates=true, troops=true, cratelimit = 2, trooplimit = 8, length = 18, cargoweightlimit = 700}, ["Mi-24V"] = {type="Mi-24V", crates=true, troops=true, cratelimit = 2, trooplimit = 8, length = 18, cargoweightlimit = 700}, ["Hercules"] = {type="Hercules", crates=true, troops=true, cratelimit = 7, trooplimit = 64, length = 25, cargoweightlimit = 19000}, -- 19t cargo, 64 paratroopers. @@ -1078,7 +1194,7 @@ CTLD.UnitTypes = { --- CTLD class version. -- @field #string version -CTLD.version="1.0.19" +CTLD.version="1.0.27" --- Instantiate a new CTLD. -- @param #CTLD self @@ -1161,6 +1277,7 @@ function CTLD:New(Coalition, Prefixes, Alias) -- radio beacons self.RadioSound = "beacon.ogg" + self.RadioPath = "l10n/DEFAULT/" -- zones stuff self.pickupZones = {} @@ -1243,6 +1360,11 @@ function CTLD:New(Coalition, Prefixes, Alias) self.usesubcats = false self.subcats = {} + -- disallow building in loadzones + self.nobuildinloadzones = true + self.movecratesbeforebuild = true + self.surfacetypes = {land.SurfaceType.LAND,land.SurfaceType.ROAD,land.SurfaceType.RUNWAY,land.SurfaceType.SHALLOW_WATER} + local AliaS = string.gsub(self.alias," ","_") self.filename = string.format("CTLD_%s_Persist.csv",AliaS) @@ -1635,12 +1757,54 @@ function CTLD:_SendMessage(Text, Time, Clearscreen, Group) return self end +--- (Internal) Find a troops CTLD_CARGO object in stock +-- @param #CTLD self +-- @param #string Name of the object +-- @return #CTLD_CARGO Cargo object, nil if it cannot be found +function CTLD:_FindTroopsCargoObject(Name) + self:T(self.lid .. " _FindTroopsCargoObject") + local cargo = nil + for _,_cargo in pairs(self.Cargo_Troops)do + local cargo = _cargo -- #CTLD_CARGO + if cargo.Name == Name then + return cargo + end + end + return nil +end + +--- (User) Pre-load troops into a helo, e.g. for airstart. Unit **must** be alive in-game, i.e. player has taken the slot! +-- @param #CTLD self +-- @param Wrapper.Unit#UNIT Unit The unit to load into, can be handed as Wrapper.Client#CLIENT object +-- @param #string Troopname The name of the Troops to be loaded. Must be created prior in the CTLD setup! +-- @return #CTLD self +-- @usage +-- local client = UNIT:FindByName("Helo-1-1") +-- if client and client:IsAlive() then +-- myctld:PreloadTroops(client,"Infantry") +-- end +function CTLD:PreloadTroops(Unit,Troopname) + self:T(self.lid .. " PreloadTroops") + local name = Troopname or "Unknown" + if Unit and Unit:IsAlive() then + local cargo = self:_FindTroopsCargoObject(name) + local group = Unit:GetGroup() + if cargo then + self:_LoadTroops(group,Unit,cargo,true) + else + self:E(self.lid.." Troops preload - Cargo Object "..name.." not found!") + end + end + return self +end + --- (Internal) Function to load troops into a heli. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit -- @param #CTLD_CARGO Cargotype -function CTLD:_LoadTroops(Group, Unit, Cargotype) +-- @param #boolean Inject +function CTLD:_LoadTroops(Group, Unit, Cargotype, Inject) self:T(self.lid .. " _LoadTroops") -- check if we have stock local instock = Cargotype:GetStock() @@ -1648,7 +1812,7 @@ function CTLD:_LoadTroops(Group, Unit, Cargotype) local cgotype = Cargotype:GetType() local cgonetmass = Cargotype:GetNetMass() local maxloadable = self:_GetMaxLoadableMass(Unit) - if type(instock) == "number" and tonumber(instock) <= 0 and tonumber(instock) ~= -1 then + if type(instock) == "number" and tonumber(instock) <= 0 and tonumber(instock) ~= -1 and not Inject then -- nothing left over self:_SendMessage(string.format("Sorry, all %s are gone!", cgoname), 10, false, Group) return self @@ -1656,21 +1820,22 @@ function CTLD:_LoadTroops(Group, Unit, Cargotype) -- landed or hovering over load zone? local grounded = not self:IsUnitInAir(Unit) local hoverload = self:CanHoverLoad(Unit) - --local dooropen = UTILS.IsLoadingDoorOpen(Unit:GetName()) and self.pilotmustopendoors -- check if we are in LOAD zone local inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) if not inzone then inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP) end - if not inzone then - self:_SendMessage("You are not close enough to a logistics zone!", 10, false, Group) - if not self.debug then return self end - elseif not grounded and not hoverload then - self:_SendMessage("You need to land or hover in position to load!", 10, false, Group) - if not self.debug then return self end - elseif self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then - self:_SendMessage("You need to open the door(s) to load troops!", 10, false, Group) - if not self.debug then return self end + if not Inject then + if not inzone then + self:_SendMessage("You are not close enough to a logistics zone!", 10, false, Group) + if not self.debug then return self end + elseif not grounded and not hoverload then + self:_SendMessage("You need to land or hover in position to load!", 10, false, Group) + if not self.debug then return self end + elseif self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then + self:_SendMessage("You need to open the door(s) to load troops!", 10, false, Group) + if not self.debug then return self end + end end -- load troops into heli local group = Group -- Wrapper.Group#GROUP @@ -2093,10 +2258,11 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop) self.CargoCounter = self.CargoCounter + 1 local realcargo = nil if drop then - realcargo = CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,true,false,cratesneeded,self.Spawned_Crates[self.CrateCounter],true,cargotype.PerCrateMass,subcat) + --CTLD_CARGO:New(ID, Name, Templates, Sorte, HasBeenMoved, LoadDirectly, CratesNeeded, Positionable, Dropped, PerCrateMass, Stock, Subcategory) + realcargo = CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,true,false,cratesneeded,self.Spawned_Crates[self.CrateCounter],true,cargotype.PerCrateMass,nil,subcat) table.insert(droppedcargo,realcargo) else - realcargo = CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,false,false,cratesneeded,self.Spawned_Crates[self.CrateCounter],true,cargotype.PerCrateMass,subcat) + realcargo = CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,false,false,cratesneeded,self.Spawned_Crates[self.CrateCounter],false,cargotype.PerCrateMass,nil,subcat) Cargo:RemoveStock() end table.insert(self.Spawned_Cargo, realcargo) @@ -2824,6 +2990,14 @@ function CTLD:_BuildCrates(Group, Unit,Engineering) return self end end + if not Engineering and self.nobuildinloadzones then + -- are we in a load zone? + local inloadzone = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) + if inloadzone then + self:_SendMessage("You cannot build in a loading area, Pilot!", 10, false, Group) + return self + end + end -- get nearby crates local finddist = self.CrateDistance or 35 local crates,number = self:_FindCratesNearby(Group,Unit, finddist,true) -- #table @@ -2834,7 +3008,7 @@ function CTLD:_BuildCrates(Group, Unit,Engineering) -- get dropped crates for _,_crate in pairs(crates) do local Crate = _crate -- #CTLD_CARGO - if Crate:WasDropped() and not Crate:IsRepair() and not Crate:IsStatic() then + if (Crate:WasDropped() or not self.movecratesbeforebuild) and not Crate:IsRepair() and not Crate:IsStatic() then -- we can build these - maybe local name = Crate:GetName() local required = Crate:GetCratesNeeded() @@ -2879,7 +3053,12 @@ function CTLD:_BuildCrates(Group, Unit,Engineering) local text = string.format("Type: %s | Required %d | Found %d | Can Build %s", name, needed, found, txtok) report:Add(text) end -- end list buildables - if not foundbuilds then report:Add(" --- None Found ---") end + if not foundbuilds then + report:Add(" --- None found! ---") + if self.movecratesbeforebuild then + report:Add("*** Crates need to be moved before building!") + end + end report:Add("------------------------------------------------------------") local text = report:Text() if not Engineering then @@ -3440,7 +3619,7 @@ function CTLD:_GetFMBeacon(Name) table.insert(self.UsedFMFrequencies, FM) beacon.name = Name beacon.frequency = FM / 1000000 - beacon.modulation = radio.modulation.FM + beacon.modulation = CTLD.RadioModulation.FM return beacon end @@ -3460,7 +3639,7 @@ function CTLD:_GetUHFBeacon(Name) table.insert(self.UsedUHFFrequencies, UHF) beacon.name = Name beacon.frequency = UHF / 1000000 - beacon.modulation = radio.modulation.AM + beacon.modulation = CTLD.RadioModulation.AM return beacon end @@ -3481,7 +3660,7 @@ function CTLD:_GetVHFBeacon(Name) table.insert(self.UsedVHFFrequencies, VHF) beacon.name = Name beacon.frequency = VHF / 1000000 - beacon.modulation = radio.modulation.FM + beacon.modulation = CTLD.RadioModulation.FM return beacon end @@ -3522,7 +3701,12 @@ function CTLD:AddCTLDZone(Name, Type, Color, Active, HasBeacon, Shiplength, Ship ctldzone.name = Name or "NONE" ctldzone.type = Type or CTLD.CargoZoneType.MOVE -- #CTLD.CargoZoneType ctldzone.hasbeacon = HasBeacon or false - + + if Type == CTLD.CargoZoneType.BEACON then + self.droppedbeaconref[ctldzone.name] = zone:GetCoordinate() + ctldzone.timestamp = timer.getTime() + end + if HasBeacon then ctldzone.fmbeacon = self:_GetFMBeacon(Name) ctldzone.uhfbeacon = self:_GetUHFBeacon(Name) @@ -3611,7 +3795,8 @@ function CTLD:CheckDroppedBeacons() for _,_beacon in pairs (self.droppedBeacons) do local beacon = _beacon -- #CTLD.CargoZone - local T0 = beacon.timestamp + if not beacon.timestamp then beacon.timestamp = timer.getTime() + timeout end + local T0 = beacon.timestamp if timer.getTime() - T0 > timeout then local name = beacon.name self.droppedbeaconref[name] = nil @@ -3682,22 +3867,53 @@ function CTLD:_AddRadioBeacon(Name, Sound, Mhz, Modulation, IsShip, IsDropped) end end local Sound = Sound or "beacon.ogg" - if IsDropped and Zone then + if Zone then + if IsDropped then local ZoneCoord = Zone - local ZoneVec3 = ZoneCoord:GetVec3() + local ZoneVec3 = ZoneCoord:GetVec3() or {x=0,y=0,z=0} local Frequency = Mhz * 1000000 -- Freq in Hertz - local Sound = "l10n/DEFAULT/"..Sound - trigger.action.radioTransmission(Sound, ZoneVec3, Modulation, false, Frequency, 1000) -- Beacon in MP only runs for 30secs straight - elseif Zone then + local Sound = self.RadioPath..Sound + trigger.action.radioTransmission(Sound, ZoneVec3, Modulation, false, Frequency, 1000, Name..math.random(1,10000)) -- Beacon in MP only runs for 30secs straight + self:T2(string.format("Beacon added | Name = %s | Sound = %s | Vec3 = %d %d %d | Freq = %f | Modulation = %d (0=AM/1=FM)",Name,Sound,ZoneVec3.x,ZoneVec3.y,ZoneVec3.z,Mhz,Modulation)) + else local ZoneCoord = Zone:GetCoordinate() - local ZoneVec3 = ZoneCoord:GetVec3() - local Frequency = Mhz * 1000000 -- Freq in Hertz - local Sound = "l10n/DEFAULT/"..Sound - trigger.action.radioTransmission(Sound, ZoneVec3, Modulation, false, Frequency, 1000) -- Beacon in MP only runs for 30secs straight + local ZoneVec3 = ZoneCoord:GetVec3() or {x=0,y=0,z=0} + local Frequency = Mhz * 1000000 -- Freq in Hert + local Sound = self.RadioPath..Sound + trigger.action.radioTransmission(Sound, ZoneVec3, Modulation, false, Frequency, 1000, Name..math.random(1,10000)) -- Beacon in MP only runs for 30secs straightt + self:T2(string.format("Beacon added | Name = %s | Sound = %s | Vec3 = {x=%d, y=%d, z=%d} | Freq = %f | Modulation = %d (0=AM/1=FM)",Name,Sound,ZoneVec3.x,ZoneVec3.y,ZoneVec3.z,Mhz,Modulation)) + end + else + self:E(self.lid.."***** _AddRadioBeacon: Zone does not exist: "..Name) end return self end +--- Set folder path where the CTLD sound files are located **within you mission (miz) file**. +-- The default path is "l10n/DEFAULT/" but sound files simply copied there will be removed by DCS the next time you save the mission. +-- However, if you create a new folder inside the miz file, which contains the sounds, it will not be deleted and can be used. +-- @param #CTLD self +-- @param #string FolderPath The path to the sound files, e.g. "CTLD_Soundfiles/". +-- @return #CTLD self +function CTLD:SetSoundfilesFolder( FolderPath ) + self:T(self.lid .. " SetSoundfilesFolder") + -- Check that it ends with / + if FolderPath then + local lastchar = string.sub( FolderPath, -1 ) + if lastchar ~= "/" then + FolderPath = FolderPath .. "/" + end + end + + -- Folderpath. + self.RadioPath = FolderPath + + -- Info message. + self:I( self.lid .. string.format( "Setting sound files folder to: %s", self.RadioPath ) ) + + return self +end + --- (Internal) Function to refresh radio beacons -- @param #CTLD self function CTLD:_RefreshRadioBeacons() @@ -3720,10 +3936,10 @@ function CTLD:_RefreshRadioBeacons() local Name = czone.name local FM = FMbeacon.frequency -- MHz local VHF = VHFbeacon.frequency -- KHz - local UHF = UHFbeacon.frequency -- MHz - self:_AddRadioBeacon(Name,Sound,FM,radio.modulation.FM, IsShip, IsDropped) - self:_AddRadioBeacon(Name,Sound,VHF,radio.modulation.FM, IsShip, IsDropped) - self:_AddRadioBeacon(Name,Sound,UHF,radio.modulation.AM, IsShip, IsDropped) + local UHF = UHFbeacon.frequency -- MHz + self:_AddRadioBeacon(Name,Sound,FM, CTLD.RadioModulation.FM, IsShip, IsDropped) + self:_AddRadioBeacon(Name,Sound,VHF,CTLD.RadioModulation.FM, IsShip, IsDropped) + self:_AddRadioBeacon(Name,Sound,UHF,CTLD.RadioModulation.AM, IsShip, IsDropped) end end end @@ -4235,6 +4451,8 @@ end -- @param #CTLD self -- @param Core.Zone#ZONE Zone The zone where to drop the troops. -- @param Ops.CTLD#CTLD_CARGO Cargo The #CTLD_CARGO object to spawn. + -- @param #table Surfacetypes (Optional) Table of surface types. Can also be a single surface type. We will try max 1000 times to find the right type! + -- @param #boolean PreciseLocation (Optional) Don't try to get a random position in the zone but use the dead center. Caution not to stack up stuff on another! -- @return #CTLD self -- @usage Use this function to pre-populate the field with Troops or Engineers at a random coordinate in a zone: -- -- create a matching #CTLD_CARGO type @@ -4242,8 +4460,8 @@ end -- -- get a #ZONE object -- local dropzone = ZONE:New("InjectZone") -- Core.Zone#ZONE -- -- and go: - -- my_ctld:InjectTroops(dropzone,InjectTroopsType) - function CTLD:InjectTroops(Zone,Cargo) + -- my_ctld:InjectTroops(dropzone,InjectTroopsType,{land.SurfaceType.LAND}) + function CTLD:InjectTroops(Zone,Cargo,Surfacetypes,PreciseLocation) self:T(self.lid.." InjectTroops") local cargo = Cargo -- #CTLD_CARGO @@ -4275,8 +4493,10 @@ end local temptable = cargo:GetTemplates() or {} local factor = 1.5 local zone = Zone - - local randomcoord = zone:GetRandomCoordinate(10,30*factor):GetVec2() + local randomcoord = zone:GetRandomCoordinate(10,30*factor,Surfacetypes):GetVec2() + if PreciseLocation then + randomcoord = zone:GetCoordinate():GetVec2() + end for _,_template in pairs(temptable) do self.TroopCounter = self.TroopCounter + 1 local alias = string.format("%s-%d", _template, math.random(1,100000)) @@ -4611,7 +4831,7 @@ end -- @param Wrapper.Group#GROUP Vehicle The #GROUP object of the vehicle or FOB build. -- @return #CTLD self function CTLD:onbeforeCratesBuild(From, Event, To, Group, Unit, Vehicle) - self:I({From, Event, To}) + self:T({From, Event, To}) if Unit and Unit:IsPlayer() and self.PlayerTaskQueue then local playername = Unit:GetPlayerName() local dropcoord = Vehicle:GetCoordinate() or COORDINATE:New(0,0,0) @@ -4737,7 +4957,7 @@ end for _,_cargo in pairs (stcstable) do local cargo = _cargo -- #CTLD_CARGO local object = cargo:GetPositionable() -- Wrapper.Static#STATIC - if object and object:IsAlive() and cargo:WasDropped() then + if object and object:IsAlive() and (cargo:WasDropped() or not cargo:HasMoved()) then statics[#statics+1] = cargo end end @@ -4978,7 +5198,7 @@ end self:InjectVehicles(dropzone,injectvehicle) elseif cargotype == CTLD_CARGO.Enum.TROOPS or cargotype == CTLD_CARGO.Enum.ENGINEERS then local injecttroops = CTLD_CARGO:New(nil,cargoname,cargotemplates,cargotype,true,true,size,nil,true,mass) - self:InjectTroops(dropzone,injecttroops) + self:InjectTroops(dropzone,injecttroops,self.surfacetypes) end elseif (type(groupname) == "string" and groupname == "STATIC") or cargotype == CTLD_CARGO.Enum.REPAIR then local cargotemplates = dataset[6] diff --git a/Moose Development/Moose/Tasking/CommandCenter.lua b/Moose Development/Moose/Tasking/CommandCenter.lua index 6562fcb90..8af4786bc 100644 --- a/Moose Development/Moose/Tasking/CommandCenter.lua +++ b/Moose Development/Moose/Tasking/CommandCenter.lua @@ -512,7 +512,7 @@ function COMMANDCENTER:AssignTask( TaskGroup ) if Task then - self:I( "Assigning task " .. Task:GetName() .. " using auto assign method " .. self.AutoAssignMethod .. " to " .. TaskGroup:GetName() .. " with task priority " .. AssignPriority ) + self:T( "Assigning task " .. Task:GetName() .. " using auto assign method " .. self.AutoAssignMethod .. " to " .. TaskGroup:GetName() .. " with task priority " .. AssignPriority ) if not self.AutoAcceptTasks == true then Task:SetAutoAssignMethod( ACT_ASSIGN_MENU_ACCEPT:New( Task.TaskBriefing ) ) diff --git a/Moose Development/Moose/Tasking/Mission.lua b/Moose Development/Moose/Tasking/Mission.lua index ba84fe162..b0f39072f 100644 --- a/Moose Development/Moose/Tasking/Mission.lua +++ b/Moose Development/Moose/Tasking/Mission.lua @@ -413,7 +413,7 @@ end -- @param Wrapper.Group#GROUP PlayerGroup The GROUP of the player joining the Mission. -- @return #boolean true if Unit is part of a Task in the Mission. function MISSION:JoinUnit( PlayerUnit, PlayerGroup ) - self:I( { Mission = self:GetName(), PlayerUnit = PlayerUnit, PlayerGroup = PlayerGroup } ) + self:T( { Mission = self:GetName(), PlayerUnit = PlayerUnit, PlayerGroup = PlayerGroup } ) local PlayerUnitAdded = false @@ -571,7 +571,7 @@ do -- Group Assignment local MissionGroupName = MissionGroup:GetName() self.AssignedGroups[MissionGroupName] = MissionGroup - self:I( string.format( "Mission %s is assigned to %s", MissionName, MissionGroupName ) ) + self:T( string.format( "Mission %s is assigned to %s", MissionName, MissionGroupName ) ) return self end @@ -698,7 +698,7 @@ end function MISSION:AddTask( Task ) local TaskName = Task:GetTaskName() - self:I( { "==> Adding TASK ", MissionName = self:GetName(), TaskName = TaskName } ) + self:T( { "==> Adding TASK ", MissionName = self:GetName(), TaskName = TaskName } ) self.Tasks[TaskName] = Task @@ -717,7 +717,7 @@ end function MISSION:RemoveTask( Task ) local TaskName = Task:GetTaskName() - self:I( { "<== Removing TASK ", MissionName = self:GetName(), TaskName = TaskName } ) + self:T( { "<== Removing TASK ", MissionName = self:GetName(), TaskName = TaskName } ) self:F( TaskName ) self.Tasks[TaskName] = self.Tasks[TaskName] or { n = 0 } diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index df656431b..d81856f3b 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -1061,8 +1061,8 @@ function UTILS.Vec2Norm(a) end --- Calculate the distance between two 2D vectors. --- @param DCS#Vec2 a Vector in 3D with x, y components. --- @param DCS#Vec2 b Vector in 3D with x, y components. +-- @param DCS#Vec2 a Vector in 2D with x, y components. +-- @param DCS#Vec2 b Vector in 2D with x, y components. -- @return #number Distance between the vectors. function UTILS.VecDist2D(a, b) @@ -1446,6 +1446,30 @@ function UTILS.GetCoalitionName(Coalition) end +--- Get the enemy coalition for a given coalition. +-- @param #number Coalition The coalition ID. +-- @param #boolean Neutral Include neutral as enemy. +-- @return #table Enemy coalition table. +function UTILS.GetCoalitionEnemy(Coalition, Neutral) + + local Coalitions={} + if Coalition then + if Coalition==coalition.side.RED then + Coalitions={coalition.side.BLUE} + elseif Coalition==coalition.side.BLUE then + Coalitions={coalition.side.RED} + elseif Coalition==coalition.side.NEUTRAL then + Coalitions={coalition.side.RED, coalition.side.BLUE} + end + end + + if Neutral then + table.insert(Coalitions, coalition.side.NEUTRAL) + end + + return Coalitions +end + --- Get the modulation name from its numerical value. -- @param #number Modulation The modulation enumerator number. Can be either 0 or 1. -- @return #string The modulation name, i.e. "AM"=0 or "FM"=1. Anything else will return "Unknown". @@ -1576,6 +1600,8 @@ function UTILS.GMTToLocalTimeDifference() return 3 -- Damascus is UTC+3 hours elseif theatre==DCSMAP.MarianaIslands then return 10 -- Guam is UTC+10 hours. + elseif theatre==DCSMAP.Falklands then + return -3 -- Fireland is UTC-3 hours. else BASE:E(string.format("ERROR: Unknown Map %s in UTILS.GMTToLocal function. Returning 0", tostring(theatre))) return 0 @@ -1907,7 +1933,7 @@ function UTILS.GenerateVHFrequencies() 705,720,722,730,735,740,745,750,770,795, 822,830,862,866, 905,907,920,935,942,950,995, - 1000,1025,1030,1050,1065,1116,1175,1182,1210 + 1000,1025,1030,1050,1065,1116,1175,1182,1210,1215 } local FreeVHFFrequencies = {} @@ -1975,7 +2001,9 @@ function UTILS.GenerateUHFrequencies() local _start = 220000000 while _start < 399000000 do - table.insert(FreeUHFFrequencies, _start) + if _start ~= 243000000 then + table.insert(FreeUHFFrequencies, _start) + end _start = _start + 500000 end @@ -2175,10 +2203,29 @@ function UTILS.CheckFileExists(Path,Filename) end end +--- Function to obtain a table of typenames from the group given with the number of units of the same type in the group. +-- @param Wrapper.Group#GROUP Group The group to list +-- @return #table Table of typnames and typename counts, e.g. `{["KAMAZ Truck"]=3,["ATZ-5"]=1}` +function UTILS.GetCountPerTypeName(Group) + local units = Group:GetUnits() + local TypeNameTable = {} + for _,_unt in pairs (units) do + local unit = _unt -- Wrapper.Unit#UNIT + local typen = unit:GetTypeName() + if not TypeNameTable[typen] then + TypeNameTable[typen] = 1 + else + TypeNameTable[typen] = TypeNameTable[typen] + 1 + end + end + return TypeNameTable +end + --- Function to save the state of a list of groups found by name -- @param #table List Table of strings with groupnames -- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. -- @param #string Filename The name of the file. +-- @param #boolean Structured Append the data with a list of typenames in the group plus their count. -- @return #boolean outcome True if saving is successful, else false. -- @usage -- We will go through the list and find the corresponding group and save the current group size (0 when dead). @@ -2186,7 +2233,7 @@ end -- Position is still saved for your usage. -- The idea is to reduce the number of units when reloading the data again to restart the saved mission. -- The data will be a simple comma separated list of groupname and size, with one header line. -function UTILS.SaveStationaryListOfGroups(List,Path,Filename) +function UTILS.SaveStationaryListOfGroups(List,Path,Filename,Structured) local filename = Filename or "StateListofGroups" local data = "--Save Stationary List of Groups: "..Filename .."\n" for _,_group in pairs (List) do @@ -2194,7 +2241,16 @@ function UTILS.SaveStationaryListOfGroups(List,Path,Filename) if group and group:IsAlive() then local units = group:CountAliveUnits() local position = group:GetVec3() - data = string.format("%s%s,%d,%d,%d,%d\n",data,_group,units,position.x,position.y,position.z) + if Structured then + local structure = UTILS.GetCountPerTypeName(group) + local strucdata = "" + for typen,anzahl in pairs (structure) do + strucdata = strucdata .. typen .. "=="..anzahl..";" + end + data = string.format("%s%s,%d,%d,%d,%d,%s\n",data,_group,units,position.x,position.y,position.z,strucdata) + else + data = string.format("%s%s,%d,%d,%d,%d\n",data,_group,units,position.x,position.y,position.z) + end else data = string.format("%s%s,0,0,0,0\n",data,_group) end @@ -2208,6 +2264,7 @@ end -- @param Core.Set#SET_BASE Set of objects to save -- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. -- @param #string Filename The name of the file. +-- @param #boolean Structured Append the data with a list of typenames in the group plus their count. -- @return #boolean outcome True if saving is successful, else false. -- @usage -- We will go through the set and find the corresponding group and save the current group size and current position. @@ -2217,7 +2274,7 @@ end -- **Note** Do NOT use dashes or hashes in group template names (-,#)! -- The data will be a simple comma separated list of groupname and size, with one header line. -- The current task/waypoint/etc cannot be restored. -function UTILS.SaveSetOfGroups(Set,Path,Filename) +function UTILS.SaveSetOfGroups(Set,Path,Filename,Structured) local filename = Filename or "SetOfGroups" local data = "--Save SET of groups: "..Filename .."\n" local List = Set:GetSetObjects() @@ -2231,7 +2288,16 @@ function UTILS.SaveSetOfGroups(Set,Path,Filename) end local units = group:CountAliveUnits() local position = group:GetVec3() - data = string.format("%s%s,%s,%d,%d,%d,%d\n",data,name,template,units,position.x,position.y,position.z) + if Structured then + local structure = UTILS.GetCountPerTypeName(group) + local strucdata = "" + for typen,anzahl in pairs (structure) do + strucdata = strucdata .. typen .. "=="..anzahl..";" + end + data = string.format("%s%s,%s,%d,%d,%d,%d,%s\n",data,name,template,units,position.x,position.y,position.z,strucdata) + else + data = string.format("%s%s,%s,%d,%d,%d,%d\n",data,name,template,units,position.x,position.y,position.z) + end end end -- save the data @@ -2295,8 +2361,41 @@ end -- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. -- @param #string Filename The name of the file. -- @param #boolean Reduce If false, existing loaded groups will not be reduced to fit the saved number. +-- @param #boolean Structured (Optional, needs Reduce = true) If true, and the data has been saved as structure before, remove the correct unit types as per the saved list. +-- @param #boolean Cinematic (Optional, needs Structured = true) If true, place a fire/smoke effect on the dead static position. +-- @param #number Effect (Optional for Cinematic) What effect to use. Defaults to a random effect. Smoke presets are: 1=small smoke and fire, 2=medium smoke and fire, 3=large smoke and fire, 4=huge smoke and fire, 5=small smoke, 6=medium smoke, 7=large smoke, 8=huge smoke. +-- @param #number Density (Optional for Cinematic) What smoke density to use, can be 0 to 1. Defaults to 0.5. -- @return #table Table of data objects (tables) containing groupname, coordinate and group object. Returns nil when file cannot be read. -function UTILS.LoadStationaryListOfGroups(Path,Filename,Reduce) +-- @return #table When using Cinematic: table of names of smoke and fire objects, so they can be extinguished with `COORDINATE.StopBigSmokeAndFire( name )` +function UTILS.LoadStationaryListOfGroups(Path,Filename,Reduce,Structured,Cinematic,Effect,Density) + + local fires = {} + + local function Smokers(name,coord,effect,density) + local eff = math.random(8) + if type(effect) == "number" then eff = effect end + coord:BigSmokeAndFire(eff,density,name) + table.insert(fires,name) + end + + local function Cruncher(group,typename,anzahl) + local units = group:GetUnits() + local reduced = 0 + for _,_unit in pairs (units) do + local typo = _unit:GetTypeName() + if typename == typo then + if Cinematic then + local coordinate = _unit:GetCoordinate() + local name = _unit:GetName() + Smokers(name,coordinate,Effect,Density) + end + _unit:Destroy(false) + reduced = reduced + 1 + if reduced == anzahl then break end + end + end + end + local reduce = true if Reduce == false then reduce = false end local filename = Filename or "StateListofGroups" @@ -2313,18 +2412,48 @@ function UTILS.LoadStationaryListOfGroups(Path,Filename,Reduce) local posx = tonumber(dataset[3]) local posy = tonumber(dataset[4]) local posz = tonumber(dataset[5]) + local structure = dataset[6] + --BASE:I({structure}) local coordinate = COORDINATE:NewFromVec3({x=posx, y=posy, z=posz}) local data = { groupname=groupname, size=size, coordinate=coordinate, group=GROUP:FindByName(groupname) } if reduce then local actualgroup = GROUP:FindByName(groupname) if actualgroup and actualgroup:IsAlive() and actualgroup:CountAliveUnits() > size then - local reduction = actualgroup:CountAliveUnits() - size - BASE:I("Reducing groupsize by ".. reduction .. " units!") - -- reduce existing group - local units = actualgroup:GetUnits() - local units2 = UTILS.ShuffleTable(units) -- randomize table - for i=1,reduction do - units2[i]:Destroy(false) + if Structured and structure then + --BASE:I("Reducing group structure!") + local loadedstructure = {} + local strcset = UTILS.Split(structure,";") + for _,_data in pairs(strcset) do + local datasplit = UTILS.Split(_data,"==") + loadedstructure[datasplit[1]] = tonumber(datasplit[2]) + end + --BASE:I({loadedstructure}) + local originalstructure = UTILS.GetCountPerTypeName(actualgroup) + --BASE:I({originalstructure}) + for _name,_number in pairs(originalstructure) do + local loadednumber = 0 + if loadedstructure[_name] then + loadednumber = loadedstructure[_name] + end + local reduce = false + if loadednumber < _number then reduce = true end + + --BASE:I(string.format("Looking at: %s | Original number: %d | Loaded number: %d | Reduce: %s",_name,_number,loadednumber,tostring(reduce))) + + if reduce then + Cruncher(actualgroup,_name,_number-loadednumber) + end + + end + else + local reduction = actualgroup:CountAliveUnits() - size + --BASE:I("Reducing groupsize by ".. reduction .. " units!") + -- reduce existing group + local units = actualgroup:GetUnits() + local units2 = UTILS.ShuffleTable(units) -- randomize table + for i=1,reduction do + units2[i]:Destroy(false) + end end end end @@ -2333,22 +2462,121 @@ function UTILS.LoadStationaryListOfGroups(Path,Filename,Reduce) else return nil end - return datatable + return datatable,fires end --- Load back a SET of groups from file. -- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. -- @param #string Filename The name of the file. -- @param #boolean Spawn If set to false, do not re-spawn the groups loaded in location and reduce to size. +-- @param #boolean Structured (Optional, needs Spawn=true)If true, and the data has been saved as structure before, remove the correct unit types as per the saved list. +-- @param #boolean Cinematic (Optional, needs Structured=true) If true, place a fire/smoke effect on the dead static position. +-- @param #number Effect (Optional for Cinematic) What effect to use. Defaults to a random effect. Smoke presets are: 1=small smoke and fire, 2=medium smoke and fire, 3=large smoke and fire, 4=huge smoke and fire, 5=small smoke, 6=medium smoke, 7=large smoke, 8=huge smoke. +-- @param #number Density (Optional for Cinematic) What smoke density to use, can be 0 to 1. Defaults to 0.5. -- @return Core.Set#SET_GROUP Set of GROUP objects. -- Returns nil when file cannot be read. Returns a table of data entries if Spawn is false: `{ groupname=groupname, size=size, coordinate=coordinate, template=template }` -function UTILS.LoadSetOfGroups(Path,Filename,Spawn) +-- @return #table When using Cinematic: table of names of smoke and fire objects, so they can be extinguished with `COORDINATE.StopBigSmokeAndFire( name )` +function UTILS.LoadSetOfGroups(Path,Filename,Spawn,Structured,Cinematic,Effect,Density) + + local fires = {} + local usedtemplates = {} local spawn = true if Spawn == false then spawn = false end - BASE:I("Spawn = "..tostring(spawn)) local filename = Filename or "SetOfGroups" local setdata = SET_GROUP:New() local datatable = {} + + local function Smokers(name,coord,effect,density) + local eff = math.random(8) + if type(effect) == "number" then eff = effect end + coord:BigSmokeAndFire(eff,density,name) + table.insert(fires,name) + end + + local function Cruncher(group,typename,anzahl) + local units = group:GetUnits() + local reduced = 0 + for _,_unit in pairs (units) do + local typo = _unit:GetTypeName() + if typename == typo then + if Cinematic then + local coordinate = _unit:GetCoordinate() + local name = _unit:GetName() + Smokers(name,coordinate,Effect,Density) + end + _unit:Destroy(false) + reduced = reduced + 1 + if reduced == anzahl then break end + end + end + end + + local function PostSpawn(args) + local spwndgrp = args[1] + local size = args[2] + local structure = args[3] + + setdata:AddObject(spwndgrp) + local actualsize = spwndgrp:CountAliveUnits() + if actualsize > size then + if Structured and structure then + + local loadedstructure = {} + local strcset = UTILS.Split(structure,";") + for _,_data in pairs(strcset) do + local datasplit = UTILS.Split(_data,"==") + loadedstructure[datasplit[1]] = tonumber(datasplit[2]) + end + + local originalstructure = UTILS.GetCountPerTypeName(spwndgrp) + + for _name,_number in pairs(originalstructure) do + local loadednumber = 0 + if loadedstructure[_name] then + loadednumber = loadedstructure[_name] + end + local reduce = false + if loadednumber < _number then reduce = true end + + if reduce then + Cruncher(spwndgrp,_name,_number-loadednumber) + end + + end + else + local reduction = actualsize-size + -- reduce existing group + local units = spwndgrp:GetUnits() + local units2 = UTILS.ShuffleTable(units) -- randomize table + for i=1,reduction do + units2[i]:Destroy(false) + end + end + end + end + + local function MultiUse(Data) + local template = Data.template + if template and usedtemplates[template] and usedtemplates[template].used and usedtemplates[template].used > 1 then + -- multispawn + if not usedtemplates[template].done then + local spwnd = 0 + local spawngrp = SPAWN:New(template) + spawngrp:InitLimit(0,usedtemplates[template].used) + for _,_entry in pairs(usedtemplates[template].data) do + spwnd = spwnd + 1 + local sgrp=spawngrp:SpawnFromCoordinate(_entry.coordinate,spwnd) + BASE:ScheduleOnce(0.5,PostSpawn,{sgrp,_entry.size,_entry.structure}) + end + usedtemplates[template].done = true + end + return true + else + return false + end + end + + --BASE:I("Spawn = "..tostring(spawn)) if UTILS.CheckFileExists(Path,filename) then local outcome,loadeddata = UTILS.LoadFromFile(Path,Filename) -- remove header @@ -2362,36 +2590,37 @@ function UTILS.LoadSetOfGroups(Path,Filename,Spawn) local posx = tonumber(dataset[4]) local posy = tonumber(dataset[5]) local posz = tonumber(dataset[6]) + local structure = dataset[7] local coordinate = COORDINATE:NewFromVec3({x=posx, y=posy, z=posz}) local group=nil - local data = { groupname=groupname, size=size, coordinate=coordinate, template=template } - table.insert(datatable,data) - if spawn then - local group = SPAWN:New(template) - :InitDelayOff() - :OnSpawnGroup( - function(spwndgrp) - setdata:AddObject(spwndgrp) - local actualsize = spwndgrp:CountAliveUnits() - if actualsize > size then - local reduction = actualsize-size - -- reduce existing group - local units = spwndgrp:GetUnits() - local units2 = UTILS.ShuffleTable(units) -- randomize table - for i=1,reduction do - units2[i]:Destroy(false) - end - end - end - ) - :SpawnFromCoordinate(coordinate) + if size > 0 then + local data = { groupname=groupname, size=size, coordinate=coordinate, template=template, structure=structure } + table.insert(datatable,data) + if usedtemplates[template] then + usedtemplates[template].used = usedtemplates[template].used + 1 + table.insert(usedtemplates[template].data,data) + else + usedtemplates[template] = { + data = {}, + used = 1, + done = false, + } + table.insert(usedtemplates[template].data,data) + end + end + end + for _id,_entry in pairs (datatable) do + if spawn and not MultiUse(_entry) and _entry.size > 0 then + local group = SPAWN:New(_entry.template) + local sgrp=group:SpawnFromCoordinate(_entry.coordinate) + BASE:ScheduleOnce(0.5,PostSpawn,{sgrp,_entry.size,_entry.structure}) end end else return nil end if spawn then - return setdata + return setdata,fires else return datatable end @@ -2410,13 +2639,11 @@ function UTILS.LoadSetOfStatics(Path,Filename) table.remove(loadeddata, 1) for _id,_entry in pairs (loadeddata) do local dataset = UTILS.Split(_entry,",") - -- staticname,position.x,position.y,position.z local staticname = dataset[1] - local posx = tonumber(dataset[2]) - local posy = tonumber(dataset[3]) - local posz = tonumber(dataset[4]) - local coordinate = COORDINATE:NewFromVec3({x=posx, y=posy, z=posz}) - datatable:AddObject(STATIC:FindByName(staticname,false)) + local StaticObject = STATIC:FindByName(staticname,false) + if StaticObject then + datatable:AddObject(StaticObject) + end end else return nil @@ -2428,9 +2655,15 @@ end -- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. -- @param #string Filename The name of the file. -- @param #boolean Reduce If false, do not destroy the units with size=0. --- @return #table Table of data objects (tables) containing staticname, size (0=dead else 1), coordinate and the static object. +-- @param #boolean Dead (Optional, needs Reduce = true) If Dead is true, re-spawn the dead object as dead and do not just delete it. +-- @param #boolean Cinematic (Optional, needs Dead = true) If true, place a fire/smoke effect on the dead static position. +-- @param #number Effect (Optional for Cinematic) What effect to use. Defaults to a random effect. Smoke presets are: 1=small smoke and fire, 2=medium smoke and fire, 3=large smoke and fire, 4=huge smoke and fire, 5=small smoke, 6=medium smoke, 7=large smoke, 8=huge smoke. +-- @param #number Density (Optional for Cinematic) What smoke density to use, can be 0 to 1. Defaults to 0.5. +-- @return #table Table of data objects (tables) containing staticname, size (0=dead else 1), coordinate and the static object. Dead objects will have coordinate points `{x=0,y=0,z=0}` +-- @return #table When using Cinematic: table of names of smoke and fire objects, so they can be extinguished with `COORDINATE.StopBigSmokeAndFire( name )` -- Returns nil when file cannot be read. -function UTILS.LoadStationaryListOfStatics(Path,Filename,Reduce) +function UTILS.LoadStationaryListOfStatics(Path,Filename,Reduce,Dead,Cinematic,Effect,Density) + local fires = {} local reduce = true if Reduce == false then reduce = false end local filename = Filename or "StateListofStatics" @@ -2453,14 +2686,31 @@ function UTILS.LoadStationaryListOfStatics(Path,Filename,Reduce) if size==0 and reduce then local static = STATIC:FindByName(staticname,false) if static then - static:Destroy(false) + if Dead then + local deadobject = SPAWNSTATIC:NewFromStatic(staticname,static:GetCountry()) + deadobject:InitDead(true) + local heading = static:GetHeading() + local coord = static:GetCoordinate() + static:Destroy(false) + deadobject:SpawnFromCoordinate(coord,heading,staticname) + if Cinematic then + local effect = math.random(8) + if type(Effect) == "number" then + effect = Effect + end + coord:BigSmokeAndFire(effect,Density,staticname) + table.insert(fires,staticname) + end + else + static:Destroy(false) + end end end end else return nil end - return datatable + return datatable,fires end --- Heading Degrees (0-360) to Cardinal @@ -2485,7 +2735,7 @@ end -- @return #string Formatted BRAA NATO call function UTILS.ToStringBRAANATO(FromGrp,ToGrp) local BRAANATO = "Merged." - local GroupNumber = FromGrp:GetSize() + local GroupNumber = ToGrp:GetSize() local GroupWords = "Singleton" if GroupNumber == 2 then GroupWords = "Two-Ship" elseif GroupNumber >= 3 then GroupWords = "Heavy" @@ -2509,3 +2759,51 @@ function UTILS.ToStringBRAANATO(FromGrp,ToGrp) end return BRAANATO end + +--- Check if an object is contained in a table. +-- @param #table Table The table. +-- @param #table Object The object to check. +-- @param #string Key (Optional) Key to check. By default, the object itself is checked. +-- @return #booolen Returns `true` if object is in table. +function UTILS.IsInTable(Table, Object, Key) + + for key, object in pairs(Table) do + if Key then + if Object[Key]==object[Key] then + return true + end + else + if object==Object then + return true + end + end + end + + return false +end + +--- Check if any object of multiple given objects is contained in a table. +-- @param #table Table The table. +-- @param #table Objects The objects to check. +-- @param #string Key (Optional) Key to check. +-- @return #booolen Returns `true` if object is in table. +function UTILS.IsAnyInTable(Table, Objects, Key) + + for _,Object in pairs(UTILS.EnsureTable(Objects)) do + + for key, object in pairs(Table) do + if Key then + if Object[Key]==object[Key] then + return true + end + else + if object==Object then + return true + end + end + end + + end + + return false +end diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index f4f658477..6c283d002 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -246,7 +246,7 @@ AIRBASE.Normandy = { -- -- * AIRBASE.PersianGulf.Abu_Dhabi_International_Airport -- * AIRBASE.PersianGulf.Abu_Musa_Island_Airport --- * AIRBASE.PersianGulf.Al-Bateen_Airport +-- * AIRBASE.PersianGulf.Al_Bateen_Airport -- * AIRBASE.PersianGulf.Al_Ain_International_Airport -- * AIRBASE.PersianGulf.Al_Dhafra_AB -- * AIRBASE.PersianGulf.Al_Maktoum_Intl @@ -265,7 +265,7 @@ AIRBASE.Normandy = { -- * AIRBASE.PersianGulf.Lavan_Island_Airport -- * AIRBASE.PersianGulf.Liwa_Airbase -- * AIRBASE.PersianGulf.Qeshm_Island --- * AIRBASE.PersianGulf.Ras_Al_Khaimah_International_Airport +-- * AIRBASE.PersianGulf.Ras_Al_Khaimah -- * AIRBASE.PersianGulf.Sas_Al_Nakheel_Airport -- * AIRBASE.PersianGulf.Sharjah_Intl -- * AIRBASE.PersianGulf.Shiraz_International_Airport @@ -510,6 +510,11 @@ AIRBASE.MarianaIslands = { -- * AIRBASE.SouthAtlantic.Porvenir_Airfield -- * AIRBASE.SouthAtlantic.Almirante_Schroeders -- * AIRBASE.SouthAtlantic.Rio_Turbio +-- * AIRBASE.SouthAtlantic.Rio_Chico +-- * AIRBASE.SouthAtlantic.Franco_Bianco +-- * AIRBASE.SouthAtlantic.Goose_Green +-- * AIRBASE.SouthAtlantic.Hipico +-- * AIRBASE.SouthAtlantic.CaletaTortel -- --@field MarianaIslands AIRBASE.SouthAtlantic={ @@ -532,6 +537,11 @@ AIRBASE.SouthAtlantic={ ["Porvenir_Airfield"]="Porvenir Airfield", ["Almirante_Schroeders"]="Almirante Schroeders", ["Rio_Turbio"]="Rio Turbio", + ["Rio_Chico"] = "Rio Chico", + ["Franco_Bianco"] = "Franco Bianco", + ["Goose_Green"] = "Goose Green", + ["Hipico"] = "Hipico", + ["CaletaTortel"] = "CaletaTortel", } --- AIRBASE.ParkingSpot ".Coordinate, ".TerminalID", ".TerminalType", ".TOAC", ".Free", ".TerminalID0", ".DistToRwy". @@ -685,6 +695,9 @@ function AIRBASE:Register(AirbaseName) else self:E(string.format("ERROR: Cound not get position Vec2 of airbase %s", AirbaseName)) end + + -- Debug info. + self:T2(string.format("Registered airbase %s", tostring(self.AirbaseName))) return self end @@ -836,7 +849,7 @@ end -- Black listed spots overrule white listed spots. -- **NOTE** that terminal IDs are not necessarily the same as those displayed in the mission editor! -- @param #AIRBASE self --- @param #table TerminalIdBlacklist Table of white listed terminal IDs. +-- @param #table TerminalIdWhitelist Table of white listed terminal IDs. -- @return #AIRBASE self -- @usage AIRBASE:FindByName("Batumi"):SetParkingSpotWhitelist({2, 3, 4}) --Only allow terminal IDs 2, 3, 4 function AIRBASE:SetParkingSpotWhitelist(TerminalIdWhitelist) @@ -1365,7 +1378,7 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, local _nspots=nspots or group:GetSize() -- Debug info. - self:E(string.format("%s: Looking for %d parking spot(s) for aircraft of size %.1f m (x=%.1f,y=%.1f,z=%.1f) at terminal type %s.", airport, _nspots, _aircraftsize, ax, ay, az, tostring(terminaltype))) + self:T(string.format("%s: Looking for %d parking spot(s) for aircraft of size %.1f m (x=%.1f,y=%.1f,z=%.1f) at terminal type %s.", airport, _nspots, _aircraftsize, ax, ay, az, tostring(terminaltype))) -- Table of valid spots. local validspots={} diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 95c8c4ade..0c743259b 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -67,7 +67,6 @@ -- * @{#CONTROLLABLE.TaskRouteToVec2}: (AIR + GROUND) Make the Controllable move to a given point. -- * @{#CONTROLLABLE.TaskRouteToVec3}: (AIR + GROUND) Make the Controllable move to a given point. -- * @{#CONTROLLABLE.TaskRouteToZone}: (AIR + GROUND) Route the controllable to a given zone. --- * @{#CONTROLLABLE.TaskReturnToBase}: (AIR) Route the controllable to an airbase. -- -- ## 2.2) EnRoute assignment -- @@ -669,12 +668,61 @@ function CONTROLLABLE:CommandActivateBeacon( Type, System, Frequency, UnitID, Ch return self end +--- Activate ACLS system of the CONTROLLABLE. The controllable should be an aircraft carrier! Also needs Link4 to work. +-- @param #CONTROLLABLE self +-- @param #number UnitID (Optional) The DCS UNIT ID of the unit the ACLS system is attached to. Defaults to the UNIT itself. +-- @param #string Name (Optional) Name of the ACLS Beacon +-- @param #number Delay (Optional) Delay in seconds before the ICLS is activated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandActivateACLS( UnitID, Name, Delay ) + + -- Command to activate ACLS system. + local CommandActivateACLS= { + id = 'ActivateACLS', + params = { + unitId = UnitID or self:GetID(), + name = Name or "ACL", + } +} + + self:T({CommandActivateACLS}) + + if Delay and Delay > 0 then + SCHEDULER:New( nil, self.CommandActivateACLS, { self, UnitID, Name }, Delay ) + else + self:SetCommand( CommandActivateACLS ) + end + + return self +end + +--- Deactivate ACLS system of the CONTROLLABLE. The controllable should be an aircraft carrier! +-- @param #CONTROLLABLE self +-- @param #number Delay (Optional) Delay in seconds before the ICLS is deactivated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandDeactivateACLS( Delay ) + + -- Command to activate ACLS system. + local CommandDeactivateACLS= { + id = 'DeactivateACLS', + params = { } +} + + if Delay and Delay > 0 then + SCHEDULER:New( nil, self.CommandDeactivateACLS, { self }, Delay ) + else + self:SetCommand( CommandDeactivateACLS ) + end + + return self +end + --- Activate ICLS system of the CONTROLLABLE. The controllable should be an aircraft carrier! -- @param #CONTROLLABLE self -- @param #number Channel ICLS channel. -- @param #number UnitID The DCS UNIT ID of the unit the ICLS system is attached to. Useful if more units are in one group. -- @param #string Callsign Morse code identification callsign. --- @param #number Delay (Optional) Delay in seconds before the ICLS is deactivated. +-- @param #number Delay (Optional) Delay in seconds before the ICLS is activated. -- @return #CONTROLLABLE self function CONTROLLABLE:CommandActivateICLS( Channel, UnitID, Callsign, Delay ) @@ -684,13 +732,13 @@ function CONTROLLABLE:CommandActivateICLS( Channel, UnitID, Callsign, Delay ) params = { ["type"] = BEACON.Type.ICLS, ["channel"] = Channel, - ["unitId"] = UnitID, + ["unitId"] = UnitID or self:GetID(), ["callsign"] = Callsign, }, } if Delay and Delay > 0 then - SCHEDULER:New( nil, self.CommandActivateICLS, { self }, Delay ) + SCHEDULER:New( nil, self.CommandActivateICLS, { self, Channel, UnitID, Callsign }, Delay ) else self:SetCommand( CommandActivateICLS ) end @@ -700,53 +748,29 @@ end --- Activate LINK4 system of the CONTROLLABLE. The controllable should be an aircraft carrier! -- @param #CONTROLLABLE self --- @param #number Frequency Link4 Frequency in MHz, e.g. 336 --- @param #number UnitID The DCS UNIT ID of the unit the LINK4 system is attached to. Useful if more units are in one group. --- @param #string Callsign Morse code identification callsign. --- @param #number Delay (Optional) Delay in seconds before the LINK4 is deactivated. +-- @param #number Frequency Link4 Frequency in MHz, e.g. 336 (defaults to 336 MHz) +-- @param #number UnitID (Optional) The DCS UNIT ID of the unit the LINK4 system is attached to. Defaults to the UNIT itself. +-- @param #string Callsign (Optional) Morse code identification callsign. +-- @param #number Delay (Optional) Delay in seconds before the LINK4 is activated. -- @return #CONTROLLABLE self function CONTROLLABLE:CommandActivateLink4(Frequency, UnitID, Callsign, Delay) - + + local freq = Frequency or 336 + -- Command to activate Link4 system. local CommandActivateLink4= { id = "ActivateLink4", params= { - ["frequency "] = Frequency*1000, - ["unitId"] = UnitID, - ["name"] = Callsign, + ["frequency "] = freq*1000000, + ["unitId"] = UnitID or self:GetID(), + ["name"] = Callsign or "LNK", } } - + + self:T({CommandActivateLink4}) + if Delay and Delay>0 then - SCHEDULER:New(nil, self.CommandActivateLink4, {self}, Delay) - else - self:SetCommand(CommandActivateLink4) - end - - return self -end - ---- Activate LINK4 system of the CONTROLLABLE. The controllable should be an aircraft carrier! --- @param #CONTROLLABLE self --- @param #number Frequency Link4 Frequency in MHz, e.g. 336 --- @param #number UnitID The DCS UNIT ID of the unit the LINK4 system is attached to. Useful if more units are in one group. --- @param #string Callsign Morse code identification callsign. --- @param #number Delay (Optional) Delay in seconds before the LINK4 is deactivated. --- @return #CONTROLLABLE self -function CONTROLLABLE:CommandActivateLink4(Frequency, UnitID, Callsign, Delay) - - -- Command to activate Link4 system. - local CommandActivateLink4= { - id = "ActivateLink4", - params= { - ["frequency "] = Frequency*1000, - ["unitId"] = UnitID, - ["name"] = Callsign, - } - } - - if Delay and Delay>0 then - SCHEDULER:New(nil, self.CommandActivateLink4, {self}, Delay) + SCHEDULER:New(nil, self.CommandActivateLink4, {self, Frequency, UnitID, Callsign}, Delay) else self:SetCommand(CommandActivateLink4) end @@ -810,24 +834,6 @@ function CONTROLLABLE:CommandDeactivateICLS( Delay ) return self end ---- Deactivate the active Link4 of the CONTROLLABLE. --- @param #CONTROLLABLE self --- @param #number Delay (Optional) Delay in seconds before the Link4 is deactivated. --- @return #CONTROLLABLE self -function CONTROLLABLE:CommandDeactivateLink4(Delay) - - -- Command to deactivate - local CommandDeactivateLink4={id='DeactivateLink4', params={}} - - if Delay and Delay>0 then - SCHEDULER:New(nil, self.CommandDeactivateLink4, {self}, Delay) - else - self:SetCommand(CommandDeactivateLink4) - end - - return self -end - --- Set callsign of the CONTROLLABLE. See [DCS command setCallsign](https://wiki.hoggitworld.com/view/DCS_command_setCallsign) -- @param #CONTROLLABLE self -- @param DCS#CALLSIGN CallName Number corresponding the the callsign identifier you wish this group to be called. @@ -1416,7 +1422,7 @@ end -- @param #CONTROLLABLE self -- @param Core.Zone#ZONE Zone The zone where to land. -- @param #number Duration The duration in seconds to stay on the ground. --- @return #CONTROLLABLE self +-- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskLandAtZone( Zone, Duration, RandomPoint ) -- Get landing point @@ -1665,6 +1671,26 @@ function CONTROLLABLE:EnRouteTaskAntiShip(TargetTypes, Priority) return DCSTask end +--- (AIR) Enroute SEAD task. +-- @param #CONTROLLABLE self +-- @param DCS#AttributeNameArray TargetTypes Array of target categories allowed to engage. Default `{"Air Defence"}`. +-- @param #number Priority (Optional) All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. Default 0. +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:EnRouteTaskSEAD(TargetTypes, Priority) + + local DCSTask = { + id = 'EngageTargets', + key = "SEAD", + --auto = false, + --enabled = true, + params = { + targetTypes = TargetTypes or {"Air Defence"}, + priority = Priority or 0 + } + } + + return DCSTask +end --- (AIR) Engaging a controllable. The task does not assign the target controllable to the unit/controllable to attack now; it just allows the unit/controllable to engage the target controllable as well as other assigned targets. -- @param #CONTROLLABLE self @@ -2395,7 +2421,7 @@ do -- Route methods -- @return DCS#Task Task. -- @return #boolean If true, path on road is possible. If false, task will route the group directly to its destination. function CONTROLLABLE:TaskGroundOnRoad( ToCoordinate, Speed, OffRoadFormation, Shortcut, FromCoordinate, WaypointFunction, WaypointFunctionArguments ) - self:I( { ToCoordinate = ToCoordinate, Speed = Speed, OffRoadFormation = OffRoadFormation, WaypointFunction = WaypointFunction, Args = WaypointFunctionArguments } ) + self:T( { ToCoordinate = ToCoordinate, Speed = Speed, OffRoadFormation = OffRoadFormation, WaypointFunction = WaypointFunction, Args = WaypointFunctionArguments } ) -- Defaults. Speed = Speed or 20 @@ -3954,4 +3980,4 @@ function CONTROLLABLE:SetAltitude(Altitude, Keep, AltType) end end return self -end \ No newline at end of file +end diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index 08853948a..19c2b9fd6 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -624,10 +624,9 @@ function GROUP:GetRange() return nil end - --- Returns a list of @{Wrapper.Unit} objects of the @{Wrapper.Group}. -- @param #GROUP self --- @return #list The list of @{Wrapper.Unit} objects of the @{Wrapper.Group}. +-- @return #table of Wrapper.Unit#UNIT objects, indexed by number. function GROUP:GetUnits() self:F2( { self.GroupName } ) local DCSGroup = self:GetDCSObject() @@ -645,7 +644,6 @@ function GROUP:GetUnits() return nil end - --- Returns a list of @{Wrapper.Unit} objects of the @{Wrapper.Group} that are occupied by a player. -- @param #GROUP self -- @return #list The list of player occupied @{Wrapper.Unit} objects of the @{Wrapper.Group}. @@ -676,41 +674,38 @@ function GROUP:IsPlayer() return self:GetUnit(1):IsPlayer() end ---- Returns the UNIT wrapper class with number UnitNumber. --- If the underlying DCS Unit does not exist, the method will return nil. . +--- Returns the UNIT wrapper object with number UnitNumber. If it doesn't exist, tries to return the next available unit. +-- If no underlying DCS Units exist, the method will return nil. -- @param #GROUP self -- @param #number UnitNumber The number of the UNIT wrapper class to be returned. --- @return Wrapper.Unit#UNIT The UNIT wrapper class. +-- @return Wrapper.Unit#UNIT The UNIT object or nil function GROUP:GetUnit( UnitNumber ) - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - + if DCSGroup then local UnitFound = nil -- 2.7.1 dead event bug, return the first alive unit instead - local units = DCSGroup:getUnits() or {} - - for _,_unit in pairs(units) do - - local UnitFound = UNIT:Find(_unit) - + -- Maybe fixed with 2.8? + local units = DCSGroup:getUnits() or {} + if units[UnitNumber] then + local UnitFound = UNIT:Find(units[UnitNumber]) if UnitFound then - return UnitFound - + end + else + for _,_unit in pairs(units) do + local UnitFound = UNIT:Find(_unit) + if UnitFound then + return UnitFound + end end end - end - - return nil - + return nil end --- Returns the DCS Unit with number UnitNumber. --- If the underlying DCS Unit does not exist, the method will return nil. . +-- If the underlying DCS Unit does not exist, the method will return try to find the next unit. Returns nil if no units are found. -- @param #GROUP self -- @param #number UnitNumber The number of the DCS Unit to be returned. -- @return DCS#Unit The DCS Unit. @@ -723,8 +718,7 @@ function GROUP:GetDCSUnit( UnitNumber ) if DCSGroup.getUnit and DCSGroup:getUnit( UnitNumber ) then return DCSGroup:getUnit( UnitNumber ) else - - local UnitFound = nil + -- 2.7.1 dead event bug, return the first alive unit instead local units = DCSGroup:getUnits() or {} @@ -803,7 +797,20 @@ function GROUP:GetFirstUnitAlive() return nil end +--- Get the first unit of the group. Might be nil! +-- @param #GROUP self +-- @return Wrapper.Unit#UNIT First unit or nil if it does not exist. +function GROUP:GetFirstUnit() + self:F3({self.GroupName}) + local DCSGroup = self:GetDCSObject() + if DCSGroup then + local units=self:GetUnits() + return units[1] + end + + return nil +end --- Returns the average velocity Vec3 vector. -- @param Wrapper.Group#GROUP self diff --git a/Moose Development/Moose/Wrapper/Marker.lua b/Moose Development/Moose/Wrapper/Marker.lua index 21936ae46..4bb7f9c7b 100644 --- a/Moose Development/Moose/Wrapper/Marker.lua +++ b/Moose Development/Moose/Wrapper/Marker.lua @@ -58,18 +58,16 @@ -- If the maker should be visible to a specific coalition, you can use the :ToCoalition() function. -- -- mymarker = MARKER:New( Coordinate , "I am Batumi Airfield" ):ToCoalition( coalition.side.BLUE ) --- --- ### To Blue Coalition --- --- ### To Red Coalition --- +-- -- This would show the marker only to the Blue coalition. -- -- ## For a Group -- +-- mymarker = MARKER:New( Coordinate , "Target Location" ):ToGroup( tankGroup ) -- -- # Removing a Marker --- +-- mymarker:Remove(60) +-- This removes the marker after 60 seconds -- -- # Updating a Marker -- @@ -175,8 +173,6 @@ function MARKER:New( Coordinate, Text ) -- Inherit everything from FSM class. local self = BASE:Inherit( self, FSM:New() ) -- #MARKER - local self=BASE:Inherit(self, FSM:New()) -- #MARKER - self.coordinate=UTILS.DeepCopy(Coordinate) self.text = Text @@ -307,7 +303,7 @@ end -- User API Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Marker is readonly. Text cannot be changed and marker cannot be removed. +--- Marker is readonly. Text cannot be changed and marker cannot be removed. The will not update the marker in the game, Call MARKER:Refresh to update state. -- @param #MARKER self -- @return #MARKER self function MARKER:ReadOnly() @@ -317,7 +313,7 @@ function MARKER:ReadOnly() return self end ---- Marker is readonly. Text cannot be changed and marker cannot be removed. +--- Marker is read and write. Text cannot be changed and marker cannot be removed. The will not update the marker in the game, Call MARKER:Refresh to update state. -- @param #MARKER self -- @return #MARKER self function MARKER:ReadWrite() diff --git a/Moose Development/Moose/Wrapper/Scenery.lua b/Moose Development/Moose/Wrapper/Scenery.lua index 4cd321063..9f40bc230 100644 --- a/Moose Development/Moose/Wrapper/Scenery.lua +++ b/Moose Development/Moose/Wrapper/Scenery.lua @@ -225,3 +225,10 @@ function SCENERY:FindAllByZoneName( ZoneName ) end end end + +--- SCENERY objects cannot be destroyed via the API (at the punishment of game crash). +--@param #SCENERY self +--@return #SCENERY self +function SCENERY:Destroy() + return self +end \ No newline at end of file diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index ecff1ef41..e9575c69a 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -229,7 +229,7 @@ function UNIT:ReSpawnAt( Coordinate, Heading ) SpawnGroupTemplate.y = Coordinate.z self:F( #SpawnGroupTemplate.units ) - for UnitID, UnitData in pairs( SpawnGroup:GetUnits() ) do + for UnitID, UnitData in pairs( SpawnGroup:GetUnits() or {} ) do local GroupUnit = UnitData -- #UNIT self:F( GroupUnit:GetName() ) if GroupUnit:IsAlive() then @@ -630,21 +630,23 @@ end -- @param Wrapper.Unit#UNIT self -- @return Wrapper.Group#GROUP The Group of the Unit or `nil` if the unit does not exist. function UNIT:GetGroup() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitGroup = GROUP:FindByName( DCSUnit:getGroup():getName() ) + self:F2( self.UnitName ) + local UnitGroup = GROUP:FindByName(self.GroupName) + if UnitGroup then return UnitGroup + else + local DCSUnit = self:GetDCSObject() + if DCSUnit then + local grp = DCSUnit:getGroup() + if grp then + local UnitGroup = GROUP:FindByName( grp:getName() ) + return UnitGroup + end + end end - return nil end - --- Need to add here functions to check if radar is on and which object etc. - --- Returns the prefix name of the DCS Unit. A prefix name is a part of the name before a '#'-sign. -- DCS Units spawned with the @{Core.Spawn#SPAWN} class contain a '#'-sign to indicate the end of the (base) DCS Unit name. -- The spawn sequence number and unit number are contained within the name after the '#' sign. diff --git a/README.md b/README.md index 69ab8403a..aec3cbd3a 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ Pene has kindly created a [tutorial series for MOOSE](https://youtube.com/playli -## [MOOSE on Discord](https://discord.gg/yBPfxC6) +## [MOOSE on Discord](https://discord.gg/aQtjcR94Qf) MOOSE has a living (chat and video) community of users, beta testers and contributors. The gathering point is a service provided by discord.com. If you want to join this community, just click Discord and you'll be on board in no time.