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/Event.lua b/Moose Development/Moose/Core/Event.lua index 9b334bf2a..595e094f4 100644 --- a/Moose Development/Moose/Core/Event.lua +++ b/Moose Development/Moose/Core/Event.lua @@ -1224,7 +1224,7 @@ function EVENT:onEvent( Event ) if Event.TgtObjectCategory == Object.Category.STATIC then -- get base data Event.TgtDCSUnit = Event.target - if Event.target:isExist() and Event.id ~= 33 then -- leave out ejected seat object + if Event.target:isExist() and Event.id ~= 33 and not Event.TgtObjectCategory == Object.Category.COORDINATE then -- leave out ejected seat object Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() Event.TgtUnitName = Event.TgtDCSUnitName Event.TgtUnit = STATIC:FindByName( Event.TgtDCSUnitName, false ) diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index a4a3384cb..b973172c1 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -186,7 +186,7 @@ do -- SET_BASE return Names end - --- Return a table of the Objects in the Set. + --- Returns a table of the Objects in the Set. -- @param #SET_BASE self -- @return #table Table of objects. function SET_BASE:GetSetObjects() -- R2.3 @@ -388,7 +388,6 @@ do -- SET_BASE -- @param #SET_BASE self -- @return Core.Base#BASE function SET_BASE:GetFirst() - local ObjectName = self.Index[1] local FirstObject = self.Set[ObjectName] self:T3( { FirstObject } ) @@ -399,8 +398,8 @@ do -- SET_BASE -- @param #SET_BASE self -- @return Core.Base#BASE function SET_BASE:GetLast() - - local ObjectName = self.Index[#self.Index] + local tablemax = table.maxn(self.Index) + local ObjectName = self.Index[tablemax] local LastObject = self.Set[ObjectName] self:T3( { LastObject } ) return LastObject @@ -410,8 +409,8 @@ do -- SET_BASE -- @param #SET_BASE self -- @return Core.Base#BASE function SET_BASE:GetRandom() - - local RandomItem = self.Set[self.Index[math.random( #self.Index )]] + local tablemax = table.maxn(self.Index) + local RandomItem = self.Set[self.Index[math.random(1,tablemax)]] self:T3( { RandomItem } ) return RandomItem end @@ -420,8 +419,7 @@ do -- SET_BASE -- @param #SET_BASE self -- @return #number Count function SET_BASE:Count() - - return self.Index and #self.Index or 0 + return self.Index and table.maxn(self.Index) or 0 end --- Copies the Filter criteria from a given Set (for rebuilding a new Set based on an existing Set). @@ -2007,12 +2005,7 @@ do -- SET_UNIT -- * @{#SET_UNIT.ForEachUnit}: Calls a function for each alive unit it finds within the SET_UNIT. -- * @{#SET_UNIT.ForEachUnitInZone}: Iterate the SET_UNIT and call an iterator function for each **alive** UNIT object presence completely in a @{Core.Zone}, providing the UNIT object and optional parameters to the called function. -- * @{#SET_UNIT.ForEachUnitNotInZone}: Iterate the SET_UNIT and call an iterator function for each **alive** UNIT object presence not in a @{Core.Zone}, providing the UNIT object and optional parameters to the called function. - -- - -- Planned iterators methods in development are (so these are not yet available): - -- - -- * @{#SET_UNIT.ForEachUnitInUnit}: Calls a function for each unit contained within the SET_UNIT. - -- * @{#SET_UNIT.ForEachUnitCompletelyInZone}: Iterate and call an iterator function for each **alive** UNIT presence completely in a @{Core.Zone}, providing the UNIT and optional parameters to the called function. - -- * @{#SET_UNIT.ForEachUnitNotInZone}: Iterate and call an iterator function for each **alive** UNIT presence not in a @{Core.Zone}, providing the UNIT and optional parameters to the called function. + -- * @{#SET_UNIT:ForEachUnitPerThreatLevel}: Iterate the SET_UNIT **sorted *per Threat Level** and call an iterator function for each **alive** UNIT, providing the UNIT and optional parameters -- -- ## 5) SET_UNIT atomic methods -- @@ -3865,6 +3858,8 @@ do -- SET_CLIENT Countries = nil, ClientPrefixes = nil, Zones = nil, + Playernames = nil, + Callsigns = nil, }, FilterMeta = { Coalitions = { @@ -3937,6 +3932,40 @@ do -- SET_CLIENT return ClientFound end + --- Builds a set of clients of certain callsigns. + -- @param #SET_CLIENT self + -- @param #string Callsigns Can be a single string e.g. "Ford", or a table of strings e.g. {"Uzi","Enfield","Chevy"}. Refers to the callsigns as they can be set in the mission editor. + -- @return #SET_CLIENT self + function SET_CLIENT:FilterCallsigns( Callsigns ) + if not self.Filter.Callsigns then + self.Filter.Callsigns = {} + end + if type( Callsigns ) ~= "table" then + Callsigns = { Callsigns } + end + for callsignID, callsign in pairs( Callsigns ) do + self.Filter.Callsigns[callsign] = callsign + end + return self + end + + --- Builds a set of clients of certain playernames. + -- @param #SET_CLIENT self + -- @param #string Playernames Can be a single string e.g. "Apple", or a table of strings e.g. {"Walter","Hermann","Gonzo"}. Useful if you have e.g. a common squadron prefix. + -- @return #SET_CLIENT self + function SET_CLIENT:FilterPlayernames( Playernames ) + if not self.Filter.Playernames then + self.Filter.Playernames = {} + end + if type( Playernames ) ~= "table" then + Playernames = { Playernames } + end + for PlayernameID, playername in pairs( Playernames ) do + self.Filter.Playernames[playername] = playername + end + return self + end + --- Builds a set of clients of coalitions. -- Possible current coalitions are red, blue and neutral. -- @param #SET_CLIENT self @@ -4224,9 +4253,10 @@ do -- SET_CLIENT if self.Filter.Active ~= nil then local MClientActive = false - if self.Filter.Active == false or (self.Filter.Active == true and MClient:IsActive() == true) then + if self.Filter.Active == false or (self.Filter.Active == true and MClient:IsActive() == true and MClient:IsAlive() == true) then MClientActive = true end + --self:I( { "Evaluated Active", MClientActive } ) MClientInclude = MClientInclude and MClientActive end @@ -4292,20 +4322,46 @@ do -- SET_CLIENT self:T( { "Evaluated Prefix", MClientPrefix } ) MClientInclude = MClientInclude and MClientPrefix end - end if self.Filter.Zones then local MClientZone = false for ZoneName, Zone in pairs( self.Filter.Zones ) do - self:T3( "Zone:", ZoneName ) - local unit = MClient:GetClientGroupUnit() - if unit and unit:IsInZone(Zone) then - MClientZone = true - end + self:T3( "Zone:", ZoneName ) + local unit = MClient:GetClientGroupUnit() + if unit and unit:IsInZone(Zone) then + MClientZone = true + end end MClientInclude = MClientInclude and MClientZone end + if self.Filter.Playernames then + local MClientPlayername = false + local playername = MClient:GetPlayerName() or "Unknown" + --self:I(playername) + for _,_Playername in pairs(self.Filter.Playernames) do + if playername and string.find(playername,_Playername) then + MClientPlayername = true + end + end + self:T( { "Evaluated Playername", MClientPlayername } ) + MClientInclude = MClientInclude and MClientPlayername + end + + if self.Filter.Callsigns then + local MClientCallsigns = false + local callsign = MClient:GetCallsign() + --self:I(callsign) + for _,_Callsign in pairs(self.Filter.Callsigns) do + if callsign and string.find(callsign,_Callsign) then + MClientCallsigns = true + end + end + self:T( { "Evaluated Callsign", MClientCallsigns } ) + MClientInclude = MClientInclude and MClientCallsigns + end + + end self:T2( MClientInclude ) return MClientInclude end @@ -7334,7 +7390,7 @@ do -- SET_SCENERY if ZoneSet then for _,_zone in pairs(ZoneSet.Set) do - --self:I("Zone type handed: "..tostring(_zone.ClassName)) + self:T("Zone type handed: "..tostring(_zone.ClassName)) table.insert(zonenames,_zone:GetName()) end self:AddSceneryByName(zonenames) @@ -7348,7 +7404,7 @@ do -- SET_SCENERY -- @param Core.Zone#ZONE Zone The zone to be scanned. Can be a ZONE_RADIUS (round) or a ZONE_POLYGON (e.g. Quad-Point) -- @return #SET_SCENERY function SET_SCENERY:NewFromZone(Zone) - local zone = Zone -- Core.Zone#ZONE_POLYGON + local zone = Zone -- Core.Zone#ZONE_RADIUS if type(Zone) == "string" then zone = ZONE:FindByName(Zone) end @@ -7468,7 +7524,29 @@ do -- SET_SCENERY return CountU end + + --- Get a table of alive objects. + -- @param #SET_GROUP self + -- @return #table Table of alive objects + -- @return Core.Set#SET_SCENERY SET of alive objects + function SET_SCENERY:GetAliveSet() + self:F2() + local AliveSet = SET_SCENERY:New() + + -- Clean the Set before returning with only the alive Groups. + for GroupName, GroupObject in pairs( self.Set ) do + local GroupObject = GroupObject -- Wrapper.Group#GROUP + if GroupObject then + if GroupObject:IsAlive() then + AliveSet:Add( GroupName, GroupObject ) + end + end + end + + return AliveSet.Set or {}, AliveSet + end + --- Iterate the SET_SCENERY and call an iterator function for each **alive** SCENERY, providing the SCENERY and optional parameters. -- @param #SET_SCENERY self -- @param #function IteratorFunction The function that will be called when there is an alive SCENERY in the SET_SCENERY. The function needs to accept a SCENERY parameter. diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index d4c20f948..96794d939 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -94,6 +94,7 @@ -- @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 @@ -309,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 = { @@ -351,6 +365,7 @@ ATIS = { relHumidity = nil, ReportmBar = false, TransmitOnlyWithPlayers = false, + ATISforFARPs = false, } --- NATO alphabet. @@ -593,7 +608,7 @@ _ATIS = {} --- ATIS class version. -- @field #string version -ATIS.version = "0.9.12" +ATIS.version = "0.9.14" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1049,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. @@ -1257,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 ) ) @@ -1473,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 --- ------------ @@ -1790,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 @@ -1865,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 @@ -1884,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 @@ -1987,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 @@ -2019,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 @@ -2050,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. @@ -2117,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 @@ -2246,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 @@ -2263,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 @@ -2399,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, ";", " . " ) diff --git a/Moose Development/Moose/Ops/Auftrag.lua b/Moose Development/Moose/Ops/Auftrag.lua index 9c97d716f..d83278c19 100644 --- a/Moose Development/Moose/Ops/Auftrag.lua +++ b/Moose Development/Moose/Ops/Auftrag.lua @@ -1317,6 +1317,62 @@ function AUFTRAG:NewCAP(ZoneCAP, Altitude, Speed, Coordinate, Heading, Leg, Targ return mission end +--- **[AIR]** Create a CAP mission on a group. +-- @param #AUFTRAG self +-- @param Wrapper.Group#GROUP Grp. +-- @param #number Altitude Orbit altitude in feet. Default is 6,000 ft. +-- @param #number Speed Orbit speed in knots. Default 250 KIAS. +-- @param #number RelHeading Relative heading [0, 360) of race-track pattern in degrees wrt heading of the carrier. Default is heading of the carrier. +-- @param #number Leg Length of race-track in NM. Default 14 NM. +-- @param #number OffsetDist Relative distance of the first race-track point wrt to the carrier. Default 6 NM. +-- @param #number OffsetAngle Relative angle of the first race-track point wrt. to the carrier. Default 180 (behind the boat). +-- @param #number UpdateDistance Threshold distance in NM before orbit pattern is updated. Default 5 NM. +-- @param #table TargetTypes (Optional) Table of target types. Default `{"Helicopters", "Ground Units", "Light armed ships"}`. +-- @param #number EngageRange Max range in nautical miles that the escort group(s) will engage enemies. Default 32 NM (60 km). +-- @return #AUFTRAG self +function AUFTRAG:NewCAPGROUP(Grp, Altitude, Speed, RelHeading, Leg, OffsetDist, OffsetAngle, UpdateDistance, TargetTypes, EngageRange) + + -- Ensure given TargetTypes parameter is a table. + if TargetTypes then + if type(TargetTypes)~="table" then + TargetTypes={TargetTypes} + end + end + -- Six NM astern. + local OffsetVec2={r=OffsetDist or 6, phi=OffsetAngle or 180} + + -- Default leg. + Leg=Leg or 14 + + local Heading=nil + if RelHeading then + Heading=-math.abs(RelHeading) + end + + -- Create orbit mission. + local mission=AUFTRAG:NewORBIT_GROUP(Grp, Altitude, Speed, Leg, Heading, OffsetVec2, UpdateDistance) + -- Mission type CAP. + mission.type=AUFTRAG.Type.CAP + mission:_SetLogID() + + -- DCS task parameters: + local engage = EngageRange or 32 + local zoneCAPGroup = ZONE_GROUP:New("CAPGroup", Grp, UTILS.NMToMeters(engage)) + mission.engageZone=zoneCAPGroup + mission.engageTargetTypes=TargetTypes or {"Air"} + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.CAP + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionROT=ENUMS.ROT.EvadeFire + + mission.categories={AUFTRAG.Category.AIRCRAFT} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + --- **[AIR]** Create a CAS mission. -- @param #AUFTRAG self -- @param Core.Zone#ZONE_RADIUS ZoneCAS Circular CAS zone. Detected targets in this zone will be engaged. @@ -6112,20 +6168,15 @@ function AUFTRAG:GetDCSMissionTask() -- Race-track vector. orbitRaceTrack=UTILS.Vec2Translate(orbitVec2, self.orbitLeg, heading) end - - -- Debug - --UTILS.RemoveMark(self.orbitCenterMarkID) - --self.orbitCenterMarkID=COORDINATE:NewFromVec2(orbitVec2):MarkToAll("Orbit Center") - - -- Debug show arrow. - --if orbitRaceTrack then - --UTILS.RemoveMark(self.orbitArrowMarkID) - --self.orbitArrowMarkID=COORDINATE:NewFromVec2(orbitVec2):ArrowToAll(COORDINATE:NewFromVec2(orbitRaceTrack)) - --end + + local orbitRaceTrackCoord = nil + if orbitRaceTrack then + orbitRaceTrackCoord = COORDINATE:NewFromVec2(orbitRaceTrack) + end -- Create orbit task. - local DCStask=CONTROLLABLE.TaskOrbit(nil, COORDINATE:NewFromVec2(orbitVec2), self.orbitAltitude, self.orbitSpeed, orbitRaceTrack) - + local DCStask=CONTROLLABLE.TaskOrbit(nil, COORDINATE:NewFromVec2(orbitVec2), self.orbitAltitude, self.orbitSpeed, orbitRaceTrackCoord) + -- Add DCS task. table.insert(DCStasks, DCStask) diff --git a/Moose Development/Moose/Ops/Awacs.lua b/Moose Development/Moose/Ops/Awacs.lua index 12ff9b59b..4b682296b 100644 --- a/Moose Development/Moose/Ops/Awacs.lua +++ b/Moose Development/Moose/Ops/Awacs.lua @@ -2,9 +2,7 @@ -- -- === -- --- ## AWACS --- --- * MOOSE AI AWACS Operations using text-to-speech. +-- **AWACS** - MOOSE AI AWACS Operations using text-to-speech. -- -- === -- @@ -19,7 +17,7 @@ -- === -- -- ### Author: **applevangelist** --- @date Last Update November 2022 +-- @date Last Update December 2022 -- @module Ops.AWACS -- @image OPS_AWACS.jpg @@ -106,7 +104,7 @@ do -- @field #boolean NoGroupTags Set to true if you don't want group tags. -- @field #boolean SuppressScreenOutput Set to true to suppress all screen output. -- @field #boolean NoMissileCalls Suppress missile callouts --- @field #boolean PlayerCapAssigment Assign players to CAP tasks when they are logged on +-- @field #boolean PlayerCapAssignment Assign players to CAP tasks when they are logged on -- @field #number GoogleTTSPadding -- @field #number WindowsTTSPadding -- @field #boolean AllowMarkers @@ -114,6 +112,7 @@ do -- @field #boolean GCI Act as GCI -- @field Wrapper.Group#GROUP GCIGroup EWR group object for GCI ops -- @field #string locale Localization +-- @field #boolean IncludeHelicopters -- @extends Core.Fsm#FSM @@ -358,7 +357,7 @@ do -- testawacs.maxassigndistance = 100 -- Don't assign targets further out than this, in NM. -- testawacs.debug = false -- set to true to produce more log output. -- testawacs.NoMissileCalls = true -- suppress missile callouts --- testawacs.PlayerCapAssigment = true -- no intercept task assignments for players +-- testawacs.PlayerCapAssignment = true -- no intercept task assignments for players -- testawacs.invisible = false -- set AWACS to be invisible to hostiles -- testawacs.immortal = false -- set AWACS to be immortal -- -- By default, the radio queue is checked every 10 secs. This is altered by the calculated length of the sentence to speak @@ -367,6 +366,7 @@ do -- testawacs.GoogleTTSPadding = 1 -- seconds -- testawacs.WindowsTTSPadding = 2.5 -- seconds -- testawacs.PikesSpecialSwitch = false -- if set to true, AWACS will omit the "doing xy knots" on the station assignement callout +-- testawacs.IncludeHelicopters = false -- if set to true, Helicopter pilots will also get the AWACS Menu and options -- -- ## 9.2 Bespoke random voices for AI CAP (Google TTS only) -- @@ -499,7 +499,7 @@ do -- @field #AWACS AWACS = { ClassName = "AWACS", -- #string - version = "0.2.49", -- #string + version = "0.2.51", -- #string lid = "", -- #string coalition = coalition.side.BLUE, -- #number coalitiontxt = "blue", -- #string @@ -580,12 +580,13 @@ AWACS = { NoMissileCalls = true, GoogleTTSPadding = 1, WindowsTTSPadding = 2.5, - PlayerCapAssigment = true, + PlayerCapAssignment = true, AllowMarkers = false, PlayerStationName = nil, GCI = false, GCIGroup = nil, locale = "en", + IncludeHelicopters = false, } --- @@ -916,13 +917,13 @@ AWACS.TaskStatus = { --@field #boolean FromAI ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO-List 0.2.42 +-- TODO-List 0.2.51 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- -- DONE - WIP - Player tasking, VID -- DONE - Localization (sensible?) -- TODO - (LOW) LotATC --- TODO - SW Optimization +-- DONE - SW Optimization -- WONTDO - Maybe check in AI only when airborne -- DONE - remove SSML tag when not on google (currently sometimes spoken) -- DONE - Maybe - Assign specific number of AI CAP to a station @@ -953,6 +954,7 @@ AWACS.TaskStatus = { -- DONE - Shift Length AWACS/AI -- DONE - (WIP) Reporting -- DONE - Do not report non-airborne groups +-- DONE - Added option for helos ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor @@ -1125,7 +1127,7 @@ function AWACS:New(Name,AirWing,Coalition,AirbaseName,AwacsOrbit,OpsZone,Station self.MenuStrict = true self.maxassigndistance = 100 --nm self.NoMissileCalls = true - self.PlayerCapAssigment = true + self.PlayerCapAssignment = true -- managed groups self.ManagedGrps = {} -- #table of #AWACS.ManagedGroup entries @@ -3470,7 +3472,7 @@ function AWACS:_CheckInAI(FlightGroup,Group,AuftragsNr) local CAPVoice = self.CAPVoice if self.PathToGoogleKey then - CAPVoice = AWACS.CapVoices[math.floor(math.random(1,10))] + CAPVoice = self.CapVoices[math.floor(math.random(1,10))] end FlightGroup:SetSRS(self.PathToSRS,self.CAPGender,self.CAPCulture,CAPVoice,self.Port,self.PathToGoogleKey,"FLIGHT") @@ -3589,13 +3591,15 @@ function AWACS:_SetClientMenus() local bogeydope = MENU_GROUP_COMMAND:New(cgrp,"Bogey Dope",basemenu,self._BogeyDope,self,cgrp) local picture = MENU_GROUP_COMMAND:New(cgrp,"Picture",basemenu,self._Picture,self,cgrp) local declare = MENU_GROUP_COMMAND:New(cgrp,"Declare",basemenu,self._Declare,self,cgrp) - local tasking = MENU_GROUP:New(cgrp,"Tasking",basemenu) local showtask = MENU_GROUP_COMMAND:New(cgrp,"Showtask",tasking,self._Showtask,self,cgrp) - local commit = MENU_GROUP_COMMAND:New(cgrp,"Commit",tasking,self._Commit,self,cgrp) - local unable = MENU_GROUP_COMMAND:New(cgrp,"Unable",tasking,self._Unable,self,cgrp) - local abort = MENU_GROUP_COMMAND:New(cgrp,"Abort",tasking,self._TaskAbort,self,cgrp) - --local judy = MENU_GROUP_COMMAND:New(cgrp,"Judy",tasking,self._Judy,self,cgrp) + + if self.PlayerCapAssignment then + local commit = MENU_GROUP_COMMAND:New(cgrp,"Commit",tasking,self._Commit,self,cgrp) + local unable = MENU_GROUP_COMMAND:New(cgrp,"Unable",tasking,self._Unable,self,cgrp) + local abort = MENU_GROUP_COMMAND:New(cgrp,"Abort",tasking,self._TaskAbort,self,cgrp) + --local judy = MENU_GROUP_COMMAND:New(cgrp,"Judy",tasking,self._Judy,self,cgrp) + end if self.AwacsROE == AWACS.ROE.POLICE or self.AwacsROE == AWACS.ROE.VID then local vid = MENU_GROUP:New(cgrp,"VID as",tasking) @@ -5534,6 +5538,20 @@ end -- FSM Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- [Internal] onbeforeStart +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AWACS self +function AWACS:onbeforeStart(From,Event,to) + self:T({From, Event, To}) + if self.IncludeHelicopters then + self.clientset:FilterCategories("helicopter") + end + return self +end + --- [Internal] onafterStart -- @param #AWACS self -- @param #string From @@ -5959,7 +5977,7 @@ function AWACS:onafterStatus(From, Event, To) local AI, Humans = self:_GetIdlePilots() -- assign Pilot if there are targets and available Pilots, prefer Humans to AI -- DONE - Implemented AI First, Humans laters - need to work out how to loop the targets to assign a pilot - if outcome and #Humans > 0 and self.PlayerCapAssigment then + if outcome and #Humans > 0 and self.PlayerCapAssignment then -- add a task for AI self:_AssignPilotToTarget(Humans,targets) end diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index 61ff3edbf..677c810e8 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -711,6 +711,7 @@ 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` -- -- ## 2.1 User functions -- @@ -1015,7 +1016,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 @@ -1078,7 +1088,7 @@ CTLD.UnitTypes = { --- CTLD class version. -- @field #string version -CTLD.version="1.0.20" +CTLD.version="1.0.24" --- Instantiate a new CTLD. -- @param #CTLD self @@ -1161,6 +1171,7 @@ function CTLD:New(Coalition, Prefixes, Alias) -- radio beacons self.RadioSound = "beacon.ogg" + self.RadioPath = "l10n/DEFAULT/" -- zones stuff self.pickupZones = {} @@ -1243,6 +1254,9 @@ function CTLD:New(Coalition, Prefixes, Alias) self.usesubcats = false self.subcats = {} + -- disallow building in loadzones + self.nobuildinloadzones = true + local AliaS = string.gsub(self.alias," ","_") self.filename = string.format("CTLD_%s_Persist.csv",AliaS) @@ -1635,12 +1649,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 +1704,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 +1712,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 +2150,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 +2882,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 @@ -3440,7 +3506,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 +3526,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 +3547,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 @@ -3690,24 +3756,45 @@ function CTLD:_AddRadioBeacon(Name, Sound, Mhz, Modulation, IsShip, IsDropped) local Sound = Sound or "beacon.ogg" if IsDropped and Zone then local ZoneCoord = Zone - local ZoneVec3 = ZoneCoord:GetVec3() + local ZoneVec3 = ZoneCoord:GetVec3(1) local Frequency = string.format("%09d",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 status = string.format("***** Beacon added Freq %s Mod %s", Mhz, UTILS.GetModulationName(Modulation)) - --MESSAGE:New(status,10,"Debug"):ToLogIf(self.debug) + local Sound = self.RadioPath..Sound + trigger.action.radioTransmission(Sound, ZoneVec3, Modulation, false, tonumber(Frequency), 1000) -- Beacon in MP only runs for 30secs straight elseif Zone then - local ZoneCoord = Zone:GetCoordinate(2) + local ZoneCoord = Zone:GetCoordinate(1) local ZoneVec3 = ZoneCoord:GetVec3() local Frequency = string.format("%09d",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 status = string.format("***** Beacon added Freq %s Mod %s", Mhz, UTILS.GetModulationName(Modulation)) - --MESSAGE:New(status,10,"Debug"):ToLogIf(self.debug) + local Sound = self.RadioPath..Sound + trigger.action.radioTransmission(Sound, ZoneVec3, Modulation, false, tonumber(Frequency), 1000) -- Beacon in MP only runs for 30secs straight 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() @@ -3730,10 +3817,14 @@ 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 + -- local co = coroutine.create(self._AddRadioBeacon) + --coroutine.resume(co, self, Name,Sound,FM,CTLD.RadioModulation.FM, IsShip, IsDropped) + --coroutine.resume(co, self, Name,Sound,VHF,CTLD.RadioModulation.FM, IsShip, IsDropped) + --coroutine.resume(co, self, Name,Sound,UHF,CTLD.RadioModulation.AM, IsShip, IsDropped) + 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 @@ -4747,7 +4838,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 diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua index 3d43ee67f..6bcfcd5d1 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -712,7 +712,7 @@ end -- @param #FLIGHTGROUP self -- @return #boolean If true, has landed somewhere. function FLIGHTGROUP:IsLandedAt() - is=self:Is("LandedAt") + local is=self:Is("LandedAt") return is end @@ -908,7 +908,7 @@ function FLIGHTGROUP:Status() if mission and mission.updateDCSTask then -- Orbit missions might need updates. - if (mission:GetType()==AUFTRAG.Type.ORBIT or mission:GetType()==AUFTRAG.Type.RECOVERYTANKER) and mission.orbitVec2 then + if (mission:GetType()==AUFTRAG.Type.ORBIT or mission:GetType()==AUFTRAG.Type.RECOVERYTANKER or mission:GetType()==AUFTRAG.Type.CAP) and mission.orbitVec2 then -- Get 2D vector of orbit target. local vec2=mission:GetTargetVec2() diff --git a/Moose Development/Moose/Ops/PlayerRecce.lua b/Moose Development/Moose/Ops/PlayerRecce.lua index 0d0ce1166..746fff5e3 100644 --- a/Moose Development/Moose/Ops/PlayerRecce.lua +++ b/Moose Development/Moose/Ops/PlayerRecce.lua @@ -366,7 +366,10 @@ function PLAYERRECCE:SetReferencePoint(Coordinate,Name) if self.RPMarker then self.RPMarker:Remove() end - local text = string.format("%s RP %s\n%s\n%s\n%s",self.Name,Name,Coordinate:ToStringLLDDM(),Coordinate:ToStringLLDMS(),Coordinate:ToStringMGRS()) + local llddm = Coordinate:ToStringLLDDM() + local lldms = Coordinate:ToStringLLDMS() + local mgrs = Coordinate:ToStringMGRS() + local text = string.format("%s RP %s\n%s\n%s\n%s",self.Name,Name,llddm,lldms,mgrs) self.RPMarker = MARKER:New(Coordinate,text) self.RPMarker:ReadOnly() self.RPMarker:ToCoalition(self.Coalition) diff --git a/Moose Development/Moose/Ops/PlayerTask.lua b/Moose Development/Moose/Ops/PlayerTask.lua index 0f6d95c78..3485cd4c1 100644 --- a/Moose Development/Moose/Ops/PlayerTask.lua +++ b/Moose Development/Moose/Ops/PlayerTask.lua @@ -57,6 +57,7 @@ do -- @field #table NextTaskSuccess -- @field #table NextTaskFailure -- @field #string FinalState +-- @field #string TypeName -- @extends Core.Fsm#FSM @@ -95,7 +96,7 @@ PLAYERTASK = { --- PLAYERTASK class version. -- @field #string version -PLAYERTASK.version="0.1.10" +PLAYERTASK.version="0.1.11" --- Generic task condition. -- @type PLAYERTASK.Condition @@ -284,6 +285,14 @@ function PLAYERTASK:GetCoalition() return self.coalition end +--- [User] Get the Ops.Target#TARGET object for this task +-- @param #PLAYERTASK self +-- @return Ops.Target#TARGET Target +function PLAYERTASK:GetTarget() + self:T(self.lid.."GetTarget") + return self.Target +end + --- [USER] Add a free text description to this task. -- @param #PLAYERTASK self -- @param #string Text @@ -770,7 +779,7 @@ function PLAYERTASK:onafterClientAdded(From, Event, To, Client) self:T({From, Event, To}) if Client and self.verbose then local text = string.format("Player %s joined task %03d!",Client:GetPlayerName() or "Generic",self.PlayerTaskNr) - self:I(self.lid..text) + self:T(self.lid..text) end self.timestamp = timer.getAbsTime() return self @@ -925,6 +934,8 @@ do -- @field #boolean ShowMagnetic Also show magnetic angles -- @field #boolean InfoHasCoordinate -- @field #boolean InfoHasLLDDM +-- @field #table PlayerMenuTag +-- @field #boolean UseTypeNames -- @extends Core.Fsm#FSM --- @@ -1111,6 +1122,13 @@ do -- BRIEFING = "Briefing", -- TARGETLOCATION ="Target location", -- COORDINATE = "Coordinate", +-- INFANTRY = "Infantry", +-- TECHNICAL = "Technical", +-- ARTILLERY = "Artillery", +-- TANKS = "Tanks", +-- AIRDEFENSE = "Airdefense", +-- SAM = "SAM", +-- GROUP = "Group", -- }, -- -- e.g. @@ -1232,6 +1250,7 @@ PLAYERTASKCONTROLLER = { PlayerFlashMenu = {}, PlayerJoinMenu = {}, PlayerInfoMenu = {}, + PlayerMenuTag = {}, noflaresmokemenu = false, TransmitOnlyWithPlayers = true, buddylasing = false, @@ -1241,6 +1260,7 @@ PLAYERTASKCONTROLLER = { ShowMagnetic = true, InfoHasLLDDM = false, InfoHasCoordinate = false, + UseTypeNames = false, } --- @@ -1339,6 +1359,13 @@ PLAYERTASKCONTROLLER.Messages = { BRIEFING = "Briefing", TARGETLOCATION ="Target location", COORDINATE = "Coordinate", + INFANTRY = "Infantry", + TECHNICAL = "Technical", + ARTILLERY = "Artillery", + TANKS = "Tanks", + AIRDEFENSE = "Airdefense", + SAM = "SAM", + GROUP = "Group", }, DE = { TASKABORT = "Auftrag abgebrochen!", @@ -1404,19 +1431,26 @@ PLAYERTASKCONTROLLER.Messages = { BRIEFING = "Briefing", TARGETLOCATION ="Zielposition", COORDINATE = "Koordinate", + INFANTRY = "Infantrie", + TECHNICAL = "Technische", + ARTILLERY = "Artillerie", + TANKS = "Panzer", + AIRDEFENSE = "Flak", + SAM = "Luftabwehr", + GROUP = "Einheit", }, } --- PLAYERTASK class version. -- @field #string version -PLAYERTASKCONTROLLER.version="0.1.51" +PLAYERTASKCONTROLLER.version="0.1.54" --- Create and run a new TASKCONTROLLER instance. -- @param #PLAYERTASKCONTROLLER self -- @param #string Name Name of this controller -- @param #number Coalition of this controller, e.g. coalition.side.BLUE -- @param #string Type Type of the tasks controlled, defaults to PLAYERTASKCONTROLLER.Type.A2G --- @param #string ClientFilter (optional) Additional prefix filter for the SET_CLIENT +-- @param #string ClientFilter (optional) Additional prefix filter for the SET_CLIENT. Can be handed as @{Core.Set#SET_CLIENT} also. -- @return #PLAYERTASKCONTROLLER self function PLAYERTASKCONTROLLER:New(Name, Coalition, Type, ClientFilter) @@ -1467,10 +1501,20 @@ function PLAYERTASKCONTROLLER:New(Name, Coalition, Type, ClientFilter) self.noflaresmokemenu = false self.ShowMagnetic = true + + self.UseTypeNames = false + + local IsClientSet = false + + if ClientFilter and type(ClientFilter) == "table" and ClientFilter.ClassName and ClientFilter.ClassName == "SET_CLIENT" then + -- we have a predefined SET_CLIENT + self.ClientSet = ClientFilter + IsClientSet = true + end - if ClientFilter then + if ClientFilter and not IsClientSet then self.ClientSet = SET_CLIENT:New():FilterCoalitions(string.lower(self.CoalitionName)):FilterActive(true):FilterPrefixes(ClientFilter):FilterStart() - else + elseif not IsClientSet then self.ClientSet = SET_CLIENT:New():FilterCoalitions(string.lower(self.CoalitionName)):FilterActive(true):FilterStart() end @@ -1605,6 +1649,24 @@ function PLAYERTASKCONTROLLER:_InitLocalization() return self end +--- [User] Show target menu entries of type names for GROUND targets (off by default!), e.g. "Tank Group..." +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SetEnableUseTypeNames() + self:T(self.lid.."SetEnableUseTypeNames") + self.UseTypeNames = true + return self +end + +--- [User] Do not show target menu entries of type names for GROUND targets +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SetDisableUseTypeNames() + self:T(self.lid.."SetDisableUseTypeNames") + self.UseTypeNames = false + return self +end + --- [User] Set flash directions option for player (player based info) -- @param #PLAYERTASKCONTROLLER self -- @param #boolean OnOff Set to `true` to switch on and `false` to switch off. Default is OFF. @@ -1961,6 +2023,9 @@ function PLAYERTASKCONTROLLER:_EventHandler(EventData) end elseif EventData.id == EVENTS.PlayerEnterAircraft and EventData.IniCoalition == self.Coalition then if EventData.IniPlayerName and EventData.IniGroup and self.UseSRS then + if self.ClientSet:IsNotInSet(CLIENT:FindByName(EventData.IniUnitName)) then + return self + end self:T(self.lid.."Event for player: "..EventData.IniPlayerName) local frequency = self.Frequency local freqtext = "" @@ -2099,7 +2164,7 @@ function PLAYERTASKCONTROLLER:_GetTasksPerType() self:T(self.lid.."_GetTasksPerType") local tasktypes = self:_GetAvailableTaskTypes() - self:T({tasktypes}) + --self:T({tasktypes}) -- Sort tasks per threat level first local datatable = self.TaskQueue:GetDataTable() @@ -2132,7 +2197,7 @@ end function PLAYERTASKCONTROLLER:_CheckTargetQueue() self:T(self.lid.."_CheckTargetQueue") if self.TargetQueue:Count() > 0 then - local object = self.TargetQueue:Pull() + local object = self.TargetQueue:Pull() -- Wrapper.Positionable#POSITIONABLE local target = TARGET:New(object) if object.menuname then target.menuname = object.menuname @@ -2140,6 +2205,38 @@ function PLAYERTASKCONTROLLER:_CheckTargetQueue() target.freetext = object.freetext end end + + if self.UseTypeNames and object:IsGround() then + -- * Threat level 0: Unit is unarmed. + -- * Threat level 1: Unit is infantry. + -- * Threat level 2: Unit is an infantry vehicle. + -- * Threat level 3: Unit is ground artillery. + -- * Threat level 4: Unit is a tank. + -- * Threat level 5: Unit is a modern tank or ifv with ATGM. + -- * Threat level 6: Unit is a AAA. + -- * Threat level 7: Unit is a SAM or manpad, IR guided. + -- * Threat level 8: Unit is a Short Range SAM, radar guided. + -- * Threat level 9: Unit is a Medium Range SAM, radar guided. + -- * Threat level 10: Unit is a Long Range SAM, radar guided. + local threat = object:GetThreatLevel() + local typekey = "INFANTRY" + if threat == 0 or threat == 2 then + typekey = "TECHNICAL" + elseif threat == 3 then + typekey = "ARTILLERY" + elseif threat == 4 or threat == 5 then + typekey = "TANKS" + elseif threat == 6 or threat == 7 then + typekey = "AIRDEFENSE" + elseif threat >= 8 then + typekey = "SAM" + end + local typename = self.gettext:GetEntry(typekey,self.locale) + local gname = self.gettext:GetEntry("GROUP",self.locale) + target.TypeName = string.format("%s %s",typename,gname) + --self:T(self.lid.."Target TypeName = "..target.TypeName) + end + self:_AddTask(target) end return self @@ -2614,6 +2711,7 @@ function PLAYERTASKCONTROLLER:_AddTask(Target) end task.coalition = self.Coalition + task.TypeName = Target.TypeName if type == AUFTRAG.Type.BOMBRUNWAY then -- task to handle event shot @@ -2652,6 +2750,7 @@ end -- @param #PLAYERTASKCONTROLLER self -- @param Ops.PlayerTask#PLAYERTASK PlayerTask -- @param #boolean Silent If true, make no "has new task" announcement +-- @param #boolen TaskFilter If true, apply the white/black-list task filters here, also -- @return #PLAYERTASKCONTROLLER self -- @usage -- Example to create a PLAYERTASK of type CTLD and give Players 10 minutes to complete: @@ -2672,9 +2771,17 @@ end -- ) -- -- taskmanager:AddPlayerTaskToQueue(PlayerTask) -function PLAYERTASKCONTROLLER:AddPlayerTaskToQueue(PlayerTask,Silent) +function PLAYERTASKCONTROLLER:AddPlayerTaskToQueue(PlayerTask,Silent,TaskFilter) self:T(self.lid.."AddPlayerTaskToQueue") if PlayerTask and PlayerTask.ClassName and PlayerTask.ClassName == "PLAYERTASK" then + if TaskFilter then + if self.UseWhiteList and (not self:_CheckTaskTypeAllowed(PlayerTask.Type)) then + return self + end + if self.UseBlackList and self:_CheckTaskTypeDisallowed(PlayerTask.Type) then + return self + end + end PlayerTask:_SetController(self) PlayerTask:SetCoalition(self.Coalition) self.TaskQueue:Push(PlayerTask) @@ -3120,25 +3227,26 @@ end -- @param Core.Menu#MENU_BASE topmenu -- @param #table tasktypes -- @param #table taskpertype +-- @param #string newtag -- @return #table taskinfomenu -function PLAYERTASKCONTROLLER:_BuildTaskInfoMenu(group,client,playername,topmenu,tasktypes,taskpertype) +function PLAYERTASKCONTROLLER:_BuildTaskInfoMenu(group,client,playername,topmenu,tasktypes,taskpertype,newtag) self:T(self.lid.."_BuildTaskInfoMenu") local taskinfomenu = nil if self.taskinfomenu then local menutaskinfo = self.gettext:GetEntry("MENUTASKINFO",self.locale) - local taskinfomenu = MENU_GROUP_DELAYED:New(group,menutaskinfo,topmenu) + local taskinfomenu = MENU_GROUP_DELAYED:New(group,menutaskinfo,topmenu):SetTag(newtag) local ittypes = {} local itaskmenu = {} + local tnow = timer.getTime() for _tasktype,_data in pairs(tasktypes) do - ittypes[_tasktype] = MENU_GROUP_DELAYED:New(group,_tasktype,taskinfomenu) + ittypes[_tasktype] = MENU_GROUP_DELAYED:New(group,_tasktype,taskinfomenu):SetTag(newtag) local tasks = taskpertype[_tasktype] or {} local n = 0 for _,_task in pairs(tasks) do _task = _task -- Ops.PlayerTask#PLAYERTASK local pilotcount = _task:CountClients() local newtext = "]" - local tnow = timer.getTime() -- marker for new tasks if tnow - _task.timestamp < 60 then newtext = "*]" @@ -3151,7 +3259,14 @@ function PLAYERTASKCONTROLLER:_BuildTaskInfoMenu(group,client,playername,topmenu text = string.format("%s (%03d) [%d%s",name,_task.PlayerTaskNr,pilotcount,newtext) end end - local taskentry = MENU_GROUP_COMMAND_DELAYED:New(group,text,ittypes[_tasktype],self._ActiveTaskInfo,self,group,client,_task) + if self.UseTypeNames then + if _task.TypeName then + --local name = self.gettext:GetEntry(_task.TypeName,self.locale) + text = string.format("%s (%03d) [%d%s",_task.TypeName,_task.PlayerTaskNr,pilotcount,newtext) + --self:T(self.lid.."Menu text = "..text) + end + end + local taskentry = MENU_GROUP_COMMAND_DELAYED:New(group,text,ittypes[_tasktype],self._ActiveTaskInfo,self,group,client,_task):SetTag(newtag) --taskentry:SetTag(playername) itaskmenu[#itaskmenu+1] = taskentry -- keep max items limit @@ -3186,11 +3301,16 @@ function PLAYERTASKCONTROLLER:_BuildMenus(Client,enforced,fromsuccess) end for _,_client in pairs(clients) do - if _client then + if _client and _client:IsAlive() then local client = _client -- Wrapper.Client#CLIENT local group = client:GetGroup() local unknown = self.gettext:GetEntry("UNKNOWN",self.locale) local playername = client:GetPlayerName() or unknown + + local oldtag = self.PlayerMenuTag[playername] + local newtag = playername..timer.getAbsTime() + self.PlayerMenuTag[playername] = newtag + if group and client then --- -- TOPMENU @@ -3202,6 +3322,8 @@ function PLAYERTASKCONTROLLER:_BuildMenus(Client,enforced,fromsuccess) if self:_CheckPlayerHasTask(playername) and not fromsuccess then playerhastask = true end local topmenu = nil + --local oldmenu = nil + local rebuilddone = false self:T("Playerhastask = "..tostring(playerhastask).." Enforced = "..tostring(enforced).." Join or Abort = "..tostring(joinorabort)) @@ -3215,16 +3337,20 @@ function PLAYERTASKCONTROLLER:_BuildMenus(Client,enforced,fromsuccess) -- 2)+3) Join or abort? if joinorabort then self.PlayerMenu[playername]:RemoveSubMenus() - self.PlayerMenu[playername]:SetTag(timer.getAbsTime()) + self.PlayerMenu[playername]:SetTag(newtag) topmenu = self.PlayerMenu[playername] elseif (not playerhastask) or enforced then -- 4) last build > 30 secs? local T0 = timer.getAbsTime() - local TDiff = T0-self.PlayerMenu[playername].MenuTag + local TDiff = T0-self.PlayerMenu[playername].PTTimeStamp self:T("TDiff = "..string.format("%.2d",TDiff)) if TDiff >= self.holdmenutime then - self.PlayerMenu[playername]:RemoveSubMenus() - self.PlayerMenu[playername]:SetTag(timer.getAbsTime()) + --self.PlayerMenu[playername]:RemoveSubMenus() + --oldmenu = self.PlayerMenu[playername] + --self.PlayerMenu[playername] = nil + self.PlayerMenu[playername] = MENU_GROUP_DELAYED:New(group,menuname,self.MenuParent) + self.PlayerMenu[playername]:SetTag(newtag) + self.PlayerMenu[playername].PTTimeStamp = timer.getAbsTime() timedbuild = true end topmenu = self.PlayerMenu[playername] @@ -3233,14 +3359,17 @@ function PLAYERTASKCONTROLLER:_BuildMenus(Client,enforced,fromsuccess) -- 1) new player# topmenu = MENU_GROUP_DELAYED:New(group,menuname,self.MenuParent) self.PlayerMenu[playername] = topmenu - self.PlayerMenu[playername]:SetTag(timer.getAbsTime()) + self.PlayerMenu[playername]:SetTag(newtag) + self.PlayerMenu[playername].PTTimeStamp = timer.getAbsTime() + enforced = true end --- -- ACTIVE TASK MENU --- if playerhastask and enforced then - --self:T("Building Active Task Menus for "..playername) + self:T("Building Active Task Menus for "..playername) + rebuilddone = true local menuactive = self.gettext:GetEntry("MENUACTIVE",self.locale) local menuinfo = self.gettext:GetEntry("MENUINFO",self.locale) local menumark = self.gettext:GetEntry("MENUMARK",self.locale) @@ -3248,44 +3377,45 @@ function PLAYERTASKCONTROLLER:_BuildMenus(Client,enforced,fromsuccess) local menuflare = self.gettext:GetEntry("MENUFLARE",self.locale) local menuabort = self.gettext:GetEntry("MENUABORT",self.locale) - local active = MENU_GROUP_DELAYED:New(group,menuactive,topmenu) - local info = MENU_GROUP_COMMAND_DELAYED:New(group,menuinfo,active,self._ActiveTaskInfo,self,group,client) - local mark = MENU_GROUP_COMMAND_DELAYED:New(group,menumark,active,self._MarkTask,self,group,client) + local active = MENU_GROUP_DELAYED:New(group,menuactive,topmenu):SetTag(newtag) + local info = MENU_GROUP_COMMAND_DELAYED:New(group,menuinfo,active,self._ActiveTaskInfo,self,group,client):SetTag(newtag) + local mark = MENU_GROUP_COMMAND_DELAYED:New(group,menumark,active,self._MarkTask,self,group,client):SetTag(newtag) if self.Type ~= PLAYERTASKCONTROLLER.Type.A2A then if self.noflaresmokemenu ~= true then -- no smoking/flaring here if A2A or designer has set noflaresmokemenu to true - local smoke = MENU_GROUP_COMMAND_DELAYED:New(group,menusmoke,active,self._SmokeTask,self,group,client) - local flare = MENU_GROUP_COMMAND_DELAYED:New(group,menuflare,active,self._FlareTask,self,group,client) + local smoke = MENU_GROUP_COMMAND_DELAYED:New(group,menusmoke,active,self._SmokeTask,self,group,client):SetTag(newtag) + local flare = MENU_GROUP_COMMAND_DELAYED:New(group,menuflare,active,self._FlareTask,self,group,client):SetTag(newtag) local IsNight = client:GetCoordinate():IsNight() if IsNight then - local light = MENU_GROUP_COMMAND_DELAYED:New(group,menuflare,active,self._IlluminateTask,self,group,client) + local light = MENU_GROUP_COMMAND_DELAYED:New(group,menuflare,active,self._IlluminateTask,self,group,client):SetTag(newtag) end end end - local abort = MENU_GROUP_COMMAND_DELAYED:New(group,menuabort,active,self._AbortTask,self,group,client) + local abort = MENU_GROUP_COMMAND_DELAYED:New(group,menuabort,active,self._AbortTask,self,group,client):SetTag(newtag) if self.activehasinfomenu and self.taskinfomenu then - --self:T("Building Active-Info Menus for "..playername) + self:T("Building Active-Info Menus for "..playername) local tasktypes = self:_GetAvailableTaskTypes() local taskpertype = self:_GetTasksPerType() if self.PlayerInfoMenu[playername] then - self.PlayerInfoMenu[playername]:RemoveSubMenus() + self.PlayerInfoMenu[playername]:RemoveSubMenus(nil,oldtag) end - self.PlayerInfoMenu[playername] = self:_BuildTaskInfoMenu(group,client,playername,topmenu,tasktypes,taskpertype) + self.PlayerInfoMenu[playername] = self:_BuildTaskInfoMenu(group,client,playername,topmenu,tasktypes,taskpertype,newtag) end elseif (self.TaskQueue:Count() > 0 and enforced) or (not playerhastask and (timedbuild or joinorabort)) then - --self:T("Building Join Menus for "..playername) + self:T("Building Join Menus for "..playername) + rebuilddone = true --- -- JOIN TASK MENU --- local tasktypes = self:_GetAvailableTaskTypes() local taskpertype = self:_GetTasksPerType() local menujoin = self.gettext:GetEntry("MENUJOIN",self.locale) - local joinmenu = MENU_GROUP_DELAYED:New(group,menujoin,topmenu) + local joinmenu = MENU_GROUP_DELAYED:New(group,menujoin,topmenu):SetTag(newtag) local ttypes = {} local taskmenu = {} for _tasktype,_data in pairs(tasktypes) do - ttypes[_tasktype] = MENU_GROUP_DELAYED:New(group,_tasktype,joinmenu) + ttypes[_tasktype] = MENU_GROUP_DELAYED:New(group,_tasktype,joinmenu):SetTag(newtag) local tasks = taskpertype[_tasktype] or {} local n = 0 for _,_task in pairs(tasks) do @@ -3305,7 +3435,7 @@ function PLAYERTASKCONTROLLER:_BuildMenus(Client,enforced,fromsuccess) text = string.format("%s (%03d) [%d%s",name,_task.PlayerTaskNr,pilotcount,newtext) end end - local taskentry = MENU_GROUP_COMMAND_DELAYED:New(group,text,ttypes[_tasktype],self._JoinTask,self,group,client,_task) + local taskentry = MENU_GROUP_COMMAND_DELAYED:New(group,text,ttypes[_tasktype],self._JoinTask,self,group,client,_task):SetTag(newtag) --taskentry:SetTag(playername) taskmenu[#taskmenu+1] = taskentry n = n + 1 @@ -3315,25 +3445,30 @@ function PLAYERTASKCONTROLLER:_BuildMenus(Client,enforced,fromsuccess) end end if self.taskinfomenu then - --self:T("Building Join-Info Menus for "..playername) + self:T("Building Join-Info Menus for "..playername) if self.PlayerInfoMenu[playername] then - self.PlayerInfoMenu[playername]:RemoveSubMenus() + self.PlayerInfoMenu[playername]:RemoveSubMenus(nil,oldtag) end - self.PlayerInfoMenu[playername] = self:_BuildTaskInfoMenu(group,client,playername,topmenu,tasktypes,taskpertype) + self.PlayerInfoMenu[playername] = self:_BuildTaskInfoMenu(group,client,playername,topmenu,tasktypes,taskpertype,newtag) end - elseif self.TaskQueue:Count() == 0 then - -- no tasks (yet) + end + if self.AllowFlash then + local flashtext = self.gettext:GetEntry("FLASHMENU",self.locale) + local flashmenu = MENU_GROUP_COMMAND_DELAYED:New(group,flashtext,topmenu,self._SwitchFlashing,self,group,client):SetTag(newtag) + end + if self.TaskQueue:Count() == 0 then + self:T("No open tasks info") local menunotasks = self.gettext:GetEntry("MENUNOTASKS",self.locale) - local joinmenu = MENU_GROUP_DELAYED:New(group,menunotasks,topmenu) + local joinmenu = MENU_GROUP_DELAYED:New(group,menunotasks,self.PlayerMenu[playername]):SetTag(newtag) + rebuilddone = true end --- -- REFRESH MENU --- - if self.AllowFlash then - local flashtext = self.gettext:GetEntry("FLASHMENU",self.locale) - local flashmenu = MENU_GROUP_COMMAND_DELAYED:New(group,flashtext,self.PlayerMenu[playername],self._SwitchFlashing,self,group,client) + if rebuilddone then + self.PlayerMenu[playername]:RemoveSubMenus(nil,oldtag) + self.PlayerMenu[playername]:Refresh() end - self.PlayerMenu[playername]:Set() end end end @@ -3652,7 +3787,7 @@ function PLAYERTASKCONTROLLER:onafterStatus(From, Event, To) self:_BuildMenus(nil,enforcedmenu) if self.verbose then - local text = string.format("New Targets: %02d | Active Tasks: %02d | Active Players: %02d | Assigned Tasks: %02d",targetcount,taskcount,playercount,assignedtasks) + local text = string.format("%s | New Targets: %02d | Active Tasks: %02d | Active Players: %02d | Assigned Tasks: %02d",self.MenuName, targetcount,taskcount,playercount,assignedtasks) self:I(text) end diff --git a/Moose Development/Moose/Ops/Target.lua b/Moose Development/Moose/Ops/Target.lua index 7eb7eb5be..4cdbbdbfa 100644 --- a/Moose Development/Moose/Ops/Target.lua +++ b/Moose Development/Moose/Ops/Target.lua @@ -625,7 +625,7 @@ function TARGET:onafterStatus(From, Event, To) -- Log output verbose=1. if self.verbose>=1 then local text=string.format("%s: Targets=%d/%d Life=%.1f/%.1f Damage=%.1f", fsmstate, self:CountTargets(), self.N0, self:GetLife(), self:GetLife0(), self:GetDamage()) - if self:CountTargets() == 0 then + if self:CountTargets() == 0 or self:GetDamage() >= 100 then text=text.." Dead!" elseif damaged then text=text.." Damaged!" @@ -644,7 +644,7 @@ function TARGET:onafterStatus(From, Event, To) self:I(self.lid..text) end - if self:CountTargets() == 0 then + if self:CountTargets() == 0 or self:GetDamage() >= 100 then self:Dead() end @@ -943,6 +943,9 @@ function TARGET:_AddObject(Object) target.Coordinate=scenery:GetCoordinate() target.Life0=scenery:GetLife0() + + if target.Life0==0 then target.Life0 = 1 end + target.Life=scenery:GetLife() target.N0=target.N0+1 @@ -1095,7 +1098,9 @@ function TARGET:GetTargetLife(Target) elseif Target.Type==TARGET.ObjectType.STATIC then if Target.Object and Target.Object:IsAlive() then - return 1 + local life=Target.Object:GetLife() + return life + --return 1 else return 0 end diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 8ae06d6b1..280fdeaad 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -2201,10 +2201,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). @@ -2212,7 +2231,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 @@ -2220,7 +2239,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 @@ -2234,6 +2262,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. @@ -2243,7 +2272,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() @@ -2257,7 +2286,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 @@ -2321,8 +2359,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" @@ -2339,18 +2410,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 @@ -2359,19 +2460,52 @@ 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 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 spawn = true if Spawn == false then spawn = false end - BASE:I("Spawn = "..tostring(spawn)) + --BASE:I("Spawn = "..tostring(spawn)) local filename = Filename or "SetOfGroups" local setdata = SET_GROUP:New() local datatable = {} @@ -2388,6 +2522,7 @@ 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 } @@ -2400,12 +2535,40 @@ function UTILS.LoadSetOfGroups(Path,Filename,Spawn) 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) + 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(spwndgrp) + --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(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 @@ -2417,7 +2580,7 @@ function UTILS.LoadSetOfGroups(Path,Filename,Spawn) return nil end if spawn then - return setdata + return setdata,fires else return datatable end @@ -2436,13 +2599,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 @@ -2454,9 +2615,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" @@ -2479,14 +2646,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 diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index a2fd0577a..bb355c1a6 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -510,6 +510,7 @@ AIRBASE.MarianaIslands = { -- * AIRBASE.SouthAtlantic.Porvenir_Airfield -- * AIRBASE.SouthAtlantic.Almirante_Schroeders -- * AIRBASE.SouthAtlantic.Rio_Turbio +-- * AIRBASE.SouthAtlantic.Rio_Chico_Airfield -- --@field MarianaIslands AIRBASE.SouthAtlantic={ @@ -532,6 +533,7 @@ AIRBASE.SouthAtlantic={ ["Porvenir_Airfield"]="Porvenir Airfield", ["Almirante_Schroeders"]="Almirante Schroeders", ["Rio_Turbio"]="Rio Turbio", + ["Rio_Chico"] = "Rio Chico", } --- AIRBASE.ParkingSpot ".Coordinate, ".TerminalID", ".TerminalType", ".TOAC", ".Free", ".TerminalID0", ".DistToRwy". @@ -839,7 +841,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) diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index 59474de9c..8cf7c9226 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -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.