From 33265507053327563a126880f36e319ed63fb3bc Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Thu, 17 Feb 2022 17:41:20 +0100 Subject: [PATCH 01/26] CSAR - added "wet feet" option for a 2nd template to be used over water --- Moose Development/Moose/Ops/CSAR.lua | 43 +++++++++++++++++++++------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/Moose Development/Moose/Ops/CSAR.lua b/Moose Development/Moose/Ops/CSAR.lua index 4a3253a42..ccb8a472f 100644 --- a/Moose Development/Moose/Ops/CSAR.lua +++ b/Moose Development/Moose/Ops/CSAR.lua @@ -118,7 +118,8 @@ -- self.SRSModulation = radio.modulation.AM -- modulation -- -- -- self.csarUsePara = false -- If set to true, will use the LandingAfterEjection Event instead of Ejection --shagrat --- +-- self.wetfeettemplate = "man in floating thingy" -- if you use a mod to have a pilot in a rescue float, put the template name in here for wet feet spawns +-- -- ## 3. Results -- -- Number of successful landings with save pilots and aggregated number of saved pilots is stored in these variables in the object: @@ -227,8 +228,9 @@ CSAR = { -- @field #number frequency Frequency of the NDB. -- @field #string player Player name if applicable. -- @field Wrapper.Group#GROUP group Spawned group object. --- @field #number timestamp Timestamp for approach process --- @field #boolean alive Group is alive or dead/rescued +-- @field #number timestamp Timestamp for approach process. +-- @field #boolean alive Group is alive or dead/rescued. +-- @field #boolean wetfeet Group is spawned over (deep) water. --- All slot / Limit settings -- @type CSAR.AircraftType @@ -248,7 +250,7 @@ CSAR.AircraftType["UH-60L"] = 10 --- CSAR class version. -- @field #string version -CSAR.version="1.0.3" +CSAR.version="1.0.4" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list @@ -390,6 +392,10 @@ function CSAR:New(Coalition, Template, Alias) -- added 0.1.3 self.csarUsePara = false -- shagrat set to true, will use the LandingAfterEjection Event instead of Ejection + + -- added 0.1.4 + self.wetfeettemplate = nil + self.usewetfeet = false -- WARNING - here\'ll be dragons -- for this to work you need to de-sanitize your mission environment in \Scripts\MissionScripting.lua @@ -502,8 +508,9 @@ end -- @param #string Typename Typename of unit. -- @param #number Frequency Frequency of the NDB in Hz -- @param #string Playername Name of Player (if applicable) +-- @param #boolean Wetfeet Ejected over water -- @return #CSAR self. -function CSAR:_CreateDownedPilotTrack(Group,Groupname,Side,OriginalUnit,Description,Typename,Frequency,Playername) +function CSAR:_CreateDownedPilotTrack(Group,Groupname,Side,OriginalUnit,Description,Typename,Frequency,Playername,Wetfeet) self:T({"_CreateDownedPilotTrack",Groupname,Side,OriginalUnit,Description,Typename,Frequency,Playername}) -- create new entry @@ -519,6 +526,7 @@ function CSAR:_CreateDownedPilotTrack(Group,Groupname,Side,OriginalUnit,Descript DownedPilot.group = Group DownedPilot.timestamp = 0 DownedPilot.alive = true + DownedPilot.wetfeet = Wetfeet or false -- Add Pilot local PilotTable = self.downedPilots @@ -568,17 +576,23 @@ end -- @param #number country Country for template. -- @param Core.Point#COORDINATE point Coordinate to spawn at. -- @param #number frequency Frequency of the pilot's beacon +-- @param #boolean wetfeet Spawn is over water -- @return Wrapper.Group#GROUP group The #GROUP object. -- @return #string alias The alias name. -function CSAR:_SpawnPilotInField(country,point,frequency) - self:T({country,point,frequency}) +function CSAR:_SpawnPilotInField(country,point,frequency,wetfeet) + self:T({country,point,frequency,tostring(wetfeet)}) local freq = frequency or 1000 local freq = freq / 1000 -- kHz for i=1,10 do math.random(i,10000) end - if point:IsSurfaceTypeWater() then point.y = 0 end + if point:IsSurfaceTypeWater() or wetfeet then + point.y = 0 + end local template = self.template + if self.usewetfeet and wetfeet then + template = self.wetfeettemplate + end local alias = string.format("Pilot %.2fkHz-%d", freq, math.random(1,99)) local coalition = self.coalition local pilotcacontrol = self.allowDownedPilotCAcontrol -- Switch AI on/oof - is this really correct for CA? @@ -644,13 +658,19 @@ function CSAR:_AddCsar(_coalition , _country, _point, _typeName, _unitName, _pla self:T({_coalition , _country, _point, _typeName, _unitName, _playerName, _freq, noMessage, _description}) local template = self.template + local wetfeet = false + + local surface = _point:GetSurfaceType() + if surface == land.SurfaceType.WATER then + wetfeet = true + end if not _freq then _freq = self:_GenerateADFFrequency() if not _freq then _freq = 333000 end --noob catch end - local _spawnedGroup, _alias = self:_SpawnPilotInField(_country,_point,_freq) + local _spawnedGroup, _alias = self:_SpawnPilotInField(_country,_point,_freq,wetfeet) local _typeName = _typeName or "Pilot" @@ -688,7 +708,7 @@ function CSAR:_AddCsar(_coalition , _country, _point, _typeName, _unitName, _pla local _GroupName = _spawnedGroup:GetName() or _alias - self:_CreateDownedPilotTrack(_spawnedGroup,_GroupName,_coalition,_unitName,_text,_typeName,_freq,_playerName) + self:_CreateDownedPilotTrack(_spawnedGroup,_GroupName,_coalition,_unitName,_text,_typeName,_freq,_playerName,wetfeet) self:_InitSARForPilot(_spawnedGroup, _unitName, _freq, noMessage) --shagrat use unitName to have the aircraft callsign / descriptive "name" etc. @@ -1999,6 +2019,9 @@ function CSAR:onafterStart(From, Event, To) self.allheligroupset = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterCategoryHelicopter():FilterStart() end self.mash = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(self.mashprefix):FilterStart() -- currently only GROUP objects, maybe support STATICs also? + if self.wetfeettemplate then + self.usewetfeet = true + end self:__Status(-10) return self end From 6b4975559d2e015a35690738ab006c8a02b73bbd Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Fri, 18 Feb 2022 08:22:57 +0100 Subject: [PATCH 02/26] CSAR - added wet feet check if also using csarUsePara (no landing event triggered) --- Moose Development/Moose/Ops/CSAR.lua | 2236 +++++++++++++++++++++++++- 1 file changed, 2232 insertions(+), 4 deletions(-) diff --git a/Moose Development/Moose/Ops/CSAR.lua b/Moose Development/Moose/Ops/CSAR.lua index ccb8a472f..354fd9f1e 100644 --- a/Moose Development/Moose/Ops/CSAR.lua +++ b/Moose Development/Moose/Ops/CSAR.lua @@ -6,6 +6,14 @@ -- -- === -- +-- ## Missions:--- **Ops** -- Combat Search and Rescue. +-- +-- === +-- +-- **CSAR** - MOOSE based Helicopter CSAR Operations. +-- +-- === +-- -- ## Missions: -- -- ### [CSAR - Combat Search & Rescue](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20CSAR) @@ -118,8 +126,8 @@ -- self.SRSModulation = radio.modulation.AM -- modulation -- -- -- self.csarUsePara = false -- If set to true, will use the LandingAfterEjection Event instead of Ejection --shagrat --- self.wetfeettemplate = "man in floating thingy" -- if you use a mod to have a pilot in a rescue float, put the template name in here for wet feet spawns --- +-- self.wetfeettemplate = "man in floating thingy" -- if you use a mod to have a pilot in a rescue float, put the template name in here for wet feet spawns. Note: in conjunction with csarUsePara this might create dual ejected pilots in edge cases. +-- -- ## 3. Results -- -- Number of successful landings with save pilots and aggregated number of saved pilots is stored in these variables in the object: @@ -955,9 +963,2229 @@ function CSAR:_EventHandler(EventData) if self.limitmaxdownedpilots and self:_ReachedPilotLimit() then return end - + + + -- TODO: Over water check --- EVENTS.LandingAfterEjection NOT triggered by DCS, so handle csarUsePara = true case + -- might create dual pilots in edge cases + + local wetfeet = false + + local surface = _unit:GetCoordinate():GetSurfaceType() + if surface == land.SurfaceType.WATER then + wetfeet = true + end -- all checks passed, get going. - if self.csarUsePara == false then --shagrat check parameter LandingAfterEjection, if true don't spawn a Pilot from EJECTION event, wait for the Chute to land + if self.csarUsePara == false or (self.csarUsePara and wetfeet ) then --shagrat check parameter LandingAfterEjection, if true don't spawn a Pilot from EJECTION event, wait for the Chute to land + local _freq = self:_GenerateADFFrequency() + self:_AddCsar(_coalition, _unit:GetCountry(), _unit:GetCoordinate() , _unit:GetTypeName(), _unit:GetName(), _event.IniPlayerName, _freq, false, "none") + return true + end + + ---- shagrat on event LANDING_AFTER_EJECTION spawn pilot at parachute location + elseif (_event.id == EVENTS.LandingAfterEjection and self.csarUsePara == true) then + self:I({EVENT=_event}) + local _LandingPos = COORDINATE:NewFromVec3(_event.initiator:getPosition().p) + local _unitname = "Aircraft" --_event.initiator:getName() or "Aircraft" --shagrat Optional use of Object name which is unfortunately 'f15_Pilot_Parachute' + local _typename = "Ejected Pilot" --_event.Initiator.getTypeName() or "Ejected Pilot" + local _country = _event.initiator:getCountry() + local _coalition = coalition.getCountryCoalition( _country ) + if _coalition == self.coalition then + local _freq = self:_GenerateADFFrequency() + self:I({coalition=_coalition,country= _country, coord=_LandingPos, name=_unitname, player=_event.IniPlayerName, freq=_freq}) + self:_AddCsar(_coalition, _country, _LandingPos, nil, _unitname, _event.IniPlayerName, _freq, false, "none")--shagrat add CSAR at Parachute location. + + Unit.destroy(_event.initiator) -- shagrat remove static Pilot model + end + return true + + elseif _event.id == EVENTS.Land then + self:T(self.lid .. " Landing") + + if _event.IniUnitName then + self.takenOff[_event.IniUnitName] = nil + end + + if self.allowFARPRescue then + + local _unit = _event.IniUnit -- Wrapper.Unit#UNIT + + if _unit == nil then + self:T(self.lid .. " Unit nil on landing") + return -- error! + end + + local _coalition = _event.IniCoalition + if _coalition ~= self.coalition then + return --ignore! + end + + self.takenOff[_event.IniUnitName] = nil + + local _place = _event.Place -- Wrapper.Airbase#AIRBASE + + if _place == nil then + self:T(self.lid .. " Landing Place Nil") + return -- error! + end + + -- anyone on board? + if self.inTransitGroups[_event.IniUnitName] == nil then + -- ignore + return + end + + if _place:GetCoalition() == self.coalition or _place:GetCoalition() == coalition.side.NEUTRAL then + self:_ScheduledSARFlight(_event.IniUnitName,_event.IniGroupName,true) + else + self:T(string.format("Airfield %d, Unit %d", _place:GetCoalition(), _unit:GetCoalition())) + end + end + + return true + end + return self +end + +--- (Internal) Initialize the action for a pilot. +-- @param #CSAR self +-- @param Wrapper.Group#GROUP _downedGroup The group to rescue. +-- @param #string _GroupName Name of the Group +-- @param #number _freq Beacon frequency. +-- @param #boolean _nomessage Send message true or false. +function CSAR:_InitSARForPilot(_downedGroup, _GroupName, _freq, _nomessage) + self:T(self.lid .. " _InitSARForPilot") + local _leader = _downedGroup:GetUnit(1) + local _groupName = _GroupName + local _freqk = _freq / 1000 + local _coordinatesText = self:_GetPositionOfWounded(_downedGroup) + local _leadername = _leader:GetName() + + if not _nomessage then + if _freq ~= 0 then --shagrat + local _text = string.format("%s requests SAR at %s, beacon at %.2f KHz", _groupName, _coordinatesText, _freqk)--shagrat _groupName to prevent 'f15_Pilot_Parachute' + self:_DisplayToAllSAR(_text,self.coalition,self.messageTime) + else --shagrat CASEVAC msg + local _text = string.format("Pickup Zone at %s.", _coordinatesText ) + self:_DisplayToAllSAR(_text,self.coalition,self.messageTime) + end + end + + for _,_heliName in pairs(self.csarUnits) do + self:_CheckWoundedGroupStatus(_heliName, _groupName) + end + + -- trigger FSM event + self:__PilotDown(2,_downedGroup, _freqk, _groupName, _coordinatesText) + + return self +end + +--- (Internal) Check if a name is in downed pilot table +-- @param #CSAR self +-- @param #string name Name to search for. +-- @return #boolean Outcome. +-- @return #CSAR.DownedPilot Table if found else nil. +function CSAR:_CheckNameInDownedPilots(name) + local PilotTable = self.downedPilots --#CSAR.DownedPilot + local found = false + local table = nil + for _,_pilot in pairs(PilotTable) do + if _pilot.name == name and _pilot.alive == true then + found = true + table = _pilot + break + end + end + return found, table +end + +--- (Internal) Check if a name is in downed pilot table and remove it. +-- @param #CSAR self +-- @param #string name Name to search for. +-- @param #boolean force Force removal. +-- @return #boolean Outcome. +function CSAR:_RemoveNameFromDownedPilots(name,force) + local PilotTable = self.downedPilots --#CSAR.DownedPilot + local found = false + for _index,_pilot in pairs(PilotTable) do + if _pilot.name == name then + self.downedPilots[_index].alive = false + end + end + return found +end + +--- (Internal) Check state of wounded group. +-- @param #CSAR self +-- @param #string heliname heliname +-- @param #string woundedgroupname woundedgroupname +function CSAR:_CheckWoundedGroupStatus(heliname,woundedgroupname) + self:T(self.lid .. " _CheckWoundedGroupStatus") + local _heliName = heliname + local _woundedGroupName = woundedgroupname + self:T({Heli = _heliName, Downed = _woundedGroupName}) + -- if wounded group is not here then message already been sent to SARs + -- stop processing any further + local _found, _downedpilot = self:_CheckNameInDownedPilots(_woundedGroupName) + if not _found then + self:T("...not found in list!") + return + end + + local _woundedGroup = _downedpilot.group + if _woundedGroup ~= nil and _woundedGroup:IsAlive() then + local _heliUnit = self:_GetSARHeli(_heliName) -- Wrapper.Unit#UNIT + + local _lookupKeyHeli = _heliName .. "_" .. _woundedGroupName --lookup key for message state tracking + + if _heliUnit == nil then + self.heliVisibleMessage[_lookupKeyHeli] = nil + self.heliCloseMessage[_lookupKeyHeli] = nil + self.landedStatus[_lookupKeyHeli] = nil + self:T("...helinunit nil!") + return + end + + local _heliCoord = _heliUnit:GetCoordinate() + local _leaderCoord = _woundedGroup:GetCoordinate() + local _distance = self:_GetDistance(_heliCoord,_leaderCoord) + -- autosmoke + if (self.autosmoke == true) and (_distance < self.autosmokedistance) and (_distance ~= -1) then + self:_PopSmokeForGroup(_woundedGroupName, _woundedGroup) + end + + if _distance < self.approachdist_near and _distance > 0 then + if self:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedGroup, _woundedGroupName) == true then + -- we\'re close, reschedule + _downedpilot.timestamp = timer.getAbsTime() + self:__Approach(-5,heliname,woundedgroupname) + end + elseif _distance >= self.approachdist_near and _distance < self.approachdist_far then + -- message once + if self.heliVisibleMessage[_lookupKeyHeli] == nil then + local _pilotName = _downedpilot.desc + if self.autosmoke == true then + local dist = self.autosmokedistance / 1000 + local disttext = string.format("%.0fkm",dist) + if _SETTINGS:IsImperial() then + local dist = UTILS.MetersToNM(self.autosmokedistance) + disttext = string.format("%.0fnm",dist) + end + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. I hear you! Finally, that is music in my ears!\nI'll pop a smoke when you are %s away.\nLand or hover by the smoke.", _heliName, _pilotName, disttext), self.messageTime,false,true) + else + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. I hear you! Finally, that is music in my ears!\nRequest a flare or smoke if you need.", _heliName, _pilotName), self.messageTime,false,true) + end + --mark as shown for THIS heli and THIS group + self.heliVisibleMessage[_lookupKeyHeli] = true + end + self.heliCloseMessage[_lookupKeyHeli] = nil + self.landedStatus[_lookupKeyHeli] = nil + --reschedule as units aren\'t dead yet , schedule for a bit slower though as we\'re far away + _downedpilot.timestamp = timer.getAbsTime() + self:__Approach(-10,heliname,woundedgroupname) + end + else + self:T("...Downed Pilot KIA?!") + if not _downedpilot.alive then + --self:__KIA(1,_downedpilot.name) + self:_RemoveNameFromDownedPilots(_downedpilot.name, true) + end + end + return self +end + +--- (Internal) Function to pop a smoke at a wounded pilot\'s positions. +-- @param #CSAR self +-- @param #string _woundedGroupName Name of the group. +-- @param Wrapper.Group#GROUP _woundedLeader Object of the group. +function CSAR:_PopSmokeForGroup(_woundedGroupName, _woundedLeader) + self:T(self.lid .. " _PopSmokeForGroup") + -- have we popped smoke already in the last 5 mins + local _lastSmoke = self.smokeMarkers[_woundedGroupName] + if _lastSmoke == nil or timer.getTime() > _lastSmoke then + + local _smokecolor = self.smokecolor + local _smokecoord = _woundedLeader:GetCoordinate():Translate( 6, math.random( 1, 360) ) --shagrat place smoke at a random 6 m distance, so smoke does not obscure the pilot + _smokecoord:Smoke(_smokecolor) + self.smokeMarkers[_woundedGroupName] = timer.getTime() + 300 -- next smoke time + end + return self +end + +--- (Internal) Function to pickup the wounded pilot from the ground. +-- @param #CSAR self +-- @param Wrapper.Unit#UNIT _heliUnit Object of the group. +-- @param #string _pilotName Name of the pilot. +-- @param Wrapper.Group#GROUP _woundedGroup Object of the group. +-- @param #string _woundedGroupName Name of the group. +function CSAR:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) + self:T(self.lid .. " _PickupUnit") + -- board + local _heliName = _heliUnit:GetName() + local _groups = self.inTransitGroups[_heliName] + local _unitsInHelicopter = self:_PilotsOnboard(_heliName) + + -- init table if there is none for this helicopter + if not _groups then + self.inTransitGroups[_heliName] = {} + _groups = self.inTransitGroups[_heliName] + end + + -- if the heli can\'t pick them up, show a message and return + local _maxUnits = self.AircraftType[_heliUnit:GetTypeName()] + if _maxUnits == nil then + _maxUnits = self.max_units + end + if _unitsInHelicopter + 1 > _maxUnits then + self:_DisplayMessageToSAR(_heliUnit, string.format("%s, %s. We\'re already crammed with %d guys! Sorry!", _pilotName, _heliName, _unitsInHelicopter, _unitsInHelicopter), self.messageTime) + return true + end + + local found,downedgrouptable = self:_CheckNameInDownedPilots(_woundedGroupName) + local grouptable = downedgrouptable --#CSAR.DownedPilot + self.inTransitGroups[_heliName][_woundedGroupName] = + { + originalUnit = grouptable.originalUnit, + woundedGroup = _woundedGroupName, + side = self.coalition, + desc = grouptable.desc, + player = grouptable.player, + } + + _woundedGroup:Destroy(false) + self:_RemoveNameFromDownedPilots(_woundedGroupName,true) + + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s I\'m in! Get to the MASH ASAP! ", _heliName, _pilotName), self.messageTime,true,true) + + self:__Boarded(5,_heliName,_woundedGroupName) + + return true +end + +--- (Internal) Move group to destination. +-- @param #CSAR self +-- @param Wrapper.Group#GROUP _leader +-- @param Core.Point#COORDINATE _destination +function CSAR:_OrderGroupToMoveToPoint(_leader, _destination) + self:T(self.lid .. " _OrderGroupToMoveToPoint") + local group = _leader + local coordinate = _destination:GetVec2() + + group:SetAIOn() + group:RouteToVec2(coordinate,5) + return self +end + + +--- (internal) Function to check if the heli door(s) are open. Thanks to Shadowze. +-- @param #CSAR self +-- @param #string unit_name Name of unit. +-- @return #boolean outcome The outcome. +function CSAR:_IsLoadingDoorOpen( unit_name ) + self:T(self.lid .. " _IsLoadingDoorOpen") + return UTILS.IsLoadingDoorOpen(unit_name) +end + +--- (Internal) Function to check if heli is close to group. +-- @param #CSAR self +-- @param #number _distance +-- @param Wrapper.Unit#UNIT _heliUnit +-- @param #string _heliName +-- @param Wrapper.Group#GROUP _woundedGroup +-- @param #string _woundedGroupName +-- @return #boolean Outcome +function CSAR:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedGroup, _woundedGroupName) + self:T(self.lid .. " _CheckCloseWoundedGroup") + + local _woundedLeader = _woundedGroup + local _lookupKeyHeli = _heliUnit:GetName() .. "_" .. _woundedGroupName --lookup key for message state tracking + + local _found, _pilotable = self:_CheckNameInDownedPilots(_woundedGroupName) -- #boolean, #CSAR.DownedPilot + local _pilotName = _pilotable.desc + + + local _reset = true + + if (_distance < 500) then + + if self.heliCloseMessage[_lookupKeyHeli] == nil then + if self.autosmoke == true then + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. You\'re close now! Land or hover at the smoke.", _heliName, _pilotName), self.messageTime,false,true) + else + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. You\'re close now! Land in a safe place, I will go there ", _heliName, _pilotName), self.messageTime,false,true) + end + self.heliCloseMessage[_lookupKeyHeli] = true + end + + -- have we landed close enough? + if not _heliUnit:InAir() then + + if self.pilotRuntoExtractPoint == true then + if (_distance < self.extractDistance) then + local _time = self.landedStatus[_lookupKeyHeli] + if _time == nil then + self.landedStatus[_lookupKeyHeli] = math.floor( (_distance - self.loadDistance) / 3.6 ) + _time = self.landedStatus[_lookupKeyHeli] + self:_OrderGroupToMoveToPoint(_woundedGroup, _heliUnit:GetCoordinate()) + self:_DisplayMessageToSAR(_heliUnit, "Wait till " .. _pilotName .. " gets in. \nETA " .. _time .. " more seconds.", self.messageTime, false) + else + _time = self.landedStatus[_lookupKeyHeli] - 10 + self.landedStatus[_lookupKeyHeli] = _time + end + if _time <= 0 or _distance < self.loadDistance then + if self.pilotmustopendoors and not self:_IsLoadingDoorOpen(_heliName) then + self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true) + return true + else + self.landedStatus[_lookupKeyHeli] = nil + self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) + return false + end + end + end + else + if (_distance < self.loadDistance) then + if self.pilotmustopendoors and not self:_IsLoadingDoorOpen(_heliName) then + self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true) + return true + else + self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) + return false + end + end + end + else + + local _unitsInHelicopter = self:_PilotsOnboard(_heliName) + local _maxUnits = self.AircraftType[_heliUnit:GetTypeName()] + if _maxUnits == nil then + _maxUnits = self.max_units + end + + if _heliUnit:InAir() and _unitsInHelicopter + 1 <= _maxUnits then + -- TODO - make variable + if _distance < self.rescuehoverdistance then + + --check height! + local leaderheight = _woundedLeader:GetHeight() + if leaderheight < 0 then leaderheight = 0 end + local _height = _heliUnit:GetHeight() - leaderheight + + -- TODO - make variable + if _height <= self.rescuehoverheight then + + local _time = self.hoverStatus[_lookupKeyHeli] + + if _time == nil then + self.hoverStatus[_lookupKeyHeli] = 10 + _time = 10 + else + _time = self.hoverStatus[_lookupKeyHeli] - 10 + self.hoverStatus[_lookupKeyHeli] = _time + end + + if _time > 0 then + self:_DisplayMessageToSAR(_heliUnit, "Hovering above " .. _pilotName .. ". \n\nHold hover for " .. _time .. " seconds to winch them up. \n\nIf the countdown stops you\'re too far away!", self.messageTime, true) + else + if self.pilotmustopendoors and not self:_IsLoadingDoorOpen(_heliName) then + self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true) + return true + else + self.hoverStatus[_lookupKeyHeli] = nil + self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) + return false + end + end + _reset = false + else + self:_DisplayMessageToSAR(_heliUnit, "Too high to winch " .. _pilotName .. " \nReduce height and hover for 10 seconds!", self.messageTime, true,true) + end + end + + end + end + end + + if _reset then + self.hoverStatus[_lookupKeyHeli] = nil + end + + if _distance < 500 then + return true + else + return false + end +end + +--- (Internal) Monitor in-flight returning groups. +-- @param #CSAR self +-- @param #string heliname Heli name +-- @param #string groupname Group name +-- @param #boolean isairport If true, EVENT.Landing took place at an airport or FARP +function CSAR:_ScheduledSARFlight(heliname,groupname, isairport) + self:T(self.lid .. " _ScheduledSARFlight") + self:T({heliname,groupname}) + local _heliUnit = self:_GetSARHeli(heliname) + local _woundedGroupName = groupname + + if (_heliUnit == nil) then + --helicopter crashed? + self.inTransitGroups[heliname] = nil + return + end + + if self.inTransitGroups[heliname] == nil or self.inTransitGroups[heliname][_woundedGroupName] == nil then + -- Groups already rescued + return + end + + local _dist = self:_GetClosestMASH(_heliUnit) + + if _dist == -1 then + return + end + + if ( _dist < self.FARPRescueDistance or isairport ) and _heliUnit:InAir() == false then + if self.pilotmustopendoors and self:_IsLoadingDoorOpen(heliname) == false then + self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me out!", self.messageTime, true) + else + self:_RescuePilots(_heliUnit) + return + end + end + + --queue up + self:__Returning(-5,heliname,_woundedGroupName, isairport) + return self +end + +--- (Internal) Mark pilot as rescued and remove from tables. +-- @param #CSAR self +-- @param Wrapper.Unit#UNIT _heliUnit +function CSAR:_RescuePilots(_heliUnit) + self:T(self.lid .. " _RescuePilots") + local _heliName = _heliUnit:GetName() + local _rescuedGroups = self.inTransitGroups[_heliName] + + if _rescuedGroups == nil then + -- Groups already rescued + return + end + + local PilotsSaved = self:_PilotsOnboard(_heliName) + + self.inTransitGroups[_heliName] = nil + + local _txt = string.format("%s: The %d pilot(s) have been taken to the\nmedical clinic. Good job!", _heliName, PilotsSaved) + + self:_DisplayMessageToSAR(_heliUnit, _txt, self.messageTime) + -- trigger event + self:__Rescued(-1,_heliUnit,_heliName, PilotsSaved) + return self +end + +--- (Internal) Check and return Wrappe.Unit#UNIT based on the name if alive. +-- @param #CSAR self +-- @param #string _unitname Name of Unit +-- @return Wrapper.Unit#UNIT The unit or nil +function CSAR:_GetSARHeli(_unitName) + self:T(self.lid .. " _GetSARHeli") + local unit = UNIT:FindByName(_unitName) + if unit and unit:IsAlive() then + return unit + else + return nil + end +end + +--- (Internal) Display message to single Unit. +-- @param #CSAR self +-- @param Wrapper.Unit#UNIT _unit Unit #UNIT to display to. +-- @param #string _text Text of message. +-- @param #number _time Message show duration. +-- @param #boolean _clear (optional) Clear screen. +-- @param #boolean _speak (optional) Speak message via SRS. +-- @param #boolean _override (optional) Override message suppression +function CSAR:_DisplayMessageToSAR(_unit, _text, _time, _clear, _speak, _override) + self:T(self.lid .. " _DisplayMessageToSAR") + local group = _unit:GetGroup() + local _clear = _clear or nil + local _time = _time or self.messageTime + if _override or not self.suppressmessages then + local m = MESSAGE:New(_text,_time,"Info",_clear):ToGroup(group) + end + -- integrate SRS + if _speak and self.useSRS then + local srstext = SOUNDTEXT:New(_text) + local path = self.SRSPath + local modulation = self.SRSModulation + local channel = self.SRSchannel + local msrs = MSRS:New(path,channel,modulation) + msrs:PlaySoundText(srstext, 2) + end + return self +end + +--- (Internal) Function to get string of a group\'s position. +-- @param #CSAR self +-- @param Wrapper.Controllable#CONTROLLABLE _woundedGroup Group or Unit object. +-- @return #string Coordinates as Text +function CSAR:_GetPositionOfWounded(_woundedGroup) + self:T(self.lid .. " _GetPositionOfWounded") + local _coordinate = _woundedGroup:GetCoordinate() + local _coordinatesText = "None" + if _coordinate then + if self.coordtype == 0 then -- Lat/Long DMTM + _coordinatesText = _coordinate:ToStringLLDDM() + elseif self.coordtype == 1 then -- Lat/Long DMS + _coordinatesText = _coordinate:ToStringLLDMS() + elseif self.coordtype == 2 then -- MGRS + _coordinatesText = _coordinate:ToStringMGRS() + else -- Bullseye Metric --(medevac.coordtype == 4 or 3) + _coordinatesText = _coordinate:ToStringBULLS(self.coalition) + end + end + return _coordinatesText +end + +--- (Internal) Display active SAR tasks to player. +-- @param #CSAR self +-- @param #string _unitName Unit to display to +function CSAR:_DisplayActiveSAR(_unitName) + self:T(self.lid .. " _DisplayActiveSAR") + local _msg = "Active MEDEVAC/SAR:" + local _heli = self:_GetSARHeli(_unitName) -- Wrapper.Unit#UNIT + if _heli == nil then + return + end + + local _heliSide = self.coalition + local _csarList = {} + + local _DownedPilotTable = self.downedPilots + self:T({Table=_DownedPilotTable}) + for _, _value in pairs(_DownedPilotTable) do + local _groupName = _value.name + self:T(string.format("Display Active Pilot: %s", tostring(_groupName))) + self:T({Table=_value}) + local _woundedGroup = _value.group + if _woundedGroup and _value.alive then + local _coordinatesText = self:_GetPositionOfWounded(_woundedGroup) + local _helicoord = _heli:GetCoordinate() + local _woundcoord = _woundedGroup:GetCoordinate() + local _distance = self:_GetDistance(_helicoord, _woundcoord) + self:T({_distance = _distance}) + local distancetext = "" + if _SETTINGS:IsImperial() then + distancetext = string.format("%.1fnm",UTILS.MetersToNM(_distance)) + else + distancetext = string.format("%.1fkm", _distance/1000.0) + end + if _value.frequency == 0 then--shagrat insert CASEVAC without Frequency + table.insert(_csarList, { dist = _distance, msg = string.format("%s at %s - %s ", _value.desc, _coordinatesText, distancetext) }) + else + table.insert(_csarList, { dist = _distance, msg = string.format("%s at %s - %.2f KHz ADF - %s ", _value.desc, _coordinatesText, _value.frequency / 1000, distancetext) }) + end + end + end + + local function sortDistance(a, b) + return a.dist < b.dist + end + + table.sort(_csarList, sortDistance) + + for _, _line in pairs(_csarList) do + _msg = _msg .. "\n" .. _line.msg + end + + self:_DisplayMessageToSAR(_heli, _msg, self.messageTime*2, false, false, true) + return self +end + +--- (Internal) Find the closest downed pilot to a heli. +-- @param #CSAR self +-- @param Wrapper.Unit#UNIT _heli Helicopter #UNIT +-- @return #table Table of results +function CSAR:_GetClosestDownedPilot(_heli) + self:T(self.lid .. " _GetClosestDownedPilot") + local _side = self.coalition + local _closestGroup = nil + local _shortestDistance = -1 + local _distance = 0 + local _closestGroupInfo = nil + local _heliCoord = _heli:GetCoordinate() or _heli:GetCoordinate() + + if _heliCoord == nil then + self:E("****Error obtaining coordinate!") + return nil + end + + local DownedPilotsTable = self.downedPilots + + for _, _groupInfo in UTILS.spairs(DownedPilotsTable) do + --for _, _groupInfo in pairs(DownedPilotsTable) do + local _woundedName = _groupInfo.name + local _tempWounded = _groupInfo.group + + -- check group exists and not moving to someone else + if _tempWounded then + local _tempCoord = _tempWounded:GetCoordinate() + _distance = self:_GetDistance(_heliCoord, _tempCoord) + + if _distance ~= nil and (_shortestDistance == -1 or _distance < _shortestDistance) then + _shortestDistance = _distance + _closestGroup = _tempWounded + _closestGroupInfo = _groupInfo + end + end + end + + return { pilot = _closestGroup, distance = _shortestDistance, groupInfo = _closestGroupInfo } +end + +--- (Internal) Fire a flare at the point of a downed pilot. +-- @param #CSAR self +-- @param #string _unitName Name of the unit. +function CSAR:_SignalFlare(_unitName) + self:T(self.lid .. " _SignalFlare") + local _heli = self:_GetSARHeli(_unitName) + if _heli == nil then + return + end + + local _closest = self:_GetClosestDownedPilot(_heli) + local smokedist = 8000 + if self.approachdist_far > smokedist then smokedist = self.approachdist_far end + if _closest ~= nil and _closest.pilot ~= nil and _closest.distance > 0 and _closest.distance < smokedist then + + local _clockDir = self:_GetClockDirection(_heli, _closest.pilot) + local _distance = 0 + if _SETTINGS:IsImperial() then + _distance = string.format("%.1fnm",UTILS.MetersToNM(_closest.distance)) + else + _distance = string.format("%.1fkm",_closest.distance) + end + local _msg = string.format("%s - Popping signal flare at your %s o\'clock. Distance %s", _unitName, _clockDir, _distance) + self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true, true) + + local _coord = _closest.pilot:GetCoordinate() + _coord:FlareRed(_clockDir) + else + local _distance = smokedist + if _SETTINGS:IsImperial() then + _distance = string.format("%.1fnm",UTILS.MetersToNM(smokedist)) + else + _distance = string.format("%.1fkm",smokedist/1000) + end + self:_DisplayMessageToSAR(_heli, string.format("No Pilots within %s",_distance), self.messageTime, false, false, true) + end + return self +end + +--- (Internal) Display info to all SAR groups. +-- @param #CSAR self +-- @param #string _message Message to display. +-- @param #number _side Coalition of message. +-- @param #number _messagetime How long to show. +function CSAR:_DisplayToAllSAR(_message, _side, _messagetime) + self:T(self.lid .. " _DisplayToAllSAR") + local messagetime = _messagetime or self.messageTime + for _, _unitName in pairs(self.csarUnits) do + local _unit = self:_GetSARHeli(_unitName) + if _unit and not self.suppressmessages then + self:_DisplayMessageToSAR(_unit, _message, _messagetime) + end + end + return self +end + +---(Internal) Request smoke at closest downed pilot. +--@param #CSAR self +--@param #string _unitName Name of the helicopter +function CSAR:_Reqsmoke( _unitName ) + self:T(self.lid .. " _Reqsmoke") + local _heli = self:_GetSARHeli(_unitName) + if _heli == nil then + return + end + local smokedist = 8000 + if smokedist < self.approachdist_far then smokedist = self.approachdist_far end + local _closest = self:_GetClosestDownedPilot(_heli) + if _closest ~= nil and _closest.pilot ~= nil and _closest.distance > 0 and _closest.distance < smokedist then + local _clockDir = self:_GetClockDirection(_heli, _closest.pilot) + local _distance = 0 + if _SETTINGS:IsImperial() then + _distance = string.format("%.1fnm",UTILS.MetersToNM(_closest.distance)) + else + _distance = string.format("%.1fkm",_closest.distance/1000) + end + local _msg = string.format("%s - Popping smoke at your %s o\'clock. Distance %s", _unitName, _clockDir, _distance) + self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true, true) + local _coord = _closest.pilot:GetCoordinate() + local color = self.smokecolor + _coord:Smoke(color) + else + local _distance = 0 + if _SETTINGS:IsImperial() then + _distance = string.format("%.1fnm",UTILS.MetersToNM(smokedist)) + else + _distance = string.format("%.1fkm",smokedist/1000) + end + self:_DisplayMessageToSAR(_heli, string.format("No Pilots within %s",_distance), self.messageTime, false, false, true) + end + return self +end + +--- (Internal) Determine distance to closest MASH. +-- @param #CSAR self +-- @param Wrapper.Unit#UNIT _heli Helicopter #UNIT +-- @retunr +function CSAR:_GetClosestMASH(_heli) + self:T(self.lid .. " _GetClosestMASH") + local _mashset = self.mash -- Core.Set#SET_GROUP + local _mashes = _mashset:GetSetObjects() -- #table + local _shortestDistance = -1 + local _distance = 0 + local _helicoord = _heli:GetCoordinate() + + local function GetCloseAirbase(coordinate,Coalition,Category) + + local a=coordinate:GetVec3() + local distmin=math.huge + local airbase=nil + for DCSairbaseID, DCSairbase in pairs(world.getAirbases(Coalition)) do + local b=DCSairbase:getPoint() + + local c=UTILS.VecSubstract(a,b) + local dist=UTILS.VecNorm(c) + + if dist 0 then + local PilotTable = self.downedPilots + for _,_pilot in pairs (PilotTable) do + self:T({_pilot}) + local pilot = _pilot -- #CSAR.DownedPilot + local group = pilot.group + local frequency = pilot.frequency or 0 -- thanks to @Thrud + if group and group:IsAlive() and frequency > 0 then + self:_AddBeaconToGroup(group,frequency) + end + end + end + return self +end + +--- (Internal) Helper function to count active downed pilots. +-- @param #CSAR self +-- @return #number Number of pilots in the field. +function CSAR:_CountActiveDownedPilots() + self:T(self.lid .. " _CountActiveDownedPilots") + local PilotsInFieldN = 0 + for _, _unitName in pairs(self.downedPilots) do + self:T({_unitName.desc}) + if _unitName.alive == true then + PilotsInFieldN = PilotsInFieldN + 1 + end + end + return PilotsInFieldN +end + +--- (Internal) Helper to decide if we're over max limit. +-- @param #CSAR self +-- @return #boolean True or false. +function CSAR:_ReachedPilotLimit() + self:T(self.lid .. " _ReachedPilotLimit") + local limit = self.maxdownedpilots + local islimited = self.limitmaxdownedpilots + local count = self:_CountActiveDownedPilots() + if islimited and (count >= limit) then + return true + else + return false + end +end + + ------------------------------ + --- FSM internal Functions --- + ------------------------------ + +--- (Internal) Function called after Start() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +function CSAR:onafterStart(From, Event, To) + self:T({From, Event, To}) + self:I(self.lid .. "Started.") + -- event handler + self:HandleEvent(EVENTS.Takeoff, self._EventHandler) + self:HandleEvent(EVENTS.Land, self._EventHandler) + self:HandleEvent(EVENTS.Ejection, self._EventHandler) + self:HandleEvent(EVENTS.LandingAfterEjection, self._EventHandler) --shagrat + self:HandleEvent(EVENTS.PlayerEnterAircraft, self._EventHandler) + self:HandleEvent(EVENTS.PlayerEnterUnit, self._EventHandler) + self:HandleEvent(EVENTS.PilotDead, self._EventHandler) + if self.useprefix then + local prefixes = self.csarPrefix or {} + self.allheligroupset = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(prefixes):FilterCategoryHelicopter():FilterStart() + else + self.allheligroupset = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterCategoryHelicopter():FilterStart() + end + self.mash = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(self.mashprefix):FilterStart() -- currently only GROUP objects, maybe support STATICs also? + if self.wetfeettemplate then + self.usewetfeet = true + end + self:__Status(-10) + return self +end + +--- (Internal) Function called before Status() event. +-- @param #CSAR self +function CSAR:_CheckDownedPilotTable() + local pilots = self.downedPilots + local npilots = {} + + for _ind,_entry in pairs(pilots) do + local _group = _entry.group + if _group:IsAlive() then + npilots[_ind] = _entry + else + if _entry.alive then + self:__KIA(1,_entry.desc) + end + end + end + self.downedPilots = npilots + return self +end + +--- (Internal) Function called before Status() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +function CSAR:onbeforeStatus(From, Event, To) + self:T({From, Event, To}) + -- housekeeping + self:_AddMedevacMenuItem() + + if not self.BeaconTimer or (self.BeaconTimer and not self.BeaconTimer:IsRunning()) then + self.BeaconTimer = TIMER:New(self._RefreshRadioBeacons,self) + self.BeaconTimer:Start(2,self.beaconRefresher) + end + + self:_CheckDownedPilotTable() + for _,_sar in pairs (self.csarUnits) do + local PilotTable = self.downedPilots + for _,_entry in pairs (PilotTable) do + if _entry.alive then + local entry = _entry -- #CSAR.DownedPilot + local name = entry.name + local timestamp = entry.timestamp or 0 + local now = timer.getAbsTime() + if now - timestamp > 17 then -- only check if we\'re not in approach mode, which is iterations of 5 and 10. + self:_CheckWoundedGroupStatus(_sar,name) + end + end + end + end + return self +end + +--- (Internal) Function called after Status() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +function CSAR:onafterStatus(From, Event, To) + self:T({From, Event, To}) + -- collect some stats + local NumberOfSARPilots = 0 + for _, _unitName in pairs(self.csarUnits) do + NumberOfSARPilots = NumberOfSARPilots + 1 + end + + local PilotsInFieldN = self:_CountActiveDownedPilots() + + local PilotsBoarded = 0 + for _, _unitName in pairs(self.inTransitGroups) do + for _,_units in pairs(_unitName) do + PilotsBoarded = PilotsBoarded + 1 + end + end + + if self.verbose > 0 then + local text = string.format("%s Active SAR: %d | Downed Pilots in field: %d (max %d) | Pilots boarded: %d | Landings: %d | Pilots rescued: %d", + self.lid,NumberOfSARPilots,PilotsInFieldN,self.maxdownedpilots,PilotsBoarded,self.rescues,self.rescuedpilots) + self:T(text) + if self.verbose < 2 then + self:I(text) + elseif self.verbose > 1 then + self:I(text) + local m = MESSAGE:New(text,"10","Status",true):ToCoalition(self.coalition) + end + end + self:__Status(-20) + return self +end + +--- (Internal) Function called after Stop() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +function CSAR:onafterStop(From, Event, To) + self:T({From, Event, To}) + -- event handler + self:UnHandleEvent(EVENTS.Takeoff) + self:UnHandleEvent(EVENTS.Land) + self:UnHandleEvent(EVENTS.Ejection) + self:UnHandleEvent(EVENTS.LandingAfterEjection) -- shagrat + self:UnHandleEvent(EVENTS.PlayerEnterUnit) + self:UnHandleEvent(EVENTS.PlayerEnterAircraft) + self:UnHandleEvent(EVENTS.PilotDead) + self:T(self.lid .. "Stopped.") + return self +end + +--- (Internal) Function called before Approach() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +-- @param #string Heliname Name of the helicopter group. +-- @param #string Woundedgroupname Name of the downed pilot\'s group. +function CSAR:onbeforeApproach(From, Event, To, Heliname, Woundedgroupname) + self:T({From, Event, To, Heliname, Woundedgroupname}) + self:_CheckWoundedGroupStatus(Heliname,Woundedgroupname) + return self +end + +--- (Internal) Function called before Boarded() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +-- @param #string Heliname Name of the helicopter group. +-- @param #string Woundedgroupname Name of the downed pilot\'s group. +function CSAR:onbeforeBoarded(From, Event, To, Heliname, Woundedgroupname) + self:T({From, Event, To, Heliname, Woundedgroupname}) + self:_ScheduledSARFlight(Heliname,Woundedgroupname) + return self +end + +--- (Internal) Function called before Returning() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +-- @param #string Heliname Name of the helicopter group. +-- @param #string Woundedgroupname Name of the downed pilot\'s group. +-- @param #boolean IsAirport True if heli has landed on an AFB (from event land). +function CSAR:onbeforeReturning(From, Event, To, Heliname, Woundedgroupname, IsAirPort) + self:T({From, Event, To, Heliname, Woundedgroupname}) + self:_ScheduledSARFlight(Heliname,Woundedgroupname, IsAirPort) + return self +end + +--- (Internal) Function called before Rescued() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +-- @param Wrapper.Unit#UNIT HeliUnit Unit of the helicopter. +-- @param #string HeliName Name of the helicopter group. +-- @param #number PilotsSaved Number of the saved pilots on board when landing. +function CSAR:onbeforeRescued(From, Event, To, HeliUnit, HeliName, PilotsSaved) + self:T({From, Event, To, HeliName, HeliUnit}) + self.rescues = self.rescues + 1 + self.rescuedpilots = self.rescuedpilots + PilotsSaved + return self +end + +--- (Internal) Function called before PilotDown() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +-- @param Wrapper.Group#GROUP Group Group object of the downed pilot. +-- @param #number Frequency Beacon frequency in kHz. +-- @param #string Leadername Name of the #UNIT of the downed pilot. +-- @param #string CoordinatesText String of the position of the pilot. Format determined by self.coordtype. +function CSAR:onbeforePilotDown(From, Event, To, Group, Frequency, Leadername, CoordinatesText) + self:T({From, Event, To, Group, Frequency, Leadername, CoordinatesText}) + return self +end +-------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- End Ops.CSAR +-------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- +-- ### [CSAR - Combat Search & Rescue](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20CSAR) +-- +-- === +-- +-- **Main Features:** +-- +-- * MOOSE-based Helicopter CSAR Operations for Players. +-- +-- === +-- +-- ### Author: **Applevangelist** (Moose Version), ***Ciribob*** (original), Thanks to: Shadowze, Cammel (testing) +-- @module Ops.CSAR +-- @image OPS_CSAR.jpg + +-- Date: Feb 2022 + +------------------------------------------------------------------------- +--- **CSAR** class, extends Core.Base#BASE, Core.Fsm#FSM +-- @type CSAR +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity level. +-- @field #string lid Class id string for output to DCS log file. +-- @field #number coalition Coalition side number, e.g. `coalition.side.RED`. +-- @extends Core.Fsm#FSM + +--- *Combat search and rescue (CSAR) are search and rescue operations that are carried out during war that are within or near combat zones.* (Wikipedia) +-- +-- === +-- +-- ![Banner Image](OPS_CSAR.jpg) +-- +-- # CSAR Concept +-- +-- * MOOSE-based Helicopter CSAR Operations for Players. +-- * Object oriented refactoring of Ciribob\'s fantastic CSAR script. +-- * No need for extra MIST loading. +-- * Additional events to tailor your mission. +-- * Optional SpawnCASEVAC to create casualties without beacon (e.g. handling dead ground vehicles and create CASVAC requests). +-- +-- ## 0. Prerequisites +-- +-- You need to load an .ogg soundfile for the pilot\'s beacons into the mission, e.g. "beacon.ogg", use a once trigger, "sound to country" for that. +-- Create a late-activated single infantry unit as template in the mission editor and name it e.g. "Downed Pilot". +-- +-- ## 1. Basic Setup +-- +-- A basic setup example is the following: +-- +-- -- Instantiate and start a CSAR for the blue side, with template "Downed Pilot" and alias "Luftrettung" +-- local my_csar = CSAR:New(coalition.side.BLUE,"Downed Pilot","Luftrettung") +-- -- options +-- my_csar.immortalcrew = true -- downed pilot spawn is immortal +-- my_csar.invisiblecrew = false -- downed pilot spawn is visible +-- -- start the FSM +-- my_csar:__Start(5) +-- +-- ## 2. Options +-- +-- The following options are available (with their defaults). Only set the ones you want changed: +-- +-- self.allowDownedPilotCAcontrol = false -- Set to false if you don\'t want to allow control by Combined Arms. +-- self.allowFARPRescue = true -- allows pilots to be rescued by landing at a FARP or Airbase. Else MASH only! +-- self.FARPRescueDistance = 1000 -- you need to be this close to a FARP or Airport for the pilot to be rescued. +-- self.autosmoke = false -- automatically smoke a downed pilot\'s location when a heli is near. +-- self.autosmokedistance = 1000 -- distance for autosmoke +-- self.coordtype = 1 -- Use Lat/Long DDM (0), Lat/Long DMS (1), MGRS (2), Bullseye imperial (3) or Bullseye metric (4) for coordinates. +-- self.csarOncrash = false -- (WIP) If set to true, will generate a downed pilot when a plane crashes as well. +-- self.enableForAI = false -- set to false to disable AI pilots from being rescued. +-- self.pilotRuntoExtractPoint = true -- Downed pilot will run to the rescue helicopter up to self.extractDistance in meters. +-- self.extractDistance = 500 -- Distance the downed pilot will start to run to the rescue helicopter. +-- self.immortalcrew = true -- Set to true to make wounded crew immortal. +-- self.invisiblecrew = false -- Set to true to make wounded crew insvisible. +-- self.loadDistance = 75 -- configure distance for pilots to get into helicopter in meters. +-- self.mashprefix = {"MASH"} -- prefixes of #GROUP objects used as MASHes. +-- self.max_units = 6 -- max number of pilots that can be carried if #CSAR.AircraftType is undefined. +-- self.messageTime = 15 -- Time to show messages for in seconds. Doubled for long messages. +-- self.radioSound = "beacon.ogg" -- the name of the sound file to use for the pilots\' radio beacons. +-- self.smokecolor = 4 -- Color of smokemarker, 0 is green, 1 is red, 2 is white, 3 is orange and 4 is blue. +-- self.useprefix = true -- Requires CSAR helicopter #GROUP names to have the prefix(es) defined below. +-- self.csarPrefix = { "helicargo", "MEDEVAC"} -- #GROUP name prefixes used for useprefix=true - DO NOT use # in helicopter names in the Mission Editor! +-- self.verbose = 0 -- set to > 1 for stats output for debugging. +-- -- (added 0.1.4) limit amount of downed pilots spawned by **ejection** events +-- self.limitmaxdownedpilots = true +-- self.maxdownedpilots = 10 +-- -- (added 0.1.8) - allow to set far/near distance for approach and optionally pilot must open doors +-- self.approachdist_far = 5000 -- switch do 10 sec interval approach mode, meters +-- self.approachdist_near = 3000 -- switch to 5 sec interval approach mode, meters +-- self.pilotmustopendoors = false -- switch to true to enable check of open doors +-- -- (added 0.1.9) +-- self.suppressmessages = false -- switch off all messaging if you want to do your own +-- -- (added 0.1.11) +-- self.rescuehoverheight = 20 -- max height for a hovering rescue in meters +-- self.rescuehoverdistance = 10 -- max distance for a hovering rescue in meters +-- -- (added 0.1.12) +-- -- Country codes for spawned pilots +-- self.countryblue= country.id.USA +-- self.countryred = country.id.RUSSIA +-- self.countryneutral = country.id.UN_PEACEKEEPERS +-- +-- ## 2.1 Experimental Features +-- +-- WARNING - Here\'ll be dragons! +-- DANGER - For this to work you need to de-sanitize your mission environment (all three entries) in \Scripts\MissionScripting.lua +-- Needs SRS => 1.9.6 to work (works on the **server** side of SRS) +-- self.useSRS = false -- Set true to use FF\'s SRS integration +-- self.SRSPath = "E:\\Progra~1\\DCS-SimpleRadio-Standalone\\" -- adjust your own path in your SRS installation -- server(!) +-- self.SRSchannel = 300 -- radio channel +-- self.SRSModulation = radio.modulation.AM -- modulation +-- -- +-- self.csarUsePara = false -- If set to true, will use the LandingAfterEjection Event instead of Ejection --shagrat +-- self.wetfeettemplate = "man in floating thingy" -- if you use a mod to have a pilot in a rescue float, put the template name in here for wet feet spawns. Note: in conjunction with csarUsePara this might create dual ejected pilots in edge cases. +-- +-- ## 3. Results +-- +-- Number of successful landings with save pilots and aggregated number of saved pilots is stored in these variables in the object: +-- +-- self.rescues -- number of successful landings *with* saved pilots +-- self.rescuedpilots -- aggregated number of pilots rescued from the field (of *all* players) +-- +-- ## 4. Events +-- +-- The class comes with a number of FSM-based events that missions designers can use to shape their mission. +-- These are: +-- +-- ### 4.1. PilotDown. +-- +-- The event is triggered when a new downed pilot is detected. Use e.g. `function my_csar:OnAfterPilotDown(...)` to link into this event: +-- +-- function my_csar:OnAfterPilotDown(from, event, to, spawnedgroup, frequency, groupname, coordinates_text) +-- ... your code here ... +-- end +-- +-- ### 4.2. Approach. +-- +-- A CSAR helicpoter is closing in on a downed pilot. Use e.g. `function my_csar:OnAfterApproach(...)` to link into this event: +-- +-- function my_csar:OnAfterApproach(from, event, to, heliname, groupname) +-- ... your code here ... +-- end +-- +-- ### 4.3. Boarded. +-- +-- The pilot has been boarded to the helicopter. Use e.g. `function my_csar:OnAfterBoarded(...)` to link into this event: +-- +-- function my_csar:OnAfterBoarded(from, event, to, heliname, groupname) +-- ... your code here ... +-- end +-- +-- ### 4.4. Returning. +-- +-- The CSAR helicopter is ready to return to an Airbase, FARP or MASH. Use e.g. `function my_csar:OnAfterReturning(...)` to link into this event: +-- +-- function my_csar:OnAfterReturning(from, event, to, heliname, groupname) +-- ... your code here ... +-- end +-- +-- ### 4.5. Rescued. +-- +-- The CSAR helicopter has landed close to an Airbase/MASH/FARP and the pilots are safe. Use e.g. `function my_csar:OnAfterRescued(...)` to link into this event: +-- +-- function my_csar:OnAfterRescued(from, event, to, heliunit, heliname, pilotssaved) +-- ... your code here ... +-- end +-- +-- ## 5. Spawn downed pilots at location to be picked up. +-- +-- If missions designers want to spawn downed pilots into the field, e.g. at mission begin to give the helicopter guys works, they can do this like so: +-- +-- -- Create downed "Pilot Wagner" in #ZONE "CSAR_Start_1" at a random point for the blue coalition +-- my_csar:SpawnCSARAtZone( "CSAR_Start_1", coalition.side.BLUE, "Pilot Wagner", true ) +-- +-- --Create a casualty and CASEVAC request from a "Point" (VEC2) for the blue coalition --shagrat +-- my_csar:SpawnCASEVAC(Point, coalition.side.BLUE) +-- +-- @field #CSAR +CSAR = { + ClassName = "CSAR", + verbose = 0, + lid = "", + coalition = 1, + coalitiontxt = "blue", + FreeVHFFrequencies = {}, + UsedVHFFrequencies = {}, + takenOff = {}, + csarUnits = {}, -- table of unit names + downedPilots = {}, + woundedGroups = {}, + landedStatus = {}, + addedTo = {}, + woundedGroups = {}, -- contains the new group of units + inTransitGroups = {}, -- contain a table for each SAR with all units he has with the original names + smokeMarkers = {}, -- tracks smoke markers for groups + heliVisibleMessage = {}, -- tracks if the first message has been sent of the heli being visible + heliCloseMessage = {}, -- tracks heli close message ie heli < 500m distance + max_units = 6, --number of pilots that can be carried + hoverStatus = {}, -- tracks status of a helis hover above a downed pilot + pilotDisabled = {}, -- tracks what aircraft a pilot is disabled for + pilotLives = {}, -- tracks how many lives a pilot has + useprefix = true, -- Use the Prefixed defined below, Requires Unit have the Prefix defined below + csarPrefix = {}, + template = nil, + mash = {}, + smokecolor = 4, + rescues = 0, + rescuedpilots = 0, + limitmaxdownedpilots = true, + maxdownedpilots = 10, +} + +--- Downed pilots info. +-- @type CSAR.DownedPilot +-- @field #number index Pilot index. +-- @field #string name Name of the spawned group. +-- @field #number side Coalition. +-- @field #string originalUnit Name of the original unit. +-- @field #string desc Description. +-- @field #string typename Typename of Unit. +-- @field #number frequency Frequency of the NDB. +-- @field #string player Player name if applicable. +-- @field Wrapper.Group#GROUP group Spawned group object. +-- @field #number timestamp Timestamp for approach process. +-- @field #boolean alive Group is alive or dead/rescued. +-- @field #boolean wetfeet Group is spawned over (deep) water. + +--- All slot / Limit settings +-- @type CSAR.AircraftType +-- @field #string typename Unit type name. +CSAR.AircraftType = {} -- Type and limit +CSAR.AircraftType["SA342Mistral"] = 2 +CSAR.AircraftType["SA342Minigun"] = 2 +CSAR.AircraftType["SA342L"] = 4 +CSAR.AircraftType["SA342M"] = 4 +CSAR.AircraftType["UH-1H"] = 8 +CSAR.AircraftType["Mi-8MTV2"] = 12 +CSAR.AircraftType["Mi-8MT"] = 12 +CSAR.AircraftType["Mi-24P"] = 8 +CSAR.AircraftType["Mi-24V"] = 8 +CSAR.AircraftType["Bell-47"] = 2 +CSAR.AircraftType["UH-60L"] = 10 + +--- CSAR class version. +-- @field #string version +CSAR.version="1.0.4a" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- DONE: SRS Integration (to be tested) +-- TODO: Maybe - add option to smoke/flare closest MASH +-- TODO: shagrat Add cargoWeight to helicopter when pilot boarded +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new CSAR object and start the FSM. +-- @param #CSAR self +-- @param #number Coalition Coalition side. Can also be passed as a string "red", "blue" or "neutral". +-- @param #string Template Name of the late activated infantry unit standing in for the downed pilot. +-- @param #string Alias An *optional* alias how this object is called in the logs etc. +-- @return #CSAR self +function CSAR:New(Coalition, Template, Alias) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #CSAR + + --set Coalition + if Coalition and type(Coalition)=="string" then + if Coalition=="blue" then + self.coalition=coalition.side.BLUE + self.coalitiontxt = Coalition + elseif Coalition=="red" then + self.coalition=coalition.side.RED + self.coalitiontxt = Coalition + elseif Coalition=="neutral" then + self.coalition=coalition.side.NEUTRAL + self.coalitiontxt = Coalition + else + self:E("ERROR: Unknown coalition in CSAR!") + end + else + self.coalition = Coalition + self.coalitiontxt = string.lower(UTILS.GetCoalitionName(self.coalition)) + end + + -- Set alias. + if Alias then + self.alias=tostring(Alias) + else + self.alias="Red Cross" + if self.coalition then + if self.coalition==coalition.side.RED then + self.alias="IFRC" + elseif self.coalition==coalition.side.BLUE then + self.alias="CSAR" + end + end + end + + -- Set some string id for output to DCS.log file. + self.lid=string.format("%s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown") + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FSM. + self:AddTransition("*", "Status", "*") -- CSAR status update. + self:AddTransition("*", "PilotDown", "*") -- Downed Pilot added + self:AddTransition("*", "Approach", "*") -- CSAR heli closing in. + self:AddTransition("*", "Boarded", "*") -- Pilot boarded. + self:AddTransition("*", "Returning", "*") -- CSAR able to return to base. + self:AddTransition("*", "Rescued", "*") -- Pilot at MASH. + self:AddTransition("*", "KIA", "*") -- Pilot killed in action. + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + -- tables, mainly for tracking actions + self.addedTo = {} + self.allheligroupset = {} -- GROUP_SET of all helis + self.csarUnits = {} -- table of CSAR unit names + self.FreeVHFFrequencies = {} + self.heliVisibleMessage = {} -- tracks if the first message has been sent of the heli being visible + self.heliCloseMessage = {} -- tracks heli close message ie heli < 500m distance + self.hoverStatus = {} -- tracks status of a helis hover above a downed pilot + self.inTransitGroups = {} -- contain a table for each SAR with all units he has with the original names + self.landedStatus = {} + self.lastCrash = {} + self.takenOff = {} + self.smokeMarkers = {} -- tracks smoke markers for groups + self.UsedVHFFrequencies = {} + self.woundedGroups = {} -- contains the new group of units + self.downedPilots = {} -- Replacement woundedGroups + self.downedpilotcounter = 1 + + -- settings, counters etc + self.rescues = 0 -- counter for successful rescue landings at FARP/AFB/MASH + self.rescuedpilots = 0 -- counter for saved pilots + self.csarOncrash = false -- If set to true, will generate a csar when a plane crashes as well. + self.allowDownedPilotCAcontrol = false -- Set to false if you don\'t want to allow control by Combined arms. + self.enableForAI = false -- set to false to disable AI units from being rescued. + self.smokecolor = 4 -- Color of smokemarker for blue side, 0 is green, 1 is red, 2 is white, 3 is orange and 4 is blue + self.coordtype = 2 -- Use Lat/Long DDM (0), Lat/Long DMS (1), MGRS (2), Bullseye imperial (3) or Bullseye metric (4) for coordinates. + self.immortalcrew = true -- Set to true to make wounded crew immortal + self.invisiblecrew = false -- Set to true to make wounded crew insvisible + self.messageTime = 15 -- Time to show longer messages for in seconds + self.pilotRuntoExtractPoint = true -- Downed Pilot will run to the rescue helicopter up to self.extractDistance METERS + self.loadDistance = 75 -- configure distance for pilot to get in helicopter in meters. + self.extractDistance = 500 -- Distance the Downed pilot will run to the rescue helicopter + self.loadtimemax = 135 -- seconds + self.radioSound = "beacon.ogg" -- the name of the sound file to use for the Pilot radio beacons. If this isnt added to the mission BEACONS WONT WORK! + self.beaconRefresher = 29 -- seconds + self.allowFARPRescue = true --allows pilot to be rescued by landing at a FARP or Airbase + self.FARPRescueDistance = 1000 -- you need to be this close to a FARP or Airport for the pilot to be rescued. + self.max_units = 6 --max number of pilots that can be carried + self.useprefix = true -- Use the Prefixed defined below, Requires Unit have the Prefix defined below + self.csarPrefix = { "helicargo", "MEDEVAC"} -- prefixes used for useprefix=true - DON\'T use # in names! + self.template = Template or "generic" -- template for downed pilot + self.mashprefix = {"MASH"} -- prefixes used to find MASHes + + self.autosmoke = false -- automatically smoke location when heli is near + self.autosmokedistance = 2000 -- distance for autosmoke + -- added 0.1.4 + self.limitmaxdownedpilots = true + self.maxdownedpilots = 25 + -- generate Frequencies + self:_GenerateVHFrequencies() + -- added 0.1.8 + self.approachdist_far = 5000 -- switch do 10 sec interval approach mode, meters + self.approachdist_near = 3000 -- switch to 5 sec interval approach mode, meters + self.pilotmustopendoors = false -- switch to true to enable check on open doors + self.suppressmessages = false + + -- added 0.1.11r1 + self.rescuehoverheight = 20 + self.rescuehoverdistance = 10 + + -- added 0.1.12 + self.countryblue= country.id.USA + self.countryred = country.id.RUSSIA + self.countryneutral = country.id.UN_PEACEKEEPERS + + -- added 0.1.3 + self.csarUsePara = false -- shagrat set to true, will use the LandingAfterEjection Event instead of Ejection + + -- added 0.1.4 + self.wetfeettemplate = nil + self.usewetfeet = false + + -- WARNING - here\'ll be dragons + -- for this to work you need to de-sanitize your mission environment in \Scripts\MissionScripting.lua + -- needs SRS => 1.9.6 to work (works on the *server* side) + self.useSRS = false -- Use FF\'s SRS integration + self.SRSPath = "E:\\Progra~1\\DCS-SimpleRadio-Standalone\\" -- adjust your own path in your server(!) + self.SRSchannel = 300 -- radio channel + self.SRSModulation = radio.modulation.AM -- modulation + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the CSAR. Initializes parameters and starts event handlers. + -- @function [parent=#CSAR] Start + -- @param #CSAR self + + --- Triggers the FSM event "Start" after a delay. Starts the CSAR. Initializes parameters and starts event handlers. + -- @function [parent=#CSAR] __Start + -- @param #CSAR self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the CSAR and all its event handlers. + -- @param #CSAR self + + --- Triggers the FSM event "Stop" after a delay. Stops the CSAR and all its event handlers. + -- @function [parent=#CSAR] __Stop + -- @param #CSAR self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Status". + -- @function [parent=#CSAR] Status + -- @param #CSAR self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#CSAR] __Status + -- @param #CSAR self + -- @param #number delay Delay in seconds. + + --- On After "PilotDown" event. Downed Pilot detected. + -- @function [parent=#CSAR] OnAfterPilotDown + -- @param #CSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Group#GROUP Group Group object of the downed pilot. + -- @param #number Frequency Beacon frequency in kHz. + -- @param #string Leadername Name of the #UNIT of the downed pilot. + -- @param #string CoordinatesText String of the position of the pilot. Format determined by self.coordtype. + + --- On After "Aproach" event. Heli close to downed Pilot. + -- @function [parent=#CSAR] OnAfterApproach + -- @param #CSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string Heliname Name of the helicopter group. + -- @param #string Woundedgroupname Name of the downed pilot\'s group. + + --- On After "Boarded" event. Downed pilot boarded heli. + -- @function [parent=#CSAR] OnAfterBoarded + -- @param #CSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string Heliname Name of the helicopter group. + -- @param #string Woundedgroupname Name of the downed pilot\'s group. + + --- On After "Returning" event. Heli can return home with downed pilot(s). + -- @function [parent=#CSAR] OnAfterReturning + -- @param #CSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string Heliname Name of the helicopter group. + -- @param #string Woundedgroupname Name of the downed pilot\'s group. + + --- On After "Rescued" event. Pilot(s) have been brought to the MASH/FARP/AFB. + -- @function [parent=#CSAR] OnAfterRescued + -- @param #CSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Unit#UNIT HeliUnit Unit of the helicopter. + -- @param #string HeliName Name of the helicopter group. + -- @param #number PilotsSaved Number of the saved pilots on board when landing. + + --- On After "KIA" event. Pilot is dead. + -- @function [parent=#CSAR] OnAfterKIA + -- @param #CSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string Pilotname Name of the pilot KIA. + + return self +end + +------------------------ +--- Helper Functions --- +------------------------ + +--- (Internal) Function to insert downed pilot tracker object. +-- @param #CSAR self +-- @param Wrapper.Group#GROUP Group The #GROUP object +-- @param #string Groupname Name of the spawned group. +-- @param #number Side Coalition. +-- @param #string OriginalUnit Name of original Unit. +-- @param #string Description Descriptive text. +-- @param #string Typename Typename of unit. +-- @param #number Frequency Frequency of the NDB in Hz +-- @param #string Playername Name of Player (if applicable) +-- @param #boolean Wetfeet Ejected over water +-- @return #CSAR self. +function CSAR:_CreateDownedPilotTrack(Group,Groupname,Side,OriginalUnit,Description,Typename,Frequency,Playername,Wetfeet) + self:T({"_CreateDownedPilotTrack",Groupname,Side,OriginalUnit,Description,Typename,Frequency,Playername}) + + -- create new entry + local DownedPilot = {} -- #CSAR.DownedPilot + DownedPilot.desc = Description or "" + DownedPilot.frequency = Frequency or 0 + DownedPilot.index = self.downedpilotcounter + DownedPilot.name = Groupname or "" + DownedPilot.originalUnit = OriginalUnit or "" + DownedPilot.player = Playername or "" + DownedPilot.side = Side or 0 + DownedPilot.typename = Typename or "" + DownedPilot.group = Group + DownedPilot.timestamp = 0 + DownedPilot.alive = true + DownedPilot.wetfeet = Wetfeet or false + + -- Add Pilot + local PilotTable = self.downedPilots + local counter = self.downedpilotcounter + PilotTable[counter] = {} + PilotTable[counter] = DownedPilot + self:T({Table=PilotTable}) + self.downedPilots = PilotTable + -- Increase counter + self.downedpilotcounter = self.downedpilotcounter+1 + return self +end + +--- (Internal) Count pilots on board. +-- @param #CSAR self +-- @param #string _heliName +-- @return #number count +function CSAR:_PilotsOnboard(_heliName) + self:T(self.lid .. " _PilotsOnboard") + local count = 0 + if self.inTransitGroups[_heliName] then + for _, _group in pairs(self.inTransitGroups[_heliName]) do + count = count + 1 + end + end + return count +end + +--- (Internal) Function to check for dupe eject events. +-- @param #CSAR self +-- @param #string _unitname Name of unit. +-- @return #boolean Outcome +function CSAR:_DoubleEjection(_unitname) + if self.lastCrash[_unitname] then + local _time = self.lastCrash[_unitname] + if timer.getTime() - _time < 10 then + self:E(self.lid.."Caught double ejection!") + return true + end + end + self.lastCrash[_unitname] = timer.getTime() + return false +end + +--- (Internal) Spawn a downed pilot +-- @param #CSAR self +-- @param #number country Country for template. +-- @param Core.Point#COORDINATE point Coordinate to spawn at. +-- @param #number frequency Frequency of the pilot's beacon +-- @param #boolean wetfeet Spawn is over water +-- @return Wrapper.Group#GROUP group The #GROUP object. +-- @return #string alias The alias name. +function CSAR:_SpawnPilotInField(country,point,frequency,wetfeet) + self:T({country,point,frequency,tostring(wetfeet)}) + local freq = frequency or 1000 + local freq = freq / 1000 -- kHz + for i=1,10 do + math.random(i,10000) + end + if point:IsSurfaceTypeWater() or wetfeet then + point.y = 0 + end + local template = self.template + if self.usewetfeet and wetfeet then + template = self.wetfeettemplate + end + local alias = string.format("Pilot %.2fkHz-%d", freq, math.random(1,99)) + local coalition = self.coalition + local pilotcacontrol = self.allowDownedPilotCAcontrol -- Switch AI on/oof - is this really correct for CA? + local _spawnedGroup = SPAWN + :NewWithAlias(template,alias) + :InitCoalition(coalition) + :InitCountry(country) + :InitAIOnOff(pilotcacontrol) + :InitDelayOff() + :SpawnFromCoordinate(point) + + return _spawnedGroup, alias -- Wrapper.Group#GROUP object +end + +--- (Internal) Add options to a downed pilot +-- @param #CSAR self +-- @param Wrapper.Group#GROUP group Group to use. +function CSAR:_AddSpecialOptions(group) + self:T(self.lid.." _AddSpecialOptions") + self:T({group}) + + local immortalcrew = self.immortalcrew + local invisiblecrew = self.invisiblecrew + if immortalcrew then + local _setImmortal = { + id = 'SetImmortal', + params = { + value = true + } + } + group:SetCommand(_setImmortal) + end + + if invisiblecrew then + local _setInvisible = { + id = 'SetInvisible', + params = { + value = true + } + } + group:SetCommand(_setInvisible) + end + + group:OptionAlarmStateGreen() + group:OptionROEHoldFire() + return self +end + +--- (Internal) Function to spawn a CSAR object into the scene. +-- @param #CSAR self +-- @param #number _coalition Coalition +-- @param DCS#country.id _country Country ID +-- @param Core.Point#COORDINATE _point Coordinate +-- @param #string _typeName Typename +-- @param #string _unitName Unitname +-- @param #string _playerName Playername +-- @param #number _freq Frequency +-- @param #boolean noMessage +-- @param #string _description Description +-- @param #boolean forcedesc Use the description only for the pilot track entry +function CSAR:_AddCsar(_coalition , _country, _point, _typeName, _unitName, _playerName, _freq, noMessage, _description, forcedesc ) + self:T(self.lid .. " _AddCsar") + self:T({_coalition , _country, _point, _typeName, _unitName, _playerName, _freq, noMessage, _description}) + + local template = self.template + local wetfeet = false + + local surface = _point:GetSurfaceType() + if surface == land.SurfaceType.WATER then + wetfeet = true + end + + if not _freq then + _freq = self:_GenerateADFFrequency() + if not _freq then _freq = 333000 end --noob catch + end + + local _spawnedGroup, _alias = self:_SpawnPilotInField(_country,_point,_freq,wetfeet) + + local _typeName = _typeName or "Pilot" + + if not noMessage then + if _freq ~= 0 then --shagrat different CASEVAC msg + self:_DisplayToAllSAR("MAYDAY MAYDAY! " .. _typeName .. " is down. ", self.coalition, self.messageTime) + else + self:_DisplayToAllSAR("Troops In Contact. " .. _typeName .. " requests CASEVAC. ", self.coalition, self.messageTime) + end + end + + if (_freq and _freq ~= 0) then --shagrat only add beacon if _freq is NOT 0 + self:_AddBeaconToGroup(_spawnedGroup, _freq) + end + + self:_AddSpecialOptions(_spawnedGroup) + + local _text = _description + if not forcedesc then + if _playerName ~= nil then + if _freq ~= 0 then --shagrat + _text = "Pilot " .. _playerName + else + _text = "TIC - " .. _playerName + end + elseif _unitName ~= nil then + if _freq ~= 0 then --shagrat + _text = "AI Pilot of " .. _unitName + else + _text = "TIC - " .. _unitName + end + end + end + self:T({_spawnedGroup, _alias}) + + local _GroupName = _spawnedGroup:GetName() or _alias + + self:_CreateDownedPilotTrack(_spawnedGroup,_GroupName,_coalition,_unitName,_text,_typeName,_freq,_playerName,wetfeet) + + self:_InitSARForPilot(_spawnedGroup, _unitName, _freq, noMessage) --shagrat use unitName to have the aircraft callsign / descriptive "name" etc. + + return self +end + +--- (Internal) 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 _zone Name of the zone. Can also be passed as a (normal, round) ZONE object. +-- @param #number _coalition Coalition. +-- @param #string _description (optional) Description. +-- @param #boolean _randomPoint (optional) Random yes or no. +-- @param #boolean _nomessage (optional) If true, don\'t send a message to SAR. +-- @param #string unitname (optional) Name of the lost unit. +-- @param #string typename (optional) Type of plane. +-- @param #boolean forcedesc (optional) Force to use the description passed only for the pilot track entry. Use to have fully custom names. +function CSAR:_SpawnCsarAtZone( _zone, _coalition, _description, _randomPoint, _nomessage, unitname, typename, forcedesc) + self:T(self.lid .. " _SpawnCsarAtZone") + local freq = self:_GenerateADFFrequency() + + local _triggerZone = nil + if type(_zone) == "string" then + _triggerZone = ZONE:New(_zone) -- trigger to use as reference position + elseif type(_zone) == "table" and _zone.ClassName then + if string.find(_zone.ClassName, "ZONE",1) then + _triggerZone = _zone -- is already a zone + end + end + + if _triggerZone == nil then + self:E(self.lid.."ERROR: Can\'t find zone called " .. _zone, 10) + return + end + + local _description = _description or "PoW" + local unitname = unitname or "Old Rusty" + local typename = typename or "Phantom II" + + local pos = {} + if _randomPoint then + local _pos = _triggerZone:GetRandomPointVec3() + pos = COORDINATE:NewFromVec3(_pos) + else + pos = _triggerZone:GetCoordinate() + end + + local _country = 0 + if _coalition == coalition.side.BLUE then + _country = self.countryblue + elseif _coalition == coalition.side.RED then + _country = self.countryred + else + _country = self.countryneutral + end + + self:_AddCsar(_coalition, _country, pos, typename, unitname, _description, freq, _nomessage, _description, forcedesc) + + return self +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 Zone Name of the zone. Can also be passed as a (normal, round) ZONE object. +-- @param #number Coalition Coalition. +-- @param #string Description (optional) Description. +-- @param #boolean RandomPoint (optional) Random yes or no. +-- @param #boolean Nomessage (optional) If true, don\'t send a message to SAR. +-- @param #string Unitname (optional) Name of the lost unit. +-- @param #string Typename (optional) Type of plane. +-- @param #boolean Forcedesc (optional) Force to use the **description passed only** for the pilot track entry. Use to have fully custom names. +-- @usage If missions designers want to spawn downed pilots into the field, e.g. at mission begin, to give the helicopter guys work, they can do this like so: +-- +-- -- Create downed "Pilot Wagner" in #ZONE "CSAR_Start_1" at a random point for the blue coalition +-- my_csar:SpawnCSARAtZone( "CSAR_Start_1", coalition.side.BLUE, "Wagner", true, false, "Charly-1-1", "F5E" ) +function CSAR:SpawnCSARAtZone(Zone, Coalition, Description, RandomPoint, Nomessage, Unitname, Typename, Forcedesc) + self:_SpawnCsarAtZone(Zone, Coalition, Description, RandomPoint, Nomessage, Unitname, Typename, Forcedesc) + return self +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 #number _coalition Coalition. +-- @param #string _description (optional) Description. +-- @param #boolean _nomessage (optional) If true, don\'t send a message to SAR. +-- @param #string unitname (optional) Name of the lost unit. +-- @param #string typename (optional) Type of plane. +-- @param #boolean forcedesc (optional) Force to use the description passed only for the pilot track entry. Use to have fully custom names. +function CSAR:_SpawnCASEVAC( _Point, _coalition, _description, _nomessage, unitname, typename, forcedesc) --shagrat added internal Function _SpawnCASEVAC + self:T(self.lid .. " _SpawnCASEVAC") + + local _description = _description or "CASEVAC" + local unitname = unitname or "CASEVAC" + local typename = typename or "Ground Commander" + + local pos = {} + pos = _Point + + local _country = 0 + if _coalition == coalition.side.BLUE then + _country = self.countryblue + elseif _coalition == coalition.side.RED then + _country = self.countryred + else + _country = self.countryneutral + end + --shagrat set frequency to 0 as "flag" for no beacon + self:_AddCsar(_coalition, _country, pos, typename, unitname, _description, 0, _nomessage, _description, forcedesc) + + return self +end + +--- 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 #number Coalition Coalition. +-- @param #string Description (optional) Description. +-- @param #boolean addBeacon (optional) yes or no. +-- @param #boolean Nomessage (optional) If true, don\'t send a message to SAR. +-- @param #string Unitname (optional) Name of the lost unit. +-- @param #string Typename (optional) Type of plane. +-- @param #boolean Forcedesc (optional) Force to use the **description passed only** for the pilot track entry. Use to have fully custom names. +-- @usage If missions designers want to spawn downed pilots into the field, e.g. at mission begin, to give the helicopter guys work, they can do this like so: +-- +-- -- Create casualty "CASEVAC" at Point #POINT_VEC2 for the blue coalition. +-- my_csar:SpawnCASEVAC( POINT_VEC2, coalition.side.BLUE ) +function CSAR:SpawnCASEVAC(Point, Coalition, Description, Nomessage, Unitname, Typename, Forcedesc) + self:_SpawnCASEVAC(Point, Coalition, Description, Nomessage, Unitname, Typename, Forcedesc) + return self +end --shagrat end added CASEVAC + +--- (Internal) Event handler. +-- @param #CSAR self +function CSAR:_EventHandler(EventData) + self:T(self.lid .. " _EventHandler") + self:T({Event = EventData.id}) + + local _event = EventData -- Core.Event#EVENTDATA + + -- no Player + if self.enableForAI == false and _event.IniPlayerName == nil then + return + end + + -- no event + if _event == nil or _event.initiator == nil then + return false + + -- take off + elseif _event.id == EVENTS.Takeoff then -- taken off + self:T(self.lid .. " Event unit - Takeoff") + + local _coalition = _event.IniCoalition + if _coalition ~= self.coalition then + return --ignore! + end + + if _event.IniGroupName then + self.takenOff[_event.IniUnitName] = true + end + + return true + + -- player enter unit + elseif _event.id == EVENTS.PlayerEnterAircraft or _event.id == EVENTS.PlayerEnterUnit then --player entered unit + self:T(self.lid .. " Event unit - Player Enter") + + local _coalition = _event.IniCoalition + if _coalition ~= self.coalition then + return --ignore! + end + + if _event.IniPlayerName then + self.takenOff[_event.IniPlayerName] = nil + end + + local _unit = _event.IniUnit + local _group = _event.IniGroup + if _unit:IsHelicopter() or _group:IsHelicopter() then + self:_AddMedevacMenuItem() + end + + return true + + elseif (_event.id == EVENTS.PilotDead and self.csarOncrash == false) then + -- Pilot dead + + self:T(self.lid .. " Event unit - Pilot Dead") + + local _unit = _event.IniUnit + local _unitname = _event.IniUnitName + local _group = _event.IniGroup + + if _unit == nil then + return -- error! + end + + local _coalition = _event.IniCoalition + if _coalition ~= self.coalition then + return --ignore! + end + + -- Catch multiple events here? + if self.takenOff[_event.IniUnitName] == true or _group:IsAirborne() then + if self:_DoubleEjection(_unitname) then + return + end + + else + self:T(self.lid .. " Pilot has not taken off, ignore") + end + + return + + elseif _event.id == EVENTS.PilotDead or _event.id == EVENTS.Ejection then + if _event.id == EVENTS.PilotDead and self.csarOncrash == false then + return + end + self:T(self.lid .. " Event unit - Pilot Ejected") + + local _unit = _event.IniUnit + local _unitname = _event.IniUnitName + local _group = _event.IniGroup + + if _unit == nil then + return -- error! + end + + local _coalition = _unit:GetCoalition() + if _coalition ~= self.coalition then + return --ignore! + end + + if not self.takenOff[_event.IniUnitName] and not _group:IsAirborne() then + self:T(self.lid .. " Pilot has not taken off, ignore") + return -- give up, pilot hasnt taken off + end + + if self:_DoubleEjection(_unitname) then + return + end + + -- limit no of pilots in the field. + if self.limitmaxdownedpilots and self:_ReachedPilotLimit() then + return + end + + + -- TODO: Over water check --- EVENTS.LandingAfterEjection NOT triggered by DCS, so handle csarUsePara = true case + -- might create dual pilots in edge cases + + local wetfeet = false + + local surface = _unit:GetCoordinate():GetSurfaceType() + if surface == land.SurfaceType.WATER then + wetfeet = true + end + -- all checks passed, get going. + if self.csarUsePara == false or (self.csarUsePara and wetfeet ) then --shagrat check parameter LandingAfterEjection, if true don't spawn a Pilot from EJECTION event, wait for the Chute to land local _freq = self:_GenerateADFFrequency() self:_AddCsar(_coalition, _unit:GetCountry(), _unit:GetCoordinate() , _unit:GetTypeName(), _unit:GetName(), _event.IniPlayerName, _freq, false, "none") return true From 8a3120be39683332180dae673875e7e6ae6b1cd9 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sat, 19 Feb 2022 13:26:02 +0100 Subject: [PATCH 03/26] AUTOLASE - added nil check for CanLase() --- .../Moose/Functional/Autolase.lua | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/Moose Development/Moose/Functional/Autolase.lua b/Moose Development/Moose/Functional/Autolase.lua index 606f9c94b..b67313f44 100644 --- a/Moose Development/Moose/Functional/Autolase.lua +++ b/Moose Development/Moose/Functional/Autolase.lua @@ -674,25 +674,27 @@ end function AUTOLASE:CanLase(Recce,Unit) local canlase = false -- cooldown? - local name = Recce:GetName() - local cooldown = self.RecceUnits[name].cooldown and self.forcecooldown - if cooldown then - local Tdiff = timer.getAbsTime() - self.RecceUnits[name].timestamp - if Tdiff < self.cooldowntime then - return false - else - self.RecceUnits[name].cooldown = false + if Recce and Recce:IsAlive() == true then + local name = Recce:GetName() + local cooldown = self.RecceUnits[name].cooldown and self.forcecooldown + if cooldown then + local Tdiff = timer.getAbsTime() - self.RecceUnits[name].timestamp + if Tdiff < self.cooldowntime then + return false + else + self.RecceUnits[name].cooldown = false + end + end + -- calculate LOS + local reccecoord = Recce:GetCoordinate() + local unitcoord = Unit:GetCoordinate() + local islos = reccecoord:IsLOS(unitcoord,2.5) + -- calculate distance + local distance = math.floor(reccecoord:Get3DDistance(unitcoord)) + local lasedistance = self:GetLosFromUnit(Recce) + if distance <= lasedistance and islos then + canlase = true end - end - -- calculate LOS - local reccecoord = Recce:GetCoordinate() - local unitcoord = Unit:GetCoordinate() - local islos = reccecoord:IsLOS(unitcoord,2.5) - -- calculate distance - local distance = math.floor(reccecoord:Get3DDistance(unitcoord)) - local lasedistance = self:GetLosFromUnit(Recce) - if distance <= lasedistance and islos then - canlase = true end return canlase end From 890dae8ba7bd9371db8001a6458c3a5a51e88555 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Mon, 21 Feb 2022 08:35:56 +0100 Subject: [PATCH 04/26] SEAD - adding workaround for AGM_154 which lost target data --- Moose Development/Moose/Functional/Sead.lua | 25 ++++++++++++--------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/Moose Development/Moose/Functional/Sead.lua b/Moose Development/Moose/Functional/Sead.lua index c7dbfe9db..2982f478e 100644 --- a/Moose Development/Moose/Functional/Sead.lua +++ b/Moose Development/Moose/Functional/Sead.lua @@ -19,7 +19,7 @@ -- -- ### Authors: **FlightControl**, **applevangelist** -- --- Last Update: Nov 2021 +-- Last Update: Feb 2022 -- -- === -- @@ -33,7 +33,7 @@ --- Make SAM sites execute evasive and defensive behaviour when being fired upon. -- -- This class is very easy to use. Just setup a SEAD object by using @{#SEAD.New}() and SAMs will evade and take defensive action when being fired upon. --- Once a HARM attack is detected, SEADwill shut down the radars of the attacked SAM site and take evasive action by moving the SAM +-- Once a HARM attack is detected, SEAD will shut down the radars of the attacked SAM site and take evasive action by moving the SAM -- vehicles around (*if* they are drivable, that is). There's a component of randomness in detection and evasion, which is based on the -- skill set of the SAM set (the higher the skill, the more likely). When a missile is fired from far away, the SAM will stay active for a -- period of time to stay defensive, before it takes evasive actions. @@ -141,7 +141,7 @@ function SEAD:New( SEADGroupPrefixes, Padding ) self:AddTransition("*", "ManageEvasion", "*") self:AddTransition("*", "CalculateHitZone", "*") - self:I("*** SEAD - Started Version 0.4.2") + self:I("*** SEAD - Started Version 0.4.3") return self end @@ -267,9 +267,10 @@ end -- @param Core.Point#COORDINATE pos0 Position of the plane when it fired -- @param #number height Height when the missile was fired -- @param Wrapper.Group#GROUP SEADGroup Attacker group +-- @param #string SEADWeaponName Weapon Name -- @return #SEAD self -function SEAD:onafterCalculateHitZone(From,Event,To,SEADWeapon,pos0,height,SEADGroup) - self:T("**** Calculating hit zone") +function SEAD:onafterCalculateHitZone(From,Event,To,SEADWeapon,pos0,height,SEADGroup,SEADWeaponName) + self:T("**** Calculating hit zone for " .. (SEADWeaponName or "None")) if SEADWeapon and SEADWeapon:isExist() then --local pos = SEADWeapon:getPoint() @@ -285,6 +286,9 @@ function SEAD:onafterCalculateHitZone(From,Event,To,SEADWeapon,pos0,height,SEADG -- velocity local wpndata = SEAD.HarmData["AGM_88"] + if string.find(SEADWeaponName,"154",1) then + wpndata = SEAD.HarmData["AGM_154"] + end local mveloc = math.floor(wpndata[2] * 340.29) local c1 = (2*mheight*9.81)/(mveloc^2) local c2 = (mveloc^2) / 9.81 @@ -459,14 +463,15 @@ function SEAD:HandleEventShot( EventData ) local _targetskill = "Random" local _targetgroupname = "none" local _target = EventData.Weapon:getTarget() -- Identify target - if not _target or self.debug then -- AGM-88 w/o target data - if string.find(SEADWeaponName,"AGM_88",1,true) then - self:I("**** Tracking AGM-88 with no target data.") + if not _target or self.debug then -- AGM-88 or 154 w/o target data + self:E("***** SEAD - No target data for " .. (SEADWeaponName or "None")) + if string.find(SEADWeaponName,"AGM_88",1,true) or string.find(SEADWeaponName,"AGM_154",1,true) then + self:I("**** Tracking AGM-88/154 with no target data.") local pos0 = SEADPlane:GetCoordinate() local fheight = SEADPlane:GetHeight() - self:__CalculateHitZone(20,SEADWeapon,pos0,fheight,SEADGroup) - return self + self:__CalculateHitZone(20,SEADWeapon,pos0,fheight,SEADGroup,SEADWeaponName) end + return self end local targetcat = _target:getCategory() -- Identify category local _targetUnit = nil -- Wrapper.Unit#UNIT From 594febaece1b6e61f110587354809b6cf7a20280 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Mon, 21 Feb 2022 19:36:13 +0100 Subject: [PATCH 05/26] CSAR - remove double class --- Moose Development/Moose/Ops/CSAR.lua | 2210 -------------------------- 1 file changed, 2210 deletions(-) diff --git a/Moose Development/Moose/Ops/CSAR.lua b/Moose Development/Moose/Ops/CSAR.lua index 354fd9f1e..f8998b328 100644 --- a/Moose Development/Moose/Ops/CSAR.lua +++ b/Moose Development/Moose/Ops/CSAR.lua @@ -256,2216 +256,6 @@ CSAR.AircraftType["Mi-24V"] = 8 CSAR.AircraftType["Bell-47"] = 2 CSAR.AircraftType["UH-60L"] = 10 ---- CSAR class version. --- @field #string version -CSAR.version="1.0.4" - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- ToDo list -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - --- DONE: SRS Integration (to be tested) --- TODO: Maybe - add option to smoke/flare closest MASH --- TODO: shagrat Add cargoWeight to helicopter when pilot boarded -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Constructor -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Create a new CSAR object and start the FSM. --- @param #CSAR self --- @param #number Coalition Coalition side. Can also be passed as a string "red", "blue" or "neutral". --- @param #string Template Name of the late activated infantry unit standing in for the downed pilot. --- @param #string Alias An *optional* alias how this object is called in the logs etc. --- @return #CSAR self -function CSAR:New(Coalition, Template, Alias) - - -- Inherit everything from FSM class. - local self=BASE:Inherit(self, FSM:New()) -- #CSAR - - --set Coalition - if Coalition and type(Coalition)=="string" then - if Coalition=="blue" then - self.coalition=coalition.side.BLUE - self.coalitiontxt = Coalition - elseif Coalition=="red" then - self.coalition=coalition.side.RED - self.coalitiontxt = Coalition - elseif Coalition=="neutral" then - self.coalition=coalition.side.NEUTRAL - self.coalitiontxt = Coalition - else - self:E("ERROR: Unknown coalition in CSAR!") - end - else - self.coalition = Coalition - self.coalitiontxt = string.lower(UTILS.GetCoalitionName(self.coalition)) - end - - -- Set alias. - if Alias then - self.alias=tostring(Alias) - else - self.alias="Red Cross" - if self.coalition then - if self.coalition==coalition.side.RED then - self.alias="IFRC" - elseif self.coalition==coalition.side.BLUE then - self.alias="CSAR" - end - end - end - - -- Set some string id for output to DCS.log file. - self.lid=string.format("%s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown") - - -- Start State. - self:SetStartState("Stopped") - - -- Add FSM transitions. - -- From State --> Event --> To State - self:AddTransition("Stopped", "Start", "Running") -- Start FSM. - self:AddTransition("*", "Status", "*") -- CSAR status update. - self:AddTransition("*", "PilotDown", "*") -- Downed Pilot added - self:AddTransition("*", "Approach", "*") -- CSAR heli closing in. - self:AddTransition("*", "Boarded", "*") -- Pilot boarded. - self:AddTransition("*", "Returning", "*") -- CSAR able to return to base. - self:AddTransition("*", "Rescued", "*") -- Pilot at MASH. - self:AddTransition("*", "KIA", "*") -- Pilot killed in action. - self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. - - -- tables, mainly for tracking actions - self.addedTo = {} - self.allheligroupset = {} -- GROUP_SET of all helis - self.csarUnits = {} -- table of CSAR unit names - self.FreeVHFFrequencies = {} - self.heliVisibleMessage = {} -- tracks if the first message has been sent of the heli being visible - self.heliCloseMessage = {} -- tracks heli close message ie heli < 500m distance - self.hoverStatus = {} -- tracks status of a helis hover above a downed pilot - self.inTransitGroups = {} -- contain a table for each SAR with all units he has with the original names - self.landedStatus = {} - self.lastCrash = {} - self.takenOff = {} - self.smokeMarkers = {} -- tracks smoke markers for groups - self.UsedVHFFrequencies = {} - self.woundedGroups = {} -- contains the new group of units - self.downedPilots = {} -- Replacement woundedGroups - self.downedpilotcounter = 1 - - -- settings, counters etc - self.rescues = 0 -- counter for successful rescue landings at FARP/AFB/MASH - self.rescuedpilots = 0 -- counter for saved pilots - self.csarOncrash = false -- If set to true, will generate a csar when a plane crashes as well. - self.allowDownedPilotCAcontrol = false -- Set to false if you don\'t want to allow control by Combined arms. - self.enableForAI = false -- set to false to disable AI units from being rescued. - self.smokecolor = 4 -- Color of smokemarker for blue side, 0 is green, 1 is red, 2 is white, 3 is orange and 4 is blue - self.coordtype = 2 -- Use Lat/Long DDM (0), Lat/Long DMS (1), MGRS (2), Bullseye imperial (3) or Bullseye metric (4) for coordinates. - self.immortalcrew = true -- Set to true to make wounded crew immortal - self.invisiblecrew = false -- Set to true to make wounded crew insvisible - self.messageTime = 15 -- Time to show longer messages for in seconds - self.pilotRuntoExtractPoint = true -- Downed Pilot will run to the rescue helicopter up to self.extractDistance METERS - self.loadDistance = 75 -- configure distance for pilot to get in helicopter in meters. - self.extractDistance = 500 -- Distance the Downed pilot will run to the rescue helicopter - self.loadtimemax = 135 -- seconds - self.radioSound = "beacon.ogg" -- the name of the sound file to use for the Pilot radio beacons. If this isnt added to the mission BEACONS WONT WORK! - self.beaconRefresher = 29 -- seconds - self.allowFARPRescue = true --allows pilot to be rescued by landing at a FARP or Airbase - self.FARPRescueDistance = 1000 -- you need to be this close to a FARP or Airport for the pilot to be rescued. - self.max_units = 6 --max number of pilots that can be carried - self.useprefix = true -- Use the Prefixed defined below, Requires Unit have the Prefix defined below - self.csarPrefix = { "helicargo", "MEDEVAC"} -- prefixes used for useprefix=true - DON\'T use # in names! - self.template = Template or "generic" -- template for downed pilot - self.mashprefix = {"MASH"} -- prefixes used to find MASHes - - self.autosmoke = false -- automatically smoke location when heli is near - self.autosmokedistance = 2000 -- distance for autosmoke - -- added 0.1.4 - self.limitmaxdownedpilots = true - self.maxdownedpilots = 25 - -- generate Frequencies - self:_GenerateVHFrequencies() - -- added 0.1.8 - self.approachdist_far = 5000 -- switch do 10 sec interval approach mode, meters - self.approachdist_near = 3000 -- switch to 5 sec interval approach mode, meters - self.pilotmustopendoors = false -- switch to true to enable check on open doors - self.suppressmessages = false - - -- added 0.1.11r1 - self.rescuehoverheight = 20 - self.rescuehoverdistance = 10 - - -- added 0.1.12 - self.countryblue= country.id.USA - self.countryred = country.id.RUSSIA - self.countryneutral = country.id.UN_PEACEKEEPERS - - -- added 0.1.3 - self.csarUsePara = false -- shagrat set to true, will use the LandingAfterEjection Event instead of Ejection - - -- added 0.1.4 - self.wetfeettemplate = nil - self.usewetfeet = false - - -- WARNING - here\'ll be dragons - -- for this to work you need to de-sanitize your mission environment in \Scripts\MissionScripting.lua - -- needs SRS => 1.9.6 to work (works on the *server* side) - self.useSRS = false -- Use FF\'s SRS integration - self.SRSPath = "E:\\Progra~1\\DCS-SimpleRadio-Standalone\\" -- adjust your own path in your server(!) - self.SRSchannel = 300 -- radio channel - self.SRSModulation = radio.modulation.AM -- modulation - - ------------------------ - --- Pseudo Functions --- - ------------------------ - - --- Triggers the FSM event "Start". Starts the CSAR. Initializes parameters and starts event handlers. - -- @function [parent=#CSAR] Start - -- @param #CSAR self - - --- Triggers the FSM event "Start" after a delay. Starts the CSAR. Initializes parameters and starts event handlers. - -- @function [parent=#CSAR] __Start - -- @param #CSAR self - -- @param #number delay Delay in seconds. - - --- Triggers the FSM event "Stop". Stops the CSAR and all its event handlers. - -- @param #CSAR self - - --- Triggers the FSM event "Stop" after a delay. Stops the CSAR and all its event handlers. - -- @function [parent=#CSAR] __Stop - -- @param #CSAR self - -- @param #number delay Delay in seconds. - - --- Triggers the FSM event "Status". - -- @function [parent=#CSAR] Status - -- @param #CSAR self - - --- Triggers the FSM event "Status" after a delay. - -- @function [parent=#CSAR] __Status - -- @param #CSAR self - -- @param #number delay Delay in seconds. - - --- On After "PilotDown" event. Downed Pilot detected. - -- @function [parent=#CSAR] OnAfterPilotDown - -- @param #CSAR self - -- @param #string From From state. - -- @param #string Event Event. - -- @param #string To To state. - -- @param Wrapper.Group#GROUP Group Group object of the downed pilot. - -- @param #number Frequency Beacon frequency in kHz. - -- @param #string Leadername Name of the #UNIT of the downed pilot. - -- @param #string CoordinatesText String of the position of the pilot. Format determined by self.coordtype. - - --- On After "Aproach" event. Heli close to downed Pilot. - -- @function [parent=#CSAR] OnAfterApproach - -- @param #CSAR self - -- @param #string From From state. - -- @param #string Event Event. - -- @param #string To To state. - -- @param #string Heliname Name of the helicopter group. - -- @param #string Woundedgroupname Name of the downed pilot\'s group. - - --- On After "Boarded" event. Downed pilot boarded heli. - -- @function [parent=#CSAR] OnAfterBoarded - -- @param #CSAR self - -- @param #string From From state. - -- @param #string Event Event. - -- @param #string To To state. - -- @param #string Heliname Name of the helicopter group. - -- @param #string Woundedgroupname Name of the downed pilot\'s group. - - --- On After "Returning" event. Heli can return home with downed pilot(s). - -- @function [parent=#CSAR] OnAfterReturning - -- @param #CSAR self - -- @param #string From From state. - -- @param #string Event Event. - -- @param #string To To state. - -- @param #string Heliname Name of the helicopter group. - -- @param #string Woundedgroupname Name of the downed pilot\'s group. - - --- On After "Rescued" event. Pilot(s) have been brought to the MASH/FARP/AFB. - -- @function [parent=#CSAR] OnAfterRescued - -- @param #CSAR self - -- @param #string From From state. - -- @param #string Event Event. - -- @param #string To To state. - -- @param Wrapper.Unit#UNIT HeliUnit Unit of the helicopter. - -- @param #string HeliName Name of the helicopter group. - -- @param #number PilotsSaved Number of the saved pilots on board when landing. - - --- On After "KIA" event. Pilot is dead. - -- @function [parent=#CSAR] OnAfterKIA - -- @param #CSAR self - -- @param #string From From state. - -- @param #string Event Event. - -- @param #string To To state. - -- @param #string Pilotname Name of the pilot KIA. - - return self -end - ------------------------- ---- Helper Functions --- ------------------------- - ---- (Internal) Function to insert downed pilot tracker object. --- @param #CSAR self --- @param Wrapper.Group#GROUP Group The #GROUP object --- @param #string Groupname Name of the spawned group. --- @param #number Side Coalition. --- @param #string OriginalUnit Name of original Unit. --- @param #string Description Descriptive text. --- @param #string Typename Typename of unit. --- @param #number Frequency Frequency of the NDB in Hz --- @param #string Playername Name of Player (if applicable) --- @param #boolean Wetfeet Ejected over water --- @return #CSAR self. -function CSAR:_CreateDownedPilotTrack(Group,Groupname,Side,OriginalUnit,Description,Typename,Frequency,Playername,Wetfeet) - self:T({"_CreateDownedPilotTrack",Groupname,Side,OriginalUnit,Description,Typename,Frequency,Playername}) - - -- create new entry - local DownedPilot = {} -- #CSAR.DownedPilot - DownedPilot.desc = Description or "" - DownedPilot.frequency = Frequency or 0 - DownedPilot.index = self.downedpilotcounter - DownedPilot.name = Groupname or "" - DownedPilot.originalUnit = OriginalUnit or "" - DownedPilot.player = Playername or "" - DownedPilot.side = Side or 0 - DownedPilot.typename = Typename or "" - DownedPilot.group = Group - DownedPilot.timestamp = 0 - DownedPilot.alive = true - DownedPilot.wetfeet = Wetfeet or false - - -- Add Pilot - local PilotTable = self.downedPilots - local counter = self.downedpilotcounter - PilotTable[counter] = {} - PilotTable[counter] = DownedPilot - self:T({Table=PilotTable}) - self.downedPilots = PilotTable - -- Increase counter - self.downedpilotcounter = self.downedpilotcounter+1 - return self -end - ---- (Internal) Count pilots on board. --- @param #CSAR self --- @param #string _heliName --- @return #number count -function CSAR:_PilotsOnboard(_heliName) - self:T(self.lid .. " _PilotsOnboard") - local count = 0 - if self.inTransitGroups[_heliName] then - for _, _group in pairs(self.inTransitGroups[_heliName]) do - count = count + 1 - end - end - return count -end - ---- (Internal) Function to check for dupe eject events. --- @param #CSAR self --- @param #string _unitname Name of unit. --- @return #boolean Outcome -function CSAR:_DoubleEjection(_unitname) - if self.lastCrash[_unitname] then - local _time = self.lastCrash[_unitname] - if timer.getTime() - _time < 10 then - self:E(self.lid.."Caught double ejection!") - return true - end - end - self.lastCrash[_unitname] = timer.getTime() - return false -end - ---- (Internal) Spawn a downed pilot --- @param #CSAR self --- @param #number country Country for template. --- @param Core.Point#COORDINATE point Coordinate to spawn at. --- @param #number frequency Frequency of the pilot's beacon --- @param #boolean wetfeet Spawn is over water --- @return Wrapper.Group#GROUP group The #GROUP object. --- @return #string alias The alias name. -function CSAR:_SpawnPilotInField(country,point,frequency,wetfeet) - self:T({country,point,frequency,tostring(wetfeet)}) - local freq = frequency or 1000 - local freq = freq / 1000 -- kHz - for i=1,10 do - math.random(i,10000) - end - if point:IsSurfaceTypeWater() or wetfeet then - point.y = 0 - end - local template = self.template - if self.usewetfeet and wetfeet then - template = self.wetfeettemplate - end - local alias = string.format("Pilot %.2fkHz-%d", freq, math.random(1,99)) - local coalition = self.coalition - local pilotcacontrol = self.allowDownedPilotCAcontrol -- Switch AI on/oof - is this really correct for CA? - local _spawnedGroup = SPAWN - :NewWithAlias(template,alias) - :InitCoalition(coalition) - :InitCountry(country) - :InitAIOnOff(pilotcacontrol) - :InitDelayOff() - :SpawnFromCoordinate(point) - - return _spawnedGroup, alias -- Wrapper.Group#GROUP object -end - ---- (Internal) Add options to a downed pilot --- @param #CSAR self --- @param Wrapper.Group#GROUP group Group to use. -function CSAR:_AddSpecialOptions(group) - self:T(self.lid.." _AddSpecialOptions") - self:T({group}) - - local immortalcrew = self.immortalcrew - local invisiblecrew = self.invisiblecrew - if immortalcrew then - local _setImmortal = { - id = 'SetImmortal', - params = { - value = true - } - } - group:SetCommand(_setImmortal) - end - - if invisiblecrew then - local _setInvisible = { - id = 'SetInvisible', - params = { - value = true - } - } - group:SetCommand(_setInvisible) - end - - group:OptionAlarmStateGreen() - group:OptionROEHoldFire() - return self -end - ---- (Internal) Function to spawn a CSAR object into the scene. --- @param #CSAR self --- @param #number _coalition Coalition --- @param DCS#country.id _country Country ID --- @param Core.Point#COORDINATE _point Coordinate --- @param #string _typeName Typename --- @param #string _unitName Unitname --- @param #string _playerName Playername --- @param #number _freq Frequency --- @param #boolean noMessage --- @param #string _description Description --- @param #boolean forcedesc Use the description only for the pilot track entry -function CSAR:_AddCsar(_coalition , _country, _point, _typeName, _unitName, _playerName, _freq, noMessage, _description, forcedesc ) - self:T(self.lid .. " _AddCsar") - self:T({_coalition , _country, _point, _typeName, _unitName, _playerName, _freq, noMessage, _description}) - - local template = self.template - local wetfeet = false - - local surface = _point:GetSurfaceType() - if surface == land.SurfaceType.WATER then - wetfeet = true - end - - if not _freq then - _freq = self:_GenerateADFFrequency() - if not _freq then _freq = 333000 end --noob catch - end - - local _spawnedGroup, _alias = self:_SpawnPilotInField(_country,_point,_freq,wetfeet) - - local _typeName = _typeName or "Pilot" - - if not noMessage then - if _freq ~= 0 then --shagrat different CASEVAC msg - self:_DisplayToAllSAR("MAYDAY MAYDAY! " .. _typeName .. " is down. ", self.coalition, self.messageTime) - else - self:_DisplayToAllSAR("Troops In Contact. " .. _typeName .. " requests CASEVAC. ", self.coalition, self.messageTime) - end - end - - if (_freq and _freq ~= 0) then --shagrat only add beacon if _freq is NOT 0 - self:_AddBeaconToGroup(_spawnedGroup, _freq) - end - - self:_AddSpecialOptions(_spawnedGroup) - - local _text = _description - if not forcedesc then - if _playerName ~= nil then - if _freq ~= 0 then --shagrat - _text = "Pilot " .. _playerName - else - _text = "TIC - " .. _playerName - end - elseif _unitName ~= nil then - if _freq ~= 0 then --shagrat - _text = "AI Pilot of " .. _unitName - else - _text = "TIC - " .. _unitName - end - end - end - self:T({_spawnedGroup, _alias}) - - local _GroupName = _spawnedGroup:GetName() or _alias - - self:_CreateDownedPilotTrack(_spawnedGroup,_GroupName,_coalition,_unitName,_text,_typeName,_freq,_playerName,wetfeet) - - self:_InitSARForPilot(_spawnedGroup, _unitName, _freq, noMessage) --shagrat use unitName to have the aircraft callsign / descriptive "name" etc. - - return self -end - ---- (Internal) 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 _zone Name of the zone. Can also be passed as a (normal, round) ZONE object. --- @param #number _coalition Coalition. --- @param #string _description (optional) Description. --- @param #boolean _randomPoint (optional) Random yes or no. --- @param #boolean _nomessage (optional) If true, don\'t send a message to SAR. --- @param #string unitname (optional) Name of the lost unit. --- @param #string typename (optional) Type of plane. --- @param #boolean forcedesc (optional) Force to use the description passed only for the pilot track entry. Use to have fully custom names. -function CSAR:_SpawnCsarAtZone( _zone, _coalition, _description, _randomPoint, _nomessage, unitname, typename, forcedesc) - self:T(self.lid .. " _SpawnCsarAtZone") - local freq = self:_GenerateADFFrequency() - - local _triggerZone = nil - if type(_zone) == "string" then - _triggerZone = ZONE:New(_zone) -- trigger to use as reference position - elseif type(_zone) == "table" and _zone.ClassName then - if string.find(_zone.ClassName, "ZONE",1) then - _triggerZone = _zone -- is already a zone - end - end - - if _triggerZone == nil then - self:E(self.lid.."ERROR: Can\'t find zone called " .. _zone, 10) - return - end - - local _description = _description or "PoW" - local unitname = unitname or "Old Rusty" - local typename = typename or "Phantom II" - - local pos = {} - if _randomPoint then - local _pos = _triggerZone:GetRandomPointVec3() - pos = COORDINATE:NewFromVec3(_pos) - else - pos = _triggerZone:GetCoordinate() - end - - local _country = 0 - if _coalition == coalition.side.BLUE then - _country = self.countryblue - elseif _coalition == coalition.side.RED then - _country = self.countryred - else - _country = self.countryneutral - end - - self:_AddCsar(_coalition, _country, pos, typename, unitname, _description, freq, _nomessage, _description, forcedesc) - - return self -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 Zone Name of the zone. Can also be passed as a (normal, round) ZONE object. --- @param #number Coalition Coalition. --- @param #string Description (optional) Description. --- @param #boolean RandomPoint (optional) Random yes or no. --- @param #boolean Nomessage (optional) If true, don\'t send a message to SAR. --- @param #string Unitname (optional) Name of the lost unit. --- @param #string Typename (optional) Type of plane. --- @param #boolean Forcedesc (optional) Force to use the **description passed only** for the pilot track entry. Use to have fully custom names. --- @usage If missions designers want to spawn downed pilots into the field, e.g. at mission begin, to give the helicopter guys work, they can do this like so: --- --- -- Create downed "Pilot Wagner" in #ZONE "CSAR_Start_1" at a random point for the blue coalition --- my_csar:SpawnCSARAtZone( "CSAR_Start_1", coalition.side.BLUE, "Wagner", true, false, "Charly-1-1", "F5E" ) -function CSAR:SpawnCSARAtZone(Zone, Coalition, Description, RandomPoint, Nomessage, Unitname, Typename, Forcedesc) - self:_SpawnCsarAtZone(Zone, Coalition, Description, RandomPoint, Nomessage, Unitname, Typename, Forcedesc) - return self -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 #number _coalition Coalition. --- @param #string _description (optional) Description. --- @param #boolean _nomessage (optional) If true, don\'t send a message to SAR. --- @param #string unitname (optional) Name of the lost unit. --- @param #string typename (optional) Type of plane. --- @param #boolean forcedesc (optional) Force to use the description passed only for the pilot track entry. Use to have fully custom names. -function CSAR:_SpawnCASEVAC( _Point, _coalition, _description, _nomessage, unitname, typename, forcedesc) --shagrat added internal Function _SpawnCASEVAC - self:T(self.lid .. " _SpawnCASEVAC") - - local _description = _description or "CASEVAC" - local unitname = unitname or "CASEVAC" - local typename = typename or "Ground Commander" - - local pos = {} - pos = _Point - - local _country = 0 - if _coalition == coalition.side.BLUE then - _country = self.countryblue - elseif _coalition == coalition.side.RED then - _country = self.countryred - else - _country = self.countryneutral - end - --shagrat set frequency to 0 as "flag" for no beacon - self:_AddCsar(_coalition, _country, pos, typename, unitname, _description, 0, _nomessage, _description, forcedesc) - - return self -end - ---- 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 #number Coalition Coalition. --- @param #string Description (optional) Description. --- @param #boolean addBeacon (optional) yes or no. --- @param #boolean Nomessage (optional) If true, don\'t send a message to SAR. --- @param #string Unitname (optional) Name of the lost unit. --- @param #string Typename (optional) Type of plane. --- @param #boolean Forcedesc (optional) Force to use the **description passed only** for the pilot track entry. Use to have fully custom names. --- @usage If missions designers want to spawn downed pilots into the field, e.g. at mission begin, to give the helicopter guys work, they can do this like so: --- --- -- Create casualty "CASEVAC" at Point #POINT_VEC2 for the blue coalition. --- my_csar:SpawnCASEVAC( POINT_VEC2, coalition.side.BLUE ) -function CSAR:SpawnCASEVAC(Point, Coalition, Description, Nomessage, Unitname, Typename, Forcedesc) - self:_SpawnCASEVAC(Point, Coalition, Description, Nomessage, Unitname, Typename, Forcedesc) - return self -end --shagrat end added CASEVAC - ---- (Internal) Event handler. --- @param #CSAR self -function CSAR:_EventHandler(EventData) - self:T(self.lid .. " _EventHandler") - self:T({Event = EventData.id}) - - local _event = EventData -- Core.Event#EVENTDATA - - -- no Player - if self.enableForAI == false and _event.IniPlayerName == nil then - return - end - - -- no event - if _event == nil or _event.initiator == nil then - return false - - -- take off - elseif _event.id == EVENTS.Takeoff then -- taken off - self:T(self.lid .. " Event unit - Takeoff") - - local _coalition = _event.IniCoalition - if _coalition ~= self.coalition then - return --ignore! - end - - if _event.IniGroupName then - self.takenOff[_event.IniUnitName] = true - end - - return true - - -- player enter unit - elseif _event.id == EVENTS.PlayerEnterAircraft or _event.id == EVENTS.PlayerEnterUnit then --player entered unit - self:T(self.lid .. " Event unit - Player Enter") - - local _coalition = _event.IniCoalition - if _coalition ~= self.coalition then - return --ignore! - end - - if _event.IniPlayerName then - self.takenOff[_event.IniPlayerName] = nil - end - - local _unit = _event.IniUnit - local _group = _event.IniGroup - if _unit:IsHelicopter() or _group:IsHelicopter() then - self:_AddMedevacMenuItem() - end - - return true - - elseif (_event.id == EVENTS.PilotDead and self.csarOncrash == false) then - -- Pilot dead - - self:T(self.lid .. " Event unit - Pilot Dead") - - local _unit = _event.IniUnit - local _unitname = _event.IniUnitName - local _group = _event.IniGroup - - if _unit == nil then - return -- error! - end - - local _coalition = _event.IniCoalition - if _coalition ~= self.coalition then - return --ignore! - end - - -- Catch multiple events here? - if self.takenOff[_event.IniUnitName] == true or _group:IsAirborne() then - if self:_DoubleEjection(_unitname) then - return - end - - else - self:T(self.lid .. " Pilot has not taken off, ignore") - end - - return - - elseif _event.id == EVENTS.PilotDead or _event.id == EVENTS.Ejection then - if _event.id == EVENTS.PilotDead and self.csarOncrash == false then - return - end - self:T(self.lid .. " Event unit - Pilot Ejected") - - local _unit = _event.IniUnit - local _unitname = _event.IniUnitName - local _group = _event.IniGroup - - if _unit == nil then - return -- error! - end - - local _coalition = _unit:GetCoalition() - if _coalition ~= self.coalition then - return --ignore! - end - - if not self.takenOff[_event.IniUnitName] and not _group:IsAirborne() then - self:T(self.lid .. " Pilot has not taken off, ignore") - return -- give up, pilot hasnt taken off - end - - if self:_DoubleEjection(_unitname) then - return - end - - -- limit no of pilots in the field. - if self.limitmaxdownedpilots and self:_ReachedPilotLimit() then - return - end - - - -- TODO: Over water check --- EVENTS.LandingAfterEjection NOT triggered by DCS, so handle csarUsePara = true case - -- might create dual pilots in edge cases - - local wetfeet = false - - local surface = _unit:GetCoordinate():GetSurfaceType() - if surface == land.SurfaceType.WATER then - wetfeet = true - end - -- all checks passed, get going. - if self.csarUsePara == false or (self.csarUsePara and wetfeet ) then --shagrat check parameter LandingAfterEjection, if true don't spawn a Pilot from EJECTION event, wait for the Chute to land - local _freq = self:_GenerateADFFrequency() - self:_AddCsar(_coalition, _unit:GetCountry(), _unit:GetCoordinate() , _unit:GetTypeName(), _unit:GetName(), _event.IniPlayerName, _freq, false, "none") - return true - end - - ---- shagrat on event LANDING_AFTER_EJECTION spawn pilot at parachute location - elseif (_event.id == EVENTS.LandingAfterEjection and self.csarUsePara == true) then - self:I({EVENT=_event}) - local _LandingPos = COORDINATE:NewFromVec3(_event.initiator:getPosition().p) - local _unitname = "Aircraft" --_event.initiator:getName() or "Aircraft" --shagrat Optional use of Object name which is unfortunately 'f15_Pilot_Parachute' - local _typename = "Ejected Pilot" --_event.Initiator.getTypeName() or "Ejected Pilot" - local _country = _event.initiator:getCountry() - local _coalition = coalition.getCountryCoalition( _country ) - if _coalition == self.coalition then - local _freq = self:_GenerateADFFrequency() - self:I({coalition=_coalition,country= _country, coord=_LandingPos, name=_unitname, player=_event.IniPlayerName, freq=_freq}) - self:_AddCsar(_coalition, _country, _LandingPos, nil, _unitname, _event.IniPlayerName, _freq, false, "none")--shagrat add CSAR at Parachute location. - - Unit.destroy(_event.initiator) -- shagrat remove static Pilot model - end - return true - - elseif _event.id == EVENTS.Land then - self:T(self.lid .. " Landing") - - if _event.IniUnitName then - self.takenOff[_event.IniUnitName] = nil - end - - if self.allowFARPRescue then - - local _unit = _event.IniUnit -- Wrapper.Unit#UNIT - - if _unit == nil then - self:T(self.lid .. " Unit nil on landing") - return -- error! - end - - local _coalition = _event.IniCoalition - if _coalition ~= self.coalition then - return --ignore! - end - - self.takenOff[_event.IniUnitName] = nil - - local _place = _event.Place -- Wrapper.Airbase#AIRBASE - - if _place == nil then - self:T(self.lid .. " Landing Place Nil") - return -- error! - end - - -- anyone on board? - if self.inTransitGroups[_event.IniUnitName] == nil then - -- ignore - return - end - - if _place:GetCoalition() == self.coalition or _place:GetCoalition() == coalition.side.NEUTRAL then - self:_ScheduledSARFlight(_event.IniUnitName,_event.IniGroupName,true) - else - self:T(string.format("Airfield %d, Unit %d", _place:GetCoalition(), _unit:GetCoalition())) - end - end - - return true - end - return self -end - ---- (Internal) Initialize the action for a pilot. --- @param #CSAR self --- @param Wrapper.Group#GROUP _downedGroup The group to rescue. --- @param #string _GroupName Name of the Group --- @param #number _freq Beacon frequency. --- @param #boolean _nomessage Send message true or false. -function CSAR:_InitSARForPilot(_downedGroup, _GroupName, _freq, _nomessage) - self:T(self.lid .. " _InitSARForPilot") - local _leader = _downedGroup:GetUnit(1) - local _groupName = _GroupName - local _freqk = _freq / 1000 - local _coordinatesText = self:_GetPositionOfWounded(_downedGroup) - local _leadername = _leader:GetName() - - if not _nomessage then - if _freq ~= 0 then --shagrat - local _text = string.format("%s requests SAR at %s, beacon at %.2f KHz", _groupName, _coordinatesText, _freqk)--shagrat _groupName to prevent 'f15_Pilot_Parachute' - self:_DisplayToAllSAR(_text,self.coalition,self.messageTime) - else --shagrat CASEVAC msg - local _text = string.format("Pickup Zone at %s.", _coordinatesText ) - self:_DisplayToAllSAR(_text,self.coalition,self.messageTime) - end - end - - for _,_heliName in pairs(self.csarUnits) do - self:_CheckWoundedGroupStatus(_heliName, _groupName) - end - - -- trigger FSM event - self:__PilotDown(2,_downedGroup, _freqk, _groupName, _coordinatesText) - - return self -end - ---- (Internal) Check if a name is in downed pilot table --- @param #CSAR self --- @param #string name Name to search for. --- @return #boolean Outcome. --- @return #CSAR.DownedPilot Table if found else nil. -function CSAR:_CheckNameInDownedPilots(name) - local PilotTable = self.downedPilots --#CSAR.DownedPilot - local found = false - local table = nil - for _,_pilot in pairs(PilotTable) do - if _pilot.name == name and _pilot.alive == true then - found = true - table = _pilot - break - end - end - return found, table -end - ---- (Internal) Check if a name is in downed pilot table and remove it. --- @param #CSAR self --- @param #string name Name to search for. --- @param #boolean force Force removal. --- @return #boolean Outcome. -function CSAR:_RemoveNameFromDownedPilots(name,force) - local PilotTable = self.downedPilots --#CSAR.DownedPilot - local found = false - for _index,_pilot in pairs(PilotTable) do - if _pilot.name == name then - self.downedPilots[_index].alive = false - end - end - return found -end - ---- (Internal) Check state of wounded group. --- @param #CSAR self --- @param #string heliname heliname --- @param #string woundedgroupname woundedgroupname -function CSAR:_CheckWoundedGroupStatus(heliname,woundedgroupname) - self:T(self.lid .. " _CheckWoundedGroupStatus") - local _heliName = heliname - local _woundedGroupName = woundedgroupname - self:T({Heli = _heliName, Downed = _woundedGroupName}) - -- if wounded group is not here then message already been sent to SARs - -- stop processing any further - local _found, _downedpilot = self:_CheckNameInDownedPilots(_woundedGroupName) - if not _found then - self:T("...not found in list!") - return - end - - local _woundedGroup = _downedpilot.group - if _woundedGroup ~= nil and _woundedGroup:IsAlive() then - local _heliUnit = self:_GetSARHeli(_heliName) -- Wrapper.Unit#UNIT - - local _lookupKeyHeli = _heliName .. "_" .. _woundedGroupName --lookup key for message state tracking - - if _heliUnit == nil then - self.heliVisibleMessage[_lookupKeyHeli] = nil - self.heliCloseMessage[_lookupKeyHeli] = nil - self.landedStatus[_lookupKeyHeli] = nil - self:T("...helinunit nil!") - return - end - - local _heliCoord = _heliUnit:GetCoordinate() - local _leaderCoord = _woundedGroup:GetCoordinate() - local _distance = self:_GetDistance(_heliCoord,_leaderCoord) - -- autosmoke - if (self.autosmoke == true) and (_distance < self.autosmokedistance) and (_distance ~= -1) then - self:_PopSmokeForGroup(_woundedGroupName, _woundedGroup) - end - - if _distance < self.approachdist_near and _distance > 0 then - if self:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedGroup, _woundedGroupName) == true then - -- we\'re close, reschedule - _downedpilot.timestamp = timer.getAbsTime() - self:__Approach(-5,heliname,woundedgroupname) - end - elseif _distance >= self.approachdist_near and _distance < self.approachdist_far then - -- message once - if self.heliVisibleMessage[_lookupKeyHeli] == nil then - local _pilotName = _downedpilot.desc - if self.autosmoke == true then - local dist = self.autosmokedistance / 1000 - local disttext = string.format("%.0fkm",dist) - if _SETTINGS:IsImperial() then - local dist = UTILS.MetersToNM(self.autosmokedistance) - disttext = string.format("%.0fnm",dist) - end - self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. I hear you! Finally, that is music in my ears!\nI'll pop a smoke when you are %s away.\nLand or hover by the smoke.", _heliName, _pilotName, disttext), self.messageTime,false,true) - else - self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. I hear you! Finally, that is music in my ears!\nRequest a flare or smoke if you need.", _heliName, _pilotName), self.messageTime,false,true) - end - --mark as shown for THIS heli and THIS group - self.heliVisibleMessage[_lookupKeyHeli] = true - end - self.heliCloseMessage[_lookupKeyHeli] = nil - self.landedStatus[_lookupKeyHeli] = nil - --reschedule as units aren\'t dead yet , schedule for a bit slower though as we\'re far away - _downedpilot.timestamp = timer.getAbsTime() - self:__Approach(-10,heliname,woundedgroupname) - end - else - self:T("...Downed Pilot KIA?!") - if not _downedpilot.alive then - --self:__KIA(1,_downedpilot.name) - self:_RemoveNameFromDownedPilots(_downedpilot.name, true) - end - end - return self -end - ---- (Internal) Function to pop a smoke at a wounded pilot\'s positions. --- @param #CSAR self --- @param #string _woundedGroupName Name of the group. --- @param Wrapper.Group#GROUP _woundedLeader Object of the group. -function CSAR:_PopSmokeForGroup(_woundedGroupName, _woundedLeader) - self:T(self.lid .. " _PopSmokeForGroup") - -- have we popped smoke already in the last 5 mins - local _lastSmoke = self.smokeMarkers[_woundedGroupName] - if _lastSmoke == nil or timer.getTime() > _lastSmoke then - - local _smokecolor = self.smokecolor - local _smokecoord = _woundedLeader:GetCoordinate():Translate( 6, math.random( 1, 360) ) --shagrat place smoke at a random 6 m distance, so smoke does not obscure the pilot - _smokecoord:Smoke(_smokecolor) - self.smokeMarkers[_woundedGroupName] = timer.getTime() + 300 -- next smoke time - end - return self -end - ---- (Internal) Function to pickup the wounded pilot from the ground. --- @param #CSAR self --- @param Wrapper.Unit#UNIT _heliUnit Object of the group. --- @param #string _pilotName Name of the pilot. --- @param Wrapper.Group#GROUP _woundedGroup Object of the group. --- @param #string _woundedGroupName Name of the group. -function CSAR:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) - self:T(self.lid .. " _PickupUnit") - -- board - local _heliName = _heliUnit:GetName() - local _groups = self.inTransitGroups[_heliName] - local _unitsInHelicopter = self:_PilotsOnboard(_heliName) - - -- init table if there is none for this helicopter - if not _groups then - self.inTransitGroups[_heliName] = {} - _groups = self.inTransitGroups[_heliName] - end - - -- if the heli can\'t pick them up, show a message and return - local _maxUnits = self.AircraftType[_heliUnit:GetTypeName()] - if _maxUnits == nil then - _maxUnits = self.max_units - end - if _unitsInHelicopter + 1 > _maxUnits then - self:_DisplayMessageToSAR(_heliUnit, string.format("%s, %s. We\'re already crammed with %d guys! Sorry!", _pilotName, _heliName, _unitsInHelicopter, _unitsInHelicopter), self.messageTime) - return true - end - - local found,downedgrouptable = self:_CheckNameInDownedPilots(_woundedGroupName) - local grouptable = downedgrouptable --#CSAR.DownedPilot - self.inTransitGroups[_heliName][_woundedGroupName] = - { - originalUnit = grouptable.originalUnit, - woundedGroup = _woundedGroupName, - side = self.coalition, - desc = grouptable.desc, - player = grouptable.player, - } - - _woundedGroup:Destroy(false) - self:_RemoveNameFromDownedPilots(_woundedGroupName,true) - - self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s I\'m in! Get to the MASH ASAP! ", _heliName, _pilotName), self.messageTime,true,true) - - self:__Boarded(5,_heliName,_woundedGroupName) - - return true -end - ---- (Internal) Move group to destination. --- @param #CSAR self --- @param Wrapper.Group#GROUP _leader --- @param Core.Point#COORDINATE _destination -function CSAR:_OrderGroupToMoveToPoint(_leader, _destination) - self:T(self.lid .. " _OrderGroupToMoveToPoint") - local group = _leader - local coordinate = _destination:GetVec2() - - group:SetAIOn() - group:RouteToVec2(coordinate,5) - return self -end - - ---- (internal) Function to check if the heli door(s) are open. Thanks to Shadowze. --- @param #CSAR self --- @param #string unit_name Name of unit. --- @return #boolean outcome The outcome. -function CSAR:_IsLoadingDoorOpen( unit_name ) - self:T(self.lid .. " _IsLoadingDoorOpen") - return UTILS.IsLoadingDoorOpen(unit_name) -end - ---- (Internal) Function to check if heli is close to group. --- @param #CSAR self --- @param #number _distance --- @param Wrapper.Unit#UNIT _heliUnit --- @param #string _heliName --- @param Wrapper.Group#GROUP _woundedGroup --- @param #string _woundedGroupName --- @return #boolean Outcome -function CSAR:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedGroup, _woundedGroupName) - self:T(self.lid .. " _CheckCloseWoundedGroup") - - local _woundedLeader = _woundedGroup - local _lookupKeyHeli = _heliUnit:GetName() .. "_" .. _woundedGroupName --lookup key for message state tracking - - local _found, _pilotable = self:_CheckNameInDownedPilots(_woundedGroupName) -- #boolean, #CSAR.DownedPilot - local _pilotName = _pilotable.desc - - - local _reset = true - - if (_distance < 500) then - - if self.heliCloseMessage[_lookupKeyHeli] == nil then - if self.autosmoke == true then - self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. You\'re close now! Land or hover at the smoke.", _heliName, _pilotName), self.messageTime,false,true) - else - self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. You\'re close now! Land in a safe place, I will go there ", _heliName, _pilotName), self.messageTime,false,true) - end - self.heliCloseMessage[_lookupKeyHeli] = true - end - - -- have we landed close enough? - if not _heliUnit:InAir() then - - if self.pilotRuntoExtractPoint == true then - if (_distance < self.extractDistance) then - local _time = self.landedStatus[_lookupKeyHeli] - if _time == nil then - self.landedStatus[_lookupKeyHeli] = math.floor( (_distance - self.loadDistance) / 3.6 ) - _time = self.landedStatus[_lookupKeyHeli] - self:_OrderGroupToMoveToPoint(_woundedGroup, _heliUnit:GetCoordinate()) - self:_DisplayMessageToSAR(_heliUnit, "Wait till " .. _pilotName .. " gets in. \nETA " .. _time .. " more seconds.", self.messageTime, false) - else - _time = self.landedStatus[_lookupKeyHeli] - 10 - self.landedStatus[_lookupKeyHeli] = _time - end - if _time <= 0 or _distance < self.loadDistance then - if self.pilotmustopendoors and not self:_IsLoadingDoorOpen(_heliName) then - self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true) - return true - else - self.landedStatus[_lookupKeyHeli] = nil - self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) - return false - end - end - end - else - if (_distance < self.loadDistance) then - if self.pilotmustopendoors and not self:_IsLoadingDoorOpen(_heliName) then - self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true) - return true - else - self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) - return false - end - end - end - else - - local _unitsInHelicopter = self:_PilotsOnboard(_heliName) - local _maxUnits = self.AircraftType[_heliUnit:GetTypeName()] - if _maxUnits == nil then - _maxUnits = self.max_units - end - - if _heliUnit:InAir() and _unitsInHelicopter + 1 <= _maxUnits then - -- TODO - make variable - if _distance < self.rescuehoverdistance then - - --check height! - local leaderheight = _woundedLeader:GetHeight() - if leaderheight < 0 then leaderheight = 0 end - local _height = _heliUnit:GetHeight() - leaderheight - - -- TODO - make variable - if _height <= self.rescuehoverheight then - - local _time = self.hoverStatus[_lookupKeyHeli] - - if _time == nil then - self.hoverStatus[_lookupKeyHeli] = 10 - _time = 10 - else - _time = self.hoverStatus[_lookupKeyHeli] - 10 - self.hoverStatus[_lookupKeyHeli] = _time - end - - if _time > 0 then - self:_DisplayMessageToSAR(_heliUnit, "Hovering above " .. _pilotName .. ". \n\nHold hover for " .. _time .. " seconds to winch them up. \n\nIf the countdown stops you\'re too far away!", self.messageTime, true) - else - if self.pilotmustopendoors and not self:_IsLoadingDoorOpen(_heliName) then - self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true) - return true - else - self.hoverStatus[_lookupKeyHeli] = nil - self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) - return false - end - end - _reset = false - else - self:_DisplayMessageToSAR(_heliUnit, "Too high to winch " .. _pilotName .. " \nReduce height and hover for 10 seconds!", self.messageTime, true,true) - end - end - - end - end - end - - if _reset then - self.hoverStatus[_lookupKeyHeli] = nil - end - - if _distance < 500 then - return true - else - return false - end -end - ---- (Internal) Monitor in-flight returning groups. --- @param #CSAR self --- @param #string heliname Heli name --- @param #string groupname Group name --- @param #boolean isairport If true, EVENT.Landing took place at an airport or FARP -function CSAR:_ScheduledSARFlight(heliname,groupname, isairport) - self:T(self.lid .. " _ScheduledSARFlight") - self:T({heliname,groupname}) - local _heliUnit = self:_GetSARHeli(heliname) - local _woundedGroupName = groupname - - if (_heliUnit == nil) then - --helicopter crashed? - self.inTransitGroups[heliname] = nil - return - end - - if self.inTransitGroups[heliname] == nil or self.inTransitGroups[heliname][_woundedGroupName] == nil then - -- Groups already rescued - return - end - - local _dist = self:_GetClosestMASH(_heliUnit) - - if _dist == -1 then - return - end - - if ( _dist < self.FARPRescueDistance or isairport ) and _heliUnit:InAir() == false then - if self.pilotmustopendoors and self:_IsLoadingDoorOpen(heliname) == false then - self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me out!", self.messageTime, true) - else - self:_RescuePilots(_heliUnit) - return - end - end - - --queue up - self:__Returning(-5,heliname,_woundedGroupName, isairport) - return self -end - ---- (Internal) Mark pilot as rescued and remove from tables. --- @param #CSAR self --- @param Wrapper.Unit#UNIT _heliUnit -function CSAR:_RescuePilots(_heliUnit) - self:T(self.lid .. " _RescuePilots") - local _heliName = _heliUnit:GetName() - local _rescuedGroups = self.inTransitGroups[_heliName] - - if _rescuedGroups == nil then - -- Groups already rescued - return - end - - local PilotsSaved = self:_PilotsOnboard(_heliName) - - self.inTransitGroups[_heliName] = nil - - local _txt = string.format("%s: The %d pilot(s) have been taken to the\nmedical clinic. Good job!", _heliName, PilotsSaved) - - self:_DisplayMessageToSAR(_heliUnit, _txt, self.messageTime) - -- trigger event - self:__Rescued(-1,_heliUnit,_heliName, PilotsSaved) - return self -end - ---- (Internal) Check and return Wrappe.Unit#UNIT based on the name if alive. --- @param #CSAR self --- @param #string _unitname Name of Unit --- @return Wrapper.Unit#UNIT The unit or nil -function CSAR:_GetSARHeli(_unitName) - self:T(self.lid .. " _GetSARHeli") - local unit = UNIT:FindByName(_unitName) - if unit and unit:IsAlive() then - return unit - else - return nil - end -end - ---- (Internal) Display message to single Unit. --- @param #CSAR self --- @param Wrapper.Unit#UNIT _unit Unit #UNIT to display to. --- @param #string _text Text of message. --- @param #number _time Message show duration. --- @param #boolean _clear (optional) Clear screen. --- @param #boolean _speak (optional) Speak message via SRS. --- @param #boolean _override (optional) Override message suppression -function CSAR:_DisplayMessageToSAR(_unit, _text, _time, _clear, _speak, _override) - self:T(self.lid .. " _DisplayMessageToSAR") - local group = _unit:GetGroup() - local _clear = _clear or nil - local _time = _time or self.messageTime - if _override or not self.suppressmessages then - local m = MESSAGE:New(_text,_time,"Info",_clear):ToGroup(group) - end - -- integrate SRS - if _speak and self.useSRS then - local srstext = SOUNDTEXT:New(_text) - local path = self.SRSPath - local modulation = self.SRSModulation - local channel = self.SRSchannel - local msrs = MSRS:New(path,channel,modulation) - msrs:PlaySoundText(srstext, 2) - end - return self -end - ---- (Internal) Function to get string of a group\'s position. --- @param #CSAR self --- @param Wrapper.Controllable#CONTROLLABLE _woundedGroup Group or Unit object. --- @return #string Coordinates as Text -function CSAR:_GetPositionOfWounded(_woundedGroup) - self:T(self.lid .. " _GetPositionOfWounded") - local _coordinate = _woundedGroup:GetCoordinate() - local _coordinatesText = "None" - if _coordinate then - if self.coordtype == 0 then -- Lat/Long DMTM - _coordinatesText = _coordinate:ToStringLLDDM() - elseif self.coordtype == 1 then -- Lat/Long DMS - _coordinatesText = _coordinate:ToStringLLDMS() - elseif self.coordtype == 2 then -- MGRS - _coordinatesText = _coordinate:ToStringMGRS() - else -- Bullseye Metric --(medevac.coordtype == 4 or 3) - _coordinatesText = _coordinate:ToStringBULLS(self.coalition) - end - end - return _coordinatesText -end - ---- (Internal) Display active SAR tasks to player. --- @param #CSAR self --- @param #string _unitName Unit to display to -function CSAR:_DisplayActiveSAR(_unitName) - self:T(self.lid .. " _DisplayActiveSAR") - local _msg = "Active MEDEVAC/SAR:" - local _heli = self:_GetSARHeli(_unitName) -- Wrapper.Unit#UNIT - if _heli == nil then - return - end - - local _heliSide = self.coalition - local _csarList = {} - - local _DownedPilotTable = self.downedPilots - self:T({Table=_DownedPilotTable}) - for _, _value in pairs(_DownedPilotTable) do - local _groupName = _value.name - self:T(string.format("Display Active Pilot: %s", tostring(_groupName))) - self:T({Table=_value}) - local _woundedGroup = _value.group - if _woundedGroup and _value.alive then - local _coordinatesText = self:_GetPositionOfWounded(_woundedGroup) - local _helicoord = _heli:GetCoordinate() - local _woundcoord = _woundedGroup:GetCoordinate() - local _distance = self:_GetDistance(_helicoord, _woundcoord) - self:T({_distance = _distance}) - local distancetext = "" - if _SETTINGS:IsImperial() then - distancetext = string.format("%.1fnm",UTILS.MetersToNM(_distance)) - else - distancetext = string.format("%.1fkm", _distance/1000.0) - end - if _value.frequency == 0 then--shagrat insert CASEVAC without Frequency - table.insert(_csarList, { dist = _distance, msg = string.format("%s at %s - %s ", _value.desc, _coordinatesText, distancetext) }) - else - table.insert(_csarList, { dist = _distance, msg = string.format("%s at %s - %.2f KHz ADF - %s ", _value.desc, _coordinatesText, _value.frequency / 1000, distancetext) }) - end - end - end - - local function sortDistance(a, b) - return a.dist < b.dist - end - - table.sort(_csarList, sortDistance) - - for _, _line in pairs(_csarList) do - _msg = _msg .. "\n" .. _line.msg - end - - self:_DisplayMessageToSAR(_heli, _msg, self.messageTime*2, false, false, true) - return self -end - ---- (Internal) Find the closest downed pilot to a heli. --- @param #CSAR self --- @param Wrapper.Unit#UNIT _heli Helicopter #UNIT --- @return #table Table of results -function CSAR:_GetClosestDownedPilot(_heli) - self:T(self.lid .. " _GetClosestDownedPilot") - local _side = self.coalition - local _closestGroup = nil - local _shortestDistance = -1 - local _distance = 0 - local _closestGroupInfo = nil - local _heliCoord = _heli:GetCoordinate() or _heli:GetCoordinate() - - if _heliCoord == nil then - self:E("****Error obtaining coordinate!") - return nil - end - - local DownedPilotsTable = self.downedPilots - - for _, _groupInfo in UTILS.spairs(DownedPilotsTable) do - --for _, _groupInfo in pairs(DownedPilotsTable) do - local _woundedName = _groupInfo.name - local _tempWounded = _groupInfo.group - - -- check group exists and not moving to someone else - if _tempWounded then - local _tempCoord = _tempWounded:GetCoordinate() - _distance = self:_GetDistance(_heliCoord, _tempCoord) - - if _distance ~= nil and (_shortestDistance == -1 or _distance < _shortestDistance) then - _shortestDistance = _distance - _closestGroup = _tempWounded - _closestGroupInfo = _groupInfo - end - end - end - - return { pilot = _closestGroup, distance = _shortestDistance, groupInfo = _closestGroupInfo } -end - ---- (Internal) Fire a flare at the point of a downed pilot. --- @param #CSAR self --- @param #string _unitName Name of the unit. -function CSAR:_SignalFlare(_unitName) - self:T(self.lid .. " _SignalFlare") - local _heli = self:_GetSARHeli(_unitName) - if _heli == nil then - return - end - - local _closest = self:_GetClosestDownedPilot(_heli) - local smokedist = 8000 - if self.approachdist_far > smokedist then smokedist = self.approachdist_far end - if _closest ~= nil and _closest.pilot ~= nil and _closest.distance > 0 and _closest.distance < smokedist then - - local _clockDir = self:_GetClockDirection(_heli, _closest.pilot) - local _distance = 0 - if _SETTINGS:IsImperial() then - _distance = string.format("%.1fnm",UTILS.MetersToNM(_closest.distance)) - else - _distance = string.format("%.1fkm",_closest.distance) - end - local _msg = string.format("%s - Popping signal flare at your %s o\'clock. Distance %s", _unitName, _clockDir, _distance) - self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true, true) - - local _coord = _closest.pilot:GetCoordinate() - _coord:FlareRed(_clockDir) - else - local _distance = smokedist - if _SETTINGS:IsImperial() then - _distance = string.format("%.1fnm",UTILS.MetersToNM(smokedist)) - else - _distance = string.format("%.1fkm",smokedist/1000) - end - self:_DisplayMessageToSAR(_heli, string.format("No Pilots within %s",_distance), self.messageTime, false, false, true) - end - return self -end - ---- (Internal) Display info to all SAR groups. --- @param #CSAR self --- @param #string _message Message to display. --- @param #number _side Coalition of message. --- @param #number _messagetime How long to show. -function CSAR:_DisplayToAllSAR(_message, _side, _messagetime) - self:T(self.lid .. " _DisplayToAllSAR") - local messagetime = _messagetime or self.messageTime - for _, _unitName in pairs(self.csarUnits) do - local _unit = self:_GetSARHeli(_unitName) - if _unit and not self.suppressmessages then - self:_DisplayMessageToSAR(_unit, _message, _messagetime) - end - end - return self -end - ----(Internal) Request smoke at closest downed pilot. ---@param #CSAR self ---@param #string _unitName Name of the helicopter -function CSAR:_Reqsmoke( _unitName ) - self:T(self.lid .. " _Reqsmoke") - local _heli = self:_GetSARHeli(_unitName) - if _heli == nil then - return - end - local smokedist = 8000 - if smokedist < self.approachdist_far then smokedist = self.approachdist_far end - local _closest = self:_GetClosestDownedPilot(_heli) - if _closest ~= nil and _closest.pilot ~= nil and _closest.distance > 0 and _closest.distance < smokedist then - local _clockDir = self:_GetClockDirection(_heli, _closest.pilot) - local _distance = 0 - if _SETTINGS:IsImperial() then - _distance = string.format("%.1fnm",UTILS.MetersToNM(_closest.distance)) - else - _distance = string.format("%.1fkm",_closest.distance/1000) - end - local _msg = string.format("%s - Popping smoke at your %s o\'clock. Distance %s", _unitName, _clockDir, _distance) - self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true, true) - local _coord = _closest.pilot:GetCoordinate() - local color = self.smokecolor - _coord:Smoke(color) - else - local _distance = 0 - if _SETTINGS:IsImperial() then - _distance = string.format("%.1fnm",UTILS.MetersToNM(smokedist)) - else - _distance = string.format("%.1fkm",smokedist/1000) - end - self:_DisplayMessageToSAR(_heli, string.format("No Pilots within %s",_distance), self.messageTime, false, false, true) - end - return self -end - ---- (Internal) Determine distance to closest MASH. --- @param #CSAR self --- @param Wrapper.Unit#UNIT _heli Helicopter #UNIT --- @retunr -function CSAR:_GetClosestMASH(_heli) - self:T(self.lid .. " _GetClosestMASH") - local _mashset = self.mash -- Core.Set#SET_GROUP - local _mashes = _mashset:GetSetObjects() -- #table - local _shortestDistance = -1 - local _distance = 0 - local _helicoord = _heli:GetCoordinate() - - local function GetCloseAirbase(coordinate,Coalition,Category) - - local a=coordinate:GetVec3() - local distmin=math.huge - local airbase=nil - for DCSairbaseID, DCSairbase in pairs(world.getAirbases(Coalition)) do - local b=DCSairbase:getPoint() - - local c=UTILS.VecSubstract(a,b) - local dist=UTILS.VecNorm(c) - - if dist 0 then - local PilotTable = self.downedPilots - for _,_pilot in pairs (PilotTable) do - self:T({_pilot}) - local pilot = _pilot -- #CSAR.DownedPilot - local group = pilot.group - local frequency = pilot.frequency or 0 -- thanks to @Thrud - if group and group:IsAlive() and frequency > 0 then - self:_AddBeaconToGroup(group,frequency) - end - end - end - return self -end - ---- (Internal) Helper function to count active downed pilots. --- @param #CSAR self --- @return #number Number of pilots in the field. -function CSAR:_CountActiveDownedPilots() - self:T(self.lid .. " _CountActiveDownedPilots") - local PilotsInFieldN = 0 - for _, _unitName in pairs(self.downedPilots) do - self:T({_unitName.desc}) - if _unitName.alive == true then - PilotsInFieldN = PilotsInFieldN + 1 - end - end - return PilotsInFieldN -end - ---- (Internal) Helper to decide if we're over max limit. --- @param #CSAR self --- @return #boolean True or false. -function CSAR:_ReachedPilotLimit() - self:T(self.lid .. " _ReachedPilotLimit") - local limit = self.maxdownedpilots - local islimited = self.limitmaxdownedpilots - local count = self:_CountActiveDownedPilots() - if islimited and (count >= limit) then - return true - else - return false - end -end - - ------------------------------ - --- FSM internal Functions --- - ------------------------------ - ---- (Internal) Function called after Start() event. --- @param #CSAR self. --- @param #string From From state. --- @param #string Event Event triggered. --- @param #string To To state. -function CSAR:onafterStart(From, Event, To) - self:T({From, Event, To}) - self:I(self.lid .. "Started.") - -- event handler - self:HandleEvent(EVENTS.Takeoff, self._EventHandler) - self:HandleEvent(EVENTS.Land, self._EventHandler) - self:HandleEvent(EVENTS.Ejection, self._EventHandler) - self:HandleEvent(EVENTS.LandingAfterEjection, self._EventHandler) --shagrat - self:HandleEvent(EVENTS.PlayerEnterAircraft, self._EventHandler) - self:HandleEvent(EVENTS.PlayerEnterUnit, self._EventHandler) - self:HandleEvent(EVENTS.PilotDead, self._EventHandler) - if self.useprefix then - local prefixes = self.csarPrefix or {} - self.allheligroupset = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(prefixes):FilterCategoryHelicopter():FilterStart() - else - self.allheligroupset = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterCategoryHelicopter():FilterStart() - end - self.mash = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(self.mashprefix):FilterStart() -- currently only GROUP objects, maybe support STATICs also? - if self.wetfeettemplate then - self.usewetfeet = true - end - self:__Status(-10) - return self -end - ---- (Internal) Function called before Status() event. --- @param #CSAR self -function CSAR:_CheckDownedPilotTable() - local pilots = self.downedPilots - local npilots = {} - - for _ind,_entry in pairs(pilots) do - local _group = _entry.group - if _group:IsAlive() then - npilots[_ind] = _entry - else - if _entry.alive then - self:__KIA(1,_entry.desc) - end - end - end - self.downedPilots = npilots - return self -end - ---- (Internal) Function called before Status() event. --- @param #CSAR self. --- @param #string From From state. --- @param #string Event Event triggered. --- @param #string To To state. -function CSAR:onbeforeStatus(From, Event, To) - self:T({From, Event, To}) - -- housekeeping - self:_AddMedevacMenuItem() - - if not self.BeaconTimer or (self.BeaconTimer and not self.BeaconTimer:IsRunning()) then - self.BeaconTimer = TIMER:New(self._RefreshRadioBeacons,self) - self.BeaconTimer:Start(2,self.beaconRefresher) - end - - self:_CheckDownedPilotTable() - for _,_sar in pairs (self.csarUnits) do - local PilotTable = self.downedPilots - for _,_entry in pairs (PilotTable) do - if _entry.alive then - local entry = _entry -- #CSAR.DownedPilot - local name = entry.name - local timestamp = entry.timestamp or 0 - local now = timer.getAbsTime() - if now - timestamp > 17 then -- only check if we\'re not in approach mode, which is iterations of 5 and 10. - self:_CheckWoundedGroupStatus(_sar,name) - end - end - end - end - return self -end - ---- (Internal) Function called after Status() event. --- @param #CSAR self. --- @param #string From From state. --- @param #string Event Event triggered. --- @param #string To To state. -function CSAR:onafterStatus(From, Event, To) - self:T({From, Event, To}) - -- collect some stats - local NumberOfSARPilots = 0 - for _, _unitName in pairs(self.csarUnits) do - NumberOfSARPilots = NumberOfSARPilots + 1 - end - - local PilotsInFieldN = self:_CountActiveDownedPilots() - - local PilotsBoarded = 0 - for _, _unitName in pairs(self.inTransitGroups) do - for _,_units in pairs(_unitName) do - PilotsBoarded = PilotsBoarded + 1 - end - end - - if self.verbose > 0 then - local text = string.format("%s Active SAR: %d | Downed Pilots in field: %d (max %d) | Pilots boarded: %d | Landings: %d | Pilots rescued: %d", - self.lid,NumberOfSARPilots,PilotsInFieldN,self.maxdownedpilots,PilotsBoarded,self.rescues,self.rescuedpilots) - self:T(text) - if self.verbose < 2 then - self:I(text) - elseif self.verbose > 1 then - self:I(text) - local m = MESSAGE:New(text,"10","Status",true):ToCoalition(self.coalition) - end - end - self:__Status(-20) - return self -end - ---- (Internal) Function called after Stop() event. --- @param #CSAR self. --- @param #string From From state. --- @param #string Event Event triggered. --- @param #string To To state. -function CSAR:onafterStop(From, Event, To) - self:T({From, Event, To}) - -- event handler - self:UnHandleEvent(EVENTS.Takeoff) - self:UnHandleEvent(EVENTS.Land) - self:UnHandleEvent(EVENTS.Ejection) - self:UnHandleEvent(EVENTS.LandingAfterEjection) -- shagrat - self:UnHandleEvent(EVENTS.PlayerEnterUnit) - self:UnHandleEvent(EVENTS.PlayerEnterAircraft) - self:UnHandleEvent(EVENTS.PilotDead) - self:T(self.lid .. "Stopped.") - return self -end - ---- (Internal) Function called before Approach() event. --- @param #CSAR self. --- @param #string From From state. --- @param #string Event Event triggered. --- @param #string To To state. --- @param #string Heliname Name of the helicopter group. --- @param #string Woundedgroupname Name of the downed pilot\'s group. -function CSAR:onbeforeApproach(From, Event, To, Heliname, Woundedgroupname) - self:T({From, Event, To, Heliname, Woundedgroupname}) - self:_CheckWoundedGroupStatus(Heliname,Woundedgroupname) - return self -end - ---- (Internal) Function called before Boarded() event. --- @param #CSAR self. --- @param #string From From state. --- @param #string Event Event triggered. --- @param #string To To state. --- @param #string Heliname Name of the helicopter group. --- @param #string Woundedgroupname Name of the downed pilot\'s group. -function CSAR:onbeforeBoarded(From, Event, To, Heliname, Woundedgroupname) - self:T({From, Event, To, Heliname, Woundedgroupname}) - self:_ScheduledSARFlight(Heliname,Woundedgroupname) - return self -end - ---- (Internal) Function called before Returning() event. --- @param #CSAR self. --- @param #string From From state. --- @param #string Event Event triggered. --- @param #string To To state. --- @param #string Heliname Name of the helicopter group. --- @param #string Woundedgroupname Name of the downed pilot\'s group. --- @param #boolean IsAirport True if heli has landed on an AFB (from event land). -function CSAR:onbeforeReturning(From, Event, To, Heliname, Woundedgroupname, IsAirPort) - self:T({From, Event, To, Heliname, Woundedgroupname}) - self:_ScheduledSARFlight(Heliname,Woundedgroupname, IsAirPort) - return self -end - ---- (Internal) Function called before Rescued() event. --- @param #CSAR self. --- @param #string From From state. --- @param #string Event Event triggered. --- @param #string To To state. --- @param Wrapper.Unit#UNIT HeliUnit Unit of the helicopter. --- @param #string HeliName Name of the helicopter group. --- @param #number PilotsSaved Number of the saved pilots on board when landing. -function CSAR:onbeforeRescued(From, Event, To, HeliUnit, HeliName, PilotsSaved) - self:T({From, Event, To, HeliName, HeliUnit}) - self.rescues = self.rescues + 1 - self.rescuedpilots = self.rescuedpilots + PilotsSaved - return self -end - ---- (Internal) Function called before PilotDown() event. --- @param #CSAR self. --- @param #string From From state. --- @param #string Event Event triggered. --- @param #string To To state. --- @param Wrapper.Group#GROUP Group Group object of the downed pilot. --- @param #number Frequency Beacon frequency in kHz. --- @param #string Leadername Name of the #UNIT of the downed pilot. --- @param #string CoordinatesText String of the position of the pilot. Format determined by self.coordtype. -function CSAR:onbeforePilotDown(From, Event, To, Group, Frequency, Leadername, CoordinatesText) - self:T({From, Event, To, Group, Frequency, Leadername, CoordinatesText}) - return self -end --------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- End Ops.CSAR --------------------------------------------------------------------------------------------------------------------------------------------------------------------- - --- --- ### [CSAR - Combat Search & Rescue](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20CSAR) --- --- === --- --- **Main Features:** --- --- * MOOSE-based Helicopter CSAR Operations for Players. --- --- === --- --- ### Author: **Applevangelist** (Moose Version), ***Ciribob*** (original), Thanks to: Shadowze, Cammel (testing) --- @module Ops.CSAR --- @image OPS_CSAR.jpg - --- Date: Feb 2022 - -------------------------------------------------------------------------- ---- **CSAR** class, extends Core.Base#BASE, Core.Fsm#FSM --- @type CSAR --- @field #string ClassName Name of the class. --- @field #number verbose Verbosity level. --- @field #string lid Class id string for output to DCS log file. --- @field #number coalition Coalition side number, e.g. `coalition.side.RED`. --- @extends Core.Fsm#FSM - ---- *Combat search and rescue (CSAR) are search and rescue operations that are carried out during war that are within or near combat zones.* (Wikipedia) --- --- === --- --- ![Banner Image](OPS_CSAR.jpg) --- --- # CSAR Concept --- --- * MOOSE-based Helicopter CSAR Operations for Players. --- * Object oriented refactoring of Ciribob\'s fantastic CSAR script. --- * No need for extra MIST loading. --- * Additional events to tailor your mission. --- * Optional SpawnCASEVAC to create casualties without beacon (e.g. handling dead ground vehicles and create CASVAC requests). --- --- ## 0. Prerequisites --- --- You need to load an .ogg soundfile for the pilot\'s beacons into the mission, e.g. "beacon.ogg", use a once trigger, "sound to country" for that. --- Create a late-activated single infantry unit as template in the mission editor and name it e.g. "Downed Pilot". --- --- ## 1. Basic Setup --- --- A basic setup example is the following: --- --- -- Instantiate and start a CSAR for the blue side, with template "Downed Pilot" and alias "Luftrettung" --- local my_csar = CSAR:New(coalition.side.BLUE,"Downed Pilot","Luftrettung") --- -- options --- my_csar.immortalcrew = true -- downed pilot spawn is immortal --- my_csar.invisiblecrew = false -- downed pilot spawn is visible --- -- start the FSM --- my_csar:__Start(5) --- --- ## 2. Options --- --- The following options are available (with their defaults). Only set the ones you want changed: --- --- self.allowDownedPilotCAcontrol = false -- Set to false if you don\'t want to allow control by Combined Arms. --- self.allowFARPRescue = true -- allows pilots to be rescued by landing at a FARP or Airbase. Else MASH only! --- self.FARPRescueDistance = 1000 -- you need to be this close to a FARP or Airport for the pilot to be rescued. --- self.autosmoke = false -- automatically smoke a downed pilot\'s location when a heli is near. --- self.autosmokedistance = 1000 -- distance for autosmoke --- self.coordtype = 1 -- Use Lat/Long DDM (0), Lat/Long DMS (1), MGRS (2), Bullseye imperial (3) or Bullseye metric (4) for coordinates. --- self.csarOncrash = false -- (WIP) If set to true, will generate a downed pilot when a plane crashes as well. --- self.enableForAI = false -- set to false to disable AI pilots from being rescued. --- self.pilotRuntoExtractPoint = true -- Downed pilot will run to the rescue helicopter up to self.extractDistance in meters. --- self.extractDistance = 500 -- Distance the downed pilot will start to run to the rescue helicopter. --- self.immortalcrew = true -- Set to true to make wounded crew immortal. --- self.invisiblecrew = false -- Set to true to make wounded crew insvisible. --- self.loadDistance = 75 -- configure distance for pilots to get into helicopter in meters. --- self.mashprefix = {"MASH"} -- prefixes of #GROUP objects used as MASHes. --- self.max_units = 6 -- max number of pilots that can be carried if #CSAR.AircraftType is undefined. --- self.messageTime = 15 -- Time to show messages for in seconds. Doubled for long messages. --- self.radioSound = "beacon.ogg" -- the name of the sound file to use for the pilots\' radio beacons. --- self.smokecolor = 4 -- Color of smokemarker, 0 is green, 1 is red, 2 is white, 3 is orange and 4 is blue. --- self.useprefix = true -- Requires CSAR helicopter #GROUP names to have the prefix(es) defined below. --- self.csarPrefix = { "helicargo", "MEDEVAC"} -- #GROUP name prefixes used for useprefix=true - DO NOT use # in helicopter names in the Mission Editor! --- self.verbose = 0 -- set to > 1 for stats output for debugging. --- -- (added 0.1.4) limit amount of downed pilots spawned by **ejection** events --- self.limitmaxdownedpilots = true --- self.maxdownedpilots = 10 --- -- (added 0.1.8) - allow to set far/near distance for approach and optionally pilot must open doors --- self.approachdist_far = 5000 -- switch do 10 sec interval approach mode, meters --- self.approachdist_near = 3000 -- switch to 5 sec interval approach mode, meters --- self.pilotmustopendoors = false -- switch to true to enable check of open doors --- -- (added 0.1.9) --- self.suppressmessages = false -- switch off all messaging if you want to do your own --- -- (added 0.1.11) --- self.rescuehoverheight = 20 -- max height for a hovering rescue in meters --- self.rescuehoverdistance = 10 -- max distance for a hovering rescue in meters --- -- (added 0.1.12) --- -- Country codes for spawned pilots --- self.countryblue= country.id.USA --- self.countryred = country.id.RUSSIA --- self.countryneutral = country.id.UN_PEACEKEEPERS --- --- ## 2.1 Experimental Features --- --- WARNING - Here\'ll be dragons! --- DANGER - For this to work you need to de-sanitize your mission environment (all three entries) in \Scripts\MissionScripting.lua --- Needs SRS => 1.9.6 to work (works on the **server** side of SRS) --- self.useSRS = false -- Set true to use FF\'s SRS integration --- self.SRSPath = "E:\\Progra~1\\DCS-SimpleRadio-Standalone\\" -- adjust your own path in your SRS installation -- server(!) --- self.SRSchannel = 300 -- radio channel --- self.SRSModulation = radio.modulation.AM -- modulation --- -- --- self.csarUsePara = false -- If set to true, will use the LandingAfterEjection Event instead of Ejection --shagrat --- self.wetfeettemplate = "man in floating thingy" -- if you use a mod to have a pilot in a rescue float, put the template name in here for wet feet spawns. Note: in conjunction with csarUsePara this might create dual ejected pilots in edge cases. --- --- ## 3. Results --- --- Number of successful landings with save pilots and aggregated number of saved pilots is stored in these variables in the object: --- --- self.rescues -- number of successful landings *with* saved pilots --- self.rescuedpilots -- aggregated number of pilots rescued from the field (of *all* players) --- --- ## 4. Events --- --- The class comes with a number of FSM-based events that missions designers can use to shape their mission. --- These are: --- --- ### 4.1. PilotDown. --- --- The event is triggered when a new downed pilot is detected. Use e.g. `function my_csar:OnAfterPilotDown(...)` to link into this event: --- --- function my_csar:OnAfterPilotDown(from, event, to, spawnedgroup, frequency, groupname, coordinates_text) --- ... your code here ... --- end --- --- ### 4.2. Approach. --- --- A CSAR helicpoter is closing in on a downed pilot. Use e.g. `function my_csar:OnAfterApproach(...)` to link into this event: --- --- function my_csar:OnAfterApproach(from, event, to, heliname, groupname) --- ... your code here ... --- end --- --- ### 4.3. Boarded. --- --- The pilot has been boarded to the helicopter. Use e.g. `function my_csar:OnAfterBoarded(...)` to link into this event: --- --- function my_csar:OnAfterBoarded(from, event, to, heliname, groupname) --- ... your code here ... --- end --- --- ### 4.4. Returning. --- --- The CSAR helicopter is ready to return to an Airbase, FARP or MASH. Use e.g. `function my_csar:OnAfterReturning(...)` to link into this event: --- --- function my_csar:OnAfterReturning(from, event, to, heliname, groupname) --- ... your code here ... --- end --- --- ### 4.5. Rescued. --- --- The CSAR helicopter has landed close to an Airbase/MASH/FARP and the pilots are safe. Use e.g. `function my_csar:OnAfterRescued(...)` to link into this event: --- --- function my_csar:OnAfterRescued(from, event, to, heliunit, heliname, pilotssaved) --- ... your code here ... --- end --- --- ## 5. Spawn downed pilots at location to be picked up. --- --- If missions designers want to spawn downed pilots into the field, e.g. at mission begin to give the helicopter guys works, they can do this like so: --- --- -- Create downed "Pilot Wagner" in #ZONE "CSAR_Start_1" at a random point for the blue coalition --- my_csar:SpawnCSARAtZone( "CSAR_Start_1", coalition.side.BLUE, "Pilot Wagner", true ) --- --- --Create a casualty and CASEVAC request from a "Point" (VEC2) for the blue coalition --shagrat --- my_csar:SpawnCASEVAC(Point, coalition.side.BLUE) --- --- @field #CSAR -CSAR = { - ClassName = "CSAR", - verbose = 0, - lid = "", - coalition = 1, - coalitiontxt = "blue", - FreeVHFFrequencies = {}, - UsedVHFFrequencies = {}, - takenOff = {}, - csarUnits = {}, -- table of unit names - downedPilots = {}, - woundedGroups = {}, - landedStatus = {}, - addedTo = {}, - woundedGroups = {}, -- contains the new group of units - inTransitGroups = {}, -- contain a table for each SAR with all units he has with the original names - smokeMarkers = {}, -- tracks smoke markers for groups - heliVisibleMessage = {}, -- tracks if the first message has been sent of the heli being visible - heliCloseMessage = {}, -- tracks heli close message ie heli < 500m distance - max_units = 6, --number of pilots that can be carried - hoverStatus = {}, -- tracks status of a helis hover above a downed pilot - pilotDisabled = {}, -- tracks what aircraft a pilot is disabled for - pilotLives = {}, -- tracks how many lives a pilot has - useprefix = true, -- Use the Prefixed defined below, Requires Unit have the Prefix defined below - csarPrefix = {}, - template = nil, - mash = {}, - smokecolor = 4, - rescues = 0, - rescuedpilots = 0, - limitmaxdownedpilots = true, - maxdownedpilots = 10, -} - ---- Downed pilots info. --- @type CSAR.DownedPilot --- @field #number index Pilot index. --- @field #string name Name of the spawned group. --- @field #number side Coalition. --- @field #string originalUnit Name of the original unit. --- @field #string desc Description. --- @field #string typename Typename of Unit. --- @field #number frequency Frequency of the NDB. --- @field #string player Player name if applicable. --- @field Wrapper.Group#GROUP group Spawned group object. --- @field #number timestamp Timestamp for approach process. --- @field #boolean alive Group is alive or dead/rescued. --- @field #boolean wetfeet Group is spawned over (deep) water. - ---- All slot / Limit settings --- @type CSAR.AircraftType --- @field #string typename Unit type name. -CSAR.AircraftType = {} -- Type and limit -CSAR.AircraftType["SA342Mistral"] = 2 -CSAR.AircraftType["SA342Minigun"] = 2 -CSAR.AircraftType["SA342L"] = 4 -CSAR.AircraftType["SA342M"] = 4 -CSAR.AircraftType["UH-1H"] = 8 -CSAR.AircraftType["Mi-8MTV2"] = 12 -CSAR.AircraftType["Mi-8MT"] = 12 -CSAR.AircraftType["Mi-24P"] = 8 -CSAR.AircraftType["Mi-24V"] = 8 -CSAR.AircraftType["Bell-47"] = 2 -CSAR.AircraftType["UH-60L"] = 10 - --- CSAR class version. -- @field #string version CSAR.version="1.0.4a" From e41ba1be45d07b6fe3b05a8a8babbc761144c2a1 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 2 Mar 2022 09:34:18 +0100 Subject: [PATCH 06/26] Troop Transport - Auftrag TROOPTRANSPORT: troops will wait in a zone and not at the exact position where the carrier lands. --- Moose Development/Moose/Ops/Auftrag.lua | 14 +++++++++----- Moose Development/Moose/Ops/OpsGroup.lua | 13 +++++++++++-- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/Moose Development/Moose/Ops/Auftrag.lua b/Moose Development/Moose/Ops/Auftrag.lua index 87b24efaf..08d316299 100644 --- a/Moose Development/Moose/Ops/Auftrag.lua +++ b/Moose Development/Moose/Ops/Auftrag.lua @@ -94,6 +94,7 @@ -- @field Core.Set#SET_GROUP transportGroupSet Groups to be transported. -- @field Core.Point#COORDINATE transportPickup Coordinate where to pickup the cargo. -- @field Core.Point#COORDINATE transportDropoff Coordinate where to drop off the cargo. +-- @field #number transportPickupRadius Radius in meters for pickup zone. Default 500 m. -- -- @field Ops.OpsTransport#OPSTRANSPORT opstransport OPS transport assignment. -- @field #number NcarriersMin Min number of required carrier assets. @@ -557,7 +558,7 @@ AUFTRAG.Category={ --- AUFTRAG class version. -- @field #string version -AUFTRAG.version="0.8.1" +AUFTRAG.version="0.8.2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1492,8 +1493,9 @@ end -- @param Core.Set#SET_GROUP TransportGroupSet The set group(s) to be transported. -- @param Core.Point#COORDINATE DropoffCoordinate Coordinate where the helo will land drop off the the troops. -- @param Core.Point#COORDINATE PickupCoordinate Coordinate where the helo will land to pick up the the cargo. Default is the fist transport group. +-- @param #number PickupRadius Radius around the pickup coordinate in meters. Default 500 m. -- @return #AUFTRAG self -function AUFTRAG:NewTROOPTRANSPORT(TransportGroupSet, DropoffCoordinate, PickupCoordinate) +function AUFTRAG:NewTROOPTRANSPORT(TransportGroupSet, DropoffCoordinate, PickupCoordinate, PickupRadius) local mission=AUFTRAG:New(AUFTRAG.Type.TROOPTRANSPORT) @@ -1509,14 +1511,16 @@ function AUFTRAG:NewTROOPTRANSPORT(TransportGroupSet, DropoffCoordinate, PickupC mission:_TargetFromObject(mission.transportGroupSet) - mission.transportPickup=PickupCoordinate or mission:GetTargetCoordinate() + mission.transportPickup=PickupCoordinate or mission:GetTargetCoordinate() mission.transportDropoff=DropoffCoordinate + + mission.transportPickupRadius=PickupRadius or 500 mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.TROOPTRANSPORT) -- Debug. - mission.transportPickup:MarkToAll("Pickup") - mission.transportDropoff:MarkToAll("Drop off") + --mission.transportPickup:MarkToAll("Pickup Transport") + --mission.transportDropoff:MarkToAll("Drop off") -- TODO: what's the best ROE here? mission.optionROE=ENUMS.ROE.ReturnFire diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index 6f582473a..7ab4996bf 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -466,7 +466,7 @@ OPSGROUP.CargoStatus={ --- OpsGroup version. -- @field #string version -OPSGROUP.version="0.7.5" +OPSGROUP.version="0.7.6" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -4580,13 +4580,22 @@ function OPSGROUP:RouteToMission(mission, delay) -- Refresh DCS task with the known controllable. mission.DCStask=mission:GetDCSMissionTask(self.group) + + -- Create a pickup zone around the pickup coordinate. The troops will go to a random point inside the zone. + -- This is necessary so the helos do not try to land at the exact same location where the troops wait. + local pradius=500 + local pickupZone=ZONE_RADIUS:New("Pickup Zone", mission.transportPickup:GetVec2(), pradius) -- Add task to embark for the troops. for _,_group in pairs(mission.transportGroupSet.Set) do local group=_group --Wrapper.Group#GROUP if group and group:IsAlive() then - local DCSTask=group:TaskEmbarkToTransport(mission.transportPickup, 500) + -- Get random coordinate inside the zone. + local pcoord=pickupZone:GetRandomCoordinate(20, pradius, {land.SurfaceType.LAND, land.SurfaceType.ROAD}) + + -- Let the troops embark the transport. + local DCSTask=group:TaskEmbarkToTransport(pcoord, pradius) group:SetTask(DCSTask, 5) end From 53c9ca3b3a875deb84c8ff84950747003f7cd5ef Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Thu, 3 Mar 2022 11:01:36 +0100 Subject: [PATCH 07/26] SEAD/MANTIS - added Silkworm ASM --- Moose Development/Moose/Functional/Mantis.lua | 2 ++ Moose Development/Moose/Functional/Sead.lua | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Moose Development/Moose/Functional/Mantis.lua b/Moose Development/Moose/Functional/Mantis.lua index 9162df5bb..e745a77ed 100644 --- a/Moose Development/Moose/Functional/Mantis.lua +++ b/Moose Development/Moose/Functional/Mantis.lua @@ -98,6 +98,7 @@ -- * Patriot -- * Rapier -- * Roland +-- * Silkworm (though strictly speaking this is a surface to ship missile) -- * SA-2, SA-3, SA-5, SA-6, SA-7, SA-8, SA-9, SA-10, SA-11, SA-13, SA-15, SA-19 -- * and from HDS (see note below): SA-2, SA-3, SA-10B, SA-10C, SA-12, SA-17, SA-20A, SA-20B, SA-23, HQ-2 -- @@ -356,6 +357,7 @@ MANTIS.SamData = { ["Avenger"] = { Range=4, Blindspot=0, Height=3, Type="Short", Radar="Avenger" }, ["Chaparrel"] = { Range=8, Blindspot=0, Height=3, Type="Short", Radar="Chaparral" }, ["Linebacker"] = { Range=4, Blindspot=0, Height=3, Type="Short", Radar="Linebacker" }, + ["Silkworm"] = { Range=90, Blindspot=1, Height=0.2, Type="Long", Radar="Silkworm" }, -- units from HDS Mod, multi launcher options is tricky ["SA-10B"] = { Range=75, Blindspot=0, Height=18, Type="Medium" , Radar="SA-10B"}, ["SA-17"] = { Range=50, Blindspot=3, Height=30, Type="Medium", Radar="SA-17" }, diff --git a/Moose Development/Moose/Functional/Sead.lua b/Moose Development/Moose/Functional/Sead.lua index 2982f478e..2af6c64f3 100644 --- a/Moose Development/Moose/Functional/Sead.lua +++ b/Moose Development/Moose/Functional/Sead.lua @@ -79,6 +79,7 @@ SEAD = { ["Kh25"] = "Kh25", ["BGM_109"] = "BGM_109", ["AGM_154"] = "AGM_154", + ["HY-2"] = "HY-2", } --- Missile enumerators - from DCS ME and Wikipedia @@ -98,6 +99,7 @@ SEAD = { ["Kh25"] = {25, 0.8}, ["BGM_109"] = {460, 0.705}, --in-game ~465kn ["AGM_154"] = {130, 0.61}, + ["HY-2"] = {90,1}, } --- Creates the main object which is handling defensive actions for SA sites or moving SA vehicles. From b0c2bad1d8ce2668208c9ec58cfeb0c93ad7cbde Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Thu, 3 Mar 2022 11:02:09 +0100 Subject: [PATCH 08/26] CTLD - small fix for finding crates when using engineers --- Moose Development/Moose/Ops/CTLD.lua | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index 251eb952f..f04515322 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -1021,7 +1021,7 @@ CTLD.UnitTypes = { --- CTLD class version. -- @field #string version -CTLD.version="1.0.9" +CTLD.version="1.0.10" --- Instantiate a new CTLD. -- @param #CTLD self @@ -2095,11 +2095,18 @@ function CTLD:_FindCratesNearby( _group, _unit, _dist, _ignoreweight) -- cycle local index = 0 local found = {} - local loadedmass = self:_GetUnitCargoMass(_unit) - local unittype = _unit:GetTypeName() - local capabilities = self:_GetUnitCapabilities(_unit) -- #CTLD.UnitCapabilities - local maxmass = capabilities.cargoweightlimit - local maxloadable = maxmass - loadedmass + local loadedmass = 0 + local unittype = "none" + local capabilities = {} + local maxmass = 2000 + local maxloadable = 2000 + if not _ignoreweight then + loadedmass = self:_GetUnitCargoMass(_unit) + unittype = _unit:GetTypeName() + capabilities = self:_GetUnitCapabilities(_unit) -- #CTLD.UnitCapabilities + maxmass = capabilities.cargoweightlimit + maxloadable = maxmass - loadedmass + end self:T(self.lid .. " Max loadable mass: " .. maxloadable) for _,_cargoobject in pairs (existingcrates) do local cargo = _cargoobject -- #CTLD_CARGO From f3d0d55a2f1cb58075e4c4db691fb9ba6424d1e7 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Thu, 3 Mar 2022 12:34:55 +0100 Subject: [PATCH 09/26] CTLD - small extra nil check in _GetUnitCargoMass(Unit --- Moose Development/Moose/Ops/CTLD.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index f04515322..c04e3e26f 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -2263,6 +2263,7 @@ end -- @return #number mass in kgs function CTLD:_GetUnitCargoMass(Unit) self:T(self.lid .. " _GetUnitCargoMass") + if not Unit then return 0 end local unitname = Unit:GetName() local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo local loadedmass = 0 -- #number From 3557706e3a13bacd07e18e3866f6a8914041b5c5 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 5 Mar 2022 14:43:34 +0100 Subject: [PATCH 10/26] OPS - Added new AUFTRAG type CASENHANCED - Improved capturing of OPSZONES by CHIEF --- .../Moose/Functional/Warehouse.lua | 238 +++++++++--------- Moose Development/Moose/Ops/Auftrag.lua | 66 ++++- Moose Development/Moose/Ops/Chief.lua | 45 +++- Moose Development/Moose/Ops/Cohort.lua | 40 +-- Moose Development/Moose/Ops/Legion.lua | 55 +++- Moose Development/Moose/Ops/OpsGroup.lua | 54 +++- Moose Development/Moose/Ops/OpsTransport.lua | 41 ++- 7 files changed, 364 insertions(+), 175 deletions(-) diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index f38c71d8e..e7e2a0a5c 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -4039,7 +4039,7 @@ function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribu self:_DebugMessage(string.format("Removing group %s", group:GetName()), 5) local opsgroup=_DATABASE:GetOpsGroup(group:GetName()) - if opsgroup then + if opsgroup then opsgroup:Despawn(0, true) opsgroup:__Stop(-0.01) else @@ -5452,6 +5452,55 @@ function WAREHOUSE:onafterAssetDead(From, Event, To, asset, request) local text=string.format("Asset %s from request id=%d is dead!", asset.templatename, request.uid) self:T(self.lid..text) self:_DebugMessage(text) + + -- Here I need to get rid of the #CARGO at the end to obtain the original name again! + local groupname=asset.spawngroupname --self:_GetNameWithOut(group) + + -- Dont trigger a Remove event for the group sets. + local NoTriggerEvent=true + + if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then + + --- + -- Easy case: Group can simply be removed from the cargogroupset. + --- + + -- Remove dead group from cargo group set. + request.cargogroupset:Remove(groupname, NoTriggerEvent) + self:T(self.lid..string.format("Removed selfpropelled cargo %s: ncargo=%d.", groupname, request.cargogroupset:Count())) + + else + + --- + -- Complicated case: Dead unit could be: + -- 1.) A Cargo unit (e.g. waiting to be picked up). + -- 2.) A Transport unit which itself holds cargo groups. + --- + + -- Check if this a cargo or transport group. + local istransport=not asset.iscargo --self:_GroupIsTransport(group, request) + + if istransport==true then + + -- Whole carrier group is dead. Remove it from the carrier group set. + request.transportgroupset:Remove(groupname, NoTriggerEvent) + self:T(self.lid..string.format("Removed transport %s: ntransport=%d", groupname, request.transportgroupset:Count())) + + elseif istransport==false then + + -- This must have been an alive cargo group that was killed outside the carrier, e.g. waiting to be transported or waiting to be put back. + -- Remove dead group from cargo group set. + request.cargogroupset:Remove(groupname, NoTriggerEvent) + self:T(self.lid..string.format("Removed transported cargo %s outside carrier: ncargo=%d", groupname, request.cargogroupset:Count())) + -- This as well? + --request.transportcargoset:RemoveCargosByName(RemoveCargoNames) + + else + --self:E(self.lid..string.format("ERROR: Group %s is neither cargo nor transport!", group:GetName())) + end + end + + end @@ -6556,7 +6605,8 @@ function WAREHOUSE:_OnEventCrashOrDead(EventData) end end - --self:I(self.lid..string.format("Warehouse %s captured event dead or crash or unit %s.", self.alias, tostring(EventData.IniUnitName))) + -- Debug info. + self:T2(self.lid..string.format("Warehouse %s captured event dead or crash or unit %s", self.alias, tostring(EventData.IniUnitName))) -- Check if an asset unit was destroyed. if EventData.IniGroup then @@ -6571,7 +6621,7 @@ function WAREHOUSE:_OnEventCrashOrDead(EventData) if wid==self.uid then -- Debug message. - self:T(self.lid..string.format("Warehouse %s captured event dead or crash of its asset unit %s.", self.alias, EventData.IniUnitName)) + self:T(self.lid..string.format("Warehouse %s captured event dead or crash of its asset unit %s", self.alias, EventData.IniUnitName)) -- Loop over all pending requests and get the one belonging to this unit. for _,request in pairs(self.pending) do @@ -6581,7 +6631,7 @@ function WAREHOUSE:_OnEventCrashOrDead(EventData) if request.uid==rid then -- Update cargo and transport group sets of this request. We need to know if this job is finished. - self:_UnitDead(EventData.IniUnit, request) + self:_UnitDead(EventData.IniUnit, EventData.IniGroup, request) end end @@ -6594,38 +6644,51 @@ end -- This is important in order to determine if a job is done and can be removed from the (pending) queue. -- @param #WAREHOUSE self -- @param Wrapper.Unit#UNIT deadunit Unit that died. +-- @param Wrapper.Group#GROUP deadgroup Group of unit that died. -- @param #WAREHOUSE.Pendingitem request Request that needs to be updated. -function WAREHOUSE:_UnitDead(deadunit, request) +function WAREHOUSE:_UnitDead(deadunit, deadgroup, request) + --env.info("FF unit dead "..deadunit:GetName()) - -- Flare unit. - if self.Debug then - deadunit:FlareRed() - end - - -- Group the dead unit belongs to. - local group=deadunit:GetGroup() - - -- Number of alive units in group. - local nalive=group:CountAliveUnits() - - -- Whole group is dead? - local groupdead=true - if nalive>0 then - groupdead=false + -- Find asset. + local asset=self:FindAssetInDB(deadgroup) + + -- Find opsgroup. + local opsgroup=_DATABASE:FindOpsGroup(deadgroup) + + local groupdead=false + if opsgroup then + + if opsgroup:IsDead() then + groupdead=true + end + + else + + -- Number of alive units in group. + local nalive=deadgroup:CountAliveUnits() + + -- Whole group is dead? + if nalive>0 then + groupdead=false + else + groupdead=true + end + end + -- Here I need to get rid of the #CARGO at the end to obtain the original name again! local unitname=self:_GetNameWithOut(deadunit) - local groupname=self:_GetNameWithOut(group) + local groupname=self:_GetNameWithOut(deadgroup) -- Group is dead! if groupdead then - self:T(self.lid..string.format("Group %s (transport=%s) is dead!", groupname, tostring(self:_GroupIsTransport(group,request)))) + -- Debug output. + self:T(self.lid..string.format("Group %s (transport=%s) is dead!", groupname, tostring(self:_GroupIsTransport(deadgroup,request)))) if self.Debug then - group:SmokeWhite() + deadgroup:SmokeWhite() end - -- Trigger AssetDead event. - local asset=self:FindAssetInDB(group) + -- Trigger AssetDead event. self:AssetDead(asset, request) end @@ -6633,19 +6696,7 @@ function WAREHOUSE:_UnitDead(deadunit, request) -- Dont trigger a Remove event for the group sets. local NoTriggerEvent=true - if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then - - --- - -- Easy case: Group can simply be removed from the cargogroupset. - --- - - -- Remove dead group from cargo group set. - if groupdead==true then - request.cargogroupset:Remove(groupname, NoTriggerEvent) - self:T(self.lid..string.format("Removed selfpropelled cargo %s: ncargo=%d.", groupname, request.cargogroupset:Count())) - end - - else + if not request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then --- -- Complicated case: Dead unit could be: @@ -6653,10 +6704,7 @@ function WAREHOUSE:_UnitDead(deadunit, request) -- 2.) A Transport unit which itself holds cargo groups. --- - -- Check if this a cargo or transport group. - local istransport=self:_GroupIsTransport(group,request) - - if istransport==true then + if not asset.iscargo then -- Get the carrier unit table holding the cargo groups inside this carrier. local cargogroupnames=request.carriercargo[unitname] @@ -6671,25 +6719,8 @@ function WAREHOUSE:_UnitDead(deadunit, request) end - -- Whole carrier group is dead. Remove it from the carrier group set. - if groupdead then - request.transportgroupset:Remove(groupname, NoTriggerEvent) - self:T(self.lid..string.format("Removed transport %s: ntransport=%d", groupname, request.transportgroupset:Count())) - end - - elseif istransport==false then - - -- This must have been an alive cargo group that was killed outside the carrier, e.g. waiting to be transported or waiting to be put back. - -- Remove dead group from cargo group set. - if groupdead==true then - request.cargogroupset:Remove(groupname, NoTriggerEvent) - self:T(self.lid..string.format("Removed transported cargo %s outside carrier: ncargo=%d", groupname, request.cargogroupset:Count())) - -- This as well? - --request.transportcargoset:RemoveCargosByName(RemoveCargoNames) - end - else - self:E(self.lid..string.format("ERROR: Group %s is neither cargo nor transport!", group:GetName())) + self:E(self.lid..string.format("ERROR: Group %s is neither cargo nor transport!", deadgroup:GetName())) end end @@ -8056,57 +8087,12 @@ end -- @return #number Request ID. function WAREHOUSE:_GetIDsFromGroup(group) - ---@param #string text The text to analyse. - local function analyse(text) - - -- Get rid of #0001 tail from spawn. - local unspawned=UTILS.Split(text, "#")[1] - - -- Split keywords. - local keywords=UTILS.Split(unspawned, "_") - local _wid=nil -- warehouse UID - local _aid=nil -- asset UID - local _rid=nil -- request UID - - -- Loop over keys. - for _,keys in pairs(keywords) do - local str=UTILS.Split(keys, "-") - local key=str[1] - local val=str[2] - if key:find("WID") then - _wid=tonumber(val) - elseif key:find("AID") then - _aid=tonumber(val) - elseif key:find("RID") then - _rid=tonumber(val) - end - end - - return _wid,_aid,_rid - end - if group then - + -- Group name - local name=group:GetName() - - -- Get asset id from group name. - local wid,aid,rid=analyse(name) - - -- Get Asset. - local asset=self:GetAssetByID(aid) - - -- Get warehouse and request id from asset table. - if asset then - wid=asset.wid - rid=asset.rid - end - - -- Debug info - self:T3(self.lid..string.format("Group Name = %s", tostring(name))) - self:T3(self.lid..string.format("Warehouse ID = %s", tostring(wid))) - self:T3(self.lid..string.format("Asset ID = %s", tostring(aid))) - self:T3(self.lid..string.format("Request ID = %s", tostring(rid))) + local groupname=group:GetName() + + local wid, aid, rid=self:_GetIDsFromGroupName(groupname) return wid,aid,rid else @@ -8115,14 +8101,13 @@ function WAREHOUSE:_GetIDsFromGroup(group) end - --- Get warehouse id, asset id and request id from group name (alias). -- @param #WAREHOUSE self --- @param Wrapper.Group#GROUP group The group from which the info is gathered. +-- @param #string groupname Name of the group from which the info is gathered. -- @return #number Warehouse ID. -- @return #number Asset ID. -- @return #number Request ID. -function WAREHOUSE:_GetIDsFromGroupOLD(group) +function WAREHOUSE:_GetIDsFromGroupName(groupname) ---@param #string text The text to analyse. local function analyse(text) @@ -8153,25 +8138,26 @@ function WAREHOUSE:_GetIDsFromGroupOLD(group) return _wid,_aid,_rid end - if group then - -- Group name - local name=group:GetName() + -- Get asset id from group name. + local wid,aid,rid=analyse(groupname) - -- Get ids - local wid,aid,rid=analyse(name) + -- Get Asset. + local asset=self:GetAssetByID(aid) - -- Debug info - self:T3(self.lid..string.format("Group Name = %s", tostring(name))) - self:T3(self.lid..string.format("Warehouse ID = %s", tostring(wid))) - self:T3(self.lid..string.format("Asset ID = %s", tostring(aid))) - self:T3(self.lid..string.format("Request ID = %s", tostring(rid))) - - return wid,aid,rid - else - self:E("WARNING: Group not found in GetIDsFromGroup() function!") + -- Get warehouse and request id from asset table. + if asset then + wid=asset.wid + rid=asset.rid end + -- Debug info + self:T3(self.lid..string.format("Group Name = %s", tostring(groupname))) + self:T3(self.lid..string.format("Warehouse ID = %s", tostring(wid))) + self:T3(self.lid..string.format("Asset ID = %s", tostring(aid))) + self:T3(self.lid..string.format("Request ID = %s", tostring(rid))) + + return wid,aid,rid end --- Filter stock assets by descriptor and attribute. diff --git a/Moose Development/Moose/Ops/Auftrag.lua b/Moose Development/Moose/Ops/Auftrag.lua index 08d316299..c50df7056 100644 --- a/Moose Development/Moose/Ops/Auftrag.lua +++ b/Moose Development/Moose/Ops/Auftrag.lua @@ -384,6 +384,7 @@ _AUFTRAGSNR=0 -- @field #string ARMOREDGUARD On guard - with armored groups. -- @field #string BARRAGE Barrage. -- @field #string ARMORATTACK Armor attack. +-- @field #string CASENHANCED Enhanced CAS. AUFTRAG.Type={ ANTISHIP="Anti Ship", AWACS="AWACS", @@ -416,6 +417,7 @@ AUFTRAG.Type={ ARMOREDGUARD="Armored Guard", BARRAGE="Barrage", ARMORATTACK="Armor Attack", + CASENHANCED="CAS Enhanced", } --- Mission status of an assigned group. @@ -558,7 +560,7 @@ AUFTRAG.Category={ --- AUFTRAG class version. -- @field #string version -AUFTRAG.version="0.8.2" +AUFTRAG.version="0.8.3" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1187,6 +1189,45 @@ function AUFTRAG:NewCAS(ZoneCAS, Altitude, Speed, Coordinate, Heading, Leg, Targ return mission end +--- **[AIR]** Create a CASENHANCED mission. Group(s) will go to the zone and patrol it randomly. +-- @param #AUFTRAG self +-- @param Core.Zone#ZONE CasZone The CAS zone. +-- @param #number Altitude Altitude in feet. Only for airborne units. Default 2000 feet ASL. +-- @param #number Speed Speed in knots. +-- @param #number RangeMax Max range in NM. Only detected targets within this radius from the group will be engaged. Default is 25 NM. +-- @param #table TargetTypes Types of target attributes that will be engaged. See [DCS enum attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes). Default `{"Helicopters", "Ground Units", "Light armed ships"}`. +-- @param Core.Set#SET_ZONE NoEngageZoneSet Set of zones in which targets are *not* engaged. Default is nowhere. +-- @return #AUFTRAG self +function AUFTRAG:NewCASENHANCED(CasZone, Altitude, Speed, RangeMax, NoEngageZoneSet, TargetTypes) + + local mission=AUFTRAG:New(AUFTRAG.Type.CASENHANCED) + + -- Ensure we got a ZONE and not just the zone name. + if type(CasZone)=="string" then + CasZone=ZONE:New(CasZone) + end + + mission:_TargetFromObject(CasZone) + + mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.CASENHANCED) + + mission:SetEngageDetected(RangeMax, TargetTypes or {"Helicopters", "Ground Units", "Light armed ships"}, CasZone, NoEngageZoneSet) + + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionROT=ENUMS.ROT.EvadeFire + + mission.missionFraction=1.0 + mission.missionSpeed=Speed and UTILS.KnotsToKmph(Speed) or nil + mission.missionAltitude=Altitude and UTILS.FeetToMeters(Altitude) or nil + + mission.categories={AUFTRAG.Category.AIRCRAFT} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + + --- **[AIR]** Create a FACA mission. -- @param #AUFTRAG self -- @param Wrapper.Group#GROUP Target Target group. Must be a GROUP object. @@ -1678,6 +1719,7 @@ function AUFTRAG:NewPATROLZONE(Zone, Speed, Altitude) return mission end + --- **[GROUND]** Create a ARMORATTACK mission. Armoured ground group(s) will go to the zone and attack. -- @param #AUFTRAG self -- @param Wrapper.Positionable#POSITIONABLE Target The target to attack. Can be a GROUP, UNIT or STATIC object. @@ -4942,6 +4984,26 @@ function AUFTRAG:GetDCSMissionTask(TaskControllable) DCStask.params=param table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.CASENHANCED then + + ------------------------- + -- CAS ENHANCED Mission -- + ------------------------- + + local DCStask={} + + DCStask.id="PatrolZone" + + -- We create a "fake" DCS task and pass the parameters to the FLIGHTGROUP. + local param={} + param.zone=self:GetObjective() + param.altitude=self.missionAltitude + param.speed=self.missionSpeed + + DCStask.params=param + + table.insert(DCStasks, DCStask) elseif self.type==AUFTRAG.Type.ARMORATTACK then @@ -5131,6 +5193,8 @@ function AUFTRAG:GetMissionTaskforMissionType(MissionType) mtask=ENUMS.MissionTask.CAS elseif MissionType==AUFTRAG.Type.PATROLZONE then mtask=ENUMS.MissionTask.CAS + elseif MissionType==AUFTRAG.Type.CASENHANCED then + mtask=ENUMS.MissionTask.CAS elseif MissionType==AUFTRAG.Type.ESCORT then mtask=ENUMS.MissionTask.ESCORT elseif MissionType==AUFTRAG.Type.FACA then diff --git a/Moose Development/Moose/Ops/Chief.lua b/Moose Development/Moose/Ops/Chief.lua index 7ba916181..3519b48a7 100644 --- a/Moose Development/Moose/Ops/Chief.lua +++ b/Moose Development/Moose/Ops/Chief.lua @@ -103,7 +103,7 @@ -- -- Strategically important zones, which should be captured can be added via the @{#CHIEF.AddStrategicZone}() function. -- --- If the zone is currently owned by another coalition and enemy ground troops are present in the zone, a CAS mission is lauchned. +-- If the zone is currently owned by another coalition and enemy ground troops are present in the zone, a CAS and an ARTY mission are lauchned, provided assets are available. -- -- Once the zone is cleaned of enemy forces, ground (infantry) troops are send there. These require a transportation via helicopters. -- So in order to deploy our own troops, infantry assets with `AUFTRAG.Type.ONGUARD` and helicopters with `AUFTRAG.Type.OPSTRANSPORT` need to be available. @@ -1731,7 +1731,7 @@ function CHIEF:CheckOpsZoneQueue() -- Has a patrol mission? local hasMissionPatrol=stratzone.opszone:_FindMissions(self.coalition,AUFTRAG.Type.ONGUARD) or stratzone.opszone:_FindMissions(self.coalition,AUFTRAG.Type.ARMOREDGUARD) -- Has a CAS mission? - local hasMissionCAS=stratzone.opszone:_FindMissions(self.coalition,AUFTRAG.Type.CAS) + local hasMissionCAS=stratzone.opszone:_FindMissions(self.coalition,AUFTRAG.Type.CASENHANCED) -- Has a ARTY mission? local hasMissionARTY=stratzone.opszone:_FindMissions(self.coalition,AUFTRAG.Type.ARTY) @@ -1775,12 +1775,13 @@ function CHIEF:CheckOpsZoneQueue() -- Recruite CAS assets. - local recruited=self:RecruitAssetsForZone(stratzone, AUFTRAG.Type.CAS, 1, 1) + local recruited=self:RecruitAssetsForZone(stratzone, AUFTRAG.Type.CASENHANCED, 1, 1) -- Debug message. self:T(self.lid..string.format("Zone is NOT empty ==> Recruit CAS assets=%s", tostring(recruited))) end + if not hasMissionARTY then -- Debug message. @@ -1811,7 +1812,7 @@ function CHIEF:CheckOpsZoneQueue() local hasMissionPATROL=stratzone.opszone:_FindMissions(self.coalition,AUFTRAG.Type.PATROLZONE) -- Has a CAS mission? - local hasMissionCAS, CASMissions = stratzone.opszone:_FindMissions(self.coalition,AUFTRAG.Type.CAS) + local hasMissionCAS, CASMissions = stratzone.opszone:_FindMissions(self.coalition,AUFTRAG.Type.CASENHANCED) local hasMissionARTY, ARTYMissions = stratzone.opszone:_FindMissions(self.coalition,AUFTRAG.Type.ARTY) if ownercoalition==self.coalition and stratzone.opszone:IsEmpty() and hasMissionCAS then @@ -2260,12 +2261,48 @@ function CHIEF:RecruitAssetsForZone(StratZone, MissionType, NassetsMin, NassetsM -- Attach mission to ops zone. StratZone.opszone:_AddMission(self.coalition, MissionType, mission) + -- Set ops zone to transport. + transport.opszone=StratZone.opszone + transport.chief=self + transport.commander=self.commander + return true else LEGION.UnRecruitAssets(assets) return false end + + elseif MissionType==AUFTRAG.Type.CASENHANCED then + + -- Create Patrol zone mission. + local caszone = StratZone.opszone.zone + local coord = caszone:GetCoordinate() + local height = UTILS.MetersToFeet(coord:GetLandHeight())+2500 + + local Speed = 200 + if assets[1] then + if assets[1].speedmax then + Speed = UTILS.KmphToKnots(assets[1].speedmax * 0.7) or 200 + end + end + + --local mission=AUFTRAG:NewCAS(caszone,height,Speed,coord,math.random(0,359),Leg) + local mission=AUFTRAG:NewCASENHANCED(caszone, height, Speed) + + -- Add assets to mission. + for _,asset in pairs(assets) do + mission:AddAsset(asset) + end + + -- Assign mission to legions. + self:MissionAssign(mission, legions) + + -- Attach mission to ops zone. + StratZone.opszone:_AddMission(self.coalition, MissionType, mission) + + return true + elseif MissionType==AUFTRAG.Type.CAS then -- Create Patrol zone mission. diff --git a/Moose Development/Moose/Ops/Cohort.lua b/Moose Development/Moose/Ops/Cohort.lua index 9dc2a66d5..3780cbf07 100644 --- a/Moose Development/Moose/Ops/Cohort.lua +++ b/Moose Development/Moose/Ops/Cohort.lua @@ -919,36 +919,38 @@ function COHORT:RecruitAssets(MissionType, Npayloads) -- ARMY/NAVYGROUP combat ready? --- + -- Disable this for now as it can cause problems - at least with transport and cargo assets. + --self:I("Attribute is: "..asset.attribute) + if flightgroup:IsArmygroup() then + -- check for fighting assets + if asset.attribute == WAREHOUSE.Attribute.GROUND_ARTILLERY or + asset.attribute == WAREHOUSE.Attribute.GROUND_TANK or + asset.attribute == WAREHOUSE.Attribute.GROUND_INFANTRY or + asset.attribute == WAREHOUSE.Attribute.GROUND_AAA or + asset.attribute == WAREHOUSE.Attribute.GROUND_SAM + then + combatready=true + end + else + combatready=false + end + + -- Not ready when rearming, retreating or returning! if flightgroup:IsRearming() or flightgroup:IsRetreating() or flightgroup:IsReturning() then combatready=false end end - -- Check transport/cargo for combat readyness! + -- Not ready when currently acting as ops transport carrier. if flightgroup:IsLoading() or flightgroup:IsTransporting() or flightgroup:IsUnloading() or flightgroup:IsPickingup() or flightgroup:IsCarrier() then combatready=false - end + end + -- Not ready when currently acting as ops transport cargo. if flightgroup:IsCargo() or flightgroup:IsBoarding() or flightgroup:IsAwaitingLift() then combatready=false end - - -- Disable this for now as it can cause problems - at least with transport and cargo assets. - --self:I("Attribute is: "..asset.attribute) - if flightgroup:IsArmygroup() then - -- check for fighting assets - if asset.attribute == WAREHOUSE.Attribute.GROUND_ARTILLERY or - asset.attribute == WAREHOUSE.Attribute.GROUND_TANK or - asset.attribute == WAREHOUSE.Attribute.GROUND_INFANTRY or - asset.attribute == WAREHOUSE.Attribute.GROUND_AAA or - asset.attribute == WAREHOUSE.Attribute.GROUND_SAM - then - combatready=true - end - else - combatready=false - end - + -- This asset is "combatready". if combatready then self:T(self.lid.."Adding SPAWNED asset to ANOTHER mission as it is COMBATREADY") diff --git a/Moose Development/Moose/Ops/Legion.lua b/Moose Development/Moose/Ops/Legion.lua index bec5b72c6..7073651da 100644 --- a/Moose Development/Moose/Ops/Legion.lua +++ b/Moose Development/Moose/Ops/Legion.lua @@ -81,6 +81,10 @@ function LEGION:New(WarehouseName, LegionName) -- Defaults: -- TODO: What? self:SetMarker(false) + + -- Dead and crash events are handled via opsgroups. + self:UnHandleEvent(EVENTS.Crash) + self:UnHandleEvent(EVENTS.Dead) -- Add FSM transitions. -- From State --> Event --> To State @@ -882,7 +886,18 @@ function LEGION:onafterTransportCancel(From, Event, To, Transport) local cargos=Transport:GetCargoOpsGroups(false) for _,_cargo in pairs(cargos) do local cargo=_cargo --Ops.OpsGroup#OPSGROUP + + -- Remover my lift. cargo:_DelMyLift(Transport) + + -- Legion of cargo group + local legion=cargo.legion + + -- Add asset back to legion. + if legion then + legion:T(self.lid..string.format("Adding cargo group %s back to legion", cargo:GetName())) + legion:__AddAsset(0.1, cargo.group, 1) + end end -- Remove asset from mission. @@ -993,7 +1008,7 @@ function LEGION:onafterNewAsset(From, Event, To, asset, assignment) -- Debug text. local text=string.format("New asset %s with assignment %s and request assignment %s", asset.spawngroupname, tostring(asset.assignment), tostring(assignment)) - self:T3(self.lid..text) + self:T(self.lid..text) -- Get cohort. local cohort=self:_GetCohort(asset.assignment) @@ -1061,6 +1076,8 @@ function LEGION:onafterNewAsset(From, Event, To, asset, assignment) -- Asset is returned to the COHORT --- + self:T(self.lid..string.format("Asset returned to legion ==> calling LegionAssetReturned event")) + -- Trigger event. self:LegionAssetReturned(cohort, asset) @@ -1078,7 +1095,7 @@ end -- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset that returned. function LEGION:onafterLegionAssetReturned(From, Event, To, Cohort, Asset) -- Debug message. - self:T(self.lid..string.format("Asset %s from Cohort %s returned! asset.assignment=\"%s\"", Asset.spawngroupname, Cohort.name, tostring(Asset.assignment))) + self:I(self.lid..string.format("Asset %s from Cohort %s returned! asset.assignment=\"%s\"", Asset.spawngroupname, Cohort.name, tostring(Asset.assignment))) -- Stop flightgroup. if Asset.flightgroup and not Asset.flightgroup:IsStopped() then @@ -1246,7 +1263,7 @@ function LEGION:onafterAssetDead(From, Event, To, asset, request) -- Remove group from the detection set of the CHIEF (INTEL). if self.commander and self.commander.chief then self.commander.chief.detectionset:RemoveGroupsByName({asset.spawngroupname}) - end + end -- Remove asset from mission is done via Mission:AssetDead() call from flightgroup onafterFlightDead function -- Remove asset from squadron same @@ -2396,12 +2413,34 @@ function LEGION.CalculateAssetMissionScore(asset, MissionType, TargetVec2, Inclu -- Reduce score for legions that are futher away. score=score-distance - -- Intercepts need to be carried out quickly. We prefer spawned assets. - --if MissionType==AUFTRAG.Type.INTERCEPT then - if asset.spawned then - score=score+25 + -- Intercepts need to be carried out quickly. We prefer spawned assets. + if asset.spawned and asset.flightgroup and asset.flightgroup:IsAlive() then + + local currmission=asset.flightgroup:GetMissionCurrent() + + if currmission then + + if currmission.type==AUFTRAG.Type.ALERT5 and currmission.alert5MissionType==MissionType then + -- Prefer assets that are on ALERT5 for this mission type. + score=score+25 + elseif currmission==AUFTRAG.Type.GCICAP and MissionType==AUFTRAG.Type.INTERCEPT then + -- Prefer assets that are on GCICAP to perform INTERCEPTS + score=score+25 + end end - --end + + if MissionType==AUFTRAG.Type.OPSTRANSPORT or MissionType==AUFTRAG.Type.AMMOSUPPLY or MissionType==AUFTRAG.Type.AWACS or MissionType==AUFTRAG.Type.FUELSUPPLY or MissionType==AUFTRAG.Type.TANKER then + -- TODO: need to check for missions that do not require ammo like transport, recon, awacs, tanker etc. + -- We better take a fresh asset. Sometimes spawned assets to something else, which is difficult to check. + score=score-10 + else + -- Combat mission. + if asset.flightgroup:IsOutOfAmmo() then + -- Assets that are out of ammo are not considered. + score=score-1000 + end + end + end -- TRANSPORT specific. if MissionType==AUFTRAG.Type.OPSTRANSPORT then diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index 7ab4996bf..4b427a81f 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -3673,7 +3673,6 @@ function OPSGROUP:onafterTaskExecute(From, Event, To, Task) -- BARRAGE is special! if Task.dcstask.id==AUFTRAG.SpecialTask.BARRAGE then - env.info("FF Barrage") local vec2=self:GetVec2() local param=Task.dcstask.params local heading=param.heading or math.random(1, 360) @@ -4230,7 +4229,7 @@ function OPSGROUP:onafterMissionStart(From, Event, To, Mission) -- IMMOBILE Group --- - env.info("FF Immobile GROUP") + --env.info("FF Immobile GROUP") -- Add waypoint task. UpdateRoute is called inside. local Clock=Mission.Tpush and UTILS.SecondsToClock(Mission.Tpush) or 5 @@ -5768,6 +5767,32 @@ function OPSGROUP:onafterElementInUtero(From, Event, To, Element) end +--- On after "ElementDamaged" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP.Element Element The flight group element. +function OPSGROUP:onafterElementDamaged(From, Event, To, Element) + self:T(self.lid..string.format("Element damaged %s", Element.name)) + + if Element and Element.status~=OPSGROUP.ElementStatus.DEAD then + + local lifepoints=Element.DCSunit:getLife() + local lifepoint0=Element.DCSunit:getLife0() + + self:T(self.lid..string.format("Element life %s: %.2f/%.2f", Element.name, lifepoints, lifepoint0)) + + if lifepoints<=1.0 then + self:T(self.lid..string.format("Element %s life %.2f <= 1.0 ==> Destroyed!", Element.name, lifepoints)) + self:ElementDestroyed(Element) + end + + end + +end + + --- On after "ElementDestroyed" event. -- @param #OPSGROUP self -- @param #string From From state. @@ -5802,10 +5827,18 @@ end function OPSGROUP:onafterElementDead(From, Event, To, Element) -- Debug info. - self:T(self.lid..string.format("Element dead %s at t=%.3f", Element.name, timer.getTime())) + self:I(self.lid..string.format("Element dead %s at t=%.3f", Element.name, timer.getTime())) -- Set element status. self:_UpdateStatus(Element, OPSGROUP.ElementStatus.DEAD) + + if self.legion then + if not self:IsInUtero() then + local asset=self.legion:GetAssetByName(self.groupname) + local request=self.legion:GetRequestByID(asset.rid) + self.legion:_UnitDead(Element.unit, self.group, request) + end + end -- Check if element was lasing and if so, switch to another unit alive to lase. if self.spot.On and self.spot.element.name==Element.name then @@ -6140,12 +6173,18 @@ function OPSGROUP:onafterDead(From, Event, To) -- All elements were destroyed ==> Asset group is gone. self.cohort:DelGroup(self.groupname) end + if self.legion then + --self.legion:Get + --self.legion:AssetDead() + end else -- Not all assets were destroyed (despawn) ==> Add asset back to legion? end - -- Stop in a sec. - --self:__Stop(-5) + -- Stop in 5 sec to give possible respawn attempts a chance. + if self.legion then + self:__Stop(-5) + end end --- On before "Stop" event. @@ -6187,6 +6226,11 @@ function OPSGROUP:onafterStop(From, Event, To) self:UnHandleEvent(EVENTS.Crash) self.currbase=nil end + + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + self:MissionCancel(mission) + end -- Stop check timers. self.timerCheckZone:Stop() diff --git a/Moose Development/Moose/Ops/OpsTransport.lua b/Moose Development/Moose/Ops/OpsTransport.lua index c15154747..7940c17d7 100644 --- a/Moose Development/Moose/Ops/OpsTransport.lua +++ b/Moose Development/Moose/Ops/OpsTransport.lua @@ -57,6 +57,8 @@ -- @field #table statusLegion Transport status of all assigned LEGIONs. -- @field #string statusCommander Staus of the COMMANDER. -- @field Ops.Commander#COMMANDER commander Commander of the transport. +-- @field Ops.Chief#CHIEF chief Chief of the transport. +-- @field Ops.OpsZone#OPSZONE opszone OPS zone. -- @field #table requestID The ID of the queued warehouse request. Necessary to cancel the request if the transport was cancelled before the request is processed. -- -- @extends Core.Fsm#FSM @@ -1305,7 +1307,7 @@ function OPSTRANSPORT:GetNcarrier() return self.Ncarrier end ---- Add asset to transport. +--- Add carrier asset to transport. -- @param #OPSTRANSPORT self -- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset to be added. -- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. @@ -1323,7 +1325,7 @@ function OPSTRANSPORT:AddAsset(Asset, TransportZoneCombo) return self end ---- Delete asset from mission. +--- Delete carrier asset from transport. -- @param #OPSTRANSPORT self -- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset to be removed. -- @return #OPSTRANSPORT self @@ -1736,12 +1738,12 @@ function OPSTRANSPORT:onafterDeadCarrierGroup(From, Event, To, OpsGroup) -- Increase dead counter. self.NcarrierDead=self.NcarrierDead+1 - if #self.carriers==0 then - self:DeadCarrierAll() - end - -- Remove group from carrier list/table. self:_DelCarrier(OpsGroup) + + if #self.carriers==0 then + self:DeadCarrierAll() + end end --- On after "DeadCarrierAll" event. @@ -1750,15 +1752,30 @@ end -- @param #string Event Event. -- @param #string To To state. function OPSTRANSPORT:onafterDeadCarrierAll(From, Event, To) - self:I(self.lid..string.format("ALL Carrier OPSGROUPs are dead! Setting stage to PLANNED if not all cargo was delivered.")) + self:I(self.lid..string.format("ALL Carrier OPSGROUPs are dead!")) + + if self.opszone then + + self:I(self.lid..string.format("Cancelling transport on CHIEF level")) + self.chief:TransportCancel(self) + + --for _,_legion in pairs(self.legions) do + -- local legion=_legion --Ops.Legion#LEGION + -- legion:TransportCancel(self) + --end + + else - -- Check if cargo was delivered. - self:_CheckDelivered() + -- Check if cargo was delivered. + self:_CheckDelivered() + + -- Set state back to PLANNED if not delivered. + if not self:IsDelivered() then + self:Planned() + end - -- Set state back to PLANNED if not delivered. - if not self:IsDelivered() then - self:Planned() end + end --- On after "Cancel" event. From e6f388518a1be03e4169f867691f1b6d1715eae9 Mon Sep 17 00:00:00 2001 From: Applevangelist <72444570+Applevangelist@users.noreply.github.com> Date: Wed, 9 Mar 2022 10:28:26 +0100 Subject: [PATCH 11/26] Update CTLD.lua (#1693) minor nil check --- Moose Development/Moose/Ops/CTLD.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index c04e3e26f..02f334590 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -2104,7 +2104,7 @@ function CTLD:_FindCratesNearby( _group, _unit, _dist, _ignoreweight) loadedmass = self:_GetUnitCargoMass(_unit) unittype = _unit:GetTypeName() capabilities = self:_GetUnitCapabilities(_unit) -- #CTLD.UnitCapabilities - maxmass = capabilities.cargoweightlimit + maxmass = capabilities.cargoweightlimit or 2000 maxloadable = maxmass - loadedmass end self:T(self.lid .. " Max loadable mass: " .. maxloadable) From ff1ebf9775670d3b77e498c965fc61248080d614 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 9 Mar 2022 19:07:14 +0100 Subject: [PATCH 12/26] OPS - Legion: Improved max number of transport assets. - OPSGROUP: fixed bugs in speed parameter --- Moose Development/Moose/Core/Zone.lua | 2 +- .../Moose/Functional/Warehouse.lua | 5 +- Moose Development/Moose/Ops/Legion.lua | 61 ++++++++++++++++--- Moose Development/Moose/Ops/OpsGroup.lua | 6 +- 4 files changed, 60 insertions(+), 14 deletions(-) diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 4bf450816..c31840728 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -1219,7 +1219,7 @@ function ZONE_RADIUS:GetRandomVec2(inner, outer, surfacetypes) while gotit==false and N<=Nmax do gotit=_checkSurface(point) if gotit then - env.info(string.format("Got random coordinate with surface type %d after N=%d/%d iterations", land.getSurfaceType(point), N, Nmax)) + --env.info(string.format("Got random coordinate with surface type %d after N=%d/%d iterations", land.getSurfaceType(point), N, Nmax)) else point=_getpoint() N=N+1 diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index e7e2a0a5c..680723d62 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -4107,6 +4107,7 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, local cargobay={} local cargobaytot=0 local cargobaymax=0 + local weights={} for _i,_unit in pairs(group:GetUnits()) do local unit=_unit --Wrapper.Unit#UNIT local Desc=unit:GetDesc() @@ -4115,8 +4116,9 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, local unitweight=forceweight or Desc.massEmpty if unitweight then weight=weight+unitweight + weights[_i]=unitweight end - + local cargomax=0 local massfuel=Desc.fuelMassMax or 0 local massempty=Desc.massEmpty or 0 @@ -4165,6 +4167,7 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, asset.speedmax=SpeedMax asset.size=smax asset.weight=weight + asset.weights=weights asset.DCSdesc=Descriptors asset.attribute=attribute asset.cargobay=cargobay diff --git a/Moose Development/Moose/Ops/Legion.lua b/Moose Development/Moose/Ops/Legion.lua index 7073651da..929bd7d73 100644 --- a/Moose Development/Moose/Ops/Legion.lua +++ b/Moose Development/Moose/Ops/Legion.lua @@ -534,6 +534,7 @@ function LEGION:CheckMissionQueue() local Transport=nil if mission.NcarriersMin then local Legions=mission.transportLegions or {self} + TransportAvail, Transport=self:AssignAssetsForTransport(Legions, assets, mission.NcarriersMin, mission.NcarriersMax, mission.transportDeployZone, mission.transportDisembarkZone) end @@ -1025,7 +1026,7 @@ function LEGION:onafterNewAsset(From, Event, To, asset, assignment) local nunits=#asset.template.units -- Debug text. - local text=string.format("Adding asset to squadron %s: assignment=%s, type=%s, attribute=%s, nunits=%d %s", cohort.name, assignment, asset.unittype, asset.attribute, nunits, tostring(cohort.ngrouping)) + local text=string.format("Adding asset to squadron %s: assignment=%s, type=%s, attribute=%s, nunits=%d ngroup=%s", cohort.name, assignment, asset.unittype, asset.attribute, nunits, tostring(cohort.ngrouping)) self:T(self.lid..text) -- Adjust number of elements in the group. @@ -1033,6 +1034,10 @@ function LEGION:onafterNewAsset(From, Event, To, asset, assignment) local template=asset.template local N=math.max(#template.units, cohort.ngrouping) + + -- We need to recalc the total weight and cargo bay. + asset.weight=0 + asset.cargobaytot=0 -- Handle units. for i=1,N do @@ -1043,15 +1048,28 @@ function LEGION:onafterNewAsset(From, Event, To, asset, assignment) -- If grouping is larger than units present, copy first unit. if i>nunits then table.insert(template.units, UTILS.DeepCopy(template.units[1])) + asset.cargobaytot=asset.cargobaytot+asset.cargobay[1] + asset.weight=asset.weight+asset.weights[1] + template.units[i].x=template.units[1].x+5*(i-nunits) + template.units[i].y=template.units[1].y+5*(i-nunits) + else + if i<=cohort.ngrouping then + asset.weight=asset.weight+asset.weights[i] + asset.cargobaytot=asset.cargobaytot+asset.cargobay[i] + end end -- Remove units if original template contains more than in grouping. - if cohort.ngroupingnunits then - unit=nil + if i>cohort.ngrouping then + template.units[i]=nil end end + -- Set number of units. asset.nunits=cohort.ngrouping + + -- Debug info. + self:T(self.lid..string.format("After regrouping: Nunits=%d, weight=%.1f cargobaytot=%.1f kg", #asset.template.units, asset.weight, asset.cargobaytot)) end -- Set takeoff type. @@ -1826,7 +1844,7 @@ function LEGION:RecruitAssetsForMission(Mission) end -- Recuit assets. - local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, Mission.type, Mission.alert5MissionType, NreqMin, NreqMax, TargetVec2, Payloads, Mission.engageRange, Mission.refuelSystem, nil) + local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, Mission.type, Mission.alert5MissionType, NreqMin, NreqMax, TargetVec2, Payloads, Mission.engageRange, Mission.refuelSystem) return recruited, assets, legions end @@ -1843,17 +1861,20 @@ function LEGION:RecruitAssetsForTransport(Transport) local cargoOpsGroups=Transport:GetCargoOpsGroups(false) local weightGroup=0 + local TotalWeight=nil -- At least one group should be spawned. if #cargoOpsGroups>0 then -- Calculate the max weight so we know which cohorts can provide carriers. + TotalWeight=0 for _,_opsgroup in pairs(cargoOpsGroups) do local opsgroup=_opsgroup --Ops.OpsGroup#OPSGROUP local weight=opsgroup:GetWeightTotal() if weight>weightGroup then weightGroup=weight end + TotalWeight=TotalWeight+weight end else -- No cargo groups! @@ -1871,7 +1892,7 @@ function LEGION:RecruitAssetsForTransport(Transport) -- Recruit assets and legions. - local recruited, assets, legions=LEGION.RecruitCohortAssets(self.cohorts, AUFTRAG.Type.OPSTRANSPORT, nil, NreqMin, NreqMax, TargetVec2, nil, nil, nil, weightGroup) + local recruited, assets, legions=LEGION.RecruitCohortAssets(self.cohorts, AUFTRAG.Type.OPSTRANSPORT, nil, NreqMin, NreqMax, TargetVec2, nil, nil, nil, weightGroup, TotalWeight) return recruited, assets, legions end @@ -1932,12 +1953,13 @@ end -- @param #number RangeMax Max range in meters. -- @param #number RefuelSystem Refuelsystem. -- @param #number CargoWeight Cargo weight for recruiting transport carriers. +-- @param #number TotalWeight Total cargo weight in kg. -- @param #table Categories Group categories. -- @param #table Attributes Group attributes. See `GROUP.Attribute.` -- @return #boolean If `true` enough assets could be recruited. -- @return #table Recruited assets. **NOTE** that we set the `asset.isReserved=true` flag so it cant be recruited by anyone else. -- @return #table Legions of recruited assets. -function LEGION.RecruitCohortAssets(Cohorts, MissionTypeRecruit, MissionTypeOpt, NreqMin, NreqMax, TargetVec2, Payloads, RangeMax, RefuelSystem, CargoWeight, Categories, Attributes) +function LEGION.RecruitCohortAssets(Cohorts, MissionTypeRecruit, MissionTypeOpt, NreqMin, NreqMax, TargetVec2, Payloads, RangeMax, RefuelSystem, CargoWeight, TotalWeight, Categories, Attributes) -- The recruited assets. local Assets={} @@ -2072,10 +2094,30 @@ function LEGION.RecruitCohortAssets(Cohorts, MissionTypeRecruit, MissionTypeOpt, --- -- Add assets to mission. + local cargobay=0 for i=1,Nassets do local asset=Assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + asset.isReserved=true + Legions[asset.legion.alias]=asset.legion + + if TotalWeight then + + -- Number of + local N=math.floor(asset.cargobaytot/asset.nunits / CargoWeight)*asset.nunits + --env.info(string.format("cargobaytot=%d, cargoweight=%d ==> N=%d", asset.cargobaytot, CargoWeight, N)) + + cargobay=cargobay + N*CargoWeight + + if cargobay>=TotalWeight then + --env.info(string.format("FF found enough assets to transport all cargo! N=%d [%d], cargobay=%.1f >= %.1f kg total weight", i, Nassets, cargobay, TotalWeight)) + Nassets=i + break + end + + end + end -- Return payloads of not needed assets. @@ -2170,7 +2212,7 @@ function LEGION:AssignAssetsForEscort(Cohorts, Assets, NescortMin, NescortMax) end -- Recruit escort asset for the mission asset. - local Erecruited, eassets, elegions=LEGION.RecruitCohortAssets(Cohorts, AUFTRAG.Type.ESCORT, nil, NescortMin, NescortMax, TargetVec2, nil, nil, nil, nil, Categories) + local Erecruited, eassets, elegions=LEGION.RecruitCohortAssets(Cohorts, AUFTRAG.Type.ESCORT, nil, NescortMin, NescortMax, TargetVec2, nil, nil, nil, nil, nil, Categories) if Erecruited then Escorts[asset.spawngroupname]={EscortLegions=elegions, EscortAssets=eassets, ecategory=asset.category, TargetTypes=TargetTypes} @@ -2281,13 +2323,14 @@ function LEGION:AssignAssetsForTransport(Legions, CargoAssets, NcarriersMin, Nca end -- Get all legions and heaviest cargo group weight - local CargoLegions={} ; local CargoWeight=nil + local CargoLegions={} ; local CargoWeight=nil ; local TotalWeight=0 for _,_asset in pairs(CargoAssets) do local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem CargoLegions[asset.legion.alias]=asset.legion if CargoWeight==nil or asset.weight>CargoWeight then CargoWeight=asset.weight end + TotalWeight=TotalWeight+asset.weight end -- Target is the deploy zone. @@ -2295,7 +2338,7 @@ function LEGION:AssignAssetsForTransport(Legions, CargoAssets, NcarriersMin, Nca -- Recruit assets and legions. local TransportAvail, CarrierAssets, CarrierLegions= - LEGION.RecruitCohortAssets(Cohorts, AUFTRAG.Type.OPSTRANSPORT, nil, NcarriersMin, NcarriersMax, TargetVec2, nil, nil, nil, CargoWeight, Categories, Attributes) + LEGION.RecruitCohortAssets(Cohorts, AUFTRAG.Type.OPSTRANSPORT, nil, NcarriersMin, NcarriersMax, TargetVec2, nil, nil, nil, CargoWeight, TotalWeight, Categories, Attributes) if TransportAvail then diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index 4b427a81f..e29ebb5c6 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -3574,7 +3574,7 @@ function OPSGROUP:onafterTaskExecute(From, Event, To, Task) --Coordinate:MarkToAll("Random Patrol Zone Coordinate") -- Speed and altitude. - local Speed=UTILS.MpsToKnots(Task.dcstask.params.speed) or UTILS.KmphToKnots(self.speedCruise) + local Speed=Task.dcstask.params.speed and UTILS.MpsToKnots(Task.dcstask.params.speed) or UTILS.KmphToKnots(self.speedCruise) --local Speed=UTILS.KmphToKnots(Task.dcstask.params.speed or self.speedCruise) local Altitude=Task.dcstask.params.altitude and UTILS.MetersToFeet(Task.dcstask.params.altitude) or nil @@ -4909,7 +4909,7 @@ function OPSGROUP:onafterPassingWaypoint(From, Event, To, Waypoint) local Coordinate=zone:GetRandomCoordinate(nil, nil, surfacetypes) -- Speed and altitude. - local Speed=UTILS.MpsToKnots(task.dcstask.params.speed) or UTILS.KmphToKnots(self.speedCruise) + local Speed=task.dcstask.params.speed and UTILS.MpsToKnots(task.dcstask.params.speed) or UTILS.KmphToKnots(self.speedCruise) -- local Speed=UTILS.KmphToKnots(speed or self.speedCruise) local Altitude=UTILS.MetersToFeet(task.dcstask.params.altitude or self.altitudeCruise) @@ -4946,7 +4946,7 @@ function OPSGROUP:onafterPassingWaypoint(From, Event, To, Waypoint) local Coordinate=zone:GetRandomCoordinate() -- Speed and altitude. - local Speed=UTILS.MpsToKnots(task.dcstask.params.speed) or UTILS.KmphToKnots(self.speedCruise) + local Speed=task.dcstask.params.speed and UTILS.MpsToKnots(task.dcstask.params.speed) or UTILS.KmphToKnots(self.speedCruise) --local Speed=UTILS.KmphToKnots(task.dcstask.params.speed or self.speedCruise) local Altitude=task.dcstask.params.altitude and UTILS.MetersToFeet(task.dcstask.params.altitude) or nil From 5dae9a197a54d620d7b52b6d55952d15248b3e8d Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 10 Mar 2022 10:05:21 +0100 Subject: [PATCH 13/26] OPS - CHIEF: fixed bug in LEGION.RecruitCohortAssets() function call - COMMANDER: added total weight to LEGION.RecruitCohortAssets() function call - POSITIONABLE: fixed bug in relFuel calculation for cargo bay size --- Moose Development/Moose/Ops/Chief.lua | 4 ++-- Moose Development/Moose/Ops/Commander.lua | 14 +++++++++----- Moose Development/Moose/Ops/OpsGroup.lua | 9 +++++++++ Moose Development/Moose/Wrapper/Positionable.lua | 2 +- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/Moose Development/Moose/Ops/Chief.lua b/Moose Development/Moose/Ops/Chief.lua index 3519b48a7..0b49d5b0c 100644 --- a/Moose Development/Moose/Ops/Chief.lua +++ b/Moose Development/Moose/Ops/Chief.lua @@ -180,7 +180,7 @@ CHIEF.Strategy = { --- CHIEF class version. -- @field #string version -CHIEF.version="0.1.0" +CHIEF.version="0.1.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -2209,7 +2209,7 @@ function CHIEF:RecruitAssetsForZone(StratZone, MissionType, NassetsMin, NassetsM end -- Recruite infantry assets. - local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, MissionType, nil, NassetsMin, NassetsMax, TargetVec2, nil, RangeMax, nil, nil, Categories, Attributes) + local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, MissionType, nil, NassetsMin, NassetsMax, TargetVec2, nil, RangeMax, nil, nil, nil, Categories, Attributes) if recruited then diff --git a/Moose Development/Moose/Ops/Commander.lua b/Moose Development/Moose/Ops/Commander.lua index 4645c307b..5627d2082 100644 --- a/Moose Development/Moose/Ops/Commander.lua +++ b/Moose Development/Moose/Ops/Commander.lua @@ -132,7 +132,7 @@ COMMANDER = { --- COMMANDER class version. -- @field #string version -COMMANDER.version="0.1.0" +COMMANDER.version="0.1.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1190,7 +1190,7 @@ function COMMANDER:RecruitAssetsForMission(Mission) local Payloads=Mission.payloads -- Recruite assets. - local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, Mission.type, Mission.alert5MissionType, NreqMin, NreqMax, TargetVec2, Payloads, Mission.engageRange, Mission.refuelSystem, nil) + local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, Mission.type, Mission.alert5MissionType, NreqMin, NreqMax, TargetVec2, Payloads, Mission.engageRange, Mission.refuelSystem) return recruited, assets, legions end @@ -1292,6 +1292,7 @@ function COMMANDER:CheckTransportQueue() -- Weight of the heaviest cargo group. Necessary condition that this fits into on carrier unit! local weightGroup=0 + local TotalWeight=0 -- Calculate the max weight so we know which cohorts can provide carriers. if #cargoOpsGroups>0 then @@ -1301,13 +1302,14 @@ function COMMANDER:CheckTransportQueue() if weight>weightGroup then weightGroup=weight end + TotalWeight=TotalWeight+weight end end if weightGroup>0 then -- Recruite assets from legions. - local recruited, assets, legions=self:RecruitAssetsForTransport(transport, weightGroup) + local recruited, assets, legions=self:RecruitAssetsForTransport(transport, weightGroup, TotalWeight) if recruited then @@ -1344,10 +1346,12 @@ end --- Recruit assets for a given OPS transport. -- @param #COMMANDER self -- @param Ops.OpsTransport#OPSTRANSPORT Transport The OPS transport. +-- @param #number CargoWeight Weight of the heaviest cargo group. +-- @param #number TotalWeight Total weight of all cargo groups. -- @return #boolean If `true`, enough assets could be recruited. -- @return #table Recruited assets. -- @return #table Legions that have recruited assets. -function COMMANDER:RecruitAssetsForTransport(Transport, CargoWeight) +function COMMANDER:RecruitAssetsForTransport(Transport, CargoWeight, TotalWeight) if CargoWeight==0 then -- No cargo groups! @@ -1381,7 +1385,7 @@ function COMMANDER:RecruitAssetsForTransport(Transport, CargoWeight) local NreqMin,NreqMax=Transport:GetRequiredCarriers() -- Recruit assets and legions. - local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, AUFTRAG.Type.OPSTRANSPORT, nil, NreqMin, NreqMax, TargetVec2, nil, nil, nil, CargoWeight) + local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, AUFTRAG.Type.OPSTRANSPORT, nil, NreqMin, NreqMax, TargetVec2, nil, nil, nil, CargoWeight, TotalWeight) return recruited, assets, legions end diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index e29ebb5c6..8244f404d 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -541,6 +541,9 @@ function OPSGROUP:New(group) else end + + -- Set gen attribute. + self.attribute=self.group:GetAttribute() local units=self.group:GetUnits() @@ -864,6 +867,12 @@ function OPSGROUP:GetLifePoints(Element) return life, life0 end +--- Get generalized attribute. +-- @param #OPSGROUP self +-- @return #string Generalized attribute. +function OPSGROUP:GetAttribute() + return self.attribute +end --- Set verbosity level. -- @param #OPSGROUP self diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index 2ff626552..cd979322b 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -1494,7 +1494,7 @@ do -- Cargo -- Fuel. The descriptor provides the max fuel mass in kg. This needs to be multiplied by the relative fuel amount to calculate the actual fuel mass on board. local massFuelMax=Desc.fuelMassMax or 0 - local relFuel=math.max(self:GetFuel() or 1.0, 1.0) -- We take 1.0 as max in case of external fuel tanks. + local relFuel=math.min(self:GetFuel() or 1.0, 1.0) -- We take 1.0 as max in case of external fuel tanks. local massFuel=massFuelMax*relFuel -- Number of soldiers according to DCS function From 2d0b4d6ae53394828511586fb782200cfec6c53f Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Fri, 11 Mar 2022 10:19:20 +0100 Subject: [PATCH 14/26] SCORING: Corrected calc error in summary scoring functions --- Moose Development/Moose/Functional/Scoring.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Functional/Scoring.lua b/Moose Development/Moose/Functional/Scoring.lua index b914d445c..50903687d 100644 --- a/Moose Development/Moose/Functional/Scoring.lua +++ b/Moose Development/Moose/Functional/Scoring.lua @@ -1704,7 +1704,7 @@ function SCORING:ReportScoreGroupDetailed( PlayerGroup ) self:F( { ReportMissions, ScoreMissions, PenaltyMissions } ) local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions - local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + ScoreGoals + PenaltyMissions + local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + PenaltyGoals + PenaltyMissions PlayerMessage = string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )%s%s%s%s%s", @@ -1760,7 +1760,7 @@ function SCORING:ReportScoreAllSummary( PlayerGroup ) self:F( { ReportMissions, ScoreMissions, PenaltyMissions } ) local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions - local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + ScoreGoals + PenaltyMissions + local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + PenaltyGoals + PenaltyMissions PlayerMessage = string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )", From 520eb4cd1db55597ecabfc3ce013762719d0cbe7 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 11 Mar 2022 22:35:57 +0100 Subject: [PATCH 15/26] OPS - Improved recovery of retreated groups --- Moose Development/Moose/Ops/ArmyGroup.lua | 4 ++++ Moose Development/Moose/Ops/OpsGroup.lua | 29 +++++++++++++++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Ops/ArmyGroup.lua b/Moose Development/Moose/Ops/ArmyGroup.lua index 6ed92c53c..09cb38bcc 100644 --- a/Moose Development/Moose/Ops/ArmyGroup.lua +++ b/Moose Development/Moose/Ops/ArmyGroup.lua @@ -115,6 +115,7 @@ function ARMYGROUP:New(group) self:AddTransition("*", "Cruise", "Cruising") -- Cruise along the given route of waypoints. self:AddTransition("*", "RTZ", "Returning") -- Group is returning to (home) zone. + self:AddTransition("Holding", "Returned", "Returned") -- Group is returned to (home) zone, e.g. when unloaded from carrier. self:AddTransition("Returning", "Returned", "Returned") -- Group is returned to (home) zone. self:AddTransition("*", "Detour", "OnDetour") -- Make a detour to a coordinate and resume route afterwards. @@ -1351,6 +1352,9 @@ function ARMYGROUP:onafterRetreat(From, Event, To, Zone, Formation) -- Set if we want to resume route after reaching the detour waypoint. wp.detour=0 + + -- Cancel all missions. + self:CancelAllMissions() end diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index 8244f404d..0b92b998a 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -6125,6 +6125,19 @@ function OPSGROUP:onbeforeDead(From, Event, To) end end +--- Cancel all missions in mission queue. +-- @param #OPSGROUP self +function OPSGROUP:CancelAllMissions() + + -- Cancel all missions. + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + self:T(self.lid.."Cancelling mission "..tostring(mission:GetName())) + self:MissionCancel(mission) + end + +end + --- On after "Dead" event. -- @param #OPSGROUP self -- @param #string From From state. @@ -8055,6 +8068,12 @@ end -- @param #OPSGROUP OpsGroupCargo Cargo OPSGROUP that was unloaded from a carrier. function OPSGROUP:onafterUnloaded(From, Event, To, OpsGroupCargo) self:T(self.lid..string.format("Unloaded OPSGROUP %s", OpsGroupCargo:GetName())) + + if OpsGroupCargo.legion and OpsGroupCargo:IsInZone(OpsGroupCargo.legion.spawnzone) then + self:T(self.lid..string.format("Unloaded group %s returned to legion", OpsGroupCargo:GetName())) + OpsGroupCargo:Returned() + end + end @@ -8592,18 +8611,24 @@ function OPSGROUP:_CheckGroupDone(delay) return end - -- Group is returning + -- Group is returning. if self:IsReturning() then self:T(self.lid.."Returning! Group NOT done...") return end - -- Group is returning + -- Group is rearming. if self:IsRearming() then self:T(self.lid.."Rearming! Group NOT done...") return end + -- Group is retreating. + if self:IsRetreating() then + self:T(self.lid.."Retreating! Group NOT done...") + return + end + -- Group is waiting. We deny all updates. if self:IsWaiting() then -- If group is waiting, we assume that is the way it is meant to be. From 229868bb20568e036024b4f83ba95e16d11ad509 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 14 Mar 2022 08:32:09 +0100 Subject: [PATCH 16/26] OPS AUFTRAG: Trooptransport set pickup radius to 100 meters. OPSGROUP: enabled pickup radius for trooptransport auftrag ARMYGOUP: removed GetPathOnRoad as it seems unncessary --- Moose Development/Moose/Ops/ArmyGroup.lua | 36 ++++++++++++++--------- Moose Development/Moose/Ops/Auftrag.lua | 4 +-- Moose Development/Moose/Ops/OpsGroup.lua | 13 +++++++- 3 files changed, 36 insertions(+), 17 deletions(-) diff --git a/Moose Development/Moose/Ops/ArmyGroup.lua b/Moose Development/Moose/Ops/ArmyGroup.lua index 09cb38bcc..542231012 100644 --- a/Moose Development/Moose/Ops/ArmyGroup.lua +++ b/Moose Development/Moose/Ops/ArmyGroup.lua @@ -982,6 +982,13 @@ function ARMYGROUP:onafterUpdateRoute(From, Event, To, n, N, Speed, Formation) -- Insert a point on road. if wp.action==ENUMS.Formation.Vehicle.OnRoad and (wp.coordinate or wp.roadcoord) then + + current=self:GetClosestRoad():WaypointGround(UTILS.MpsToKmph(self.speedWp), ENUMS.Formation.Vehicle.OnRoad) + table.insert(waypoints, 2, current) + + -- Removing this for now as I don't see why it is necessary and it is very CPU intensive. + -- You only need the start and end waypoint on the road. Other waypoints on the road are not necessray. + --[[ -- take direct line if on road is too long local wptable,length,valid=self:GetCoordinate():GetPathOnRoad(wp.coordinate or wp.roadcoord,true,false,false,false) or {} @@ -1003,7 +1010,8 @@ function ARMYGROUP:onafterUpdateRoute(From, Event, To, n, N, Speed, Formation) else current=self:GetClosestRoad():WaypointGround(UTILS.MpsToKmph(self.speedWp), ENUMS.Formation.Vehicle.OnRoad) table.insert(waypoints, count, current) - end + end + ]] end -- Debug output. @@ -1106,16 +1114,6 @@ end -- @param #string To To state. function ARMYGROUP:onafterOutOfAmmo(From, Event, To) self:T(self.lid..string.format("Group is out of ammo at t=%.3f", timer.getTime())) - - -- Get current task. - local task=self:GetTaskCurrent() - - if task then - if task.dcstask.id=="FireAtPoint" or task.dcstask.id==AUFTRAG.SpecialTask.BARRAGE then - self:T(self.lid..string.format("Cancelling current %s task because out of ammo!", task.dcstask.id)) - self:TaskCancel(task) - end - end -- Fist, check if we want to rearm once out-of-ammo. --TODO: IsMobile() check @@ -1139,6 +1137,16 @@ function ARMYGROUP:onafterOutOfAmmo(From, Event, To) if self.rtzOnOutOfAmmo then self:__RTZ(-1) end + + -- Get current task. + local task=self:GetTaskCurrent() + + if task then + if task.dcstask.id=="FireAtPoint" or task.dcstask.id==AUFTRAG.SpecialTask.BARRAGE then + self:T(self.lid..string.format("Cancelling current %s task because out of ammo!", task.dcstask.id)) + self:TaskCancel(task) + end + end end @@ -1224,9 +1232,6 @@ end -- @param Core.Zone#ZONE Zone The zone to return to. -- @param #number Formation Formation of the group. function ARMYGROUP:onafterRTZ(From, Event, To, Zone, Formation) - - -- ID of current waypoint. - local uid=self:GetWaypointCurrent().uid -- Zone. local zone=Zone or self.homezone @@ -1241,6 +1246,9 @@ function ARMYGROUP:onafterRTZ(From, Event, To, Zone, Formation) self:T(self.lid..string.format("RTZ to Zone %s", zone:GetName())) local Coordinate=zone:GetRandomCoordinate() + + -- ID of current waypoint. + local uid=self:GetWaypointCurrentUID() -- Add waypoint after current. local wp=self:AddWaypoint(Coordinate, nil, uid, Formation, true) diff --git a/Moose Development/Moose/Ops/Auftrag.lua b/Moose Development/Moose/Ops/Auftrag.lua index c50df7056..9da1d8b41 100644 --- a/Moose Development/Moose/Ops/Auftrag.lua +++ b/Moose Development/Moose/Ops/Auftrag.lua @@ -1534,7 +1534,7 @@ end -- @param Core.Set#SET_GROUP TransportGroupSet The set group(s) to be transported. -- @param Core.Point#COORDINATE DropoffCoordinate Coordinate where the helo will land drop off the the troops. -- @param Core.Point#COORDINATE PickupCoordinate Coordinate where the helo will land to pick up the the cargo. Default is the fist transport group. --- @param #number PickupRadius Radius around the pickup coordinate in meters. Default 500 m. +-- @param #number PickupRadius Radius around the pickup coordinate in meters. Default 100 m. -- @return #AUFTRAG self function AUFTRAG:NewTROOPTRANSPORT(TransportGroupSet, DropoffCoordinate, PickupCoordinate, PickupRadius) @@ -1555,7 +1555,7 @@ function AUFTRAG:NewTROOPTRANSPORT(TransportGroupSet, DropoffCoordinate, PickupC mission.transportPickup=PickupCoordinate or mission:GetTargetCoordinate() mission.transportDropoff=DropoffCoordinate - mission.transportPickupRadius=PickupRadius or 500 + mission.transportPickupRadius=PickupRadius or 100 mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.TROOPTRANSPORT) diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index 0b92b998a..acffca586 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -2627,6 +2627,17 @@ function OPSGROUP:GetWaypointCurrent() return self.waypoints[self.currentwp] end +--- Get current waypoint UID. +-- @param #OPSGROUP self +-- @return #number Current waypoint UID. +function OPSGROUP:GetWaypointCurrentUID() + local wp=self:GetWaypointCurrent() + if wp then + return wp.uid + end + return nil +end + --- Get coordinate of next waypoint of the group. -- @param #OPSGROUP self -- @param #boolean cyclic If true, return first waypoint if last waypoint was reached. @@ -4591,7 +4602,7 @@ function OPSGROUP:RouteToMission(mission, delay) -- Create a pickup zone around the pickup coordinate. The troops will go to a random point inside the zone. -- This is necessary so the helos do not try to land at the exact same location where the troops wait. - local pradius=500 + local pradius=mission.transportPickupRadius local pickupZone=ZONE_RADIUS:New("Pickup Zone", mission.transportPickup:GetVec2(), pradius) -- Add task to embark for the troops. From e1ab6b6c937f0e572e9e2b6a05843f0f2a89cfcd Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Mon, 14 Mar 2022 09:12:06 +0100 Subject: [PATCH 17/26] CSAR - remove timer check for "open the door" message to make behaviour more realistic --- Moose Development/Moose/Ops/CSAR.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Ops/CSAR.lua b/Moose Development/Moose/Ops/CSAR.lua index f8998b328..de2c30b5f 100644 --- a/Moose Development/Moose/Ops/CSAR.lua +++ b/Moose Development/Moose/Ops/CSAR.lua @@ -258,7 +258,7 @@ CSAR.AircraftType["UH-60L"] = 10 --- CSAR class version. -- @field #string version -CSAR.version="1.0.4a" +CSAR.version="1.0.4c" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list @@ -1332,7 +1332,8 @@ function CSAR:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedG _time = self.landedStatus[_lookupKeyHeli] - 10 self.landedStatus[_lookupKeyHeli] = _time end - if _time <= 0 or _distance < self.loadDistance then + --if _time <= 0 or _distance < self.loadDistance then + if _distance < self.loadDistance + 5 or _distance <= 13 then if self.pilotmustopendoors and not self:_IsLoadingDoorOpen(_heliName) then self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true) return true From 2808a9dcc56d0e6f069d0a1bd9a81fd645fa9ae6 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Fri, 18 Mar 2022 07:41:10 +0100 Subject: [PATCH 18/26] CONTROLLABLE - added SetSpeed() and SetAltitude() from latest release --- .../Moose/Wrapper/Controllable.lua | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 35ede51b9..1bc571be9 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -3844,3 +3844,45 @@ function POSITIONABLE:IsSubmarine() return nil end + + +--- Sets the controlled group to go the specified speed in meters per second. +-- @param #CONTROLLABLE self +-- @param #number Speed Speed in meters per second +-- @param #boolean Keep (Optional) When set to true will maintain that speed on passing waypoints. If no present or false the controlled group will return to the speed as defined by their route. +-- @return #CONTROLLABLE self +function CONTROLLABLE:SetSpeed(Speed, Keep) + self:F2( { self.ControllableName } ) + -- Set default if not specified. + local speed = Speed or 5 + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + if Controller then + Controller:setSpeed(speed, Keep) + end + end + return self +end + +--- [AIR] Sets the controlled aircraft group to the specified altitude in meters. +-- @param #CONTROLLABLE self +-- @param #number Altitude Altitude in meters +-- @param #boolean Keep (Optional) When set to true will maintain that altitude on passing waypoints. If no present or false the controlled group will return to the altitude as defined by their route. +-- @param #string AltType (Optional) Will specify the altitude type used. If nil the altitude type of the current waypoint will be used. Accepted values are "BARO" and "RADIO". +-- @return #CONTROLLABLE self +function CONTROLLABLE:SetAltitude(Altitude, Keep, AltType) + self:F2( { self.ControllableName } ) + -- Set default if not specified. + local altitude = Altitude or 1000 + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + if Controller then + if self:IsAir() then + Controller:setAltitude(altitude, Keep, AltType) + end + end + end + return self +end \ No newline at end of file From c1ffa47e9d81a0920357f6a3839a7495af61f68c Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Fri, 18 Mar 2022 07:59:49 +0100 Subject: [PATCH 19/26] changed descriptions --- Moose Development/Moose/Wrapper/Controllable.lua | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 1bc571be9..b27f74903 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -3846,10 +3846,10 @@ function POSITIONABLE:IsSubmarine() end ---- Sets the controlled group to go the specified speed in meters per second. +--- Sets the controlled group to go at the specified speed in meters per second. -- @param #CONTROLLABLE self -- @param #number Speed Speed in meters per second --- @param #boolean Keep (Optional) When set to true will maintain that speed on passing waypoints. If no present or false the controlled group will return to the speed as defined by their route. +-- @param #boolean Keep (Optional) When set to true, will maintain the speed on passing waypoints. If not present or false, the controlled group will return to the speed as defined by their route. -- @return #CONTROLLABLE self function CONTROLLABLE:SetSpeed(Speed, Keep) self:F2( { self.ControllableName } ) @@ -3865,11 +3865,11 @@ function CONTROLLABLE:SetSpeed(Speed, Keep) return self end ---- [AIR] Sets the controlled aircraft group to the specified altitude in meters. +--- [AIR] Sets the controlled aircraft group to fly at the specified altitude in meters. -- @param #CONTROLLABLE self --- @param #number Altitude Altitude in meters --- @param #boolean Keep (Optional) When set to true will maintain that altitude on passing waypoints. If no present or false the controlled group will return to the altitude as defined by their route. --- @param #string AltType (Optional) Will specify the altitude type used. If nil the altitude type of the current waypoint will be used. Accepted values are "BARO" and "RADIO". +-- @param #number Altitude Altitude in meters. +-- @param #boolean Keep (Optional) When set to true, will maintain the altitude on passing waypoints. If not present or false, the controlled group will return to the altitude as defined by their route. +-- @param #string AltType (Optional) Specifies the altitude type used. If nil, the altitude type of the current waypoint will be used. Accepted values are "BARO" and "RADIO". -- @return #CONTROLLABLE self function CONTROLLABLE:SetAltitude(Altitude, Keep, AltType) self:F2( { self.ControllableName } ) From 5192c188f4da6be2499829414a52da83c6b78711 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Fri, 18 Mar 2022 09:49:06 +0100 Subject: [PATCH 20/26] AIRBASE - added 10 new AB names in Syria --- Moose Development/Moose/Wrapper/Airbase.lua | 22 +++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index c2423fce6..59ffbad8d 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -385,6 +385,16 @@ AIRBASE.TheChannel = { -- * AIRBASE.Syria.Beirut_Rafic_Hariri -- * AIRBASE.Syria.An_Nasiriyah -- * AIRBASE.Syria.Abu_al_Duhur +-- * AIRBASE.Syria.At_Tanf +-- * AIRBASE.Syria.H3 +-- * AIRBASE.Syria.H3_Northwest +-- * AIRBASE.Syria.H3_Southwest +-- * AIRBASE.Syria.Kharab_Ishk +-- * AIRBASE.Syria.Raj_al_Issa_East +-- * AIRBASE.Syria.Raj_al_Issa_West +-- * AIRBASE.Syria.Ruwayshid +-- * AIRBASE.Syria.Sanliurfa +-- * AIRBASE.Syria.Tal_Siman -- --@field Syria AIRBASE.Syria={ @@ -440,10 +450,18 @@ AIRBASE.Syria={ ["Beirut_Rafic_Hariri"]="Beirut-Rafic Hariri", ["An_Nasiriyah"]="An Nasiriyah", ["Abu_al_Duhur"]="Abu al-Duhur", + ["At_Tanf"]="At Tanf", + ["H3"]="H3", + ["H3_Northwest"]="H3 Northwest", + ["H3_Southwest"]="H3 Southwest", + ["Kharab_Ishk"]="Kharab Ishk", + ["Raj_al_Issa_East"]="Raj al Issa East", + ["Raj_al_Issa_West"]="Raj al Issa West", + ["Ruwayshid"]="Ruwayshid", + ["Sanliurfa"]="Sanliurfa", + ["Tal_Siman"]="Tal Siman", } - - --- Airbases of the Mariana Islands map: -- -- * AIRBASE.MarianaIslands.Rota_Intl From 3ea1881ff577248c3a00190b821528fa0c865433 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 20 Mar 2022 22:20:04 +0100 Subject: [PATCH 21/26] OPS INTEL v0.3.0 - Added option to detect statics via `INTEL:SetDetectStatics` function. - Added `INTEL:KnowObject` function to make intel aware of GROUPs or STATICs. - Improved cluster analysis. - Changed `NewCluster` event: removed contact as first parameter. --- Moose Development/Moose/Ops/ArmyGroup.lua | 2 +- Moose Development/Moose/Ops/Intelligence.lua | 970 ++++++++++++++----- Moose Development/Moose/Wrapper/Group.lua | 15 +- Moose Development/Moose/Wrapper/Marker.lua | 4 +- 4 files changed, 759 insertions(+), 232 deletions(-) diff --git a/Moose Development/Moose/Ops/ArmyGroup.lua b/Moose Development/Moose/Ops/ArmyGroup.lua index 542231012..c08870400 100644 --- a/Moose Development/Moose/Ops/ArmyGroup.lua +++ b/Moose Development/Moose/Ops/ArmyGroup.lua @@ -46,7 +46,7 @@ -- -- # The ARMYGROUP Concept -- --- This class enhances naval groups. +-- This class enhances ground groups. -- -- @field #ARMYGROUP ARMYGROUP = { diff --git a/Moose Development/Moose/Ops/Intelligence.lua b/Moose Development/Moose/Ops/Intelligence.lua index 1eaf89c3b..60329ee5f 100644 --- a/Moose Development/Moose/Ops/Intelligence.lua +++ b/Moose Development/Moose/Ops/Intelligence.lua @@ -34,16 +34,16 @@ -- @field #boolean clustermarkers If true, create cluster markers on F10 map. -- @field #number clustercounter Running number of clusters. -- @field #number dTforget Time interval in seconds before a known contact which is not detected any more is forgotten. --- @field #number clusterradius Radius im kilometers in which groups/units are considered to belong to a cluster +-- @field #number clusterradius Radius in meters in which groups/units are considered to belong to a cluster. -- @field #number prediction Seconds default to be used with CalcClusterFuturePosition. +-- @field #boolean detectStatics If `true`, detect STATIC objects. Default `false`. +-- @field #number statusupdate Time interval in seconds after which the status is refreshed. Default 60 sec. Should be negative. -- @extends Core.Fsm#FSM --- Top Secret! -- -- === -- --- ![Banner Image](..\Presentations\CarrierAirWing\INTEL_Main.jpg) --- -- # The INTEL Concept -- -- * Lightweight replacement for @{Functional.Detection#DETECTION} @@ -54,32 +54,32 @@ -- -- # Basic Usage -- --- ## set up a detection SET_GROUP +-- ## Set up a detection SET_GROUP -- --- `Red_DetectionSetGroup = SET_GROUP:New()` --- `Red_DetectionSetGroup:FilterPrefixes( { "Red EWR" } )` --- `Red_DetectionSetGroup:FilterOnce()` +-- Red_DetectionSetGroup = SET_GROUP:New() +-- Red_DetectionSetGroup:FilterPrefixes( { "Red EWR" } ) +-- Red_DetectionSetGroup:FilterOnce() -- --- ## New Intel type detection for the red side, logname "KGB" +-- ## New Intel type detection for the red side, logname "KGB" -- --- `RedIntel = INTEL:New(Red_DetectionSetGroup,"red","KGB")` --- `RedIntel:SetClusterAnalysis(true,true)` --- `RedIntel:SetVerbosity(2)` --- `RedIntel:__Start(2)` +-- RedIntel = INTEL:New(Red_DetectionSetGroup, "red", "KGB") +-- RedIntel:SetClusterAnalysis(true, true) +-- RedIntel:SetVerbosity(2) +-- RedIntel:__Start(2) -- --- ## Hook into new contacts found +-- ## Hook into new contacts found -- --- `function RedIntel:OnAfterNewContact(From, Event, To, Contact)` --- `local text = string.format("NEW contact %s detected by %s", Contact.groupname, Contact.recce or "unknown")` --- `local m = MESSAGE:New(text,15,"KGB"):ToAll()` --- `end` +-- function RedIntel:OnAfterNewContact(From, Event, To, Contact) +-- local text = string.format("NEW contact %s detected by %s", Contact.groupname, Contact.recce or "unknown") +-- MESSAGE:New(text, 15, "KGB"):ToAll() +-- end -- -- ## And/or new clusters found -- --- `function RedIntel:OnAfterNewCluster(From, Event, To, Contact, Cluster)` --- `local text = string.format("NEW cluster %d size %d with contact %s", Cluster.index, Cluster.size, Contact.groupname)` --- `local m = MESSAGE:New(text,15,"KGB"):ToAll()` --- `end` +-- function RedIntel:OnAfterNewCluster(From, Event, To, Cluster) +-- local text = string.format("NEW cluster #%d of size %d", Cluster.index, Cluster.size) +-- MESSAGE:New(text,15,"KGB"):ToAll() +-- end -- -- -- @field #INTEL @@ -95,10 +95,11 @@ INTEL = { ContactsUnknown = {}, Clusters = {}, clustercounter = 1, - clusterradius = 15, + clusterradius = 15000, clusteranalysis = true, clustermarkers = false, prediction = 300, + detectStatics = false, } --- Detected item info. @@ -114,12 +115,14 @@ INTEL = { -- @field Core.Point#COORDINATE position Last known position of the item. -- @field DCS#Vec3 velocity 3D velocity vector. Components x,y and z in m/s. -- @field #number speed Last known speed in m/s. --- @field #boolean isship --- @field #boolean ishelo --- @field #boolean isground +-- @field #boolean isship If `true`, contact is a naval group. +-- @field #boolean ishelo If `true`, contact is a helo group. +-- @field #boolean isground If `true`, contact is a ground group. +-- @field #boolean isStatic If `true`, contact is a STATIC object. -- @field Ops.Auftrag#AUFTRAG mission The current Auftrag attached to this contact. -- @field Ops.Target#TARGET target The Target attached to this contact. -- @field #string recce The name of the recce unit that detected this contact. +-- @field #string ctype Contact type. --- Cluster info. -- @type INTEL.Cluster @@ -131,19 +134,36 @@ INTEL = { -- @field #number threatlevelAve Average of threat levels. -- @field Core.Point#COORDINATE coordinate Coordinate of the cluster. -- @field Wrapper.Marker#MARKER marker F10 marker. --- @field Ops.Auftrag#AUFTRAG mission The current Auftrag attached to this cluster +-- @field #number markerID Marker ID. +-- @field Ops.Auftrag#AUFTRAG mission The current Auftrag attached to this cluster. +-- @field #string ctype Cluster type. +--- Contact or cluster type. +-- @type INTEL.Ctype +-- @field #string GROUND Ground. +-- @field #string NAVAL Ship. +-- @field #string AIRCRAFT Airpane or helicopter. +-- @field #string STRUCTURE Static structure. +INTEL.Ctype={ + GROUND="Ground", + NAVAL="Naval", + AIRCRAFT="Aircraft", + STRUCTURE="Structure" +} --- INTEL class version. -- @field #string version -INTEL.version="0.2.7" +INTEL.version="0.3.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- DONE: Filter detection methods. +-- TODO: Make forget times user inpupt. Currently these are hard coded. +-- TODO: Add min cluster size. Only create new clusters if they have a certain group size. -- TODO: process detected set asynchroniously for better performance. +-- DONE: Add statics. +-- DONE: Filter detection methods. -- DONE: Accept zones. -- DONE: Reject zones. -- NOGO: SetAttributeZone --> return groups of generalized attributes in a zone. @@ -233,7 +253,7 @@ function INTEL:New(DetectionSet, Coalition, Alias) self:AddTransition("*", "LostContact", "*") -- Contact could not be detected any more. self:AddTransition("*", "NewCluster", "*") -- New cluster has been detected. - self:AddTransition("*", "LostCluster", "*") -- Cluster could not be detected any more. + self:AddTransition("*", "LostCluster", "*") -- Cluster could not be detected any more. -- Defaults @@ -254,6 +274,7 @@ function INTEL:New(DetectionSet, Coalition, Alias) -- @param #INTEL self -- @param #number delay Delay in seconds. + --- Triggers the FSM event "Stop". Stops the INTEL and all its event handlers. -- @param #INTEL self @@ -262,6 +283,7 @@ function INTEL:New(DetectionSet, Coalition, Alias) -- @param #INTEL self -- @param #number delay Delay in seconds. + --- Triggers the FSM event "Status". -- @function [parent=#INTEL] Status -- @param #INTEL self @@ -271,6 +293,18 @@ function INTEL:New(DetectionSet, Coalition, Alias) -- @param #INTEL self -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "NewContact". + -- @function [parent=#INTEL] NewContact + -- @param #INTEL self + -- @param #INTEL.Contact Contact Detected contact. + + --- Triggers the FSM event "NewContact" after a delay. + -- @function [parent=#INTEL] NewContact + -- @param #INTEL self + -- @param #number delay Delay in seconds. + -- @param #INTEL.Contact Contact Detected contact. + --- On After "NewContact" event. -- @function [parent=#INTEL] OnAfterNewContact -- @param #INTEL self @@ -279,6 +313,18 @@ function INTEL:New(DetectionSet, Coalition, Alias) -- @param #string To To state. -- @param #INTEL.Contact Contact Detected contact. + + --- Triggers the FSM event "LostContact". + -- @function [parent=#INTEL] LostContact + -- @param #INTEL self + -- @param #INTEL.Contact Contact Lost contact. + + --- Triggers the FSM event "LostContact" after a delay. + -- @function [parent=#INTEL] LostContact + -- @param #INTEL self + -- @param #number delay Delay in seconds. + -- @param #INTEL.Contact Contact Lost contact. + --- On After "LostContact" event. -- @function [parent=#INTEL] OnAfterLostContact -- @param #INTEL self @@ -287,14 +333,39 @@ function INTEL:New(DetectionSet, Coalition, Alias) -- @param #string To To state. -- @param #INTEL.Contact Contact Lost contact. + + --- Triggers the FSM event "NewCluster". + -- @function [parent=#INTEL] NewCluster + -- @param #INTEL self + -- @param #INTEL.Cluster Cluster Detected cluster. + + --- Triggers the FSM event "NewCluster" after a delay. + -- @function [parent=#INTEL] NewCluster + -- @param #INTEL self + -- @param #number delay Delay in seconds. + -- @param #INTEL.Cluster Cluster Detected cluster. + --- On After "NewCluster" event. -- @function [parent=#INTEL] OnAfterNewCluster -- @param #INTEL self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. - -- @param #INTEL.Contact Contact Detected contact. - -- @param #INTEL.Cluster Cluster Detected cluster + -- @param #INTEL.Cluster Cluster Detected cluster. + + + --- Triggers the FSM event "LostCluster". + -- @function [parent=#INTEL] LostCluster + -- @param #INTEL self + -- @param #INTEL.Cluster Cluster Lost cluster. + -- @param Ops.Auftrag#AUFTRAG Mission The Auftrag connected with this cluster or `nil`. + + --- Triggers the FSM event "LostCluster" after a delay. + -- @function [parent=#INTEL] LostCluster + -- @param #INTEL self + -- @param #number delay Delay in seconds. + -- @param #INTEL.Cluster Cluster Lost cluster. + -- @param Ops.Auftrag#AUFTRAG Mission The Auftrag connected with this cluster or `nil`. --- On After "LostCluster" event. -- @function [parent=#INTEL] OnAfterLostCluster @@ -302,8 +373,8 @@ function INTEL:New(DetectionSet, Coalition, Alias) -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. - -- @param #INTEL.Cluster Cluster Lost cluster - -- @param Ops.Auftrag#AUFTRAG Mission The Auftrag connected with this cluster or nil + -- @param #INTEL.Cluster Cluster Lost cluster. + -- @param Ops.Auftrag#AUFTRAG Mission The Auftrag connected with this cluster or `nil`. return self end @@ -367,14 +438,13 @@ function INTEL:RemoveRejectZone(RejectZone) return self end ---- Set forget contacts time interval. +--- **OBSOLETE, will be removed in next version!** Set forget contacts time interval. -- Previously known contacts that are not detected any more, are "lost" after this time. -- This avoids fast oscillations between a contact being detected and undetected. -- @param #INTEL self -- @param #number TimeInterval Time interval in seconds. Default is 120 sec. -- @return #INTEL self function INTEL:SetForgetTime(TimeInterval) - self.dTforget=TimeInterval or 120 return self end @@ -444,6 +514,18 @@ function INTEL:SetClusterAnalysis(Switch, Markers) return self end +--- Set whether STATIC objects are detected. +-- @param #INTEL self +-- @param #boolean Switch If `true`, statics are detected. +-- @return #INTEL self +function INTEL:SetDetectStatics(Switch) + if Switch and Switch==true then + self.detectStatics=true + else + self.detectStatics=false + end +end + --- Set verbosity level for debugging. -- @param #INTEL self -- @param #number Verbosity The higher, the noisier, e.g. 0=off, 2=debug @@ -477,13 +559,12 @@ function INTEL:AddMissionToCluster(Cluster, Mission) return self end ---- Change radius of the Clusters +--- Change radius of the Clusters. -- @param #INTEL self --- @param #number radius The radius of the clusters +-- @param #number radius The radius of the clusters in kilometers. Default 15 km. -- @return #INTEL self function INTEL:SetClusterRadius(radius) - local radius = radius or 15 - self.clusterradius = radius + self.clusterradius = (radius or 15)*1000 return self end @@ -528,6 +609,23 @@ function INTEL:GetClusterTable() end end +--- Get name of a contact. +-- @param #INTEL self +-- @param #INTEL.Contact Contact The contact. +-- @return #string Name of the contact. +function INTEL:GetContactName(Contact) + return Contact.groupname +end + +--- Get category name of a contact. +-- @param #INTEL self +-- @param #INTEL.Contact Contact The contact. +-- @return #string Category name. +function INTEL:GetContactCategoryName(Contact) + return Contact.categoryname +end + + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Start & Status ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -580,7 +678,7 @@ function INTEL:onafterStatus(From, Event, To) for _,_contact in pairs(self.Contacts) do local contact=_contact --#INTEL.Contact local dT=timer.getAbsTime()-contact.Tdetected - text=text..string.format("\n- %s (%s): %s, units=%d, T=%d sec", contact.categoryname, contact.attribute, contact.groupname, contact.group:CountAliveUnits(), dT) + text=text..string.format("\n- %s (%s): %s, units=%d, T=%d sec", contact.categoryname, contact.attribute, contact.groupname, contact.isStatic and 1 or contact.group:CountAliveUnits(), dT) if contact.mission then local mission=contact.mission --Ops.Auftrag#AUFTRAG text=text..string.format(" mission name=%s type=%s target=%s", mission.name, mission.type, mission:GetTargetName() or "unknown") @@ -599,8 +697,10 @@ function INTEL:UpdateIntel() -- Set of all detected units. local DetectedUnits={} + -- Set of which units was detected by which recce local RecceDetecting = {} + -- Loop over all units providing intel. for _,_group in pairs(self.detectionset.Set or {}) do local group=_group --Wrapper.Group#GROUP @@ -681,19 +781,27 @@ function INTEL:UpdateIntel() -- Create detected groups. local DetectedGroups={} + local DetectedStatics={} local RecceGroups={} for unitname,_unit in pairs(DetectedUnits) do local unit=_unit --Wrapper.Unit#UNIT - local group=unit:GetGroup() - if group then - local groupname = group:GetName() - DetectedGroups[groupname]=group - RecceGroups[groupname]=RecceDetecting[unitname] + if unit:IsInstanceOf("UNIT") then + local group=unit:GetGroup() + if group then + local groupname = group:GetName() + DetectedGroups[groupname]=group + RecceGroups[groupname]=RecceDetecting[unitname] + end + else + if self.detectStatics then + DetectedStatics[unitname]=unit + RecceGroups[unitname]=RecceDetecting[unitname] + end end end -- Create detected contacts. - self:CreateDetectedItems(DetectedGroups, RecceGroups) + self:CreateDetectedItems(DetectedGroups, DetectedStatics, RecceGroups) -- Paint a picture of the battlefield. if self.clusteranalysis then @@ -702,48 +810,50 @@ function INTEL:UpdateIntel() end - - - - ---- Create detected items. +--- Update an #INTEL.Contact item. -- @param #INTEL self --- @param #table DetectedGroups Table of detected Groups --- @param #table RecceDetecting Table of detecting recce names -function INTEL:CreateDetectedItems(DetectedGroups, RecceDetecting) - self:F({RecceDetecting=RecceDetecting}) +-- @param #INTEL.Contact Contact Contact. +-- @return #INTEL.Contact The contact. +function INTEL:_UpdateContact(Contact) - -- Current time. - local Tnow=timer.getAbsTime() + if Contact.isStatic then + + -- Statics don't need to be updated. + + else + + if Contact.group and Contact.group:IsAlive() then - for groupname,_group in pairs(DetectedGroups) do - local group=_group --Wrapper.Group#GROUP + Contact.Tdetected=timer.getAbsTime() + Contact.position=Contact.group:GetCoordinate() + Contact.velocity=Contact.group:GetVelocityVec3() + Contact.speed=Contact.group:GetVelocityMPS() + + end + + end +end - -- Get contact if already known. - local detecteditem=self:GetContactByName(groupname) +--- Create an #INTEL.Contact item from a given GROUP or STATIC object. +-- @param #INTEL self +-- @param Wrapper.Positionable#POSITIONABLE Positionable The GROUP or STATIC object. +-- @param #string RecceName The name of the recce group that has detected this contact. +-- @return #INTEL.Contact The contact. +function INTEL:_CreateContact(Positionable, RecceName) - if detecteditem then - --- - -- Detected item already exists ==> Update data. - --- + if Positionable and Positionable:IsAlive() then - detecteditem.Tdetected=Tnow - detecteditem.position=group:GetCoordinate() - detecteditem.velocity=group:GetVelocityVec3() - detecteditem.speed=group:GetVelocityMPS() - - else - --- - -- Detected item does not exist in our list yet. - --- - - -- Create new contact. - local item={} --#INTEL.Contact - - item.groupname=groupname + -- Create new contact. + local item={} --#INTEL.Contact + + if Positionable:IsInstanceOf("GROUP") then + + local group=Positionable --Wrapper.Group#GROUP + + item.groupname=group:GetName() item.group=group - item.Tdetected=Tnow + item.Tdetected=timer.getAbsTime() item.typename=group:GetTypeName() item.attribute=group:GetAttribute() item.category=group:GetCategory() @@ -752,17 +862,78 @@ function INTEL:CreateDetectedItems(DetectedGroups, RecceDetecting) item.position=group:GetCoordinate() item.velocity=group:GetVelocityVec3() item.speed=group:GetVelocityMPS() - item.recce=RecceDetecting[groupname] + item.recce=RecceName item.isground = group:IsGround() or false item.isship = group:IsShip() or false - self:T(string.format("%s group detect by %s/%s", groupname, RecceDetecting[groupname] or "unknown", item.recce or "unknown")) - - -- Add contact to table. - self:AddContact(item) - - -- Trigger new contact event. - self:NewContact(item) + item.isStatic=false + + if item.category==Group.Category.AIRPLANE or item.category==Group.Category.HELICOPTER then + item.ctype=INTEL.Ctype.AIRCRAFT + elseif item.category==Group.Category.GROUND or item.category==Group.Category.TRAIN then + item.ctype=INTEL.Ctype.GROUND + elseif item.category==Group.Category.SHIP then + item.ctype=INTEL.Ctype.NAVAL + end + + return item + + elseif Positionable:IsInstanceOf("STATIC") then + + local static=Positionable --Wrapper.Static#STATIC + + item.groupname=static:GetName() + item.group=static + item.Tdetected=timer.getAbsTime() + item.typename=static:GetTypeName() or "Unknown" + item.attribute="Static" + item.category=3 --static:GetCategory() + item.categoryname=static:GetCategoryName() or "Unknown" + item.threatlevel=static:GetThreatLevel() or 0 + item.position=static:GetCoordinate() + item.velocity=static:GetVelocityVec3() + item.speed=0 + item.recce=RecceName + item.isground = true + item.isship = false + item.isStatic=true + item.ctype=INTEL.Ctype.STRUCTURE + + return item + else + self:E(self.lid..string.format("ERROR: object needs to be a GROUP or STATIC!")) end + + end + + return nil +end + +--- Create detected items. +-- @param #INTEL self +-- @param #table DetectedGroups Table of detected Groups. +-- @param #table DetectedStatics Table of detected Statics. +-- @param #table RecceDetecting Table of detecting recce names. +function INTEL:CreateDetectedItems(DetectedGroups, DetectedStatics, RecceDetecting) + self:F({RecceDetecting=RecceDetecting}) + + -- Current time. + local Tnow=timer.getAbsTime() + + -- Loop over groups. + for groupname,_group in pairs(DetectedGroups) do + local group=_group --Wrapper.Group#GROUP + + -- Create or update contact for this group. + self:KnowObject(group, RecceDetecting[groupname]) + + end + + -- Loop over statics. + for staticname,_static in pairs(DetectedStatics) do + local static=_static --Wrapper.Static#STATIC + + -- Create or update contact for this group. + self:KnowObject(static, RecceDetecting[staticname]) end @@ -789,8 +960,8 @@ end -- If no detection method is given, the detection will use all the available methods by default. -- @param #INTEL self -- @param Wrapper.Unit#UNIT Unit The unit detecting. --- @param #table DetectedUnits Table of detected units to be filled --- @param #table RecceDetecting Table of recce per unit to be filled +-- @param #table DetectedUnits Table of detected units to be filled. +-- @param #table RecceDetecting Table of recce per unit to be filled. -- @param #boolean DetectVisual (Optional) If *false*, do not include visually detected targets. -- @param #boolean DetectOptical (Optional) If *false*, do not include optically detected targets. -- @param #boolean DetectRadar (Optional) If *false*, do not include targets detected by radar. @@ -824,6 +995,13 @@ function INTEL:GetDetectedUnits(Unit, DetectedUnits, RecceDetecting, DetectVisua DetectedUnits[name]=unit RecceDetecting[name]=reccename self:T(string.format("Unit %s detect by %s", name, reccename)) + else + local static=STATIC:FindByName(name, false) + if static then + --env.info("FF found static "..name) + DetectedUnits[name]=static + RecceDetecting[name]=reccename + end end else @@ -846,8 +1024,13 @@ end -- @param #string To To state. -- @param #INTEL.Contact Contact Detected contact. function INTEL:onafterNewContact(From, Event, To, Contact) + + -- Debug text. self:F(self.lid..string.format("NEW contact %s", Contact.groupname)) + + -- Add to table of unknown contacts. table.insert(self.ContactsUnknown, Contact) + end --- On after "LostContact" event. @@ -855,10 +1038,15 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #INTEL.Contact Contact Detected contact. +-- @param #INTEL.Contact Contact Lost contact. function INTEL:onafterLostContact(From, Event, To, Contact) + + -- Debug text. self:F(self.lid..string.format("LOST contact %s", Contact.groupname)) + + -- Add to table of lost contacts. table.insert(self.ContactsLost, Contact) + end --- On after "NewCluster" event. @@ -866,10 +1054,15 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #INTEL.Contact Contact Detected contact. --- @param #INTEL.Cluster Cluster Detected cluster -function INTEL:onafterNewCluster(From, Event, To, Contact, Cluster) - self:F(self.lid..string.format("NEW cluster %d size %d with contact %s", Cluster.index, Cluster.size, Contact.groupname)) +-- @param #INTEL.Cluster Cluster Detected cluster. +function INTEL:onafterNewCluster(From, Event, To, Cluster) + + -- Debug text. + self:F(self.lid..string.format("NEW cluster #%d [%s] of size %d", Cluster.index, Cluster.ctype, Cluster.size)) + + -- Add cluster to table. + self:_AddCluster(Cluster) + end --- On after "LostCluster" event. @@ -877,21 +1070,80 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #INTEL.Cluster Cluster Lost cluster --- @param Ops.Auftrag#AUFTRAG Mission The Auftrag connected with this cluster or nil +-- @param #INTEL.Cluster Cluster Lost cluster. +-- @param Ops.Auftrag#AUFTRAG Mission The Auftrag connected with this cluster or `nil`. function INTEL:onafterLostCluster(From, Event, To, Cluster, Mission) - local text = self.lid..string.format("LOST cluster %d", Cluster.index) + + -- Debug text. + local text = self.lid..string.format("LOST cluster #%d [%s]", Cluster.index, Cluster.ctype) + if Mission then local mission=Mission --Ops.Auftrag#AUFTRAG text=text..string.format(" mission name=%s type=%s target=%s", mission.name, mission.type, mission:GetTargetName() or "unknown") end + self:T(text) + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Misc Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Make the INTEL aware of a object that was not detected (yet). This will add the object to the contacts table and trigger a `NewContact` event. +-- @param #INTEL self +-- @param Wrapper.Positionable#POSITIONABLE Positionable Group or static object. +-- @param #string RecceName Name of the recce group that detected this object. +-- @param #number Tdetected Abs. mission time in seconds, when the object is detected. Default now. +-- @return #INTEL self +function INTEL:KnowObject(Positionable, RecceName, Tdetected) + + local Tnow=timer.getAbsTime() + Tdetected=Tdetected or Tnow + + if Positionable and Positionable:IsAlive() then + + if Tdetected>Tnow then + -- Delay call. + self:ScheduleOnce(Tdetected-Tnow, self.KnowObject, self, Positionable, RecceName) + else + + -- Name of the object. + local name=Positionable:GetName() + + -- Try to get the contact by name. + local contact=self:GetContactByName(name) + + if contact then + + -- Update contact info. + self:_UpdateContact(contact) + + else + + -- Create new contact. + contact=self:_CreateContact(Positionable, RecceName) + + if contact then + + -- Debug info. + self:T(string.format("%s contact detected by %s", contact.groupname, RecceName or "unknown")) + + -- Add contact to table. + self:AddContact(contact) + + -- Trigger new contact event. + self:NewContact(contact) + + end + + end + end + end + + return self +end + --- Get a contact by name. -- @param #INTEL self -- @param #string groupname Name of the contact group. @@ -908,11 +1160,38 @@ function INTEL:GetContactByName(groupname) return nil end +--- Check if a Contact is already known. It is checked, whether the contact is in the contacts table. +-- @param #INTEL self +-- @param #INTEL.Contact Contact The contact to be added. +-- @return #boolean If `true`, contact is already known. +function INTEL:_IsContactKnown(Contact) + + for i,_contact in pairs(self.Contacts) do + local contact=_contact --#INTEL.Contact + if contact.groupname==Contact.groupname then + return true + end + end + + return false +end + + --- Add a contact to our list. -- @param #INTEL self -- @param #INTEL.Contact Contact The contact to be added. +-- @return #INTEL self function INTEL:AddContact(Contact) - table.insert(self.Contacts, Contact) + + -- First check if the contact is already in the table. + if self:_IsContactKnown(Contact) then + self:E(self.lid..string.format("WARNING: Contact %s is already in the contact table!", tostring(Contact.groupname))) + else + self:T(self.lid..string.format("Adding new Contact %s to table", tostring(Contact.groupname))) + table.insert(self.Contacts, Contact) + end + + return self end --- Remove a contact from our list. @@ -941,11 +1220,17 @@ function INTEL:_CheckContactLost(Contact) if Contact.group==nil or not Contact.group:IsAlive() then return true end + + -- We never forget statics as they don't move. + if Contact.isStatic then + return false + end -- Time since last detected. local dT=timer.getAbsTime()-Contact.Tdetected - local dTforget=self.dTforget + local dTforget=nil + if Contact.category==Group.Category.GROUND then dTforget=60*60*2 -- 2 hours elseif Contact.category==Group.Category.AIRPLANE then @@ -973,131 +1258,206 @@ end --- [Internal] Paint picture of the battle field. Does Cluster analysis and updates clusters. Sets markers if markers are enabled. -- @param #INTEL self function INTEL:PaintPicture() + self:F(self.lid.."Painting Picture!") -- First remove all lost contacts from clusters. for _,_contact in pairs(self.ContactsLost) do local contact=_contact --#INTEL.Contact + + -- Get cluster this contact belongs to (if any). local cluster=self:GetClusterOfContact(contact) + if cluster then self:RemoveContactFromCluster(contact, cluster) end end - -- clean up cluster table + + -- Clean up cluster table. local ClusterSet = {} + + -- Now check if whole clusters were lost. for _i,_cluster in pairs(self.Clusters) do - if (_cluster.size > 0) and (self:ClusterCountUnits(_cluster) > 0) then + local cluster=_cluster --#INTEL.Cluster + + if cluster.size>0 and self:ClusterCountUnits(cluster)>0 then + -- This one has size>0 and units>0 table.insert(ClusterSet,_cluster) else - local mission = _cluster.mission or nil - local marker = _cluster.marker - local markerID = _cluster.markerID - if marker then - marker:Remove() + + -- This cluster is gone. + + -- Remove marker. + if cluster.marker then + cluster.marker:Remove() end - if markerID then - COORDINATE:RemoveMark(markerID) + + -- Marker of the arrow. + if cluster.markerID then + COORDINATE:RemoveMark(cluster.markerID) end - self:LostCluster(_cluster, mission) + + -- Lost cluster. + self:LostCluster(cluster, cluster.mission) end end + + -- Set Clusters. self.Clusters = ClusterSet - -- update positions + + -- Update positions. self:_UpdateClusterPositions() + for _,_contact in pairs(self.Contacts) do local contact=_contact --#INTEL.Contact + + -- Debug info. self:T(string.format("Paint Picture: checking for %s",contact.groupname)) - -- Check if this contact is in any cluster. - local isincluster=self:CheckContactInClusters(contact) -- Get the current cluster (if any) this contact belongs to. local currentcluster=self:GetClusterOfContact(contact) - if currentcluster then - --self:I(string.format("Paint Picture: %s has current cluster",contact.groupname)) + if currentcluster then --- -- Contact is currently part of a cluster. --- -- Check if the contact is still connected to the cluster. local isconnected=self:IsContactConnectedToCluster(contact, currentcluster) - - if (not isconnected) and (currentcluster.size > 1) then - --self:I(string.format("Paint Picture: %s has LOST current cluster",contact.groupname)) - local cluster=self:IsContactPartOfAnyClusters(contact) - + + if isconnected then + + else + + --- Not connected to current cluster any more. + + -- Remove from current cluster. + self:RemoveContactFromCluster(contact, currentcluster) + + -- Find new cluster. + local cluster=self:_GetClosestClusterOfContact(contact) + if cluster then + -- Add contact to cluster. self:AddContactToCluster(contact, cluster) else - local newcluster=self:CreateCluster(contact.position) - self:AddContactToCluster(contact, newcluster) - self:NewCluster(contact, newcluster) + -- Create a new cluster. + local newcluster=self:_CreateClusterFromContact(contact) + + -- Trigger new cluster event. + self:NewCluster(newcluster) end - + end - else --- -- Contact is not in any cluster yet. --- - --self:I(string.format("Paint Picture: %s has NO current cluster",contact.groupname)) - local cluster=self:IsContactPartOfAnyClusters(contact) + + -- Debug info. + self:T(self.lid..string.format("Paint Picture: contact %s has NO current cluster", contact.groupname)) + + -- Get the closest existing cluster of this contact. + local cluster=self:_GetClosestClusterOfContact(contact) if cluster then + + -- Debug info. + self:T(self.lid..string.format("Paint Picture: contact %s has closest cluster #%d",contact.groupname, cluster.index)) + + -- Add contact to this cluster. self:AddContactToCluster(contact, cluster) + else - local newcluster=self:CreateCluster(contact.position) - self:AddContactToCluster(contact, newcluster) - self:NewCluster(contact, newcluster) + -- Create a brand new cluster. + local newcluster=self:_CreateClusterFromContact(contact) + + -- Trigger event for a new cluster. + self:NewCluster(newcluster) end end end - + -- Update positions. + self:_UpdateClusterPositions() -- Update F10 marker text if cluster has changed. if self.clustermarkers then for _,_cluster in pairs(self.Clusters) do local cluster=_cluster --#INTEL.Cluster --local coordinate=self:GetClusterCoordinate(cluster) + -- Update F10 marker. + MESSAGE:New("Updating cluster marker and future position", 10):ToAll() + + -- Update cluster markers. self:UpdateClusterMarker(cluster) - self:CalcClusterFuturePosition(cluster,self.prediction) + + -- Extrapolate future position of the cluster. + self:CalcClusterFuturePosition(cluster, 300) + end end end --- Create a new cluster. -- @param #INTEL self --- @param Core.Point#COORDINATE coordinate The coordinate of the cluster. -- @return #INTEL.Cluster cluster The cluster. -function INTEL:CreateCluster(coordinate) +function INTEL:_CreateCluster() - -- Create new cluster + -- Create new cluster. local cluster={} --#INTEL.Cluster cluster.index=self.clustercounter - cluster.coordinate=coordinate + cluster.coordinate=COORDINATE:New(0, 0, 0) cluster.threatlevelSum=0 cluster.threatlevelMax=0 cluster.size=0 cluster.Contacts={} - -- Add cluster. - table.insert(self.Clusters, cluster) - -- Increase counter. self.clustercounter=self.clustercounter+1 return cluster end +--- Create a new cluster from a first contact. The contact is automatically added to the cluster. +-- @param #INTEL self +-- @param #INTEL.Contact Contact The first contact. +-- @return #INTEL.Cluster cluster The cluster. +function INTEL:_CreateClusterFromContact(Contact) + + local cluster=self:_CreateCluster() + + self:T(self.lid..string.format("Created NEW cluster #%d with first contact %s", cluster.index, Contact.groupname)) + + cluster.coordinate:UpdateFromCoordinate(Contact.position) + + cluster.ctype=Contact.ctype + + self:AddContactToCluster(Contact, cluster) + + return cluster +end + +--- Add cluster to table. +-- @param #INTEL self +-- @param #INTEL.Cluster Cluster The cluster to add. +function INTEL:_AddCluster(Cluster) + + --TODO: Check if cluster is already in the table. + + -- Add cluster. + table.insert(self.Clusters, Cluster) + +end + --- Add a contact to the cluster. -- @param #INTEL self -- @param #INTEL.Contact contact The contact. @@ -1105,13 +1465,18 @@ end function INTEL:AddContactToCluster(contact, cluster) if contact and cluster then - + -- Add neighbour to cluster contacts. table.insert(cluster.Contacts, contact) + -- Add to threat level sum. cluster.threatlevelSum=cluster.threatlevelSum+contact.threatlevel + -- Increase size. cluster.size=cluster.size+1 + + -- Debug info. + self:T(self.lid..string.format("Adding contact %s to cluster #%d [%s] ==> New size=%d", contact.groupname, cluster.index, cluster.ctype, cluster.size)) end end @@ -1124,16 +1489,23 @@ function INTEL:RemoveContactFromCluster(contact, cluster) if contact and cluster then - for i,_contact in pairs(cluster.Contacts) do - local Contact=_contact --#INTEL.Contact + for i=#cluster.Contacts,1,-1 do + local Contact=cluster.Contacts[i] --#INTEL.Contact if Contact.groupname==contact.groupname then + -- Remove threat level sum. cluster.threatlevelSum=cluster.threatlevelSum-contact.threatlevel + + -- Decrease cluster size. cluster.size=cluster.size-1 + -- Remove from table. table.remove(cluster.Contacts, i) + -- Debug info. + self:T(self.lid..string.format("Removing contact %s from cluster #%d ==> New cluster size=%d", contact.groupname, cluster.index, cluster.size)) + return end @@ -1203,14 +1575,26 @@ function INTEL:CalcClusterDirection(cluster) local direction = 0 local n=0 for _,_contact in pairs(cluster.Contacts) do - local group = _contact.group -- Wrapper.Group#GROUP - if group:IsAlive() then - direction = direction + group:GetHeading() + local contact=_contact --#INTEL.Contact + + if (not contact.isStatic) and contact.group:IsAlive() then + direction = direction + contact.group:GetHeading() n=n+1 end end - return math.floor(direction / n) - + + --TODO: This calculation is WRONG! + -- Simple example for two groups: + -- First group is going West, i.e. heading 090 + -- Second group is going East, i.e. heading 270 + -- Total is 360/2=180, i.e. South! + -- It should not go anywhere as the two movements cancel each other. + + if n==0 then + return 0 + else + return math.floor(direction / n) + end end --- Calculate cluster speed. @@ -1219,37 +1603,76 @@ end -- @return #number Speed average of all groups in the cluster in MPS. function INTEL:CalcClusterSpeed(cluster) - local velocity = 0 - local n=0 + local velocity = 0 ; local n=0 for _,_contact in pairs(cluster.Contacts) do - local group = _contact.group -- Wrapper.Group#GROUP - if group:IsAlive() then - velocity = velocity + group:GetVelocityMPS() + local contact=_contact --#INTEL.Contact + + if (not contact.isStatic) and contact.group:IsAlive() then + velocity = velocity + contact.group:GetVelocityMPS() n=n+1 end + end - return math.floor(velocity / n) + + if n==0 then + return 0 + else + return math.floor(velocity / n) + end +end +--- Calculate cluster velocity vector. +-- @param #INTEL self +-- @param #INTEL.Cluster cluster The cluster of contacts. +-- @return DCS#Vec3 Velocity vector in m/s. +function INTEL:CalcClusterVelocityVec3(cluster) + + local v={x=0, y=0, z=0} --DCS#Vec3 + + for _,_contact in pairs(cluster.Contacts) do + local contact=_contact --#INTEL.Contact + + if (not contact.isStatic) and contact.group:IsAlive() then + local vec=contact.group:GetVelocityVec3() + v.x=v.x+vec.x + v.y=v.y+vec.y + v.z=v.y+vec.z + end + end + + return v end --- Calculate cluster future position after given seconds. -- @param #INTEL self -- @param #INTEL.Cluster cluster The cluster of contacts. --- @param #number seconds Timeframe in seconds. +-- @param #number seconds Time interval in seconds. Default is `self.prediction`. -- @return Core.Point#COORDINATE Calculated future position of the cluster. -function INTEL:CalcClusterFuturePosition(cluster,seconds) - local speed = self:CalcClusterSpeed(cluster) -- #number MPS - local direction = self:CalcClusterDirection(cluster) -- #number heading - -- local currposition = cluster.coordinate -- Core.Point#COORDINATE - local currposition = self:GetClusterCoordinate(cluster) -- Core.Point#COORDINATE - local distance = speed * seconds -- #number in meters the cluster will travel - local futureposition = currposition:Translate(distance,direction,true,false) - if self.clustermarkers and (self.verbose > 1) then +function INTEL:CalcClusterFuturePosition(cluster, seconds) + + -- Get current position of the cluster. + local p=self:GetClusterCoordinate(cluster) + + -- Velocity vector in m/s. + local v=self:CalcClusterVelocityVec3(cluster) + + -- Time in seconds. + local t=seconds or self.prediction + + -- Extrapolated vec3. + local Vec3={x=p.x+v.x*t, y=p.y+v.y*t, z=p.z+v.z*t} + + -- Future position. + local futureposition=COORDINATE:NewFromVec3(Vec3) + + -- Create an arrow pointing in the direction of the movement. + if self.clustermarkers and self.verbose>1 then if cluster.markerID then COORDINATE:RemoveMark(cluster.markerID) end - cluster.markerID = currposition:ArrowToAll(futureposition,self.coalition,{1,0,0},1,{1,1,0},0.5,2,true,"Postion Calc") + cluster.markerID = p:ArrowToAll(futureposition, self.coalition, {1,0,0}, 1, {1,1,0}, 0.5, 2, true, "Position Calc") end + return futureposition end @@ -1279,20 +1702,26 @@ end -- @param #INTEL self -- @param #INTEL.Contact contact The contact. -- @param #INTEL.Cluster cluster The cluster the check. --- @return #boolean If true, contact is connected to this cluster. +-- @return #boolean If `true`, contact is connected to this cluster. +-- @return #number Distance to cluster in meters. function INTEL:IsContactConnectedToCluster(contact, cluster) + -- Must be of the same type. We do not want to mix aircraft with ground units. + if contact.ctype~=cluster.ctype then + return false, math.huge + end + for _,_contact in pairs(cluster.Contacts) do local Contact=_contact --#INTEL.Contact - if Contact.groupname~=contact.groupname then + -- Do not calcuate the distance to the contact itself unless it is the only contact in the cluster. + if Contact.groupname~=contact.groupname or cluster.size==1 then --local dist=Contact.position:Get2DDistance(contact.position) local dist=Contact.position:DistanceFromPointVec2(contact.position) - local radius = self.clusterradius or 15 - if dist1000 then + local dist=UTILS.VecDist3D(a,b) + + if dist>Threshold then return true else return false @@ -1397,20 +1895,46 @@ end -- @param #INTEL self function INTEL:_UpdateClusterPositions() for _,_cluster in pairs (self.Clusters) do - local coord = self:GetClusterCoordinate(_cluster) - _cluster.coordinate = coord - self:T(self.lid..string.format("Cluster size: %s", _cluster.size)) + local cluster=_cluster --#INTEL.Cluster + + -- Update cluster coordinate. + local coord = self:GetClusterCoordinate(cluster, true) + + -- Debug info. + self:T(self.lid..string.format("Updating Cluster position size: %s", cluster.size)) end end ---- Count number of units in cluster +--- Count number of alive units in contact. +-- @param #INTEL self +-- @param #INTEL.Contact Contact The contact. +-- @return #number unitcount +function INTEL:ContactCountUnits(Contact) + if Contact.isStatic then + if Contact.group and Contact.group:IsAlive() then + return 1 + else + return 0 + end + else + if Contact.group then + local n=Contact.group:CountAliveUnits() + return n + else + return 0 + end + end +end + +--- Count number of alive units in cluster. -- @param #INTEL self -- @param #INTEL.Cluster Cluster The cluster -- @return #number unitcount function INTEL:ClusterCountUnits(Cluster) local unitcount = 0 - for _,_group in pairs (Cluster.Contacts) do -- get Wrapper.GROUP#GROUP _group - unitcount = unitcount + _group.group:CountAliveUnits() + for _,_contact in pairs (Cluster.Contacts) do + local contact=_contact --#INTEL.Contact + unitcount = unitcount + self:ContactCountUnits(contact) end return unitcount end @@ -1423,27 +1947,28 @@ function INTEL:UpdateClusterMarker(cluster) -- Create a marker. local unitcount = self:ClusterCountUnits(cluster) - local text=string.format("Cluster #%d. Size %d, Units %d, TLsum=%d", cluster.index, cluster.size, unitcount, cluster.threatlevelSum) + local text=string.format("Cluster #%d: %s\nSize %d\nUnits %d\nTLsum=%d", cluster.index, cluster.ctype, cluster.size, unitcount, cluster.threatlevelSum) if not cluster.marker then - if self.coalition == coalition.side.RED then - cluster.marker=MARKER:New(cluster.coordinate, text):ToRed() - elseif self.coalition == coalition.side.BLUE then - cluster.marker=MARKER:New(cluster.coordinate, text):ToBlue() - else - cluster.marker=MARKER:New(cluster.coordinate, text):ToNeutral() - end + + -- First time ==> need to create a new marker object. + cluster.marker=MARKER:New(cluster.coordinate, text):ToCoalition(self.coalition) + else + -- Need to refresh? local refresh=false + -- Check if marker text changed. if cluster.marker.text~=text then cluster.marker.text=text refresh=true end - if cluster.marker.coordinate~=cluster.coordinate then - cluster.marker.coordinate=cluster.coordinate + -- Check if coordinate changed. + local coordchange=self:_CheckClusterCoordinateChanged(cluster, cluster.marker.coordinate) + if coordchange then + cluster.marker.coordinate:UpdateFromCoordinate(cluster.coordinate) refresh=true end @@ -1520,25 +2045,30 @@ INTEL_DLINK.version = "0.0.1" -- Contact duplicates are removed. Clusters might contain duplicates (Might fix that later, WIP). -- -- Basic setup: --- local datalink = INTEL_DLINK:New({myintel1,myintel2}), "FSB", 20, 300) --- datalink:__Start(2) +-- +-- local datalink = INTEL_DLINK:New({myintel1,myintel2}), "FSB", 20, 300) +-- datalink:__Start(2) -- -- Add an Intel while running: --- datalink:AddIntel(myintel3) +-- +-- datalink:AddIntel(myintel3) -- -- Gather the data: --- datalink:GetContactTable() -- #table of #INTEL.Contact contacts. --- datalink:GetClusterTable() -- #table of #INTEL.Cluster clusters. --- datalink:GetDetectedItemCoordinates() -- #table of contact coordinates, to be compatible with @{Functional.Detection#DETECTION}. +-- +-- datalink:GetContactTable() -- #table of #INTEL.Contact contacts. +-- datalink:GetClusterTable() -- #table of #INTEL.Cluster clusters. +-- datalink:GetDetectedItemCoordinates() -- #table of contact coordinates, to be compatible with @{Functional.Detection#DETECTION}. -- -- Gather data with the event function: --- function datalink:OnAfterCollected(From, Event, To, Contacts, Clusters) --- ... ... --- end +-- +-- function datalink:OnAfterCollected(From, Event, To, Contacts, Clusters) +-- ... ... +-- end -- function INTEL_DLINK:New(Intels, Alias, Interval, Cachetime) + -- Inherit everything from FSM class. - local self=BASE:Inherit(self, FSM:New()) -- #INTEL + local self=BASE:Inherit(self, FSM:New()) -- #INTEL_DLINK self.intels = Intels or {} self.contacts = {} diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index 3655729d1..bd5ac2a48 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -765,8 +765,7 @@ end --- Returns the average velocity Vec3 vector. -- @param Wrapper.Group#GROUP self --- @return DCS#Vec3 The velocity Vec3 vector --- @return #nil The GROUP is not existing or alive. +-- @return DCS#Vec3 The velocity Vec3 vector or `#nil` if the GROUP is not existing or alive. function GROUP:GetVelocityVec3() self:F2( self.GroupName ) @@ -1009,9 +1008,8 @@ end --- Returns a random @{DCS#Vec3} vector (point in 3D of the UNIT within the mission) within a range around the first UNIT of the GROUP. -- @param #GROUP self --- @param #number Radius --- @return DCS#Vec3 The random 3D point vector around the first UNIT of the GROUP. --- @return #nil The GROUP is invalid or empty +-- @param #number Radius Radius in meters. +-- @return DCS#Vec3 The random 3D point vector around the first UNIT of the GROUP or #nil The GROUP is invalid or empty. -- @usage -- -- If Radius is ignored, returns the DCS#Vec3 of first UNIT of the GROUP function GROUP:GetRandomVec3(Radius) @@ -1032,8 +1030,7 @@ end --- Returns the mean heading of every UNIT in the GROUP in degrees -- @param #GROUP self --- @return #number mean heading of the GROUP --- @return #nil The first UNIT is not existing or alive. +-- @return #number Mean heading of the GROUP in degrees or #nil The first UNIT is not existing or alive. function GROUP:GetHeading() self:F2(self.GroupName) @@ -1061,8 +1058,8 @@ end --- Return the fuel state and unit reference for the unit with the least -- amount of fuel in the group. -- @param #GROUP self --- @return #number The fuel state of the unit with the least amount of fuel --- @return #Unit reference to #Unit object for further processing +-- @return #number The fuel state of the unit with the least amount of fuel. +-- @return #Unit reference to #Unit object for further processing. function GROUP:GetFuelMin() self:F3(self.ControllableName) diff --git a/Moose Development/Moose/Wrapper/Marker.lua b/Moose Development/Moose/Wrapper/Marker.lua index 695d2179d..6dfcd4f71 100644 --- a/Moose Development/Moose/Wrapper/Marker.lua +++ b/Moose Development/Moose/Wrapper/Marker.lua @@ -152,7 +152,7 @@ _MARKERID=0 --- Marker class version. -- @field #string version -MARKER.version="0.1.0" +MARKER.version="0.1.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -177,7 +177,7 @@ function MARKER:New(Coordinate, Text) -- Inherit everything from FSM class. local self=BASE:Inherit(self, FSM:New()) -- #MARKER - self.coordinate=Coordinate + self.coordinate=UTILS.DeepCopy(Coordinate) self.text=Text From 14dea99ccd652d699cd8dc28fa966514b6d9bedf Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 22 Mar 2022 09:31:16 +0100 Subject: [PATCH 22/26] Update Utils.lua - Removed useless UTILS.GetDate() function --- Moose Development/Moose/Utilities/Utils.lua | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 8b7ebb2b6..b4201ef6c 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -1321,26 +1321,6 @@ function UTILS.GetMissionDayOfYear(Time) end ---- Returns the current date. --- @return #string Mission date in yyyy/mm/dd format. --- @return #number The year anno domini. --- @return #number The month. --- @return #number The day. -function UTILS.GetDate() - - -- Mission start date - local date, year, month, day=UTILS.GetDCSMissionDate() - - local time=timer.getAbsTime() - - local clock=UTILS.SecondsToClock(time, false) - - local x=tonumber(UTILS.Split(clock, "+")[2]) - - local day=day+x - -end - --- Returns the magnetic declination of the map. -- Returned values for the current maps are: -- From 4164bbeba8e6a536ffb4386136395493685b42f5 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Tue, 22 Mar 2022 10:38:07 +0100 Subject: [PATCH 23/26] CSAR/CTLD - added type to script --- Moose Development/Moose/Ops/CSAR.lua | 5 +++-- Moose Development/Moose/Ops/CTLD.lua | 1 + Moose Development/Moose/Utilities/Utils.lua | 5 +++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Ops/CSAR.lua b/Moose Development/Moose/Ops/CSAR.lua index de2c30b5f..8b71f4195 100644 --- a/Moose Development/Moose/Ops/CSAR.lua +++ b/Moose Development/Moose/Ops/CSAR.lua @@ -254,11 +254,12 @@ CSAR.AircraftType["Mi-8MT"] = 12 CSAR.AircraftType["Mi-24P"] = 8 CSAR.AircraftType["Mi-24V"] = 8 CSAR.AircraftType["Bell-47"] = 2 -CSAR.AircraftType["UH-60L"] = 10 +CSAR.AircraftType["UH-60L"] = 10 +CSAR.AircraftType["AH-64D_BLK_II"] = 2 --- CSAR class version. -- @field #string version -CSAR.version="1.0.4c" +CSAR.version="1.0.4d" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index 02f334590..4ae5f4720 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -1017,6 +1017,7 @@ CTLD.UnitTypes = { ["Hercules"] = {type="Hercules", crates=true, troops=true, cratelimit = 7, trooplimit = 64, length = 25, cargoweightlimit = 19000}, -- 19t cargo, 64 paratroopers. --Actually it's longer, but the center coord is off-center of the model. ["UH-60L"] = {type="UH-60L", crates=true, troops=true, cratelimit = 2, trooplimit = 20, length = 16, cargoweightlimit = 3500}, -- 4t cargo, 20 (unsec) seats + ["AH-64D_BLK_II"] = {type="AH-64D_BLK_II", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 17, cargoweightlimit = 200}, -- 2 ppl **outside** the helo } --- CTLD class version. diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index b4201ef6c..b4b39056f 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -1731,6 +1731,11 @@ function UTILS.IsLoadingDoorOpen( unit_name ) ret_val = true end + if string.find(type_name, "AH-64D") then + BASE:T(unit_name .. " front door(s) are open") + ret_val = true -- no doors on this one ;) + end + if ret_val == false then BASE:T(unit_name .. " all doors are closed") end From 5f57d4ddcdedf6390bc334e348e0506aab9e8f76 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Wed, 23 Mar 2022 07:57:06 +0100 Subject: [PATCH 24/26] docu changes --- Moose Development/Moose/Core/Zone.lua | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index c31840728..6a5bde026 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -9,7 +9,7 @@ -- * Create polygon zones. -- * Create moving zones around a unit. -- * Create moving zones around a group. --- * Provide the zone behaviour. Some zones are static, while others are moveable. +-- * Provide the zone behavior. Some zones are static, while others are moveable. -- * Enquiry if a coordinate is within a zone. -- * Smoke zones. -- * Set a zone probability to control zone selection. @@ -20,10 +20,10 @@ -- * Draw zones (circular and polygon) on the F10 map. -- -- --- There are essentially two core functions that zones accomodate: +-- There are essentially two core functions that zones accommodate: -- -- * Test if an object is within the zone boundaries. --- * Provide the zone behaviour. Some zones are static, while others are moveable. +-- * Provide the zone behavior. Some zones are static, while others are moveable. -- -- The object classes are using the zone classes to test the zone boundaries, which can take various forms: -- @@ -2126,12 +2126,12 @@ end -- -- ## Declare a ZONE_POLYGON directly in the DCS mission editor! -- --- You can declare a ZONE_POLYGON using the DCS mission editor by adding the ~ZONE_POLYGON tag in the group name. +-- You can declare a ZONE_POLYGON using the DCS mission editor by adding the #ZONE_POLYGON tag in the group name. -- --- So, imagine you have a group declared in the mission editor, with group name `DefenseZone~ZONE_POLYGON`. +-- So, imagine you have a group declared in the mission editor, with group name `DefenseZone#ZONE_POLYGON`. -- Then during mission startup, when loading Moose.lua, this group will be detected as a ZONE_POLYGON declaration. -- Within the background, a ZONE_POLYGON object will be created within the @{Core.Database} using the properties of the group. --- The ZONE_POLYGON name will be the group name without the ~ZONE_POLYGON tag. +-- The ZONE_POLYGON name will be the group name without the #ZONE_POLYGON tag. -- -- So, you can search yourself for the ZONE_POLYGON by using the @{#ZONE_POLYGON.FindByName}() method. -- In this example, `local PolygonZone = ZONE_POLYGON:FindByName( "DefenseZone" )` would return the ZONE_POLYGON object From 0411120551b10ea6253ebbcfc980c2f7c6f0b701 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 25 Mar 2022 09:49:26 +0100 Subject: [PATCH 25/26] OPS OPSGROUP - Added damage check for groups that are not alive. - Added `:ReturnToLegion` function. - Added `:Teleport` function LEGION - Aircraft dont start hot if on Alert5 mission AUFTRAG - Added Formation parameter to PATROLZONE (for ground only) ARMYGROUP - Set Vee formation for engage group --- .../Moose/Functional/Warehouse.lua | 42 ++- Moose Development/Moose/Ops/ArmyGroup.lua | 22 +- Moose Development/Moose/Ops/Auftrag.lua | 9 +- Moose Development/Moose/Ops/Cohort.lua | 8 +- Moose Development/Moose/Ops/FlightGroup.lua | 61 +---- Moose Development/Moose/Ops/Legion.lua | 10 +- Moose Development/Moose/Ops/NavyGroup.lua | 11 +- Moose Development/Moose/Ops/OpsGroup.lua | 242 +++++++++++++----- Moose Development/Moose/Utilities/Utils.lua | 16 ++ 9 files changed, 255 insertions(+), 166 deletions(-) diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 680723d62..8063af4f8 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -5452,9 +5452,10 @@ end -- @param #WAREHOUSE.Assetitem asset The asset that is dead. -- @param #WAREHOUSE.Pendingitem request The request of the dead asset. function WAREHOUSE:onafterAssetDead(From, Event, To, asset, request) + + -- Debug message. local text=string.format("Asset %s from request id=%d is dead!", asset.templatename, request.uid) self:T(self.lid..text) - self:_DebugMessage(text) -- Here I need to get rid of the #CARGO at the end to obtain the original name again! local groupname=asset.spawngroupname --self:_GetNameWithOut(group) @@ -6650,35 +6651,30 @@ end -- @param Wrapper.Group#GROUP deadgroup Group of unit that died. -- @param #WAREHOUSE.Pendingitem request Request that needs to be updated. function WAREHOUSE:_UnitDead(deadunit, deadgroup, request) - --env.info("FF unit dead "..deadunit:GetName()) + self:F(self.lid.."FF unit dead "..deadunit:GetName()) - -- Find asset. - local asset=self:FindAssetInDB(deadgroup) - -- Find opsgroup. local opsgroup=_DATABASE:FindOpsGroup(deadgroup) - local groupdead=false - if opsgroup then - - if opsgroup:IsDead() then - groupdead=true - end - - else + -- Check if we have an opsgroup. + if opsgroup then + -- Handled in OPSGROUP:onafterDead() now. + return nil + end - -- Number of alive units in group. - local nalive=deadgroup:CountAliveUnits() - - -- Whole group is dead? - if nalive>0 then - groupdead=false - else - groupdead=true - end - + -- Number of alive units in group. + local nalive=deadgroup:CountAliveUnits() + + -- Whole group is dead? + local groupdead=false + if nalive>0 then + groupdead=false + else + groupdead=true end + -- Find asset. + local asset=self:FindAssetInDB(deadgroup) -- Here I need to get rid of the #CARGO at the end to obtain the original name again! local unitname=self:_GetNameWithOut(deadunit) diff --git a/Moose Development/Moose/Ops/ArmyGroup.lua b/Moose Development/Moose/Ops/ArmyGroup.lua index c08870400..f4a881f55 100644 --- a/Moose Development/Moose/Ops/ArmyGroup.lua +++ b/Moose Development/Moose/Ops/ArmyGroup.lua @@ -653,7 +653,10 @@ function ARMYGROUP:Status() end end end - + + else + -- Check damage of elements and group. + self:_CheckDamage() end -- Check that group EXISTS. @@ -692,7 +695,7 @@ function ARMYGROUP:Status() local text=string.format("State %s: Alive=%s", fsmstate, tostring(self:IsAlive())) self:I(self.lid..text) end - + end --- @@ -707,7 +710,6 @@ function ARMYGROUP:Status() local name=element.name local status=element.status local unit=element.unit - --local life=unit:GetLifeRelative() or 0 local life,life0=self:GetLifePoints(element) local life0=element.life0 @@ -927,7 +929,7 @@ function ARMYGROUP:onafterUpdateRoute(From, Event, To, n, N, Speed, Formation) -- Next waypoint. local wp=UTILS.DeepCopy(self.waypoints[i]) --Ops.OpsGroup#OPSGROUP.Waypoint - self:T({wp}) + -- Speed. if Speed then wp.speed=UTILS.KnotsToMps(tonumber(Speed)) @@ -1429,6 +1431,7 @@ end function ARMYGROUP:onafterEngageTarget(From, Event, To, Target) self:T(self.lid.."Engaging Target") + -- Make sure this is a target. if Target:IsInstanceOf("TARGET") then self.engage.Target=Target else @@ -1438,11 +1441,9 @@ function ARMYGROUP:onafterEngageTarget(From, Event, To, Target) -- Target coordinate. self.engage.Coordinate=UTILS.DeepCopy(self.engage.Target:GetCoordinate()) - + -- Get a coordinate close to the target. local intercoord=self:GetCoordinate():GetIntermediateCoordinate(self.engage.Coordinate, 0.9) - - -- Backup ROE and alarm state. self.engage.roe=self:GetROE() self.engage.alarmstate=self:GetAlarmstate() @@ -1454,6 +1455,10 @@ function ARMYGROUP:onafterEngageTarget(From, Event, To, Target) -- ID of current waypoint. local uid=self:GetWaypointCurrent().uid + -- Set formation. + --TODO: make this input. + local Formation=ENUMS.Formation.Vehicle.Vee + -- Add waypoint after current. self.engage.Waypoint=self:AddWaypoint(intercoord, nil, uid, Formation, true) @@ -1599,8 +1604,7 @@ function ARMYGROUP:AddWaypoint(Coordinate, Speed, AfterWaypointWithID, Formation -- Speed in knots. Speed=Speed or self:GetSpeedCruise() - -- Formation - + -- Formation. if not Formation then if self.formationPerma then Formation = self.formationPerma diff --git a/Moose Development/Moose/Ops/Auftrag.lua b/Moose Development/Moose/Ops/Auftrag.lua index 9da1d8b41..927b5f2a7 100644 --- a/Moose Development/Moose/Ops/Auftrag.lua +++ b/Moose Development/Moose/Ops/Auftrag.lua @@ -560,7 +560,7 @@ AUFTRAG.Category={ --- AUFTRAG class version. -- @field #string version -AUFTRAG.version="0.8.3" +AUFTRAG.version="0.8.4" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1690,8 +1690,9 @@ end -- @param Core.Zone#ZONE Zone The patrol zone. -- @param #number Speed Speed in knots. -- @param #number Altitude Altitude in feet. Only for airborne units. Default 2000 feet ASL. +-- @param #string Formation Formation used during patrol. -- @return #AUFTRAG self -function AUFTRAG:NewPATROLZONE(Zone, Speed, Altitude) +function AUFTRAG:NewPATROLZONE(Zone, Speed, Altitude, Formation) local mission=AUFTRAG:New(AUFTRAG.Type.PATROLZONE) @@ -1715,6 +1716,8 @@ function AUFTRAG:NewPATROLZONE(Zone, Speed, Altitude) mission.categories={AUFTRAG.Category.ALL} mission.DCStask=mission:GetDCSMissionTask() + + mission.DCStask.params.formation=Formation return mission end @@ -3504,7 +3507,7 @@ function AUFTRAG:SetGroupStatus(opsgroup, status) local groupsDone=self:CheckGroupsDone() -- Debug info. - self:T2(self.lid..string.format("Setting OPSGROUP %s status to %s. IsNotOver=%s CheckGroupsDone=%s", opsgroup.groupname, self:GetGroupStatus(opsgroup), tostring(self:IsNotOver()), tostring(self:CheckGroupsDone()))) + self:T2(self.lid..string.format("Setting OPSGROUP %s status to %s. IsNotOver=%s CheckGroupsDone=%s", opsgroup.groupname, self:GetGroupStatus(opsgroup), tostring(self:IsNotOver()), tostring(groupsDone))) -- Check if ALL flights are done with their mission. if isNotOver and groupsDone then diff --git a/Moose Development/Moose/Ops/Cohort.lua b/Moose Development/Moose/Ops/Cohort.lua index 3780cbf07..eda955a9e 100644 --- a/Moose Development/Moose/Ops/Cohort.lua +++ b/Moose Development/Moose/Ops/Cohort.lua @@ -31,7 +31,8 @@ -- @field #string livery Livery of the cohort. -- @field #number skill Skill of cohort members. -- @field Ops.Legion#LEGION legion The LEGION object the cohort belongs to. --- @field #number Ngroups Number of asset OPS groups this cohort has. +-- @field #number Ngroups Number of asset OPS groups this cohort has. +-- @field #number Nkilled Number of destroyed asset groups. -- @field #number engageRange Mission range in meters. -- @field #string attribute Generalized attribute of the cohort template group. -- @field #table tacanChannel List of TACAN channels available to the cohort. @@ -67,6 +68,7 @@ COHORT = { skill = nil, legion = nil, Ngroups = nil, + Ngroups = 0, engageRange = nil, tacanChannel = {}, weightAsset = 99999, @@ -275,12 +277,10 @@ end --- Set number of units in groups. -- @param #COHORT self --- @param #number nunits Number of units. Must be >=1 and <=4. Default 2. +-- @param #number nunits Number of units. Default 2. -- @return #COHORT self function COHORT:SetGrouping(nunits) self.ngrouping=nunits or 2 - if self.ngrouping<1 then self.ngrouping=1 end - if self.ngrouping>4 then self.ngrouping=4 end return self end diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua index a83d19fac..2f5582908 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -168,7 +168,7 @@ FLIGHTGROUP.Attribute = { --- FLIGHTGROUP class version. -- @field #string version -FLIGHTGROUP.version="0.7.0" +FLIGHTGROUP.version="0.7.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -730,60 +730,6 @@ end -- Status ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ----- Update status. --- @param #FLIGHTGROUP self -function FLIGHTGROUP:onbeforeStatus(From, Event, To) - - -- First we check if elements are still alive. Could be that they were despawned without notice, e.g. when landing on a too small airbase. - for i,_element in pairs(self.elements) do - local element=_element --Ops.OpsGroup#OPSGROUP.Element - - -- Check that element is not already dead or not yet alive. - if element.status~=OPSGROUP.ElementStatus.DEAD and element.status~=OPSGROUP.ElementStatus.INUTERO then - - -- Unit shortcut. - local unit=element.unit - - local isdead=false - if unit and unit:IsAlive() then - - -- Get life points. - local life=unit:GetLife() or 0 - - -- Units with life <=1 are dead. - if life<=1 then - --env.info(string.format("FF unit %s: live<=1 in status at T=%.3f", unit:GetName(), timer.getTime())) - isdead=true - end - - else - -- Not alive any more. - --env.info(string.format("FF unit %s: NOT alive in status at T=%.3f", unit:GetName(), timer.getTime())) - isdead=true - end - - -- This one is dead. - if isdead then - local text=string.format("Element %s is dead at t=%.3f but has status %s! Maybe despawned without notice or landed at a too small airbase. Calling ElementDead in 60 sec to give other events a chance", - tostring(element.name), timer.getTime(), tostring(element.status)) - self:T(self.lid..text) - self:__ElementDead(60, element) - end - - end - end - - if self:IsDead() then - self:T(self.lid..string.format("Onbefore Status DEAD ==> false")) - return false - elseif self:IsStopped() then - self:T(self.lid..string.format("Onbefore Status STOPPED ==> false")) - return false - end - - return true -end - --- Status update. -- @param #FLIGHTGROUP self function FLIGHTGROUP:Status() @@ -845,7 +791,10 @@ function FLIGHTGROUP:Status() end end end - + + else + -- Check damage. + self:_CheckDamage() end --- diff --git a/Moose Development/Moose/Ops/Legion.lua b/Moose Development/Moose/Ops/Legion.lua index 929bd7d73..8da5350d8 100644 --- a/Moose Development/Moose/Ops/Legion.lua +++ b/Moose Development/Moose/Ops/Legion.lua @@ -45,13 +45,14 @@ LEGION = { --- LEGION class version. -- @field #string version -LEGION.version="0.2.0" +LEGION.version="0.2.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Create FLEED class. +-- DONE: Aircraft will not start hot on Alert5. -- DONE: OPS transport. -- DONE: Make general so it can be inherited by AIRWING and BRIGADE classes. @@ -748,6 +749,10 @@ function LEGION:onafterMissionRequest(From, Event, To, Mission) if Mission.missionTask then asset.missionTask=Mission.missionTask end + + if Mission.type==AUFTRAG.Type.ALERT5 then + asset.takeoffType=COORDINATE.WaypointType.TakeOffParking + end end @@ -1096,6 +1101,9 @@ function LEGION:onafterNewAsset(From, Event, To, asset, assignment) self:T(self.lid..string.format("Asset returned to legion ==> calling LegionAssetReturned event")) + -- Set takeoff type in case it was overwritten for an ALERT5 mission. + asset.takeoffType=cohort.takeoffType + -- Trigger event. self:LegionAssetReturned(cohort, asset) diff --git a/Moose Development/Moose/Ops/NavyGroup.lua b/Moose Development/Moose/Ops/NavyGroup.lua index b11f0bbac..595deb24a 100644 --- a/Moose Development/Moose/Ops/NavyGroup.lua +++ b/Moose Development/Moose/Ops/NavyGroup.lua @@ -90,7 +90,7 @@ NAVYGROUP = { --- NavyGroup version. -- @field #string version -NAVYGROUP.version="0.7.0" +NAVYGROUP.version="0.7.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -760,6 +760,12 @@ function NAVYGROUP:Status(From, Event, To) -- Check into wind queue. self:_CheckTurnsIntoWind() + + -- Check ammo status. + self:_CheckAmmoStatus() + + -- Check damage of elements and group. + self:_CheckDamage() -- Check if group got stuck. self:_CheckStuck() @@ -775,6 +781,9 @@ function NAVYGROUP:Status(From, Event, To) end end + else + -- Check damage of elements and group. + self:_CheckDamage() end -- Group exists but can also be inactive. diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index acffca586..21baf01fd 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -888,6 +888,7 @@ end -- @param Ops.Legion#LEGION Legion The Legion. -- @return #OPSGROUP self function OPSGROUP:_SetLegion(Legion) + self:T2(self.lid..string.format("Adding opsgroup to legion %s", Legion.alias)) self.legion=Legion return self end @@ -1618,13 +1619,6 @@ function OPSGROUP:Despawn(Delay, NoEventRemoveUnit) self.scheduleIDDespawn=self:ScheduleOnce(Delay, OPSGROUP.Despawn, self, 0, NoEventRemoveUnit) else - if self.legion and not NoEventRemoveUnit then - -- Add asset back in 10 seconds. - self:T(self.lid..string.format("Despawning Group by adding asset to LEGION!")) - self.legion:AddAsset(self.group, 1) - return - end - -- Debug info. self:T(self.lid..string.format("Despawning Group!")) @@ -1656,6 +1650,30 @@ function OPSGROUP:Despawn(Delay, NoEventRemoveUnit) return self end +--- Return group back to the legion it belongs to. +-- Group is despawned and added back to the stock. +-- @param #OPSGROUP self +-- @param #number Delay Delay in seconds before the group will be despawned. Default immediately +-- @return #OPSGROUP self +function OPSGROUP:ReturnToLegion(Delay) + + if Delay and Delay>0 then + self.scheduleIDDespawn=self:ScheduleOnce(Delay, OPSGROUP.ReturnToLegion, self) + else + + if self.legion then + -- Add asset back. + self:T(self.lid..string.format("Adding asset back to LEGION")) + self.legion:AddAsset(self.group, 1) + else + self:E(self.lid..string.format("ERROR: Group does not belong to a LEGION!")) + end + + end + + return self +end + --- Destroy a unit of the group. A *Unit Lost* for aircraft or *Dead* event for ground/naval units is generated. -- @param #OPSGROUP self -- @param #string UnitName Name of the unit which should be destroyed. @@ -1679,6 +1697,9 @@ function OPSGROUP:DestroyUnit(UnitName, Delay) else self:CreateEventDead(EventTime, unit) end + + -- Despawn unit. + unit:destroy() end @@ -3595,7 +3616,6 @@ function OPSGROUP:onafterTaskExecute(From, Event, To, Task) -- Speed and altitude. local Speed=Task.dcstask.params.speed and UTILS.MpsToKnots(Task.dcstask.params.speed) or UTILS.KmphToKnots(self.speedCruise) - --local Speed=UTILS.KmphToKnots(Task.dcstask.params.speed or self.speedCruise) local Altitude=Task.dcstask.params.altitude and UTILS.MetersToFeet(Task.dcstask.params.altitude) or nil local currUID=self:GetWaypointCurrent().uid @@ -3605,7 +3625,7 @@ function OPSGROUP:onafterTaskExecute(From, Event, To, Task) if self.isFlightgroup then wp=FLIGHTGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Altitude) elseif self.isArmygroup then - wp=ARMYGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Formation) + wp=ARMYGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Task.dcstask.params.formation) elseif self.isNavygroup then wp=NAVYGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Altitude) end @@ -3632,7 +3652,6 @@ function OPSGROUP:onafterTaskExecute(From, Event, To, Task) -- Speed and altitude. local Speed=UTILS.MpsToKnots(Task.dcstask.params.speed) or UTILS.KmphToKnots(self.speedCruise) - --local Speed=UTILS.KmphToKnots(Task.dcstask.params.speed or self.speedCruise) local Altitude=Task.dcstask.params.altitude and UTILS.MetersToFeet(Task.dcstask.params.altitude) or nil --Coordinate:MarkToAll("Recon Waypoint Execute") @@ -4939,7 +4958,7 @@ function OPSGROUP:onafterPassingWaypoint(From, Event, To, Waypoint) if self.isFlightgroup then wp=FLIGHTGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Altitude) elseif self.isArmygroup then - wp=ARMYGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Formation) + wp=ARMYGROUP.AddWaypoint(self, Coordinate, Speed, currUID, task.dcstask.params.formation) elseif self.isNavygroup then wp=NAVYGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Altitude) end @@ -4967,7 +4986,6 @@ function OPSGROUP:onafterPassingWaypoint(From, Event, To, Waypoint) -- Speed and altitude. local Speed=task.dcstask.params.speed and UTILS.MpsToKnots(task.dcstask.params.speed) or UTILS.KmphToKnots(self.speedCruise) - --local Speed=UTILS.KmphToKnots(task.dcstask.params.speed or self.speedCruise) local Altitude=task.dcstask.params.altitude and UTILS.MetersToFeet(task.dcstask.params.altitude) or nil -- Debug. @@ -5796,12 +5814,19 @@ end function OPSGROUP:onafterElementDamaged(From, Event, To, Element) self:T(self.lid..string.format("Element damaged %s", Element.name)) - if Element and Element.status~=OPSGROUP.ElementStatus.DEAD then - - local lifepoints=Element.DCSunit:getLife() - local lifepoint0=Element.DCSunit:getLife0() + if Element and (Element.status~=OPSGROUP.ElementStatus.DEAD and Element.status~=OPSGROUP.ElementStatus.INUTERO) then + + local lifepoints=0 - self:T(self.lid..string.format("Element life %s: %.2f/%.2f", Element.name, lifepoints, lifepoint0)) + if Element.DCSunit and Element.DCSunit:isExist() then + + -- Get life of unit + lifepoints=Element.DCSunit:getLife() + + -- Debug output. + self:T(self.lid..string.format("Element life %s: %.2f/%.2f", Element.name, lifepoints, Element.life0)) + + end if lifepoints<=1.0 then self:T(self.lid..string.format("Element %s life %.2f <= 1.0 ==> Destroyed!", Element.name, lifepoints)) @@ -5809,10 +5834,9 @@ function OPSGROUP:onafterElementDamaged(From, Event, To, Element) end end - + end - --- On after "ElementDestroyed" event. -- @param #OPSGROUP self -- @param #string From From state. @@ -5851,14 +5875,6 @@ function OPSGROUP:onafterElementDead(From, Event, To, Element) -- Set element status. self:_UpdateStatus(Element, OPSGROUP.ElementStatus.DEAD) - - if self.legion then - if not self:IsInUtero() then - local asset=self.legion:GetAssetByName(self.groupname) - local request=self.legion:GetRequestByID(asset.rid) - self.legion:_UnitDead(Element.unit, self.group, request) - end - end -- Check if element was lasing and if so, switch to another unit alive to lase. if self.spot.On and self.spot.element.name==Element.name then @@ -5952,6 +5968,80 @@ function OPSGROUP:onafterRespawn(From, Event, To, Template) end +--- Teleport the group to a different location. +-- @param #OPSGROUP self +-- @param Core.Point#COORDINATE Coordinate Coordinate where the group is teleported to. +-- @param #number Delay Delay in seconds before respawn happens. Default 0. +-- @return #OPSGROUP self +function OPSGROUP:Teleport(Coordinate, Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP.Teleport, self, Coordinate) + else + + -- Debug message. + self:T(self.lid.."FF Teleporting...") + --Coordinate:MarkToAll("Teleport "..self.groupname) + + -- Check if we have a mission running. + if self.currentmission>0 then + self:T(self.lid.."Pausing current mission") + self:PauseMission() + end + + -- Get copy of template. + local Template=UTILS.DeepCopy(self.template) --DCS#Template + + -- Template units. + local units=Template.units + + -- Table with teleported vectors. + local d={} + for i=1,#units do + local unit=units[i] + d[i]={x=Coordinate.x+(units[i].x-units[1].x), y=Coordinate.z+units[i].y-units[1].y} + --COORDINATE:NewFromVec2(d[i]):MarkToAll(unit.name.." teleported") + end + + for i=#units,1,-1 do + local unit=units[i] + + -- Get element. + local element=self:GetElementByName(unit.name) + + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + + -- No parking. + unit.parking=nil + unit.parking_id=nil + + -- Current position. + local vec3=element.unit:GetVec3() + + -- Current heading. + local heading=element.unit:GetHeading() + + -- Set new x,y. + unit.x=d[i].x + unit.y=d[i].y + + -- Set altitude. + unit.alt=Coordinate.y + + -- Set heading. + unit.heading=math.rad(heading) + unit.psi=-unit.heading + else + table.remove(units, i) + end + end + + -- Respawn from new template. + self:_Respawn(0, Template, true) + + end +end + --- Respawn the group. -- @param #OPSGROUP self -- @param #number Delay Delay in seconds before respawn happens. Default 0. @@ -5967,58 +6057,60 @@ function OPSGROUP:_Respawn(Delay, Template, Reset) -- Debug message. self:T2(self.lid.."FF _Respawn") - -- Given template or get old. + -- Given template or get copy of old. Template=Template or self:_GetTemplate(true) + + -- Number of destroyed units. + self.Ndestroyed=0 + -- Check if group is currently alive. if self:IsAlive() then --- -- Group is ALIVE --- - --[[ - - -- Get units. - local units=self.group:GetUnits() - - -- Loop over template units. - for UnitID, Unit in pairs(Template.units) do - - for _,_unit in pairs(units) do - local unit=_unit --Wrapper.Unit#UNIT - - if unit:GetName()==Unit.name then - local vec3=unit:GetVec3() - local heading=unit:GetHeading() - Unit.x=vec3.x - Unit.y=vec3.z - Unit.alt=vec3.y - Unit.heading=math.rad(heading) - Unit.psi=-Unit.heading - end - end - - end - - ]] - + -- Template units. local units=Template.units for i=#units,1,-1 do local unit=units[i] + + -- Get the element. local element=self:GetElementByName(unit.name) + if element and element.status~=OPSGROUP.ElementStatus.DEAD then - unit.parking=element.parking and element.parking.TerminalID or unit.parking - unit.parking_id=nil - local vec3=element.unit:GetVec3() - local heading=element.unit:GetHeading() - unit.x=vec3.x - unit.y=vec3.z - unit.alt=vec3.y - unit.heading=math.rad(heading) - unit.psi=-unit.heading + + if not Reset then + + -- Parking ID. + unit.parking=element.parking and element.parking.TerminalID or unit.parking + unit.parking_id=nil + + -- Get current position vector. + local vec3=element.unit:GetVec3() + + -- Get heading. + local heading=element.unit:GetHeading() + + -- Set unit position. + unit.x=vec3.x + unit.y=vec3.z + unit.alt=vec3.y + + -- Set heading in rad. + unit.heading=math.rad(heading) + unit.psi=-unit.heading + + end + else + + -- Element is dead. Remove from template. table.remove(units, i) + + self.Ndestroyed=self.Ndestroyed+1 + end end @@ -6029,7 +6121,7 @@ function OPSGROUP:_Respawn(Delay, Template, Reset) else --- - -- Group is DESPAWNED + -- Group is NOT ALIVE --- -- Ensure elements in utero. @@ -6059,8 +6151,7 @@ function OPSGROUP:_Respawn(Delay, Template, Reset) self.isDestroyed=false - self.groupinitialized=false - self.Ndestroyed=0 + self.groupinitialized=false self.wpcounter=1 self.currentwp=1 @@ -6069,7 +6160,7 @@ function OPSGROUP:_Respawn(Delay, Template, Reset) -- Init Group. self:_InitGroup(Template) - + -- Reset events. --self:ResetEvents() @@ -6213,11 +6304,24 @@ function OPSGROUP:onafterDead(From, Event, To) else -- Not all assets were destroyed (despawn) ==> Add asset back to legion? end - - -- Stop in 5 sec to give possible respawn attempts a chance. + if self.legion then + if not self:IsInUtero() then + + -- Get asset. + local asset=self.legion:GetAssetByName(self.groupname) + + -- Get request. + local request=self.legion:GetRequestByID(asset.rid) + + -- Trigger asset dead event. + self.legion:AssetDead(asset, request) + end + + -- Stop in 5 sec to give possible respawn attempts a chance. self:__Stop(-5) end + end --- On before "Stop" event. diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index b4b39056f..0244ee757 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -1097,6 +1097,14 @@ function UTILS.VecSubstract(a, b) return {x=a.x-b.x, y=a.y-b.y, z=a.z-b.z} end +--- Calculate the difference between two 2D vectors by substracting the x,y components from each other. +-- @param DCS#Vec2 a Vector in 2D with x, y components. +-- @param DCS#Vec2 b Vector in 2D with x, y components. +-- @return DCS#Vec2 Vector c=a-b with c(i)=a(i)-b(i), i=x,y. +function UTILS.Vec2Substract(a, b) + return {x=a.x-b.x, y=a.y-b.y} +end + --- Calculate the total vector of two 3D vectors by adding the x,y,z components of each other. -- @param DCS#Vec3 a Vector in 3D with x, y, z components. -- @param DCS#Vec3 b Vector in 3D with x, y, z components. @@ -1105,6 +1113,14 @@ function UTILS.VecAdd(a, b) return {x=a.x+b.x, y=a.y+b.y, z=a.z+b.z} end +--- Calculate the total vector of two 2D vectors by adding the x,y components of each other. +-- @param DCS#Vec2 a Vector in 2D with x, y components. +-- @param DCS#Vec2 b Vector in 2D with x, y components. +-- @return DCS#Vec2 Vector c=a+b with c(i)=a(i)+b(i), i=x,y. +function UTILS.Vec2Add(a, b) + return {x=a.x+b.x, y=a.y+b.y} +end + --- Calculate the angle between two 3D vectors. -- @param DCS#Vec3 a Vector in 3D with x, y, z components. -- @param DCS#Vec3 b Vector in 3D with x, y, z components. From f92e8a285a0fff0a43a8517238a438184f30c073 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 25 Mar 2022 10:27:32 +0100 Subject: [PATCH 26/26] OPS - Added nil check on vec3 for _UndateEngageTarget in ARMYGROUP and NAVYGROUP --- Moose Development/Moose/Ops/ArmyGroup.lua | 53 +++++++++++++---------- Moose Development/Moose/Ops/NavyGroup.lua | 53 +++++++++++++---------- 2 files changed, 62 insertions(+), 44 deletions(-) diff --git a/Moose Development/Moose/Ops/ArmyGroup.lua b/Moose Development/Moose/Ops/ArmyGroup.lua index f4a881f55..98d372ade 100644 --- a/Moose Development/Moose/Ops/ArmyGroup.lua +++ b/Moose Development/Moose/Ops/ArmyGroup.lua @@ -1475,37 +1475,46 @@ function ARMYGROUP:_UpdateEngageTarget() -- Get current position vector. local vec3=self.engage.Target:GetVec3() + + if vec3 then - -- Distance to last known position of target. - local dist=UTILS.VecDist3D(vec3, self.engage.Coordinate:GetVec3()) - - -- Check if target moved more than 100 meters. - if dist>100 then - - --env.info("FF Update Engage Target Moved "..self.engage.Target:GetName()) - - -- Update new position. - self.engage.Coordinate:UpdateFromVec3(vec3) - - -- ID of current waypoint. - local uid=self:GetWaypointCurrent().uid - - -- Remove current waypoint - self:RemoveWaypointByID(self.engage.Waypoint.uid) + -- Distance to last known position of target. + local dist=UTILS.VecDist3D(vec3, self.engage.Coordinate:GetVec3()) - local intercoord=self:GetCoordinate():GetIntermediateCoordinate(self.engage.Coordinate, 0.9) + -- Check if target moved more than 100 meters. + if dist>100 then + + --env.info("FF Update Engage Target Moved "..self.engage.Target:GetName()) + + -- Update new position. + self.engage.Coordinate:UpdateFromVec3(vec3) - -- Add waypoint after current. - self.engage.Waypoint=self:AddWaypoint(intercoord, nil, uid, Formation, true) + -- ID of current waypoint. + local uid=self:GetWaypointCurrent().uid + + -- Remove current waypoint + self:RemoveWaypointByID(self.engage.Waypoint.uid) + + local intercoord=self:GetCoordinate():GetIntermediateCoordinate(self.engage.Coordinate, 0.9) - -- Set if we want to resume route after reaching the detour waypoint. - self.engage.Waypoint.detour=0 + -- Add waypoint after current. + self.engage.Waypoint=self:AddWaypoint(intercoord, nil, uid, Formation, true) + + -- Set if we want to resume route after reaching the detour waypoint. + self.engage.Waypoint.detour=0 + + end + + else + + -- Could not get position of target (not alive any more?) ==> Disengage. + self:Disengage() end else - -- Target not alive any more == Disengage. + -- Target not alive any more ==> Disengage. self:Disengage() end diff --git a/Moose Development/Moose/Ops/NavyGroup.lua b/Moose Development/Moose/Ops/NavyGroup.lua index 595deb24a..b3226c04e 100644 --- a/Moose Development/Moose/Ops/NavyGroup.lua +++ b/Moose Development/Moose/Ops/NavyGroup.lua @@ -1439,37 +1439,46 @@ function NAVYGROUP:_UpdateEngageTarget() -- Get current position vector. local vec3=self.engage.Target:GetVec3() + + if vec3 then - -- Distance to last known position of target. - local dist=UTILS.VecDist3D(vec3, self.engage.Coordinate:GetVec3()) - - -- Check if target moved more than 100 meters. - if dist>100 then - - --env.info("FF Update Engage Target Moved "..self.engage.Target:GetName()) - - -- Update new position. - self.engage.Coordinate:UpdateFromVec3(vec3) - - -- ID of current waypoint. - local uid=self:GetWaypointCurrent().uid - - -- Remove current waypoint - self:RemoveWaypointByID(self.engage.Waypoint.uid) + -- Distance to last known position of target. + local dist=UTILS.VecDist3D(vec3, self.engage.Coordinate:GetVec3()) - local intercoord=self:GetCoordinate():GetIntermediateCoordinate(self.engage.Coordinate, 0.9) + -- Check if target moved more than 100 meters. + if dist>100 then + + --env.info("FF Update Engage Target Moved "..self.engage.Target:GetName()) + + -- Update new position. + self.engage.Coordinate:UpdateFromVec3(vec3) - -- Add waypoint after current. - self.engage.Waypoint=self:AddWaypoint(intercoord, nil, uid, Formation, true) + -- ID of current waypoint. + local uid=self:GetWaypointCurrent().uid + + -- Remove current waypoint + self:RemoveWaypointByID(self.engage.Waypoint.uid) + + local intercoord=self:GetCoordinate():GetIntermediateCoordinate(self.engage.Coordinate, 0.9) - -- Set if we want to resume route after reaching the detour waypoint. - self.engage.Waypoint.detour=0 + -- Add waypoint after current. + self.engage.Waypoint=self:AddWaypoint(intercoord, nil, uid, Formation, true) + + -- Set if we want to resume route after reaching the detour waypoint. + self.engage.Waypoint.detour=0 + + end + + else + + -- Could not get position of target (not alive any more?) ==> Disengage. + self:Disengage() end else - -- Target not alive any more == Disengage. + -- Target not alive any more ==> Disengage. self:Disengage() end