diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 4bf450816..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: -- @@ -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 @@ -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 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 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/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 )", diff --git a/Moose Development/Moose/Functional/Sead.lua b/Moose Development/Moose/Functional/Sead.lua index c7dbfe9db..2af6c64f3 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. @@ -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. @@ -141,7 +143,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 +269,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 +288,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 +465,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 diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 5b66b13d2..ccb3fdbd3 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 @@ -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 @@ -5449,9 +5452,59 @@ 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) + + -- 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 +6609,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 +6625,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 +6635,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 +6648,46 @@ 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) + self:F(self.lid.."FF unit dead "..deadunit:GetName()) - -- Flare unit. - if self.Debug then - deadunit:FlareRed() + -- Find opsgroup. + local opsgroup=_DATABASE:FindOpsGroup(deadgroup) + + -- Check if we have an opsgroup. + if opsgroup then + -- Handled in OPSGROUP:onafterDead() now. + return nil end - -- Group the dead unit belongs to. - local group=deadunit:GetGroup() - -- Number of alive units in group. - local nalive=group:CountAliveUnits() + local nalive=deadgroup:CountAliveUnits() -- Whole group is dead? - local groupdead=true + 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) - 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 +6695,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 +6703,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 +6718,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 @@ -8065,57 +8095,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 @@ -8124,14 +8109,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) @@ -8162,25 +8146,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/ArmyGroup.lua b/Moose Development/Moose/Ops/ArmyGroup.lua index 6ed92c53c..98d372ade 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 = { @@ -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. @@ -652,7 +653,10 @@ function ARMYGROUP:Status() end end end - + + else + -- Check damage of elements and group. + self:_CheckDamage() end -- Check that group EXISTS. @@ -691,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 --- @@ -706,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 @@ -926,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)) @@ -981,6 +984,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 {} @@ -1002,7 +1012,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. @@ -1105,16 +1116,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 @@ -1138,6 +1139,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 @@ -1223,9 +1234,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 @@ -1240,6 +1248,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) @@ -1351,6 +1362,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 @@ -1417,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 @@ -1426,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() @@ -1442,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) @@ -1458,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 @@ -1587,8 +1613,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 f41e01e1b..894d6b421 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. @@ -383,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", @@ -415,6 +417,7 @@ AUFTRAG.Type={ ARMOREDGUARD="Armored Guard", BARRAGE="Barrage", ARMORATTACK="Armor Attack", + CASENHANCED="CAS Enhanced", } --- Mission status of an assigned group. @@ -557,7 +560,7 @@ AUFTRAG.Category={ --- AUFTRAG class version. -- @field #string version -AUFTRAG.version="0.8.1" +AUFTRAG.version="0.8.4" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1186,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. @@ -1492,8 +1534,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 100 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 +1552,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 100 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 @@ -1645,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) @@ -1670,10 +1716,13 @@ function AUFTRAG:NewPATROLZONE(Zone, Speed, Altitude) mission.categories={AUFTRAG.Category.ALL} mission.DCStask=mission:GetDCSMissionTask() + + mission.DCStask.params.formation=Formation 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. @@ -3478,7 +3527,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 @@ -4958,6 +5007,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 @@ -5147,6 +5216,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/CSAR.lua b/Moose Development/Moose/Ops/CSAR.lua index 4a3253a42..8b71f4195 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,7 +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. 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: @@ -227,8 +236,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 @@ -244,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.3" +CSAR.version="1.0.4d" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list @@ -390,6 +401,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 +517,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 +535,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 +585,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 +667,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 +717,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. @@ -935,9 +964,19 @@ 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 @@ -1294,7 +1333,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 @@ -1999,6 +2039,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 diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index 251eb952f..4ae5f4720 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -1017,11 +1017,12 @@ 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. -- @field #string version -CTLD.version="1.0.9" +CTLD.version="1.0.10" --- Instantiate a new CTLD. -- @param #CTLD self @@ -2095,11 +2096,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 or 2000 + maxloadable = maxmass - loadedmass + end self:T(self.lid .. " Max loadable mass: " .. maxloadable) for _,_cargoobject in pairs (existingcrates) do local cargo = _cargoobject -- #CTLD_CARGO @@ -2256,6 +2264,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 diff --git a/Moose Development/Moose/Ops/Chief.lua b/Moose Development/Moose/Ops/Chief.lua index 7ba916181..0b49d5b0c 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. @@ -180,7 +180,7 @@ CHIEF.Strategy = { --- CHIEF class version. -- @field #string version -CHIEF.version="0.1.0" +CHIEF.version="0.1.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -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 @@ -2208,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 @@ -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..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 @@ -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/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/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/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/Ops/Legion.lua b/Moose Development/Moose/Ops/Legion.lua index bec5b72c6..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. @@ -81,6 +82,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 @@ -530,6 +535,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 @@ -743,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 @@ -882,7 +892,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 +1014,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) @@ -1010,7 +1031,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. @@ -1018,6 +1039,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 @@ -1028,15 +1053,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. @@ -1061,6 +1099,11 @@ 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")) + + -- Set takeoff type in case it was overwritten for an ALERT5 mission. + asset.takeoffType=cohort.takeoffType + -- Trigger event. self:LegionAssetReturned(cohort, asset) @@ -1078,7 +1121,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 +1289,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 @@ -1809,7 +1852,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 @@ -1826,17 +1869,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! @@ -1854,7 +1900,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 @@ -1915,12 +1961,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={} @@ -2055,10 +2102,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. @@ -2153,7 +2220,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} @@ -2264,13 +2331,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. @@ -2278,7 +2346,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 @@ -2396,12 +2464,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/NavyGroup.lua b/Moose Development/Moose/Ops/NavyGroup.lua index b11f0bbac..b3226c04e 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. @@ -1430,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 diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index 6f582473a..21baf01fd 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 @@ -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 @@ -879,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 @@ -1609,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!")) @@ -1647,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. @@ -1670,6 +1697,9 @@ function OPSGROUP:DestroyUnit(UnitName, Delay) else self:CreateEventDead(EventTime, unit) end + + -- Despawn unit. + unit:destroy() end @@ -2618,6 +2648,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. @@ -3574,8 +3615,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=UTILS.KmphToKnots(Task.dcstask.params.speed or self.speedCruise) + local Speed=Task.dcstask.params.speed and UTILS.MpsToKnots(Task.dcstask.params.speed) or UTILS.KmphToKnots(self.speedCruise) local Altitude=Task.dcstask.params.altitude and UTILS.MetersToFeet(Task.dcstask.params.altitude) or nil local currUID=self:GetWaypointCurrent().uid @@ -3585,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 @@ -3612,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") @@ -3673,7 +3712,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 +4268,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 @@ -4580,13 +4618,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=mission.transportPickupRadius + 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 @@ -4901,7 +4948,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) @@ -4911,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 @@ -4938,8 +4985,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=UTILS.KmphToKnots(task.dcstask.params.speed or self.speedCruise) + local Speed=task.dcstask.params.speed and UTILS.MpsToKnots(task.dcstask.params.speed) or UTILS.KmphToKnots(self.speedCruise) local Altitude=task.dcstask.params.altitude and UTILS.MetersToFeet(task.dcstask.params.altitude) or nil -- Debug. @@ -5759,6 +5805,38 @@ 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 and Element.status~=OPSGROUP.ElementStatus.INUTERO) then + + local lifepoints=0 + + 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)) + self:ElementDestroyed(Element) + end + + end + +end + --- On after "ElementDestroyed" event. -- @param #OPSGROUP self -- @param #string From From state. @@ -5793,7 +5871,7 @@ 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) @@ -5890,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. @@ -5905,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 @@ -5967,7 +6121,7 @@ function OPSGROUP:_Respawn(Delay, Template, Reset) else --- - -- Group is DESPAWNED + -- Group is NOT ALIVE --- -- Ensure elements in utero. @@ -5997,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 @@ -6007,7 +6160,7 @@ function OPSGROUP:_Respawn(Delay, Template, Reset) -- Init Group. self:_InitGroup(Template) - + -- Reset events. --self:ResetEvents() @@ -6074,6 +6227,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. @@ -6131,12 +6297,31 @@ 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) + + 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. @@ -6178,6 +6363,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() @@ -7993,6 +8183,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 @@ -8530,18 +8726,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. 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. diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 8b7ebb2b6..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. @@ -1321,26 +1337,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: -- @@ -1751,6 +1747,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 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 diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 35ede51b9..b27f74903 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 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 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 } ) + -- 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 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 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 } ) + -- 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 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 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