From 8ddcc0001fed639318e088758f4f4aaf17d5b608 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Thu, 6 May 2021 13:04:27 +0200 Subject: [PATCH] MarkerOps_Base Demo --- .../MOP-100 - MARKEROPS_BASE - Basic Demo.lua | 86 + .../MOP-100 - MARKEROPS_BASE - Basic Demo.miz | Bin 0 -> 455749 bytes .../l10n/DEFAULT/MarkerOps_Base_Demo.lua | 86 + .../l10n/DEFAULT/Moose_dev_small.lua | 78101 ++++++++++++++++ .../_unpacked/l10n/DEFAULT/dictionary | 9 + .../_unpacked/l10n/DEFAULT/mapResource | 5 + .../_unpacked/mission | 762 + .../_unpacked/options | 345 + .../_unpacked/theatre | 1 + .../_unpacked/warehouses | 845 + .../MOP-100 - MARKEROPS_BASE/pack.ps1 | 10 + .../MOP-100 - MARKEROPS_BASE/unpack.ps1 | 7 + 12 files changed, 80257 insertions(+) create mode 100644 MOP - MarkerOps_Base/MOP-100 - MARKEROPS_BASE/MOP-100 - MARKEROPS_BASE - Basic Demo.lua create mode 100644 MOP - MarkerOps_Base/MOP-100 - MARKEROPS_BASE/MOP-100 - MARKEROPS_BASE - Basic Demo.miz create mode 100644 MOP - MarkerOps_Base/MOP-100 - MARKEROPS_BASE/_unpacked/l10n/DEFAULT/MarkerOps_Base_Demo.lua create mode 100644 MOP - MarkerOps_Base/MOP-100 - MARKEROPS_BASE/_unpacked/l10n/DEFAULT/Moose_dev_small.lua create mode 100644 MOP - MarkerOps_Base/MOP-100 - MARKEROPS_BASE/_unpacked/l10n/DEFAULT/dictionary create mode 100644 MOP - MarkerOps_Base/MOP-100 - MARKEROPS_BASE/_unpacked/l10n/DEFAULT/mapResource create mode 100644 MOP - MarkerOps_Base/MOP-100 - MARKEROPS_BASE/_unpacked/mission create mode 100644 MOP - MarkerOps_Base/MOP-100 - MARKEROPS_BASE/_unpacked/options create mode 100644 MOP - MarkerOps_Base/MOP-100 - MARKEROPS_BASE/_unpacked/theatre create mode 100644 MOP - MarkerOps_Base/MOP-100 - MARKEROPS_BASE/_unpacked/warehouses create mode 100644 MOP - MarkerOps_Base/MOP-100 - MARKEROPS_BASE/pack.ps1 create mode 100644 MOP - MarkerOps_Base/MOP-100 - MARKEROPS_BASE/unpack.ps1 diff --git a/MOP - MarkerOps_Base/MOP-100 - MARKEROPS_BASE/MOP-100 - MARKEROPS_BASE - Basic Demo.lua b/MOP - MarkerOps_Base/MOP-100 - MARKEROPS_BASE/MOP-100 - MARKEROPS_BASE - Basic Demo.lua new file mode 100644 index 0000000000..0ee97708dc --- /dev/null +++ b/MOP - MarkerOps_Base/MOP-100 - MARKEROPS_BASE/MOP-100 - MARKEROPS_BASE - Basic Demo.lua @@ -0,0 +1,86 @@ +------------------------------------------------------------------------- +-- MOP-100 - MARKEROPS_BASE - Basic Demo +------------------------------------------------------------------------- +-- Documentation +-- +-- MARKEROPS_BASE: https://flightcontrol-master.github.io/MOOSE_DOCS_DEVELOP/Documentation/Core.MarkerOps_Base.html +-- +------------------------------------------------------------------------- +-- On the F10, call a tanker to start from the carrier. It will fly to +-- an initial zone. Set a marker on the F10 map with keyword "TankerDemo". +-- The Tanker will fly there. Set a marker on the F10 map with keywords +-- "TankerDemo RTB". The tanke will RTB to the carrier. +------------------------------------------------------------------------- +-- Date: May 2021 +------------------------------------------------------------------------- + +-- globals +mytanker = nil +tankergroup = nil +TankerAuftrag = nil + +function menucalltanker() + + if not mytanker then + -- new MARKEROPS_BASE object + mytanker = MARKEROPS_BASE:New("TankerDemo",{"RTB"}) -- Core.MarkerOps_Base#MARKEROPS_BASE + -- start FlightGroup + tankergroup = FLIGHTGROUP:New("Tanker") + tankergroup:SetHomebase(AIRBASE:FindByName("Truman")) + tankergroup:SetDefaultRadio(245,"AM",false) + tankergroup:SetDespawnAfterLanding() + tankergroup:SwitchTACAN(45, "TKR", 1, "X") + tankergroup:SetDefaultCallsign(CALLSIGN.Tanker.Texaco,1) + -- Mission + local InitialHold = ZONE:New("Initial Hold"):GetCoordinate() + TankerAuftrag = AUFTRAG:NewTANKER(InitialHold,18000,UTILS.KnotsToAltKIAS(220,18000),90,20,0) + TankerAuftrag:SetMissionRange(500) + tankergroup:AddMission(TankerAuftrag) + else + local status = tankergroup:GetState() + local m = MESSAGE:New(string.format("Tanker %s ops in status: %s", mytanker.Tag, status),10,"Info",true):ToAll() + end + + -- Handler function + local function Handler(Keywords,Coord) + + local MustRTB = false + for _,_word in pairs (Keywords) do + if string.lower(_word) == "rtb" then + MustRTB = true + end + end + + -- cancel current Auftrag + TankerAuftrag:Cancel() + + -- check if we need to RTB + if MustRTB then + tankergroup:RTB(AIRBASE:FindByName("Truman")) + else + -- no, fly to coordinate of marker + local auftrag = AUFTRAG:NewTANKER(Coord,18000,UTILS.KnotsToAltKIAS(220,18000),90,20,0) + auftrag:SetMissionRange(500) + tankergroup:AddMission(auftrag) + TankerAuftrag = auftrag + end + end + + -- Event functions + function mytanker:OnAfterMarkAdded(From,Event,To,Text,Keywords,Coord) + local m = MESSAGE:New(string.format("Tanker %s Mark Added.", self.Tag),10,"Info",true):ToAll() + Handler(Keywords,Coord) + end + + function mytanker:OnAfterMarkChanged(From,Event,To,Text,Keywords,Coord) + local m = MESSAGE:New(string.format("Tanker %s Mark Changed.", self.Tag),10,"Info",true):ToAll() + Handler(Keywords,Coord) + end + + function mytanker:OnAfterMarkDeleted(From,Event,To) + local m = MESSAGE:New(string.format("Tanker %s Mark Deleted.", self.Tag),10,"Info",true):ToAll() + end +end + +MenuTop = MENU_COALITION:New( coalition.side.BLUE,"Call Tanker") +MenuTanker = MENU_COALITION_COMMAND:New(coalition.side.BLUE,"Start Tanker",MenuTop,menucalltanker) diff --git a/MOP - MarkerOps_Base/MOP-100 - MARKEROPS_BASE/MOP-100 - MARKEROPS_BASE - Basic Demo.miz b/MOP - MarkerOps_Base/MOP-100 - MARKEROPS_BASE/MOP-100 - MARKEROPS_BASE - Basic Demo.miz new file mode 100644 index 0000000000000000000000000000000000000000..6f651e3786c18d948108dc3f1f478fca4d60b253 GIT binary patch literal 455749 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 z%0A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

D3Jr`y%~iLTOUHk-~&MG{=b@)%I& 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

CNUwLM(l8EM%<)Ng2vYcp+ed*u^0{3;y)a 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

    T#95i&)&?)xa6%nXx8z)ff97|K4liplSH@Ym1 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%0Dn0b^a@$wPmz;YeA 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 g0sPNod|X5S&u2kN77F^G3$ULD_~*0 then +latHemi='N' +else +latHemi='S' +end +if lon>0 then +lonHemi='E' +else +lonHemi='W' +end +lat=math.abs(lat) +lon=math.abs(lon) +local latDeg=math.floor(lat) +local latMin=(lat-latDeg)*60 +local lonDeg=math.floor(lon) +local lonMin=(lon-lonDeg)*60 +if DMS then +local oldLatMin=latMin +latMin=math.floor(latMin) +local latSec=routines.utils.round((oldLatMin-latMin)*60,acc) +local oldLonMin=lonMin +lonMin=math.floor(lonMin) +local lonSec=routines.utils.round((oldLonMin-lonMin)*60,acc) +if latSec==60 then +latSec=0 +latMin=latMin+1 +end +if lonSec==60 then +lonSec=0 +lonMin=lonMin+1 +end +local secFrmtStr +if acc<=0 then +secFrmtStr='%02d' +else +local width=3+acc +secFrmtStr='%0'..width..'.'..acc..'f' +end +return string.format('%02d',latDeg)..' '..string.format('%02d',latMin)..'\' '..string.format(secFrmtStr,latSec)..'"'..latHemi..' ' +..string.format('%02d',lonDeg)..' '..string.format('%02d',lonMin)..'\' '..string.format(secFrmtStr,lonSec)..'"'..lonHemi +else +latMin=routines.utils.round(latMin,acc) +lonMin=routines.utils.round(lonMin,acc) +if latMin==60 then +latMin=0 +latDeg=latDeg+1 +end +if lonMin==60 then +lonMin=0 +lonDeg=lonDeg+1 +end +local minFrmtStr +if acc<=0 then +minFrmtStr='%02d' +else +local width=3+acc +minFrmtStr='%0'..width..'.'..acc..'f' +end +return string.format('%02d',latDeg)..' '..string.format(minFrmtStr,latMin)..'\''..latHemi..' ' +..string.format('%02d',lonDeg)..' '..string.format(minFrmtStr,lonMin)..'\''..lonHemi +end +end +routines.tostringBR=function(az,dist,alt,metric) +az=routines.utils.round(routines.utils.toDegree(az),0) +if metric then +dist=routines.utils.round(dist/1000,2) +else +dist=routines.utils.round(routines.utils.metersToNM(dist),2) +end +local s=string.format('%03d',az)..' for '..dist +if alt then +if metric then +s=s..' at '..routines.utils.round(alt,0) +else +s=s..' at '..routines.utils.round(routines.utils.metersToFeet(alt),0) +end +end +return s +end +routines.getNorthCorrection=function(point) +if not point.z then +point.z=point.y +point.y=0 +end +local lat,lon=coord.LOtoLL(point) +local north_posit=coord.LLtoLO(lat+1,lon) +return math.atan2(north_posit.z-point.z,north_posit.x-point.x) +end +do +local idNum=0 +routines.addEventHandler=function(f) +local handler={} +idNum=idNum+1 +handler.id=idNum +handler.f=f +handler.onEvent=function(self,event) +self.f(event) +end +world.addEventHandler(handler) +end +routines.removeEventHandler=function(id) +for key,handler in pairs(world.eventHandlers)do +if handler.id and handler.id==id then +world.eventHandlers[key]=nil +return true +end +end +return false +end +end +function routines.getRandPointInCircle(point,radius,innerRadius) +local theta=2*math.pi*math.random() +local rad=math.random()+math.random() +if rad>1 then +rad=2-rad +end +local radMult +if innerRadius and innerRadius<=radius then +radMult=(radius-innerRadius)*rad+innerRadius +else +radMult=radius*rad +end +if not point.z then +point.z=point.y +end +local rndCoord +if radius>0 then +rndCoord={x=math.cos(theta)*radMult+point.x,y=math.sin(theta)*radMult+point.z} +else +rndCoord={x=point.x,y=point.z} +end +return rndCoord +end +routines.goRoute=function(group,path) +local misTask={ +id='Mission', +params={ +route={ +points=routines.utils.deepCopy(path), +}, +}, +} +if type(group)=='string'then +group=Group.getByName(group) +end +local groupCon=group:getController() +if groupCon then +groupCon:setTask(misTask) +return true +end +Controller.setTask(groupCon,misTask) +return false +end +routines.ground={} +routines.fixedWing={} +routines.heli={} +routines.ground.buildWP=function(point,overRideForm,overRideSpeed) +local wp={} +wp.x=point.x +if point.z then +wp.y=point.z +else +wp.y=point.y +end +local form,speed +if point.speed and not overRideSpeed then +wp.speed=point.speed +elseif type(overRideSpeed)=='number'then +wp.speed=overRideSpeed +else +wp.speed=routines.utils.kmphToMps(20) +end +if point.form and not overRideForm then +form=point.form +else +form=overRideForm +end +if not form then +wp.action='Cone' +else +form=string.lower(form) +if form=='off_road'or form=='off road'then +wp.action='Off Road' +elseif form=='on_road'or form=='on road'then +wp.action='On Road' +elseif form=='rank'or form=='line_abrest'or form=='line abrest'or form=='lineabrest'then +wp.action='Rank' +elseif form=='cone'then +wp.action='Cone' +elseif form=='diamond'then +wp.action='Diamond' +elseif form=='vee'then +wp.action='Vee' +elseif form=='echelon_left'or form=='echelon left'or form=='echelonl'then +wp.action='EchelonL' +elseif form=='echelon_right'or form=='echelon right'or form=='echelonr'then +wp.action='EchelonR' +else +wp.action='Cone' +end +end +wp.type='Turning Point' +return wp +end +routines.fixedWing.buildWP=function(point,WPtype,speed,alt,altType) +local wp={} +wp.x=point.x +if point.z then +wp.y=point.z +else +wp.y=point.y +end +if alt and type(alt)=='number'then +wp.alt=alt +else +wp.alt=2000 +end +if altType then +altType=string.lower(altType) +if altType=='radio'or'agl'then +wp.alt_type='RADIO' +elseif altType=='baro'or'asl'then +wp.alt_type='BARO' +end +else +wp.alt_type='RADIO' +end +if point.speed then +speed=point.speed +end +if point.type then +WPtype=point.type +end +if not speed then +wp.speed=routines.utils.kmphToMps(500) +else +wp.speed=speed +end +if not WPtype then +wp.action='Turning Point' +else +WPtype=string.lower(WPtype) +if WPtype=='flyover'or WPtype=='fly over'or WPtype=='fly_over'then +wp.action='Fly Over Point' +elseif WPtype=='turningpoint'or WPtype=='turning point'or WPtype=='turning_point'then +wp.action='Turning Point' +else +wp.action='Turning Point' +end +end +wp.type='Turning Point' +return wp +end +routines.heli.buildWP=function(point,WPtype,speed,alt,altType) +local wp={} +wp.x=point.x +if point.z then +wp.y=point.z +else +wp.y=point.y +end +if alt and type(alt)=='number'then +wp.alt=alt +else +wp.alt=500 +end +if altType then +altType=string.lower(altType) +if altType=='radio'or'agl'then +wp.alt_type='RADIO' +elseif altType=='baro'or'asl'then +wp.alt_type='BARO' +end +else +wp.alt_type='RADIO' +end +if point.speed then +speed=point.speed +end +if point.type then +WPtype=point.type +end +if not speed then +wp.speed=routines.utils.kmphToMps(200) +else +wp.speed=speed +end +if not WPtype then +wp.action='Turning Point' +else +WPtype=string.lower(WPtype) +if WPtype=='flyover'or WPtype=='fly over'or WPtype=='fly_over'then +wp.action='Fly Over Point' +elseif WPtype=='turningpoint'or WPtype=='turning point'or WPtype=='turning_point'then +wp.action='Turning Point' +else +wp.action='Turning Point' +end +end +wp.type='Turning Point' +return wp +end +routines.groupToRandomPoint=function(vars) +local group=vars.group +local point=vars.point +local radius=vars.radius or 0 +local innerRadius=vars.innerRadius +local form=vars.form or'Cone' +local heading=vars.heading or math.random()*2*math.pi +local headingDegrees=vars.headingDegrees +local speed=vars.speed or routines.utils.kmphToMps(20) +local useRoads +if not vars.disableRoads then +useRoads=true +else +useRoads=false +end +local path={} +if headingDegrees then +heading=headingDegrees*math.pi/180 +end +if heading>=2*math.pi then +heading=heading-2*math.pi +end +local rndCoord=routines.getRandPointInCircle(point,radius,innerRadius) +local offset={} +local posStart=routines.getLeadPos(group) +offset.x=routines.utils.round(math.sin(heading-(math.pi/2))*50+rndCoord.x,3) +offset.z=routines.utils.round(math.cos(heading+(math.pi/2))*50+rndCoord.y,3) +path[#path+1]=routines.ground.buildWP(posStart,form,speed) +if useRoads==true and((point.x-posStart.x)^2+(point.z-posStart.z)^2)^0.5>radius*1.3 then +path[#path+1]=routines.ground.buildWP({['x']=posStart.x+11,['z']=posStart.z+11},'off_road',speed) +path[#path+1]=routines.ground.buildWP(posStart,'on_road',speed) +path[#path+1]=routines.ground.buildWP(offset,'on_road',speed) +else +path[#path+1]=routines.ground.buildWP({['x']=posStart.x+25,['z']=posStart.z+25},form,speed) +end +path[#path+1]=routines.ground.buildWP(offset,form,speed) +path[#path+1]=routines.ground.buildWP(rndCoord,form,speed) +routines.goRoute(group,path) +return +end +routines.groupRandomDistSelf=function(gpData,dist,form,heading,speed) +local pos=routines.getLeadPos(gpData) +local fakeZone={} +fakeZone.radius=dist or math.random(300,1000) +fakeZone.point={x=pos.x,y,pos.y,z=pos.z} +routines.groupToRandomZone(gpData,fakeZone,form,heading,speed) +return +end +routines.groupToRandomZone=function(gpData,zone,form,heading,speed) +if type(gpData)=='string'then +gpData=Group.getByName(gpData) +end +if type(zone)=='string'then +zone=trigger.misc.getZone(zone) +elseif type(zone)=='table'and not zone.radius then +zone=trigger.misc.getZone(zone[math.random(1,#zone)]) +end +if speed then +speed=routines.utils.kmphToMps(speed) +end +local vars={} +vars.group=gpData +vars.radius=zone.radius +vars.form=form +vars.headingDegrees=heading +vars.speed=speed +vars.point=routines.utils.zoneToVec3(zone) +routines.groupToRandomPoint(vars) +return +end +routines.isTerrainValid=function(coord,terrainTypes) +if coord.z then +coord.y=coord.z +end +local typeConverted={} +if type(terrainTypes)=='string'then +for constId,constData in pairs(land.SurfaceType)do +if string.lower(constId)==string.lower(terrainTypes)or string.lower(constData)==string.lower(terrainTypes)then +table.insert(typeConverted,constId) +end +end +elseif type(terrainTypes)=='table'then +for typeId,typeData in pairs(terrainTypes)do +for constId,constData in pairs(land.SurfaceType)do +if string.lower(constId)==string.lower(typeData)or string.lower(constData)==string.lower(typeId)then +table.insert(typeConverted,constId) +end +end +end +end +for validIndex,validData in pairs(typeConverted)do +if land.getSurfaceType(coord)==land.SurfaceType[validData]then +return true +end +end +return false +end +routines.groupToPoint=function(gpData,point,form,heading,speed,useRoads) +if type(point)=='string'then +point=trigger.misc.getZone(point) +end +if speed then +speed=routines.utils.kmphToMps(speed) +end +local vars={} +vars.group=gpData +vars.form=form +vars.headingDegrees=heading +vars.speed=speed +vars.disableRoads=useRoads +vars.point=routines.utils.zoneToVec3(point) +routines.groupToRandomPoint(vars) +return +end +routines.getLeadPos=function(group) +if type(group)=='string'then +group=Group.getByName(group) +end +local units=group:getUnits() +local leader=units[1] +if not leader then +local lowestInd=math.huge +for ind,unit in pairs(units)do +if ind0 then +local maxPos=-math.huge +local maxPosInd +for i=1,#unitPosTbl do +local rotatedVec2=routines.vec.rotateVec2(routines.utils.makeVec2(unitPosTbl[i]),heading) +if(not maxPos)or maxPos=1.0 then +CurrentZoneID=routines.IsUnitInZones(CargoUnit,LandingZones) +if CurrentZoneID then +break +end +end +end +end +return CurrentZoneID +end +function routines.IsUnitInZones(TransportUnit,LandingZones) +local TransportZoneResult=nil +local TransportZonePos=nil +local TransportZone=nil +if TransportUnit then +local TransportUnitPos=TransportUnit:getPosition().p +if type(LandingZones)=="table"then +for LandingZoneID,LandingZoneName in pairs(LandingZones)do +TransportZone=trigger.misc.getZone(LandingZoneName) +if TransportZone then +TransportZonePos={radius=TransportZone.radius,x=TransportZone.point.x,y=TransportZone.point.y,z=TransportZone.point.z} +if(((TransportUnitPos.x-TransportZonePos.x)^2+(TransportUnitPos.z-TransportZonePos.z)^2)^0.5<=TransportZonePos.radius)then +TransportZoneResult=LandingZoneID +break +end +end +end +else +TransportZone=trigger.misc.getZone(LandingZones) +TransportZonePos={radius=TransportZone.radius,x=TransportZone.point.x,y=TransportZone.point.y,z=TransportZone.point.z} +if(((TransportUnitPos.x-TransportZonePos.x)^2+(TransportUnitPos.z-TransportZonePos.z)^2)^0.5<=TransportZonePos.radius)then +TransportZoneResult=1 +end +end +if TransportZoneResult then +else +end +return TransportZoneResult +else +return nil +end +end +function routines.IsUnitNearZonesRadius(TransportUnit,LandingZones,ZoneRadius) +local TransportZoneResult=nil +local TransportZonePos=nil +local TransportZone=nil +if TransportUnit then +local TransportUnitPos=TransportUnit:getPosition().p +if type(LandingZones)=="table"then +for LandingZoneID,LandingZoneName in pairs(LandingZones)do +TransportZone=trigger.misc.getZone(LandingZoneName) +if TransportZone then +TransportZonePos={radius=TransportZone.radius,x=TransportZone.point.x,y=TransportZone.point.y,z=TransportZone.point.z} +if(((TransportUnitPos.x-TransportZonePos.x)^2+(TransportUnitPos.z-TransportZonePos.z)^2)^0.5<=ZoneRadius)then +TransportZoneResult=LandingZoneID +break +end +end +end +else +TransportZone=trigger.misc.getZone(LandingZones) +TransportZonePos={radius=TransportZone.radius,x=TransportZone.point.x,y=TransportZone.point.y,z=TransportZone.point.z} +if(((TransportUnitPos.x-TransportZonePos.x)^2+(TransportUnitPos.z-TransportZonePos.z)^2)^0.5<=ZoneRadius)then +TransportZoneResult=1 +end +end +if TransportZoneResult then +else +end +return TransportZoneResult +else +return nil +end +end +function routines.IsStaticInZones(TransportStatic,LandingZones) +local TransportZoneResult=nil +local TransportZonePos=nil +local TransportZone=nil +local TransportStaticPos=TransportStatic:getPosition().p +if type(LandingZones)=="table"then +for LandingZoneID,LandingZoneName in pairs(LandingZones)do +TransportZone=trigger.misc.getZone(LandingZoneName) +if TransportZone then +TransportZonePos={radius=TransportZone.radius,x=TransportZone.point.x,y=TransportZone.point.y,z=TransportZone.point.z} +if(((TransportStaticPos.x-TransportZonePos.x)^2+(TransportStaticPos.z-TransportZonePos.z)^2)^0.5<=TransportZonePos.radius)then +TransportZoneResult=LandingZoneID +break +end +end +end +else +TransportZone=trigger.misc.getZone(LandingZones) +TransportZonePos={radius=TransportZone.radius,x=TransportZone.point.x,y=TransportZone.point.y,z=TransportZone.point.z} +if(((TransportStaticPos.x-TransportZonePos.x)^2+(TransportStaticPos.z-TransportZonePos.z)^2)^0.5<=TransportZonePos.radius)then +TransportZoneResult=1 +end +end +return TransportZoneResult +end +function routines.IsUnitInRadius(CargoUnit,ReferencePosition,Radius) +local Valid=true +local CargoPos=CargoUnit:getPosition().p +local ReferenceP=ReferencePosition.p +if(((CargoPos.x-ReferenceP.x)^2+(CargoPos.z-ReferenceP.z)^2)^0.5<=Radius)then +else +Valid=false +end +return Valid +end +function routines.IsPartOfGroupInRadius(CargoGroup,ReferencePosition,Radius) +local Valid=true +Valid=routines.ValidateGroup(CargoGroup,"CargoGroup",Valid) +local CargoUnits=CargoGroup:getUnits() +for CargoUnitId,CargoUnit in pairs(CargoUnits)do +local CargoUnitPos=CargoUnit:getPosition().p +local ReferenceP=ReferencePosition.p +if(((CargoUnitPos.x-ReferenceP.x)^2+(CargoUnitPos.z-ReferenceP.z)^2)^0.5<=Radius)then +else +Valid=false +break +end +end +return Valid +end +function routines.ValidateString(Variable,VariableName,Valid) +if type(Variable)=="string"then +if Variable==""then +error("routines.ValidateString: error: "..VariableName.." must be filled out!") +Valid=false +end +else +error("routines.ValidateString: error: "..VariableName.." is not a string.") +Valid=false +end +return Valid +end +function routines.ValidateNumber(Variable,VariableName,Valid) +if type(Variable)=="number"then +else +error("routines.ValidateNumber: error: "..VariableName.." is not a number.") +Valid=false +end +return Valid +end +function routines.ValidateGroup(Variable,VariableName,Valid) +if Variable==nil then +error("routines.ValidateGroup: error: "..VariableName.." is a nil value!") +Valid=false +end +return Valid +end +function routines.ValidateZone(LandingZones,VariableName,Valid) +if LandingZones==nil then +error("routines.ValidateGroup: error: "..VariableName.." is a nil value!") +Valid=false +end +if type(LandingZones)=="table"then +for LandingZoneID,LandingZoneName in pairs(LandingZones)do +if trigger.misc.getZone(LandingZoneName)==nil then +error("routines.ValidateGroup: error: Zone "..LandingZoneName.." does not exist!") +Valid=false +break +end +end +else +if trigger.misc.getZone(LandingZones)==nil then +error("routines.ValidateGroup: error: Zone "..LandingZones.." does not exist!") +Valid=false +end +end +return Valid +end +function routines.ValidateEnumeration(Variable,VariableName,Enum,Valid) +local ValidVariable=false +for EnumId,EnumData in pairs(Enum)do +if Variable==EnumData then +ValidVariable=true +break +end +end +if ValidVariable then +else +error('TransportValidateEnum: " .. VariableName .. " is not a valid type.'..Variable) +Valid=false +end +return Valid +end +function routines.getGroupRoute(groupIdent,task) +local gpId=groupIdent +if type(groupIdent)=='string'and not tonumber(groupIdent)then +gpId=_DATABASE.Templates.Groups[groupIdent].groupId +end +for coa_name,coa_data in pairs(env.mission.coalition)do +if(coa_name=='red'or coa_name=='blue')and type(coa_data)=='table'then +if coa_data.country then +for cntry_id,cntry_data in pairs(coa_data.country)do +for obj_type_name,obj_type_data in pairs(cntry_data)do +if obj_type_name=="helicopter"or obj_type_name=="ship"or obj_type_name=="plane"or obj_type_name=="vehicle"then +if((type(obj_type_data)=='table')and obj_type_data.group and(type(obj_type_data.group)=='table')and(#obj_type_data.group>0))then +for group_num,group_data in pairs(obj_type_data.group)do +if group_data and group_data.groupId==gpId then +if group_data.route and group_data.route.points and#group_data.route.points>0 then +local points={} +for point_num,point in pairs(group_data.route.points)do +local routeData={} +if env.mission.version>7 then +routeData.name=env.getValueDictByKey(point.name) +else +routeData.name=point.name +end +if not point.point then +routeData.x=point.x +routeData.y=point.y +else +routeData.point=point.point +end +routeData.form=point.action +routeData.speed=point.speed +routeData.alt=point.alt +routeData.alt_type=point.alt_type +routeData.airdromeId=point.airdromeId +routeData.helipadId=point.helipadId +routeData.type=point.type +routeData.action=point.action +if task then +routeData.task=point.task +end +points[point_num]=routeData +end +return points +end +return +end +end +end +end +end +end +end +end +end +end +routines.ground.patrolRoute=function(vars) +local tempRoute={} +local useRoute={} +local gpData=vars.gpData +if type(gpData)=='string'then +gpData=Group.getByName(gpData) +end +local useGroupRoute +if not vars.useGroupRoute then +useGroupRoute=vars.gpData +else +useGroupRoute=vars.useGroupRoute +end +local routeProvided=false +if not vars.route then +if useGroupRoute then +tempRoute=routines.getGroupRoute(useGroupRoute) +end +else +useRoute=vars.route +local posStart=routines.getLeadPos(gpData) +useRoute[1]=routines.ground.buildWP(posStart,useRoute[1].action,useRoute[1].speed) +routeProvided=true +end +local overRideSpeed=vars.speed or'default' +local pType=vars.pType +local offRoadForm=vars.offRoadForm or'default' +local onRoadForm=vars.onRoadForm or'default' +if routeProvided==false and#tempRoute>0 then +local posStart=routines.getLeadPos(gpData) +useRoute[#useRoute+1]=routines.ground.buildWP(posStart,offRoadForm,overRideSpeed) +for i=1,#tempRoute do +local tempForm=tempRoute[i].action +local tempSpeed=tempRoute[i].speed +if offRoadForm=='default'then +tempForm=tempRoute[i].action +end +if onRoadForm=='default'then +onRoadForm='On Road' +end +if(string.lower(tempRoute[i].action)=='on road'or string.lower(tempRoute[i].action)=='onroad'or string.lower(tempRoute[i].action)=='on_road')then +tempForm=onRoadForm +else +tempForm=offRoadForm +end +if type(overRideSpeed)=='number'then +tempSpeed=overRideSpeed +end +useRoute[#useRoute+1]=routines.ground.buildWP(tempRoute[i],tempForm,tempSpeed) +end +if pType and string.lower(pType)=='doubleback'then +local curRoute=routines.utils.deepCopy(useRoute) +for i=#curRoute,2,-1 do +useRoute[#useRoute+1]=routines.ground.buildWP(curRoute[i],curRoute[i].action,curRoute[i].speed) +end +end +useRoute[1].action=useRoute[#useRoute].action +end +local cTask3={} +local newPatrol={} +newPatrol.route=useRoute +newPatrol.gpData=gpData:getName() +cTask3[#cTask3+1]='routines.ground.patrolRoute(' +cTask3[#cTask3+1]=routines.utils.oneLineSerialize(newPatrol) +cTask3[#cTask3+1]=')' +cTask3=table.concat(cTask3) +local tempTask={ +id='WrappedAction', +params={ +action={ +id='Script', +params={ +command=cTask3, +}, +}, +}, +} +useRoute[#useRoute].task=tempTask +routines.goRoute(gpData,useRoute) +return +end +routines.ground.patrol=function(gpData,pType,form,speed) +local vars={} +if type(gpData)=='table'and gpData:getName()then +gpData=gpData:getName() +end +vars.useGroupRoute=gpData +vars.gpData=gpData +vars.pType=pType +vars.offRoadForm=form +vars.speed=speed +routines.ground.patrolRoute(vars) +return +end +function routines.GetUnitHeight(CheckUnit) +local UnitPoint=CheckUnit:getPoint() +local UnitPosition={x=UnitPoint.x,y=UnitPoint.z} +local UnitHeight=UnitPoint.y +local LandHeight=land.getHeight(UnitPosition) +return UnitHeight-LandHeight +end +Su34Status={status={}} +boardMsgRed={statusMsg=""} +boardMsgAll={timeMsg=""} +SpawnSettings={} +Su34MenuPath={} +Su34Menus=0 +function Su34AttackCarlVinson(groupName) +local groupSu34=Group.getByName(groupName) +local controllerSu34=groupSu34.getController(groupSu34) +local groupCarlVinson=Group.getByName("US Carl Vinson #001") +controllerSu34.setOption(controllerSu34,AI.Option.Air.id.ROE,AI.Option.Air.val.ROE.OPEN_FIRE) +controllerSu34.setOption(controllerSu34,AI.Option.Air.id.REACTION_ON_THREAT,AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE) +if groupCarlVinson~=nil then +controllerSu34.pushTask(controllerSu34,{id='AttackGroup',params={groupId=groupCarlVinson:getID(),expend=AI.Task.WeaponExpend.ALL,attackQtyLimit=true}}) +end +Su34Status.status[groupName]=1 +MessageToRed(string.format('%s: ',groupName)..'Attacking carrier Carl Vinson. ',10,'RedStatus'..groupName) +end +function Su34AttackWest(groupName) +local groupSu34=Group.getByName(groupName) +local controllerSu34=groupSu34.getController(groupSu34) +local groupShipWest1=Group.getByName("US Ship West #001") +local groupShipWest2=Group.getByName("US Ship West #002") +controllerSu34.setOption(controllerSu34,AI.Option.Air.id.ROE,AI.Option.Air.val.ROE.OPEN_FIRE) +controllerSu34.setOption(controllerSu34,AI.Option.Air.id.REACTION_ON_THREAT,AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE) +if groupShipWest1~=nil then +controllerSu34.pushTask(controllerSu34,{id='AttackGroup',params={groupId=groupShipWest1:getID(),expend=AI.Task.WeaponExpend.ALL,attackQtyLimit=true}}) +end +if groupShipWest2~=nil then +controllerSu34.pushTask(controllerSu34,{id='AttackGroup',params={groupId=groupShipWest2:getID(),expend=AI.Task.WeaponExpend.ALL,attackQtyLimit=true}}) +end +Su34Status.status[groupName]=2 +MessageToRed(string.format('%s: ',groupName)..'Attacking invading ships in the west. ',10,'RedStatus'..groupName) +end +function Su34AttackNorth(groupName) +local groupSu34=Group.getByName(groupName) +local controllerSu34=groupSu34.getController(groupSu34) +local groupShipNorth1=Group.getByName("US Ship North #001") +local groupShipNorth2=Group.getByName("US Ship North #002") +local groupShipNorth3=Group.getByName("US Ship North #003") +controllerSu34.setOption(controllerSu34,AI.Option.Air.id.ROE,AI.Option.Air.val.ROE.OPEN_FIRE) +controllerSu34.setOption(controllerSu34,AI.Option.Air.id.REACTION_ON_THREAT,AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE) +if groupShipNorth1~=nil then +controllerSu34.pushTask(controllerSu34,{id='AttackGroup',params={groupId=groupShipNorth1:getID(),expend=AI.Task.WeaponExpend.ALL,attackQtyLimit=false}}) +end +if groupShipNorth2~=nil then +controllerSu34.pushTask(controllerSu34,{id='AttackGroup',params={groupId=groupShipNorth2:getID(),expend=AI.Task.WeaponExpend.ALL,attackQtyLimit=false}}) +end +if groupShipNorth3~=nil then +controllerSu34.pushTask(controllerSu34,{id='AttackGroup',params={groupId=groupShipNorth3:getID(),expend=AI.Task.WeaponExpend.ALL,attackQtyLimit=false}}) +end +Su34Status.status[groupName]=3 +MessageToRed(string.format('%s: ',groupName)..'Attacking invading ships in the north. ',10,'RedStatus'..groupName) +end +function Su34Orbit(groupName) +local groupSu34=Group.getByName(groupName) +local controllerSu34=groupSu34:getController() +controllerSu34.setOption(controllerSu34,AI.Option.Air.id.ROE,AI.Option.Air.val.ROE.WEAPON_HOLD) +controllerSu34.setOption(controllerSu34,AI.Option.Air.id.REACTION_ON_THREAT,AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE) +controllerSu34:pushTask({id='ControlledTask',params={task={id='Orbit',params={pattern=AI.Task.OrbitPattern.RACE_TRACK}},stopCondition={duration=600}}}) +Su34Status.status[groupName]=4 +MessageToRed(string.format('%s: ',groupName)..'In orbit and awaiting further instructions. ',10,'RedStatus'..groupName) +end +function Su34TakeOff(groupName) +local groupSu34=Group.getByName(groupName) +local controllerSu34=groupSu34:getController() +controllerSu34.setOption(controllerSu34,AI.Option.Air.id.ROE,AI.Option.Air.val.ROE.WEAPON_HOLD) +controllerSu34.setOption(controllerSu34,AI.Option.Air.id.REACTION_ON_THREAT,AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE) +Su34Status.status[groupName]=8 +MessageToRed(string.format('%s: ',groupName)..'Take-Off. ',10,'RedStatus'..groupName) +end +function Su34Hold(groupName) +local groupSu34=Group.getByName(groupName) +local controllerSu34=groupSu34:getController() +controllerSu34.setOption(controllerSu34,AI.Option.Air.id.ROE,AI.Option.Air.val.ROE.WEAPON_HOLD) +controllerSu34.setOption(controllerSu34,AI.Option.Air.id.REACTION_ON_THREAT,AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE) +Su34Status.status[groupName]=5 +MessageToRed(string.format('%s: ',groupName)..'Holding Weapons. ',10,'RedStatus'..groupName) +end +function Su34RTB(groupName) +Su34Status.status[groupName]=6 +MessageToRed(string.format('%s: ',groupName)..'Return to Krasnodar. ',10,'RedStatus'..groupName) +end +function Su34Destroyed(groupName) +Su34Status.status[groupName]=7 +MessageToRed(string.format('%s: ',groupName)..'Destroyed. ',30,'RedStatus'..groupName) +end +function GroupAlive(groupName) +local groupTest=Group.getByName(groupName) +local groupExists=false +if groupTest then +groupExists=groupTest:isExist() +end +return groupExists +end +function Su34IsDead() +end +function Su34OverviewStatus() +local msg="" +local currentStatus=0 +local Exists=false +for groupName,currentStatus in pairs(Su34Status.status)do +env.info(('Su34 Overview Status: GroupName = '..groupName)) +Alive=GroupAlive(groupName) +if Alive then +if currentStatus==1 then +msg=msg..string.format("%s: ",groupName) +msg=msg.."Attacking carrier Carl Vinson. " +elseif currentStatus==2 then +msg=msg..string.format("%s: ",groupName) +msg=msg.."Attacking supporting ships in the west. " +elseif currentStatus==3 then +msg=msg..string.format("%s: ",groupName) +msg=msg.."Attacking invading ships in the north. " +elseif currentStatus==4 then +msg=msg..string.format("%s: ",groupName) +msg=msg.."In orbit and awaiting further instructions. " +elseif currentStatus==5 then +msg=msg..string.format("%s: ",groupName) +msg=msg.."Holding Weapons. " +elseif currentStatus==6 then +msg=msg..string.format("%s: ",groupName) +msg=msg.."Return to Krasnodar. " +elseif currentStatus==7 then +msg=msg..string.format("%s: ",groupName) +msg=msg.."Destroyed. " +elseif currentStatus==8 then +msg=msg..string.format("%s: ",groupName) +msg=msg.."Take-Off. " +end +else +if currentStatus==7 then +msg=msg..string.format("%s: ",groupName) +msg=msg.."Destroyed. " +else +Su34Destroyed(groupName) +end +end +end +boardMsgRed.statusMsg=msg +end +function UpdateBoardMsg() +Su34OverviewStatus() +MessageToRed(boardMsgRed.statusMsg,15,'RedStatus') +end +function MusicReset(flg) +trigger.action.setUserFlag(95,flg) +end +function PlaneActivate(groupNameFormat,flg) +local groupName=groupNameFormat..string.format("#%03d",trigger.misc.getUserFlag(flg)) +trigger.action.activateGroup(Group.getByName(groupName)) +end +function Su34Menu(groupName) +local groupSu34=Group.getByName(groupName) +if Su34Status.status[groupName]==1 or +Su34Status.status[groupName]==2 or +Su34Status.status[groupName]==3 or +Su34Status.status[groupName]==4 or +Su34Status.status[groupName]==5 then +if Su34MenuPath[groupName]==nil then +if planeMenuPath==nil then +planeMenuPath=missionCommands.addSubMenuForCoalition( +coalition.side.RED, +"SU-34 anti-ship flights", +nil +) +end +Su34MenuPath[groupName]=missionCommands.addSubMenuForCoalition( +coalition.side.RED, +"Flight "..groupName, +planeMenuPath +) +missionCommands.addCommandForCoalition( +coalition.side.RED, +"Attack carrier Carl Vinson", +Su34MenuPath[groupName], +Su34AttackCarlVinson, +groupName +) +missionCommands.addCommandForCoalition( +coalition.side.RED, +"Attack ships in the west", +Su34MenuPath[groupName], +Su34AttackWest, +groupName +) +missionCommands.addCommandForCoalition( +coalition.side.RED, +"Attack ships in the north", +Su34MenuPath[groupName], +Su34AttackNorth, +groupName +) +missionCommands.addCommandForCoalition( +coalition.side.RED, +"Hold position and await instructions", +Su34MenuPath[groupName], +Su34Orbit, +groupName +) +missionCommands.addCommandForCoalition( +coalition.side.RED, +"Report status", +Su34MenuPath[groupName], +Su34OverviewStatus +) +end +else +if Su34MenuPath[groupName]then +missionCommands.removeItemForCoalition(coalition.side.RED,Su34MenuPath[groupName]) +end +end +end +function ChooseInfantry(TeleportPrefixTable,TeleportMax) +TeleportPrefixTableCount=#TeleportPrefixTable +TeleportPrefixTableIndex=math.random(1,TeleportPrefixTableCount) +local TeleportFound=false +local TeleportLoop=true +local Index=TeleportPrefixTableIndex +local TeleportPrefix='' +while TeleportLoop do +TeleportPrefix=TeleportPrefixTable[Index] +if SpawnSettings[TeleportPrefix]then +if SpawnSettings[TeleportPrefix]['SpawnCount']-10 then +local PlayerFound=false +local MusicStart=0 +local MusicTime=0 +for SndQueueIdx,SndQueue in pairs(_MusicTable.Queue)do +if SndQueue.PlayerName==PlayerName then +PlayerFound=true +MusicStart=SndQueue.Start +MusicTime=_MusicTable.Files[SndQueue.Ref].Time +break +end +end +if PlayerFound then +if MusicStart+MusicTime<=timer.getTime()then +MusicOut=true +end +else +MusicOut=true +end +end +if MusicOut then +else +end +return MusicOut +end +function MusicScheduler() +if _MusicTable['Queue']~=nil and _MusicTable.FileCnt>0 then +for SndQueueIdx,SndQueue in pairs(_MusicTable.Queue)do +if SndQueue.Continue then +if MusicCanStart(SndQueue.PlayerName)then +MusicToPlayer('',SndQueue.PlayerName,true) +end +end +end +end +end +env.info(('Init: Scripts Loaded v1.1')) +SMOKECOLOR=trigger.smokeColor +FLARECOLOR=trigger.flareColor +BIGSMOKEPRESET={ +SmallSmokeAndFire=1, +MediumSmokeAndFire=2, +LargeSmokeAndFire=3, +HugeSmokeAndFire=4, +SmallSmoke=5, +MediumSmoke=6, +LargeSmoke=7, +HugeSmoke=8, +} +DCSMAP={ +Caucasus="Caucasus", +NTTR="Nevada", +Normandy="Normandy", +PersianGulf="PersianGulf", +TheChannel="TheChannel", +Syria="Syria", +} +CALLSIGN={ +Aircraft={ +Enfield=1, +Springfield=2, +Uzi=3, +Colt=4, +Dodge=5, +Ford=6, +Chevy=7, +Pontiac=8, +Hawg=9, +Boar=10, +Pig=11, +Tusk=12, +}, +AWACS={ +Overlord=1, +Magic=2, +Wizard=3, +Focus=4, +Darkstar=5, +}, +Tanker={ +Texaco=1, +Arco=2, +Shell=3, +}, +JTAC={ +Axeman=1, +Darknight=2, +Warrior=3, +Pointer=4, +Eyeball=5, +Moonbeam=6, +Whiplash=7, +Finger=8, +Pinpoint=9, +Ferret=10, +Shaba=11, +Playboy=12, +Hammer=13, +Jaguar=14, +Deathstar=15, +Anvil=16, +Firefly=17, +Mantis=18, +Badger=19, +}, +FARP={ +London=1, +Dallas=2, +Paris=3, +Moscow=4, +Berlin=5, +Rome=6, +Madrid=7, +Warsaw=8, +Dublin=9, +Perth=10, +}, +} +UTILS={ +_MarkID=1 +} +UTILS.IsInstanceOf=function(object,className) +if not type(className)=='string'then +if type(className)=='table'and className.IsInstanceOf~=nil then +className=className.ClassName +else +local err_str='className parameter should be a string; parameter received: '..type(className) +return false +end +end +if type(object)=='table'and object.IsInstanceOf~=nil then +return object:IsInstanceOf(className) +else +local basicDataTypes={'string','number','function','boolean','nil','table'} +for _,basicDataType in ipairs(basicDataTypes)do +if className==basicDataType then +return type(object)==basicDataType +end +end +end +return false +end +UTILS.DeepCopy=function(object) +local lookup_table={} +local function _copy(object) +if type(object)~="table"then +return object +elseif lookup_table[object]then +return lookup_table[object] +end +local new_table={} +lookup_table[object]=new_table +for index,value in pairs(object)do +new_table[_copy(index)]=_copy(value) +end +return setmetatable(new_table,getmetatable(object)) +end +local objectreturn=_copy(object) +return objectreturn +end +UTILS.OneLineSerialize=function(tbl) +lookup_table={} +local function _Serialize(tbl) +if type(tbl)=='table'then +if lookup_table[tbl]then +return lookup_table[object] +end +local tbl_str={} +lookup_table[tbl]=tbl_str +tbl_str[#tbl_str+1]='{' +for ind,val in pairs(tbl)do +local ind_str={} +if type(ind)=="number"then +ind_str[#ind_str+1]='[' +ind_str[#ind_str+1]=tostring(ind) +ind_str[#ind_str+1]=']=' +else +ind_str[#ind_str+1]='[' +ind_str[#ind_str+1]=routines.utils.basicSerialize(ind) +ind_str[#ind_str+1]=']=' +end +local val_str={} +if((type(val)=='number')or(type(val)=='boolean'))then +val_str[#val_str+1]=tostring(val) +val_str[#val_str+1]=',' +tbl_str[#tbl_str+1]=table.concat(ind_str) +tbl_str[#tbl_str+1]=table.concat(val_str) +elseif type(val)=='string'then +val_str[#val_str+1]=routines.utils.basicSerialize(val) +val_str[#val_str+1]=',' +tbl_str[#tbl_str+1]=table.concat(ind_str) +tbl_str[#tbl_str+1]=table.concat(val_str) +elseif type(val)=='nil'then +val_str[#val_str+1]='nil,' +tbl_str[#tbl_str+1]=table.concat(ind_str) +tbl_str[#tbl_str+1]=table.concat(val_str) +elseif type(val)=='table'then +if ind=="__index"then +else +val_str[#val_str+1]=_Serialize(val) +val_str[#val_str+1]=',' +tbl_str[#tbl_str+1]=table.concat(ind_str) +tbl_str[#tbl_str+1]=table.concat(val_str) +end +elseif type(val)=='function'then +tbl_str[#tbl_str+1]="f() "..tostring(ind) +tbl_str[#tbl_str+1]=',' +else +env.info('unable to serialize value type '..routines.utils.basicSerialize(type(val))..' at index '..tostring(ind)) +env.info(debug.traceback()) +end +end +tbl_str[#tbl_str+1]='}' +return table.concat(tbl_str) +else +return tostring(tbl) +end +end +local objectreturn=_Serialize(tbl) +return objectreturn +end +UTILS.BasicSerialize=function(s) +if s==nil then +return"\"\"" +else +if((type(s)=='number')or(type(s)=='boolean')or(type(s)=='function')or(type(s)=='table')or(type(s)=='userdata'))then +return tostring(s) +elseif type(s)=='string'then +s=string.format('%q',s) +return s +end +end +end +UTILS.ToDegree=function(angle) +return angle*180/math.pi +end +UTILS.ToRadian=function(angle) +return angle*math.pi/180 +end +UTILS.MetersToNM=function(meters) +return meters/1852 +end +UTILS.KiloMetersToNM=function(kilometers) +return kilometers/1852*1000 +end +UTILS.MetersToSM=function(meters) +return meters/1609.34 +end +UTILS.KiloMetersToSM=function(kilometers) +return kilometers/1609.34*1000 +end +UTILS.MetersToFeet=function(meters) +return meters/0.3048 +end +UTILS.KiloMetersToFeet=function(kilometers) +return kilometers/0.3048*1000 +end +UTILS.NMToMeters=function(NM) +return NM*1852 +end +UTILS.NMToKiloMeters=function(NM) +return NM*1852/1000 +end +UTILS.FeetToMeters=function(feet) +return feet*0.3048 +end +UTILS.KnotsToKmph=function(knots) +return knots*1.852 +end +UTILS.KmphToKnots=function(knots) +return knots/1.852 +end +UTILS.KmphToMps=function(kmph) +return kmph/3.6 +end +UTILS.MpsToKmph=function(mps) +return mps*3.6 +end +UTILS.MiphToMps=function(miph) +return miph*0.44704 +end +UTILS.MpsToMiph=function(mps) +return mps/0.44704 +end +UTILS.MpsToKnots=function(mps) +return mps*1.94384 +end +UTILS.KnotsToMps=function(knots) +return knots/1.94384 +end +UTILS.CelciusToFarenheit=function(Celcius) +return Celcius*9/5+32 +end +UTILS.hPa2inHg=function(hPa) +return hPa*0.0295299830714 +end +UTILS.KnotsToAltKIAS=function(knots,altitude) +return(knots*0.018*(altitude/1000))+knots +end +UTILS.hPa2mmHg=function(hPa) +return hPa*0.7500615613030 +end +UTILS.kg2lbs=function(kg) +return kg*2.20462 +end +UTILS.tostringLL=function(lat,lon,acc,DMS) +local latHemi,lonHemi +if lat>0 then +latHemi='N' +else +latHemi='S' +end +if lon>0 then +lonHemi='E' +else +lonHemi='W' +end +lat=math.abs(lat) +lon=math.abs(lon) +local latDeg=math.floor(lat) +local latMin=(lat-latDeg)*60 +local lonDeg=math.floor(lon) +local lonMin=(lon-lonDeg)*60 +if DMS then +local oldLatMin=latMin +latMin=math.floor(latMin) +local latSec=UTILS.Round((oldLatMin-latMin)*60,acc) +local oldLonMin=lonMin +lonMin=math.floor(lonMin) +local lonSec=UTILS.Round((oldLonMin-lonMin)*60,acc) +if latSec==60 then +latSec=0 +latMin=latMin+1 +end +if lonSec==60 then +lonSec=0 +lonMin=lonMin+1 +end +local secFrmtStr +secFrmtStr='%02d' +if acc<=0 then +secFrmtStr='%02d' +else +local width=3+acc +secFrmtStr='%0'..width..'.'..acc..'f' +end +return string.format('%03d°',latDeg)..string.format('%02d',latMin)..'\''..string.format(secFrmtStr,latSec)..'"'..latHemi..' ' +..string.format('%03d°',lonDeg)..string.format('%02d',lonMin)..'\''..string.format(secFrmtStr,lonSec)..'"'..lonHemi +else +latMin=UTILS.Round(latMin,acc) +lonMin=UTILS.Round(lonMin,acc) +if latMin==60 then +latMin=0 +latDeg=latDeg+1 +end +if lonMin==60 then +lonMin=0 +lonDeg=lonDeg+1 +end +local minFrmtStr +if acc<=0 then +minFrmtStr='%02d' +else +local width=3+acc +minFrmtStr='%0'..width..'.'..acc..'f' +end +return string.format('%03d°',latDeg)..' '..string.format(minFrmtStr,latMin)..'\''..latHemi..' ' +..string.format('%03d°',lonDeg)..' '..string.format(minFrmtStr,lonMin)..'\''..lonHemi +end +end +UTILS.tostringMGRS=function(MGRS,acc) +if acc==0 then +return MGRS.UTMZone..' '..MGRS.MGRSDigraph +else +local Easting=tostring(MGRS.Easting) +local Northing=tostring(MGRS.Northing) +local nE=5-string.len(Easting) +local nN=5-string.len(Northing) +for i=1,nE do Easting="0"..Easting end +for i=1,nN do Northing="0"..Northing end +return string.format("%s %s %s %s",MGRS.UTMZone,MGRS.MGRSDigraph,string.sub(Easting,1,acc),string.sub(Northing,1,acc)) +end +end +function UTILS.Round(num,idp) +local mult=10^(idp or 0) +return math.floor(num*mult+0.5)/mult +end +function UTILS.DoString(s) +local f,err=loadstring(s) +if f then +return true,f() +else +return false,err +end +end +function UTILS.spairs(t,order) +local keys={} +for k in pairs(t)do keys[#keys+1]=k end +if order then +table.sort(keys,function(a,b)return order(t,a,b)end) +else +table.sort(keys) +end +local i=0 +return function() +i=i+1 +if keys[i]then +return keys[i],t[keys[i]] +end +end +end +function UTILS.kpairs(t,getkey,order) +local keys={} +local keyso={} +for k,o in pairs(t)do keys[#keys+1]=k keyso[#keyso+1]=getkey(o)end +if order then +table.sort(keys,function(a,b)return order(t,a,b)end) +else +table.sort(keys) +end +local i=0 +return function() +i=i+1 +if keys[i]then +return keyso[i],t[keys[i]] +end +end +end +function UTILS.rpairs(t) +local keys={} +for k in pairs(t)do keys[#keys+1]=k end +local random={} +local j=#keys +for i=1,j do +local k=math.random(1,#keys) +random[i]=keys[k] +table.remove(keys,k) +end +local i=0 +return function() +i=i+1 +if random[i]then +return random[i],t[random[i]] +end +end +end +function UTILS.GetMarkID() +UTILS._MarkID=UTILS._MarkID+1 +return UTILS._MarkID +end +function UTILS.IsInRadius(InVec2,Vec2,Radius) +local InRadius=((InVec2.x-Vec2.x)^2+(InVec2.y-Vec2.y)^2)^0.5<=Radius +return InRadius +end +function UTILS.IsInSphere(InVec3,Vec3,Radius) +local InSphere=((InVec3.x-Vec3.x)^2+(InVec3.y-Vec3.y)^2+(InVec3.z-Vec3.z)^2)^0.5<=Radius +return InSphere +end +function UTILS.BeaufortScale(speed) +local bn=nil +local bd=nil +if speed<0.51 then +bn=0 +bd="Calm" +elseif speed<2.06 then +bn=1 +bd="Light Air" +elseif speed<3.60 then +bn=2 +bd="Light Breeze" +elseif speed<5.66 then +bn=3 +bd="Gentle Breeze" +elseif speed<8.23 then +bn=4 +bd="Moderate Breeze" +elseif speed<11.32 then +bn=5 +bd="Fresh Breeze" +elseif speed<14.40 then +bn=6 +bd="Strong Breeze" +elseif speed<17.49 then +bn=7 +bd="Moderate Gale" +elseif speed<21.09 then +bn=8 +bd="Fresh Gale" +elseif speed<24.69 then +bn=9 +bd="Strong Gale" +elseif speed<28.81 then +bn=10 +bd="Storm" +elseif speed<32.92 then +bn=11 +bd="Violent Storm" +else +bn=12 +bd="Hurricane" +end +return bn,bd +end +function UTILS.Split(str,sep) +local result={} +local regex=("([^%s]+)"):format(sep) +for each in str:gmatch(regex)do +table.insert(result,each) +end +return result +end +function UTILS.SecondsToClock(seconds,short) +if seconds==nil then +return nil +end +local seconds=tonumber(seconds) +local _seconds=seconds%(60*60*24) +if seconds<0 then +return nil +else +local hours=string.format("%02.f",math.floor(_seconds/3600)) +local mins=string.format("%02.f",math.floor(_seconds/60-(hours*60))) +local secs=string.format("%02.f",math.floor(_seconds-hours*3600-mins*60)) +local days=string.format("%d",seconds/(60*60*24)) +local clock=hours..":"..mins..":"..secs.."+"..days +if short then +if hours=="00"then +clock=hours..":"..mins..":"..secs +else +clock=hours..":"..mins..":"..secs +end +end +return clock +end +end +function UTILS.SecondsOfToday() +local time=timer.getAbsTime() +local clock=UTILS.SecondsToClock(time,true) +return UTILS.ClockToSeconds(clock) +end +function UTILS.SecondsToMidnight() +return 24*60*60-UTILS.SecondsOfToday() +end +function UTILS.ClockToSeconds(clock) +if clock==nil then +return nil +end +local seconds=0 +local dsplit=UTILS.Split(clock,"+") +if#dsplit>1 then +seconds=seconds+tonumber(dsplit[2])*60*60*24 +end +local tsplit=UTILS.Split(dsplit[1],":") +local i=1 +for _,time in ipairs(tsplit)do +if i==1 then +seconds=seconds+tonumber(time)*60*60 +elseif i==2 then +seconds=seconds+tonumber(time)*60 +elseif i==3 then +seconds=seconds+tonumber(time) +end +i=i+1 +end +return seconds +end +function UTILS.DisplayMissionTime(duration) +duration=duration or 5 +local Tnow=timer.getAbsTime() +local mission_time=Tnow-timer.getTime0() +local mission_time_minutes=mission_time/60 +local mission_time_seconds=mission_time%60 +local local_time=UTILS.SecondsToClock(Tnow) +local text=string.format("Time: %s - %02d:%02d",local_time,mission_time_minutes,mission_time_seconds) +MESSAGE:New(text,duration):ToAll() +end +function UTILS.ReplaceIllegalCharacters(Text,ReplaceBy) +ReplaceBy=ReplaceBy or"_" +local text=Text:gsub("[<>|/?*:\\]",ReplaceBy) +return text +end +function UTILS.RandomGaussian(x0,sigma,xmin,xmax,imax) +sigma=sigma or 10 +imax=imax or 100 +local r +local gotit=false +local i=0 +while not gotit do +local x1=math.random() +local x2=math.random() +r=math.sqrt(-2*sigma*sigma*math.log(x1))*math.cos(2*math.pi*x2)+x0 +i=i+1 +if(r>=xmin and r<=xmax)or i>imax then +gotit=true +end +end +return r +end +function UTILS.Randomize(value,fac,lower,upper) +local min +if lower then +min=math.max(value-value*fac,lower) +else +min=value-value*fac +end +local max +if upper then +max=math.min(value+value*fac,upper) +else +max=value+value*fac +end +local r=math.random(min,max) +return r +end +function UTILS.VecDot(a,b) +return a.x*b.x+a.y*b.y+a.z*b.z +end +function UTILS.VecNorm(a) +return math.sqrt(UTILS.VecDot(a,a)) +end +function UTILS.VecDist2D(a,b) +local c={x=b.x-a.x,y=b.y-a.y} +local d=math.sqrt(c.x*c.x+c.y*c.y) +return d +end +function UTILS.VecDist3D(a,b) +local c={x=b.x-a.x,y=b.y-a.y,z=b.z-a.z} +local d=math.sqrt(UTILS.VecDot(c,c)) +return d +end +function UTILS.VecCross(a,b) +return{x=a.y*b.z-a.z*b.y,y=a.z*b.x-a.x*b.z,z=a.x*b.y-a.y*b.x} +end +function UTILS.VecSubstract(a,b) +return{x=a.x-b.x,y=a.y-b.y,z=a.z-b.z} +end +function UTILS.VecAdd(a,b) +return{x=a.x+b.x,y=a.y+b.y,z=a.z+b.z} +end +function UTILS.VecAngle(a,b) +local cosalpha=UTILS.VecDot(a,b)/(UTILS.VecNorm(a)*UTILS.VecNorm(b)) +local alpha=0 +if cosalpha>=0.9999999999 then +alpha=0 +elseif cosalpha<=-0.999999999 then +alpha=math.pi +else +alpha=math.acos(cosalpha) +end +return math.deg(alpha) +end +function UTILS.VecHdg(a) +local h=math.deg(math.atan2(a.z,a.x)) +if h<0 then +h=h+360 +end +return h +end +function UTILS.HdgDiff(h1,h2) +local alpha=math.rad(tonumber(h1)) +local beta=math.rad(tonumber(h2)) +local v1={x=math.cos(alpha),y=0,z=math.sin(alpha)} +local v2={x=math.cos(beta),y=0,z=math.sin(beta)} +local delta=UTILS.VecAngle(v1,v2) +return math.abs(delta) +end +function UTILS.VecTranslate(a,distance,angle) +local SX=a.x +local SY=a.z +local Radians=math.rad(angle or 0) +local TX=distance*math.cos(Radians)+SX +local TY=distance*math.sin(Radians)+SY +return{x=TX,y=a.y,z=TY} +end +function UTILS.Rotate2D(a,angle) +local phi=math.rad(angle) +local x=a.z +local y=a.x +local Z=x*math.cos(phi)-y*math.sin(phi) +local X=x*math.sin(phi)+y*math.cos(phi) +local Y=a.y +local A={x=X,y=Y,z=Z} +return A +end +function UTILS.TACANToFrequency(TACANChannel,TACANMode) +if type(TACANChannel)~="number"then +return nil +end +if TACANMode~="X"and TACANMode~="Y"then +return nil +end +local A=1151 +local B=64 +if TACANChannel<64 then +B=1 +end +if TACANMode=='Y'then +A=1025 +if TACANChannel<64 then +A=1088 +end +else +if TACANChannel<64 then +A=962 +end +end +return(A+TACANChannel-B)*1000000 +end +function UTILS.GetDCSMap() +return env.mission.theatre +end +function UTILS.GetDCSMissionDate() +local year=tostring(env.mission.date.Year) +local month=tostring(env.mission.date.Month) +local day=tostring(env.mission.date.Day) +return string.format("%s/%s/%s",year,month,day),tonumber(year),tonumber(month),tonumber(day) +end +function UTILS.GetMissionDay(Time) +Time=Time or timer.getAbsTime() +local clock=UTILS.SecondsToClock(Time,false) +local x=tonumber(UTILS.Split(clock,"+")[2]) +return x +end +function UTILS.GetMissionDayOfYear(Time) +local Date,Year,Month,Day=UTILS.GetDCSMissionDate() +local d=UTILS.GetMissionDay(Time) +return UTILS.GetDayOfYear(Year,Month,Day)+d +end +function UTILS.GetDate() +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 +function UTILS.GetMagneticDeclination(map) +map=map or UTILS.GetDCSMap() +local declination=0 +if map==DCSMAP.Caucasus then +declination=6 +elseif map==DCSMAP.NTTR then +declination=12 +elseif map==DCSMAP.Normandy then +declination=-10 +elseif map==DCSMAP.PersianGulf then +declination=2 +elseif map==DCSMAP.TheChannel then +declination=-10 +elseif map==DCSMAP.Syria then +declination=5 +else +declination=0 +end +return declination +end +function UTILS.FileExists(file) +if io then +local f=io.open(file,"r") +if f~=nil then +io.close(f) +return true +else +return false +end +else +return nil +end +end +function UTILS.CheckMemory(output) +local time=timer.getTime() +local clock=UTILS.SecondsToClock(time) +local mem=collectgarbage("count") +if output then +env.info(string.format("T=%s Memory usage %d kByte = %.2f MByte",clock,mem,mem/1024)) +end +return mem +end +function UTILS.GetCoalitionName(Coalition) +if Coalition then +if Coalition==coalition.side.BLUE then +return"Blue" +elseif Coalition==coalition.side.RED then +return"Red" +elseif Coalition==coalition.side.NEUTRAL then +return"Neutral" +else +return"Unknown" +end +else +return"Unknown" +end +end +function UTILS.GetModulationName(Modulation) +if Modulation then +if Modulation==0 then +return"AM" +elseif Modulation==1 then +return"FM" +else +return"Unknown" +end +else +return"Unknown" +end +end +function UTILS.GetCallsignName(Callsign) +for name,value in pairs(CALLSIGN.Aircraft)do +if value==Callsign then +return name +end +end +for name,value in pairs(CALLSIGN.AWACS)do +if value==Callsign then +return name +end +end +for name,value in pairs(CALLSIGN.JTAC)do +if value==Callsign then +return name +end +end +for name,value in pairs(CALLSIGN.Tanker)do +if value==Callsign then +return name +end +end +return"Ghostrider" +end +function UTILS.GMTToLocalTimeDifference() +local theatre=UTILS.GetDCSMap() +if theatre==DCSMAP.Caucasus then +return 4 +elseif theatre==DCSMAP.PersianGulf then +return 4 +elseif theatre==DCSMAP.NTTR then +return-8 +elseif theatre==DCSMAP.Normandy then +return 0 +elseif theatre==DCSMAP.TheChannel then +return 2 +elseif theatre==DCSMAP.Syria then +return 3 +else +BASE:E(string.format("ERROR: Unknown Map %s in UTILS.GMTToLocal function. Returning 0",tostring(theatre))) +return 0 +end +end +function UTILS.GetDayOfYear(Year,Month,Day) +local floor=math.floor +local n1=floor(275*Month/9) +local n2=floor((Month+9)/12) +local n3=(1+floor((Year-4*floor(Year/4)+2)/3)) +return n1-(n2*n3)+Day-30 +end +function UTILS.GetSunRiseAndSet(DayOfYear,Latitude,Longitude,Rising,Tlocal) +local zenith=90.83 +local latitude=Latitude +local longitude=Longitude +local rising=Rising +local n=DayOfYear +Tlocal=Tlocal or 0 +local rad=math.rad +local deg=math.deg +local floor=math.floor +local frac=function(n)return n-floor(n)end +local cos=function(d)return math.cos(rad(d))end +local acos=function(d)return deg(math.acos(d))end +local sin=function(d)return math.sin(rad(d))end +local asin=function(d)return deg(math.asin(d))end +local tan=function(d)return math.tan(rad(d))end +local atan=function(d)return deg(math.atan(d))end +local function fit_into_range(val,min,max) +local range=max-min +local count +if val=max then +count=floor((val-max)/range)+1 +return val-count*range +else +return val +end +end +local lng_hour=longitude/15 +local t +if rising then +t=n+((6-lng_hour)/24) +else +t=n+((18-lng_hour)/24) +end +local M=(0.9856*t)-3.289 +local L=fit_into_range(M+(1.916*sin(M))+(0.020*sin(2*M))+282.634,0,360) +local RA=fit_into_range(atan(0.91764*tan(L)),0,360) +local Lquadrant=floor(L/90)*90 +local RAquadrant=floor(RA/90)*90 +RA=RA+Lquadrant-RAquadrant +RA=RA/15 +local sinDec=0.39782*sin(L) +local cosDec=cos(asin(sinDec)) +local cosH=(cos(zenith)-(sinDec*sin(latitude)))/(cosDec*cos(latitude)) +if rising and cosH>1 then +return"N/R" +elseif cosH<-1 then +return"N/S" +end +local H +if rising then +H=360-acos(cosH) +else +H=acos(cosH) +end +H=H/15 +local T=H+RA-(0.06571*t)-6.622 +local UT=fit_into_range(T-lng_hour+Tlocal,0,24) +return floor(UT)*60*60+frac(UT)*60*60 +end +function UTILS.GetSunrise(Day,Month,Year,Latitude,Longitude,Tlocal) +local DayOfYear=UTILS.GetDayOfYear(Year,Month,Day) +return UTILS.GetSunRiseAndSet(DayOfYear,Latitude,Longitude,true,Tlocal) +end +function UTILS.GetSunset(Day,Month,Year,Latitude,Longitude,Tlocal) +local DayOfYear=UTILS.GetDayOfYear(Year,Month,Day) +return UTILS.GetSunRiseAndSet(DayOfYear,Latitude,Longitude,false,Tlocal) +end +function UTILS.GetOSTime() +if os then +return os.clock() +end +return nil +end +function UTILS.ShuffleTable(t) +if t==nil or type(t)~="table"then +BASE:I("Error in ShuffleTable: Missing or wrong tyåe of Argument") +return +end +math.random() +math.random() +math.random() +local TempTable={} +for i=1,#t do +local r=math.random(1,#t) +TempTable[i]=t[r] +table.remove(t,r) +end +return TempTable +end +PROFILER={ +ClassName="PROFILER", +Counters={}, +dInfo={}, +fTime={}, +fTimeTotal={}, +eventHandler={}, +logUnknown=false, +ThreshCPS=0.0, +ThreshTtot=0.005, +fileNamePrefix="MooseProfiler", +fileNameSuffix="txt" +} +function PROFILER.Start(Delay,Duration) +local go=true +if not os then +env.error("ERROR: Profiler needs os to be desanitized!") +go=false +end +if not io then +env.error("ERROR: Profiler needs io to be desanitized!") +go=false +end +if not lfs then +env.error("ERROR: Profiler needs lfs to be desanitized!") +go=false +end +if not go then +return +end +if Delay and Delay>0 then +BASE:ScheduleOnce(Delay,PROFILER.Start,0,Duration) +else +PROFILER.TstartGame=timer.getTime() +PROFILER.TstartOS=os.clock() +world.addEventHandler(PROFILER.eventHandler) +env.info('############################ Profiler Started ############################') +if Duration then +env.info(string.format("- Will be running for %d seconds",Duration)) +else +env.info(string.format("- Will be stopped when mission ends")) +end +env.info(string.format("- Calls per second threshold %.3f/sec",PROFILER.ThreshCPS)) +env.info(string.format("- Total function time threshold %.3f sec",PROFILER.ThreshTtot)) +env.info(string.format("- Output file \"%s\" in your DCS log file folder",PROFILER.getfilename(PROFILER.fileNameSuffix))) +env.info(string.format("- Output file \"%s\" in CSV format",PROFILER.getfilename("csv"))) +env.info('###############################################################################') +local duration=Duration or 600 +trigger.action.outText("### Profiler running ###",duration) +debug.sethook(PROFILER.hook,"cr") +if Duration then +PROFILER.Stop(Duration) +end +end +end +function PROFILER.Stop(Delay) +if Delay and Delay>0 then +BASE:ScheduleOnce(Delay,PROFILER.Stop) +else +debug.sethook() +local runTimeGame=timer.getTime()-PROFILER.TstartGame +local runTimeOS=os.clock()-PROFILER.TstartOS +PROFILER.showInfo(runTimeGame,runTimeOS) +end +end +function PROFILER.eventHandler:onEvent(event) +if event.id==world.event.S_EVENT_MISSION_END then +PROFILER.Stop() +end +end +function PROFILER.hook(event) +local f=debug.getinfo(2,"f").func +if event=='call'then +if PROFILER.Counters[f]==nil then +PROFILER.Counters[f]=1 +PROFILER.dInfo[f]=debug.getinfo(2,"Sn") +if PROFILER.fTimeTotal[f]==nil then +PROFILER.fTimeTotal[f]=0 +end +else +PROFILER.Counters[f]=PROFILER.Counters[f]+1 +end +if PROFILER.fTime[f]==nil then +PROFILER.fTime[f]=os.clock() +end +elseif(event=='return')then +if PROFILER.fTime[f]~=nil then +PROFILER.fTimeTotal[f]=PROFILER.fTimeTotal[f]+(os.clock()-PROFILER.fTime[f]) +PROFILER.fTime[f]=nil +end +end +end +function PROFILER.getData(func) +local n=PROFILER.dInfo[func] +if n.what=="C"then +return n.name,"?","?",PROFILER.fTimeTotal[func] +end +return n.name,n.short_src,n.linedefined,PROFILER.fTimeTotal[func] +end +function PROFILER._flog(f,txt) +f:write(txt.."\r\n") +end +function PROFILER.showTable(data,f,runTimeGame) +for i=1,#data do +local t=data[i] +local cps=t.count/runTimeGame +local threshCPS=cps>=PROFILER.ThreshCPS +local threshTot=t.tm>=PROFILER.ThreshTtot +if threshCPS and threshTot then +local text=string.format("%30s: %8d calls %8.1f/sec - Time Total %8.3f sec (%.3f %%) %5.3f sec/call %s line %s",t.func,t.count,cps,t.tm,t.tm/runTimeGame*100,t.tm/t.count,tostring(t.src),tostring(t.line)) +PROFILER._flog(f,text) +end +end +end +function PROFILER.printCSV(data,runTimeGame) +local file=PROFILER.getfilename("csv") +local g=io.open(file,'w') +local text="Function,Total Calls,Calls per Sec,Total Time,Total in %,Sec per Call,Source File;Line Number," +g:write(text.."\r\n") +for i=1,#data do +local t=data[i] +local cps=t.count/runTimeGame +local txt=string.format("%s,%d,%.1f,%.3f,%.3f,%.3f,%s,%s,",t.func,t.count,cps,t.tm,t.tm/runTimeGame*100,t.tm/t.count,tostring(t.src),tostring(t.line)) +g:write(txt.."\r\n") +end +g:close() +end +function PROFILER.getfilename(ext) +local dir=lfs.writedir()..[[Logs\]] +ext=ext or PROFILER.fileNameSuffix +local file=dir..PROFILER.fileNamePrefix.."."..ext +if not UTILS.FileExists(file)then +return file +end +for i=1,999 do +local file=string.format("%s%s-%03d.%s",dir,PROFILER.fileNamePrefix,i,ext) +if not UTILS.FileExists(file)then +return file +end +end +end +function PROFILER.showInfo(runTimeGame,runTimeOS) +local file=PROFILER.getfilename(PROFILER.fileNameSuffix) +local f=io.open(file,'w') +local Ttot=0 +local Calls=0 +local t={} +local tcopy=nil +local tserialize=nil +local tforgen=nil +local tpairs=nil +for func,count in pairs(PROFILER.Counters)do +local s,src,line,tm=PROFILER.getData(func) +if PROFILER.logUnknown==true then +if s==nil then s=""end +end +if s~=nil then +local T= +{func=s, +src=src, +line=line, +count=count, +tm=tm, +} +if s=="_copy"then +if tcopy==nil then +tcopy=T +else +tcopy.count=tcopy.count+T.count +tcopy.tm=tcopy.tm+T.tm +end +elseif s=="_Serialize"then +if tserialize==nil then +tserialize=T +else +tserialize.count=tserialize.count+T.count +tserialize.tm=tserialize.tm+T.tm +end +elseif s=="(for generator)"then +if tforgen==nil then +tforgen=T +else +tforgen.count=tforgen.count+T.count +tforgen.tm=tforgen.tm+T.tm +end +elseif s=="pairs"then +if tpairs==nil then +tpairs=T +else +tpairs.count=tpairs.count+T.count +tpairs.tm=tpairs.tm+T.tm +end +else +table.insert(t,T) +end +Ttot=Ttot+tm +Calls=Calls+count +end +end +if tcopy then +table.insert(t,tcopy) +end +if tserialize then +table.insert(t,tserialize) +end +if tforgen then +table.insert(t,tforgen) +end +if tpairs then +table.insert(t,tpairs) +end +env.info('############################ Profiler Stopped ############################') +env.info(string.format("* Runtime Game : %s = %d sec",UTILS.SecondsToClock(runTimeGame,true),runTimeGame)) +env.info(string.format("* Runtime Real : %s = %d sec",UTILS.SecondsToClock(runTimeOS,true),runTimeOS)) +env.info(string.format("* Function time : %s = %.1f sec (%.1f percent of runtime game)",UTILS.SecondsToClock(Ttot,true),Ttot,Ttot/runTimeGame*100)) +env.info(string.format("* Total functions : %d",#t)) +env.info(string.format("* Total func calls : %d",Calls)) +env.info(string.format("* Writing to file : \"%s\"",file)) +env.info(string.format("* Writing to file : \"%s\"",PROFILER.getfilename("csv"))) +env.info("##############################################################################") +table.sort(t,function(a,b)return a.tm>b.tm end) +PROFILER._flog(f,"") +PROFILER._flog(f,"************************************************************************************************************************") +PROFILER._flog(f,"************************************************************************************************************************") +PROFILER._flog(f,"************************************************************************************************************************") +PROFILER._flog(f,"") +PROFILER._flog(f,"-------------------------") +PROFILER._flog(f,"---- Profiler Report ----") +PROFILER._flog(f,"-------------------------") +PROFILER._flog(f,"") +PROFILER._flog(f,string.format("* Runtime Game : %s = %.1f sec",UTILS.SecondsToClock(runTimeGame,true),runTimeGame)) +PROFILER._flog(f,string.format("* Runtime Real : %s = %.1f sec",UTILS.SecondsToClock(runTimeOS,true),runTimeOS)) +PROFILER._flog(f,string.format("* Function time : %s = %.1f sec (%.1f %% of runtime game)",UTILS.SecondsToClock(Ttot,true),Ttot,Ttot/runTimeGame*100)) +PROFILER._flog(f,"") +PROFILER._flog(f,string.format("* Total functions = %d",#t)) +PROFILER._flog(f,string.format("* Total func calls = %d",Calls)) +PROFILER._flog(f,"") +PROFILER._flog(f,string.format("* Calls per second threshold = %.3f/sec",PROFILER.ThreshCPS)) +PROFILER._flog(f,string.format("* Total func time threshold = %.3f sec",PROFILER.ThreshTtot)) +PROFILER._flog(f,"") +PROFILER._flog(f,"************************************************************************************************************************") +PROFILER._flog(f,"") +PROFILER.showTable(t,f,runTimeGame) +table.sort(t,function(a,b)return a.tm/a.count>b.tm/b.count end) +PROFILER._flog(f,"") +PROFILER._flog(f,"************************************************************************************************************************") +PROFILER._flog(f,"") +PROFILER._flog(f,"--------------------------------------") +PROFILER._flog(f,"---- Data Sorted by Time per Call ----") +PROFILER._flog(f,"--------------------------------------") +PROFILER._flog(f,"") +PROFILER.showTable(t,f,runTimeGame) +table.sort(t,function(a,b)return a.count>b.count end) +PROFILER._flog(f,"") +PROFILER._flog(f,"************************************************************************************************************************") +PROFILER._flog(f,"") +PROFILER._flog(f,"------------------------------------") +PROFILER._flog(f,"---- Data Sorted by Total Calls ----") +PROFILER._flog(f,"------------------------------------") +PROFILER._flog(f,"") +PROFILER.showTable(t,f,runTimeGame) +PROFILER._flog(f,"") +PROFILER._flog(f,"************************************************************************************************************************") +PROFILER._flog(f,"************************************************************************************************************************") +PROFILER._flog(f,"************************************************************************************************************************") +f:close() +PROFILER.printCSV(t,runTimeGame) +end +local _TraceOnOff=true +local _TraceLevel=1 +local _TraceAll=false +local _TraceClass={} +local _TraceClassMethod={} +local _ClassID=0 +BASE={ +ClassName="BASE", +ClassID=0, +Events={}, +States={}, +Debug=debug, +Scheduler=nil, +} +BASE.__={} +BASE._={ +Schedules={} +} +FORMATION={ +Cone="Cone", +Vee="Vee" +} +function BASE:New() +local self=routines.utils.deepCopy(self) +_ClassID=_ClassID+1 +self.ClassID=_ClassID +return self +end +function BASE:Inherit(Child,Parent) +local Child=routines.utils.deepCopy(Child) +if Child~=nil then +if rawget(Child,"__")then +setmetatable(Child,{__index=Child.__}) +setmetatable(Child.__,{__index=Parent}) +else +setmetatable(Child,{__index=Parent}) +end +end +return Child +end +local function getParent(Child) +local Parent=nil +if Child.ClassName=='BASE'then +Parent=nil +else +if rawget(Child,"__")then +Parent=getmetatable(Child.__).__index +else +Parent=getmetatable(Child).__index +end +end +return Parent +end +function BASE:GetParent(Child,FromClass) +local Parent +if Child.ClassName=='BASE'then +Parent=nil +else +if FromClass then +while(Child.ClassName~="BASE"and Child.ClassName~=FromClass.ClassName)do +Child=getParent(Child) +end +end +if Child.ClassName=='BASE'then +Parent=nil +else +Parent=getParent(Child) +end +end +return Parent +end +function BASE:IsInstanceOf(ClassName) +if type(ClassName)~='string'then +if type(ClassName)=='table'and ClassName.ClassName~=nil then +ClassName=ClassName.ClassName +else +local err_str='className parameter should be a string; parameter received: '..type(ClassName) +self:E(err_str) +return false +end +end +ClassName=string.upper(ClassName) +if string.upper(self.ClassName)==ClassName then +return true +end +local Parent=getParent(self) +while Parent do +if string.upper(Parent.ClassName)==ClassName then +return true +end +Parent=getParent(Parent) +end +return false +end +function BASE:GetClassNameAndID() +return string.format('%s#%09d',self.ClassName,self.ClassID) +end +function BASE:GetClassName() +return self.ClassName +end +function BASE:GetClassID() +return self.ClassID +end +do +function BASE:EventDispatcher() +return _EVENTDISPATCHER +end +function BASE:GetEventPriority() +return self._.EventPriority or 5 +end +function BASE:SetEventPriority(EventPriority) +self._.EventPriority=EventPriority +end +function BASE:EventRemoveAll() +self:EventDispatcher():RemoveAll(self) +return self +end +function BASE:HandleEvent(EventID,EventFunction) +self:EventDispatcher():OnEventGeneric(EventFunction,self,EventID) +return self +end +function BASE:UnHandleEvent(EventID) +self:EventDispatcher():RemoveEvent(self,EventID) +return self +end +end +function BASE:CreateEventBirth(EventTime,Initiator,IniUnitName,place,subplace) +self:F({EventTime,Initiator,IniUnitName,place,subplace}) +local Event={ +id=world.event.S_EVENT_BIRTH, +time=EventTime, +initiator=Initiator, +IniUnitName=IniUnitName, +place=place, +subplace=subplace +} +world.onEvent(Event) +end +function BASE:CreateEventCrash(EventTime,Initiator) +self:F({EventTime,Initiator}) +local Event={ +id=world.event.S_EVENT_CRASH, +time=EventTime, +initiator=Initiator, +} +world.onEvent(Event) +end +function BASE:CreateEventUnitLost(EventTime,Initiator) +self:F({EventTime,Initiator}) +local Event={ +id=world.event.S_EVENT_UNIT_LOST, +time=EventTime, +initiator=Initiator, +} +world.onEvent(Event) +end +function BASE:CreateEventDead(EventTime,Initiator) +self:F({EventTime,Initiator}) +local Event={ +id=world.event.S_EVENT_DEAD, +time=EventTime, +initiator=Initiator, +} +world.onEvent(Event) +end +function BASE:CreateEventRemoveUnit(EventTime,Initiator) +self:F({EventTime,Initiator}) +local Event={ +id=EVENTS.RemoveUnit, +time=EventTime, +initiator=Initiator, +} +world.onEvent(Event) +end +function BASE:CreateEventTakeoff(EventTime,Initiator) +self:F({EventTime,Initiator}) +local Event={ +id=world.event.S_EVENT_TAKEOFF, +time=EventTime, +initiator=Initiator, +} +world.onEvent(Event) +end +function BASE:CreateEventPlayerEnterAircraft(PlayerUnit) +self:F({PlayerUnit}) +local Event={ +id=EVENTS.PlayerEnterAircraft, +time=timer.getTime(), +initiator=PlayerUnit:GetDCSObject() +} +world.onEvent(Event) +end +function BASE:onEvent(event) +if self then +for EventID,EventObject in pairs(self.Events)do +if EventObject.EventEnabled then +if event.id==EventObject.Event then +if self==EventObject.Self then +if event.initiator and event.initiator:isExist()then +event.IniUnitName=event.initiator:getName() +end +if event.target and event.target:isExist()then +event.TgtUnitName=event.target:getName() +end +end +end +end +end +end +end +do +function BASE:ScheduleOnce(Start,SchedulerFunction,...) +self:F2({Start}) +self:T3({...}) +local ObjectName="-" +ObjectName=self.ClassName..self.ClassID +self:F3({"ScheduleOnce: ",ObjectName,Start}) +if not self.Scheduler then +self.Scheduler=SCHEDULER:New(self) +end +local ScheduleID=_SCHEDULEDISPATCHER:AddSchedule( +self, +SchedulerFunction, +{...}, +Start, +nil, +nil, +nil +) +self._.Schedules[#self._.Schedules+1]=ScheduleID +return self._.Schedules[#self._.Schedules] +end +function BASE:ScheduleRepeat(Start,Repeat,RandomizeFactor,Stop,SchedulerFunction,...) +self:F2({Start}) +self:T3({...}) +local ObjectName="-" +ObjectName=self.ClassName..self.ClassID +self:F3({"ScheduleRepeat: ",ObjectName,Start,Repeat,RandomizeFactor,Stop}) +if not self.Scheduler then +self.Scheduler=SCHEDULER:New(self) +end +local ScheduleID=self.Scheduler:Schedule( +self, +SchedulerFunction, +{...}, +Start, +Repeat, +RandomizeFactor, +Stop, +4 +) +self._.Schedules[#self._.Schedules+1]=ScheduleID +return self._.Schedules[#self._.Schedules] +end +function BASE:ScheduleStop(SchedulerFunction) +self:F3({"ScheduleStop:"}) +if self.Scheduler then +_SCHEDULEDISPATCHER:Stop(self.Scheduler,self._.Schedules[SchedulerFunction]) +end +end +end +function BASE:SetState(Object,Key,Value) +local ClassNameAndID=Object:GetClassNameAndID() +self.States[ClassNameAndID]=self.States[ClassNameAndID]or{} +self.States[ClassNameAndID][Key]=Value +return self.States[ClassNameAndID][Key] +end +function BASE:GetState(Object,Key) +local ClassNameAndID=Object:GetClassNameAndID() +if self.States[ClassNameAndID]then +local Value=self.States[ClassNameAndID][Key]or false +return Value +end +return nil +end +function BASE:ClearState(Object,StateName) +local ClassNameAndID=Object:GetClassNameAndID() +if self.States[ClassNameAndID]then +self.States[ClassNameAndID][StateName]=nil +end +end +function BASE:TraceOn() +self:TraceOnOff(true) +end +function BASE:TraceOff() +self:TraceOnOff(false) +end +function BASE:TraceOnOff(TraceOnOff) +if TraceOnOff==false then +self:I("Tracing in MOOSE is OFF") +_TraceOnOff=false +else +self:I("Tracing in MOOSE is ON") +_TraceOnOff=true +end +end +function BASE:IsTrace() +if BASE.Debug and(_TraceAll==true)or(_TraceClass[self.ClassName]or _TraceClassMethod[self.ClassName])then +return true +else +return false +end +end +function BASE:TraceLevel(Level) +_TraceLevel=Level or 1 +self:I("Tracing level ".._TraceLevel) +end +function BASE:TraceAll(TraceAll) +if TraceAll==false then +_TraceAll=false +else +_TraceAll=true +end +if _TraceAll then +self:I("Tracing all methods in MOOSE ") +else +self:I("Switched off tracing all methods in MOOSE") +end +end +function BASE:TraceClass(Class) +_TraceClass[Class]=true +_TraceClassMethod[Class]={} +self:I("Tracing class "..Class) +end +function BASE:TraceClassMethod(Class,Method) +if not _TraceClassMethod[Class]then +_TraceClassMethod[Class]={} +_TraceClassMethod[Class].Method={} +end +_TraceClassMethod[Class].Method[Method]=true +self:I("Tracing method "..Method.." of class "..Class) +end +function BASE:_F(Arguments,DebugInfoCurrentParam,DebugInfoFromParam) +if BASE.Debug and(_TraceAll==true)or(_TraceClass[self.ClassName]or _TraceClassMethod[self.ClassName])then +local DebugInfoCurrent=DebugInfoCurrentParam and DebugInfoCurrentParam or BASE.Debug.getinfo(2,"nl") +local DebugInfoFrom=DebugInfoFromParam and DebugInfoFromParam or BASE.Debug.getinfo(3,"l") +local Function="function" +if DebugInfoCurrent.name then +Function=DebugInfoCurrent.name +end +if _TraceAll==true or _TraceClass[self.ClassName]or _TraceClassMethod[self.ClassName].Method[Function]then +local LineCurrent=0 +if DebugInfoCurrent.currentline then +LineCurrent=DebugInfoCurrent.currentline +end +local LineFrom=0 +if DebugInfoFrom then +LineFrom=DebugInfoFrom.currentline +end +env.info(string.format("%6d(%6d)/%1s:%30s%05d.%s(%s)",LineCurrent,LineFrom,"F",self.ClassName,self.ClassID,Function,routines.utils.oneLineSerialize(Arguments))) +end +end +end +function BASE:F(Arguments) +if BASE.Debug and _TraceOnOff then +local DebugInfoCurrent=BASE.Debug.getinfo(2,"nl") +local DebugInfoFrom=BASE.Debug.getinfo(3,"l") +if _TraceLevel>=1 then +self:_F(Arguments,DebugInfoCurrent,DebugInfoFrom) +end +end +end +function BASE:F2(Arguments) +if BASE.Debug and _TraceOnOff then +local DebugInfoCurrent=BASE.Debug.getinfo(2,"nl") +local DebugInfoFrom=BASE.Debug.getinfo(3,"l") +if _TraceLevel>=2 then +self:_F(Arguments,DebugInfoCurrent,DebugInfoFrom) +end +end +end +function BASE:F3(Arguments) +if BASE.Debug and _TraceOnOff then +local DebugInfoCurrent=BASE.Debug.getinfo(2,"nl") +local DebugInfoFrom=BASE.Debug.getinfo(3,"l") +if _TraceLevel>=3 then +self:_F(Arguments,DebugInfoCurrent,DebugInfoFrom) +end +end +end +function BASE:_T(Arguments,DebugInfoCurrentParam,DebugInfoFromParam) +if BASE.Debug and(_TraceAll==true)or(_TraceClass[self.ClassName]or _TraceClassMethod[self.ClassName])then +local DebugInfoCurrent=DebugInfoCurrentParam and DebugInfoCurrentParam or BASE.Debug.getinfo(2,"nl") +local DebugInfoFrom=DebugInfoFromParam and DebugInfoFromParam or BASE.Debug.getinfo(3,"l") +local Function="function" +if DebugInfoCurrent.name then +Function=DebugInfoCurrent.name +end +if _TraceAll==true or _TraceClass[self.ClassName]or _TraceClassMethod[self.ClassName].Method[Function]then +local LineCurrent=0 +if DebugInfoCurrent.currentline then +LineCurrent=DebugInfoCurrent.currentline +end +local LineFrom=0 +if DebugInfoFrom then +LineFrom=DebugInfoFrom.currentline +end +env.info(string.format("%6d(%6d)/%1s:%30s%05d.%s",LineCurrent,LineFrom,"T",self.ClassName,self.ClassID,routines.utils.oneLineSerialize(Arguments))) +end +end +end +function BASE:T(Arguments) +if BASE.Debug and _TraceOnOff then +local DebugInfoCurrent=BASE.Debug.getinfo(2,"nl") +local DebugInfoFrom=BASE.Debug.getinfo(3,"l") +if _TraceLevel>=1 then +self:_T(Arguments,DebugInfoCurrent,DebugInfoFrom) +end +end +end +function BASE:T2(Arguments) +if BASE.Debug and _TraceOnOff then +local DebugInfoCurrent=BASE.Debug.getinfo(2,"nl") +local DebugInfoFrom=BASE.Debug.getinfo(3,"l") +if _TraceLevel>=2 then +self:_T(Arguments,DebugInfoCurrent,DebugInfoFrom) +end +end +end +function BASE:T3(Arguments) +if BASE.Debug and _TraceOnOff then +local DebugInfoCurrent=BASE.Debug.getinfo(2,"nl") +local DebugInfoFrom=BASE.Debug.getinfo(3,"l") +if _TraceLevel>=3 then +self:_T(Arguments,DebugInfoCurrent,DebugInfoFrom) +end +end +end +function BASE:E(Arguments) +if BASE.Debug then +local DebugInfoCurrent=BASE.Debug.getinfo(2,"nl") +local DebugInfoFrom=BASE.Debug.getinfo(3,"l") +local Function="function" +if DebugInfoCurrent.name then +Function=DebugInfoCurrent.name +end +local LineCurrent=DebugInfoCurrent.currentline +local LineFrom=-1 +if DebugInfoFrom then +LineFrom=DebugInfoFrom.currentline +end +env.info(string.format("%6d(%6d)/%1s:%30s%05d.%s(%s)",LineCurrent,LineFrom,"E",self.ClassName,self.ClassID,Function,routines.utils.oneLineSerialize(Arguments))) +else +env.info(string.format("%1s:%30s%05d(%s)","E",self.ClassName,self.ClassID,routines.utils.oneLineSerialize(Arguments))) +end +end +function BASE:I(Arguments) +if BASE.Debug then +local DebugInfoCurrent=BASE.Debug.getinfo(2,"nl") +local DebugInfoFrom=BASE.Debug.getinfo(3,"l") +local Function="function" +if DebugInfoCurrent.name then +Function=DebugInfoCurrent.name +end +local LineCurrent=DebugInfoCurrent.currentline +local LineFrom=-1 +if DebugInfoFrom then +LineFrom=DebugInfoFrom.currentline +end +env.info(string.format("%6d(%6d)/%1s:%30s%05d.%s(%s)",LineCurrent,LineFrom,"I",self.ClassName,self.ClassID,Function,routines.utils.oneLineSerialize(Arguments))) +else +env.info(string.format("%1s:%30s%05d(%s)","I",self.ClassName,self.ClassID,routines.utils.oneLineSerialize(Arguments))) +end +end +do +USERFLAG={ +ClassName="USERFLAG", +UserFlagName=nil, +} +function USERFLAG:New(UserFlagName) +local self=BASE:Inherit(self,BASE:New()) +self.UserFlagName=UserFlagName +return self +end +function USERFLAG:GetName() +return self.UserFlagName +end +function USERFLAG:Set(Number,Delay) +if Delay and Delay>0 then +self:ScheduleOnce(Delay,USERFLAG.Set,self,Number) +else +trigger.action.setUserFlag(self.UserFlagName,Number) +end +return self +end +function USERFLAG:Get() +return trigger.misc.getUserFlag(self.UserFlagName) +end +function USERFLAG:Is(Number) +return trigger.misc.getUserFlag(self.UserFlagName)==Number +end +end +do +USERSOUND={ +ClassName="USERSOUND", +} +function USERSOUND:New(UserSoundFileName) +local self=BASE:Inherit(self,BASE:New()) +self.UserSoundFileName=UserSoundFileName +return self +end +function USERSOUND:SetFileName(UserSoundFileName) +self.UserSoundFileName=UserSoundFileName +return self +end +function USERSOUND:ToAll() +trigger.action.outSound(self.UserSoundFileName) +return self +end +function USERSOUND:ToCoalition(Coalition) +trigger.action.outSoundForCoalition(Coalition,self.UserSoundFileName) +return self +end +function USERSOUND:ToCountry(Country) +trigger.action.outSoundForCountry(Country,self.UserSoundFileName) +return self +end +function USERSOUND:ToGroup(Group,Delay) +Delay=Delay or 0 +if Delay>0 then +SCHEDULER:New(nil,USERSOUND.ToGroup,{self,Group},Delay) +else +trigger.action.outSoundForGroup(Group:GetID(),self.UserSoundFileName) +end +return self +end +end +REPORT={ +ClassName="REPORT", +Title="", +} +function REPORT:New(Title) +local self=BASE:Inherit(self,BASE:New()) +self.Report={} +self:SetTitle(Title or"") +self:SetIndent(3) +return self +end +function REPORT:HasText() +return#self.Report>0 +end +function REPORT:SetIndent(Indent) +self.Indent=Indent +return self +end +function REPORT:Add(Text) +self.Report[#self.Report+1]=Text +return self +end +function REPORT:AddIndent(Text,Separator) +self.Report[#self.Report+1]=((Separator and Separator..string.rep(" ",self.Indent-1))or string.rep(" ",self.Indent))..Text:gsub("\n","\n"..string.rep(" ",self.Indent)) +return self +end +function REPORT:Text(Delimiter) +Delimiter=Delimiter or"\n" +local ReportText=(self.Title~=""and self.Title..Delimiter or self.Title)..table.concat(self.Report,Delimiter)or"" +return ReportText +end +function REPORT:SetTitle(Title) +self.Title=Title +return self +end +function REPORT:GetCount() +return#self.Report +end +SCHEDULER={ +ClassName="SCHEDULER", +Schedules={}, +MasterObject=nil, +ShowTrace=nil, +} +function SCHEDULER:New(MasterObject,SchedulerFunction,SchedulerArguments,Start,Repeat,RandomizeFactor,Stop) +local self=BASE:Inherit(self,BASE:New()) +self:F2({Start,Repeat,RandomizeFactor,Stop}) +local ScheduleID=nil +self.MasterObject=MasterObject +self.ShowTrace=false +if SchedulerFunction then +ScheduleID=self:Schedule(MasterObject,SchedulerFunction,SchedulerArguments,Start,Repeat,RandomizeFactor,Stop,3) +end +return self,ScheduleID +end +function SCHEDULER:Schedule(MasterObject,SchedulerFunction,SchedulerArguments,Start,Repeat,RandomizeFactor,Stop,TraceLevel,Fsm) +self:F2({Start,Repeat,RandomizeFactor,Stop}) +self:T3({SchedulerArguments}) +local ObjectName="-" +if MasterObject and MasterObject.ClassName and MasterObject.ClassID then +ObjectName=MasterObject.ClassName..MasterObject.ClassID +end +self:F3({"Schedule :",ObjectName,tostring(MasterObject),Start,Repeat,RandomizeFactor,Stop}) +self.MasterObject=MasterObject +local ScheduleID=_SCHEDULEDISPATCHER:AddSchedule( +self, +SchedulerFunction, +SchedulerArguments, +Start, +Repeat, +RandomizeFactor, +Stop, +TraceLevel or 3, +Fsm +) +self.Schedules[#self.Schedules+1]=ScheduleID +return ScheduleID +end +function SCHEDULER:Start(ScheduleID) +self:F3({ScheduleID}) +self:T(string.format("Starting scheduler ID=%s",tostring(ScheduleID))) +_SCHEDULEDISPATCHER:Start(self,ScheduleID) +end +function SCHEDULER:Stop(ScheduleID) +self:F3({ScheduleID}) +self:T(string.format("Stopping scheduler ID=%s",tostring(ScheduleID))) +_SCHEDULEDISPATCHER:Stop(self,ScheduleID) +end +function SCHEDULER:Remove(ScheduleID) +self:F3({ScheduleID}) +self:T(string.format("Removing scheduler ID=%s",tostring(ScheduleID))) +_SCHEDULEDISPATCHER:RemoveSchedule(self,ScheduleID) +end +function SCHEDULER:Clear() +self:F3() +self:T(string.format("Clearing scheduler")) +_SCHEDULEDISPATCHER:Clear(self) +end +function SCHEDULER:ShowTrace() +_SCHEDULEDISPATCHER:ShowTrace(self) +end +function SCHEDULER:NoTrace() +_SCHEDULEDISPATCHER:NoTrace(self) +end +SCHEDULEDISPATCHER={ +ClassName="SCHEDULEDISPATCHER", +CallID=0, +PersistentSchedulers={}, +ObjectSchedulers={}, +Schedule=nil, +} +function SCHEDULEDISPATCHER:New() +local self=BASE:Inherit(self,BASE:New()) +self:F3() +return self +end +function SCHEDULEDISPATCHER:AddSchedule(Scheduler,ScheduleFunction,ScheduleArguments,Start,Repeat,Randomize,Stop,TraceLevel,Fsm) +self:F2({Scheduler,ScheduleFunction,ScheduleArguments,Start,Repeat,Randomize,Stop,TraceLevel,Fsm}) +self.CallID=self.CallID+1 +local CallID=self.CallID.."#"..(Scheduler.MasterObject and Scheduler.MasterObject.GetClassNameAndID and Scheduler.MasterObject:GetClassNameAndID()or"")or"" +self:T2(string.format("Adding schedule #%d CallID=%s",self.CallID,CallID)) +self.PersistentSchedulers=self.PersistentSchedulers or{} +self.ObjectSchedulers=self.ObjectSchedulers or setmetatable({},{__mode="v"}) +if Scheduler.MasterObject then +self.ObjectSchedulers[CallID]=Scheduler +self:F3({CallID=CallID,ObjectScheduler=tostring(self.ObjectSchedulers[CallID]),MasterObject=tostring(Scheduler.MasterObject)}) +else +self.PersistentSchedulers[CallID]=Scheduler +self:F3({CallID=CallID,PersistentScheduler=self.PersistentSchedulers[CallID]}) +end +self.Schedule=self.Schedule or setmetatable({},{__mode="k"}) +self.Schedule[Scheduler]=self.Schedule[Scheduler]or{} +self.Schedule[Scheduler][CallID]={} +self.Schedule[Scheduler][CallID].Function=ScheduleFunction +self.Schedule[Scheduler][CallID].Arguments=ScheduleArguments +self.Schedule[Scheduler][CallID].StartTime=timer.getTime()+(Start or 0) +self.Schedule[Scheduler][CallID].Start=Start+0.001 +self.Schedule[Scheduler][CallID].Repeat=Repeat or 0 +self.Schedule[Scheduler][CallID].Randomize=Randomize or 0 +self.Schedule[Scheduler][CallID].Stop=Stop +local Info={} +if debug then +TraceLevel=TraceLevel or 2 +Info=debug.getinfo(TraceLevel,"nlS") +local name_fsm=debug.getinfo(TraceLevel-1,"n").name +if name_fsm then +Info.name=name_fsm +end +end +self:T3(self.Schedule[Scheduler][CallID]) +self.Schedule[Scheduler][CallID].CallHandler=function(Params) +local CallID=Params.CallID +local Info=Params.Info or{} +local Source=Info.source or"?" +local Line=Info.currentline or"?" +local Name=Info.name or"?" +local ErrorHandler=function(errmsg) +env.info("Error in timer function: "..errmsg) +if BASE.Debug~=nil then +env.info(BASE.Debug.traceback()) +end +return errmsg +end +local Scheduler=self.ObjectSchedulers[CallID] +if not Scheduler then +Scheduler=self.PersistentSchedulers[CallID] +end +if Scheduler then +local MasterObject=tostring(Scheduler.MasterObject) +local Schedule=self.Schedule[Scheduler][CallID] +local SchedulerObject=Scheduler.MasterObject +local ShowTrace=Scheduler.ShowTrace +local ScheduleFunction=Schedule.Function +local ScheduleArguments=Schedule.Arguments or{} +local Start=Schedule.Start +local Repeat=Schedule.Repeat or 0 +local Randomize=Schedule.Randomize or 0 +local Stop=Schedule.Stop or 0 +local ScheduleID=Schedule.ScheduleID +local Prefix=(Repeat==0)and"--->"or"+++>" +local Status,Result +if SchedulerObject then +local function Timer() +if ShowTrace then +SchedulerObject:T(Prefix..Name..":"..Line.." ("..Source..")") +end +return ScheduleFunction(SchedulerObject,unpack(ScheduleArguments)) +end +Status,Result=xpcall(Timer,ErrorHandler) +else +local function Timer() +if ShowTrace then +self:T(Prefix..Name..":"..Line.." ("..Source..")") +end +return ScheduleFunction(unpack(ScheduleArguments)) +end +Status,Result=xpcall(Timer,ErrorHandler) +end +local CurrentTime=timer.getTime() +local StartTime=Schedule.StartTime +self:F3({CallID=CallID,ScheduleID=ScheduleID,Master=MasterObject,CurrentTime=CurrentTime,StartTime=StartTime,Start=Start,Repeat=Repeat,Randomize=Randomize,Stop=Stop}) +if Status and((Result==nil)or(Result and Result~=false))then +if Repeat~=0 and((Stop==0)or(Stop~=0 and CurrentTime<=StartTime+Stop))then +local ScheduleTime=CurrentTime+Repeat+math.random(-(Randomize*Repeat/2),(Randomize*Repeat/2))+0.0001 +return ScheduleTime +else +self:Stop(Scheduler,CallID) +end +else +self:Stop(Scheduler,CallID) +end +else +self:I("<<<>"..Name..":"..Line.." ("..Source..")") +end +return nil +end +self:Start(Scheduler,CallID,Info) +return CallID +end +function SCHEDULEDISPATCHER:RemoveSchedule(Scheduler,CallID) +self:F2({Remove=CallID,Scheduler=Scheduler}) +if CallID then +self:Stop(Scheduler,CallID) +self.Schedule[Scheduler][CallID]=nil +end +end +function SCHEDULEDISPATCHER:Start(Scheduler,CallID,Info) +self:F2({Start=CallID,Scheduler=Scheduler}) +if CallID then +local Schedule=self.Schedule[Scheduler][CallID] +if not Schedule.ScheduleID then +local Tnow=timer.getTime() +Schedule.StartTime=Tnow +Schedule.ScheduleID=timer.scheduleFunction(Schedule.CallHandler,{CallID=CallID,Info=Info},Tnow+Schedule.Start) +self:T(string.format("Starting scheduledispatcher Call ID=%s ==> Schedule ID=%s",tostring(CallID),tostring(Schedule.ScheduleID))) +end +else +for CallID,Schedule in pairs(self.Schedule[Scheduler]or{})do +self:Start(Scheduler,CallID,Info) +end +end +end +function SCHEDULEDISPATCHER:Stop(Scheduler,CallID) +self:F2({Stop=CallID,Scheduler=Scheduler}) +if CallID then +local Schedule=self.Schedule[Scheduler][CallID] +if Schedule.ScheduleID then +self:T(string.format("scheduledispatcher stopping scheduler CallID=%s, ScheduleID=%s",tostring(CallID),tostring(Schedule.ScheduleID))) +timer.removeFunction(Schedule.ScheduleID) +Schedule.ScheduleID=nil +else +self:T(string.format("Error no ScheduleID for CallID=%s",tostring(CallID))) +end +else +for CallID,Schedule in pairs(self.Schedule[Scheduler]or{})do +self:Stop(Scheduler,CallID) +end +end +end +function SCHEDULEDISPATCHER:Clear(Scheduler) +self:F2({Scheduler=Scheduler}) +for CallID,Schedule in pairs(self.Schedule[Scheduler]or{})do +self:Stop(Scheduler,CallID) +end +end +function SCHEDULEDISPATCHER:ShowTrace(Scheduler) +self:F2({Scheduler=Scheduler}) +Scheduler.ShowTrace=true +end +function SCHEDULEDISPATCHER:NoTrace(Scheduler) +self:F2({Scheduler=Scheduler}) +Scheduler.ShowTrace=false +end +EVENT={ +ClassName="EVENT", +ClassID=0, +MissionEnd=false, +} +world.event.S_EVENT_NEW_CARGO=world.event.S_EVENT_MAX+1000 +world.event.S_EVENT_DELETE_CARGO=world.event.S_EVENT_MAX+1001 +world.event.S_EVENT_NEW_ZONE=world.event.S_EVENT_MAX+1002 +world.event.S_EVENT_DELETE_ZONE=world.event.S_EVENT_MAX+1003 +world.event.S_EVENT_NEW_ZONE_GOAL=world.event.S_EVENT_MAX+1004 +world.event.S_EVENT_DELETE_ZONE_GOAL=world.event.S_EVENT_MAX+1005 +world.event.S_EVENT_REMOVE_UNIT=world.event.S_EVENT_MAX+1006 +world.event.S_EVENT_PLAYER_ENTER_AIRCRAFT=world.event.S_EVENT_MAX+1007 +EVENTS={ +Shot=world.event.S_EVENT_SHOT, +Hit=world.event.S_EVENT_HIT, +Takeoff=world.event.S_EVENT_TAKEOFF, +Land=world.event.S_EVENT_LAND, +Crash=world.event.S_EVENT_CRASH, +Ejection=world.event.S_EVENT_EJECTION, +Refueling=world.event.S_EVENT_REFUELING, +Dead=world.event.S_EVENT_DEAD, +PilotDead=world.event.S_EVENT_PILOT_DEAD, +BaseCaptured=world.event.S_EVENT_BASE_CAPTURED, +MissionStart=world.event.S_EVENT_MISSION_START, +MissionEnd=world.event.S_EVENT_MISSION_END, +TookControl=world.event.S_EVENT_TOOK_CONTROL, +RefuelingStop=world.event.S_EVENT_REFUELING_STOP, +Birth=world.event.S_EVENT_BIRTH, +HumanFailure=world.event.S_EVENT_HUMAN_FAILURE, +EngineStartup=world.event.S_EVENT_ENGINE_STARTUP, +EngineShutdown=world.event.S_EVENT_ENGINE_SHUTDOWN, +PlayerEnterUnit=world.event.S_EVENT_PLAYER_ENTER_UNIT, +PlayerLeaveUnit=world.event.S_EVENT_PLAYER_LEAVE_UNIT, +PlayerComment=world.event.S_EVENT_PLAYER_COMMENT, +ShootingStart=world.event.S_EVENT_SHOOTING_START, +ShootingEnd=world.event.S_EVENT_SHOOTING_END, +MarkAdded=world.event.S_EVENT_MARK_ADDED, +MarkChange=world.event.S_EVENT_MARK_CHANGE, +MarkRemoved=world.event.S_EVENT_MARK_REMOVED, +NewCargo=world.event.S_EVENT_NEW_CARGO, +DeleteCargo=world.event.S_EVENT_DELETE_CARGO, +NewZone=world.event.S_EVENT_NEW_ZONE, +DeleteZone=world.event.S_EVENT_DELETE_ZONE, +NewZoneGoal=world.event.S_EVENT_NEW_ZONE_GOAL, +DeleteZoneGoal=world.event.S_EVENT_DELETE_ZONE_GOAL, +RemoveUnit=world.event.S_EVENT_REMOVE_UNIT, +PlayerEnterAircraft=world.event.S_EVENT_PLAYER_ENTER_AIRCRAFT, +DetailedFailure=world.event.S_EVENT_DETAILED_FAILURE or-1, +Kill=world.event.S_EVENT_KILL or-1, +Score=world.event.S_EVENT_SCORE or-1, +UnitLost=world.event.S_EVENT_UNIT_LOST or-1, +LandingAfterEjection=world.event.S_EVENT_LANDING_AFTER_EJECTION or-1, +ParatrooperLanding=world.event.S_EVENT_PARATROOPER_LENDING or-1, +DiscardChairAfterEjection=world.event.S_EVENT_DISCARD_CHAIR_AFTER_EJECTION or-1, +WeaponAdd=world.event.S_EVENT_WEAPON_ADD or-1, +TriggerZone=world.event.S_EVENT_TRIGGER_ZONE or-1, +LandingQualityMark=world.event.S_EVENT_LANDING_QUALITY_MARK or-1, +BDA=world.event.S_EVENT_BDA or-1, +} +local _EVENTMETA={ +[world.event.S_EVENT_SHOT]={ +Order=1, +Side="I", +Event="OnEventShot", +Text="S_EVENT_SHOT" +}, +[world.event.S_EVENT_HIT]={ +Order=1, +Side="T", +Event="OnEventHit", +Text="S_EVENT_HIT" +}, +[world.event.S_EVENT_TAKEOFF]={ +Order=1, +Side="I", +Event="OnEventTakeoff", +Text="S_EVENT_TAKEOFF" +}, +[world.event.S_EVENT_LAND]={ +Order=1, +Side="I", +Event="OnEventLand", +Text="S_EVENT_LAND" +}, +[world.event.S_EVENT_CRASH]={ +Order=-1, +Side="I", +Event="OnEventCrash", +Text="S_EVENT_CRASH" +}, +[world.event.S_EVENT_EJECTION]={ +Order=1, +Side="I", +Event="OnEventEjection", +Text="S_EVENT_EJECTION" +}, +[world.event.S_EVENT_REFUELING]={ +Order=1, +Side="I", +Event="OnEventRefueling", +Text="S_EVENT_REFUELING" +}, +[world.event.S_EVENT_DEAD]={ +Order=-1, +Side="I", +Event="OnEventDead", +Text="S_EVENT_DEAD" +}, +[world.event.S_EVENT_PILOT_DEAD]={ +Order=1, +Side="I", +Event="OnEventPilotDead", +Text="S_EVENT_PILOT_DEAD" +}, +[world.event.S_EVENT_BASE_CAPTURED]={ +Order=1, +Side="I", +Event="OnEventBaseCaptured", +Text="S_EVENT_BASE_CAPTURED" +}, +[world.event.S_EVENT_MISSION_START]={ +Order=1, +Side="N", +Event="OnEventMissionStart", +Text="S_EVENT_MISSION_START" +}, +[world.event.S_EVENT_MISSION_END]={ +Order=1, +Side="N", +Event="OnEventMissionEnd", +Text="S_EVENT_MISSION_END" +}, +[world.event.S_EVENT_TOOK_CONTROL]={ +Order=1, +Side="N", +Event="OnEventTookControl", +Text="S_EVENT_TOOK_CONTROL" +}, +[world.event.S_EVENT_REFUELING_STOP]={ +Order=1, +Side="I", +Event="OnEventRefuelingStop", +Text="S_EVENT_REFUELING_STOP" +}, +[world.event.S_EVENT_BIRTH]={ +Order=1, +Side="I", +Event="OnEventBirth", +Text="S_EVENT_BIRTH" +}, +[world.event.S_EVENT_HUMAN_FAILURE]={ +Order=1, +Side="I", +Event="OnEventHumanFailure", +Text="S_EVENT_HUMAN_FAILURE" +}, +[world.event.S_EVENT_ENGINE_STARTUP]={ +Order=1, +Side="I", +Event="OnEventEngineStartup", +Text="S_EVENT_ENGINE_STARTUP" +}, +[world.event.S_EVENT_ENGINE_SHUTDOWN]={ +Order=1, +Side="I", +Event="OnEventEngineShutdown", +Text="S_EVENT_ENGINE_SHUTDOWN" +}, +[world.event.S_EVENT_PLAYER_ENTER_UNIT]={ +Order=1, +Side="I", +Event="OnEventPlayerEnterUnit", +Text="S_EVENT_PLAYER_ENTER_UNIT" +}, +[world.event.S_EVENT_PLAYER_LEAVE_UNIT]={ +Order=-1, +Side="I", +Event="OnEventPlayerLeaveUnit", +Text="S_EVENT_PLAYER_LEAVE_UNIT" +}, +[world.event.S_EVENT_PLAYER_COMMENT]={ +Order=1, +Side="I", +Event="OnEventPlayerComment", +Text="S_EVENT_PLAYER_COMMENT" +}, +[world.event.S_EVENT_SHOOTING_START]={ +Order=1, +Side="I", +Event="OnEventShootingStart", +Text="S_EVENT_SHOOTING_START" +}, +[world.event.S_EVENT_SHOOTING_END]={ +Order=1, +Side="I", +Event="OnEventShootingEnd", +Text="S_EVENT_SHOOTING_END" +}, +[world.event.S_EVENT_MARK_ADDED]={ +Order=1, +Side="I", +Event="OnEventMarkAdded", +Text="S_EVENT_MARK_ADDED" +}, +[world.event.S_EVENT_MARK_CHANGE]={ +Order=1, +Side="I", +Event="OnEventMarkChange", +Text="S_EVENT_MARK_CHANGE" +}, +[world.event.S_EVENT_MARK_REMOVED]={ +Order=1, +Side="I", +Event="OnEventMarkRemoved", +Text="S_EVENT_MARK_REMOVED" +}, +[EVENTS.NewCargo]={ +Order=1, +Event="OnEventNewCargo", +Text="S_EVENT_NEW_CARGO" +}, +[EVENTS.DeleteCargo]={ +Order=1, +Event="OnEventDeleteCargo", +Text="S_EVENT_DELETE_CARGO" +}, +[EVENTS.NewZone]={ +Order=1, +Event="OnEventNewZone", +Text="S_EVENT_NEW_ZONE" +}, +[EVENTS.DeleteZone]={ +Order=1, +Event="OnEventDeleteZone", +Text="S_EVENT_DELETE_ZONE" +}, +[EVENTS.NewZoneGoal]={ +Order=1, +Event="OnEventNewZoneGoal", +Text="S_EVENT_NEW_ZONE_GOAL" +}, +[EVENTS.DeleteZoneGoal]={ +Order=1, +Event="OnEventDeleteZoneGoal", +Text="S_EVENT_DELETE_ZONE_GOAL" +}, +[EVENTS.RemoveUnit]={ +Order=-1, +Event="OnEventRemoveUnit", +Text="S_EVENT_REMOVE_UNIT" +}, +[EVENTS.PlayerEnterAircraft]={ +Order=1, +Event="OnEventPlayerEnterAircraft", +Text="S_EVENT_PLAYER_ENTER_AIRCRAFT" +}, +[EVENTS.DetailedFailure]={ +Order=1, +Event="OnEventDetailedFailure", +Text="S_EVENT_DETAILED_FAILURE" +}, +[EVENTS.Kill]={ +Order=1, +Event="OnEventKill", +Text="S_EVENT_KILL" +}, +[EVENTS.Score]={ +Order=1, +Event="OnEventScore", +Text="S_EVENT_SCORE" +}, +[EVENTS.UnitLost]={ +Order=1, +Event="OnEventUnitLost", +Text="S_EVENT_UNIT_LOST" +}, +[EVENTS.LandingAfterEjection]={ +Order=1, +Event="OnEventLandingAfterEjection", +Text="S_EVENT_LANDING_AFTER_EJECTION" +}, +[EVENTS.ParatrooperLanding]={ +Order=1, +Event="OnEventParatrooperLanding", +Text="S_EVENT_PARATROOPER_LENDING" +}, +[EVENTS.DiscardChairAfterEjection]={ +Order=1, +Event="OnEventDiscardChairAfterEjection", +Text="S_EVENT_DISCARD_CHAIR_AFTER_EJECTION" +}, +[EVENTS.WeaponAdd]={ +Order=1, +Event="OnEventWeaponAdd", +Text="S_EVENT_WEAPON_ADD" +}, +[EVENTS.TriggerZone]={ +Order=1, +Event="OnEventTriggerZone", +Text="S_EVENT_TRIGGER_ZONE" +}, +[EVENTS.LandingQualityMark]={ +Order=1, +Event="OnEventLandingQualityMark", +Text="S_EVENT_LANDING_QUALITYMARK" +}, +[EVENTS.BDA]={ +Order=1, +Event="OnEventBDA", +Text="S_EVENT_BDA" +}, +} +function EVENT:New() +local self=BASE:Inherit(self,BASE:New()) +self.EventHandler=world.addEventHandler(self) +return self +end +function EVENT:Init(EventID,EventClass) +self:F3({_EVENTMETA[EventID].Text,EventClass}) +if not self.Events[EventID]then +self.Events[EventID]={} +end +local EventPriority=EventClass:GetEventPriority() +if not self.Events[EventID][EventPriority]then +self.Events[EventID][EventPriority]=setmetatable({},{__mode="k"}) +end +if not self.Events[EventID][EventPriority][EventClass]then +self.Events[EventID][EventPriority][EventClass]={} +end +return self.Events[EventID][EventPriority][EventClass] +end +function EVENT:RemoveEvent(EventClass,EventID) +self:F2({"Removing subscription for class: ",EventClass:GetClassNameAndID()}) +local EventPriority=EventClass:GetEventPriority() +self.Events=self.Events or{} +self.Events[EventID]=self.Events[EventID]or{} +self.Events[EventID][EventPriority]=self.Events[EventID][EventPriority]or{} +self.Events[EventID][EventPriority][EventClass]=nil +return self +end +function EVENT:Reset(EventObject) +self:F({"Resetting subscriptions for class: ",EventObject:GetClassNameAndID()}) +local EventPriority=EventObject:GetEventPriority() +for EventID,EventData in pairs(self.Events)do +if self.EventsDead then +if self.EventsDead[EventID]then +if self.EventsDead[EventID][EventPriority]then +if self.EventsDead[EventID][EventPriority][EventObject]then +self.Events[EventID][EventPriority][EventObject]=self.EventsDead[EventID][EventPriority][EventObject] +end +end +end +end +end +end +function EVENT:RemoveAll(EventClass) +local EventClassName=EventClass:GetClassNameAndID() +local EventPriority=EventClass:GetEventPriority() +for EventID,EventData in pairs(self.Events)do +self.Events[EventID][EventPriority][EventClass]=nil +end +return self +end +function EVENT:OnEventForTemplate(EventTemplate,EventFunction,EventClass,EventID) +self:F2(EventTemplate.name) +for EventUnitID,EventUnit in pairs(EventTemplate.units)do +self:OnEventForUnit(EventUnit.name,EventFunction,EventClass,EventID) +end +return self +end +function EVENT:OnEventGeneric(EventFunction,EventClass,EventID) +self:F2({EventID,EventClass,EventFunction}) +local EventData=self:Init(EventID,EventClass) +EventData.EventFunction=EventFunction +return self +end +function EVENT:OnEventForUnit(UnitName,EventFunction,EventClass,EventID) +self:F2(UnitName) +local EventData=self:Init(EventID,EventClass) +EventData.EventUnit=true +EventData.EventFunction=EventFunction +return self +end +function EVENT:OnEventForGroup(GroupName,EventFunction,EventClass,EventID,...) +local Event=self:Init(EventID,EventClass) +Event.EventGroup=true +Event.EventFunction=EventFunction +Event.Params=arg +return self +end +do +function EVENT:OnBirthForTemplate(EventTemplate,EventFunction,EventClass) +self:F2(EventTemplate.name) +self:OnEventForTemplate(EventTemplate,EventFunction,EventClass,EVENTS.Birth) +return self +end +end +do +function EVENT:OnCrashForTemplate(EventTemplate,EventFunction,EventClass) +self:F2(EventTemplate.name) +self:OnEventForTemplate(EventTemplate,EventFunction,EventClass,EVENTS.Crash) +return self +end +end +do +function EVENT:OnDeadForTemplate(EventTemplate,EventFunction,EventClass) +self:F2(EventTemplate.name) +self:OnEventForTemplate(EventTemplate,EventFunction,EventClass,EVENTS.Dead) +return self +end +end +do +function EVENT:OnLandForTemplate(EventTemplate,EventFunction,EventClass) +self:F2(EventTemplate.name) +self:OnEventForTemplate(EventTemplate,EventFunction,EventClass,EVENTS.Land) +return self +end +end +do +function EVENT:OnTakeOffForTemplate(EventTemplate,EventFunction,EventClass) +self:F2(EventTemplate.name) +self:OnEventForTemplate(EventTemplate,EventFunction,EventClass,EVENTS.Takeoff) +return self +end +end +do +function EVENT:OnEngineShutDownForTemplate(EventTemplate,EventFunction,EventClass) +self:F2(EventTemplate.name) +self:OnEventForTemplate(EventTemplate,EventFunction,EventClass,EVENTS.EngineShutdown) +return self +end +end +do +function EVENT:CreateEventNewCargo(Cargo) +self:F({Cargo}) +local Event={ +id=EVENTS.NewCargo, +time=timer.getTime(), +cargo=Cargo, +} +world.onEvent(Event) +end +function EVENT:CreateEventDeleteCargo(Cargo) +self:F({Cargo}) +local Event={ +id=EVENTS.DeleteCargo, +time=timer.getTime(), +cargo=Cargo, +} +world.onEvent(Event) +end +function EVENT:CreateEventNewZone(Zone) +self:F({Zone}) +local Event={ +id=EVENTS.NewZone, +time=timer.getTime(), +zone=Zone, +} +world.onEvent(Event) +end +function EVENT:CreateEventDeleteZone(Zone) +self:F({Zone}) +local Event={ +id=EVENTS.DeleteZone, +time=timer.getTime(), +zone=Zone, +} +world.onEvent(Event) +end +function EVENT:CreateEventNewZoneGoal(ZoneGoal) +self:F({ZoneGoal}) +local Event={ +id=EVENTS.NewZoneGoal, +time=timer.getTime(), +ZoneGoal=ZoneGoal, +} +world.onEvent(Event) +end +function EVENT:CreateEventDeleteZoneGoal(ZoneGoal) +self:F({ZoneGoal}) +local Event={ +id=EVENTS.DeleteZoneGoal, +time=timer.getTime(), +ZoneGoal=ZoneGoal, +} +world.onEvent(Event) +end +function EVENT:CreateEventPlayerEnterUnit(PlayerUnit) +self:F({PlayerUnit}) +local Event={ +id=EVENTS.PlayerEnterUnit, +time=timer.getTime(), +initiator=PlayerUnit:GetDCSObject() +} +world.onEvent(Event) +end +function EVENT:CreateEventPlayerEnterAircraft(PlayerUnit) +self:F({PlayerUnit}) +local Event={ +id=EVENTS.PlayerEnterAircraft, +time=timer.getTime(), +initiator=PlayerUnit:GetDCSObject() +} +world.onEvent(Event) +end +end +function EVENT:onEvent(Event) +local ErrorHandler=function(errmsg) +env.info("Error in SCHEDULER function:"..errmsg) +if BASE.Debug~=nil then +env.info(debug.traceback()) +end +return errmsg +end +local EventMeta=_EVENTMETA[Event.id] +if EventMeta then +if self and +self.Events and +self.Events[Event.id]and +self.MissionEnd==false and +(Event.initiator~=nil or(Event.initiator==nil and Event.id~=EVENTS.PlayerLeaveUnit))then +if Event.id and Event.id==EVENTS.MissionEnd then +self.MissionEnd=true +end +if Event.initiator then +Event.IniObjectCategory=Event.initiator:getCategory() +if Event.IniObjectCategory==Object.Category.UNIT then +Event.IniDCSUnit=Event.initiator +Event.IniDCSUnitName=Event.IniDCSUnit:getName() +Event.IniUnitName=Event.IniDCSUnitName +Event.IniDCSGroup=Event.IniDCSUnit:getGroup() +Event.IniUnit=UNIT:FindByName(Event.IniDCSUnitName) +if not Event.IniUnit then +Event.IniUnit=CLIENT:FindByName(Event.IniDCSUnitName,'',true) +end +Event.IniDCSGroupName="" +if Event.IniDCSGroup and Event.IniDCSGroup:isExist()then +Event.IniDCSGroupName=Event.IniDCSGroup:getName() +Event.IniGroup=GROUP:FindByName(Event.IniDCSGroupName) +Event.IniGroupName=Event.IniDCSGroupName +end +Event.IniPlayerName=Event.IniDCSUnit:getPlayerName() +Event.IniCoalition=Event.IniDCSUnit:getCoalition() +Event.IniTypeName=Event.IniDCSUnit:getTypeName() +Event.IniCategory=Event.IniDCSUnit:getDesc().category +end +if Event.IniObjectCategory==Object.Category.STATIC then +if Event.id==31 then +Event.IniDCSUnit=Event.initiator +local ID=Event.initiator.id_ +Event.IniDCSUnitName=string.format("Ejected Pilot ID %s",tostring(ID)) +Event.IniUnitName=Event.IniDCSUnitName +Event.IniCoalition=0 +Event.IniCategory=0 +Event.IniTypeName="Ejected Pilot" +elseif Event.id==33 then +Event.IniDCSUnit=Event.initiator +local ID=Event.initiator.id_ +Event.IniDCSUnitName=string.format("Ejection Seat ID %s",tostring(ID)) +Event.IniUnitName=Event.IniDCSUnitName +Event.IniCoalition=0 +Event.IniCategory=0 +Event.IniTypeName="Ejection Seat" +else +Event.IniDCSUnit=Event.initiator +Event.IniDCSUnitName=Event.IniDCSUnit:getName() +Event.IniUnitName=Event.IniDCSUnitName +Event.IniUnit=STATIC:FindByName(Event.IniDCSUnitName,false) +Event.IniCoalition=Event.IniDCSUnit:getCoalition() +Event.IniCategory=Event.IniDCSUnit:getDesc().category +Event.IniTypeName=Event.IniDCSUnit:getTypeName() +end +end +if Event.IniObjectCategory==Object.Category.CARGO then +Event.IniDCSUnit=Event.initiator +Event.IniDCSUnitName=Event.IniDCSUnit:getName() +Event.IniUnitName=Event.IniDCSUnitName +Event.IniUnit=CARGO:FindByName(Event.IniDCSUnitName) +Event.IniCoalition=Event.IniDCSUnit:getCoalition() +Event.IniCategory=Event.IniDCSUnit:getDesc().category +Event.IniTypeName=Event.IniDCSUnit:getTypeName() +end +if Event.IniObjectCategory==Object.Category.SCENERY then +Event.IniDCSUnit=Event.initiator +Event.IniDCSUnitName=Event.IniDCSUnit:getName() +Event.IniUnitName=Event.IniDCSUnitName +Event.IniUnit=SCENERY:Register(Event.IniDCSUnitName,Event.initiator) +Event.IniCategory=Event.IniDCSUnit:getDesc().category +Event.IniTypeName=Event.initiator:isExist()and Event.IniDCSUnit:getTypeName()or"SCENERY" +end +if Event.IniObjectCategory==Object.Category.BASE then +Event.IniDCSUnit=Event.initiator +Event.IniDCSUnitName=Event.IniDCSUnit:getName() +Event.IniUnitName=Event.IniDCSUnitName +Event.IniUnit=AIRBASE:FindByName(Event.IniDCSUnitName) +Event.IniCoalition=Event.IniDCSUnit:getCoalition() +Event.IniCategory=Event.IniDCSUnit:getDesc().category +Event.IniTypeName=Event.IniDCSUnit:getTypeName() +end +end +if Event.target then +Event.TgtObjectCategory=Event.target:getCategory() +if Event.TgtObjectCategory==Object.Category.UNIT then +Event.TgtDCSUnit=Event.target +Event.TgtDCSGroup=Event.TgtDCSUnit:getGroup() +Event.TgtDCSUnitName=Event.TgtDCSUnit:getName() +Event.TgtUnitName=Event.TgtDCSUnitName +Event.TgtUnit=UNIT:FindByName(Event.TgtDCSUnitName) +Event.TgtDCSGroupName="" +if Event.TgtDCSGroup and Event.TgtDCSGroup:isExist()then +Event.TgtDCSGroupName=Event.TgtDCSGroup:getName() +Event.TgtGroup=GROUP:FindByName(Event.TgtDCSGroupName) +Event.TgtGroupName=Event.TgtDCSGroupName +end +Event.TgtPlayerName=Event.TgtDCSUnit:getPlayerName() +Event.TgtCoalition=Event.TgtDCSUnit:getCoalition() +Event.TgtCategory=Event.TgtDCSUnit:getDesc().category +Event.TgtTypeName=Event.TgtDCSUnit:getTypeName() +end +if Event.TgtObjectCategory==Object.Category.STATIC then +BASE:T({StaticTgtEvent=Event.id}) +Event.TgtDCSUnit=Event.target +if Event.target:isExist()and Event.id~=33 then +Event.TgtDCSUnitName=Event.TgtDCSUnit:getName() +Event.TgtUnitName=Event.TgtDCSUnitName +Event.TgtUnit=STATIC:FindByName(Event.TgtDCSUnitName,false) +Event.TgtCoalition=Event.TgtDCSUnit:getCoalition() +Event.TgtCategory=Event.TgtDCSUnit:getDesc().category +Event.TgtTypeName=Event.TgtDCSUnit:getTypeName() +else +Event.TgtDCSUnitName=string.format("No target object for Event ID %s",tostring(Event.id)) +Event.TgtUnitName=Event.TgtDCSUnitName +Event.TgtUnit=nil +Event.TgtCoalition=0 +Event.TgtCategory=0 +if Event.id==6 then +Event.TgtTypeName="Ejected Pilot" +Event.TgtDCSUnitName=string.format("Ejected Pilot ID %s",tostring(Event.IniDCSUnitName)) +Event.TgtUnitName=Event.TgtDCSUnitName +elseif Event.id==33 then +Event.TgtTypeName="Ejection Seat" +Event.TgtDCSUnitName=string.format("Ejection Seat ID %s",tostring(Event.IniDCSUnitName)) +Event.TgtUnitName=Event.TgtDCSUnitName +else +Event.TgtTypeName="Static" +end +end +end +if Event.TgtObjectCategory==Object.Category.SCENERY then +Event.TgtDCSUnit=Event.target +Event.TgtDCSUnitName=Event.TgtDCSUnit:getName() +Event.TgtUnitName=Event.TgtDCSUnitName +Event.TgtUnit=SCENERY:Register(Event.TgtDCSUnitName,Event.target) +Event.TgtCategory=Event.TgtDCSUnit:getDesc().category +Event.TgtTypeName=Event.TgtDCSUnit:getTypeName() +end +end +if Event.weapon then +Event.Weapon=Event.weapon +Event.WeaponName=Event.Weapon:getTypeName() +Event.WeaponUNIT=CLIENT:Find(Event.Weapon,'',true) +Event.WeaponPlayerName=Event.WeaponUNIT and Event.Weapon:getPlayerName() +Event.WeaponCoalition=Event.WeaponUNIT and Event.Weapon:getCoalition() +Event.WeaponCategory=Event.WeaponUNIT and Event.Weapon:getDesc().category +Event.WeaponTypeName=Event.WeaponUNIT and Event.Weapon:getTypeName() +end +if Event.place then +if Event.id==EVENTS.LandingAfterEjection then +else +Event.Place=AIRBASE:Find(Event.place) +Event.PlaceName=Event.Place:GetName() +end +end +if Event.idx then +Event.MarkID=Event.idx +Event.MarkVec3=Event.pos +Event.MarkCoordinate=COORDINATE:NewFromVec3(Event.pos) +Event.MarkText=Event.text +Event.MarkCoalition=Event.coalition +Event.MarkGroupID=Event.groupID +end +if Event.cargo then +Event.Cargo=Event.cargo +Event.CargoName=Event.cargo.Name +end +if Event.zone then +Event.Zone=Event.zone +Event.ZoneName=Event.zone.ZoneName +end +local PriorityOrder=EventMeta.Order +local PriorityBegin=PriorityOrder==-1 and 5 or 1 +local PriorityEnd=PriorityOrder==-1 and 1 or 5 +if Event.IniObjectCategory~=Object.Category.STATIC then +self:F({EventMeta.Text,Event,Event.IniDCSUnitName,Event.TgtDCSUnitName,PriorityOrder}) +end +for EventPriority=PriorityBegin,PriorityEnd,PriorityOrder do +if self.Events[Event.id][EventPriority]then +for EventClass,EventData in pairs(self.Events[Event.id][EventPriority])do +Event.IniGroup=GROUP:FindByName(Event.IniDCSGroupName) +Event.TgtGroup=GROUP:FindByName(Event.TgtDCSGroupName) +if EventData.EventUnit then +if EventClass:IsAlive()or +Event.id==EVENTS.PlayerEnterUnit or +Event.id==EVENTS.Crash or +Event.id==EVENTS.Dead or +Event.id==EVENTS.RemoveUnit then +local UnitName=EventClass:GetName() +if(EventMeta.Side=="I"and UnitName==Event.IniDCSUnitName)or +(EventMeta.Side=="T"and UnitName==Event.TgtDCSUnitName)then +if EventData.EventFunction then +if Event.IniObjectCategory~=3 then +self:F({"Calling EventFunction for UNIT ",EventClass:GetClassNameAndID(),", Unit ",Event.IniUnitName,EventPriority}) +end +local Result,Value=xpcall( +function() +return EventData.EventFunction(EventClass,Event) +end,ErrorHandler) +else +local EventFunction=EventClass[EventMeta.Event] +if EventFunction and type(EventFunction)=="function"then +if Event.IniObjectCategory~=3 then +self:F({"Calling "..EventMeta.Event.." for Class ",EventClass:GetClassNameAndID(),EventPriority}) +end +local Result,Value=xpcall( +function() +return EventFunction(EventClass,Event) +end,ErrorHandler) +end +end +end +else +self:RemoveEvent(EventClass,Event.id) +end +else +if EventData.EventGroup then +if EventClass:IsAlive()or +Event.id==EVENTS.PlayerEnterUnit or +Event.id==EVENTS.Crash or +Event.id==EVENTS.Dead or +Event.id==EVENTS.RemoveUnit then +local GroupName=EventClass:GetName() +if(EventMeta.Side=="I"and GroupName==Event.IniDCSGroupName)or +(EventMeta.Side=="T"and GroupName==Event.TgtDCSGroupName)then +if EventData.EventFunction then +if Event.IniObjectCategory~=3 then +self:F({"Calling EventFunction for GROUP ",EventClass:GetClassNameAndID(),", Unit ",Event.IniUnitName,EventPriority}) +end +local Result,Value=xpcall( +function() +return EventData.EventFunction(EventClass,Event,unpack(EventData.Params)) +end,ErrorHandler) +else +local EventFunction=EventClass[EventMeta.Event] +if EventFunction and type(EventFunction)=="function"then +if Event.IniObjectCategory~=3 then +self:F({"Calling "..EventMeta.Event.." for GROUP ",EventClass:GetClassNameAndID(),EventPriority}) +end +local Result,Value=xpcall( +function() +return EventFunction(EventClass,Event,unpack(EventData.Params)) +end,ErrorHandler) +end +end +end +else +end +else +if not EventData.EventUnit then +if EventData.EventFunction then +if Event.IniObjectCategory~=3 then +self:F2({"Calling EventFunction for Class ",EventClass:GetClassNameAndID(),EventPriority}) +end +local Result,Value=xpcall( +function() +return EventData.EventFunction(EventClass,Event) +end,ErrorHandler) +else +local EventFunction=EventClass[EventMeta.Event] +if EventFunction and type(EventFunction)=="function"then +if Event.IniObjectCategory~=3 then +self:F2({"Calling "..EventMeta.Event.." for Class ",EventClass:GetClassNameAndID(),EventPriority}) +end +local Result,Value=xpcall( +function() +local Result,Value=EventFunction(EventClass,Event) +return Result,Value +end,ErrorHandler) +end +end +end +end +end +end +end +end +if Event.id==EVENTS.DeleteCargo then +Event.Cargo.NoDestroy=nil +end +else +self:T({EventMeta.Text,Event}) +end +else +self:E(string.format("WARNING: Could not get EVENTMETA data for event ID=%d! Is this an unknown/new DCS event?",tostring(Event.id))) +end +Event=nil +end +EVENTHANDLER={ +ClassName="EVENTHANDLER", +ClassID=0, +} +function EVENTHANDLER:New() +self=BASE:Inherit(self,BASE:New()) +return self +end +SETTINGS={ +ClassName="SETTINGS", +ShowPlayerMenu=true, +MenuShort=false, +MenuStatic=false, +} +SETTINGS.__Enum={} +SETTINGS.__Enum.Era={ +WWII=1, +Korea=2, +Cold=3, +Modern=4, +} +do +function SETTINGS:Set(PlayerName) +if PlayerName==nil then +local self=BASE:Inherit(self,BASE:New()) +self:SetMetric() +self:SetA2G_BR() +self:SetA2A_BRAA() +self:SetLL_Accuracy(3) +self:SetMGRS_Accuracy(5) +self:SetMessageTime(MESSAGE.Type.Briefing,180) +self:SetMessageTime(MESSAGE.Type.Detailed,60) +self:SetMessageTime(MESSAGE.Type.Information,30) +self:SetMessageTime(MESSAGE.Type.Overview,60) +self:SetMessageTime(MESSAGE.Type.Update,15) +self:SetEraModern() +return self +else +local Settings=_DATABASE:GetPlayerSettings(PlayerName) +if not Settings then +Settings=BASE:Inherit(self,BASE:New()) +_DATABASE:SetPlayerSettings(PlayerName,Settings) +end +return Settings +end +end +function SETTINGS:SetMenutextShort(onoff) +_SETTINGS.MenuShort=onoff +end +function SETTINGS:SetMenuStatic(onoff) +_SETTINGS.MenuStatic=onoff +end +function SETTINGS:SetMetric() +self.Metric=true +end +function SETTINGS:IsMetric() +return(self.Metric~=nil and self.Metric==true)or(self.Metric==nil and _SETTINGS:IsMetric()) +end +function SETTINGS:SetImperial() +self.Metric=false +end +function SETTINGS:IsImperial() +return(self.Metric~=nil and self.Metric==false)or(self.Metric==nil and _SETTINGS:IsMetric()) +end +function SETTINGS:SetLL_Accuracy(LL_Accuracy) +self.LL_Accuracy=LL_Accuracy +end +function SETTINGS:GetLL_DDM_Accuracy() +return self.LL_DDM_Accuracy or _SETTINGS:GetLL_DDM_Accuracy() +end +function SETTINGS:SetMGRS_Accuracy(MGRS_Accuracy) +self.MGRS_Accuracy=MGRS_Accuracy +end +function SETTINGS:GetMGRS_Accuracy() +return self.MGRS_Accuracy or _SETTINGS:GetMGRS_Accuracy() +end +function SETTINGS:SetMessageTime(MessageType,MessageTime) +self.MessageTypeTimings=self.MessageTypeTimings or{} +self.MessageTypeTimings[MessageType]=MessageTime +end +function SETTINGS:GetMessageTime(MessageType) +return(self.MessageTypeTimings and self.MessageTypeTimings[MessageType])or _SETTINGS:GetMessageTime(MessageType) +end +function SETTINGS:SetA2G_LL_DMS() +self.A2GSystem="LL DMS" +end +function SETTINGS:SetA2G_LL_DDM() +self.A2GSystem="LL DDM" +end +function SETTINGS:IsA2G_LL_DMS() +return(self.A2GSystem and self.A2GSystem=="LL DMS")or(not self.A2GSystem and _SETTINGS:IsA2G_LL_DMS()) +end +function SETTINGS:IsA2G_LL_DDM() +return(self.A2GSystem and self.A2GSystem=="LL DDM")or(not self.A2GSystem and _SETTINGS:IsA2G_LL_DDM()) +end +function SETTINGS:SetA2G_MGRS() +self.A2GSystem="MGRS" +end +function SETTINGS:IsA2G_MGRS() +return(self.A2GSystem and self.A2GSystem=="MGRS")or(not self.A2GSystem and _SETTINGS:IsA2G_MGRS()) +end +function SETTINGS:SetA2G_BR() +self.A2GSystem="BR" +end +function SETTINGS:IsA2G_BR() +return(self.A2GSystem and self.A2GSystem=="BR")or(not self.A2GSystem and _SETTINGS:IsA2G_BR()) +end +function SETTINGS:SetA2A_BRAA() +self.A2ASystem="BRAA" +end +function SETTINGS:IsA2A_BRAA() +return(self.A2ASystem and self.A2ASystem=="BRAA")or(not self.A2ASystem and _SETTINGS:IsA2A_BRAA()) +end +function SETTINGS:SetA2A_BULLS() +self.A2ASystem="BULLS" +end +function SETTINGS:IsA2A_BULLS() +return(self.A2ASystem and self.A2ASystem=="BULLS")or(not self.A2ASystem and _SETTINGS:IsA2A_BULLS()) +end +function SETTINGS:SetA2A_LL_DMS() +self.A2ASystem="LL DMS" +end +function SETTINGS:SetA2A_LL_DDM() +self.A2ASystem="LL DDM" +end +function SETTINGS:IsA2A_LL_DMS() +return(self.A2ASystem and self.A2ASystem=="LL DMS")or(not self.A2ASystem and _SETTINGS:IsA2A_LL_DMS()) +end +function SETTINGS:IsA2A_LL_DDM() +return(self.A2ASystem and self.A2ASystem=="LL DDM")or(not self.A2ASystem and _SETTINGS:IsA2A_LL_DDM()) +end +function SETTINGS:SetA2A_MGRS() +self.A2ASystem="MGRS" +end +function SETTINGS:IsA2A_MGRS() +return(self.A2ASystem and self.A2ASystem=="MGRS")or(not self.A2ASystem and _SETTINGS:IsA2A_MGRS()) +end +function SETTINGS:SetSystemMenu(MenuGroup,RootMenu) +local MenuText="System Settings" +local MenuTime=timer.getTime() +local SettingsMenu=MENU_GROUP:New(MenuGroup,MenuText,RootMenu):SetTime(MenuTime) +local text="A2G Coordinate System" +if _SETTINGS.MenuShort then +text="A2G Coordinates" +end +local A2GCoordinateMenu=MENU_GROUP:New(MenuGroup,text,SettingsMenu):SetTime(MenuTime) +if not self:IsA2G_LL_DMS()then +local text="Lat/Lon Degree Min Sec (LL DMS)" +if _SETTINGS.MenuShort then +text="LL DMS" +end +MENU_GROUP_COMMAND:New(MenuGroup,text,A2GCoordinateMenu,self.A2GMenuSystem,self,MenuGroup,RootMenu,"LL DMS"):SetTime(MenuTime) +end +if not self:IsA2G_LL_DDM()then +local text="Lat/Lon Degree Dec Min (LL DDM)" +if _SETTINGS.MenuShort then +text="LL DDM" +end +MENU_GROUP_COMMAND:New(MenuGroup,"Lat/Lon Degree Dec Min (LL DDM)",A2GCoordinateMenu,self.A2GMenuSystem,self,MenuGroup,RootMenu,"LL DDM"):SetTime(MenuTime) +end +if self:IsA2G_LL_DDM()then +local text1="LL DDM Accuracy 1" +local text2="LL DDM Accuracy 2" +local text3="LL DDM Accuracy 3" +if _SETTINGS.MenuShort then +text1="LL DDM" +end +MENU_GROUP_COMMAND:New(MenuGroup,"LL DDM Accuracy 1",A2GCoordinateMenu,self.MenuLL_DDM_Accuracy,self,MenuGroup,RootMenu,1):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"LL DDM Accuracy 2",A2GCoordinateMenu,self.MenuLL_DDM_Accuracy,self,MenuGroup,RootMenu,2):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"LL DDM Accuracy 3",A2GCoordinateMenu,self.MenuLL_DDM_Accuracy,self,MenuGroup,RootMenu,3):SetTime(MenuTime) +end +if not self:IsA2G_BR()then +local text="Bearing, Range (BR)" +if _SETTINGS.MenuShort then +text="BR" +end +MENU_GROUP_COMMAND:New(MenuGroup,text,A2GCoordinateMenu,self.A2GMenuSystem,self,MenuGroup,RootMenu,"BR"):SetTime(MenuTime) +end +if not self:IsA2G_MGRS()then +local text="Military Grid (MGRS)" +if _SETTINGS.MenuShort then +text="MGRS" +end +MENU_GROUP_COMMAND:New(MenuGroup,text,A2GCoordinateMenu,self.A2GMenuSystem,self,MenuGroup,RootMenu,"MGRS"):SetTime(MenuTime) +end +if self:IsA2G_MGRS()then +MENU_GROUP_COMMAND:New(MenuGroup,"MGRS Accuracy 1",A2GCoordinateMenu,self.MenuMGRS_Accuracy,self,MenuGroup,RootMenu,1):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"MGRS Accuracy 2",A2GCoordinateMenu,self.MenuMGRS_Accuracy,self,MenuGroup,RootMenu,2):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"MGRS Accuracy 3",A2GCoordinateMenu,self.MenuMGRS_Accuracy,self,MenuGroup,RootMenu,3):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"MGRS Accuracy 4",A2GCoordinateMenu,self.MenuMGRS_Accuracy,self,MenuGroup,RootMenu,4):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"MGRS Accuracy 5",A2GCoordinateMenu,self.MenuMGRS_Accuracy,self,MenuGroup,RootMenu,5):SetTime(MenuTime) +end +local text="A2A Coordinate System" +if _SETTINGS.MenuShort then +text="A2A Coordinates" +end +local A2ACoordinateMenu=MENU_GROUP:New(MenuGroup,text,SettingsMenu):SetTime(MenuTime) +if not self:IsA2A_LL_DMS()then +local text="Lat/Lon Degree Min Sec (LL DMS)" +if _SETTINGS.MenuShort then +text="LL DMS" +end +MENU_GROUP_COMMAND:New(MenuGroup,text,A2ACoordinateMenu,self.A2AMenuSystem,self,MenuGroup,RootMenu,"LL DMS"):SetTime(MenuTime) +end +if not self:IsA2A_LL_DDM()then +local text="Lat/Lon Degree Dec Min (LL DDM)" +if _SETTINGS.MenuShort then +text="LL DDM" +end +MENU_GROUP_COMMAND:New(MenuGroup,text,A2ACoordinateMenu,self.A2AMenuSystem,self,MenuGroup,RootMenu,"LL DDM"):SetTime(MenuTime) +end +if self:IsA2A_LL_DDM()or self:IsA2A_LL_DMS()then +MENU_GROUP_COMMAND:New(MenuGroup,"LL Accuracy 0",A2ACoordinateMenu,self.MenuLL_DDM_Accuracy,self,MenuGroup,RootMenu,0):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"LL Accuracy 1",A2ACoordinateMenu,self.MenuLL_DDM_Accuracy,self,MenuGroup,RootMenu,1):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"LL Accuracy 2",A2ACoordinateMenu,self.MenuLL_DDM_Accuracy,self,MenuGroup,RootMenu,2):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"LL Accuracy 3",A2ACoordinateMenu,self.MenuLL_DDM_Accuracy,self,MenuGroup,RootMenu,3):SetTime(MenuTime) +end +if not self:IsA2A_BULLS()then +local text="Bullseye (BULLS)" +if _SETTINGS.MenuShort then +text="Bulls" +end +MENU_GROUP_COMMAND:New(MenuGroup,text,A2ACoordinateMenu,self.A2AMenuSystem,self,MenuGroup,RootMenu,"BULLS"):SetTime(MenuTime) +end +if not self:IsA2A_BRAA()then +local text="Bearing Range Altitude Aspect (BRAA)" +if _SETTINGS.MenuShort then +text="BRAA" +end +MENU_GROUP_COMMAND:New(MenuGroup,text,A2ACoordinateMenu,self.A2AMenuSystem,self,MenuGroup,RootMenu,"BRAA"):SetTime(MenuTime) +end +if not self:IsA2A_MGRS()then +local text="Military Grid (MGRS)" +if _SETTINGS.MenuShort then +text="MGRS" +end +MENU_GROUP_COMMAND:New(MenuGroup,text,A2ACoordinateMenu,self.A2AMenuSystem,self,MenuGroup,RootMenu,"MGRS"):SetTime(MenuTime) +end +if self:IsA2A_MGRS()then +MENU_GROUP_COMMAND:New(MenuGroup,"MGRS Accuracy 1",A2ACoordinateMenu,self.MenuMGRS_Accuracy,self,MenuGroup,RootMenu,1):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"MGRS Accuracy 2",A2ACoordinateMenu,self.MenuMGRS_Accuracy,self,MenuGroup,RootMenu,2):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"MGRS Accuracy 3",A2ACoordinateMenu,self.MenuMGRS_Accuracy,self,MenuGroup,RootMenu,3):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"MGRS Accuracy 4",A2ACoordinateMenu,self.MenuMGRS_Accuracy,self,MenuGroup,RootMenu,4):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"MGRS Accuracy 5",A2ACoordinateMenu,self.MenuMGRS_Accuracy,self,MenuGroup,RootMenu,5):SetTime(MenuTime) +end +local text="Measures and Weights System" +if _SETTINGS.MenuShort then +text="Unit System" +end +local MetricsMenu=MENU_GROUP:New(MenuGroup,text,SettingsMenu):SetTime(MenuTime) +if self:IsMetric()then +local text="Imperial (Miles,Feet)" +if _SETTINGS.MenuShort then +text="Imperial" +end +MENU_GROUP_COMMAND:New(MenuGroup,text,MetricsMenu,self.MenuMWSystem,self,MenuGroup,RootMenu,false):SetTime(MenuTime) +end +if self:IsImperial()then +local text="Metric (Kilometers,Meters)" +if _SETTINGS.MenuShort then +text="Metric" +end +MENU_GROUP_COMMAND:New(MenuGroup,text,MetricsMenu,self.MenuMWSystem,self,MenuGroup,RootMenu,true):SetTime(MenuTime) +end +local text="Messages and Reports" +if _SETTINGS.MenuShort then +text="Messages & Reports" +end +local MessagesMenu=MENU_GROUP:New(MenuGroup,text,SettingsMenu):SetTime(MenuTime) +local UpdateMessagesMenu=MENU_GROUP:New(MenuGroup,"Update Messages",MessagesMenu):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"Off",UpdateMessagesMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Update,0):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"5 seconds",UpdateMessagesMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Update,5):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"10 seconds",UpdateMessagesMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Update,10):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"15 seconds",UpdateMessagesMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Update,15):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"30 seconds",UpdateMessagesMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Update,30):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"1 minute",UpdateMessagesMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Update,60):SetTime(MenuTime) +local InformationMessagesMenu=MENU_GROUP:New(MenuGroup,"Information Messages",MessagesMenu):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"5 seconds",InformationMessagesMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Information,5):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"10 seconds",InformationMessagesMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Information,10):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"15 seconds",InformationMessagesMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Information,15):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"30 seconds",InformationMessagesMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Information,30):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"1 minute",InformationMessagesMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Information,60):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"2 minutes",InformationMessagesMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Information,120):SetTime(MenuTime) +local BriefingReportsMenu=MENU_GROUP:New(MenuGroup,"Briefing Reports",MessagesMenu):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"15 seconds",BriefingReportsMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Briefing,15):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"30 seconds",BriefingReportsMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Briefing,30):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"1 minute",BriefingReportsMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Briefing,60):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"2 minutes",BriefingReportsMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Briefing,120):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"3 minutes",BriefingReportsMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Briefing,180):SetTime(MenuTime) +local OverviewReportsMenu=MENU_GROUP:New(MenuGroup,"Overview Reports",MessagesMenu):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"15 seconds",OverviewReportsMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Overview,15):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"30 seconds",OverviewReportsMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Overview,30):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"1 minute",OverviewReportsMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Overview,60):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"2 minutes",OverviewReportsMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Overview,120):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"3 minutes",OverviewReportsMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.Overview,180):SetTime(MenuTime) +local DetailedReportsMenu=MENU_GROUP:New(MenuGroup,"Detailed Reports",MessagesMenu):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"15 seconds",DetailedReportsMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.DetailedReportsMenu,15):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"30 seconds",DetailedReportsMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.DetailedReportsMenu,30):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"1 minute",DetailedReportsMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.DetailedReportsMenu,60):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"2 minutes",DetailedReportsMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.DetailedReportsMenu,120):SetTime(MenuTime) +MENU_GROUP_COMMAND:New(MenuGroup,"3 minutes",DetailedReportsMenu,self.MenuMessageTimingsSystem,self,MenuGroup,RootMenu,MESSAGE.Type.DetailedReportsMenu,180):SetTime(MenuTime) +SettingsMenu:Remove(MenuTime) +return self +end +function SETTINGS:SetPlayerMenuOn() +self.ShowPlayerMenu=true +end +function SETTINGS:SetPlayerMenuOff() +self.ShowPlayerMenu=false +end +function SETTINGS:SetPlayerMenu(PlayerUnit) +if _SETTINGS.ShowPlayerMenu==true then +local PlayerGroup=PlayerUnit:GetGroup() +local PlayerName=PlayerUnit:GetPlayerName() +local PlayerNames=PlayerGroup:GetPlayerNames() +local PlayerMenu=MENU_GROUP:New(PlayerGroup,'Settings "'..PlayerName..'"') +self.PlayerMenu=PlayerMenu +self:I(string.format("Setting menu for player %s",tostring(PlayerName))) +local submenu=MENU_GROUP:New(PlayerGroup,"LL Accuracy",PlayerMenu) +MENU_GROUP_COMMAND:New(PlayerGroup,"LL 0 Decimals",submenu,self.MenuGroupLL_DDM_AccuracySystem,self,PlayerUnit,PlayerGroup,PlayerName,0) +MENU_GROUP_COMMAND:New(PlayerGroup,"LL 1 Decimal",submenu,self.MenuGroupLL_DDM_AccuracySystem,self,PlayerUnit,PlayerGroup,PlayerName,1) +MENU_GROUP_COMMAND:New(PlayerGroup,"LL 2 Decimals",submenu,self.MenuGroupLL_DDM_AccuracySystem,self,PlayerUnit,PlayerGroup,PlayerName,2) +MENU_GROUP_COMMAND:New(PlayerGroup,"LL 3 Decimals",submenu,self.MenuGroupLL_DDM_AccuracySystem,self,PlayerUnit,PlayerGroup,PlayerName,3) +MENU_GROUP_COMMAND:New(PlayerGroup,"LL 4 Decimals",submenu,self.MenuGroupLL_DDM_AccuracySystem,self,PlayerUnit,PlayerGroup,PlayerName,4) +local submenu=MENU_GROUP:New(PlayerGroup,"MGRS Accuracy",PlayerMenu) +MENU_GROUP_COMMAND:New(PlayerGroup,"MRGS Accuracy 0",submenu,self.MenuGroupMGRS_AccuracySystem,self,PlayerUnit,PlayerGroup,PlayerName,0) +MENU_GROUP_COMMAND:New(PlayerGroup,"MRGS Accuracy 1",submenu,self.MenuGroupMGRS_AccuracySystem,self,PlayerUnit,PlayerGroup,PlayerName,1) +MENU_GROUP_COMMAND:New(PlayerGroup,"MRGS Accuracy 2",submenu,self.MenuGroupMGRS_AccuracySystem,self,PlayerUnit,PlayerGroup,PlayerName,2) +MENU_GROUP_COMMAND:New(PlayerGroup,"MRGS Accuracy 3",submenu,self.MenuGroupMGRS_AccuracySystem,self,PlayerUnit,PlayerGroup,PlayerName,3) +MENU_GROUP_COMMAND:New(PlayerGroup,"MRGS Accuracy 4",submenu,self.MenuGroupMGRS_AccuracySystem,self,PlayerUnit,PlayerGroup,PlayerName,4) +MENU_GROUP_COMMAND:New(PlayerGroup,"MRGS Accuracy 5",submenu,self.MenuGroupMGRS_AccuracySystem,self,PlayerUnit,PlayerGroup,PlayerName,5) +local text="A2G Coordinate System" +if _SETTINGS.MenuShort then +text="A2G Coordinates" +end +local A2GCoordinateMenu=MENU_GROUP:New(PlayerGroup,text,PlayerMenu) +if not self:IsA2G_LL_DMS()or _SETTINGS.MenuStatic then +local text="Lat/Lon Degree Min Sec (LL DMS)" +if _SETTINGS.MenuShort then +text="A2G LL DMS" +end +MENU_GROUP_COMMAND:New(PlayerGroup,text,A2GCoordinateMenu,self.MenuGroupA2GSystem,self,PlayerUnit,PlayerGroup,PlayerName,"LL DMS") +end +if not self:IsA2G_LL_DDM()or _SETTINGS.MenuStatic then +local text="Lat/Lon Degree Dec Min (LL DDM)" +if _SETTINGS.MenuShort then +text="A2G LL DDM" +end +MENU_GROUP_COMMAND:New(PlayerGroup,text,A2GCoordinateMenu,self.MenuGroupA2GSystem,self,PlayerUnit,PlayerGroup,PlayerName,"LL DDM") +end +if not self:IsA2G_BR()or _SETTINGS.MenuStatic then +local text="Bearing, Range (BR)" +if _SETTINGS.MenuShort then +text="A2G BR" +end +MENU_GROUP_COMMAND:New(PlayerGroup,text,A2GCoordinateMenu,self.MenuGroupA2GSystem,self,PlayerUnit,PlayerGroup,PlayerName,"BR") +end +if not self:IsA2G_MGRS()or _SETTINGS.MenuStatic then +local text="Military Grid (MGRS)" +if _SETTINGS.MenuShort then +text="A2G MGRS" +end +MENU_GROUP_COMMAND:New(PlayerGroup,text,A2GCoordinateMenu,self.MenuGroupA2GSystem,self,PlayerUnit,PlayerGroup,PlayerName,"MGRS") +end +local text="A2A Coordinate System" +if _SETTINGS.MenuShort then +text="A2A Coordinates" +end +local A2ACoordinateMenu=MENU_GROUP:New(PlayerGroup,text,PlayerMenu) +if not self:IsA2A_LL_DMS()or _SETTINGS.MenuStatic then +local text="Lat/Lon Degree Min Sec (LL DMS)" +if _SETTINGS.MenuShort then +text="A2A LL DMS" +end +MENU_GROUP_COMMAND:New(PlayerGroup,text,A2ACoordinateMenu,self.MenuGroupA2GSystem,self,PlayerUnit,PlayerGroup,PlayerName,"LL DMS") +end +if not self:IsA2A_LL_DDM()or _SETTINGS.MenuStatic then +local text="Lat/Lon Degree Dec Min (LL DDM)" +if _SETTINGS.MenuShort then +text="A2A LL DDM" +end +MENU_GROUP_COMMAND:New(PlayerGroup,text,A2ACoordinateMenu,self.MenuGroupA2ASystem,self,PlayerUnit,PlayerGroup,PlayerName,"LL DDM") +end +if not self:IsA2A_BULLS()or _SETTINGS.MenuStatic then +local text="Bullseye (BULLS)" +if _SETTINGS.MenuShort then +text="A2A BULLS" +end +MENU_GROUP_COMMAND:New(PlayerGroup,text,A2ACoordinateMenu,self.MenuGroupA2ASystem,self,PlayerUnit,PlayerGroup,PlayerName,"BULLS") +end +if not self:IsA2A_BRAA()or _SETTINGS.MenuStatic then +local text="Bearing Range Altitude Aspect (BRAA)" +if _SETTINGS.MenuShort then +text="A2A BRAA" +end +MENU_GROUP_COMMAND:New(PlayerGroup,text,A2ACoordinateMenu,self.MenuGroupA2ASystem,self,PlayerUnit,PlayerGroup,PlayerName,"BRAA") +end +if not self:IsA2A_MGRS()or _SETTINGS.MenuStatic then +local text="Military Grid (MGRS)" +if _SETTINGS.MenuShort then +text="A2A MGRS" +end +MENU_GROUP_COMMAND:New(PlayerGroup,text,A2ACoordinateMenu,self.MenuGroupA2ASystem,self,PlayerUnit,PlayerGroup,PlayerName,"MGRS") +end +local text="Measures and Weights System" +if _SETTINGS.MenuShort then +text="Unit System" +end +local MetricsMenu=MENU_GROUP:New(PlayerGroup,text,PlayerMenu) +if self:IsMetric()or _SETTINGS.MenuStatic then +local text="Imperial (Miles,Feet)" +if _SETTINGS.MenuShort then +text="Imperial" +end +MENU_GROUP_COMMAND:New(PlayerGroup,text,MetricsMenu,self.MenuGroupMWSystem,self,PlayerUnit,PlayerGroup,PlayerName,false) +end +if self:IsImperial()or _SETTINGS.MenuStatic then +local text="Metric (Kilometers,Meters)" +if _SETTINGS.MenuShort then +text="Metric" +end +MENU_GROUP_COMMAND:New(PlayerGroup,text,MetricsMenu,self.MenuGroupMWSystem,self,PlayerUnit,PlayerGroup,PlayerName,true) +end +local text="Messages and Reports" +if _SETTINGS.MenuShort then +text="Messages & Reports" +end +local MessagesMenu=MENU_GROUP:New(PlayerGroup,text,PlayerMenu) +local UpdateMessagesMenu=MENU_GROUP:New(PlayerGroup,"Update Messages",MessagesMenu) +MENU_GROUP_COMMAND:New(PlayerGroup,"Updates Off",UpdateMessagesMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Update,0) +MENU_GROUP_COMMAND:New(PlayerGroup,"Updates 5 sec",UpdateMessagesMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Update,5) +MENU_GROUP_COMMAND:New(PlayerGroup,"Updates 10 sec",UpdateMessagesMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Update,10) +MENU_GROUP_COMMAND:New(PlayerGroup,"Updates 15 sec",UpdateMessagesMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Update,15) +MENU_GROUP_COMMAND:New(PlayerGroup,"Updates 30 sec",UpdateMessagesMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Update,30) +MENU_GROUP_COMMAND:New(PlayerGroup,"Updates 1 min",UpdateMessagesMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Update,60) +local InformationMessagesMenu=MENU_GROUP:New(PlayerGroup,"Info Messages",MessagesMenu) +MENU_GROUP_COMMAND:New(PlayerGroup,"Info 5 sec",InformationMessagesMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Information,5) +MENU_GROUP_COMMAND:New(PlayerGroup,"Info 10 sec",InformationMessagesMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Information,10) +MENU_GROUP_COMMAND:New(PlayerGroup,"Info 15 sec",InformationMessagesMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Information,15) +MENU_GROUP_COMMAND:New(PlayerGroup,"Info 30 sec",InformationMessagesMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Information,30) +MENU_GROUP_COMMAND:New(PlayerGroup,"Info 1 min",InformationMessagesMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Information,60) +MENU_GROUP_COMMAND:New(PlayerGroup,"Info 2 min",InformationMessagesMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Information,120) +local BriefingReportsMenu=MENU_GROUP:New(PlayerGroup,"Briefing Reports",MessagesMenu) +MENU_GROUP_COMMAND:New(PlayerGroup,"Brief 15 sec",BriefingReportsMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Briefing,15) +MENU_GROUP_COMMAND:New(PlayerGroup,"Brief 30 sec",BriefingReportsMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Briefing,30) +MENU_GROUP_COMMAND:New(PlayerGroup,"Brief 1 min",BriefingReportsMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Briefing,60) +MENU_GROUP_COMMAND:New(PlayerGroup,"Brief 2 min",BriefingReportsMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Briefing,120) +MENU_GROUP_COMMAND:New(PlayerGroup,"Brief 3 min",BriefingReportsMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Briefing,180) +local OverviewReportsMenu=MENU_GROUP:New(PlayerGroup,"Overview Reports",MessagesMenu) +MENU_GROUP_COMMAND:New(PlayerGroup,"Overview 15 sec",OverviewReportsMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Overview,15) +MENU_GROUP_COMMAND:New(PlayerGroup,"Overview 30 sec",OverviewReportsMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Overview,30) +MENU_GROUP_COMMAND:New(PlayerGroup,"Overview 1 min",OverviewReportsMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Overview,60) +MENU_GROUP_COMMAND:New(PlayerGroup,"Overview 2 min",OverviewReportsMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Overview,120) +MENU_GROUP_COMMAND:New(PlayerGroup,"Overview 3 min",OverviewReportsMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.Overview,180) +local DetailedReportsMenu=MENU_GROUP:New(PlayerGroup,"Detailed Reports",MessagesMenu) +MENU_GROUP_COMMAND:New(PlayerGroup,"Detailed 15 sec",DetailedReportsMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.DetailedReportsMenu,15) +MENU_GROUP_COMMAND:New(PlayerGroup,"Detailed 30 sec",DetailedReportsMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.DetailedReportsMenu,30) +MENU_GROUP_COMMAND:New(PlayerGroup,"Detailed 1 min",DetailedReportsMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.DetailedReportsMenu,60) +MENU_GROUP_COMMAND:New(PlayerGroup,"Detailed 2 min",DetailedReportsMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.DetailedReportsMenu,120) +MENU_GROUP_COMMAND:New(PlayerGroup,"Detailed 3 min",DetailedReportsMenu,self.MenuGroupMessageTimingsSystem,self,PlayerUnit,PlayerGroup,PlayerName,MESSAGE.Type.DetailedReportsMenu,180) +end +return self +end +function SETTINGS:RemovePlayerMenu(PlayerUnit) +if self.PlayerMenu then +self.PlayerMenu:Remove() +self.PlayerMenu=nil +end +return self +end +function SETTINGS:A2GMenuSystem(MenuGroup,RootMenu,A2GSystem) +self.A2GSystem=A2GSystem +MESSAGE:New(string.format("Settings: Default A2G coordinate system set to %s for all players!",A2GSystem),5):ToAll() +self:SetSystemMenu(MenuGroup,RootMenu) +end +function SETTINGS:A2AMenuSystem(MenuGroup,RootMenu,A2ASystem) +self.A2ASystem=A2ASystem +MESSAGE:New(string.format("Settings: Default A2A coordinate system set to %s for all players!",A2ASystem),5):ToAll() +self:SetSystemMenu(MenuGroup,RootMenu) +end +function SETTINGS:MenuLL_DDM_Accuracy(MenuGroup,RootMenu,LL_Accuracy) +self.LL_Accuracy=LL_Accuracy +MESSAGE:New(string.format("Settings: Default LL accuracy set to %s for all players!",LL_Accuracy),5):ToAll() +self:SetSystemMenu(MenuGroup,RootMenu) +end +function SETTINGS:MenuMGRS_Accuracy(MenuGroup,RootMenu,MGRS_Accuracy) +self.MGRS_Accuracy=MGRS_Accuracy +MESSAGE:New(string.format("Settings: Default MGRS accuracy set to %s for all players!",MGRS_Accuracy),5):ToAll() +self:SetSystemMenu(MenuGroup,RootMenu) +end +function SETTINGS:MenuMWSystem(MenuGroup,RootMenu,MW) +self.Metric=MW +MESSAGE:New(string.format("Settings: Default measurement format set to %s for all players!",MW and"Metric"or"Imperial"),5):ToAll() +self:SetSystemMenu(MenuGroup,RootMenu) +end +function SETTINGS:MenuMessageTimingsSystem(MenuGroup,RootMenu,MessageType,MessageTime) +self:SetMessageTime(MessageType,MessageTime) +MESSAGE:New(string.format("Settings: Default message time set for %s to %d.",MessageType,MessageTime),5):ToAll() +end +do +function SETTINGS:MenuGroupA2GSystem(PlayerUnit,PlayerGroup,PlayerName,A2GSystem) +BASE:E({self,PlayerUnit:GetName(),A2GSystem}) +self.A2GSystem=A2GSystem +MESSAGE:New(string.format("Settings: A2G format set to %s for player %s.",A2GSystem,PlayerName),5):ToGroup(PlayerGroup) +if _SETTINGS.MenuStatic==false then +self:RemovePlayerMenu(PlayerUnit) +self:SetPlayerMenu(PlayerUnit) +end +end +function SETTINGS:MenuGroupA2ASystem(PlayerUnit,PlayerGroup,PlayerName,A2ASystem) +self.A2ASystem=A2ASystem +MESSAGE:New(string.format("Settings: A2A format set to %s for player %s.",A2ASystem,PlayerName),5):ToGroup(PlayerGroup) +if _SETTINGS.MenuStatic==false then +self:RemovePlayerMenu(PlayerUnit) +self:SetPlayerMenu(PlayerUnit) +end +end +function SETTINGS:MenuGroupLL_DDM_AccuracySystem(PlayerUnit,PlayerGroup,PlayerName,LL_Accuracy) +self.LL_Accuracy=LL_Accuracy +MESSAGE:New(string.format("Settings: LL format accuracy set to %d decimal places for player %s.",LL_Accuracy,PlayerName),5):ToGroup(PlayerGroup) +if _SETTINGS.MenuStatic==false then +self:RemovePlayerMenu(PlayerUnit) +self:SetPlayerMenu(PlayerUnit) +end +end +function SETTINGS:MenuGroupMGRS_AccuracySystem(PlayerUnit,PlayerGroup,PlayerName,MGRS_Accuracy) +self.MGRS_Accuracy=MGRS_Accuracy +MESSAGE:New(string.format("Settings: MGRS format accuracy set to %d for player %s.",MGRS_Accuracy,PlayerName),5):ToGroup(PlayerGroup) +if _SETTINGS.MenuStatic==false then +self:RemovePlayerMenu(PlayerUnit) +self:SetPlayerMenu(PlayerUnit) +end +end +function SETTINGS:MenuGroupMWSystem(PlayerUnit,PlayerGroup,PlayerName,MW) +self.Metric=MW +MESSAGE:New(string.format("Settings: Measurement format set to %s for player %s.",MW and"Metric"or"Imperial",PlayerName),5):ToGroup(PlayerGroup) +if _SETTINGS.MenuStatic==false then +self:RemovePlayerMenu(PlayerUnit) +self:SetPlayerMenu(PlayerUnit) +end +end +function SETTINGS:MenuGroupMessageTimingsSystem(PlayerUnit,PlayerGroup,PlayerName,MessageType,MessageTime) +self:SetMessageTime(MessageType,MessageTime) +MESSAGE:New(string.format("Settings: Default message time set for %s to %d.",MessageType,MessageTime),5):ToGroup(PlayerGroup) +end +end +function SETTINGS:SetEraWWII() +self.Era=SETTINGS.__Enum.Era.WWII +end +function SETTINGS:SetEraKorea() +self.Era=SETTINGS.__Enum.Era.Korea +end +function SETTINGS:SetEraCold() +self.Era=SETTINGS.__Enum.Era.Cold +end +function SETTINGS:SetEraModern() +self.Era=SETTINGS.__Enum.Era.Modern +end +end +MENU_INDEX={} +MENU_INDEX.MenuMission={} +MENU_INDEX.MenuMission.Menus={} +MENU_INDEX.Coalition={} +MENU_INDEX.Coalition[coalition.side.BLUE]={} +MENU_INDEX.Coalition[coalition.side.BLUE].Menus={} +MENU_INDEX.Coalition[coalition.side.RED]={} +MENU_INDEX.Coalition[coalition.side.RED].Menus={} +MENU_INDEX.Group={} +function MENU_INDEX:ParentPath(ParentMenu,MenuText) +local Path=ParentMenu and"@"..table.concat(ParentMenu.MenuPath or{},"@")or"" +if ParentMenu then +if ParentMenu:IsInstanceOf("MENU_GROUP")or ParentMenu:IsInstanceOf("MENU_GROUP_COMMAND")then +local GroupName=ParentMenu.Group:GetName() +if not self.Group[GroupName].Menus[Path]then +BASE:E({Path=Path,GroupName=GroupName}) +error("Parent path not found in menu index for group menu") +return nil +end +elseif ParentMenu:IsInstanceOf("MENU_COALITION")or ParentMenu:IsInstanceOf("MENU_COALITION_COMMAND")then +local Coalition=ParentMenu.Coalition +if not self.Coalition[Coalition].Menus[Path]then +BASE:E({Path=Path,Coalition=Coalition}) +error("Parent path not found in menu index for coalition menu") +return nil +end +elseif ParentMenu:IsInstanceOf("MENU_MISSION")or ParentMenu:IsInstanceOf("MENU_MISSION_COMMAND")then +if not self.MenuMission.Menus[Path]then +BASE:E({Path=Path}) +error("Parent path not found in menu index for mission menu") +return nil +end +end +end +Path=Path.."@"..MenuText +return Path +end +function MENU_INDEX:PrepareMission() +self.MenuMission.Menus=self.MenuMission.Menus or{} +end +function MENU_INDEX:PrepareCoalition(CoalitionSide) +self.Coalition[CoalitionSide]=self.Coalition[CoalitionSide]or{} +self.Coalition[CoalitionSide].Menus=self.Coalition[CoalitionSide].Menus or{} +end +function MENU_INDEX:PrepareGroup(Group) +if Group and Group:IsAlive()~=nil then +local GroupName=Group:GetName() +self.Group[GroupName]=self.Group[GroupName]or{} +self.Group[GroupName].Menus=self.Group[GroupName].Menus or{} +end +end +function MENU_INDEX:HasMissionMenu(Path) +return self.MenuMission.Menus[Path] +end +function MENU_INDEX:SetMissionMenu(Path,Menu) +self.MenuMission.Menus[Path]=Menu +end +function MENU_INDEX:ClearMissionMenu(Path) +self.MenuMission.Menus[Path]=nil +end +function MENU_INDEX:HasCoalitionMenu(Coalition,Path) +return self.Coalition[Coalition].Menus[Path] +end +function MENU_INDEX:SetCoalitionMenu(Coalition,Path,Menu) +self.Coalition[Coalition].Menus[Path]=Menu +end +function MENU_INDEX:ClearCoalitionMenu(Coalition,Path) +self.Coalition[Coalition].Menus[Path]=nil +end +function MENU_INDEX:HasGroupMenu(Group,Path) +if Group and Group:IsAlive()then +local MenuGroupName=Group:GetName() +return self.Group[MenuGroupName].Menus[Path] +end +return nil +end +function MENU_INDEX:SetGroupMenu(Group,Path,Menu) +local MenuGroupName=Group:GetName() +Group:F({MenuGroupName=MenuGroupName,Path=Path}) +self.Group[MenuGroupName].Menus[Path]=Menu +end +function MENU_INDEX:ClearGroupMenu(Group,Path) +local MenuGroupName=Group:GetName() +self.Group[MenuGroupName].Menus[Path]=nil +end +function MENU_INDEX:Refresh(Group) +for MenuID,Menu in pairs(self.MenuMission.Menus)do +Menu:Refresh() +end +for MenuID,Menu in pairs(self.Coalition[coalition.side.BLUE].Menus)do +Menu:Refresh() +end +for MenuID,Menu in pairs(self.Coalition[coalition.side.RED].Menus)do +Menu:Refresh() +end +local GroupName=Group:GetName() +for MenuID,Menu in pairs(self.Group[GroupName].Menus)do +Menu:Refresh() +end +end +do +MENU_BASE={ +ClassName="MENU_BASE", +MenuPath=nil, +MenuText="", +MenuParentPath=nil +} +function MENU_BASE:New(MenuText,ParentMenu) +local MenuParentPath={} +if ParentMenu~=nil then +MenuParentPath=ParentMenu.MenuPath +end +local self=BASE:Inherit(self,BASE:New()) +self.MenuPath=nil +self.MenuText=MenuText +self.ParentMenu=ParentMenu +self.MenuParentPath=MenuParentPath +self.Path=(self.ParentMenu and"@"..table.concat(self.MenuParentPath or{},"@")or"").."@"..self.MenuText +self.Menus={} +self.MenuCount=0 +self.MenuStamp=timer.getTime() +self.MenuRemoveParent=false +if self.ParentMenu then +self.ParentMenu.Menus=self.ParentMenu.Menus or{} +self.ParentMenu.Menus[MenuText]=self +end +return self +end +function MENU_BASE:SetParentMenu(MenuText,Menu) +if self.ParentMenu then +self.ParentMenu.Menus=self.ParentMenu.Menus or{} +self.ParentMenu.Menus[MenuText]=Menu +self.ParentMenu.MenuCount=self.ParentMenu.MenuCount+1 +end +end +function MENU_BASE:ClearParentMenu(MenuText) +if self.ParentMenu and self.ParentMenu.Menus[MenuText]then +self.ParentMenu.Menus[MenuText]=nil +self.ParentMenu.MenuCount=self.ParentMenu.MenuCount-1 +if self.ParentMenu.MenuCount==0 then +end +end +end +function MENU_BASE:SetRemoveParent(RemoveParent) +self.MenuRemoveParent=RemoveParent +return self +end +function MENU_BASE:GetMenu(MenuText) +return self.Menus[MenuText] +end +function MENU_BASE:SetStamp(MenuStamp) +self.MenuStamp=MenuStamp +return self +end +function MENU_BASE:GetStamp() +return timer.getTime() +end +function MENU_BASE:SetTime(MenuStamp) +self.MenuStamp=MenuStamp +return self +end +function MENU_BASE:SetTag(MenuTag) +self.MenuTag=MenuTag +return self +end +end +do +MENU_COMMAND_BASE={ +ClassName="MENU_COMMAND_BASE", +CommandMenuFunction=nil, +CommandMenuArgument=nil, +MenuCallHandler=nil, +} +function MENU_COMMAND_BASE:New(MenuText,ParentMenu,CommandMenuFunction,CommandMenuArguments) +local self=BASE:Inherit(self,MENU_BASE:New(MenuText,ParentMenu)) +local ErrorHandler=function(errmsg) +env.info("MOOSE error in MENU COMMAND function: "..errmsg) +if BASE.Debug~=nil then +env.info(BASE.Debug.traceback()) +end +return errmsg +end +self:SetCommandMenuFunction(CommandMenuFunction) +self:SetCommandMenuArguments(CommandMenuArguments) +self.MenuCallHandler=function() +local function MenuFunction() +return self.CommandMenuFunction(unpack(self.CommandMenuArguments)) +end +local Status,Result=xpcall(MenuFunction,ErrorHandler) +end +return self +end +function MENU_COMMAND_BASE:SetCommandMenuFunction(CommandMenuFunction) +self.CommandMenuFunction=CommandMenuFunction +return self +end +function MENU_COMMAND_BASE:SetCommandMenuArguments(CommandMenuArguments) +self.CommandMenuArguments=CommandMenuArguments +return self +end +end +do +MENU_MISSION={ +ClassName="MENU_MISSION" +} +function MENU_MISSION:New(MenuText,ParentMenu) +MENU_INDEX:PrepareMission() +local Path=MENU_INDEX:ParentPath(ParentMenu,MenuText) +local MissionMenu=MENU_INDEX:HasMissionMenu(Path) +if MissionMenu then +return MissionMenu +else +local self=BASE:Inherit(self,MENU_BASE:New(MenuText,ParentMenu)) +MENU_INDEX:SetMissionMenu(Path,self) +self.MenuPath=missionCommands.addSubMenu(self.MenuText,self.MenuParentPath) +self:SetParentMenu(self.MenuText,self) +return self +end +end +function MENU_MISSION:Refresh() +do +missionCommands.removeItem(self.MenuPath) +self.MenuPath=missionCommands.addSubMenu(self.MenuText,self.MenuParentPath) +end +end +function MENU_MISSION:RemoveSubMenus() +for MenuID,Menu in pairs(self.Menus or{})do +Menu:Remove() +end +self.Menus=nil +end +function MENU_MISSION:Remove(MenuStamp,MenuTag) +MENU_INDEX:PrepareMission() +local Path=MENU_INDEX:ParentPath(self.ParentMenu,self.MenuText) +local MissionMenu=MENU_INDEX:HasMissionMenu(Path) +if MissionMenu==self then +self:RemoveSubMenus() +if not MenuStamp or self.MenuStamp~=MenuStamp then +if(not MenuTag)or(MenuTag and self.MenuTag and MenuTag==self.MenuTag)then +self:F({Text=self.MenuText,Path=self.MenuPath}) +if self.MenuPath~=nil then +missionCommands.removeItem(self.MenuPath) +end +MENU_INDEX:ClearMissionMenu(self.Path) +self:ClearParentMenu(self.MenuText) +return nil +end +end +else +BASE:E({"Cannot Remove MENU_MISSION",Path=Path,ParentMenu=self.ParentMenu,MenuText=self.MenuText}) +end +return self +end +end +do +MENU_MISSION_COMMAND={ +ClassName="MENU_MISSION_COMMAND" +} +function MENU_MISSION_COMMAND:New(MenuText,ParentMenu,CommandMenuFunction,...) +MENU_INDEX:PrepareMission() +local Path=MENU_INDEX:ParentPath(ParentMenu,MenuText) +local MissionMenu=MENU_INDEX:HasMissionMenu(Path) +if MissionMenu then +MissionMenu:SetCommandMenuFunction(CommandMenuFunction) +MissionMenu:SetCommandMenuArguments(arg) +return MissionMenu +else +local self=BASE:Inherit(self,MENU_COMMAND_BASE:New(MenuText,ParentMenu,CommandMenuFunction,arg)) +MENU_INDEX:SetMissionMenu(Path,self) +self.MenuPath=missionCommands.addCommand(MenuText,self.MenuParentPath,self.MenuCallHandler) +self:SetParentMenu(self.MenuText,self) +return self +end +end +function MENU_MISSION_COMMAND:Refresh() +do +missionCommands.removeItem(self.MenuPath) +missionCommands.addCommand(self.MenuText,self.MenuParentPath,self.MenuCallHandler) +end +end +function MENU_MISSION_COMMAND:Remove() +MENU_INDEX:PrepareMission() +local Path=MENU_INDEX:ParentPath(self.ParentMenu,self.MenuText) +local MissionMenu=MENU_INDEX:HasMissionMenu(Path) +if MissionMenu==self then +if not MenuStamp or self.MenuStamp~=MenuStamp then +if(not MenuTag)or(MenuTag and self.MenuTag and MenuTag==self.MenuTag)then +self:F({Text=self.MenuText,Path=self.MenuPath}) +if self.MenuPath~=nil then +missionCommands.removeItem(self.MenuPath) +end +MENU_INDEX:ClearMissionMenu(self.Path) +self:ClearParentMenu(self.MenuText) +return nil +end +end +else +BASE:E({"Cannot Remove MENU_MISSION_COMMAND",Path=Path,ParentMenu=self.ParentMenu,MenuText=self.MenuText}) +end +return self +end +end +do +MENU_COALITION={ +ClassName="MENU_COALITION" +} +function MENU_COALITION:New(Coalition,MenuText,ParentMenu) +MENU_INDEX:PrepareCoalition(Coalition) +local Path=MENU_INDEX:ParentPath(ParentMenu,MenuText) +local CoalitionMenu=MENU_INDEX:HasCoalitionMenu(Coalition,Path) +if CoalitionMenu then +return CoalitionMenu +else +local self=BASE:Inherit(self,MENU_BASE:New(MenuText,ParentMenu)) +MENU_INDEX:SetCoalitionMenu(Coalition,Path,self) +self.Coalition=Coalition +self.MenuPath=missionCommands.addSubMenuForCoalition(Coalition,MenuText,self.MenuParentPath) +self:SetParentMenu(self.MenuText,self) +return self +end +end +function MENU_COALITION:Refresh() +do +missionCommands.removeItemForCoalition(self.Coalition,self.MenuPath) +missionCommands.addSubMenuForCoalition(self.Coalition,self.MenuText,self.MenuParentPath) +end +end +function MENU_COALITION:RemoveSubMenus() +for MenuID,Menu in pairs(self.Menus or{})do +Menu:Remove() +end +self.Menus=nil +end +function MENU_COALITION:Remove(MenuStamp,MenuTag) +MENU_INDEX:PrepareCoalition(self.Coalition) +local Path=MENU_INDEX:ParentPath(self.ParentMenu,self.MenuText) +local CoalitionMenu=MENU_INDEX:HasCoalitionMenu(self.Coalition,Path) +if CoalitionMenu==self then +self:RemoveSubMenus() +if not MenuStamp or self.MenuStamp~=MenuStamp then +if(not MenuTag)or(MenuTag and self.MenuTag and MenuTag==self.MenuTag)then +self:F({Coalition=self.Coalition,Text=self.MenuText,Path=self.MenuPath}) +if self.MenuPath~=nil then +missionCommands.removeItemForCoalition(self.Coalition,self.MenuPath) +end +MENU_INDEX:ClearCoalitionMenu(self.Coalition,Path) +self:ClearParentMenu(self.MenuText) +return nil +end +end +else +BASE:E({"Cannot Remove MENU_COALITION",Path=Path,ParentMenu=self.ParentMenu,MenuText=self.MenuText,Coalition=self.Coalition}) +end +return self +end +end +do +MENU_COALITION_COMMAND={ +ClassName="MENU_COALITION_COMMAND" +} +function MENU_COALITION_COMMAND:New(Coalition,MenuText,ParentMenu,CommandMenuFunction,...) +MENU_INDEX:PrepareCoalition(Coalition) +local Path=MENU_INDEX:ParentPath(ParentMenu,MenuText) +local CoalitionMenu=MENU_INDEX:HasCoalitionMenu(Coalition,Path) +if CoalitionMenu then +CoalitionMenu:SetCommandMenuFunction(CommandMenuFunction) +CoalitionMenu:SetCommandMenuArguments(arg) +return CoalitionMenu +else +local self=BASE:Inherit(self,MENU_COMMAND_BASE:New(MenuText,ParentMenu,CommandMenuFunction,arg)) +MENU_INDEX:SetCoalitionMenu(Coalition,Path,self) +self.Coalition=Coalition +self.MenuPath=missionCommands.addCommandForCoalition(self.Coalition,MenuText,self.MenuParentPath,self.MenuCallHandler) +self:SetParentMenu(self.MenuText,self) +return self +end +end +function MENU_COALITION_COMMAND:Refresh() +do +missionCommands.removeItemForCoalition(self.Coalition,self.MenuPath) +missionCommands.addCommandForCoalition(self.Coalition,self.MenuText,self.MenuParentPath,self.MenuCallHandler) +end +end +function MENU_COALITION_COMMAND:Remove(MenuStamp,MenuTag) +MENU_INDEX:PrepareCoalition(self.Coalition) +local Path=MENU_INDEX:ParentPath(self.ParentMenu,self.MenuText) +local CoalitionMenu=MENU_INDEX:HasCoalitionMenu(self.Coalition,Path) +if CoalitionMenu==self then +if not MenuStamp or self.MenuStamp~=MenuStamp then +if(not MenuTag)or(MenuTag and self.MenuTag and MenuTag==self.MenuTag)then +self:F({Coalition=self.Coalition,Text=self.MenuText,Path=self.MenuPath}) +if self.MenuPath~=nil then +missionCommands.removeItemForCoalition(self.Coalition,self.MenuPath) +end +MENU_INDEX:ClearCoalitionMenu(self.Coalition,Path) +self:ClearParentMenu(self.MenuText) +return nil +end +end +else +BASE:E({"Cannot Remove MENU_COALITION_COMMAND",Path=Path,ParentMenu=self.ParentMenu,MenuText=self.MenuText,Coalition=self.Coalition}) +end +return self +end +end +do +local _MENUGROUPS={} +MENU_GROUP={ +ClassName="MENU_GROUP" +} +function MENU_GROUP:New(Group,MenuText,ParentMenu) +MENU_INDEX:PrepareGroup(Group) +local Path=MENU_INDEX:ParentPath(ParentMenu,MenuText) +local GroupMenu=MENU_INDEX:HasGroupMenu(Group,Path) +if GroupMenu then +return GroupMenu +else +self=BASE:Inherit(self,MENU_BASE:New(MenuText,ParentMenu)) +MENU_INDEX:SetGroupMenu(Group,Path,self) +self.Group=Group +self.GroupID=Group:GetID() +self.MenuPath=missionCommands.addSubMenuForGroup(self.GroupID,MenuText,self.MenuParentPath) +self:SetParentMenu(self.MenuText,self) +return self +end +end +function MENU_GROUP:Refresh() +do +missionCommands.removeItemForGroup(self.GroupID,self.MenuPath) +missionCommands.addSubMenuForGroup(self.GroupID,self.MenuText,self.MenuParentPath) +for MenuText,Menu in pairs(self.Menus or{})do +Menu:Refresh() +end +end +end +function MENU_GROUP:RemoveSubMenus(MenuStamp,MenuTag) +for MenuText,Menu in pairs(self.Menus or{})do +Menu:Remove(MenuStamp,MenuTag) +end +self.Menus=nil +end +function MENU_GROUP:Remove(MenuStamp,MenuTag) +MENU_INDEX:PrepareGroup(self.Group) +local Path=MENU_INDEX:ParentPath(self.ParentMenu,self.MenuText) +local GroupMenu=MENU_INDEX:HasGroupMenu(self.Group,Path) +if GroupMenu==self then +self:RemoveSubMenus(MenuStamp,MenuTag) +if not MenuStamp or self.MenuStamp~=MenuStamp then +if(not MenuTag)or(MenuTag and self.MenuTag and MenuTag==self.MenuTag)then +if self.MenuPath~=nil then +self:F({Group=self.GroupID,Text=self.MenuText,Path=self.MenuPath}) +missionCommands.removeItemForGroup(self.GroupID,self.MenuPath) +end +MENU_INDEX:ClearGroupMenu(self.Group,Path) +self:ClearParentMenu(self.MenuText) +return nil +end +end +else +BASE:E({"Cannot Remove MENU_GROUP",Path=Path,ParentMenu=self.ParentMenu,MenuText=self.MenuText,Group=self.Group}) +return nil +end +return self +end +MENU_GROUP_COMMAND={ +ClassName="MENU_GROUP_COMMAND" +} +function MENU_GROUP_COMMAND:New(Group,MenuText,ParentMenu,CommandMenuFunction,...) +MENU_INDEX:PrepareGroup(Group) +local Path=MENU_INDEX:ParentPath(ParentMenu,MenuText) +local GroupMenu=MENU_INDEX:HasGroupMenu(Group,Path) +if GroupMenu then +GroupMenu:SetCommandMenuFunction(CommandMenuFunction) +GroupMenu:SetCommandMenuArguments(arg) +return GroupMenu +else +self=BASE:Inherit(self,MENU_COMMAND_BASE:New(MenuText,ParentMenu,CommandMenuFunction,arg)) +MENU_INDEX:SetGroupMenu(Group,Path,self) +self.Group=Group +self.GroupID=Group:GetID() +self.MenuPath=missionCommands.addCommandForGroup(self.GroupID,MenuText,self.MenuParentPath,self.MenuCallHandler) +self:SetParentMenu(self.MenuText,self) +return self +end +end +function MENU_GROUP_COMMAND:Refresh() +do +missionCommands.removeItemForGroup(self.GroupID,self.MenuPath) +missionCommands.addCommandForGroup(self.GroupID,self.MenuText,self.MenuParentPath,self.MenuCallHandler) +end +end +function MENU_GROUP_COMMAND:Remove(MenuStamp,MenuTag) +MENU_INDEX:PrepareGroup(self.Group) +local Path=MENU_INDEX:ParentPath(self.ParentMenu,self.MenuText) +local GroupMenu=MENU_INDEX:HasGroupMenu(self.Group,Path) +if GroupMenu==self then +if not MenuStamp or self.MenuStamp~=MenuStamp then +if(not MenuTag)or(MenuTag and self.MenuTag and MenuTag==self.MenuTag)then +if self.MenuPath~=nil then +self:F({Group=self.GroupID,Text=self.MenuText,Path=self.MenuPath}) +missionCommands.removeItemForGroup(self.GroupID,self.MenuPath) +end +MENU_INDEX:ClearGroupMenu(self.Group,Path) +self:ClearParentMenu(self.MenuText) +return nil +end +end +else +BASE:E({"Cannot Remove MENU_GROUP_COMMAND",Path=Path,ParentMenu=self.ParentMenu,MenuText=self.MenuText,Group=self.Group}) +end +return self +end +end +do +MENU_GROUP_DELAYED={ +ClassName="MENU_GROUP_DELAYED" +} +function MENU_GROUP_DELAYED:New(Group,MenuText,ParentMenu) +MENU_INDEX:PrepareGroup(Group) +local Path=MENU_INDEX:ParentPath(ParentMenu,MenuText) +local GroupMenu=MENU_INDEX:HasGroupMenu(Group,Path) +if GroupMenu then +return GroupMenu +else +self=BASE:Inherit(self,MENU_BASE:New(MenuText,ParentMenu)) +MENU_INDEX:SetGroupMenu(Group,Path,self) +self.Group=Group +self.GroupID=Group:GetID() +if self.MenuParentPath then +self.MenuPath=UTILS.DeepCopy(self.MenuParentPath) +else +self.MenuPath={} +end +table.insert(self.MenuPath,self.MenuText) +self:SetParentMenu(self.MenuText,self) +return self +end +end +function MENU_GROUP_DELAYED:Set() +do +if not self.MenuSet then +missionCommands.addSubMenuForGroup(self.GroupID,self.MenuText,self.MenuParentPath) +self.MenuSet=true +end +for MenuText,Menu in pairs(self.Menus or{})do +Menu:Set() +end +end +end +function MENU_GROUP_DELAYED:Refresh() +do +missionCommands.removeItemForGroup(self.GroupID,self.MenuPath) +missionCommands.addSubMenuForGroup(self.GroupID,self.MenuText,self.MenuParentPath) +for MenuText,Menu in pairs(self.Menus or{})do +Menu:Refresh() +end +end +end +function MENU_GROUP_DELAYED:RemoveSubMenus(MenuStamp,MenuTag) +for MenuText,Menu in pairs(self.Menus or{})do +Menu:Remove(MenuStamp,MenuTag) +end +self.Menus=nil +end +function MENU_GROUP_DELAYED:Remove(MenuStamp,MenuTag) +MENU_INDEX:PrepareGroup(self.Group) +local Path=MENU_INDEX:ParentPath(self.ParentMenu,self.MenuText) +local GroupMenu=MENU_INDEX:HasGroupMenu(self.Group,Path) +if GroupMenu==self then +self:RemoveSubMenus(MenuStamp,MenuTag) +if not MenuStamp or self.MenuStamp~=MenuStamp then +if(not MenuTag)or(MenuTag and self.MenuTag and MenuTag==self.MenuTag)then +if self.MenuPath~=nil then +self:F({Group=self.GroupID,Text=self.MenuText,Path=self.MenuPath}) +missionCommands.removeItemForGroup(self.GroupID,self.MenuPath) +end +MENU_INDEX:ClearGroupMenu(self.Group,Path) +self:ClearParentMenu(self.MenuText) +return nil +end +end +else +BASE:E({"Cannot Remove MENU_GROUP_DELAYED",Path=Path,ParentMenu=self.ParentMenu,MenuText=self.MenuText,Group=self.Group}) +return nil +end +return self +end +MENU_GROUP_COMMAND_DELAYED={ +ClassName="MENU_GROUP_COMMAND_DELAYED" +} +function MENU_GROUP_COMMAND_DELAYED:New(Group,MenuText,ParentMenu,CommandMenuFunction,...) +MENU_INDEX:PrepareGroup(Group) +local Path=MENU_INDEX:ParentPath(ParentMenu,MenuText) +local GroupMenu=MENU_INDEX:HasGroupMenu(Group,Path) +if GroupMenu then +GroupMenu:SetCommandMenuFunction(CommandMenuFunction) +GroupMenu:SetCommandMenuArguments(arg) +return GroupMenu +else +self=BASE:Inherit(self,MENU_COMMAND_BASE:New(MenuText,ParentMenu,CommandMenuFunction,arg)) +MENU_INDEX:SetGroupMenu(Group,Path,self) +self.Group=Group +self.GroupID=Group:GetID() +if self.MenuParentPath then +self.MenuPath=UTILS.DeepCopy(self.MenuParentPath) +else +self.MenuPath={} +end +table.insert(self.MenuPath,self.MenuText) +self:SetParentMenu(self.MenuText,self) +return self +end +end +function MENU_GROUP_COMMAND_DELAYED:Set() +do +if not self.MenuSet then +self.MenuPath=missionCommands.addCommandForGroup(self.GroupID,self.MenuText,self.MenuParentPath,self.MenuCallHandler) +self.MenuSet=true +end +end +end +function MENU_GROUP_COMMAND_DELAYED:Refresh() +do +missionCommands.removeItemForGroup(self.GroupID,self.MenuPath) +missionCommands.addCommandForGroup(self.GroupID,self.MenuText,self.MenuParentPath,self.MenuCallHandler) +end +end +function MENU_GROUP_COMMAND_DELAYED:Remove(MenuStamp,MenuTag) +MENU_INDEX:PrepareGroup(self.Group) +local Path=MENU_INDEX:ParentPath(self.ParentMenu,self.MenuText) +local GroupMenu=MENU_INDEX:HasGroupMenu(self.Group,Path) +if GroupMenu==self then +if not MenuStamp or self.MenuStamp~=MenuStamp then +if(not MenuTag)or(MenuTag and self.MenuTag and MenuTag==self.MenuTag)then +if self.MenuPath~=nil then +self:F({Group=self.GroupID,Text=self.MenuText,Path=self.MenuPath}) +missionCommands.removeItemForGroup(self.GroupID,self.MenuPath) +end +MENU_INDEX:ClearGroupMenu(self.Group,Path) +self:ClearParentMenu(self.MenuText) +return nil +end +end +else +BASE:E({"Cannot Remove MENU_GROUP_COMMAND_DELAYED",Path=Path,ParentMenu=self.ParentMenu,MenuText=self.MenuText,Group=self.Group}) +end +return self +end +end +ZONE_BASE={ +ClassName="ZONE_BASE", +ZoneName="", +ZoneProbability=1, +} +function ZONE_BASE:New(ZoneName) +local self=BASE:Inherit(self,FSM:New()) +self:F(ZoneName) +self.ZoneName=ZoneName +return self +end +function ZONE_BASE:GetName() +self:F2() +return self.ZoneName +end +function ZONE_BASE:SetName(ZoneName) +self:F2() +self.ZoneName=ZoneName +end +function ZONE_BASE:IsVec2InZone(Vec2) +self:F2(Vec2) +return false +end +function ZONE_BASE:IsVec3InZone(Vec3) +local InZone=self:IsVec2InZone({x=Vec3.x,y=Vec3.z}) +return InZone +end +function ZONE_BASE:IsCoordinateInZone(Coordinate) +local InZone=self:IsVec2InZone(Coordinate:GetVec2()) +return InZone +end +function ZONE_BASE:IsPointVec2InZone(PointVec2) +local InZone=self:IsVec2InZone(PointVec2:GetVec2()) +return InZone +end +function ZONE_BASE:IsPointVec3InZone(PointVec3) +local InZone=self:IsPointVec2InZone(PointVec3) +return InZone +end +function ZONE_BASE:GetVec2() +return nil +end +function ZONE_BASE:GetPointVec2() +self:F2(self.ZoneName) +local Vec2=self:GetVec2() +local PointVec2=POINT_VEC2:NewFromVec2(Vec2) +self:T2({PointVec2}) +return PointVec2 +end +function ZONE_BASE:GetVec3(Height) +self:F2(self.ZoneName) +Height=Height or 0 +local Vec2=self:GetVec2() +local Vec3={x=Vec2.x,y=Height and Height or land.getHeight(self:GetVec2()),z=Vec2.y} +self:T2({Vec3}) +return Vec3 +end +function ZONE_BASE:GetPointVec3(Height) +self:F2(self.ZoneName) +local Vec3=self:GetVec3(Height) +local PointVec3=POINT_VEC3:NewFromVec3(Vec3) +self:T2({PointVec3}) +return PointVec3 +end +function ZONE_BASE:GetCoordinate(Height) +self:F2(self.ZoneName) +local Vec3=self:GetVec3(Height) +if self.Coordinate then +self.Coordinate.x=Vec3.x +self.Coordinate.y=Vec3.y +self.Coordinate.z=Vec3.z +else +self.Coordinate=COORDINATE:NewFromVec3(Vec3) +end +return self.Coordinate +end +function ZONE_BASE:GetRandomVec2() +return nil +end +function ZONE_BASE:GetRandomPointVec2() +return nil +end +function ZONE_BASE:GetRandomPointVec3() +return nil +end +function ZONE_BASE:GetBoundingSquare() +return nil +end +function ZONE_BASE:BoundZone() +self:F2() +end +function ZONE_BASE:SmokeZone(SmokeColor) +self:F2(SmokeColor) +end +function ZONE_BASE:SetZoneProbability(ZoneProbability) +self:F({self:GetName(),ZoneProbability=ZoneProbability}) +self.ZoneProbability=ZoneProbability or 1 +return self +end +function ZONE_BASE:GetZoneProbability() +self:F2() +return self.ZoneProbability +end +function ZONE_BASE:GetZoneMaybe() +self:F2() +local Randomization=math.random() +if Randomization<=self.ZoneProbability then +return self +else +return nil +end +end +ZONE_RADIUS={ +ClassName="ZONE_RADIUS", +} +function ZONE_RADIUS:New(ZoneName,Vec2,Radius) +local self=BASE:Inherit(self,ZONE_BASE:New(ZoneName)) +self:F({ZoneName,Vec2,Radius}) +self.Radius=Radius +self.Vec2=Vec2 +return self +end +function ZONE_RADIUS:UpdateFromVec2(Vec2,Radius) +self.Vec2=Vec2 +if Radius then +self.Radius=Radius +end +return self +end +function ZONE_RADIUS:UpdateFromVec3(Vec3,Radius) +self.Vec2.x=Vec3.x +self.Vec2.y=Vec3.z +if Radius then +self.Radius=Radius +end +return self +end +function ZONE_RADIUS:MarkZone(Points) +local Point={} +local Vec2=self:GetVec2() +Points=Points and Points or 360 +local Angle +local RadialBase=math.pi*2 +for Angle=0,360,(360/Points)do +local Radial=Angle*RadialBase/360 +Point.x=Vec2.x+math.cos(Radial)*self:GetRadius() +Point.y=Vec2.y+math.sin(Radial)*self:GetRadius() +COORDINATE:NewFromVec2(Point):MarkToAll(self:GetName()) +end +end +function ZONE_RADIUS:BoundZone(Points,CountryID,UnBound) +local Point={} +local Vec2=self:GetVec2() +Points=Points and Points or 360 +local Angle +local RadialBase=math.pi*2 +for Angle=0,360,(360/Points)do +local Radial=Angle*RadialBase/360 +Point.x=Vec2.x+math.cos(Radial)*self:GetRadius() +Point.y=Vec2.y+math.sin(Radial)*self:GetRadius() +local CountryName=_DATABASE.COUNTRY_NAME[CountryID] +local Tire={ +["country"]=CountryName, +["category"]="Fortifications", +["canCargo"]=false, +["shape_name"]="H-tyre_B_WF", +["type"]="Black_Tyre_WF", +["y"]=Point.y, +["x"]=Point.x, +["name"]=string.format("%s-Tire #%0d",self:GetName(),Angle), +["heading"]=0, +} +local Group=coalition.addStaticObject(CountryID,Tire) +if UnBound and UnBound==true then +Group:destroy() +end +end +return self +end +function ZONE_RADIUS:SmokeZone(SmokeColor,Points,AddHeight,AngleOffset) +self:F2(SmokeColor) +local Point={} +local Vec2=self:GetVec2() +AddHeight=AddHeight or 0 +AngleOffset=AngleOffset or 0 +Points=Points and Points or 360 +local Angle +local RadialBase=math.pi*2 +for Angle=0,360,360/Points do +local Radial=(Angle+AngleOffset)*RadialBase/360 +Point.x=Vec2.x+math.cos(Radial)*self:GetRadius() +Point.y=Vec2.y+math.sin(Radial)*self:GetRadius() +POINT_VEC2:New(Point.x,Point.y,AddHeight):Smoke(SmokeColor) +end +return self +end +function ZONE_RADIUS:FlareZone(FlareColor,Points,Azimuth,AddHeight) +self:F2({FlareColor,Azimuth}) +local Point={} +local Vec2=self:GetVec2() +AddHeight=AddHeight or 0 +Points=Points and Points or 360 +local Angle +local RadialBase=math.pi*2 +for Angle=0,360,360/Points do +local Radial=Angle*RadialBase/360 +Point.x=Vec2.x+math.cos(Radial)*self:GetRadius() +Point.y=Vec2.y+math.sin(Radial)*self:GetRadius() +POINT_VEC2:New(Point.x,Point.y,AddHeight):Flare(FlareColor,Azimuth) +end +return self +end +function ZONE_RADIUS:GetRadius() +self:F2(self.ZoneName) +self:T2({self.Radius}) +return self.Radius +end +function ZONE_RADIUS:SetRadius(Radius) +self:F2(self.ZoneName) +self.Radius=Radius +self:T2({self.Radius}) +return self.Radius +end +function ZONE_RADIUS:GetVec2() +self:F2(self.ZoneName) +self:T2({self.Vec2}) +return self.Vec2 +end +function ZONE_RADIUS:SetVec2(Vec2) +self:F2(self.ZoneName) +self.Vec2=Vec2 +self:T2({self.Vec2}) +return self.Vec2 +end +function ZONE_RADIUS:GetVec3(Height) +self:F2({self.ZoneName,Height}) +Height=Height or 0 +local Vec2=self:GetVec2() +local Vec3={x=Vec2.x,y=land.getHeight(self:GetVec2())+Height,z=Vec2.y} +self:T2({Vec3}) +return Vec3 +end +function ZONE_RADIUS:Scan(ObjectCategories,UnitCategories) +self.ScanData={} +self.ScanData.Coalitions={} +self.ScanData.Scenery={} +self.ScanData.Units={} +local ZoneCoord=self:GetCoordinate() +local ZoneRadius=self:GetRadius() +self:F({ZoneCoord=ZoneCoord,ZoneRadius=ZoneRadius,ZoneCoordLL=ZoneCoord:ToStringLLDMS()}) +local SphereSearch={ +id=world.VolumeType.SPHERE, +params={ +point=ZoneCoord:GetVec3(), +radius=ZoneRadius, +} +} +local function EvaluateZone(ZoneObject) +if ZoneObject then +local ObjectCategory=ZoneObject:getCategory() +if(ObjectCategory==Object.Category.UNIT and ZoneObject:isExist()and ZoneObject:isActive())or(ObjectCategory==Object.Category.STATIC and ZoneObject:isExist())then +local CoalitionDCSUnit=ZoneObject:getCoalition() +local Include=false +if not UnitCategories then +Include=true +else +local CategoryDCSUnit=ZoneObject:getDesc().category +for UnitCategoryID,UnitCategory in pairs(UnitCategories)do +if UnitCategory==CategoryDCSUnit then +Include=true +break +end +end +end +if Include then +local CoalitionDCSUnit=ZoneObject:getCoalition() +self.ScanData.Coalitions[CoalitionDCSUnit]=true +self.ScanData.Units[ZoneObject]=ZoneObject +self:F2({Name=ZoneObject:getName(),Coalition=CoalitionDCSUnit}) +end +end +if ObjectCategory==Object.Category.SCENERY then +local SceneryType=ZoneObject:getTypeName() +local SceneryName=ZoneObject:getName() +self.ScanData.Scenery[SceneryType]=self.ScanData.Scenery[SceneryType]or{} +self.ScanData.Scenery[SceneryType][SceneryName]=SCENERY:Register(SceneryName,ZoneObject) +self:F2({SCENERY=self.ScanData.Scenery[SceneryType][SceneryName]}) +end +end +return true +end +world.searchObjects(ObjectCategories,SphereSearch,EvaluateZone) +end +function ZONE_RADIUS:GetScannedUnits() +return self.ScanData.Units +end +function ZONE_RADIUS:GetScannedSetUnit() +local SetUnit=SET_UNIT:New() +if self.ScanData then +for ObjectID,UnitObject in pairs(self.ScanData.Units)do +local UnitObject=UnitObject +if UnitObject:isExist()then +local FoundUnit=UNIT:FindByName(UnitObject:getName()) +if FoundUnit then +SetUnit:AddUnit(FoundUnit) +else +local FoundStatic=STATIC:FindByName(UnitObject:getName()) +if FoundStatic then +SetUnit:AddUnit(FoundStatic) +end +end +end +end +end +return SetUnit +end +function ZONE_RADIUS:GetScannedSetGroup() +self.ScanSetGroup=self.ScanSetGroup or SET_GROUP:New() +self.ScanSetGroup.Set={} +if self.ScanData then +for ObjectID,UnitObject in pairs(self.ScanData.Units)do +local UnitObject=UnitObject +if UnitObject:isExist()then +local FoundUnit=UNIT:FindByName(UnitObject:getName()) +if FoundUnit then +local group=FoundUnit:GetGroup() +self.ScanSetGroup:AddGroup(group) +end +end +end +end +return self.ScanSetGroup +end +function ZONE_RADIUS:CountScannedCoalitions() +local Count=0 +for CoalitionID,Coalition in pairs(self.ScanData.Coalitions)do +Count=Count+1 +end +return Count +end +function ZONE_RADIUS:CheckScannedCoalition(Coalition) +if Coalition then +return self.ScanData.Coalitions[Coalition] +end +return nil +end +function ZONE_RADIUS:GetScannedCoalition(Coalition) +if Coalition then +return self.ScanData.Coalitions[Coalition] +else +local Count=0 +local ReturnCoalition=nil +for CoalitionID,Coalition in pairs(self.ScanData.Coalitions)do +Count=Count+1 +ReturnCoalition=CoalitionID +end +if Count~=1 then +ReturnCoalition=nil +end +return ReturnCoalition +end +end +function ZONE_RADIUS:GetScannedSceneryType(SceneryType) +return self.ScanData.Scenery[SceneryType] +end +function ZONE_RADIUS:GetScannedScenery() +return self.ScanData.Scenery +end +function ZONE_RADIUS:IsAllInZoneOfCoalition(Coalition) +return self:CountScannedCoalitions()==1 and self:GetScannedCoalition(Coalition)==true +end +function ZONE_RADIUS:IsAllInZoneOfOtherCoalition(Coalition) +return self:CountScannedCoalitions()==1 and self:GetScannedCoalition(Coalition)==nil +end +function ZONE_RADIUS:IsSomeInZoneOfCoalition(Coalition) +return self:CountScannedCoalitions()>1 and self:GetScannedCoalition(Coalition)==true +end +function ZONE_RADIUS:IsNoneInZoneOfCoalition(Coalition) +return self:GetScannedCoalition(Coalition)==nil +end +function ZONE_RADIUS:IsNoneInZone() +return self:CountScannedCoalitions()==0 +end +function ZONE_RADIUS:SearchZone(EvaluateFunction,ObjectCategories) +local SearchZoneResult=true +local ZoneCoord=self:GetCoordinate() +local ZoneRadius=self:GetRadius() +self:F({ZoneCoord=ZoneCoord,ZoneRadius=ZoneRadius,ZoneCoordLL=ZoneCoord:ToStringLLDMS()}) +local SphereSearch={ +id=world.VolumeType.SPHERE, +params={ +point=ZoneCoord:GetVec3(), +radius=ZoneRadius/2, +} +} +local function EvaluateZone(ZoneDCSUnit) +local ZoneUnit=UNIT:Find(ZoneDCSUnit) +return EvaluateFunction(ZoneUnit) +end +world.searchObjects(Object.Category.UNIT,SphereSearch,EvaluateZone) +end +function ZONE_RADIUS:IsVec2InZone(Vec2) +self:F2(Vec2) +local ZoneVec2=self:GetVec2() +if ZoneVec2 then +if((Vec2.x-ZoneVec2.x)^2+(Vec2.y-ZoneVec2.y)^2)^0.5<=self:GetRadius()then +return true +end +end +return false +end +function ZONE_RADIUS:IsVec3InZone(Vec3) +self:F2(Vec3) +local InZone=self:IsVec2InZone({x=Vec3.x,y=Vec3.z}) +return InZone +end +function ZONE_RADIUS:GetRandomVec2(inner,outer) +self:F(self.ZoneName,inner,outer) +local Point={} +local Vec2=self:GetVec2() +local _inner=inner or 0 +local _outer=outer or self:GetRadius() +local angle=math.random()*math.pi*2; +Point.x=Vec2.x+math.cos(angle)*math.random(_inner,_outer); +Point.y=Vec2.y+math.sin(angle)*math.random(_inner,_outer); +self:T({Point}) +return Point +end +function ZONE_RADIUS:GetRandomPointVec2(inner,outer) +self:F(self.ZoneName,inner,outer) +local PointVec2=POINT_VEC2:NewFromVec2(self:GetRandomVec2(inner,outer)) +self:T3({PointVec2}) +return PointVec2 +end +function ZONE_RADIUS:GetRandomVec3(inner,outer) +self:F(self.ZoneName,inner,outer) +local Vec2=self:GetRandomVec2(inner,outer) +self:T3({x=Vec2.x,y=self.y,z=Vec2.y}) +return{x=Vec2.x,y=self.y,z=Vec2.y} +end +function ZONE_RADIUS:GetRandomPointVec3(inner,outer) +self:F(self.ZoneName,inner,outer) +local PointVec3=POINT_VEC3:NewFromVec2(self:GetRandomVec2(inner,outer)) +self:T3({PointVec3}) +return PointVec3 +end +function ZONE_RADIUS:GetRandomCoordinate(inner,outer) +self:F(self.ZoneName,inner,outer) +local Coordinate=COORDINATE:NewFromVec2(self:GetRandomVec2(inner,outer)) +self:T3({Coordinate=Coordinate}) +return Coordinate +end +ZONE={ +ClassName="ZONE", +} +function ZONE:New(ZoneName) +local Zone=trigger.misc.getZone(ZoneName) +if not Zone then +error("Zone "..ZoneName.." does not exist.") +return nil +end +local self=BASE:Inherit(self,ZONE_RADIUS:New(ZoneName,{x=Zone.point.x,y=Zone.point.z},Zone.radius)) +self:F(ZoneName) +self.Zone=Zone +return self +end +function ZONE:FindByName(ZoneName) +local ZoneFound=_DATABASE:FindZone(ZoneName) +return ZoneFound +end +ZONE_UNIT={ +ClassName="ZONE_UNIT", +} +function ZONE_UNIT:New(ZoneName,ZoneUNIT,Radius,Offset) +if Offset then +if(Offset.dx or Offset.dy)and(Offset.rho or Offset.theta)then +error("Cannot use (dx, dy) with (rho, theta)") +end +self.dy=Offset.dy or 0.0 +self.dx=Offset.dx or 0.0 +self.rho=Offset.rho or 0.0 +self.theta=(Offset.theta or 0.0)*math.pi/180.0 +self.relative_to_unit=Offset.relative_to_unit or false +end +local self=BASE:Inherit(self,ZONE_RADIUS:New(ZoneName,ZoneUNIT:GetVec2(),Radius)) +self:F({ZoneName,ZoneUNIT:GetVec2(),Radius}) +self.ZoneUNIT=ZoneUNIT +self.LastVec2=ZoneUNIT:GetVec2() +_EVENTDISPATCHER:CreateEventNewZone(self) +return self +end +function ZONE_UNIT:GetVec2() +self:F2(self.ZoneName) +local ZoneVec2=self.ZoneUNIT:GetVec2() +if ZoneVec2 then +local heading +if self.relative_to_unit then +heading=(self.ZoneUNIT:GetHeading()or 0.0)*math.pi/180.0 +else +heading=0.0 +end +if(self.dx or self.dy)then +ZoneVec2.x=ZoneVec2.x+self.dx*math.cos(-heading)+self.dy*math.sin(-heading) +ZoneVec2.y=ZoneVec2.y-self.dx*math.sin(-heading)+self.dy*math.cos(-heading) +end +if(self.rho or self.theta)then +ZoneVec2.x=ZoneVec2.x+self.rho*math.cos(self.theta+heading) +ZoneVec2.y=ZoneVec2.y+self.rho*math.sin(self.theta+heading) +end +self.LastVec2=ZoneVec2 +return ZoneVec2 +else +return self.LastVec2 +end +self:T2({ZoneVec2}) +return nil +end +function ZONE_UNIT:GetRandomVec2() +self:F(self.ZoneName) +local RandomVec2={} +local Vec2=self:GetVec2() +if not Vec2 then +Vec2=self.LastVec2 +end +local angle=math.random()*math.pi*2; +RandomVec2.x=Vec2.x+math.cos(angle)*math.random()*self:GetRadius(); +RandomVec2.y=Vec2.y+math.sin(angle)*math.random()*self:GetRadius(); +self:T({RandomVec2}) +return RandomVec2 +end +function ZONE_UNIT:GetVec3(Height) +self:F2(self.ZoneName) +Height=Height or 0 +local Vec2=self:GetVec2() +local Vec3={x=Vec2.x,y=land.getHeight(self:GetVec2())+Height,z=Vec2.y} +self:T2({Vec3}) +return Vec3 +end +ZONE_GROUP={ +ClassName="ZONE_GROUP", +} +function ZONE_GROUP:New(ZoneName,ZoneGROUP,Radius) +local self=BASE:Inherit(self,ZONE_RADIUS:New(ZoneName,ZoneGROUP:GetVec2(),Radius)) +self:F({ZoneName,ZoneGROUP:GetVec2(),Radius}) +self._.ZoneGROUP=ZoneGROUP +self._.ZoneVec2Cache=self._.ZoneGROUP:GetVec2() +_EVENTDISPATCHER:CreateEventNewZone(self) +return self +end +function ZONE_GROUP:GetVec2() +self:F(self.ZoneName) +local ZoneVec2=nil +if self._.ZoneGROUP:IsAlive()then +ZoneVec2=self._.ZoneGROUP:GetVec2() +self._.ZoneVec2Cache=ZoneVec2 +else +ZoneVec2=self._.ZoneVec2Cache +end +self:T({ZoneVec2}) +return ZoneVec2 +end +function ZONE_GROUP:GetRandomVec2() +self:F(self.ZoneName) +local Point={} +local Vec2=self._.ZoneGROUP:GetVec2() +local angle=math.random()*math.pi*2; +Point.x=Vec2.x+math.cos(angle)*math.random()*self:GetRadius(); +Point.y=Vec2.y+math.sin(angle)*math.random()*self:GetRadius(); +self:T({Point}) +return Point +end +function ZONE_GROUP:GetRandomPointVec2(inner,outer) +self:F(self.ZoneName,inner,outer) +local PointVec2=POINT_VEC2:NewFromVec2(self:GetRandomVec2()) +self:T3({PointVec2}) +return PointVec2 +end +ZONE_POLYGON_BASE={ +ClassName="ZONE_POLYGON_BASE", +} +function ZONE_POLYGON_BASE:New(ZoneName,PointsArray) +local self=BASE:Inherit(self,ZONE_BASE:New(ZoneName)) +self:F({ZoneName,PointsArray}) +if PointsArray then +self._.Polygon={} +for i=1,#PointsArray do +self._.Polygon[i]={} +self._.Polygon[i].x=PointsArray[i].x +self._.Polygon[i].y=PointsArray[i].y +end +end +return self +end +function ZONE_POLYGON_BASE:UpdateFromVec2(Vec2Array) +self._.Polygon={} +for i=1,#Vec2Array do +self._.Polygon[i]={} +self._.Polygon[i].x=Vec2Array[i].x +self._.Polygon[i].y=Vec2Array[i].y +end +return self +end +function ZONE_POLYGON_BASE:UpdateFromVec3(Vec3Array) +self._.Polygon={} +for i=1,#Vec3Array do +self._.Polygon[i]={} +self._.Polygon[i].x=Vec3Array[i].x +self._.Polygon[i].y=Vec3Array[i].z +end +return self +end +function ZONE_POLYGON_BASE:GetVec2() +self:F(self.ZoneName) +local Bounds=self:GetBoundingSquare() +return{x=(Bounds.x2+Bounds.x1)/2,y=(Bounds.y2+Bounds.y1)/2} +end +function ZONE_POLYGON_BASE:Flush() +self:F2() +self:F({Polygon=self.ZoneName,Coordinates=self._.Polygon}) +return self +end +function ZONE_POLYGON_BASE:BoundZone(UnBound) +local i +local j +local Segments=10 +i=1 +j=#self._.Polygon +while i<=#self._.Polygon do +self:T({i,j,self._.Polygon[i],self._.Polygon[j]}) +local DeltaX=self._.Polygon[j].x-self._.Polygon[i].x +local DeltaY=self._.Polygon[j].y-self._.Polygon[i].y +for Segment=0,Segments do +local PointX=self._.Polygon[i].x+(Segment*DeltaX/Segments) +local PointY=self._.Polygon[i].y+(Segment*DeltaY/Segments) +local Tire={ +["country"]="USA", +["category"]="Fortifications", +["canCargo"]=false, +["shape_name"]="H-tyre_B_WF", +["type"]="Black_Tyre_WF", +["y"]=PointY, +["x"]=PointX, +["name"]=string.format("%s-Tire #%0d",self:GetName(),((i-1)*Segments)+Segment), +["heading"]=0, +} +local Group=coalition.addStaticObject(country.id.USA,Tire) +if UnBound and UnBound==true then +Group:destroy() +end +end +j=i +i=i+1 +end +return self +end +function ZONE_POLYGON_BASE:SmokeZone(SmokeColor,Segments) +self:F2(SmokeColor) +Segments=Segments or 10 +local i=1 +local j=#self._.Polygon +while i<=#self._.Polygon do +self:T({i,j,self._.Polygon[i],self._.Polygon[j]}) +local DeltaX=self._.Polygon[j].x-self._.Polygon[i].x +local DeltaY=self._.Polygon[j].y-self._.Polygon[i].y +for Segment=0,Segments do +local PointX=self._.Polygon[i].x+(Segment*DeltaX/Segments) +local PointY=self._.Polygon[i].y+(Segment*DeltaY/Segments) +POINT_VEC2:New(PointX,PointY):Smoke(SmokeColor) +end +j=i +i=i+1 +end +return self +end +function ZONE_POLYGON_BASE:FlareZone(FlareColor,Segments,Azimuth,AddHeight) +self:F2(FlareColor) +Segments=Segments or 10 +AddHeight=AddHeight or 0 +local i=1 +local j=#self._.Polygon +while i<=#self._.Polygon do +self:T({i,j,self._.Polygon[i],self._.Polygon[j]}) +local DeltaX=self._.Polygon[j].x-self._.Polygon[i].x +local DeltaY=self._.Polygon[j].y-self._.Polygon[i].y +for Segment=0,Segments do +local PointX=self._.Polygon[i].x+(Segment*DeltaX/Segments) +local PointY=self._.Polygon[i].y+(Segment*DeltaY/Segments) +POINT_VEC2:New(PointX,PointY,AddHeight):Flare(FlareColor,Azimuth) +end +j=i +i=i+1 +end +return self +end +function ZONE_POLYGON_BASE:IsVec2InZone(Vec2) +self:F2(Vec2) +local Next +local Prev +local InPolygon=false +Next=1 +Prev=#self._.Polygon +while Next<=#self._.Polygon do +self:T({Next,Prev,self._.Polygon[Next],self._.Polygon[Prev]}) +if(((self._.Polygon[Next].y>Vec2.y)~=(self._.Polygon[Prev].y>Vec2.y))and +(Vec2.x<(self._.Polygon[Prev].x-self._.Polygon[Next].x)*(Vec2.y-self._.Polygon[Next].y)/(self._.Polygon[Prev].y-self._.Polygon[Next].y)+self._.Polygon[Next].x) +)then +InPolygon=not InPolygon +end +self:T2({InPolygon=InPolygon}) +Prev=Next +Next=Next+1 +end +self:T({InPolygon=InPolygon}) +return InPolygon +end +function ZONE_POLYGON_BASE:GetRandomVec2() +self:F2() +local Vec2Found=false +local Vec2 +local BS=self:GetBoundingSquare() +self:T2(BS) +while Vec2Found==false do +Vec2={x=math.random(BS.x1,BS.x2),y=math.random(BS.y1,BS.y2)} +self:T2(Vec2) +if self:IsVec2InZone(Vec2)then +Vec2Found=true +end +end +self:T2(Vec2) +return Vec2 +end +function ZONE_POLYGON_BASE:GetRandomPointVec2() +self:F2() +local PointVec2=POINT_VEC2:NewFromVec2(self:GetRandomVec2()) +self:T2(PointVec2) +return PointVec2 +end +function ZONE_POLYGON_BASE:GetRandomPointVec3() +self:F2() +local PointVec3=POINT_VEC3:NewFromVec2(self:GetRandomVec2()) +self:T2(PointVec3) +return PointVec3 +end +function ZONE_POLYGON_BASE:GetRandomCoordinate() +self:F2() +local Coordinate=COORDINATE:NewFromVec2(self:GetRandomVec2()) +self:T2(Coordinate) +return Coordinate +end +function ZONE_POLYGON_BASE:GetBoundingSquare() +local x1=self._.Polygon[1].x +local y1=self._.Polygon[1].y +local x2=self._.Polygon[1].x +local y2=self._.Polygon[1].y +for i=2,#self._.Polygon do +self:T2({self._.Polygon[i],x1,y1,x2,y2}) +x1=(x1>self._.Polygon[i].x)and self._.Polygon[i].x or x1 +x2=(x2self._.Polygon[i].y)and self._.Polygon[i].y or y1 +y2=(y20))then +for group_num,Template in pairs(obj_type_data.group)do +if obj_type_name~="static"and Template and Template.units and type(Template.units)=='table'then +self:_RegisterGroupTemplate(Template,CoalitionSide,_DATABASECategory[string.lower(CategoryName)],CountryID) +else +self:_RegisterStaticTemplate(Template,CoalitionSide,_DATABASECategory[string.lower(CategoryName)],CountryID) +end +end +end +end +end +end +end +end +end +end +return self +end +function DATABASE:AccountHits(Event) +self:F({Event}) +if Event.IniPlayerName~=nil then +self:T("Hitting Something") +if Event.TgtCategory then +self.HITS[Event.TgtUnitName]=self.HITS[Event.TgtUnitName]or{} +local Hit=self.HITS[Event.TgtUnitName] +Hit.Players=Hit.Players or{} +Hit.Players[Event.IniPlayerName]=true +end +end +if Event.WeaponPlayerName~=nil then +self:T("Hitting Scenery") +if Event.TgtCategory then +if Event.WeaponCoalition then +self.HITS[Event.TgtUnitName]=self.HITS[Event.TgtUnitName]or{} +local Hit=self.HITS[Event.TgtUnitName] +Hit.Players=Hit.Players or{} +Hit.Players[Event.WeaponPlayerName]=true +else +end +end +end +end +function DATABASE:AccountDestroys(Event) +self:F({Event}) +local TargetUnit=nil +local TargetGroup=nil +local TargetUnitName="" +local TargetGroupName="" +local TargetPlayerName="" +local TargetCoalition=nil +local TargetCategory=nil +local TargetType=nil +local TargetUnitCoalition=nil +local TargetUnitCategory=nil +local TargetUnitType=nil +if Event.IniDCSUnit then +TargetUnit=Event.IniUnit +TargetUnitName=Event.IniDCSUnitName +TargetGroup=Event.IniDCSGroup +TargetGroupName=Event.IniDCSGroupName +TargetPlayerName=Event.IniPlayerName +TargetCoalition=Event.IniCoalition +TargetCategory=Event.IniCategory +TargetType=Event.IniTypeName +TargetUnitType=TargetType +self:T({TargetUnitName,TargetGroupName,TargetPlayerName,TargetCoalition,TargetCategory,TargetType}) +end +local Destroyed=false +if self.HITS[Event.IniUnitName]then +self.DESTROYS[Event.IniUnitName]=self.DESTROYS[Event.IniUnitName]or{} +self.DESTROYS[Event.IniUnitName]=true +end +end +do +SET_BASE={ +ClassName="SET_BASE", +Filter={}, +Set={}, +List={}, +Index={}, +Database=nil, +CallScheduler=nil, +TimeInterval=nil, +YieldInterval=nil, +} +function SET_BASE:New(Database) +local self=BASE:Inherit(self,FSM:New()) +self.Database=Database +self:SetStartState("Started") +self:AddTransition("*","Added","*") +self:AddTransition("*","Removed","*") +self.YieldInterval=10 +self.TimeInterval=0.001 +self.Set={} +self.Index={} +self.CallScheduler=SCHEDULER:New(self) +self:SetEventPriority(2) +return self +end +function SET_BASE:Clear() +for Name,Object in pairs(self.Set)do +self:Remove(Name) +end +return self +end +function SET_BASE:_Find(ObjectName) +local ObjectFound=self.Set[ObjectName] +return ObjectFound +end +function SET_BASE:GetSet() +self:F2() +return self.Set or{} +end +function SET_BASE:GetSetNames() +self:F2() +local Names={} +for Name,Object in pairs(self.Set)do +table.insert(Names,Name) +end +return Names +end +function SET_BASE:GetSetObjects() +self:F2() +local Objects={} +for Name,Object in pairs(self.Set)do +table.insert(Objects,Object) +end +return Objects +end +function SET_BASE:Remove(ObjectName,NoTriggerEvent) +self:F2({ObjectName=ObjectName}) +local Object=self.Set[ObjectName] +if Object then +for Index,Key in ipairs(self.Index)do +if Key==ObjectName then +table.remove(self.Index,Index) +self.Set[ObjectName]=nil +break +end +end +if not NoTriggerEvent then +self:Removed(ObjectName,Object) +end +end +end +function SET_BASE:Add(ObjectName,Object) +self:F2({ObjectName=ObjectName,Object=Object}) +if self.Set[ObjectName]then +self:Remove(ObjectName,true) +end +self.Set[ObjectName]=Object +table.insert(self.Index,ObjectName) +self:Added(ObjectName,Object) +end +function SET_BASE:AddObject(Object) +self:F2(Object.ObjectName) +self:T(Object.UnitName) +self:T(Object.ObjectName) +self:Add(Object.ObjectName,Object) +end +function SET_BASE:GetSetUnion(SetB) +local union=SET_BASE:New() +for _,ObjectA in pairs(self.Set)do +union:AddObject(ObjectA) +end +for _,ObjectB in pairs(SetB.Set)do +union:AddObject(ObjectB) +end +return union +end +function SET_BASE:GetSetIntersection(SetB) +local intersection=SET_BASE:New() +local union=self:GetSetUnion(SetB) +for _,Object in pairs(union.Set)do +if self:IsIncludeObject(Object)and SetB:IsIncludeObject(Object)then +intersection:AddObject(intersection) +end +end +return intersection +end +function SET_BASE:GetSetComplement(SetB) +local complement=SET_BASE:New() +local union=self:GetSetUnion(SetA,SetB) +for _,Object in pairs(union.Set)do +if SetA:IsIncludeObject(Object)and SetB:IsIncludeObject(Object)then +intersection:Add(intersection) +end +end +return intersection +end +function SET_BASE:CompareSets(SetA,SetB) +for _,ObjectB in pairs(SetB.Set)do +if SetA:IsIncludeObject(ObjectB)then +SetA:Add(ObjectB) +end +end +return SetA +end +function SET_BASE:Get(ObjectName) +self:F(ObjectName) +local Object=self.Set[ObjectName] +self:T3({ObjectName,Object}) +return Object +end +function SET_BASE:GetFirst() +local ObjectName=self.Index[1] +local FirstObject=self.Set[ObjectName] +self:T3({FirstObject}) +return FirstObject +end +function SET_BASE:GetLast() +local ObjectName=self.Index[#self.Index] +local LastObject=self.Set[ObjectName] +self:T3({LastObject}) +return LastObject +end +function SET_BASE:GetRandom() +local RandomItem=self.Set[self.Index[math.random(#self.Index)]] +self:T3({RandomItem}) +return RandomItem +end +function SET_BASE:Count() +return self.Index and#self.Index or 0 +end +function SET_BASE:SetDatabase(BaseSet) +local OtherFilter=routines.utils.deepCopy(BaseSet.Filter) +self.Filter=OtherFilter +self.Database=BaseSet:GetSet() +return self +end +function SET_BASE:SetIteratorIntervals(YieldInterval,TimeInterval) +self.YieldInterval=YieldInterval +self.TimeInterval=TimeInterval +return self +end +function SET_BASE:SetSomeIteratorLimit(Limit) +self.SomeIteratorLimit=Limit or 1 +return self +end +function SET_BASE:GetSomeIteratorLimit() +return self.SomeIteratorLimit or self:Count() +end +function SET_BASE:FilterOnce() +for ObjectName,Object in pairs(self.Database)do +if self:IsIncludeObject(Object)then +self:Add(ObjectName,Object) +end +end +return self +end +function SET_BASE:_FilterStart() +for ObjectName,Object in pairs(self.Database)do +if self:IsIncludeObject(Object)then +self:Add(ObjectName,Object) +end +end +return self +end +function SET_BASE:FilterDeads() +self:HandleEvent(EVENTS.Dead,self._EventOnDeadOrCrash) +return self +end +function SET_BASE:FilterCrashes() +self:HandleEvent(EVENTS.Crash,self._EventOnDeadOrCrash) +return self +end +function SET_BASE:FilterStop() +self:UnHandleEvent(EVENTS.Birth) +self:UnHandleEvent(EVENTS.Dead) +self:UnHandleEvent(EVENTS.Crash) +return self +end +function SET_BASE:FindNearestObjectFromPointVec2(PointVec2) +self:F2(PointVec2) +local NearestObject=nil +local ClosestDistance=nil +for ObjectID,ObjectData in pairs(self.Set)do +if NearestObject==nil then +NearestObject=ObjectData +ClosestDistance=PointVec2:DistanceFromPointVec2(ObjectData:GetCoordinate()) +else +local Distance=PointVec2:DistanceFromPointVec2(ObjectData:GetCoordinate()) +if Distance=Limit then +break +end +end +return true +end +local co=CoRoutine +local function Schedule() +local status,res=co() +self:T3({status,res}) +if status==false then +error(res) +end +if res==false then +return true +end +return false +end +Schedule() +return self +end +function SET_BASE:IsIncludeObject(Object) +self:F3(Object) +return true +end +function SET_BASE:IsInSet(ObjectName) +self:F3(Object) +return true +end +function SET_BASE:GetObjectNames() +self:F3() +local ObjectNames="" +for ObjectName,Object in pairs(self.Set)do +ObjectNames=ObjectNames..ObjectName..", " +end +return ObjectNames +end +function SET_BASE:Flush(MasterObject) +self:F3() +local ObjectNames="" +for ObjectName,Object in pairs(self.Set)do +ObjectNames=ObjectNames..ObjectName..", " +end +self:F({MasterObject=MasterObject and MasterObject:GetClassNameAndID(),"Objects in Set:",ObjectNames}) +return ObjectNames +end +end +do +SET_GROUP={ +ClassName="SET_GROUP", +Filter={ +Coalitions=nil, +Categories=nil, +Countries=nil, +GroupPrefixes=nil, +}, +FilterMeta={ +Coalitions={ +red=coalition.side.RED, +blue=coalition.side.BLUE, +neutral=coalition.side.NEUTRAL, +}, +Categories={ +plane=Group.Category.AIRPLANE, +helicopter=Group.Category.HELICOPTER, +ground=Group.Category.GROUND, +ship=Group.Category.SHIP, +structure=Group.Category.STRUCTURE, +}, +}, +} +function SET_GROUP:New() +local self=BASE:Inherit(self,SET_BASE:New(_DATABASE.GROUPS)) +self:FilterActive(false) +return self +end +function SET_GROUP:GetAliveSet() +self:F2() +local AliveSet=SET_GROUP:New() +for GroupName,GroupObject in pairs(self.Set)do +local GroupObject=GroupObject +if GroupObject then +if GroupObject:IsAlive()then +AliveSet:Add(GroupName,GroupObject) +end +end +end +return AliveSet.Set or{} +end +function SET_GROUP:GetUnitTypeNames() +self:F2() +local MT={} +local UnitTypes={} +local ReportUnitTypes=REPORT:New() +for GroupID,GroupData in pairs(self:GetSet())do +local Units=GroupData:GetUnits() +for UnitID,UnitData in pairs(Units)do +if UnitData:IsAlive()then +local UnitType=UnitData:GetTypeName() +if not UnitTypes[UnitType]then +UnitTypes[UnitType]=1 +else +UnitTypes[UnitType]=UnitTypes[UnitType]+1 +end +end +end +end +for UnitTypeID,UnitType in pairs(UnitTypes)do +ReportUnitTypes:Add(UnitType.." of "..UnitTypeID) +end +return ReportUnitTypes +end +function SET_GROUP:AddGroup(group) +self:Add(group:GetName(),group) +for UnitID,UnitData in pairs(group:GetUnits())do +UnitData:SetCargoBayWeightLimit() +end +return self +end +function SET_GROUP:AddGroupsByName(AddGroupNames) +local AddGroupNamesArray=(type(AddGroupNames)=="table")and AddGroupNames or{AddGroupNames} +for AddGroupID,AddGroupName in pairs(AddGroupNamesArray)do +self:Add(AddGroupName,GROUP:FindByName(AddGroupName)) +end +return self +end +function SET_GROUP:RemoveGroupsByName(RemoveGroupNames) +local RemoveGroupNamesArray=(type(RemoveGroupNames)=="table")and RemoveGroupNames or{RemoveGroupNames} +for RemoveGroupID,RemoveGroupName in pairs(RemoveGroupNamesArray)do +self:Remove(RemoveGroupName) +end +return self +end +function SET_GROUP:FindGroup(GroupName) +local GroupFound=self.Set[GroupName] +return GroupFound +end +function SET_GROUP:FindNearestGroupFromPointVec2(PointVec2) +self:F2(PointVec2) +local NearestGroup=nil +local ClosestDistance=nil +for ObjectID,ObjectData in pairs(self.Set)do +if NearestGroup==nil then +NearestGroup=ObjectData +ClosestDistance=PointVec2:DistanceFromPointVec2(ObjectData:GetCoordinate()) +else +local Distance=PointVec2:DistanceFromPointVec2(ObjectData:GetCoordinate()) +if DistanceMaxThreatLevelA2G then +MaxThreatLevelA2G=ThreatLevelA2G +MaxThreatText=ThreatText +end +end +self:F({MaxThreatLevelA2G=MaxThreatLevelA2G,MaxThreatText=MaxThreatText}) +return MaxThreatLevelA2G,MaxThreatText +end +function SET_UNIT:GetCoordinate() +local Coordinate=self:GetFirst():GetCoordinate() +local x1=Coordinate.x +local x2=Coordinate.x +local y1=Coordinate.y +local y2=Coordinate.y +local z1=Coordinate.z +local z2=Coordinate.z +local MaxVelocity=0 +local AvgHeading=nil +local MovingCount=0 +for UnitName,UnitData in pairs(self:GetSet())do +local Unit=UnitData +local Coordinate=Unit:GetCoordinate() +x1=(Coordinate.xx2)and Coordinate.x or x2 +y1=(Coordinate.yy2)and Coordinate.y or y2 +z1=(Coordinate.yz2)and Coordinate.z or z2 +local Velocity=Coordinate:GetVelocity() +if Velocity~=0 then +MaxVelocity=(MaxVelocity5 then +HeadingSet=nil +break +end +end +end +end +return HeadingSet +end +function SET_UNIT:HasRadar(RadarType) +self:F2(RadarType) +local RadarCount=0 +for UnitID,UnitData in pairs(self:GetSet())do +local UnitSensorTest=UnitData +local HasSensors +if RadarType then +HasSensors=UnitSensorTest:HasSensors(Unit.SensorType.RADAR,RadarType) +else +HasSensors=UnitSensorTest:HasSensors(Unit.SensorType.RADAR) +end +self:T3(HasSensors) +if HasSensors then +RadarCount=RadarCount+1 +end +end +return RadarCount +end +function SET_UNIT:HasSEAD() +self:F2() +local SEADCount=0 +for UnitID,UnitData in pairs(self:GetSet())do +local UnitSEAD=UnitData +if UnitSEAD:IsAlive()then +local UnitSEADAttributes=UnitSEAD:GetDesc().attributes +local HasSEAD=UnitSEAD:HasSEAD() +self:T3(HasSEAD) +if HasSEAD then +SEADCount=SEADCount+1 +end +end +end +return SEADCount +end +function SET_UNIT:HasGroundUnits() +self:F2() +local GroundUnitCount=0 +for UnitID,UnitData in pairs(self:GetSet())do +local UnitTest=UnitData +if UnitTest:IsGround()then +GroundUnitCount=GroundUnitCount+1 +end +end +return GroundUnitCount +end +function SET_UNIT:HasAirUnits() +self:F2() +local AirUnitCount=0 +for UnitID,UnitData in pairs(self:GetSet())do +local UnitTest=UnitData +if UnitTest:IsAir()then +AirUnitCount=AirUnitCount+1 +end +end +return AirUnitCount +end +function SET_UNIT:HasFriendlyUnits(FriendlyCoalition) +self:F2() +local FriendlyUnitCount=0 +for UnitID,UnitData in pairs(self:GetSet())do +local UnitTest=UnitData +if UnitTest:IsFriendly(FriendlyCoalition)then +FriendlyUnitCount=FriendlyUnitCount+1 +end +end +return FriendlyUnitCount +end +function SET_UNIT:IsIncludeObject(MUnit) +self:F2(MUnit) +local MUnitInclude=false +if MUnit:IsAlive()~=nil then +MUnitInclude=true +if self.Filter.Active~=nil then +local MUnitActive=false +if self.Filter.Active==false or(self.Filter.Active==true and MUnit:IsActive()==true)then +MUnitActive=true +end +MUnitInclude=MUnitInclude and MUnitActive +end +if self.Filter.Coalitions then +local MUnitCoalition=false +for CoalitionID,CoalitionName in pairs(self.Filter.Coalitions)do +self:F({"Coalition:",MUnit:GetCoalition(),self.FilterMeta.Coalitions[CoalitionName],CoalitionName}) +if self.FilterMeta.Coalitions[CoalitionName]and self.FilterMeta.Coalitions[CoalitionName]==MUnit:GetCoalition()then +MUnitCoalition=true +end +end +MUnitInclude=MUnitInclude and MUnitCoalition +end +if self.Filter.Categories then +local MUnitCategory=false +for CategoryID,CategoryName in pairs(self.Filter.Categories)do +self:T3({"Category:",MUnit:GetDesc().category,self.FilterMeta.Categories[CategoryName],CategoryName}) +if self.FilterMeta.Categories[CategoryName]and self.FilterMeta.Categories[CategoryName]==MUnit:GetDesc().category then +MUnitCategory=true +end +end +MUnitInclude=MUnitInclude and MUnitCategory +end +if self.Filter.Types then +local MUnitType=false +for TypeID,TypeName in pairs(self.Filter.Types)do +self:T3({"Type:",MUnit:GetTypeName(),TypeName}) +if TypeName==MUnit:GetTypeName()then +MUnitType=true +end +end +MUnitInclude=MUnitInclude and MUnitType +end +if self.Filter.Countries then +local MUnitCountry=false +for CountryID,CountryName in pairs(self.Filter.Countries)do +self:T3({"Country:",MUnit:GetCountry(),CountryName}) +if country.id[CountryName]==MUnit:GetCountry()then +MUnitCountry=true +end +end +MUnitInclude=MUnitInclude and MUnitCountry +end +if self.Filter.UnitPrefixes then +local MUnitPrefix=false +for UnitPrefixId,UnitPrefix in pairs(self.Filter.UnitPrefixes)do +self:T3({"Prefix:",string.find(MUnit:GetName(),UnitPrefix,1),UnitPrefix}) +if string.find(MUnit:GetName(),UnitPrefix,1)then +MUnitPrefix=true +end +end +MUnitInclude=MUnitInclude and MUnitPrefix +end +if self.Filter.RadarTypes then +local MUnitRadar=false +for RadarTypeID,RadarType in pairs(self.Filter.RadarTypes)do +self:T3({"Radar:",RadarType}) +if MUnit:HasSensors(Unit.SensorType.RADAR,RadarType)==true then +if MUnit:GetRadar()==true then +self:T3("RADAR Found") +end +MUnitRadar=true +end +end +MUnitInclude=MUnitInclude and MUnitRadar +end +if self.Filter.SEAD then +local MUnitSEAD=false +if MUnit:HasSEAD()==true then +self:T3("SEAD Found") +MUnitSEAD=true +end +MUnitInclude=MUnitInclude and MUnitSEAD +end +end +self:T2(MUnitInclude) +return MUnitInclude +end +function SET_UNIT:GetTypeNames(Delimiter) +Delimiter=Delimiter or", " +local TypeReport=REPORT:New() +local Types={} +for UnitName,UnitData in pairs(self:GetSet())do +local Unit=UnitData +local UnitTypeName=Unit:GetTypeName() +if not Types[UnitTypeName]then +Types[UnitTypeName]=UnitTypeName +TypeReport:Add(UnitTypeName) +end +end +return TypeReport:Text(Delimiter) +end +function SET_UNIT:SetCargoBayWeightLimit() +local Set=self:GetSet() +for UnitID,UnitData in pairs(Set)do +UnitData:SetCargoBayWeightLimit() +end +end +end +do +SET_STATIC={ +ClassName="SET_STATIC", +Statics={}, +Filter={ +Coalitions=nil, +Categories=nil, +Types=nil, +Countries=nil, +StaticPrefixes=nil, +}, +FilterMeta={ +Coalitions={ +red=coalition.side.RED, +blue=coalition.side.BLUE, +neutral=coalition.side.NEUTRAL, +}, +Categories={ +plane=Unit.Category.AIRPLANE, +helicopter=Unit.Category.HELICOPTER, +ground=Unit.Category.GROUND_STATIC, +ship=Unit.Category.SHIP, +structure=Unit.Category.STRUCTURE, +}, +}, +} +function SET_STATIC:New() +local self=BASE:Inherit(self,SET_BASE:New(_DATABASE.STATICS)) +return self +end +function SET_STATIC:AddStatic(AddStatic) +self:F2(AddStatic:GetName()) +self:Add(AddStatic:GetName(),AddStatic) +return self +end +function SET_STATIC:AddStaticsByName(AddStaticNames) +local AddStaticNamesArray=(type(AddStaticNames)=="table")and AddStaticNames or{AddStaticNames} +self:T(AddStaticNamesArray) +for AddStaticID,AddStaticName in pairs(AddStaticNamesArray)do +self:Add(AddStaticName,STATIC:FindByName(AddStaticName)) +end +return self +end +function SET_STATIC:RemoveStaticsByName(RemoveStaticNames) +local RemoveStaticNamesArray=(type(RemoveStaticNames)=="table")and RemoveStaticNames or{RemoveStaticNames} +for RemoveStaticID,RemoveStaticName in pairs(RemoveStaticNamesArray)do +self:Remove(RemoveStaticName) +end +return self +end +function SET_STATIC:FindStatic(StaticName) +local StaticFound=self.Set[StaticName] +return StaticFound +end +function SET_STATIC:FilterCoalitions(Coalitions) +if not self.Filter.Coalitions then +self.Filter.Coalitions={} +end +if type(Coalitions)~="table"then +Coalitions={Coalitions} +end +for CoalitionID,Coalition in pairs(Coalitions)do +self.Filter.Coalitions[Coalition]=Coalition +end +return self +end +function SET_STATIC:FilterCategories(Categories) +if not self.Filter.Categories then +self.Filter.Categories={} +end +if type(Categories)~="table"then +Categories={Categories} +end +for CategoryID,Category in pairs(Categories)do +self.Filter.Categories[Category]=Category +end +return self +end +function SET_STATIC:FilterTypes(Types) +if not self.Filter.Types then +self.Filter.Types={} +end +if type(Types)~="table"then +Types={Types} +end +for TypeID,Type in pairs(Types)do +self.Filter.Types[Type]=Type +end +return self +end +function SET_STATIC:FilterCountries(Countries) +if not self.Filter.Countries then +self.Filter.Countries={} +end +if type(Countries)~="table"then +Countries={Countries} +end +for CountryID,Country in pairs(Countries)do +self.Filter.Countries[Country]=Country +end +return self +end +function SET_STATIC:FilterPrefixes(Prefixes) +if not self.Filter.StaticPrefixes then +self.Filter.StaticPrefixes={} +end +if type(Prefixes)~="table"then +Prefixes={Prefixes} +end +for PrefixID,Prefix in pairs(Prefixes)do +self.Filter.StaticPrefixes[Prefix]=Prefix +end +return self +end +function SET_STATIC:FilterStart() +if _DATABASE then +self:_FilterStart() +self:HandleEvent(EVENTS.Birth,self._EventOnBirth) +self:HandleEvent(EVENTS.Dead,self._EventOnDeadOrCrash) +self:HandleEvent(EVENTS.Crash,self._EventOnDeadOrCrash) +end +return self +end +function SET_STATIC:CountAlive() +local Set=self:GetSet() +local CountU=0 +for UnitID,UnitData in pairs(Set)do +if UnitData and UnitData:IsAlive()then +CountU=CountU+1 +end +end +return CountU +end +function SET_STATIC:AddInDatabase(Event) +self:F3({Event}) +if Event.IniObjectCategory==Object.Category.STATIC then +if not self.Database[Event.IniDCSUnitName]then +self.Database[Event.IniDCSUnitName]=STATIC:Register(Event.IniDCSUnitName) +self:T3(self.Database[Event.IniDCSUnitName]) +end +end +return Event.IniDCSUnitName,self.Database[Event.IniDCSUnitName] +end +function SET_STATIC:FindInDatabase(Event) +self:F2({Event.IniDCSUnitName,self.Set[Event.IniDCSUnitName],Event}) +return Event.IniDCSUnitName,self.Set[Event.IniDCSUnitName] +end +do +function SET_STATIC:IsPartiallyInZone(Zone) +local IsPartiallyInZone=false +local function EvaluateZone(ZoneStatic) +local ZoneStaticName=ZoneStatic:GetName() +if self:FindStatic(ZoneStaticName)then +IsPartiallyInZone=true +return false +end +return true +end +return IsPartiallyInZone +end +function SET_STATIC:IsNotInZone(Zone) +local IsNotInZone=true +local function EvaluateZone(ZoneStatic) +local ZoneStaticName=ZoneStatic:GetName() +if self:FindStatic(ZoneStaticName)then +IsNotInZone=false +return false +end +return true +end +Zone:Search(EvaluateZone) +return IsNotInZone +end +function SET_STATIC:ForEachStaticInZone(IteratorFunction,...) +self:F2(arg) +self:ForEach(IteratorFunction,arg,self:GetSet()) +return self +end +end +function SET_STATIC:ForEachStatic(IteratorFunction,...) +self:F2(arg) +self:ForEach(IteratorFunction,arg,self:GetSet()) +return self +end +function SET_STATIC:ForEachStaticCompletelyInZone(ZoneObject,IteratorFunction,...) +self:F2(arg) +self:ForEach(IteratorFunction,arg,self:GetSet(), +function(ZoneObject,StaticObject) +if StaticObject:IsInZone(ZoneObject)then +return true +else +return false +end +end,{ZoneObject}) +return self +end +function SET_STATIC:ForEachStaticNotInZone(ZoneObject,IteratorFunction,...) +self:F2(arg) +self:ForEach(IteratorFunction,arg,self:GetSet(), +function(ZoneObject,StaticObject) +if StaticObject:IsNotInZone(ZoneObject)then +return true +else +return false +end +end,{ZoneObject}) +return self +end +function SET_STATIC:GetStaticTypes() +self:F2() +local MT={} +local StaticTypes={} +for StaticID,StaticData in pairs(self:GetSet())do +local TextStatic=StaticData +if TextStatic:IsAlive()then +local StaticType=TextStatic:GetTypeName() +if not StaticTypes[StaticType]then +StaticTypes[StaticType]=1 +else +StaticTypes[StaticType]=StaticTypes[StaticType]+1 +end +end +end +for StaticTypeID,StaticType in pairs(StaticTypes)do +MT[#MT+1]=StaticType.." of "..StaticTypeID +end +return StaticTypes +end +function SET_STATIC:GetStaticTypesText() +self:F2() +local MT={} +local StaticTypes=self:GetStaticTypes() +for StaticTypeID,StaticType in pairs(StaticTypes)do +MT[#MT+1]=StaticType.." of "..StaticTypeID +end +return table.concat(MT,", ") +end +function SET_STATIC:GetCoordinate() +local Coordinate=self:GetFirst():GetCoordinate() +local x1=Coordinate.x +local x2=Coordinate.x +local y1=Coordinate.y +local y2=Coordinate.y +local z1=Coordinate.z +local z2=Coordinate.z +local MaxVelocity=0 +local AvgHeading=nil +local MovingCount=0 +for StaticName,StaticData in pairs(self:GetSet())do +local Static=StaticData +local Coordinate=Static:GetCoordinate() +x1=(Coordinate.xx2)and Coordinate.x or x2 +y1=(Coordinate.yy2)and Coordinate.y or y2 +z1=(Coordinate.yz2)and Coordinate.z or z2 +local Velocity=Coordinate:GetVelocity() +if Velocity~=0 then +MaxVelocity=(MaxVelocity5 then +HeadingSet=nil +break +end +end +end +end +return HeadingSet +end +function SET_STATIC:CalculateThreatLevelA2G() +local MaxThreatLevelA2G=0 +local MaxThreatText="" +for StaticName,StaticData in pairs(self:GetSet())do +local ThreatStatic=StaticData +local ThreatLevelA2G,ThreatText=ThreatStatic:GetThreatLevel() +if ThreatLevelA2G>MaxThreatLevelA2G then +MaxThreatLevelA2G=ThreatLevelA2G +MaxThreatText=ThreatText +end +end +self:F({MaxThreatLevelA2G=MaxThreatLevelA2G,MaxThreatText=MaxThreatText}) +return MaxThreatLevelA2G,MaxThreatText +end +function SET_STATIC:IsIncludeObject(MStatic) +self:F2(MStatic) +local MStaticInclude=true +if self.Filter.Coalitions then +local MStaticCoalition=false +for CoalitionID,CoalitionName in pairs(self.Filter.Coalitions)do +self:T3({"Coalition:",MStatic:GetCoalition(),self.FilterMeta.Coalitions[CoalitionName],CoalitionName}) +if self.FilterMeta.Coalitions[CoalitionName]and self.FilterMeta.Coalitions[CoalitionName]==MStatic:GetCoalition()then +MStaticCoalition=true +end +end +MStaticInclude=MStaticInclude and MStaticCoalition +end +if self.Filter.Categories then +local MStaticCategory=false +for CategoryID,CategoryName in pairs(self.Filter.Categories)do +self:T3({"Category:",MStatic:GetDesc().category,self.FilterMeta.Categories[CategoryName],CategoryName}) +if self.FilterMeta.Categories[CategoryName]and self.FilterMeta.Categories[CategoryName]==MStatic:GetDesc().category then +MStaticCategory=true +end +end +MStaticInclude=MStaticInclude and MStaticCategory +end +if self.Filter.Types then +local MStaticType=false +for TypeID,TypeName in pairs(self.Filter.Types)do +self:T3({"Type:",MStatic:GetTypeName(),TypeName}) +if TypeName==MStatic:GetTypeName()then +MStaticType=true +end +end +MStaticInclude=MStaticInclude and MStaticType +end +if self.Filter.Countries then +local MStaticCountry=false +for CountryID,CountryName in pairs(self.Filter.Countries)do +self:T3({"Country:",MStatic:GetCountry(),CountryName}) +if country.id[CountryName]==MStatic:GetCountry()then +MStaticCountry=true +end +end +MStaticInclude=MStaticInclude and MStaticCountry +end +if self.Filter.StaticPrefixes then +local MStaticPrefix=false +for StaticPrefixId,StaticPrefix in pairs(self.Filter.StaticPrefixes)do +self:T3({"Prefix:",string.find(MStatic:GetName(),StaticPrefix,1),StaticPrefix}) +if string.find(MStatic:GetName(),StaticPrefix,1)then +MStaticPrefix=true +end +end +MStaticInclude=MStaticInclude and MStaticPrefix +end +self:T2(MStaticInclude) +return MStaticInclude +end +function SET_STATIC:GetTypeNames(Delimiter) +Delimiter=Delimiter or", " +local TypeReport=REPORT:New() +local Types={} +for StaticName,StaticData in pairs(self:GetSet())do +local Static=StaticData +local StaticTypeName=Static:GetTypeName() +if not Types[StaticTypeName]then +Types[StaticTypeName]=StaticTypeName +TypeReport:Add(StaticTypeName) +end +end +return TypeReport:Text(Delimiter) +end +end +do +SET_CLIENT={ +ClassName="SET_CLIENT", +Clients={}, +Filter={ +Coalitions=nil, +Categories=nil, +Types=nil, +Countries=nil, +ClientPrefixes=nil, +}, +FilterMeta={ +Coalitions={ +red=coalition.side.RED, +blue=coalition.side.BLUE, +neutral=coalition.side.NEUTRAL, +}, +Categories={ +plane=Unit.Category.AIRPLANE, +helicopter=Unit.Category.HELICOPTER, +ground=Unit.Category.GROUND_UNIT, +ship=Unit.Category.SHIP, +structure=Unit.Category.STRUCTURE, +}, +}, +} +function SET_CLIENT:New() +local self=BASE:Inherit(self,SET_BASE:New(_DATABASE.CLIENTS)) +self:FilterActive(false) +return self +end +function SET_CLIENT:AddClientsByName(AddClientNames) +local AddClientNamesArray=(type(AddClientNames)=="table")and AddClientNames or{AddClientNames} +for AddClientID,AddClientName in pairs(AddClientNamesArray)do +self:Add(AddClientName,CLIENT:FindByName(AddClientName)) +end +return self +end +function SET_CLIENT:RemoveClientsByName(RemoveClientNames) +local RemoveClientNamesArray=(type(RemoveClientNames)=="table")and RemoveClientNames or{RemoveClientNames} +for RemoveClientID,RemoveClientName in pairs(RemoveClientNamesArray)do +self:Remove(RemoveClientName.ClientName) +end +return self +end +function SET_CLIENT:FindClient(ClientName) +local ClientFound=self.Set[ClientName] +return ClientFound +end +function SET_CLIENT:FilterCoalitions(Coalitions) +if not self.Filter.Coalitions then +self.Filter.Coalitions={} +end +if type(Coalitions)~="table"then +Coalitions={Coalitions} +end +for CoalitionID,Coalition in pairs(Coalitions)do +self.Filter.Coalitions[Coalition]=Coalition +end +return self +end +function SET_CLIENT:FilterCategories(Categories) +if not self.Filter.Categories then +self.Filter.Categories={} +end +if type(Categories)~="table"then +Categories={Categories} +end +for CategoryID,Category in pairs(Categories)do +self.Filter.Categories[Category]=Category +end +return self +end +function SET_CLIENT:FilterTypes(Types) +if not self.Filter.Types then +self.Filter.Types={} +end +if type(Types)~="table"then +Types={Types} +end +for TypeID,Type in pairs(Types)do +self.Filter.Types[Type]=Type +end +return self +end +function SET_CLIENT:FilterCountries(Countries) +if not self.Filter.Countries then +self.Filter.Countries={} +end +if type(Countries)~="table"then +Countries={Countries} +end +for CountryID,Country in pairs(Countries)do +self.Filter.Countries[Country]=Country +end +return self +end +function SET_CLIENT:FilterPrefixes(Prefixes) +if not self.Filter.ClientPrefixes then +self.Filter.ClientPrefixes={} +end +if type(Prefixes)~="table"then +Prefixes={Prefixes} +end +for PrefixID,Prefix in pairs(Prefixes)do +self.Filter.ClientPrefixes[Prefix]=Prefix +end +return self +end +function SET_CLIENT:FilterActive(Active) +Active=Active or not(Active==false) +self.Filter.Active=Active +return self +end +function SET_CLIENT:FilterStart() +if _DATABASE then +self:_FilterStart() +self:HandleEvent(EVENTS.Birth,self._EventOnBirth) +self:HandleEvent(EVENTS.Dead,self._EventOnDeadOrCrash) +self:HandleEvent(EVENTS.Crash,self._EventOnDeadOrCrash) +end +return self +end +function SET_CLIENT:AddInDatabase(Event) +self:F3({Event}) +return Event.IniDCSUnitName,self.Database[Event.IniDCSUnitName] +end +function SET_CLIENT:FindInDatabase(Event) +self:F3({Event}) +return Event.IniDCSUnitName,self.Database[Event.IniDCSUnitName] +end +function SET_CLIENT:ForEachClient(IteratorFunction,...) +self:F2(arg) +self:ForEach(IteratorFunction,arg,self:GetSet()) +return self +end +function SET_CLIENT:ForEachClientInZone(ZoneObject,IteratorFunction,...) +self:F2(arg) +self:ForEach(IteratorFunction,arg,self:GetSet(), +function(ZoneObject,ClientObject) +if ClientObject:IsInZone(ZoneObject)then +return true +else +return false +end +end,{ZoneObject}) +return self +end +function SET_CLIENT:ForEachClientNotInZone(ZoneObject,IteratorFunction,...) +self:F2(arg) +self:ForEach(IteratorFunction,arg,self:GetSet(), +function(ZoneObject,ClientObject) +if ClientObject:IsNotInZone(ZoneObject)then +return true +else +return false +end +end,{ZoneObject}) +return self +end +function SET_CLIENT:IsIncludeObject(MClient) +self:F2(MClient) +local MClientInclude=true +if MClient then +local MClientName=MClient.UnitName +if self.Filter.Active~=nil then +local MClientActive=false +if self.Filter.Active==false or(self.Filter.Active==true and MClient:IsActive()==true)then +MClientActive=true +end +MClientInclude=MClientInclude and MClientActive +end +if self.Filter.Coalitions then +local MClientCoalition=false +for CoalitionID,CoalitionName in pairs(self.Filter.Coalitions)do +local ClientCoalitionID=_DATABASE:GetCoalitionFromClientTemplate(MClientName) +self:T3({"Coalition:",ClientCoalitionID,self.FilterMeta.Coalitions[CoalitionName],CoalitionName}) +if self.FilterMeta.Coalitions[CoalitionName]and self.FilterMeta.Coalitions[CoalitionName]==ClientCoalitionID then +MClientCoalition=true +end +end +self:T({"Evaluated Coalition",MClientCoalition}) +MClientInclude=MClientInclude and MClientCoalition +end +if self.Filter.Categories then +local MClientCategory=false +for CategoryID,CategoryName in pairs(self.Filter.Categories)do +local ClientCategoryID=_DATABASE:GetCategoryFromClientTemplate(MClientName) +self:T3({"Category:",ClientCategoryID,self.FilterMeta.Categories[CategoryName],CategoryName}) +if self.FilterMeta.Categories[CategoryName]and self.FilterMeta.Categories[CategoryName]==ClientCategoryID then +MClientCategory=true +end +end +self:T({"Evaluated Category",MClientCategory}) +MClientInclude=MClientInclude and MClientCategory +end +if self.Filter.Types then +local MClientType=false +for TypeID,TypeName in pairs(self.Filter.Types)do +self:T3({"Type:",MClient:GetTypeName(),TypeName}) +if TypeName==MClient:GetTypeName()then +MClientType=true +end +end +self:T({"Evaluated Type",MClientType}) +MClientInclude=MClientInclude and MClientType +end +if self.Filter.Countries then +local MClientCountry=false +for CountryID,CountryName in pairs(self.Filter.Countries)do +local ClientCountryID=_DATABASE:GetCountryFromClientTemplate(MClientName) +self:T3({"Country:",ClientCountryID,country.id[CountryName],CountryName}) +if country.id[CountryName]and country.id[CountryName]==ClientCountryID then +MClientCountry=true +end +end +self:T({"Evaluated Country",MClientCountry}) +MClientInclude=MClientInclude and MClientCountry +end +if self.Filter.ClientPrefixes then +local MClientPrefix=false +for ClientPrefixId,ClientPrefix in pairs(self.Filter.ClientPrefixes)do +self:T3({"Prefix:",string.find(MClient.UnitName,ClientPrefix,1),ClientPrefix}) +if string.find(MClient.UnitName,ClientPrefix,1)then +MClientPrefix=true +end +end +self:T({"Evaluated Prefix",MClientPrefix}) +MClientInclude=MClientInclude and MClientPrefix +end +end +self:T2(MClientInclude) +return MClientInclude +end +end +do +SET_PLAYER={ +ClassName="SET_PLAYER", +Clients={}, +Filter={ +Coalitions=nil, +Categories=nil, +Types=nil, +Countries=nil, +ClientPrefixes=nil, +}, +FilterMeta={ +Coalitions={ +red=coalition.side.RED, +blue=coalition.side.BLUE, +neutral=coalition.side.NEUTRAL, +}, +Categories={ +plane=Unit.Category.AIRPLANE, +helicopter=Unit.Category.HELICOPTER, +ground=Unit.Category.GROUND_UNIT, +ship=Unit.Category.SHIP, +structure=Unit.Category.STRUCTURE, +}, +}, +} +function SET_PLAYER:New() +local self=BASE:Inherit(self,SET_BASE:New(_DATABASE.PLAYERS)) +return self +end +function SET_PLAYER:AddClientsByName(AddClientNames) +local AddClientNamesArray=(type(AddClientNames)=="table")and AddClientNames or{AddClientNames} +for AddClientID,AddClientName in pairs(AddClientNamesArray)do +self:Add(AddClientName,CLIENT:FindByName(AddClientName)) +end +return self +end +function SET_PLAYER:RemoveClientsByName(RemoveClientNames) +local RemoveClientNamesArray=(type(RemoveClientNames)=="table")and RemoveClientNames or{RemoveClientNames} +for RemoveClientID,RemoveClientName in pairs(RemoveClientNamesArray)do +self:Remove(RemoveClientName.ClientName) +end +return self +end +function SET_PLAYER:FindClient(PlayerName) +local ClientFound=self.Set[PlayerName] +return ClientFound +end +function SET_PLAYER:FilterCoalitions(Coalitions) +if not self.Filter.Coalitions then +self.Filter.Coalitions={} +end +if type(Coalitions)~="table"then +Coalitions={Coalitions} +end +for CoalitionID,Coalition in pairs(Coalitions)do +self.Filter.Coalitions[Coalition]=Coalition +end +return self +end +function SET_PLAYER:FilterCategories(Categories) +if not self.Filter.Categories then +self.Filter.Categories={} +end +if type(Categories)~="table"then +Categories={Categories} +end +for CategoryID,Category in pairs(Categories)do +self.Filter.Categories[Category]=Category +end +return self +end +function SET_PLAYER:FilterTypes(Types) +if not self.Filter.Types then +self.Filter.Types={} +end +if type(Types)~="table"then +Types={Types} +end +for TypeID,Type in pairs(Types)do +self.Filter.Types[Type]=Type +end +return self +end +function SET_PLAYER:FilterCountries(Countries) +if not self.Filter.Countries then +self.Filter.Countries={} +end +if type(Countries)~="table"then +Countries={Countries} +end +for CountryID,Country in pairs(Countries)do +self.Filter.Countries[Country]=Country +end +return self +end +function SET_PLAYER:FilterPrefixes(Prefixes) +if not self.Filter.ClientPrefixes then +self.Filter.ClientPrefixes={} +end +if type(Prefixes)~="table"then +Prefixes={Prefixes} +end +for PrefixID,Prefix in pairs(Prefixes)do +self.Filter.ClientPrefixes[Prefix]=Prefix +end +return self +end +function SET_PLAYER:FilterStart() +if _DATABASE then +self:_FilterStart() +self:HandleEvent(EVENTS.Birth,self._EventOnBirth) +self:HandleEvent(EVENTS.Dead,self._EventOnDeadOrCrash) +self:HandleEvent(EVENTS.Crash,self._EventOnDeadOrCrash) +end +return self +end +function SET_PLAYER:AddInDatabase(Event) +self:F3({Event}) +return Event.IniDCSUnitName,self.Database[Event.IniDCSUnitName] +end +function SET_PLAYER:FindInDatabase(Event) +self:F3({Event}) +return Event.IniDCSUnitName,self.Database[Event.IniDCSUnitName] +end +function SET_PLAYER:ForEachPlayer(IteratorFunction,...) +self:F2(arg) +self:ForEach(IteratorFunction,arg,self:GetSet()) +return self +end +function SET_PLAYER:ForEachPlayerInZone(ZoneObject,IteratorFunction,...) +self:F2(arg) +self:ForEach(IteratorFunction,arg,self:GetSet(), +function(ZoneObject,ClientObject) +if ClientObject:IsInZone(ZoneObject)then +return true +else +return false +end +end,{ZoneObject}) +return self +end +function SET_PLAYER:ForEachPlayerNotInZone(ZoneObject,IteratorFunction,...) +self:F2(arg) +self:ForEach(IteratorFunction,arg,self:GetSet(), +function(ZoneObject,ClientObject) +if ClientObject:IsNotInZone(ZoneObject)then +return true +else +return false +end +end,{ZoneObject}) +return self +end +function SET_PLAYER:IsIncludeObject(MClient) +self:F2(MClient) +local MClientInclude=true +if MClient then +local MClientName=MClient.UnitName +if self.Filter.Coalitions then +local MClientCoalition=false +for CoalitionID,CoalitionName in pairs(self.Filter.Coalitions)do +local ClientCoalitionID=_DATABASE:GetCoalitionFromClientTemplate(MClientName) +self:T3({"Coalition:",ClientCoalitionID,self.FilterMeta.Coalitions[CoalitionName],CoalitionName}) +if self.FilterMeta.Coalitions[CoalitionName]and self.FilterMeta.Coalitions[CoalitionName]==ClientCoalitionID then +MClientCoalition=true +end +end +self:T({"Evaluated Coalition",MClientCoalition}) +MClientInclude=MClientInclude and MClientCoalition +end +if self.Filter.Categories then +local MClientCategory=false +for CategoryID,CategoryName in pairs(self.Filter.Categories)do +local ClientCategoryID=_DATABASE:GetCategoryFromClientTemplate(MClientName) +self:T3({"Category:",ClientCategoryID,self.FilterMeta.Categories[CategoryName],CategoryName}) +if self.FilterMeta.Categories[CategoryName]and self.FilterMeta.Categories[CategoryName]==ClientCategoryID then +MClientCategory=true +end +end +self:T({"Evaluated Category",MClientCategory}) +MClientInclude=MClientInclude and MClientCategory +end +if self.Filter.Types then +local MClientType=false +for TypeID,TypeName in pairs(self.Filter.Types)do +self:T3({"Type:",MClient:GetTypeName(),TypeName}) +if TypeName==MClient:GetTypeName()then +MClientType=true +end +end +self:T({"Evaluated Type",MClientType}) +MClientInclude=MClientInclude and MClientType +end +if self.Filter.Countries then +local MClientCountry=false +for CountryID,CountryName in pairs(self.Filter.Countries)do +local ClientCountryID=_DATABASE:GetCountryFromClientTemplate(MClientName) +self:T3({"Country:",ClientCountryID,country.id[CountryName],CountryName}) +if country.id[CountryName]and country.id[CountryName]==ClientCountryID then +MClientCountry=true +end +end +self:T({"Evaluated Country",MClientCountry}) +MClientInclude=MClientInclude and MClientCountry +end +if self.Filter.ClientPrefixes then +local MClientPrefix=false +for ClientPrefixId,ClientPrefix in pairs(self.Filter.ClientPrefixes)do +self:T3({"Prefix:",string.find(MClient.UnitName,ClientPrefix,1),ClientPrefix}) +if string.find(MClient.UnitName,ClientPrefix,1)then +MClientPrefix=true +end +end +self:T({"Evaluated Prefix",MClientPrefix}) +MClientInclude=MClientInclude and MClientPrefix +end +end +self:T2(MClientInclude) +return MClientInclude +end +end +do +SET_AIRBASE={ +ClassName="SET_AIRBASE", +Airbases={}, +Filter={ +Coalitions=nil, +}, +FilterMeta={ +Coalitions={ +red=coalition.side.RED, +blue=coalition.side.BLUE, +neutral=coalition.side.NEUTRAL, +}, +Categories={ +airdrome=Airbase.Category.AIRDROME, +helipad=Airbase.Category.HELIPAD, +ship=Airbase.Category.SHIP, +}, +}, +} +function SET_AIRBASE:New() +local self=BASE:Inherit(self,SET_BASE:New(_DATABASE.AIRBASES)) +return self +end +function SET_AIRBASE:AddAirbase(airbase) +self:Add(airbase:GetName(),airbase) +return self +end +function SET_AIRBASE:AddAirbasesByName(AddAirbaseNames) +local AddAirbaseNamesArray=(type(AddAirbaseNames)=="table")and AddAirbaseNames or{AddAirbaseNames} +for AddAirbaseID,AddAirbaseName in pairs(AddAirbaseNamesArray)do +self:Add(AddAirbaseName,AIRBASE:FindByName(AddAirbaseName)) +end +return self +end +function SET_AIRBASE:RemoveAirbasesByName(RemoveAirbaseNames) +local RemoveAirbaseNamesArray=(type(RemoveAirbaseNames)=="table")and RemoveAirbaseNames or{RemoveAirbaseNames} +for RemoveAirbaseID,RemoveAirbaseName in pairs(RemoveAirbaseNamesArray)do +self:Remove(RemoveAirbaseName) +end +return self +end +function SET_AIRBASE:FindAirbase(AirbaseName) +local AirbaseFound=self.Set[AirbaseName] +return AirbaseFound +end +function SET_AIRBASE:FindAirbaseInRange(Coordinate,Range) +local AirbaseFound=nil +for AirbaseName,AirbaseObject in pairs(self.Set)do +local AirbaseCoordinate=AirbaseObject:GetCoordinate() +local Distance=Coordinate:Get2DDistance(AirbaseCoordinate) +self:F({Distance=Distance}) +if Distance<=Range then +AirbaseFound=AirbaseObject +break +end +end +return AirbaseFound +end +function SET_AIRBASE:GetRandomAirbase() +local RandomAirbase=self:GetRandom() +self:F({RandomAirbase=RandomAirbase:GetName()}) +return RandomAirbase +end +function SET_AIRBASE:FilterCoalitions(Coalitions) +if not self.Filter.Coalitions then +self.Filter.Coalitions={} +end +if type(Coalitions)~="table"then +Coalitions={Coalitions} +end +for CoalitionID,Coalition in pairs(Coalitions)do +self.Filter.Coalitions[Coalition]=Coalition +end +return self +end +function SET_AIRBASE:FilterCategories(Categories) +if not self.Filter.Categories then +self.Filter.Categories={} +end +if type(Categories)~="table"then +Categories={Categories} +end +for CategoryID,Category in pairs(Categories)do +self.Filter.Categories[Category]=Category +end +return self +end +function SET_AIRBASE:FilterStart() +if _DATABASE then +self:HandleEvent(EVENTS.BaseCaptured) +self:HandleEvent(EVENTS.Dead) +for ObjectName,Object in pairs(self.Database)do +if self:IsIncludeObject(Object)then +self:Add(ObjectName,Object) +else +self:RemoveAirbasesByName(ObjectName) +end +end +end +return self +end +function SET_AIRBASE:OnEventBaseCaptured(EventData) +for ObjectName,Object in pairs(self.Database)do +if self:IsIncludeObject(Object)then +self:Add(ObjectName,Object) +else +self:RemoveAirbasesByName(ObjectName) +end +end +end +function SET_AIRBASE:OnEventDead(EventData) +local airbaseName,airbase=self:FindInDatabase(EventData) +if airbase and airbase:IsShip()or airbase:IsHelipad()then +self:RemoveAirbasesByName(airbaseName) +end +end +function SET_AIRBASE:AddInDatabase(Event) +return Event.IniDCSUnitName,self.Database[Event.IniDCSUnitName] +end +function SET_AIRBASE:FindInDatabase(Event) +self:F3({Event}) +return Event.IniDCSUnitName,self.Database[Event.IniDCSUnitName] +end +function SET_AIRBASE:ForEachAirbase(IteratorFunction,...) +self:F2(arg) +self:ForEach(IteratorFunction,arg,self:GetSet()) +return self +end +function SET_AIRBASE:FindNearestAirbaseFromPointVec2(PointVec2) +self:F2(PointVec2) +local NearestAirbase=self:FindNearestObjectFromPointVec2(PointVec2) +return NearestAirbase +end +function SET_AIRBASE:IsIncludeObject(MAirbase) +self:F2(MAirbase) +local MAirbaseInclude=true +if MAirbase then +local MAirbaseName=MAirbase:GetName() +if self.Filter.Coalitions then +local MAirbaseCoalition=false +for CoalitionID,CoalitionName in pairs(self.Filter.Coalitions)do +local AirbaseCoalitionID=_DATABASE:GetCoalitionFromAirbase(MAirbaseName) +self:T3({"Coalition:",AirbaseCoalitionID,self.FilterMeta.Coalitions[CoalitionName],CoalitionName}) +if self.FilterMeta.Coalitions[CoalitionName]and self.FilterMeta.Coalitions[CoalitionName]==AirbaseCoalitionID then +MAirbaseCoalition=true +end +end +self:T({"Evaluated Coalition",MAirbaseCoalition}) +MAirbaseInclude=MAirbaseInclude and MAirbaseCoalition +end +if self.Filter.Categories then +local MAirbaseCategory=false +for CategoryID,CategoryName in pairs(self.Filter.Categories)do +local AirbaseCategoryID=_DATABASE:GetCategoryFromAirbase(MAirbaseName) +self:T3({"Category:",AirbaseCategoryID,self.FilterMeta.Categories[CategoryName],CategoryName}) +if self.FilterMeta.Categories[CategoryName]and self.FilterMeta.Categories[CategoryName]==AirbaseCategoryID then +MAirbaseCategory=true +end +end +self:T({"Evaluated Category",MAirbaseCategory}) +MAirbaseInclude=MAirbaseInclude and MAirbaseCategory +end +end +self:T2(MAirbaseInclude) +return MAirbaseInclude +end +end +do +SET_CARGO={ +ClassName="SET_CARGO", +Cargos={}, +Filter={ +Coalitions=nil, +Types=nil, +Countries=nil, +ClientPrefixes=nil, +}, +FilterMeta={ +Coalitions={ +red=coalition.side.RED, +blue=coalition.side.BLUE, +neutral=coalition.side.NEUTRAL, +}, +}, +} +function SET_CARGO:New() +local self=BASE:Inherit(self,SET_BASE:New(_DATABASE.CARGOS)) +return self +end +function SET_CARGO:AddCargo(Cargo) +self:Add(Cargo:GetName(),Cargo) +return self +end +function SET_CARGO:AddCargosByName(AddCargoNames) +local AddCargoNamesArray=(type(AddCargoNames)=="table")and AddCargoNames or{AddCargoNames} +for AddCargoID,AddCargoName in pairs(AddCargoNamesArray)do +self:Add(AddCargoName,CARGO:FindByName(AddCargoName)) +end +return self +end +function SET_CARGO:RemoveCargosByName(RemoveCargoNames) +local RemoveCargoNamesArray=(type(RemoveCargoNames)=="table")and RemoveCargoNames or{RemoveCargoNames} +for RemoveCargoID,RemoveCargoName in pairs(RemoveCargoNamesArray)do +self:Remove(RemoveCargoName.CargoName) +end +return self +end +function SET_CARGO:FindCargo(CargoName) +local CargoFound=self.Set[CargoName] +return CargoFound +end +function SET_CARGO:FilterCoalitions(Coalitions) +if not self.Filter.Coalitions then +self.Filter.Coalitions={} +end +if type(Coalitions)~="table"then +Coalitions={Coalitions} +end +for CoalitionID,Coalition in pairs(Coalitions)do +self.Filter.Coalitions[Coalition]=Coalition +end +return self +end +function SET_CARGO:FilterTypes(Types) +if not self.Filter.Types then +self.Filter.Types={} +end +if type(Types)~="table"then +Types={Types} +end +for TypeID,Type in pairs(Types)do +self.Filter.Types[Type]=Type +end +return self +end +function SET_CARGO:FilterCountries(Countries) +if not self.Filter.Countries then +self.Filter.Countries={} +end +if type(Countries)~="table"then +Countries={Countries} +end +for CountryID,Country in pairs(Countries)do +self.Filter.Countries[Country]=Country +end +return self +end +function SET_CARGO:FilterPrefixes(Prefixes) +if not self.Filter.CargoPrefixes then +self.Filter.CargoPrefixes={} +end +if type(Prefixes)~="table"then +Prefixes={Prefixes} +end +for PrefixID,Prefix in pairs(Prefixes)do +self.Filter.CargoPrefixes[Prefix]=Prefix +end +return self +end +function SET_CARGO:FilterStart() +if _DATABASE then +self:_FilterStart() +self:HandleEvent(EVENTS.NewCargo) +self:HandleEvent(EVENTS.DeleteCargo) +end +return self +end +function SET_CARGO:FilterStop() +self:UnHandleEvent(EVENTS.NewCargo) +self:UnHandleEvent(EVENTS.DeleteCargo) +return self +end +function SET_CARGO:AddInDatabase(Event) +self:F3({Event}) +return Event.IniDCSUnitName,self.Database[Event.IniDCSUnitName] +end +function SET_CARGO:FindInDatabase(Event) +self:F3({Event}) +return Event.IniDCSUnitName,self.Database[Event.IniDCSUnitName] +end +function SET_CARGO:ForEachCargo(IteratorFunction,...) +self:F2(arg) +self:ForEach(IteratorFunction,arg,self:GetSet()) +return self +end +function SET_CARGO:FindNearestCargoFromPointVec2(PointVec2) +self:F2(PointVec2) +local NearestCargo=self:FindNearestObjectFromPointVec2(PointVec2) +return NearestCargo +end +function SET_CARGO:FirstCargoWithState(State) +local FirstCargo=nil +for CargoName,Cargo in pairs(self.Set)do +if Cargo:Is(State)then +FirstCargo=Cargo +break +end +end +return FirstCargo +end +function SET_CARGO:FirstCargoWithStateAndNotDeployed(State) +local FirstCargo=nil +for CargoName,Cargo in pairs(self.Set)do +if Cargo:Is(State)and not Cargo:IsDeployed()then +FirstCargo=Cargo +break +end +end +return FirstCargo +end +function SET_CARGO:FirstCargoUnLoaded() +local FirstCargo=self:FirstCargoWithState("UnLoaded") +return FirstCargo +end +function SET_CARGO:FirstCargoUnLoadedAndNotDeployed() +local FirstCargo=self:FirstCargoWithStateAndNotDeployed("UnLoaded") +return FirstCargo +end +function SET_CARGO:FirstCargoLoaded() +local FirstCargo=self:FirstCargoWithState("Loaded") +return FirstCargo +end +function SET_CARGO:FirstCargoDeployed() +local FirstCargo=self:FirstCargoWithState("Deployed") +return FirstCargo +end +function SET_CARGO:IsIncludeObject(MCargo) +self:F2(MCargo) +local MCargoInclude=true +if MCargo then +local MCargoName=MCargo:GetName() +if self.Filter.Coalitions then +local MCargoCoalition=false +for CoalitionID,CoalitionName in pairs(self.Filter.Coalitions)do +local CargoCoalitionID=MCargo:GetCoalition() +self:T3({"Coalition:",CargoCoalitionID,self.FilterMeta.Coalitions[CoalitionName],CoalitionName}) +if self.FilterMeta.Coalitions[CoalitionName]and self.FilterMeta.Coalitions[CoalitionName]==CargoCoalitionID then +MCargoCoalition=true +end +end +self:F({"Evaluated Coalition",MCargoCoalition}) +MCargoInclude=MCargoInclude and MCargoCoalition +end +if self.Filter.Types then +local MCargoType=false +for TypeID,TypeName in pairs(self.Filter.Types)do +self:T3({"Type:",MCargo:GetType(),TypeName}) +if TypeName==MCargo:GetType()then +MCargoType=true +end +end +self:F({"Evaluated Type",MCargoType}) +MCargoInclude=MCargoInclude and MCargoType +end +if self.Filter.CargoPrefixes then +local MCargoPrefix=false +for CargoPrefixId,CargoPrefix in pairs(self.Filter.CargoPrefixes)do +self:T3({"Prefix:",string.find(MCargo.Name,CargoPrefix,1),CargoPrefix}) +if string.find(MCargo.Name,CargoPrefix,1)then +MCargoPrefix=true +end +end +self:F({"Evaluated Prefix",MCargoPrefix}) +MCargoInclude=MCargoInclude and MCargoPrefix +end +end +self:T2(MCargoInclude) +return MCargoInclude +end +function SET_CARGO:OnEventNewCargo(EventData) +self:F({"New Cargo",EventData}) +if EventData.Cargo then +if EventData.Cargo and self:IsIncludeObject(EventData.Cargo)then +self:Add(EventData.Cargo.Name,EventData.Cargo) +end +end +end +function SET_CARGO:OnEventDeleteCargo(EventData) +self:F3({EventData}) +if EventData.Cargo then +local Cargo=_DATABASE:FindCargo(EventData.Cargo.Name) +if Cargo and Cargo.Name then +self:F({CargoNoDestroy=Cargo.NoDestroy}) +if Cargo.NoDestroy then +else +self:Remove(Cargo.Name) +end +end +end +end +end +do +SET_ZONE={ +ClassName="SET_ZONE", +Zones={}, +Filter={ +Prefixes=nil, +}, +FilterMeta={ +}, +} +function SET_ZONE:New() +local self=BASE:Inherit(self,SET_BASE:New(_DATABASE.ZONES)) +return self +end +function SET_ZONE:AddZonesByName(AddZoneNames) +local AddZoneNamesArray=(type(AddZoneNames)=="table")and AddZoneNames or{AddZoneNames} +for AddAirbaseID,AddZoneName in pairs(AddZoneNamesArray)do +self:Add(AddZoneName,ZONE:FindByName(AddZoneName)) +end +return self +end +function SET_ZONE:AddZone(Zone) +self:Add(Zone:GetName(),Zone) +return self +end +function SET_ZONE:RemoveZonesByName(RemoveZoneNames) +local RemoveZoneNamesArray=(type(RemoveZoneNames)=="table")and RemoveZoneNames or{RemoveZoneNames} +for RemoveZoneID,RemoveZoneName in pairs(RemoveZoneNamesArray)do +self:Remove(RemoveZoneName) +end +return self +end +function SET_ZONE:FindZone(ZoneName) +local ZoneFound=self.Set[ZoneName] +return ZoneFound +end +function SET_ZONE:GetRandomZone(margin) +local margin=margin or 100 +if self:Count()~=0 then +local Index=self.Index +local ZoneFound=nil +local counter=0 +while(not ZoneFound)or(counter=self.x and z-Precision<=self.z and z+Precision>=self.z +end +function COORDINATE:ScanObjects(radius,scanunits,scanstatics,scanscenery) +self:F(string.format("Scanning in radius %.1f m.",radius or 100)) +local SphereSearch={ +id=world.VolumeType.SPHERE, +params={ +point=self:GetVec3(), +radius=radius, +} +} +radius=radius or 100 +if scanunits==nil then +scanunits=true +end +if scanstatics==nil then +scanstatics=true +end +if scanscenery==nil then +scanscenery=false +end +local scanobjects={} +if scanunits then +table.insert(scanobjects,Object.Category.UNIT) +end +if scanstatics then +table.insert(scanobjects,Object.Category.STATIC) +end +if scanscenery then +table.insert(scanobjects,Object.Category.SCENERY) +end +local Units={} +local Statics={} +local Scenery={} +local gotstatics=false +local gotunits=false +local gotscenery=false +local function EvaluateZone(ZoneObject) +if ZoneObject then +local ObjectCategory=ZoneObject:getCategory() +if ObjectCategory==Object.Category.UNIT and ZoneObject:isExist()then +table.insert(Units,UNIT:Find(ZoneObject)) +gotunits=true +elseif ObjectCategory==Object.Category.STATIC and ZoneObject:isExist()then +table.insert(Statics,ZoneObject) +gotstatics=true +elseif ObjectCategory==Object.Category.SCENERY then +table.insert(Scenery,ZoneObject) +gotscenery=true +end +end +return true +end +world.searchObjects(scanobjects,SphereSearch,EvaluateZone) +for _,unit in pairs(Units)do +self:T(string.format("Scan found unit %s",unit:GetName())) +end +for _,static in pairs(Statics)do +self:T(string.format("Scan found static %s",static:getName())) +_DATABASE:AddStatic(static:getName()) +end +for _,scenery in pairs(Scenery)do +self:T(string.format("Scan found scenery %s typename=%s",scenery:getName(),scenery:getTypeName())) +end +return gotunits,gotstatics,gotscenery,Units,Statics,Scenery +end +function COORDINATE:ScanUnits(radius) +local _,_,_,units=self:ScanObjects(radius,true,false,false) +local set=SET_UNIT:New() +for _,unit in pairs(units)do +set:AddUnit(unit) +end +return set +end +function COORDINATE:FindClosestUnit(radius) +local units=self:ScanUnits(radius) +local umin=nil +local dmin=math.huge +for _,_unit in pairs(units.Set)do +local unit=_unit +local coordinate=unit:GetCoordinate() +local d=self:Get2DDistance(coordinate) +if d1 then +Radials=2-Radials +end +local RadialMultiplier +if InnerRadius and InnerRadius<=OuterRadius then +RadialMultiplier=(OuterRadius-InnerRadius)*Radials+InnerRadius +else +RadialMultiplier=OuterRadius*Radials +end +local RandomVec2 +if OuterRadius>0 then +RandomVec2={x=math.cos(Theta)*RadialMultiplier+self.x,y=math.sin(Theta)*RadialMultiplier+self.z} +else +RandomVec2={x=self.x,y=self.z} +end +return RandomVec2 +end +function COORDINATE:GetRandomCoordinateInRadius(OuterRadius,InnerRadius) +self:F2({OuterRadius,InnerRadius}) +return COORDINATE:NewFromVec2(self:GetRandomVec2InRadius(OuterRadius,InnerRadius)) +end +function COORDINATE:GetRandomVec3InRadius(OuterRadius,InnerRadius) +local RandomVec2=self:GetRandomVec2InRadius(OuterRadius,InnerRadius) +local y=self.y+math.random(InnerRadius,OuterRadius) +local RandomVec3={x=RandomVec2.x,y=y,z=RandomVec2.y} +return RandomVec3 +end +function COORDINATE:GetLandHeight() +local Vec2={x=self.x,y=self.z} +return land.getHeight(Vec2) +end +function COORDINATE:SetHeading(Heading) +self.Heading=Heading +end +function COORDINATE:GetHeading() +return self.Heading +end +function COORDINATE:SetVelocity(Velocity) +self.Velocity=Velocity +end +function COORDINATE:GetVelocity() +local Velocity=self.Velocity +return Velocity or 0 +end +function COORDINATE:GetName() +local name=self:ToStringMGRS() +return name +end +function COORDINATE:GetMovingText(Settings) +return self:GetVelocityText(Settings)..", "..self:GetHeadingText(Settings) +end +function COORDINATE:GetDirectionVec3(TargetCoordinate) +return{x=TargetCoordinate.x-self.x,y=TargetCoordinate.y-self.y,z=TargetCoordinate.z-self.z} +end +function COORDINATE:GetNorthCorrectionRadians() +local TargetVec3=self:GetVec3() +local lat,lon=coord.LOtoLL(TargetVec3) +local north_posit=coord.LLtoLO(lat+1,lon) +return math.atan2(north_posit.z-TargetVec3.z,north_posit.x-TargetVec3.x) +end +function COORDINATE:GetAngleRadians(DirectionVec3) +local DirectionRadians=math.atan2(DirectionVec3.z,DirectionVec3.x) +if DirectionRadians<0 then +DirectionRadians=DirectionRadians+2*math.pi +end +return DirectionRadians +end +function COORDINATE:GetAngleDegrees(DirectionVec3) +local AngleRadians=self:GetAngleRadians(DirectionVec3) +local Angle=UTILS.ToDegree(AngleRadians) +return Angle +end +function COORDINATE:GetIntermediateCoordinate(ToCoordinate,Fraction) +local f=Fraction or 0.5 +local vec=UTILS.VecSubstract(ToCoordinate,self) +vec.x=f*vec.x +vec.y=f*vec.y +vec.z=f*vec.z +vec=UTILS.VecAdd(self,vec) +local coord=COORDINATE:New(vec.x,vec.y,vec.z) +return coord +end +function COORDINATE:Get2DDistance(TargetCoordinate) +local a={x=TargetCoordinate.x-self.x,y=0,z=TargetCoordinate.z-self.z} +local norm=UTILS.VecNorm(a) +return norm +end +function COORDINATE:GetTemperature(height) +self:F2(height) +local y=height or self.y +local point={x=self.x,y=height or self.y,z=self.z} +local T,P=atmosphere.getTemperatureAndPressure(point) +return T-273.15 +end +function COORDINATE:GetTemperatureText(height,Settings) +local DegreesCelcius=self:GetTemperature(height) +local Settings=Settings or _SETTINGS +if DegreesCelcius then +if Settings:IsMetric()then +return string.format(" %-2.2f °C",DegreesCelcius) +else +return string.format(" %-2.2f °F",UTILS.CelciusToFarenheit(DegreesCelcius)) +end +else +return" no temperature" +end +return nil +end +function COORDINATE:GetPressure(height) +local point={x=self.x,y=height or self.y,z=self.z} +local T,P=atmosphere.getTemperatureAndPressure(point) +return P/100 +end +function COORDINATE:GetPressureText(height,Settings) +local Pressure_hPa=self:GetPressure(height) +local Pressure_mmHg=Pressure_hPa*0.7500615613030 +local Pressure_inHg=Pressure_hPa*0.0295299830714 +local Settings=Settings or _SETTINGS +if Pressure_hPa then +if Settings:IsMetric()then +return string.format(" %4.1f hPa (%3.1f mmHg)",Pressure_hPa,Pressure_mmHg) +else +return string.format(" %4.1f hPa (%3.2f inHg)",Pressure_hPa,Pressure_inHg) +end +else +return" no pressure" +end +return nil +end +function COORDINATE:HeadingTo(ToCoordinate) +local dz=ToCoordinate.z-self.z +local dx=ToCoordinate.x-self.x +local heading=math.deg(math.atan2(dz,dx)) +if heading<0 then +heading=360+heading +end +return heading +end +function COORDINATE:GetWind(height) +local landheight=self:GetLandHeight()+0.1 +local point={x=self.x,y=math.max(height or self.y,landheight),z=self.z} +local wind=atmosphere.getWind(point) +local direction=math.deg(math.atan2(wind.z,wind.x)) +if direction<0 then +direction=360+direction +end +if direction>180 then +direction=direction-180 +else +direction=direction+180 +end +local strength=math.sqrt((wind.x)^2+(wind.z)^2) +return direction,strength +end +function COORDINATE:GetWindWithTurbulenceVec3(height) +local landheight=self:GetLandHeight()+0.1 +local point={x=self.x,y=math.max(height or self.y,landheight),z=self.z} +local vec3=atmosphere.getWindWithTurbulence(point) +return vec3 +end +function COORDINATE:GetWindText(height,Settings) +local Direction,Strength=self:GetWind(height) +local Settings=Settings or _SETTINGS +if Direction and Strength then +if Settings:IsMetric()then +return string.format(" %d ° at %3.2f mps",Direction,UTILS.MpsToKmph(Strength)) +else +return string.format(" %d ° at %3.2f kps",Direction,UTILS.MpsToKnots(Strength)) +end +else +return" no wind" +end +return nil +end +function COORDINATE:Get3DDistance(TargetCoordinate) +local TargetVec3=TargetCoordinate:GetVec3() +local SourceVec3=self:GetVec3() +return((TargetVec3.x-SourceVec3.x)^2+(TargetVec3.y-SourceVec3.y)^2+(TargetVec3.z-SourceVec3.z)^2)^0.5 +end +function COORDINATE:GetBearingText(AngleRadians,Precision,Settings,Language) +local Settings=Settings or _SETTINGS +local AngleDegrees=UTILS.Round(UTILS.ToDegree(AngleRadians),Precision) +local s=string.format('%03d°',AngleDegrees) +return s +end +function COORDINATE:GetDistanceText(Distance,Settings,Language) +local Settings=Settings or _SETTINGS +local Language=Language or"EN" +local DistanceText +if Settings:IsMetric()then +if Language=="EN"then +DistanceText=" for "..UTILS.Round(Distance/1000,2).." km" +elseif Language=="RU"then +DistanceText=" за "..UTILS.Round(Distance/1000,2).." километров" +end +else +if Language=="EN"then +DistanceText=" for "..UTILS.Round(UTILS.MetersToNM(Distance),2).." miles" +elseif Language=="RU"then +DistanceText=" за "..UTILS.Round(UTILS.MetersToNM(Distance),2).." миль" +end +end +return DistanceText +end +function COORDINATE:GetAltitudeText(Settings,Language) +local Altitude=self.y +local Settings=Settings or _SETTINGS +local Language=Language or"EN" +if Altitude~=0 then +if Settings:IsMetric()then +if Language=="EN"then +return" at "..UTILS.Round(self.y,-3).." meters" +elseif Language=="RU"then +return" в "..UTILS.Round(self.y,-3).." метры" +end +else +if Language=="EN"then +return" at "..UTILS.Round(UTILS.MetersToFeet(self.y),-3).." feet" +elseif Language=="RU"then +return" в "..UTILS.Round(self.y,-3).." ноги" +end +end +else +return"" +end +end +function COORDINATE:GetVelocityText(Settings) +local Velocity=self:GetVelocity() +local Settings=Settings or _SETTINGS +if Velocity then +if Settings:IsMetric()then +return string.format(" moving at %d km/h",UTILS.MpsToKmph(Velocity)) +else +return string.format(" moving at %d mi/h",UTILS.MpsToKmph(Velocity)/1.852) +end +else +return" stationary" +end +end +function COORDINATE:GetHeadingText(Settings) +local Heading=self:GetHeading() +if Heading then +return string.format(" bearing %3d°",Heading) +else +return" bearing unknown" +end +end +function COORDINATE:GetBRText(AngleRadians,Distance,Settings,Language) +local Settings=Settings or _SETTINGS +local BearingText=self:GetBearingText(AngleRadians,0,Settings,Language) +local DistanceText=self:GetDistanceText(Distance,Settings,Language) +local BRText=BearingText..DistanceText +return BRText +end +function COORDINATE:GetBRAText(AngleRadians,Distance,Settings,Language) +local Settings=Settings or _SETTINGS +local BearingText=self:GetBearingText(AngleRadians,0,Settings,Language) +local DistanceText=self:GetDistanceText(Distance,Settings,Language) +local AltitudeText=self:GetAltitudeText(Settings,Language) +local BRAText=BearingText..DistanceText..AltitudeText +return BRAText +end +function COORDINATE:SetAltitude(altitude,asl) +local alt=altitude +if asl then +alt=altitude +else +alt=self:GetLandHeight()+altitude +end +self.y=alt +return self +end +function COORDINATE:WaypointAir(AltType,Type,Action,Speed,SpeedLocked,airbase,DCSTasks,description,timeReFuAr) +self:F2({AltType,Type,Action,Speed,SpeedLocked}) +AltType=AltType or"RADIO" +if SpeedLocked==nil then +SpeedLocked=true +end +Speed=Speed or 500 +local RoutePoint={} +RoutePoint.x=self.x +RoutePoint.y=self.z +RoutePoint.alt=self.y +RoutePoint.alt_type=AltType +RoutePoint.type=Type or nil +RoutePoint.action=Action or nil +RoutePoint.speed=Speed/3.6 +RoutePoint.speed_locked=SpeedLocked +RoutePoint.ETA=0 +RoutePoint.ETA_locked=true +RoutePoint.name=description +if airbase then +local AirbaseID=airbase:GetID() +local AirbaseCategory=airbase:GetAirbaseCategory() +if AirbaseCategory==Airbase.Category.SHIP or AirbaseCategory==Airbase.Category.HELIPAD then +RoutePoint.linkUnit=AirbaseID +RoutePoint.helipadId=AirbaseID +elseif AirbaseCategory==Airbase.Category.AIRDROME then +RoutePoint.airdromeId=AirbaseID +else +self:E("ERROR: Unknown airbase category in COORDINATE:WaypointAir()!") +end +end +if Type==COORDINATE.WaypointType.LandingReFuAr then +RoutePoint.timeReFuAr=timeReFuAr or 10 +end +RoutePoint.task={} +RoutePoint.task.id="ComboTask" +RoutePoint.task.params={} +RoutePoint.task.params.tasks=DCSTasks or{} +self:T({RoutePoint=RoutePoint}) +return RoutePoint +end +function COORDINATE:WaypointAirTurningPoint(AltType,Speed,DCSTasks,description) +return self:WaypointAir(AltType,COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.TurningPoint,Speed,true,nil,DCSTasks,description) +end +function COORDINATE:WaypointAirFlyOverPoint(AltType,Speed) +return self:WaypointAir(AltType,COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.FlyoverPoint,Speed) +end +function COORDINATE:WaypointAirTakeOffParkingHot(AltType,Speed) +return self:WaypointAir(AltType,COORDINATE.WaypointType.TakeOffParkingHot,COORDINATE.WaypointAction.FromParkingAreaHot,Speed) +end +function COORDINATE:WaypointAirTakeOffParking(AltType,Speed) +return self:WaypointAir(AltType,COORDINATE.WaypointType.TakeOffParking,COORDINATE.WaypointAction.FromParkingArea,Speed) +end +function COORDINATE:WaypointAirTakeOffRunway(AltType,Speed) +return self:WaypointAir(AltType,COORDINATE.WaypointType.TakeOff,COORDINATE.WaypointAction.FromRunway,Speed) +end +function COORDINATE:WaypointAirLanding(Speed,airbase,DCSTasks,description) +return self:WaypointAir(nil,COORDINATE.WaypointType.Land,COORDINATE.WaypointAction.Landing,Speed,false,airbase,DCSTasks,description) +end +function COORDINATE:WaypointAirLandingReFu(Speed,airbase,timeReFuAr,DCSTasks,description) +return self:WaypointAir(nil,COORDINATE.WaypointType.LandingReFuAr,COORDINATE.WaypointAction.LandingReFuAr,Speed,false,airbase,DCSTasks,description,timeReFuAr or 10) +end +function COORDINATE:WaypointGround(Speed,Formation,DCSTasks) +self:F2({Speed,Formation,DCSTasks}) +local RoutePoint={} +RoutePoint.x=self.x +RoutePoint.y=self.z +RoutePoint.alt=self:GetLandHeight()+1 +RoutePoint.alt_type=COORDINATE.WaypointAltType.BARO +RoutePoint.type="Turning Point" +RoutePoint.action=Formation or"Off Road" +RoutePoint.formation_template="" +RoutePoint.ETA=0 +RoutePoint.ETA_locked=true +RoutePoint.speed=(Speed or 20)/3.6 +RoutePoint.speed_locked=true +RoutePoint.task={} +RoutePoint.task.id="ComboTask" +RoutePoint.task.params={} +RoutePoint.task.params.tasks=DCSTasks or{} +return RoutePoint +end +function COORDINATE:WaypointNaval(Speed,Depth,DCSTasks) +self:F2({Speed,Depth,DCSTasks}) +local RoutePoint={} +RoutePoint.x=self.x +RoutePoint.y=self.z +RoutePoint.alt=Depth or self.y +RoutePoint.alt_type="BARO" +RoutePoint.type="Turning Point" +RoutePoint.action="Turning Point" +RoutePoint.formation_template="" +RoutePoint.ETA=0 +RoutePoint.ETA_locked=true +RoutePoint.speed=(Speed or 20)/3.6 +RoutePoint.speed_locked=true +RoutePoint.task={} +RoutePoint.task.id="ComboTask" +RoutePoint.task.params={} +RoutePoint.task.params.tasks=DCSTasks or{} +return RoutePoint +end +function COORDINATE:GetClosestAirbase2(Category,Coalition) +local airbases=AIRBASE.GetAllAirbases(Coalition) +local closest=nil +local distmin=nil +for _,_airbase in pairs(airbases)do +local airbase=_airbase +if airbase then +local category=airbase:GetAirbaseCategory() +if Category and Category==category or Category==nil then +local dist=self:Get2DDistance(airbase:GetCoordinate()) +if closest==nil then +distmin=dist +closest=airbase +else +if dist=2 then +for i=1,#Path-1 do +Way=Way+Path[i+1]:Get2DDistance(Path[i]) +end +else +return nil,nil,false +end +return Path,Way,GotPath +end +function COORDINATE:GetSurfaceType() +local vec2=self:GetVec2() +local surface=land.getSurfaceType(vec2) +return surface +end +function COORDINATE:IsSurfaceTypeLand() +return self:GetSurfaceType()==land.SurfaceType.LAND +end +function COORDINATE:IsSurfaceTypeLand() +return self:GetSurfaceType()==land.SurfaceType.LAND +end +function COORDINATE:IsSurfaceTypeRoad() +return self:GetSurfaceType()==land.SurfaceType.ROAD +end +function COORDINATE:IsSurfaceTypeRunway() +return self:GetSurfaceType()==land.SurfaceType.RUNWAY +end +function COORDINATE:IsSurfaceTypeShallowWater() +return self:GetSurfaceType()==land.SurfaceType.SHALLOW_WATER +end +function COORDINATE:IsSurfaceTypeWater() +return self:GetSurfaceType()==land.SurfaceType.WATER +end +function COORDINATE:Explosion(ExplosionIntensity,Delay) +self:F2({ExplosionIntensity}) +ExplosionIntensity=ExplosionIntensity or 100 +if Delay and Delay>0 then +self:ScheduleOnce(Delay,self.Explosion,self,ExplosionIntensity) +else +trigger.action.explosion(self:GetVec3(),ExplosionIntensity) +end +return self +end +function COORDINATE:IlluminationBomb(power) +self:F2() +trigger.action.illuminationBomb(self:GetVec3(),power) +end +function COORDINATE:Smoke(SmokeColor) +self:F2({SmokeColor}) +trigger.action.smoke(self:GetVec3(),SmokeColor) +end +function COORDINATE:SmokeGreen() +self:F2() +self:Smoke(SMOKECOLOR.Green) +end +function COORDINATE:SmokeRed() +self:F2() +self:Smoke(SMOKECOLOR.Red) +end +function COORDINATE:SmokeWhite() +self:F2() +self:Smoke(SMOKECOLOR.White) +end +function COORDINATE:SmokeOrange() +self:F2() +self:Smoke(SMOKECOLOR.Orange) +end +function COORDINATE:SmokeBlue() +self:F2() +self:Smoke(SMOKECOLOR.Blue) +end +function COORDINATE:BigSmokeAndFire(preset,density) +self:F2({preset=preset,density=density}) +density=density or 0.5 +trigger.action.effectSmokeBig(self:GetVec3(),preset,density) +end +function COORDINATE:BigSmokeAndFireSmall(density) +self:F2({density=density}) +density=density or 0.5 +self:BigSmokeAndFire(BIGSMOKEPRESET.SmallSmokeAndFire,density) +end +function COORDINATE:BigSmokeAndFireMedium(density) +self:F2({density=density}) +density=density or 0.5 +self:BigSmokeAndFire(BIGSMOKEPRESET.MediumSmokeAndFire,density) +end +function COORDINATE:BigSmokeAndFireLarge(density) +self:F2({density=density}) +density=density or 0.5 +self:BigSmokeAndFire(BIGSMOKEPRESET.LargeSmokeAndFire,density) +end +function COORDINATE:BigSmokeAndFireHuge(density) +self:F2({density=density}) +density=density or 0.5 +self:BigSmokeAndFire(BIGSMOKEPRESET.HugeSmokeAndFire,density) +end +function COORDINATE:BigSmokeSmall(density) +self:F2({density=density}) +density=density or 0.5 +self:BigSmokeAndFire(BIGSMOKEPRESET.SmallSmoke,density) +end +function COORDINATE:BigSmokeMedium(density) +self:F2({density=density}) +density=density or 0.5 +self:BigSmokeAndFire(BIGSMOKEPRESET.MediumSmoke,density) +end +function COORDINATE:BigSmokeLarge(density) +self:F2({density=density}) +density=density or 0.5 +self:BigSmokeAndFire(BIGSMOKEPRESET.LargeSmoke,density) +end +function COORDINATE:BigSmokeHuge(density) +self:F2({density=density}) +density=density or 0.5 +self:BigSmokeAndFire(BIGSMOKEPRESET.HugeSmoke,density) +end +function COORDINATE:Flare(FlareColor,Azimuth) +self:F2({FlareColor}) +trigger.action.signalFlare(self:GetVec3(),FlareColor,Azimuth and Azimuth or 0) +end +function COORDINATE:FlareWhite(Azimuth) +self:F2(Azimuth) +self:Flare(FLARECOLOR.White,Azimuth) +end +function COORDINATE:FlareYellow(Azimuth) +self:F2(Azimuth) +self:Flare(FLARECOLOR.Yellow,Azimuth) +end +function COORDINATE:FlareGreen(Azimuth) +self:F2(Azimuth) +self:Flare(FLARECOLOR.Green,Azimuth) +end +function COORDINATE:FlareRed(Azimuth) +self:F2(Azimuth) +self:Flare(FLARECOLOR.Red,Azimuth) +end +do +function COORDINATE:MarkToAll(MarkText,ReadOnly,Text) +local MarkID=UTILS.GetMarkID() +if ReadOnly==nil then +ReadOnly=false +end +local text=Text or"" +trigger.action.markToAll(MarkID,MarkText,self:GetVec3(),ReadOnly,text) +return MarkID +end +function COORDINATE:MarkToCoalition(MarkText,Coalition,ReadOnly,Text) +local MarkID=UTILS.GetMarkID() +if ReadOnly==nil then +ReadOnly=false +end +local text=Text or"" +trigger.action.markToCoalition(MarkID,MarkText,self:GetVec3(),Coalition,ReadOnly,text) +return MarkID +end +function COORDINATE:MarkToCoalitionRed(MarkText,ReadOnly,Text) +return self:MarkToCoalition(MarkText,coalition.side.RED,ReadOnly,Text) +end +function COORDINATE:MarkToCoalitionBlue(MarkText,ReadOnly,Text) +return self:MarkToCoalition(MarkText,coalition.side.BLUE,ReadOnly,Text) +end +function COORDINATE:MarkToGroup(MarkText,MarkGroup,ReadOnly,Text) +local MarkID=UTILS.GetMarkID() +if ReadOnly==nil then +ReadOnly=false +end +local text=Text or"" +trigger.action.markToGroup(MarkID,MarkText,self:GetVec3(),MarkGroup:GetID(),ReadOnly,text) +return MarkID +end +function COORDINATE:RemoveMark(MarkID) +trigger.action.removeMark(MarkID) +end +function COORDINATE:LineToAll(Endpoint,Coalition,LineType,Color,Alpha,ReadOnly,Text) +local MarkID=UTILS.GetMarkID() +if ReadOnly==nil then +ReadOnly=false +end +local vec3=Endpoint:GetVec3() +Coalition=Coalition or-1 +Color=Color or{1,0,0} +Color[4]=Alpha or 1.0 +LineType=LineType or 1 +trigger.action.lineToAll(Coalition,MarkID,self:GetVec3(),vec3,Color,LineType,ReadOnly,Text or"") +return MarkID +end +function COORDINATE:CircleToAll(Radius,Coalition,LineType,Color,Alpha,FillColor,FillAlpha,ReadOnly,Text) +local MarkID=UTILS.GetMarkID() +if ReadOnly==nil then +ReadOnly=false +end +local vec3=self:GetVec3() +Radius=Radius or 1000 +Coalition=Coalition or-1 +Color=Color or{1,0,0} +Color[4]=Alpha or 1.0 +LineType=LineType or 1 +FillColor=FillColor or{1,0,0} +FillColor[4]=FillAlpha or 0.5 +trigger.action.circleToAll(Coalition,MarkID,vec3,Radius,Color,FillColor,LineType,ReadOnly,Text or"") +return MarkID +end +end +function COORDINATE:IsLOS(ToCoordinate,Offset) +Offset=Offset or 2 +local FromVec3=self:GetVec3() +FromVec3.y=FromVec3.y+Offset +local ToVec3=ToCoordinate:GetVec3() +ToVec3.y=ToVec3.y+Offset +local IsLOS=land.isVisible(FromVec3,ToVec3) +return IsLOS +end +function COORDINATE:IsInRadius(Coordinate,Radius) +local InVec2=self:GetVec2() +local Vec2=Coordinate:GetVec2() +local InRadius=UTILS.IsInRadius(InVec2,Vec2,Radius) +return InRadius +end +function COORDINATE:IsInSphere(Coordinate,Radius) +local InVec3=self:GetVec3() +local Vec3=Coordinate:GetVec3() +local InSphere=UTILS.IsInSphere(InVec3,Vec3,Radius) +return InSphere +end +function COORDINATE:GetSunriseAtDate(Day,Month,Year,InSeconds) +local DayOfYear=UTILS.GetDayOfYear(Year,Month,Day) +local Latitude,Longitude=self:GetLLDDM() +local Tdiff=UTILS.GMTToLocalTimeDifference() +local sunrise=UTILS.GetSunRiseAndSet(DayOfYear,Latitude,Longitude,true,Tdiff) +if InSeconds then +return sunrise +else +return UTILS.SecondsToClock(sunrise,true) +end +end +function COORDINATE:GetSunriseAtDayOfYear(DayOfYear,InSeconds) +local Latitude,Longitude=self:GetLLDDM() +local Tdiff=UTILS.GMTToLocalTimeDifference() +local sunrise=UTILS.GetSunRiseAndSet(DayOfYear,Latitude,Longitude,true,Tdiff) +if InSeconds then +return sunrise +else +return UTILS.SecondsToClock(sunrise,true) +end +end +function COORDINATE:GetSunrise(InSeconds) +local DayOfYear=UTILS.GetMissionDayOfYear() +local Latitude,Longitude=self:GetLLDDM() +local Tdiff=UTILS.GMTToLocalTimeDifference() +local sunrise=UTILS.GetSunRiseAndSet(DayOfYear,Latitude,Longitude,true,Tdiff) +local date=UTILS.GetDCSMissionDate() +if InSeconds then +return sunrise +else +return UTILS.SecondsToClock(sunrise,true) +end +end +function COORDINATE:GetMinutesToSunrise(OnlyToday) +local time=UTILS.SecondsOfToday() +local sunrise=nil +local delta=nil +if OnlyToday then +sunrise=self:GetSunrise(true) +delta=sunrise-time +else +local DayOfYear=UTILS.GetMissionDayOfYear()+1 +local Latitude,Longitude=self:GetLLDDM() +local Tdiff=UTILS.GMTToLocalTimeDifference() +sunrise=UTILS.GetSunRiseAndSet(DayOfYear,Latitude,Longitude,true,Tdiff) +delta=sunrise+UTILS.SecondsToMidnight() +end +return delta/60 +end +function COORDINATE:IsDay(Clock) +if Clock then +local Time=UTILS.ClockToSeconds(Clock) +local clock=UTILS.Split(Clock,"+")[1] +local DayOfYear=UTILS.GetMissionDayOfYear(Time) +local Latitude,Longitude=self:GetLLDDM() +local Tdiff=UTILS.GMTToLocalTimeDifference() +local sunrise=UTILS.GetSunRiseAndSet(DayOfYear,Latitude,Longitude,true,Tdiff) +local sunset=UTILS.GetSunRiseAndSet(DayOfYear,Latitude,Longitude,false,Tdiff) +local time=UTILS.ClockToSeconds(clock) +if time>sunrise and time<=sunset then +return true +else +return false +end +else +local sunrise=self:GetSunrise(true) +local sunset=self:GetSunset(true) +local time=UTILS.SecondsOfToday() +if time>sunrise and time<=sunset then +return true +else +return false +end +end +end +function COORDINATE:IsNight(Clock) +return not self:IsDay(Clock) +end +function COORDINATE:GetSunsetAtDate(Day,Month,Year,InSeconds) +local DayOfYear=UTILS.GetDayOfYear(Year,Month,Day) +local Latitude,Longitude=self:GetLLDDM() +local Tdiff=UTILS.GMTToLocalTimeDifference() +local sunset=UTILS.GetSunRiseAndSet(DayOfYear,Latitude,Longitude,false,Tdiff) +if InSeconds then +return sunset +else +return UTILS.SecondsToClock(sunset,true) +end +end +function COORDINATE:GetSunset(InSeconds) +local DayOfYear=UTILS.GetMissionDayOfYear() +local Latitude,Longitude=self:GetLLDDM() +local Tdiff=UTILS.GMTToLocalTimeDifference() +local sunrise=UTILS.GetSunRiseAndSet(DayOfYear,Latitude,Longitude,false,Tdiff) +local date=UTILS.GetDCSMissionDate() +if InSeconds then +return sunrise +else +return UTILS.SecondsToClock(sunrise,true) +end +end +function COORDINATE:GetMinutesToSunset(OnlyToday) +local time=UTILS.SecondsOfToday() +local sunset=nil +local delta=nil +if OnlyToday then +sunset=self:GetSunset(true) +delta=sunset-time +else +local DayOfYear=UTILS.GetMissionDayOfYear()+1 +local Latitude,Longitude=self:GetLLDDM() +local Tdiff=UTILS.GMTToLocalTimeDifference() +sunset=UTILS.GetSunRiseAndSet(DayOfYear,Latitude,Longitude,false,Tdiff) +delta=sunset+UTILS.SecondsToMidnight() +end +return delta/60 +end +function COORDINATE:ToStringBR(FromCoordinate,Settings) +local DirectionVec3=FromCoordinate:GetDirectionVec3(self) +local AngleRadians=self:GetAngleRadians(DirectionVec3) +local Distance=self:Get2DDistance(FromCoordinate) +return"BR, "..self:GetBRText(AngleRadians,Distance,Settings) +end +function COORDINATE:ToStringBRA(FromCoordinate,Settings,Language) +local DirectionVec3=FromCoordinate:GetDirectionVec3(self) +local AngleRadians=self:GetAngleRadians(DirectionVec3) +local Distance=FromCoordinate:Get2DDistance(self) +local Altitude=self:GetAltitudeText() +return"BRA, "..self:GetBRAText(AngleRadians,Distance,Settings,Language) +end +function COORDINATE:ToStringBULLS(Coalition,Settings) +local BullsCoordinate=COORDINATE:NewFromVec3(coalition.getMainRefPoint(Coalition)) +local DirectionVec3=BullsCoordinate:GetDirectionVec3(self) +local AngleRadians=self:GetAngleRadians(DirectionVec3) +local Distance=self:Get2DDistance(BullsCoordinate) +local Altitude=self:GetAltitudeText() +return"BULLS, "..self:GetBRText(AngleRadians,Distance,Settings) +end +function COORDINATE:ToStringAspect(TargetCoordinate) +local Heading=self.Heading +local DirectionVec3=self:GetDirectionVec3(TargetCoordinate) +local Angle=self:GetAngleDegrees(DirectionVec3) +if Heading then +local Aspect=Angle-Heading +if Aspect>-135 and Aspect<=-45 then +return"Flanking" +end +if Aspect>-45 and Aspect<=45 then +return"Hot" +end +if Aspect>45 and Aspect<=135 then +return"Flanking" +end +if Aspect>135 or Aspect<=-135 then +return"Cold" +end +end +return"" +end +function COORDINATE:GetLLDDM() +return coord.LOtoLL(self:GetVec3()) +end +function COORDINATE:ToStringLLDMS(Settings) +local LL_Accuracy=Settings and Settings.LL_Accuracy or _SETTINGS.LL_Accuracy +local lat,lon=coord.LOtoLL(self:GetVec3()) +return"LL DMS "..UTILS.tostringLL(lat,lon,LL_Accuracy,true) +end +function COORDINATE:ToStringLLDDM(Settings) +local LL_Accuracy=Settings and Settings.LL_Accuracy or _SETTINGS.LL_Accuracy +local lat,lon=coord.LOtoLL(self:GetVec3()) +return"LL DDM "..UTILS.tostringLL(lat,lon,LL_Accuracy,false) +end +function COORDINATE:ToStringMGRS(Settings) +local MGRS_Accuracy=Settings and Settings.MGRS_Accuracy or _SETTINGS.MGRS_Accuracy +local lat,lon=coord.LOtoLL(self:GetVec3()) +local MGRS=coord.LLtoMGRS(lat,lon) +return"MGRS "..UTILS.tostringMGRS(MGRS,MGRS_Accuracy) +end +function COORDINATE:ToStringFromRP(ReferenceCoord,ReferenceName,Controllable,Settings) +self:F2({ReferenceCoord=ReferenceCoord,ReferenceName=ReferenceName}) +local Settings=Settings or(Controllable and _DATABASE:GetPlayerSettings(Controllable:GetPlayerName()))or _SETTINGS +local IsAir=Controllable and Controllable:IsAirPlane()or false +if IsAir then +local DirectionVec3=ReferenceCoord:GetDirectionVec3(self) +local AngleRadians=self:GetAngleRadians(DirectionVec3) +local Distance=self:Get2DDistance(ReferenceCoord) +return"Targets are the last seen "..self:GetBRText(AngleRadians,Distance,Settings).." from "..ReferenceName +else +local DirectionVec3=ReferenceCoord:GetDirectionVec3(self) +local AngleRadians=self:GetAngleRadians(DirectionVec3) +local Distance=self:Get2DDistance(ReferenceCoord) +return"Target are located "..self:GetBRText(AngleRadians,Distance,Settings).." from "..ReferenceName +end +return nil +end +function COORDINATE:ToStringA2G(Controllable,Settings) +self:F2({Controllable=Controllable and Controllable:GetName()}) +local Settings=Settings or(Controllable and _DATABASE:GetPlayerSettings(Controllable:GetPlayerName()))or _SETTINGS +if Settings:IsA2G_BR()then +if Controllable then +local Coordinate=Controllable:GetCoordinate() +return Controllable and self:ToStringBR(Coordinate,Settings)or self:ToStringMGRS(Settings) +else +return self:ToStringMGRS(Settings) +end +end +if Settings:IsA2G_LL_DMS()then +return self:ToStringLLDMS(Settings) +end +if Settings:IsA2G_LL_DDM()then +return self:ToStringLLDDM(Settings) +end +if Settings:IsA2G_MGRS()then +return self:ToStringMGRS(Settings) +end +return nil +end +function COORDINATE:ToStringA2A(Controllable,Settings,Language) +self:F2({Controllable=Controllable and Controllable:GetName()}) +local Settings=Settings or(Controllable and _DATABASE:GetPlayerSettings(Controllable:GetPlayerName()))or _SETTINGS +if Settings:IsA2A_BRAA()then +if Controllable then +local Coordinate=Controllable:GetCoordinate() +return self:ToStringBRA(Coordinate,Settings,Language) +else +return self:ToStringMGRS(Settings,Language) +end +end +if Settings:IsA2A_BULLS()then +local Coalition=Controllable:GetCoalition() +return self:ToStringBULLS(Coalition,Settings,Language) +end +if Settings:IsA2A_LL_DMS()then +return self:ToStringLLDMS(Settings,Language) +end +if Settings:IsA2A_LL_DDM()then +return self:ToStringLLDDM(Settings,Language) +end +if Settings:IsA2A_MGRS()then +return self:ToStringMGRS(Settings,Language) +end +return nil +end +function COORDINATE:ToString(Controllable,Settings,Task) +local Settings=Settings or(Controllable and _DATABASE:GetPlayerSettings(Controllable:GetPlayerName()))or _SETTINGS +local ModeA2A=nil +if Task then +if Task:IsInstanceOf(TASK_A2A)then +ModeA2A=true +else +if Task:IsInstanceOf(TASK_A2G)then +ModeA2A=false +else +if Task:IsInstanceOf(TASK_CARGO)then +ModeA2A=false +end +if Task:IsInstanceOf(TASK_CAPTURE_ZONE)then +ModeA2A=false +end +end +end +end +if ModeA2A==nil then +local IsAir=Controllable and(Controllable:IsAirPlane()or Controllable:IsHelicopter())or false +if IsAir then +ModeA2A=true +else +ModeA2A=false +end +end +if ModeA2A==true then +return self:ToStringA2A(Controllable,Settings) +else +return self:ToStringA2G(Controllable,Settings) +end +return nil +end +function COORDINATE:ToStringPressure(Controllable,Settings) +self:F2({Controllable=Controllable and Controllable:GetName()}) +local Settings=Settings or(Controllable and _DATABASE:GetPlayerSettings(Controllable:GetPlayerName()))or _SETTINGS +return self:GetPressureText(nil,Settings) +end +function COORDINATE:ToStringWind(Controllable,Settings) +self:F2({Controllable=Controllable and Controllable:GetName()}) +local Settings=Settings or(Controllable and _DATABASE:GetPlayerSettings(Controllable:GetPlayerName()))or _SETTINGS +return self:GetWindText(nil,Settings) +end +function COORDINATE:ToStringTemperature(Controllable,Settings) +self:F2({Controllable=Controllable and Controllable:GetName()}) +local Settings=Settings or(Controllable and _DATABASE:GetPlayerSettings(Controllable:GetPlayerName()))or _SETTINGS +return self:GetTemperatureText(nil,Settings) +end +end +do +POINT_VEC3={ +ClassName="POINT_VEC3", +Metric=true, +RoutePointAltType={ +BARO="BARO", +}, +RoutePointType={ +TakeOffParking="TakeOffParking", +TurningPoint="Turning Point", +}, +RoutePointAction={ +FromParkingArea="From Parking Area", +TurningPoint="Turning Point", +}, +} +function POINT_VEC3:New(x,y,z) +local self=BASE:Inherit(self,COORDINATE:New(x,y,z)) +self:F2(self) +return self +end +function POINT_VEC3:NewFromVec2(Vec2,LandHeightAdd) +local self=BASE:Inherit(self,COORDINATE:NewFromVec2(Vec2,LandHeightAdd)) +self:F2(self) +return self +end +function POINT_VEC3:NewFromVec3(Vec3) +local self=BASE:Inherit(self,COORDINATE:NewFromVec3(Vec3)) +self:F2(self) +return self +end +function POINT_VEC3:GetX() +return self.x +end +function POINT_VEC3:GetY() +return self.y +end +function POINT_VEC3:GetZ() +return self.z +end +function POINT_VEC3:SetX(x) +self.x=x +return self +end +function POINT_VEC3:SetY(y) +self.y=y +return self +end +function POINT_VEC3:SetZ(z) +self.z=z +return self +end +function POINT_VEC3:AddX(x) +self.x=self.x+x +return self +end +function POINT_VEC3:AddY(y) +self.y=self.y+y +return self +end +function POINT_VEC3:AddZ(z) +self.z=self.z+z +return self +end +function POINT_VEC3:GetRandomPointVec3InRadius(OuterRadius,InnerRadius) +return POINT_VEC3:NewFromVec3(self:GetRandomVec3InRadius(OuterRadius,InnerRadius)) +end +end +do +POINT_VEC2={ +ClassName="POINT_VEC2", +} +function POINT_VEC2:New(x,y,LandHeightAdd) +local LandHeight=land.getHeight({["x"]=x,["y"]=y}) +LandHeightAdd=LandHeightAdd or 0 +LandHeight=LandHeight+LandHeightAdd +local self=BASE:Inherit(self,COORDINATE:New(x,LandHeight,y)) +self:F2(self) +return self +end +function POINT_VEC2:NewFromVec2(Vec2,LandHeightAdd) +local LandHeight=land.getHeight(Vec2) +LandHeightAdd=LandHeightAdd or 0 +LandHeight=LandHeight+LandHeightAdd +local self=BASE:Inherit(self,COORDINATE:NewFromVec2(Vec2,LandHeightAdd)) +self:F2(self) +return self +end +function POINT_VEC2:NewFromVec3(Vec3) +local self=BASE:Inherit(self,COORDINATE:NewFromVec3(Vec3)) +self:F2(self) +return self +end +function POINT_VEC2:GetX() +return self.x +end +function POINT_VEC2:GetY() +return self.z +end +function POINT_VEC2:SetX(x) +self.x=x +return self +end +function POINT_VEC2:SetY(y) +self.z=y +return self +end +function POINT_VEC2:GetLat() +return self.x +end +function POINT_VEC2:SetLat(x) +self.x=x +return self +end +function POINT_VEC2:GetLon() +return self.z +end +function POINT_VEC2:SetLon(z) +self.z=z +return self +end +function POINT_VEC2:GetAlt() +return self.y~=0 or land.getHeight({x=self.x,y=self.z}) +end +function POINT_VEC2:SetAlt(Altitude) +self.y=Altitude or land.getHeight({x=self.x,y=self.z}) +return self +end +function POINT_VEC2:AddX(x) +self.x=self.x+x +return self +end +function POINT_VEC2:AddY(y) +self.z=self.z+y +return self +end +function POINT_VEC2:AddAlt(Altitude) +self.y=land.getHeight({x=self.x,y=self.z})+Altitude or 0 +return self +end +function POINT_VEC2:GetRandomPointVec2InRadius(OuterRadius,InnerRadius) +self:F2({OuterRadius,InnerRadius}) +return POINT_VEC2:NewFromVec2(self:GetRandomVec2InRadius(OuterRadius,InnerRadius)) +end +function POINT_VEC2:DistanceFromPointVec2(PointVec2Reference) +self:F2(PointVec2Reference) +local Distance=((PointVec2Reference.x-self.x)^2+(PointVec2Reference.z-self.z)^2)^0.5 +self:T2(Distance) +return Distance +end +end +do +VELOCITY={ +ClassName="VELOCITY", +} +function VELOCITY:New(VelocityMps) +local self=BASE:Inherit(self,BASE:New()) +self:F({}) +self.Velocity=VelocityMps +return self +end +function VELOCITY:Set(VelocityMps) +self.Velocity=VelocityMps +return self +end +function VELOCITY:Get() +return self.Velocity +end +function VELOCITY:SetKmph(VelocityKmph) +self.Velocity=UTILS.KmphToMps(VelocityKmph) +return self +end +function VELOCITY:GetKmph() +return UTILS.MpsToKmph(self.Velocity) +end +function VELOCITY:SetMiph(VelocityMiph) +self.Velocity=UTILS.MiphToMps(VelocityMiph) +return self +end +function VELOCITY:GetMiph() +return UTILS.MpsToMiph(self.Velocity) +end +function VELOCITY:GetText(Settings) +local Settings=Settings or _SETTINGS +if self.Velocity~=0 then +if Settings:IsMetric()then +return string.format("%d km/h",UTILS.MpsToKmph(self.Velocity)) +else +return string.format("%d mi/h",UTILS.MpsToMiph(self.Velocity)) +end +else +return"stationary" +end +end +function VELOCITY:ToString(VelocityGroup,Settings) +self:F({Group=VelocityGroup and VelocityGroup:GetName()}) +local Settings=Settings or(VelocityGroup and _DATABASE:GetPlayerSettings(VelocityGroup:GetPlayerName()))or _SETTINGS +return self:GetText(Settings) +end +end +do +VELOCITY_POSITIONABLE={ +ClassName="VELOCITY_POSITIONABLE", +} +function VELOCITY_POSITIONABLE:New(Positionable) +local self=BASE:Inherit(self,VELOCITY:New()) +self:F({}) +self.Positionable=Positionable +return self +end +function VELOCITY_POSITIONABLE:Get() +return self.Positionable:GetVelocityMPS()or 0 +end +function VELOCITY_POSITIONABLE:GetKmph() +return UTILS.MpsToKmph(self.Positionable:GetVelocityMPS()or 0) +end +function VELOCITY_POSITIONABLE:GetMiph() +return UTILS.MpsToMiph(self.Positionable:GetVelocityMPS()or 0) +end +function VELOCITY_POSITIONABLE:ToString() +self:F({Group=self.Positionable and self.Positionable:GetName()}) +local Settings=Settings or(self.Positionable and _DATABASE:GetPlayerSettings(self.Positionable:GetPlayerName()))or _SETTINGS +self.Velocity=self.Positionable:GetVelocityMPS() +return self:GetText(Settings) +end +end +MESSAGE={ +ClassName="MESSAGE", +MessageCategory=0, +MessageID=0, +} +MESSAGE.Type={ +Update="Update", +Information="Information", +Briefing="Briefing Report", +Overview="Overview Report", +Detailed="Detailed Report" +} +function MESSAGE:New(MessageText,MessageDuration,MessageCategory,ClearScreen) +local self=BASE:Inherit(self,BASE:New()) +self:F({MessageText,MessageDuration,MessageCategory}) +self.MessageType=nil +if MessageCategory and MessageCategory~=""then +if MessageCategory:sub(-1)~="\n"then +self.MessageCategory=MessageCategory..": " +else +self.MessageCategory=MessageCategory:sub(1,-2)..":\n" +end +else +self.MessageCategory="" +end +self.ClearScreen=false +if ClearScreen~=nil then +self.ClearScreen=ClearScreen +end +self.MessageDuration=MessageDuration or 5 +self.MessageTime=timer.getTime() +self.MessageText=MessageText:gsub("^\n","",1):gsub("\n$","",1) +self.MessageSent=false +self.MessageGroup=false +self.MessageCoalition=false +return self +end +function MESSAGE:NewType(MessageText,MessageType,ClearScreen) +local self=BASE:Inherit(self,BASE:New()) +self:F({MessageText}) +self.MessageType=MessageType +self.ClearScreen=false +if ClearScreen~=nil then +self.ClearScreen=ClearScreen +end +self.MessageTime=timer.getTime() +self.MessageText=MessageText:gsub("^\n","",1):gsub("\n$","",1) +return self +end +function MESSAGE:Clear() +self:F() +self.ClearScreen=true +return self +end +function MESSAGE:ToClient(Client,Settings) +self:F(Client) +if Client and Client:GetClientGroupID()then +if self.MessageType then +local Settings=Settings or(Client and _DATABASE:GetPlayerSettings(Client:GetPlayerName()))or _SETTINGS +self.MessageDuration=Settings:GetMessageTime(self.MessageType) +self.MessageCategory="" +end +if self.MessageDuration~=0 then +local ClientGroupID=Client:GetClientGroupID() +self:T(self.MessageCategory..self.MessageText:gsub("\n$",""):gsub("\n$","").." / "..self.MessageDuration) +trigger.action.outTextForGroup(ClientGroupID,self.MessageCategory..self.MessageText:gsub("\n$",""):gsub("\n$",""),self.MessageDuration,self.ClearScreen) +end +end +return self +end +function MESSAGE:ToGroup(Group,Settings) +self:F(Group.GroupName) +if Group then +if self.MessageType then +local Settings=Settings or(Group and _DATABASE:GetPlayerSettings(Group:GetPlayerName()))or _SETTINGS +self.MessageDuration=Settings:GetMessageTime(self.MessageType) +self.MessageCategory="" +end +if self.MessageDuration~=0 then +self:T(self.MessageCategory..self.MessageText:gsub("\n$",""):gsub("\n$","").." / "..self.MessageDuration) +trigger.action.outTextForGroup(Group:GetID(),self.MessageCategory..self.MessageText:gsub("\n$",""):gsub("\n$",""),self.MessageDuration,self.ClearScreen) +end +end +return self +end +function MESSAGE:ToBlue() +self:F() +self:ToCoalition(coalition.side.BLUE) +return self +end +function MESSAGE:ToRed() +self:F() +self:ToCoalition(coalition.side.RED) +return self +end +function MESSAGE:ToCoalition(CoalitionSide,Settings) +self:F(CoalitionSide) +if self.MessageType then +local Settings=Settings or _SETTINGS +self.MessageDuration=Settings:GetMessageTime(self.MessageType) +self.MessageCategory="" +end +if CoalitionSide then +if self.MessageDuration~=0 then +self:T(self.MessageCategory..self.MessageText:gsub("\n$",""):gsub("\n$","").." / "..self.MessageDuration) +trigger.action.outTextForCoalition(CoalitionSide,self.MessageText:gsub("\n$",""):gsub("\n$",""),self.MessageDuration,self.ClearScreen) +end +end +return self +end +function MESSAGE:ToCoalitionIf(CoalitionSide,Condition) +self:F(CoalitionSide) +if Condition and Condition==true then +self:ToCoalition(CoalitionSide) +end +return self +end +function MESSAGE:ToAll(Settings) +self:F() +if self.MessageType then +local Settings=Settings or _SETTINGS +self.MessageDuration=Settings:GetMessageTime(self.MessageType) +self.MessageCategory="" +end +if self.MessageDuration~=0 then +self:T(self.MessageCategory..self.MessageText:gsub("\n$",""):gsub("\n$","").." / "..self.MessageDuration) +trigger.action.outText(self.MessageCategory..self.MessageText:gsub("\n$",""):gsub("\n$",""),self.MessageDuration,self.ClearScreen) +end +return self +end +function MESSAGE:ToAllIf(Condition) +if Condition and Condition==true then +self:ToAll() +end +return self +end +do +FSM={ +ClassName="FSM", +} +function FSM:New() +self=BASE:Inherit(self,BASE:New()) +self.options=options or{} +self.options.subs=self.options.subs or{} +self.current=self.options.initial or'none' +self.Events={} +self.subs={} +self.endstates={} +self.Scores={} +self._StartState="none" +self._Transitions={} +self._Processes={} +self._EndStates={} +self._Scores={} +self._EventSchedules={} +self.CallScheduler=SCHEDULER:New(self) +return self +end +function FSM:SetStartState(State) +self._StartState=State +self.current=State +end +function FSM:GetStartState() +return self._StartState or{} +end +function FSM:AddTransition(From,Event,To) +local Transition={} +Transition.From=From +Transition.Event=Event +Transition.To=To +self:T2(Transition) +self._Transitions[Transition]=Transition +self:_eventmap(self.Events,Transition) +end +function FSM:GetTransitions() +return self._Transitions or{} +end +function FSM:AddProcess(From,Event,Process,ReturnEvents) +self:T({From,Event}) +local Sub={} +Sub.From=From +Sub.Event=Event +Sub.fsm=Process +Sub.StartEvent="Start" +Sub.ReturnEvents=ReturnEvents +self._Processes[Sub]=Sub +self:_submap(self.subs,Sub,nil) +self:AddTransition(From,Event,From) +return Process +end +function FSM:GetProcesses() +self:F({Processes=self._Processes}) +return self._Processes or{} +end +function FSM:GetProcess(From,Event) +for ProcessID,Process in pairs(self:GetProcesses())do +if Process.From==From and Process.Event==Event then +return Process.fsm +end +end +error("Sub-Process from state "..From.." with event "..Event.." not found!") +end +function FSM:SetProcess(From,Event,Fsm) +for ProcessID,Process in pairs(self:GetProcesses())do +if Process.From==From and Process.Event==Event then +Process.fsm=Fsm +return true +end +end +error("Sub-Process from state "..From.." with event "..Event.." not found!") +end +function FSM:AddEndState(State) +self._EndStates[State]=State +self.endstates[State]=State +end +function FSM:GetEndStates() +return self._EndStates or{} +end +function FSM:AddScore(State,ScoreText,Score) +self:F({State,ScoreText,Score}) +self._Scores[State]=self._Scores[State]or{} +self._Scores[State].ScoreText=ScoreText +self._Scores[State].Score=Score +return self +end +function FSM:AddScoreProcess(From,Event,State,ScoreText,Score) +self:F({From,Event,State,ScoreText,Score}) +local Process=self:GetProcess(From,Event) +Process._Scores[State]=Process._Scores[State]or{} +Process._Scores[State].ScoreText=ScoreText +Process._Scores[State].Score=Score +self:T(Process._Scores) +return Process +end +function FSM:GetScores() +return self._Scores or{} +end +function FSM:GetSubs() +return self.options.subs +end +function FSM:LoadCallBacks(CallBackTable) +for name,callback in pairs(CallBackTable or{})do +self[name]=callback +end +end +function FSM:_eventmap(Events,EventStructure) +local Event=EventStructure.Event +local __Event="__"..EventStructure.Event +self[Event]=self[Event]or self:_create_transition(Event) +self[__Event]=self[__Event]or self:_delayed_transition(Event) +self:T2("Added methods: "..Event..", "..__Event) +Events[Event]=self.Events[Event]or{map={}} +self:_add_to_map(Events[Event].map,EventStructure) +end +function FSM:_submap(subs,sub,name) +subs[sub.From]=subs[sub.From]or{} +subs[sub.From][sub.Event]=subs[sub.From][sub.Event]or{} +subs[sub.From][sub.Event][sub]={} +subs[sub.From][sub.Event][sub].fsm=sub.fsm +subs[sub.From][sub.Event][sub].StartEvent=sub.StartEvent +subs[sub.From][sub.Event][sub].ReturnEvents=sub.ReturnEvents or{} +subs[sub.From][sub.Event][sub].name=name +subs[sub.From][sub.Event][sub].fsmparent=self +end +function FSM:_call_handler(step,trigger,params,EventName) +local handler=step..trigger +if self[handler]then +self._EventSchedules[EventName]=nil +local ErrorHandler=function(errmsg) +env.info("Error in SCHEDULER function:"..errmsg) +if BASE.Debug~=nil then +env.info(BASE.Debug.traceback()) +end +return errmsg +end +local Result,Value=xpcall(function()return self[handler](self,unpack(params))end,ErrorHandler) +return Value +end +end +function FSM._handler(self,EventName,...) +local Can,To=self:can(EventName) +if To=="*"then +To=self.current +end +if Can then +local From=self.current +local Params={From,EventName,To,...} +if self["onleave"..From]or +self["OnLeave"..From]or +self["onbefore"..EventName]or +self["OnBefore"..EventName]or +self["onafter"..EventName]or +self["OnAfter"..EventName]or +self["onenter"..To]or +self["OnEnter"..To]then +if self:_call_handler("onbefore",EventName,Params,EventName)==false then +self:T("*** FSM *** Cancel".." *** "..self.current.." --> "..EventName.." --> "..To.." *** onbefore"..EventName) +return false +else +if self:_call_handler("OnBefore",EventName,Params,EventName)==false then +self:T("*** FSM *** Cancel".." *** "..self.current.." --> "..EventName.." --> "..To.." *** OnBefore"..EventName) +return false +else +if self:_call_handler("onleave",From,Params,EventName)==false then +self:T("*** FSM *** Cancel".." *** "..self.current.." --> "..EventName.." --> "..To.." *** onleave"..From) +return false +else +if self:_call_handler("OnLeave",From,Params,EventName)==false then +self:T("*** FSM *** Cancel".." *** "..self.current.." --> "..EventName.." --> "..To.." *** OnLeave"..From) +return false +end +end +end +end +else +local ClassName=self:GetClassName() +if ClassName=="FSM"then +self:T("*** FSM *** Transit *** "..self.current.." --> "..EventName.." --> "..To) +end +if ClassName=="FSM_TASK"then +self:T("*** FSM *** Transit *** "..self.current.." --> "..EventName.." --> "..To.." *** Task: "..self.TaskName) +end +if ClassName=="FSM_CONTROLLABLE"then +self:T("*** FSM *** Transit *** "..self.current.." --> "..EventName.." --> "..To.." *** TaskUnit: "..self.Controllable.ControllableName.." *** ") +end +if ClassName=="FSM_PROCESS"then +self:T("*** FSM *** Transit *** "..self.current.." --> "..EventName.." --> "..To.." *** Task: "..self.Task:GetName()..", TaskUnit: "..self.Controllable.ControllableName.." *** ") +end +end +self.current=To +local execute=true +local subtable=self:_gosub(From,EventName) +for _,sub in pairs(subtable)do +self:T("*** FSM *** Sub *** "..sub.StartEvent) +sub.fsm.fsmparent=self +sub.fsm.ReturnEvents=sub.ReturnEvents +sub.fsm[sub.StartEvent](sub.fsm) +execute=false +end +local fsmparent,Event=self:_isendstate(To) +if fsmparent and Event then +self:T("*** FSM *** End *** "..Event) +self:_call_handler("onenter",To,Params,EventName) +self:_call_handler("OnEnter",To,Params,EventName) +self:_call_handler("onafter",EventName,Params,EventName) +self:_call_handler("OnAfter",EventName,Params,EventName) +self:_call_handler("onstate","change",Params,EventName) +fsmparent[Event](fsmparent) +execute=false +end +if execute then +self:_call_handler("onafter",EventName,Params,EventName) +self:_call_handler("OnAfter",EventName,Params,EventName) +self:_call_handler("onenter",To,Params,EventName) +self:_call_handler("OnEnter",To,Params,EventName) +self:_call_handler("onstate","change",Params,EventName) +end +else +self:T("*** FSM *** NO Transition *** "..self.current.." --> "..EventName.." --> ? ") +end +return nil +end +function FSM:_delayed_transition(EventName) +return function(self,DelaySeconds,...) +self:T2("Delayed Event: "..EventName) +local CallID=0 +if DelaySeconds~=nil then +if DelaySeconds<0 then +DelaySeconds=math.abs(DelaySeconds) +if not self._EventSchedules[EventName]then +CallID=self.CallScheduler:Schedule(self,self._handler,{EventName,...},DelaySeconds or 1,nil,nil,nil,4,true) +self._EventSchedules[EventName]=CallID +self:T2(string.format("NEGATIVE Event %s delayed by %.1f sec SCHEDULED with CallID=%s",EventName,DelaySeconds,tostring(CallID))) +else +self:T2(string.format("NEGATIVE Event %s delayed by %.1f sec CANCELLED as we already have such an event in the queue.",EventName,DelaySeconds)) +end +else +CallID=self.CallScheduler:Schedule(self,self._handler,{EventName,...},DelaySeconds or 1,nil,nil,nil,4,true) +self:T2(string.format("Event %s delayed by %.1f sec SCHEDULED with CallID=%s",EventName,DelaySeconds,tostring(CallID))) +end +else +error("FSM: An asynchronous event trigger requires a DelaySeconds parameter!!! This can be positive or negative! Sorry, but will not process this.") +end +self:T2({CallID=CallID}) +end +end +function FSM:_create_transition(EventName) +return function(self,...)return self._handler(self,EventName,...)end +end +function FSM:_gosub(ParentFrom,ParentEvent) +local fsmtable={} +if self.subs[ParentFrom]and self.subs[ParentFrom][ParentEvent]then +self:T({ParentFrom,ParentEvent,self.subs[ParentFrom],self.subs[ParentFrom][ParentEvent]}) +return self.subs[ParentFrom][ParentEvent] +else +return{} +end +end +function FSM:_isendstate(Current) +local FSMParent=self.fsmparent +if FSMParent and self.endstates[Current]then +FSMParent.current=Current +local ParentFrom=FSMParent.current +local Event=self.ReturnEvents[Current] +if Event then +return FSMParent,Event +else +end +end +return nil +end +function FSM:_add_to_map(Map,Event) +self:F3({Map,Event}) +if type(Event.From)=='string'then +Map[Event.From]=Event.To +else +for _,From in ipairs(Event.From)do +Map[From]=Event.To +end +end +self:T3({Map,Event}) +end +function FSM:GetState() +return self.current +end +function FSM:GetCurrentState() +return self.current +end +function FSM:Is(State) +return self.current==State +end +function FSM:is(state) +return self.current==state +end +function FSM:can(e) +local Event=self.Events[e] +local To=Event and Event.map[self.current]or Event.map['*'] +return To~=nil,To +end +function FSM:cannot(e) +return not self:can(e) +end +end +do +FSM_CONTROLLABLE={ +ClassName="FSM_CONTROLLABLE", +} +function FSM_CONTROLLABLE:New(Controllable) +local self=BASE:Inherit(self,FSM:New()) +if Controllable then +self:SetControllable(Controllable) +end +self:AddTransition("*","Stop","Stopped") +return self +end +function FSM_CONTROLLABLE:OnAfterStop(Controllable,From,Event,To) +self.CallScheduler:Clear() +end +function FSM_CONTROLLABLE:SetControllable(FSMControllable) +self.Controllable=FSMControllable +end +function FSM_CONTROLLABLE:GetControllable() +return self.Controllable +end +function FSM_CONTROLLABLE:_call_handler(step,trigger,params,EventName) +local handler=step..trigger +local ErrorHandler=function(errmsg) +env.info("Error in SCHEDULER function:"..errmsg) +if BASE.Debug~=nil then +env.info(BASE.Debug.traceback()) +end +return errmsg +end +if self[handler]then +self:T("*** FSM *** "..step.." *** "..params[1].." --> "..params[2].." --> "..params[3].." *** TaskUnit: "..self.Controllable:GetName()) +self._EventSchedules[EventName]=nil +local Result,Value=xpcall(function()return self[handler](self,self.Controllable,unpack(params))end,ErrorHandler) +return Value +end +end +end +do +FSM_PROCESS={ +ClassName="FSM_PROCESS", +} +function FSM_PROCESS:New(Controllable,Task) +local self=BASE:Inherit(self,FSM_CONTROLLABLE:New()) +self:Assign(Controllable,Task) +return self +end +function FSM_PROCESS:Init(FsmProcess) +self:T("No Initialisation") +end +function FSM_PROCESS:_call_handler(step,trigger,params,EventName) +local handler=step..trigger +local ErrorHandler=function(errmsg) +env.info("Error in FSM_PROCESS call handler:"..errmsg) +if BASE.Debug~=nil then +env.info(BASE.Debug.traceback()) +end +return errmsg +end +if self[handler]then +if handler~="onstatechange"then +self:T("*** FSM *** "..step.." *** "..params[1].." --> "..params[2].." --> "..params[3].." *** Task: "..self.Task:GetName()..", TaskUnit: "..self.Controllable:GetName()) +end +self._EventSchedules[EventName]=nil +local Result,Value +if self.Controllable and self.Controllable:IsAlive()==true then +Result,Value=xpcall(function()return self[handler](self,self.Controllable,self.Task,unpack(params))end,ErrorHandler) +end +return Value +end +end +function FSM_PROCESS:Copy(Controllable,Task) +self:T({self:GetClassNameAndID()}) +local NewFsm=self:New(Controllable,Task) +NewFsm:Assign(Controllable,Task) +NewFsm:Init(self) +NewFsm:SetStartState(self:GetStartState()) +for TransitionID,Transition in pairs(self:GetTransitions())do +NewFsm:AddTransition(Transition.From,Transition.Event,Transition.To) +end +for ProcessID,Process in pairs(self:GetProcesses())do +local FsmProcess=NewFsm:AddProcess(Process.From,Process.Event,Process.fsm:Copy(Controllable,Task),Process.ReturnEvents) +end +for EndStateID,EndState in pairs(self:GetEndStates())do +self:T(EndState) +NewFsm:AddEndState(EndState) +end +for ScoreID,Score in pairs(self:GetScores())do +self:T(Score) +NewFsm:AddScore(ScoreID,Score.ScoreText,Score.Score) +end +return NewFsm +end +function FSM_PROCESS:Remove() +self:F({self:GetClassNameAndID()}) +self:F("Clearing Schedules") +self.CallScheduler:Clear() +for ProcessID,Process in pairs(self:GetProcesses())do +if Process.fsm then +Process.fsm:Remove() +Process.fsm=nil +end +end +return self +end +function FSM_PROCESS:SetTask(Task) +self.Task=Task +return self +end +function FSM_PROCESS:GetTask() +return self.Task +end +function FSM_PROCESS:GetMission() +return self.Task.Mission +end +function FSM_PROCESS:GetCommandCenter() +return self:GetTask():GetMission():GetCommandCenter() +end +function FSM_PROCESS:Message(Message) +self:F({Message=Message}) +local CC=self:GetCommandCenter() +local TaskGroup=self.Controllable:GetGroup() +local PlayerName=self.Controllable:GetPlayerName() +PlayerName=PlayerName and" ("..PlayerName..")"or"" +local Callsign=self.Controllable:GetCallsign() +local Prefix=Callsign and" @ "..Callsign..PlayerName or"" +Message=Prefix..": "..Message +CC:MessageToGroup(Message,TaskGroup) +end +function FSM_PROCESS:Assign(ProcessUnit,Task) +self:SetControllable(ProcessUnit) +self:SetTask(Task) +return self +end +function FSM_PROCESS:onenterFailed(ProcessUnit,Task,From,Event,To) +self:T("*** FSM *** Failed *** "..Task:GetName().."/"..ProcessUnit:GetName().." *** "..From.." --> "..Event.." --> "..To) +self.Task:Fail() +end +function FSM_PROCESS:onstatechange(ProcessUnit,Task,From,Event,To) +if From~=To then +self:T("*** FSM *** Change *** "..Task:GetName().."/"..ProcessUnit:GetName().." *** "..From.." --> "..Event.." --> "..To) +end +if self._Scores[To]then +local Task=self.Task +local Scoring=Task:GetScoring() +if Scoring then +Scoring:_AddMissionTaskScore(Task.Mission,ProcessUnit,self._Scores[To].ScoreText,self._Scores[To].Score) +end +end +end +end +do +FSM_TASK={ +ClassName="FSM_TASK", +} +function FSM_TASK:New(TaskName) +local self=BASE:Inherit(self,FSM_CONTROLLABLE:New()) +self["onstatechange"]=self.OnStateChange +self.TaskName=TaskName +return self +end +function FSM_TASK:_call_handler(step,trigger,params,EventName) +local handler=step..trigger +local ErrorHandler=function(errmsg) +env.info("Error in SCHEDULER function:"..errmsg) +if BASE.Debug~=nil then +env.info(BASE.Debug.traceback()) +end +return errmsg +end +if self[handler]then +self:T("*** FSM *** "..step.." *** "..params[1].." --> "..params[2].." --> "..params[3].." *** Task: "..self.TaskName) +self._EventSchedules[EventName]=nil +local Result,Value=xpcall(function()return self[handler](self,unpack(params))end,ErrorHandler) +return Value +end +end +end +do +FSM_SET={ +ClassName="FSM_SET", +} +function FSM_SET:New(FSMSet) +self=BASE:Inherit(self,FSM:New()) +if FSMSet then +self:Set(FSMSet) +end +return self +end +function FSM_SET:Set(FSMSet) +self:F(FSMSet) +self.Set=FSMSet +end +function FSM_SET:Get() +return self.Controllable +end +function FSM_SET:_call_handler(step,trigger,params,EventName) +local handler=step..trigger +if self[handler]then +self:T("*** FSM *** "..step.." *** "..params[1].." --> "..params[2].." --> "..params[3]) +self._EventSchedules[EventName]=nil +return self[handler](self,self.Set,unpack(params)) +end +end +end +RADIO={ +ClassName="RADIO", +FileName="", +Frequency=0, +Modulation=radio.modulation.AM, +Subtitle="", +SubtitleDuration=0, +Power=100, +Loop=false, +alias=nil, +} +function RADIO:New(Positionable) +local self=BASE:Inherit(self,BASE:New()) +self:F(Positionable) +if Positionable:GetPointVec2()then +self.Positionable=Positionable +return self +end +self:E({error="The passed positionable is invalid, no RADIO created!",positionable=Positionable}) +return nil +end +function RADIO:SetAlias(alias) +self.alias=tostring(alias) +return self +end +function RADIO:GetAlias() +return tostring(self.alias) +end +function RADIO:SetFileName(FileName) +self:F2(FileName) +if type(FileName)=="string"then +if FileName:find(".ogg")or FileName:find(".wav")then +if not FileName:find("l10n/DEFAULT/")then +FileName="l10n/DEFAULT/"..FileName +end +self.FileName=FileName +return self +end +end +self:E({"File name invalid. Maybe something wrong with the extension?",FileName}) +return self +end +function RADIO:SetFrequency(Frequency) +self:F2(Frequency) +if type(Frequency)=="number"then +if(Frequency>=30 and Frequency<=87.995)or(Frequency>=108 and Frequency<=173.995)or(Frequency>=225 and Frequency<=399.975)then +self.Frequency=Frequency*1000000 +if self.Positionable.ClassName=="UNIT"or self.Positionable.ClassName=="GROUP"then +local commandSetFrequency={ +id="SetFrequency", +params={ +frequency=self.Frequency, +modulation=self.Modulation, +} +} +self:T2(commandSetFrequency) +self.Positionable:SetCommand(commandSetFrequency) +end +return self +end +end +self:E({"Frequency is outside of DCS Frequency ranges (30-80, 108-152, 225-400). Frequency unchanged.",Frequency}) +return self +end +function RADIO:SetModulation(Modulation) +self:F2(Modulation) +if type(Modulation)=="number"then +if Modulation==radio.modulation.AM or Modulation==radio.modulation.FM then +self.Modulation=Modulation +return self +end +end +self:E({"Modulation is invalid. Use DCS's enum radio.modulation. Modulation unchanged.",self.Modulation}) +return self +end +function RADIO:SetPower(Power) +self:F2(Power) +if type(Power)=="number"then +self.Power=math.floor(math.abs(Power)) +else +self:E({"Power is invalid. Power unchanged.",self.Power}) +end +return self +end +function RADIO:SetLoop(Loop) +self:F2(Loop) +if type(Loop)=="boolean"then +self.Loop=Loop +return self +end +self:E({"Loop is invalid. Loop unchanged.",self.Loop}) +return self +end +function RADIO:SetSubtitle(Subtitle,SubtitleDuration) +self:F2({Subtitle,SubtitleDuration}) +if type(Subtitle)=="string"then +self.Subtitle=Subtitle +else +self.Subtitle="" +self:E({"Subtitle is invalid. Subtitle reset.",self.Subtitle}) +end +if type(SubtitleDuration)=="number"then +self.SubtitleDuration=SubtitleDuration +else +self.SubtitleDuration=0 +self:E({"SubtitleDuration is invalid. SubtitleDuration reset.",self.SubtitleDuration}) +end +return self +end +function RADIO:NewGenericTransmission(FileName,Frequency,Modulation,Power,Loop) +self:F({FileName,Frequency,Modulation,Power}) +self:SetFileName(FileName) +if Frequency then self:SetFrequency(Frequency)end +if Modulation then self:SetModulation(Modulation)end +if Power then self:SetPower(Power)end +if Loop then self:SetLoop(Loop)end +return self +end +function RADIO:NewUnitTransmission(FileName,Subtitle,SubtitleDuration,Frequency,Modulation,Loop) +self:F({FileName,Subtitle,SubtitleDuration,Frequency,Modulation,Loop}) +self:SetFileName(FileName) +if Modulation then +self:SetModulation(Modulation) +end +if Frequency then +self:SetFrequency(Frequency) +end +if Subtitle then +self:SetSubtitle(Subtitle,SubtitleDuration or 0) +end +if Loop then +self:SetLoop(Loop) +end +return self +end +function RADIO:Broadcast(viatrigger) +self:F({viatrigger=viatrigger}) +if(self.Positionable.ClassName=="UNIT"or self.Positionable.ClassName=="GROUP")and(not viatrigger)then +self:T("Broadcasting from a UNIT or a GROUP") +local commandTransmitMessage={ +id="TransmitMessage", +params={ +file=self.FileName, +duration=self.SubtitleDuration, +subtitle=self.Subtitle, +loop=self.Loop, +}} +self:T3(commandTransmitMessage) +self.Positionable:SetCommand(commandTransmitMessage) +else +self:T("Broadcasting from a POSITIONABLE") +trigger.action.radioTransmission(self.FileName,self.Positionable:GetPositionVec3(),self.Modulation,self.Loop,self.Frequency,self.Power,tostring(self.ID)) +end +return self +end +function RADIO:StopBroadcast() +self:F() +if self.Positionable.ClassName=="UNIT"or self.Positionable.ClassName=="GROUP"then +local commandStopTransmission={id="StopTransmission",params={}} +self.Positionable:SetCommand(commandStopTransmission) +else +trigger.action.stopRadioTransmission(tostring(self.ID)) +end +return self +end +BEACON={ +ClassName="BEACON", +Positionable=nil, +name=nil, +} +BEACON.Type={ +NULL=0, +VOR=1, +DME=2, +VOR_DME=3, +TACAN=4, +VORTAC=5, +RSBN=128, +BROADCAST_STATION=1024, +HOMER=8, +AIRPORT_HOMER=4104, +AIRPORT_HOMER_WITH_MARKER=4136, +ILS_FAR_HOMER=16408, +ILS_NEAR_HOMER=16424, +ILS_LOCALIZER=16640, +ILS_GLIDESLOPE=16896, +PRMG_LOCALIZER=33024, +PRMG_GLIDESLOPE=33280, +ICLS=131584, +ICLS_LOCALIZER=131328, +ICLS_GLIDESLOPE=131584, +NAUTICAL_HOMER=65536, +} +BEACON.System={ +PAR_10=1, +RSBN_5=2, +TACAN=3, +TACAN_TANKER_X=4, +TACAN_TANKER_Y=5, +VOR=6, +ILS_LOCALIZER=7, +ILS_GLIDESLOPE=8, +PRMG_LOCALIZER=9, +PRMG_GLIDESLOPE=10, +BROADCAST_STATION=11, +VORTAC=12, +TACAN_AA_MODE_X=13, +TACAN_AA_MODE_Y=14, +VORDME=15, +ICLS_LOCALIZER=16, +ICLS_GLIDESLOPE=17, +} +function BEACON:New(Positionable) +local self=BASE:Inherit(self,BASE:New()) +self:F(Positionable) +if Positionable:GetPointVec2()then +self.Positionable=Positionable +self.name=Positionable:GetName() +self:I(string.format("New BEACON %s",tostring(self.name))) +return self +end +self:E({"The passed positionable is invalid, no BEACON created",Positionable}) +return nil +end +function BEACON:ActivateTACAN(Channel,Mode,Message,Bearing,Duration) +self:T({channel=Channel,mode=Mode,callsign=Message,bearing=Bearing,duration=Duration}) +local Frequency=UTILS.TACANToFrequency(Channel,Mode) +if not Frequency then +self:E({"The passed TACAN channel is invalid, the BEACON is not emitting"}) +return self +end +local Type=BEACON.Type.TACAN +local System=BEACON.System.TACAN +local AA=self.Positionable:IsAir() +if AA then +System=5 +if Mode~="Y"then +self:E({"WARNING: The POSITIONABLE you want to attach the AA Tacan Beacon is an aircraft: Mode should Y !The BEACON is not emitting.",self.Positionable}) +end +end +local UnitID=self.Positionable:GetID() +self:I({string.format("BEACON Activating TACAN %s: Channel=%d%s, Morse=%s, Bearing=%s, Duration=%s!",tostring(self.name),Channel,Mode,Message,tostring(Bearing),tostring(Duration))}) +self.Positionable:CommandActivateBeacon(Type,System,Frequency,UnitID,Channel,Mode,AA,Message,Bearing) +if Duration then +self.Positionable:DeactivateBeacon(Duration) +end +return self +end +function BEACON:ActivateICLS(Channel,Callsign,Duration) +self:F({Channel=Channel,Callsign=Callsign,Duration=Duration}) +local UnitID=self.Positionable:GetID() +self:T2({"ICLS BEACON started!"}) +self.Positionable:CommandActivateICLS(Channel,UnitID,Callsign) +if Duration then +self.Positionable:DeactivateBeacon(Duration) +end +return self +end +function BEACON:AATACAN(TACANChannel,Message,Bearing,BeaconDuration) +self:F({TACANChannel,Message,Bearing,BeaconDuration}) +local IsValid=true +if not self.Positionable:IsAir()then +self:E({"The POSITIONABLE you want to attach the AA Tacan Beacon is not an aircraft ! The BEACON is not emitting",self.Positionable}) +IsValid=false +end +local Frequency=self:_TACANToFrequency(TACANChannel,"Y") +if not Frequency then +self:E({"The passed TACAN channel is invalid, the BEACON is not emitting"}) +IsValid=false +end +local System +if Bearing then +System=5 +else +System=14 +end +if IsValid then +self:T2({"AA TACAN BEACON started !"}) +self.Positionable:SetCommand({ +id="ActivateBeacon", +params={ +type=4, +system=System, +callsign=Message, +frequency=Frequency, +} +}) +if BeaconDuration then +SCHEDULER:New(nil, +function() +self:StopAATACAN() +end,{},BeaconDuration) +end +end +return self +end +function BEACON:StopAATACAN() +self:F() +if not self.Positionable then +self:E({"Start the beacon first before stoping it !"}) +else +self.Positionable:SetCommand({ +id='DeactivateBeacon', +params={ +} +}) +end +end +function BEACON:RadioBeacon(FileName,Frequency,Modulation,Power,BeaconDuration) +self:F({FileName,Frequency,Modulation,Power,BeaconDuration}) +local IsValid=false +if type(FileName)=="string"then +if FileName:find(".ogg")or FileName:find(".wav")then +if not FileName:find("l10n/DEFAULT/")then +FileName="l10n/DEFAULT/"..FileName +end +IsValid=true +end +end +if not IsValid then +self:E({"File name invalid. Maybe something wrong with the extension ? ",FileName}) +end +if type(Frequency)~="number"and IsValid then +self:E({"Frequency invalid. ",Frequency}) +IsValid=false +end +Frequency=Frequency*1000000 +if Modulation~=radio.modulation.AM and Modulation~=radio.modulation.FM and IsValid then +self:E({"Modulation is invalid. Use DCS's enum radio.modulation.",Modulation}) +IsValid=false +end +if type(Power)~="number"and IsValid then +self:E({"Power is invalid. ",Power}) +IsValid=false +end +Power=math.floor(math.abs(Power)) +if IsValid then +self:T2({"Activating Beacon on ",Frequency,Modulation}) +trigger.action.radioTransmission(FileName,self.Positionable:GetPositionVec3(),Modulation,true,Frequency,Power,tostring(self.ID)) +if BeaconDuration then +SCHEDULER:New(nil, +function() +self:StopRadioBeacon() +end,{},BeaconDuration) +end +end +end +function BEACON:StopRadioBeacon() +self:F() +trigger.action.stopRadioTransmission(tostring(self.ID)) +return self +end +function BEACON:_TACANToFrequency(TACANChannel,TACANMode) +self:F3({TACANChannel,TACANMode}) +if type(TACANChannel)~="number"then +if TACANMode~="X"and TACANMode~="Y"then +return nil +end +end +local A=1151 +local B=64 +if TACANChannel<64 then +B=1 +end +if TACANMode=='Y'then +A=1025 +if TACANChannel<64 then +A=1088 +end +else +if TACANChannel<64 then +A=962 +end +end +return(A+TACANChannel-B)*1000000 +end +RADIOQUEUE={ +ClassName="RADIOQUEUE", +Debugmode=nil, +lid=nil, +frequency=nil, +modulation=nil, +scheduler=nil, +RQid=nil, +queue={}, +alias=nil, +dt=nil, +delay=nil, +Tlast=nil, +sendercoord=nil, +sendername=nil, +senderinit=nil, +power=nil, +numbers={}, +checking=nil, +schedonce=false, +} +function RADIOQUEUE:New(frequency,modulation,alias) +local self=BASE:Inherit(self,BASE:New()) +self.alias=alias or"My Radio" +self.lid=string.format("RADIOQUEUE %s | ",self.alias) +if frequency==nil then +self:E(self.lid.."ERROR: No frequency specified as first parameter!") +return nil +end +self.frequency=frequency*1000000 +self.modulation=modulation or radio.modulation.AM +self:SetRadioPower() +self.scheduler=SCHEDULER:New() +self.scheduler:NoTrace() +return self +end +function RADIOQUEUE:Start(delay,dt) +self.delay=delay or 1 +self.dt=dt or 0.01 +self:I(self.lid..string.format("Starting RADIOQUEUE %s on Frequency %.2f MHz [modulation=%d] in %.1f seconds (dt=%.3f sec)",self.alias,self.frequency/1000000,self.modulation,self.delay,self.dt)) +if self.schedonce then +self:_CheckRadioQueueDelayed(delay) +else +self.RQid=self.scheduler:Schedule(nil,RADIOQUEUE._CheckRadioQueue,{self},delay,dt) +end +return self +end +function RADIOQUEUE:Stop() +self:I(self.lid.."Stopping RADIOQUEUE.") +self.scheduler:Stop(self.RQid) +self.queue={} +return self +end +function RADIOQUEUE:SetSenderCoordinate(coordinate) +self.sendercoord=coordinate +return self +end +function RADIOQUEUE:SetSenderUnitName(name) +self.sendername=name +return self +end +function RADIOQUEUE:SetRadioPower(power) +self.power=power or 100 +return self +end +function RADIOQUEUE:SetDigit(digit,filename,duration,path,subtitle,subduration) +local transmission={} +transmission.filename=filename +transmission.duration=duration +transmission.path=path or"l10n/DEFAULT/" +transmission.subtitle=nil +transmission.subduration=nil +if type(digit)=="number"then +digit=tostring(digit) +end +self.numbers[digit]=transmission +return self +end +function RADIOQUEUE:AddTransmission(transmission) +self:F({transmission=transmission}) +transmission.isplaying=false +transmission.Tstarted=nil +table.insert(self.queue,transmission) +if self.schedonce and not self.checking then +self:_CheckRadioQueueDelayed() +end +return self +end +function RADIOQUEUE:NewTransmission(filename,duration,path,tstart,interval,subtitle,subduration) +if not filename then +self:E(self.lid.."ERROR: No filename specified.") +return nil +end +if type(filename)~="string"then +self:E(self.lid.."ERROR: Filename specified is NOT a string.") +return nil +end +if not duration then +self:E(self.lid.."ERROR: No duration of transmission specified.") +return nil +end +if type(duration)~="number"then +self:E(self.lid.."ERROR: Duration specified is NOT a number.") +return nil +end +local transmission={} +transmission.filename=filename +transmission.duration=duration +transmission.path=path or"l10n/DEFAULT/" +transmission.Tplay=tstart or timer.getAbsTime() +transmission.subtitle=subtitle +transmission.interval=interval or 0 +if transmission.subtitle then +transmission.subduration=subduration or 5 +else +transmission.subduration=nil +end +self:AddTransmission(transmission) +return self +end +function RADIOQUEUE:Number2Transmission(number,delay,interval) +local function _split(str) +local chars={} +for i=1,#str do +local c=str:sub(i,i) +table.insert(chars,c) +end +return chars +end +local numbers=_split(number) +local wait=0 +for i=1,#numbers do +local n=numbers[i] +local transmission=UTILS.DeepCopy(self.numbers[n]) +transmission.Tplay=timer.getAbsTime()+(delay or 0) +if interval and i==1 then +transmission.interval=interval +end +self:AddTransmission(transmission) +wait=wait+transmission.duration +end +return wait +end +function RADIOQUEUE:Broadcast(transmission) +local sender=self:_GetRadioSender() +local filename=string.format("%s%s",transmission.path,transmission.filename) +if sender then +self:T(self.lid..string.format("Broadcasting from aircraft %s",sender:GetName())) +if not self.senderinit then +local commandFrequency={ +id="SetFrequency", +params={ +frequency=self.frequency, +modulation=self.modulation, +}} +sender:SetCommand(commandFrequency) +self.senderinit=true +end +local subtitle=nil +local duration=nil +if transmission.subtitle and transmission.subduration and transmission.subduration>0 then +subtitle=transmission.subtitle +duration=transmission.subduration +end +local commandTransmit={ +id="TransmitMessage", +params={ +file=filename, +duration=duration, +subtitle=subtitle, +loop=false, +}} +sender:SetCommand(commandTransmit) +if self.Debugmode then +local text=string.format("file=%s, freq=%.2f MHz, duration=%.2f sec, subtitle=%s",filename,self.frequency/1000000,transmission.duration,transmission.subtitle or"") +MESSAGE:New(text,2,"RADIOQUEUE "..self.alias):ToAll() +end +else +self:T(self.lid..string.format("Broadcasting via trigger.action.radioTransmission().")) +local vec3=nil +if self.sendername then +vec3=self:_GetRadioSenderCoord() +end +if self.sendercoord and not vec3 then +vec3=self.sendercoord:GetVec3() +end +if vec3 then +self:T("Sending") +self:T({filename=filename,vec3=vec3,modulation=self.modulation,frequency=self.frequency,power=self.power}) +trigger.action.radioTransmission(filename,vec3,self.modulation,false,self.frequency,self.power) +if self.Debugmode then +local text=string.format("file=%s, freq=%.2f MHz, duration=%.2f sec, subtitle=%s",filename,self.frequency/1000000,transmission.duration,transmission.subtitle or"") +MESSAGE:New(string.format(text,filename,transmission.duration,transmission.subtitle or""),5,"RADIOQUEUE "..self.alias):ToAll() +end +end +end +end +function RADIOQUEUE:_CheckRadioQueueDelayed(delay) +self.checking=true +self:ScheduleOnce(delay or self.dt,RADIOQUEUE._CheckRadioQueue,self) +end +function RADIOQUEUE:_CheckRadioQueue() +if#self.queue==0 then +self.checking=false +return +end +local time=timer.getAbsTime() +local playing=false +local next=nil +local remove=nil +for i,_transmission in ipairs(self.queue)do +local transmission=_transmission +if time>=transmission.Tplay then +if transmission.isplaying then +if time>=transmission.Tstarted+transmission.duration then +transmission.isplaying=false +remove=i +self.Tlast=time +else +playing=true +end +else +local Tlast=self.Tlast +if transmission.interval==nil then +if next==nil then +next=transmission +end +else +if Tlast==nil or time-Tlast>=transmission.interval then +next=transmission +else +end +end +if next or Tlast then +break +end +end +else +end +end +if next~=nil and not playing then +self:Broadcast(next) +next.isplaying=true +next.Tstarted=time +end +if remove then +table.remove(self.queue,remove) +end +if self.schedonce then +self:_CheckRadioQueueDelayed() +end +end +function RADIOQUEUE:_GetRadioSender() +local sender=nil +if self.sendername then +sender=UNIT:FindByName(self.sendername) +if sender and sender:IsAlive()and(sender:IsAir()or sender:IsGround())then +return sender +end +end +return nil +end +function RADIOQUEUE:_GetRadioSenderCoord() +local vec3=nil +if self.sendername then +local sender=UNIT:FindByName(self.sendername) +if sender and sender:IsAlive()then +return sender:GetVec3() +end +local sender=STATIC:FindByName(self.sendername,false) +if sender then +return sender:GetVec3() +end +end +return nil +end +RADIOSPEECH={ +ClassName="RADIOSPEECH", +Vocabulary={ +EN={}, +DE={}, +RU={}, +} +} +RADIOSPEECH.Vocabulary.EN={ +["1"]={"1",0.25}, +["2"]={"2",0.25}, +["3"]={"3",0.30}, +["4"]={"4",0.35}, +["5"]={"5",0.35}, +["6"]={"6",0.42}, +["7"]={"7",0.38}, +["8"]={"8",0.20}, +["9"]={"9",0.32}, +["10"]={"10",0.35}, +["11"]={"11",0.40}, +["12"]={"12",0.42}, +["13"]={"13",0.38}, +["14"]={"14",0.42}, +["15"]={"15",0.42}, +["16"]={"16",0.52}, +["17"]={"17",0.59}, +["18"]={"18",0.40}, +["19"]={"19",0.47}, +["20"]={"20",0.38}, +["30"]={"30",0.29}, +["40"]={"40",0.35}, +["50"]={"50",0.32}, +["60"]={"60",0.44}, +["70"]={"70",0.48}, +["80"]={"80",0.26}, +["90"]={"90",0.36}, +["100"]={"100",0.55}, +["200"]={"200",0.55}, +["300"]={"300",0.61}, +["400"]={"400",0.60}, +["500"]={"500",0.61}, +["600"]={"600",0.65}, +["700"]={"700",0.70}, +["800"]={"800",0.54}, +["900"]={"900",0.60}, +["1000"]={"1000",0.60}, +["2000"]={"2000",0.61}, +["3000"]={"3000",0.64}, +["4000"]={"4000",0.62}, +["5000"]={"5000",0.69}, +["6000"]={"6000",0.69}, +["7000"]={"7000",0.75}, +["8000"]={"8000",0.59}, +["9000"]={"9000",0.65}, +["chevy"]={"chevy",0.35}, +["colt"]={"colt",0.35}, +["springfield"]={"springfield",0.65}, +["dodge"]={"dodge",0.35}, +["enfield"]={"enfield",0.5}, +["ford"]={"ford",0.32}, +["pontiac"]={"pontiac",0.55}, +["uzi"]={"uzi",0.28}, +["degrees"]={"degrees",0.5}, +["kilometers"]={"kilometers",0.65}, +["km"]={"kilometers",0.65}, +["miles"]={"miles",0.45}, +["meters"]={"meters",0.41}, +["mi"]={"miles",0.45}, +["feet"]={"feet",0.29}, +["br"]={"br",1.1}, +["bra"]={"bra",0.3}, +["returning to base"]={"returning_to_base",0.85}, +["on route to ground target"]={"on_route_to_ground_target",1.05}, +["intercepting bogeys"]={"intercepting_bogeys",1.00}, +["engaging ground target"]={"engaging_ground_target",1.20}, +["engaging bogeys"]={"engaging_bogeys",0.81}, +["wheels up"]={"wheels_up",0.42}, +["landing at base"]={"landing at base",0.8}, +["patrolling"]={"patrolling",0.55}, +["for"]={"for",0.31}, +["and"]={"and",0.31}, +["at"]={"at",0.3}, +["dot"]={"dot",0.26}, +["defender"]={"defender",0.45}, +} +RADIOSPEECH.Vocabulary.RU={ +["1"]={"1",0.34}, +["2"]={"2",0.30}, +["3"]={"3",0.23}, +["4"]={"4",0.51}, +["5"]={"5",0.31}, +["6"]={"6",0.44}, +["7"]={"7",0.25}, +["8"]={"8",0.43}, +["9"]={"9",0.45}, +["10"]={"10",0.53}, +["11"]={"11",0.66}, +["12"]={"12",0.70}, +["13"]={"13",0.66}, +["14"]={"14",0.80}, +["15"]={"15",0.65}, +["16"]={"16",0.75}, +["17"]={"17",0.74}, +["18"]={"18",0.85}, +["19"]={"19",0.80}, +["20"]={"20",0.58}, +["30"]={"30",0.51}, +["40"]={"40",0.51}, +["50"]={"50",0.67}, +["60"]={"60",0.76}, +["70"]={"70",0.68}, +["80"]={"80",0.84}, +["90"]={"90",0.71}, +["100"]={"100",0.35}, +["200"]={"200",0.59}, +["300"]={"300",0.53}, +["400"]={"400",0.70}, +["500"]={"500",0.50}, +["600"]={"600",0.58}, +["700"]={"700",0.64}, +["800"]={"800",0.77}, +["900"]={"900",0.75}, +["1000"]={"1000",0.87}, +["2000"]={"2000",0.83}, +["3000"]={"3000",0.84}, +["4000"]={"4000",1.00}, +["5000"]={"5000",0.77}, +["6000"]={"6000",0.90}, +["7000"]={"7000",0.77}, +["8000"]={"8000",0.92}, +["9000"]={"9000",0.87}, +["степени"]={"degrees",0.5}, +["километров"]={"kilometers",0.65}, +["km"]={"kilometers",0.65}, +["миль"]={"miles",0.45}, +["mi"]={"miles",0.45}, +["метры"]={"meters",0.41}, +["m"]={"meters",0.41}, +["ноги"]={"feet",0.37}, +["br"]={"br",1.1}, +["bra"]={"bra",0.3}, +["возвращаясь на базу"]={"returning_to_base",1.40}, +["на пути к наземной цели"]={"on_route_to_ground_target",1.45}, +["перехват самолетов"]={"intercepting_bogeys",1.22}, +["поражение наземной цели"]={"engaging_ground_target",1.53}, +["захватывающие самолеты"]={"engaging_bogeys",1.68}, +["колеса вверх"]={"wheels_up",0.92}, +["посадка на базу"]={"landing at base",1.04}, +["патрулирующий"]={"patrolling",0.96}, +["за"]={"for",0.27}, +["и"]={"and",0.17}, +["в"]={"at",0.19}, +["dot"]={"dot",0.51}, +["defender"]={"defender",0.45}, +} +function RADIOSPEECH:New(frequency,modulation) +local self=BASE:Inherit(self,RADIOQUEUE:New(frequency,modulation)) +self.Language="EN" +self:BuildTree() +return self +end +function RADIOSPEECH:SetLanguage(Langauge) +self.Language=Langauge +end +function RADIOSPEECH:AddSentenceToSpeech(RemainingSentence,Speech,Sentence,Data) +self:I({RemainingSentence,Speech,Sentence,Data}) +local Token,RemainingSentence=RemainingSentence:match("^ *([^ ]+)(.*)") +self:I({Token=Token,RemainingSentence=RemainingSentence}) +if Token then +if not Speech[Token]then +Speech[Token]={} +if RemainingSentence and RemainingSentence~=""then +Speech[Token].Next={} +self:AddSentenceToSpeech(RemainingSentence,Speech[Token].Next,Sentence,Data) +else +Speech[Token].Sentence=Sentence +Speech[Token].Data=Data +end +end +end +end +function RADIOSPEECH:BuildTree() +self.Speech={} +for Language,Sentences in pairs(self.Vocabulary)do +self:I({Language=Language,Sentences=Sentences}) +self.Speech[Language]={} +for Sentence,Data in pairs(Sentences)do +self:I({Sentence=Sentence,Data=Data}) +self:AddSentenceToSpeech(Sentence,self.Speech[Language],Sentence,Data) +end +end +self:I({Speech=self.Speech}) +return self +end +function RADIOSPEECH:SpeakWords(Sentence,Speech,Language) +local OriginalSentence=Sentence +local Word,RemainderSentence=Sentence:match("^[., ]*([^ .,]+)(.*)") +self:I({Word=Word,Speech=Speech[Word],RemainderSentence=RemainderSentence}) +if Word then +if Word~=""and tonumber(Word)==nil then +Word=Word:lower() +if Speech[Word]then +if Speech[Word].Next==nil then +self:I({Sentence=Speech[Word].Sentence,Data=Speech[Word].Data}) +self:NewTransmission(Speech[Word].Data[1]..".wav",Speech[Word].Data[2],Language.."/") +else +if RemainderSentence and RemainderSentence~=""then +return self:SpeakWords(RemainderSentence,Speech[Word].Next,Language) +end +end +end +return RemainderSentence +end +return OriginalSentence +else +return"" +end +end +function RADIOSPEECH:SpeakDigits(Sentence,Speech,Langauge) +local OriginalSentence=Sentence +local Digits,RemainderSentence=Sentence:match("^[., ]*([^ .,]+)(.*)") +self:I({Digits=Digits,Speech=Speech[Digits],RemainderSentence=RemainderSentence}) +if Digits then +if Digits~=""and tonumber(Digits)~=nil then +local Number=tonumber(Digits) +local Multiple=nil +while Number>=0 do +if Number>1000 then +Multiple=math.floor(Number/1000)*1000 +elseif Number>100 then +Multiple=math.floor(Number/100)*100 +elseif Number>20 then +Multiple=math.floor(Number/10)*10 +elseif Number>=0 then +Multiple=Number +end +Sentence=tostring(Multiple) +if Speech[Sentence]then +self:I({Speech=Speech[Sentence].Sentence,Data=Speech[Sentence].Data}) +self:NewTransmission(Speech[Sentence].Data[1]..".wav",Speech[Sentence].Data[2],Langauge.."/") +end +Number=Number-Multiple +Number=(Number==0)and-1 or Number +end +return RemainderSentence +end +return OriginalSentence +else +return"" +end +end +function RADIOSPEECH:Speak(Sentence,Language) +self:I({Sentence,Language}) +local Language=Language or"EN" +self:I({Language=Language}) +local Speech=self.Speech[Language] +self:I({Speech=Speech,Language=Language}) +self:NewTransmission("_In.wav",0.52,Language.."/") +repeat +Sentence=self:SpeakWords(Sentence,Speech,Language) +self:I({Sentence=Sentence}) +Sentence=self:SpeakDigits(Sentence,Speech,Language) +self:I({Sentence=Sentence}) +until not Sentence or Sentence=="" +self:NewTransmission("_Out.wav",0.28,Language.."/") +end +SPAWN={ +ClassName="SPAWN", +SpawnTemplatePrefix=nil, +SpawnAliasPrefix=nil, +} +SPAWN.Takeoff={ +Air=1, +Runway=2, +Hot=3, +Cold=4, +} +function SPAWN:New(SpawnTemplatePrefix) +local self=BASE:Inherit(self,BASE:New()) +self:F({SpawnTemplatePrefix}) +local TemplateGroup=GROUP:FindByName(SpawnTemplatePrefix) +if TemplateGroup then +self.SpawnTemplatePrefix=SpawnTemplatePrefix +self.SpawnIndex=0 +self.SpawnCount=0 +self.AliveUnits=0 +self.SpawnIsScheduled=false +self.SpawnTemplate=self._GetTemplate(self,SpawnTemplatePrefix) +self.Repeat=false +self.UnControlled=false +self.SpawnInitLimit=false +self.SpawnMaxUnitsAlive=0 +self.SpawnMaxGroups=0 +self.SpawnRandomize=false +self.SpawnVisible=false +self.AIOnOff=true +self.SpawnUnControlled=false +self.SpawnInitKeepUnitNames=false +self.DelayOnOff=false +self.SpawnGrouping=nil +self.SpawnInitLivery=nil +self.SpawnInitSkill=nil +self.SpawnInitFreq=nil +self.SpawnInitModu=nil +self.SpawnInitRadio=nil +self.SpawnInitModex=nil +self.SpawnInitAirbase=nil +self.TweakedTemplate=false +self.SpawnGroups={} +else +error("SPAWN:New: There is no group declared in the mission editor with SpawnTemplatePrefix = '"..SpawnTemplatePrefix.."'") +end +self:SetEventPriority(5) +self.SpawnHookScheduler=SCHEDULER:New(nil) +return self +end +function SPAWN:NewWithAlias(SpawnTemplatePrefix,SpawnAliasPrefix) +local self=BASE:Inherit(self,BASE:New()) +self:F({SpawnTemplatePrefix,SpawnAliasPrefix}) +local TemplateGroup=GROUP:FindByName(SpawnTemplatePrefix) +if TemplateGroup then +self.SpawnTemplatePrefix=SpawnTemplatePrefix +self.SpawnAliasPrefix=SpawnAliasPrefix +self.SpawnIndex=0 +self.SpawnCount=0 +self.AliveUnits=0 +self.SpawnIsScheduled=false +self.SpawnTemplate=self._GetTemplate(self,SpawnTemplatePrefix) +self.Repeat=false +self.UnControlled=false +self.SpawnInitLimit=false +self.SpawnMaxUnitsAlive=0 +self.SpawnMaxGroups=0 +self.SpawnRandomize=false +self.SpawnVisible=false +self.AIOnOff=true +self.SpawnUnControlled=false +self.SpawnInitKeepUnitNames=false +self.DelayOnOff=false +self.SpawnGrouping=nil +self.SpawnInitLivery=nil +self.SpawnInitSkill=nil +self.SpawnInitFreq=nil +self.SpawnInitModu=nil +self.SpawnInitRadio=nil +self.SpawnInitModex=nil +self.SpawnInitAirbase=nil +self.TweakedTemplate=false +self.SpawnGroups={} +else +error("SPAWN:New: There is no group declared in the mission editor with SpawnTemplatePrefix = '"..SpawnTemplatePrefix.."'") +end +self:SetEventPriority(5) +self.SpawnHookScheduler=SCHEDULER:New(nil) +return self +end +function SPAWN:NewFromTemplate(SpawnTemplate,SpawnTemplatePrefix,SpawnAliasPrefix) +local self=BASE:Inherit(self,BASE:New()) +self:F({SpawnTemplate,SpawnTemplatePrefix,SpawnAliasPrefix}) +if SpawnAliasPrefix==nil or SpawnAliasPrefix==""then +BASE:I("ERROR: in function NewFromTemplate, required paramter SpawnAliasPrefix is not set") +return nil +end +if SpawnTemplate then +self.SpawnTemplate=SpawnTemplate +self.SpawnTemplatePrefix=SpawnTemplatePrefix +self.SpawnAliasPrefix=SpawnAliasPrefix +self.SpawnIndex=0 +self.SpawnCount=0 +self.AliveUnits=0 +self.SpawnIsScheduled=false +self.Repeat=false +self.UnControlled=false +self.SpawnInitLimit=false +self.SpawnMaxUnitsAlive=0 +self.SpawnMaxGroups=0 +self.SpawnRandomize=false +self.SpawnVisible=false +self.AIOnOff=true +self.SpawnUnControlled=false +self.SpawnInitKeepUnitNames=false +self.DelayOnOff=false +self.Grouping=nil +self.SpawnInitLivery=nil +self.SpawnInitSkill=nil +self.SpawnInitFreq=nil +self.SpawnInitModu=nil +self.SpawnInitRadio=nil +self.SpawnInitModex=nil +self.SpawnInitAirbase=nil +self.TweakedTemplate=true +self.SpawnGroups={} +else +error("There is no template provided for SpawnTemplatePrefix = '"..SpawnTemplatePrefix.."'") +end +self:SetEventPriority(5) +self.SpawnHookScheduler=SCHEDULER:New(nil) +return self +end +function SPAWN:InitLimit(SpawnMaxUnitsAlive,SpawnMaxGroups) +self:F({self.SpawnTemplatePrefix,SpawnMaxUnitsAlive,SpawnMaxGroups}) +self.SpawnInitLimit=true +self.SpawnMaxUnitsAlive=SpawnMaxUnitsAlive +self.SpawnMaxGroups=SpawnMaxGroups +for SpawnGroupID=1,self.SpawnMaxGroups do +self:_InitializeSpawnGroups(SpawnGroupID) +end +return self +end +function SPAWN:InitKeepUnitNames(KeepUnitNames) +self:F() +self.SpawnInitKeepUnitNames=KeepUnitNames or true +return self +end +function SPAWN:InitLateActivated(LateActivated) +self:F() +self.LateActivated=LateActivated or true +return self +end +function SPAWN:InitAirbase(AirbaseName,Takeoff,TerminalType) +self:F() +self.SpawnInitAirbase=AIRBASE:FindByName(AirbaseName) +self.SpawnInitTakeoff=Takeoff or SPAWN.Takeoff.Hot +self.SpawnInitTerminalType=TerminalType +return self +end +function SPAWN:InitHeading(HeadingMin,HeadingMax) +self:F() +self.SpawnInitHeadingMin=HeadingMin +self.SpawnInitHeadingMax=HeadingMax +return self +end +function SPAWN:InitGroupHeading(HeadingMin,HeadingMax,unitVar) +self:F({HeadingMin=HeadingMin,HeadingMax=HeadingMax,unitVar=unitVar}) +self.SpawnInitGroupHeadingMin=HeadingMin +self.SpawnInitGroupHeadingMax=HeadingMax +self.SpawnInitGroupUnitVar=unitVar +return self +end +function SPAWN:InitCoalition(Coalition) +self:F({coalition=Coalition}) +self.SpawnInitCoalition=Coalition +return self +end +function SPAWN:InitCountry(Country) +self:F() +self.SpawnInitCountry=Country +return self +end +function SPAWN:InitCategory(Category) +self:F() +self.SpawnInitCategory=Category +return self +end +function SPAWN:InitLivery(Livery) +self:F({livery=Livery}) +self.SpawnInitLivery=Livery +return self +end +function SPAWN:InitSkill(Skill) +self:F({skill=Skill}) +if Skill:lower()=="average"then +self.SpawnInitSkill="Average" +elseif Skill:lower()=="good"then +self.SpawnInitSkill="Good" +elseif Skill:lower()=="excellent"then +self.SpawnInitSkill="Excellent" +elseif Skill:lower()=="random"then +self.SpawnInitSkill="Random" +else +self.SpawnInitSkill="High" +end +return self +end +function SPAWN:InitRadioCommsOnOff(switch) +self:F({switch=switch}) +self.SpawnInitRadio=switch or true +return self +end +function SPAWN:InitRadioFrequency(frequency) +self:F({frequency=frequency}) +self.SpawnInitFreq=frequency +return self +end +function SPAWN:InitRadioModulation(modulation) +self:F({modulation=modulation}) +if modulation and modulation:lower()=="fm"then +self.SpawnInitModu=radio.modulation.FM +else +self.SpawnInitModu=radio.modulation.AM +end +return self +end +function SPAWN:InitModex(modex) +if modex then +self.SpawnInitModex=tonumber(modex) +end +return self +end +function SPAWN:InitRandomizeRoute(SpawnStartPoint,SpawnEndPoint,SpawnRadius,SpawnHeight) +self:F({self.SpawnTemplatePrefix,SpawnStartPoint,SpawnEndPoint,SpawnRadius,SpawnHeight}) +self.SpawnRandomizeRoute=true +self.SpawnRandomizeRouteStartPoint=SpawnStartPoint +self.SpawnRandomizeRouteEndPoint=SpawnEndPoint +self.SpawnRandomizeRouteRadius=SpawnRadius +self.SpawnRandomizeRouteHeight=SpawnHeight +for GroupID=1,self.SpawnMaxGroups do +self:_RandomizeRoute(GroupID) +end +return self +end +function SPAWN:InitRandomizePosition(RandomizePosition,OuterRadius,InnerRadius) +self:F({self.SpawnTemplatePrefix,RandomizePosition,OuterRadius,InnerRadius}) +self.SpawnRandomizePosition=RandomizePosition or false +self.SpawnRandomizePositionOuterRadius=OuterRadius or 0 +self.SpawnRandomizePositionInnerRadius=InnerRadius or 0 +for GroupID=1,self.SpawnMaxGroups do +self:_RandomizeRoute(GroupID) +end +return self +end +function SPAWN:InitRandomizeUnits(RandomizeUnits,OuterRadius,InnerRadius) +self:F({self.SpawnTemplatePrefix,RandomizeUnits,OuterRadius,InnerRadius}) +self.SpawnRandomizeUnits=RandomizeUnits or false +self.SpawnOuterRadius=OuterRadius or 0 +self.SpawnInnerRadius=InnerRadius or 0 +for GroupID=1,self.SpawnMaxGroups do +self:_RandomizeRoute(GroupID) +end +return self +end +function SPAWN:InitRandomizeTemplate(SpawnTemplatePrefixTable) +self:F({self.SpawnTemplatePrefix,SpawnTemplatePrefixTable}) +self.SpawnTemplatePrefixTable=SpawnTemplatePrefixTable +self.SpawnRandomizeTemplate=true +for SpawnGroupID=1,self.SpawnMaxGroups do +self:_RandomizeTemplate(SpawnGroupID) +end +return self +end +function SPAWN:InitRandomizeTemplateSet(SpawnTemplateSet) +self:F({self.SpawnTemplatePrefix}) +self.SpawnTemplatePrefixTable=SpawnTemplateSet:GetSetNames() +self.SpawnRandomizeTemplate=true +for SpawnGroupID=1,self.SpawnMaxGroups do +self:_RandomizeTemplate(SpawnGroupID) +end +return self +end +function SPAWN:InitRandomizeTemplatePrefixes(SpawnTemplatePrefixes) +self:F({self.SpawnTemplatePrefix}) +local SpawnTemplateSet=SET_GROUP:New():FilterPrefixes(SpawnTemplatePrefixes):FilterOnce() +self:InitRandomizeTemplateSet(SpawnTemplateSet) +return self +end +function SPAWN:InitGrouping(Grouping) +self:F({self.SpawnTemplatePrefix,Grouping}) +self.SpawnGrouping=Grouping +return self +end +function SPAWN:InitRandomizeZones(SpawnZoneTable) +self:F({self.SpawnTemplatePrefix,SpawnZoneTable}) +self.SpawnZoneTable=SpawnZoneTable +self.SpawnRandomizeZones=true +for SpawnGroupID=1,self.SpawnMaxGroups do +self:_RandomizeZones(SpawnGroupID) +end +return self +end +function SPAWN:InitRepeat() +self:F({self.SpawnTemplatePrefix,self.SpawnIndex}) +self.Repeat=true +self.RepeatOnEngineShutDown=false +self.RepeatOnLanding=true +return self +end +function SPAWN:InitRepeatOnLanding() +self:F({self.SpawnTemplatePrefix}) +self:InitRepeat() +self.RepeatOnEngineShutDown=false +self.RepeatOnLanding=true +return self +end +function SPAWN:InitRepeatOnEngineShutDown() +self:F({self.SpawnTemplatePrefix}) +self:InitRepeat() +self.RepeatOnEngineShutDown=true +self.RepeatOnLanding=false +return self +end +function SPAWN:InitCleanUp(SpawnCleanUpInterval) +self:F({self.SpawnTemplatePrefix,SpawnCleanUpInterval}) +self.SpawnCleanUpInterval=SpawnCleanUpInterval +self.SpawnCleanUpTimeStamps={} +local SpawnGroup,SpawnCursor=self:GetFirstAliveGroup() +self:T({"CleanUp Scheduler:",SpawnGroup}) +self.CleanUpScheduler=SCHEDULER:New(self,self._SpawnCleanUpScheduler,{},1,SpawnCleanUpInterval,0.2) +return self +end +function SPAWN:InitArray(SpawnAngle,SpawnWidth,SpawnDeltaX,SpawnDeltaY) +self:F({self.SpawnTemplatePrefix,SpawnAngle,SpawnWidth,SpawnDeltaX,SpawnDeltaY}) +self.SpawnVisible=true +local SpawnX=0 +local SpawnY=0 +local SpawnXIndex=0 +local SpawnYIndex=0 +for SpawnGroupID=1,self.SpawnMaxGroups do +self:T({SpawnX,SpawnY,SpawnXIndex,SpawnYIndex}) +self.SpawnGroups[SpawnGroupID].Visible=true +self.SpawnGroups[SpawnGroupID].Spawned=false +SpawnXIndex=SpawnXIndex+1 +if SpawnWidth and SpawnWidth~=0 then +if SpawnXIndex>=SpawnWidth then +SpawnXIndex=0 +SpawnYIndex=SpawnYIndex+1 +end +end +local SpawnRootX=self.SpawnGroups[SpawnGroupID].SpawnTemplate.x +local SpawnRootY=self.SpawnGroups[SpawnGroupID].SpawnTemplate.y +self:_TranslateRotate(SpawnGroupID,SpawnRootX,SpawnRootY,SpawnX,SpawnY,SpawnAngle) +self.SpawnGroups[SpawnGroupID].SpawnTemplate.lateActivation=true +self.SpawnGroups[SpawnGroupID].SpawnTemplate.visible=true +self.SpawnGroups[SpawnGroupID].Visible=true +self:HandleEvent(EVENTS.Birth,self._OnBirth) +self:HandleEvent(EVENTS.Dead,self._OnDeadOrCrash) +self:HandleEvent(EVENTS.Crash,self._OnDeadOrCrash) +self:HandleEvent(EVENTS.RemoveUnit,self._OnDeadOrCrash) +if self.Repeat then +self:HandleEvent(EVENTS.Takeoff,self._OnTakeOff) +self:HandleEvent(EVENTS.Land,self._OnLand) +end +if self.RepeatOnEngineShutDown then +self:HandleEvent(EVENTS.EngineShutdown,self._OnEngineShutDown) +end +self.SpawnGroups[SpawnGroupID].Group=_DATABASE:Spawn(self.SpawnGroups[SpawnGroupID].SpawnTemplate) +SpawnX=SpawnXIndex*SpawnDeltaX +SpawnY=SpawnYIndex*SpawnDeltaY +end +return self +end +do +function SPAWN:InitAIOnOff(AIOnOff) +self.AIOnOff=AIOnOff +return self +end +function SPAWN:InitAIOn() +return self:InitAIOnOff(true) +end +function SPAWN:InitAIOff() +return self:InitAIOnOff(false) +end +end +do +function SPAWN:InitDelayOnOff(DelayOnOff) +self.DelayOnOff=DelayOnOff +return self +end +function SPAWN:InitDelayOn() +return self:InitDelayOnOff(true) +end +function SPAWN:InitDelayOff() +return self:InitDelayOnOff(false) +end +end +function SPAWN:Spawn() +self:F({self.SpawnTemplatePrefix,self.SpawnIndex,self.AliveUnits}) +if self.SpawnInitAirbase then +return self:SpawnAtAirbase(self.SpawnInitAirbase,self.SpawnInitTakeoff,nil,self.SpawnInitTerminalType) +else +return self:SpawnWithIndex(self.SpawnIndex+1) +end +end +function SPAWN:ReSpawn(SpawnIndex) +self:F({self.SpawnTemplatePrefix,SpawnIndex}) +if not SpawnIndex then +SpawnIndex=1 +end +local SpawnGroup=self:GetGroupFromIndex(SpawnIndex) +local WayPoints=SpawnGroup and SpawnGroup.WayPoints or nil +if SpawnGroup then +local SpawnDCSGroup=SpawnGroup:GetDCSObject() +if SpawnDCSGroup then +SpawnGroup:Destroy() +end +end +local SpawnGroup=self:SpawnWithIndex(SpawnIndex) +if SpawnGroup and WayPoints then +SpawnGroup:WayPointInitialize(WayPoints) +SpawnGroup:WayPointExecute(1,5) +end +if SpawnGroup.ReSpawnFunction then +SpawnGroup:ReSpawnFunction() +end +SpawnGroup:ResetEvents() +return SpawnGroup +end +function SPAWN:SetSpawnIndex(SpawnIndex) +self.SpawnIndex=SpawnIndex or 0 +end +function SPAWN:SpawnWithIndex(SpawnIndex,NoBirth) +self:F2({SpawnTemplatePrefix=self.SpawnTemplatePrefix,SpawnIndex=SpawnIndex,AliveUnits=self.AliveUnits,SpawnMaxGroups=self.SpawnMaxGroups}) +if self:_GetSpawnIndex(SpawnIndex)then +if self.SpawnGroups[self.SpawnIndex].Visible then +self.SpawnGroups[self.SpawnIndex].Group:Activate() +else +local SpawnTemplate=self.SpawnGroups[self.SpawnIndex].SpawnTemplate +self:T(SpawnTemplate.name) +if SpawnTemplate then +local PointVec3=POINT_VEC3:New(SpawnTemplate.route.points[1].x,SpawnTemplate.route.points[1].alt,SpawnTemplate.route.points[1].y) +self:T({"Current point of ",self.SpawnTemplatePrefix,PointVec3}) +if self.SpawnRandomizePosition then +local RandomVec2=PointVec3:GetRandomVec2InRadius(self.SpawnRandomizePositionOuterRadius,self.SpawnRandomizePositionInnerRadius) +local CurrentX=SpawnTemplate.units[1].x +local CurrentY=SpawnTemplate.units[1].y +SpawnTemplate.x=RandomVec2.x +SpawnTemplate.y=RandomVec2.y +for UnitID=1,#SpawnTemplate.units do +SpawnTemplate.units[UnitID].x=SpawnTemplate.units[UnitID].x+(RandomVec2.x-CurrentX) +SpawnTemplate.units[UnitID].y=SpawnTemplate.units[UnitID].y+(RandomVec2.y-CurrentY) +self:T('SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) +end +end +if self.SpawnRandomizeUnits then +for UnitID=1,#SpawnTemplate.units do +local RandomVec2=PointVec3:GetRandomVec2InRadius(self.SpawnOuterRadius,self.SpawnInnerRadius) +SpawnTemplate.units[UnitID].x=RandomVec2.x +SpawnTemplate.units[UnitID].y=RandomVec2.y +self:T('SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) +end +end +local function _Heading(courseDeg) +local h +if courseDeg<=180 then +h=math.rad(courseDeg) +else +h=-math.rad(360-courseDeg) +end +return h +end +local Rad180=math.rad(180) +local function _HeadingRad(courseRad) +if courseRad<=Rad180 then +return courseRad +else +return-((2*Rad180)-courseRad) +end +end +local function _RandomInRange(min,max) +if min and max then +return min+(math.random()*(max-min)) +else +return min +end +end +if self.SpawnInitGroupHeadingMin and#SpawnTemplate.units>0 then +local pivotX=SpawnTemplate.units[1].x +local pivotY=SpawnTemplate.units[1].y +local headingRad=math.rad(_RandomInRange(self.SpawnInitGroupHeadingMin or 0,self.SpawnInitGroupHeadingMax)) +local cosHeading=math.cos(headingRad) +local sinHeading=math.sin(headingRad) +local unitVarRad=math.rad(self.SpawnInitGroupUnitVar or 0) +for UnitID=1,#SpawnTemplate.units do +if UnitID>1 then +local unitXOff=SpawnTemplate.units[UnitID].x-pivotX +local unitYOff=SpawnTemplate.units[UnitID].y-pivotY +SpawnTemplate.units[UnitID].x=pivotX+(unitXOff*cosHeading)-(unitYOff*sinHeading) +SpawnTemplate.units[UnitID].y=pivotY+(unitYOff*cosHeading)+(unitXOff*sinHeading) +end +local unitHeading=SpawnTemplate.units[UnitID].heading+headingRad +SpawnTemplate.units[UnitID].heading=_HeadingRad(_RandomInRange(unitHeading-unitVarRad,unitHeading+unitVarRad)) +SpawnTemplate.units[UnitID].psi=-SpawnTemplate.units[UnitID].heading +end +end +if self.SpawnInitHeadingMin then +for UnitID=1,#SpawnTemplate.units do +SpawnTemplate.units[UnitID].heading=_Heading(_RandomInRange(self.SpawnInitHeadingMin,self.SpawnInitHeadingMax)) +SpawnTemplate.units[UnitID].psi=-SpawnTemplate.units[UnitID].heading +end +end +if self.SpawnInitLivery then +for UnitID=1,#SpawnTemplate.units do +SpawnTemplate.units[UnitID].livery_id=self.SpawnInitLivery +end +end +if self.SpawnInitSkill then +for UnitID=1,#SpawnTemplate.units do +SpawnTemplate.units[UnitID].skill=self.SpawnInitSkill +end +end +if self.SpawnInitModex then +for UnitID=1,#SpawnTemplate.units do +SpawnTemplate.units[UnitID].onboard_num=string.format("%03d",self.SpawnInitModex+(UnitID-1)) +end +end +if self.SpawnInitRadio then +SpawnTemplate.communication=self.SpawnInitRadio +end +if self.SpawnInitFreq then +SpawnTemplate.frequency=self.SpawnInitFreq +end +if self.SpawnInitModu then +SpawnTemplate.modulation=self.SpawnInitModu +end +SpawnTemplate.CategoryID=self.SpawnInitCategory or SpawnTemplate.CategoryID +SpawnTemplate.CountryID=self.SpawnInitCountry or SpawnTemplate.CountryID +SpawnTemplate.CoalitionID=self.SpawnInitCoalition or SpawnTemplate.CoalitionID +end +if not NoBirth then +self:HandleEvent(EVENTS.Birth,self._OnBirth) +end +self:HandleEvent(EVENTS.Dead,self._OnDeadOrCrash) +self:HandleEvent(EVENTS.Crash,self._OnDeadOrCrash) +self:HandleEvent(EVENTS.RemoveUnit,self._OnDeadOrCrash) +if self.Repeat then +self:HandleEvent(EVENTS.Takeoff,self._OnTakeOff) +self:HandleEvent(EVENTS.Land,self._OnLand) +end +if self.RepeatOnEngineShutDown then +self:HandleEvent(EVENTS.EngineShutdown,self._OnEngineShutDown) +end +self.SpawnGroups[self.SpawnIndex].Group=_DATABASE:Spawn(SpawnTemplate) +local SpawnGroup=self.SpawnGroups[self.SpawnIndex].Group +if SpawnGroup then +SpawnGroup:SetAIOnOff(self.AIOnOff) +end +self:T3(SpawnTemplate.name) +if self.SpawnFunctionHook then +self.SpawnHookScheduler:Schedule(nil,self.SpawnFunctionHook,{self.SpawnGroups[self.SpawnIndex].Group,unpack(self.SpawnFunctionArguments)},0.1) +end +end +self.SpawnGroups[self.SpawnIndex].Spawned=true +return self.SpawnGroups[self.SpawnIndex].Group +else +end +return nil +end +function SPAWN:SpawnScheduled(SpawnTime,SpawnTimeVariation) +self:F({SpawnTime,SpawnTimeVariation}) +if SpawnTime~=nil and SpawnTimeVariation~=nil then +local InitialDelay=0 +if self.DelayOnOff==true then +InitialDelay=math.random(SpawnTime-SpawnTime*SpawnTimeVariation,SpawnTime+SpawnTime*SpawnTimeVariation) +end +self.SpawnScheduler=SCHEDULER:New(self,self._Scheduler,{},InitialDelay,SpawnTime,SpawnTimeVariation) +end +return self +end +function SPAWN:SpawnScheduleStart() +self:F({self.SpawnTemplatePrefix}) +self.SpawnScheduler:Start() +return self +end +function SPAWN:SpawnScheduleStop() +self:F({self.SpawnTemplatePrefix}) +self.SpawnScheduler:Stop() +return self +end +function SPAWN:OnSpawnGroup(SpawnCallBackFunction,...) +self:F("OnSpawnGroup") +self.SpawnFunctionHook=SpawnCallBackFunction +self.SpawnFunctionArguments={} +if arg then +self.SpawnFunctionArguments=arg +end +return self +end +function SPAWN:SpawnAtAirbase(SpawnAirbase,Takeoff,TakeoffAltitude,TerminalType,EmergencyAirSpawn,Parkingdata) +self:F({self.SpawnTemplatePrefix,SpawnAirbase,Takeoff,TakeoffAltitude,TerminalType}) +local PointVec3=SpawnAirbase:GetCoordinate() +self:T2(PointVec3) +Takeoff=Takeoff or SPAWN.Takeoff.Hot +if EmergencyAirSpawn==nil then +EmergencyAirSpawn=true +end +self:F({SpawnIndex=self.SpawnIndex}) +if self:_GetSpawnIndex(self.SpawnIndex+1)then +local SpawnTemplate=self.SpawnGroups[self.SpawnIndex].SpawnTemplate +self:F({SpawnTemplate=SpawnTemplate}) +if SpawnTemplate then +local GroupAlive=self:GetGroupFromIndex(self.SpawnIndex) +self:F({GroupAlive=GroupAlive}) +self:T({"Current point of ",self.SpawnTemplatePrefix,SpawnAirbase}) +local TemplateGroup=GROUP:FindByName(self.SpawnTemplatePrefix) +local TemplateUnit=TemplateGroup:GetUnit(1) +local group=TemplateGroup +local istransport=group:HasAttribute("Transports")and group:HasAttribute("Planes") +local isawacs=group:HasAttribute("AWACS") +local isfighter=group:HasAttribute("Fighters")or group:HasAttribute("Interceptors")or group:HasAttribute("Multirole fighters")or(group:HasAttribute("Bombers")and not group:HasAttribute("Strategic bombers")) +local isbomber=group:HasAttribute("Strategic bombers") +local istanker=group:HasAttribute("Tankers") +local ishelo=TemplateUnit:HasAttribute("Helicopters") +local nunits=#SpawnTemplate.units +local SpawnPoint=SpawnTemplate.route.points[1] +SpawnPoint.linkUnit=nil +SpawnPoint.helipadId=nil +SpawnPoint.airdromeId=nil +local AirbaseID=SpawnAirbase:GetID() +local AirbaseCategory=SpawnAirbase:GetAirbaseCategory() +self:F({AirbaseCategory=AirbaseCategory}) +if AirbaseCategory==Airbase.Category.SHIP then +SpawnPoint.linkUnit=AirbaseID +SpawnPoint.helipadId=AirbaseID +elseif AirbaseCategory==Airbase.Category.HELIPAD then +SpawnPoint.linkUnit=AirbaseID +SpawnPoint.helipadId=AirbaseID +elseif AirbaseCategory==Airbase.Category.AIRDROME then +SpawnPoint.airdromeId=AirbaseID +end +SpawnPoint.alt=0 +SpawnPoint.type=GROUPTEMPLATE.Takeoff[Takeoff][1] +SpawnPoint.action=GROUPTEMPLATE.Takeoff[Takeoff][2] +local spawnonground=not(Takeoff==SPAWN.Takeoff.Air) +self:T({spawnonground=spawnonground,TOtype=Takeoff,TOair=Takeoff==SPAWN.Takeoff.Air}) +local spawnonship=false +local spawnonfarp=false +local spawnonrunway=false +local spawnonairport=false +if spawnonground then +if AirbaseCategory==Airbase.Category.SHIP then +spawnonship=true +elseif AirbaseCategory==Airbase.Category.HELIPAD then +spawnonfarp=true +elseif AirbaseCategory==Airbase.Category.AIRDROME then +spawnonairport=true +end +spawnonrunway=Takeoff==SPAWN.Takeoff.Runway +end +local parkingspots={} +local parkingindex={} +local spots +if spawnonground and not SpawnTemplate.parked then +local nfree=0 +local termtype=TerminalType +if spawnonrunway then +if spawnonship then +if ishelo then +termtype=AIRBASE.TerminalType.HelicopterUsable +else +termtype=AIRBASE.TerminalType.OpenMedOrBig +end +else +termtype=AIRBASE.TerminalType.Runway +end +end +local scanradius=50 +local scanunits=true +local scanstatics=true +local scanscenery=false +local verysafe=false +if spawnonship or spawnonfarp or spawnonrunway then +self:T(string.format("Group %s is spawned on farp/ship/runway %s.",self.SpawnTemplatePrefix,SpawnAirbase:GetName())) +nfree=SpawnAirbase:GetFreeParkingSpotsNumber(termtype,true) +spots=SpawnAirbase:GetFreeParkingSpotsTable(termtype,true) +else +if ishelo then +if termtype==nil then +self:T(string.format("Helo group %s is at %s using terminal type %d.",self.SpawnTemplatePrefix,SpawnAirbase:GetName(),AIRBASE.TerminalType.HelicopterOnly)) +spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup,AIRBASE.TerminalType.HelicopterOnly,scanradius,scanunits,scanstatics,scanscenery,verysafe,nunits,Parkingdata) +nfree=#spots +if nfree=1 then +for i=1,nunits do +table.insert(parkingspots,spots[1].Coordinate) +table.insert(parkingindex,spots[1].TerminalID) +end +PointVec3=spots[1].Coordinate +else +_notenough=true +end +elseif spawnonairport then +if nfree>=nunits then +for i=1,nunits do +table.insert(parkingspots,spots[i].Coordinate) +table.insert(parkingindex,spots[i].TerminalID) +end +else +_notenough=true +end +end +if _notenough then +if EmergencyAirSpawn and not self.SpawnUnControlled then +self:E(string.format("WARNING: Group %s has no parking spots at %s ==> air start!",self.SpawnTemplatePrefix,SpawnAirbase:GetName())) +spawnonground=false +spawnonship=false +spawnonfarp=false +spawnonrunway=false +SpawnPoint.type=GROUPTEMPLATE.Takeoff[GROUP.Takeoff.Air][1] +SpawnPoint.action=GROUPTEMPLATE.Takeoff[GROUP.Takeoff.Air][2] +PointVec3.x=PointVec3.x+math.random(-500,500) +PointVec3.z=PointVec3.z+math.random(-500,500) +if ishelo then +PointVec3.y=PointVec3:GetLandHeight()+math.random(100,1000) +else +PointVec3.y=PointVec3:GetLandHeight()+math.random(500,2500) +end +Takeoff=GROUP.Takeoff.Air +else +self:E(string.format("WARNING: Group %s has no parking spots at %s ==> No emergency air start or uncontrolled spawning ==> No spawn!",self.SpawnTemplatePrefix,SpawnAirbase:GetName())) +return nil +end +end +else +if TakeoffAltitude then +PointVec3.y=TakeoffAltitude +else +if ishelo then +PointVec3.y=PointVec3:GetLandHeight()+math.random(100,1000) +else +PointVec3.y=PointVec3:GetLandHeight()+math.random(500,2500) +end +end +end +if not SpawnTemplate.parked then +SpawnTemplate.parked=true +for UnitID=1,nunits do +self:T2('Before Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) +local UnitTemplate=SpawnTemplate.units[UnitID] +local SX=UnitTemplate.x +local SY=UnitTemplate.y +local BX=SpawnTemplate.route.points[1].x +local BY=SpawnTemplate.route.points[1].y +local TX=PointVec3.x+(SX-BX) +local TY=PointVec3.z+(SY-BY) +if spawnonground then +if spawnonship or spawnonfarp or spawnonrunway then +self:T(string.format("Group %s spawning at farp, ship or runway %s.",self.SpawnTemplatePrefix,SpawnAirbase:GetName())) +SpawnTemplate.units[UnitID].x=PointVec3.x +SpawnTemplate.units[UnitID].y=PointVec3.z +SpawnTemplate.units[UnitID].alt=PointVec3.y +else +self:T(string.format("Group %s spawning at airbase %s on parking spot id %d",self.SpawnTemplatePrefix,SpawnAirbase:GetName(),parkingindex[UnitID])) +SpawnTemplate.units[UnitID].x=parkingspots[UnitID].x +SpawnTemplate.units[UnitID].y=parkingspots[UnitID].z +SpawnTemplate.units[UnitID].alt=parkingspots[UnitID].y +end +else +self:T(string.format("Group %s spawning in air at %s.",self.SpawnTemplatePrefix,SpawnAirbase:GetName())) +SpawnTemplate.units[UnitID].x=TX +SpawnTemplate.units[UnitID].y=TY +SpawnTemplate.units[UnitID].alt=PointVec3.y +end +UnitTemplate.parking=nil +UnitTemplate.parking_id=nil +if parkingindex[UnitID]then +UnitTemplate.parking=parkingindex[UnitID] +end +self:T(string.format("Group %s unit number %d: Parking = %s",self.SpawnTemplatePrefix,UnitID,tostring(UnitTemplate.parking))) +self:T(string.format("Group %s unit number %d: Parking ID = %s",self.SpawnTemplatePrefix,UnitID,tostring(UnitTemplate.parking_id))) +self:T2('After Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) +end +end +SpawnPoint.x=PointVec3.x +SpawnPoint.y=PointVec3.z +SpawnPoint.alt=PointVec3.y +SpawnTemplate.x=PointVec3.x +SpawnTemplate.y=PointVec3.z +SpawnTemplate.uncontrolled=self.SpawnUnControlled +local GroupSpawned=self:SpawnWithIndex(self.SpawnIndex) +if Takeoff==GROUP.Takeoff.Air then +for UnitID,UnitSpawned in pairs(GroupSpawned:GetUnits())do +SCHEDULER:New(nil,BASE.CreateEventTakeoff,{GroupSpawned,timer.getTime(),UnitSpawned:GetDCSObject()},5) +end +end +if Takeoff~=SPAWN.Takeoff.Runway and Takeoff~=SPAWN.Takeoff.Air and spawnonairport then +SCHEDULER:New(nil,AIRBASE.CheckOnRunWay,{SpawnAirbase,GroupSpawned,75,true},1.0) +end +return GroupSpawned +end +end +return nil +end +function SPAWN:SpawnAtParkingSpot(Airbase,Spots,Takeoff) +self:F({Airbase=Airbase,Spots=Spots,Takeoff=Takeoff}) +if type(Spots)~="table"then +Spots={Spots} +end +local group=GROUP:FindByName(self.SpawnTemplatePrefix) +local nunits=self.SpawnGrouping or#group:GetUnits() +if nunits then +if#Spots=nunits then +return self:SpawnAtAirbase(Airbase,Takeoff,nil,nil,nil,Parkingdata) +else +self:E("ERROR: Could not find enough free parking spots!") +end +else +self:E("ERROR: Could not get number of units in group!") +end +return nil +end +function SPAWN:ParkAircraft(SpawnAirbase,TerminalType,Parkingdata,SpawnIndex) +self:F({SpawnIndex=SpawnIndex,SpawnMaxGroups=self.SpawnMaxGroups}) +local PointVec3=SpawnAirbase:GetCoordinate() +self:T2(PointVec3) +local Takeoff=SPAWN.Takeoff.Cold +local SpawnTemplate=self.SpawnGroups[SpawnIndex].SpawnTemplate +if SpawnTemplate then +local GroupAlive=self:GetGroupFromIndex(SpawnIndex) +self:T({"Current point of ",self.SpawnTemplatePrefix,SpawnAirbase}) +local TemplateGroup=GROUP:FindByName(self.SpawnTemplatePrefix) +local TemplateUnit=TemplateGroup:GetUnit(1) +local ishelo=TemplateUnit:HasAttribute("Helicopters") +local isbomber=TemplateUnit:HasAttribute("Bombers") +local istransport=TemplateUnit:HasAttribute("Transports") +local isfighter=TemplateUnit:HasAttribute("Battleplanes") +local nunits=#SpawnTemplate.units +local SpawnPoint=SpawnTemplate.route.points[1] +SpawnPoint.linkUnit=nil +SpawnPoint.helipadId=nil +SpawnPoint.airdromeId=nil +local AirbaseID=SpawnAirbase:GetID() +local AirbaseCategory=SpawnAirbase:GetAirbaseCategory() +self:F({AirbaseCategory=AirbaseCategory}) +if AirbaseCategory==Airbase.Category.SHIP then +SpawnPoint.linkUnit=AirbaseID +SpawnPoint.helipadId=AirbaseID +elseif AirbaseCategory==Airbase.Category.HELIPAD then +SpawnPoint.linkUnit=AirbaseID +SpawnPoint.helipadId=AirbaseID +elseif AirbaseCategory==Airbase.Category.AIRDROME then +SpawnPoint.airdromeId=AirbaseID +end +SpawnPoint.alt=0 +SpawnPoint.type=GROUPTEMPLATE.Takeoff[Takeoff][1] +SpawnPoint.action=GROUPTEMPLATE.Takeoff[Takeoff][2] +local spawnonground=not(Takeoff==SPAWN.Takeoff.Air) +self:T({spawnonground=spawnonground,TOtype=Takeoff,TOair=Takeoff==SPAWN.Takeoff.Air}) +local spawnonship=false +local spawnonfarp=false +local spawnonrunway=false +local spawnonairport=false +if spawnonground then +if AirbaseCategory==Airbase.Category.SHIP then +spawnonship=true +elseif AirbaseCategory==Airbase.Category.HELIPAD then +spawnonfarp=true +elseif AirbaseCategory==Airbase.Category.AIRDROME then +spawnonairport=true +end +spawnonrunway=Takeoff==SPAWN.Takeoff.Runway +end +local parkingspots={} +local parkingindex={} +local spots +if spawnonground and not SpawnTemplate.parked then +local nfree=0 +local termtype=TerminalType +local scanradius=50 +local scanunits=true +local scanstatics=true +local scanscenery=false +local verysafe=false +if spawnonship or spawnonfarp or spawnonrunway then +self:T(string.format("Group %s is spawned on farp/ship/runway %s.",self.SpawnTemplatePrefix,SpawnAirbase:GetName())) +nfree=SpawnAirbase:GetFreeParkingSpotsNumber(termtype,true) +spots=SpawnAirbase:GetFreeParkingSpotsTable(termtype,true) +else +if ishelo then +if termtype==nil then +self:T(string.format("Helo group %s is at %s using terminal type %d.",self.SpawnTemplatePrefix,SpawnAirbase:GetName(),AIRBASE.TerminalType.HelicopterOnly)) +spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup,AIRBASE.TerminalType.HelicopterOnly,scanradius,scanunits,scanstatics,scanscenery,verysafe,nunits,Parkingdata) +nfree=#spots +if nfree=1 then +for i=1,nunits do +table.insert(parkingspots,spots[1].Coordinate) +table.insert(parkingindex,spots[1].TerminalID) +end +PointVec3=spots[1].Coordinate +else +_notenough=true +end +elseif spawnonairport then +if nfree>=nunits then +for i=1,nunits do +table.insert(parkingspots,spots[i].Coordinate) +table.insert(parkingindex,spots[i].TerminalID) +end +else +_notenough=true +end +end +if _notenough then +if not self.SpawnUnControlled then +else +self:E(string.format("WARNING: Group %s has no parking spots at %s ==> No emergency air start or uncontrolled spawning ==> No spawn!",self.SpawnTemplatePrefix,SpawnAirbase:GetName())) +return nil +end +end +else +end +if not SpawnTemplate.parked then +SpawnTemplate.parked=true +for UnitID=1,nunits do +self:F('Before Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) +local UnitTemplate=SpawnTemplate.units[UnitID] +local SX=UnitTemplate.x +local SY=UnitTemplate.y +local BX=SpawnTemplate.route.points[1].x +local BY=SpawnTemplate.route.points[1].y +local TX=PointVec3.x+(SX-BX) +local TY=PointVec3.z+(SY-BY) +if spawnonground then +if spawnonship or spawnonfarp or spawnonrunway then +self:T(string.format("Group %s spawning at farp, ship or runway %s.",self.SpawnTemplatePrefix,SpawnAirbase:GetName())) +SpawnTemplate.units[UnitID].x=PointVec3.x +SpawnTemplate.units[UnitID].y=PointVec3.z +SpawnTemplate.units[UnitID].alt=PointVec3.y +else +self:T(string.format("Group %s spawning at airbase %s on parking spot id %d",self.SpawnTemplatePrefix,SpawnAirbase:GetName(),parkingindex[UnitID])) +SpawnTemplate.units[UnitID].x=parkingspots[UnitID].x +SpawnTemplate.units[UnitID].y=parkingspots[UnitID].z +SpawnTemplate.units[UnitID].alt=parkingspots[UnitID].y +end +else +self:T(string.format("Group %s spawning in air at %s.",self.SpawnTemplatePrefix,SpawnAirbase:GetName())) +SpawnTemplate.units[UnitID].x=TX +SpawnTemplate.units[UnitID].y=TY +SpawnTemplate.units[UnitID].alt=PointVec3.y +end +UnitTemplate.parking=nil +UnitTemplate.parking_id=nil +if parkingindex[UnitID]then +UnitTemplate.parking=parkingindex[UnitID] +end +self:T2(string.format("Group %s unit number %d: Parking = %s",self.SpawnTemplatePrefix,UnitID,tostring(UnitTemplate.parking))) +self:T2(string.format("Group %s unit number %d: Parking ID = %s",self.SpawnTemplatePrefix,UnitID,tostring(UnitTemplate.parking_id))) +self:T2('After Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) +end +end +SpawnPoint.x=PointVec3.x +SpawnPoint.y=PointVec3.z +SpawnPoint.alt=PointVec3.y +SpawnTemplate.x=PointVec3.x +SpawnTemplate.y=PointVec3.z +SpawnTemplate.uncontrolled=true +local GroupSpawned=self:SpawnWithIndex(SpawnIndex,true) +if Takeoff==GROUP.Takeoff.Air then +for UnitID,UnitSpawned in pairs(GroupSpawned:GetUnits())do +SCHEDULER:New(nil,BASE.CreateEventTakeoff,{GroupSpawned,timer.getTime(),UnitSpawned:GetDCSObject()},5) +end +end +if Takeoff~=SPAWN.Takeoff.Runway and Takeoff~=SPAWN.Takeoff.Air and spawnonairport then +SCHEDULER:New(nil,AIRBASE.CheckOnRunWay,{SpawnAirbase,GroupSpawned,75,true},1.0) +end +end +end +function SPAWN:ParkAtAirbase(SpawnAirbase,TerminalType,Parkingdata) +self:F({self.SpawnTemplatePrefix,SpawnAirbase,TerminalType}) +self:ParkAircraft(SpawnAirbase,TerminalType,Parkingdata,1) +for SpawnIndex=2,self.SpawnMaxGroups do +self:ParkAircraft(SpawnAirbase,TerminalType,Parkingdata,SpawnIndex) +end +self:SetSpawnIndex(0) +return nil +end +function SPAWN:SpawnFromVec3(Vec3,SpawnIndex) +self:F({self.SpawnTemplatePrefix,Vec3,SpawnIndex}) +local PointVec3=POINT_VEC3:NewFromVec3(Vec3) +self:T2(PointVec3) +if SpawnIndex then +else +SpawnIndex=self.SpawnIndex+1 +end +if self:_GetSpawnIndex(SpawnIndex)then +local SpawnTemplate=self.SpawnGroups[self.SpawnIndex].SpawnTemplate +if SpawnTemplate then +self:T({"Current point of ",self.SpawnTemplatePrefix,Vec3}) +local TemplateHeight=SpawnTemplate.route and SpawnTemplate.route.points[1].alt or nil +SpawnTemplate.route=SpawnTemplate.route or{} +SpawnTemplate.route.points=SpawnTemplate.route.points or{} +SpawnTemplate.route.points[1]=SpawnTemplate.route.points[1]or{} +SpawnTemplate.route.points[1].x=SpawnTemplate.route.points[1].x or 0 +SpawnTemplate.route.points[1].y=SpawnTemplate.route.points[1].y or 0 +for UnitID=1,#SpawnTemplate.units do +local UnitTemplate=SpawnTemplate.units[UnitID] +local SX=UnitTemplate.x or 0 +local SY=UnitTemplate.y or 0 +local BX=SpawnTemplate.route.points[1].x +local BY=SpawnTemplate.route.points[1].y +local TX=Vec3.x+(SX-BX) +local TY=Vec3.z+(SY-BY) +SpawnTemplate.units[UnitID].x=TX +SpawnTemplate.units[UnitID].y=TY +if SpawnTemplate.CategoryID~=Group.Category.SHIP then +SpawnTemplate.units[UnitID].alt=Vec3.y or TemplateHeight +end +self:T('After Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) +end +SpawnTemplate.route.points[1].x=Vec3.x +SpawnTemplate.route.points[1].y=Vec3.z +if SpawnTemplate.CategoryID~=Group.Category.SHIP then +SpawnTemplate.route.points[1].alt=Vec3.y or TemplateHeight +end +SpawnTemplate.x=Vec3.x +SpawnTemplate.y=Vec3.z +SpawnTemplate.alt=Vec3.y or TemplateHeight +return self:SpawnWithIndex(self.SpawnIndex) +end +end +return nil +end +function SPAWN:SpawnFromCoordinate(Coordinate,SpawnIndex) +self:F({self.SpawnTemplatePrefix,SpawnIndex}) +return self:SpawnFromVec3(Coordinate:GetVec3(),SpawnIndex) +end +function SPAWN:SpawnFromPointVec3(PointVec3,SpawnIndex) +self:F({self.SpawnTemplatePrefix,SpawnIndex}) +return self:SpawnFromVec3(PointVec3:GetVec3(),SpawnIndex) +end +function SPAWN:SpawnFromVec2(Vec2,MinHeight,MaxHeight,SpawnIndex) +self:F({self.SpawnTemplatePrefix,self.SpawnIndex,Vec2,MinHeight,MaxHeight,SpawnIndex}) +local Height=nil +if MinHeight and MaxHeight then +Height=math.random(MinHeight,MaxHeight) +end +return self:SpawnFromVec3({x=Vec2.x,y=Height,z=Vec2.y},SpawnIndex) +end +function SPAWN:SpawnFromPointVec2(PointVec2,MinHeight,MaxHeight,SpawnIndex) +self:F({self.SpawnTemplatePrefix,self.SpawnIndex}) +return self:SpawnFromVec2(PointVec2:GetVec2(),MinHeight,MaxHeight,SpawnIndex) +end +function SPAWN:SpawnFromUnit(HostUnit,MinHeight,MaxHeight,SpawnIndex) +self:F({self.SpawnTemplatePrefix,HostUnit,MinHeight,MaxHeight,SpawnIndex}) +if HostUnit and HostUnit:IsAlive()~=nil then +return self:SpawnFromVec2(HostUnit:GetVec2(),MinHeight,MaxHeight,SpawnIndex) +end +return nil +end +function SPAWN:SpawnFromStatic(HostStatic,MinHeight,MaxHeight,SpawnIndex) +self:F({self.SpawnTemplatePrefix,HostStatic,MinHeight,MaxHeight,SpawnIndex}) +if HostStatic and HostStatic:IsAlive()then +return self:SpawnFromVec2(HostStatic:GetVec2(),MinHeight,MaxHeight,SpawnIndex) +end +return nil +end +function SPAWN:SpawnInZone(Zone,RandomizeGroup,MinHeight,MaxHeight,SpawnIndex) +self:F({self.SpawnTemplatePrefix,Zone,RandomizeGroup,MinHeight,MaxHeight,SpawnIndex}) +if Zone then +if RandomizeGroup then +return self:SpawnFromVec2(Zone:GetRandomVec2(),MinHeight,MaxHeight,SpawnIndex) +else +return self:SpawnFromVec2(Zone:GetVec2(),MinHeight,MaxHeight,SpawnIndex) +end +end +return nil +end +function SPAWN:InitUnControlled(UnControlled) +self:F2({self.SpawnTemplatePrefix,UnControlled}) +self.SpawnUnControlled=(UnControlled==true)and true or nil +for SpawnGroupID=1,self.SpawnMaxGroups do +self.SpawnGroups[SpawnGroupID].UnControlled=self.SpawnUnControlled +end +return self +end +function SPAWN:GetCoordinate() +local LateGroup=GROUP:FindByName(self.SpawnTemplatePrefix) +if LateGroup then +return LateGroup:GetCoordinate() +end +return nil +end +function SPAWN:SpawnGroupName(SpawnIndex) +self:F({self.SpawnTemplatePrefix,SpawnIndex}) +local SpawnPrefix=self.SpawnTemplatePrefix +if self.SpawnAliasPrefix then +SpawnPrefix=self.SpawnAliasPrefix +end +if SpawnIndex then +local SpawnName=string.format('%s#%03d',SpawnPrefix,SpawnIndex) +self:T(SpawnName) +return SpawnName +else +self:T(SpawnPrefix) +return SpawnPrefix +end +end +function SPAWN:GetFirstAliveGroup() +self:F({self.SpawnTemplatePrefix,self.SpawnAliasPrefix}) +for SpawnIndex=1,self.SpawnCount do +local SpawnGroup=self:GetGroupFromIndex(SpawnIndex) +if SpawnGroup and SpawnGroup:IsAlive()then +return SpawnGroup,SpawnIndex +end +end +return nil,nil +end +function SPAWN:GetNextAliveGroup(SpawnIndexStart) +self:F({self.SpawnTemplatePrefix,self.SpawnAliasPrefix,SpawnIndexStart}) +SpawnIndexStart=SpawnIndexStart+1 +for SpawnIndex=SpawnIndexStart,self.SpawnCount do +local SpawnGroup=self:GetGroupFromIndex(SpawnIndex) +if SpawnGroup and SpawnGroup:IsAlive()then +return SpawnGroup,SpawnIndex +end +end +return nil,nil +end +function SPAWN:GetLastAliveGroup() +self:F({self.SpawnTemplatePrefix,self.SpawnAliasPrefix}) +for SpawnIndex=self.SpawnCount,1,-1 do +local SpawnGroup=self:GetGroupFromIndex(SpawnIndex) +if SpawnGroup and SpawnGroup:IsAlive()then +self.SpawnIndex=SpawnIndex +return SpawnGroup +end +end +self.SpawnIndex=nil +return nil +end +function SPAWN:GetGroupFromIndex(SpawnIndex) +self:F({self.SpawnTemplatePrefix,self.SpawnAliasPrefix,SpawnIndex}) +if not SpawnIndex then +SpawnIndex=1 +end +if self.SpawnGroups and self.SpawnGroups[SpawnIndex]then +local SpawnGroup=self.SpawnGroups[SpawnIndex].Group +return SpawnGroup +else +return nil +end +end +function SPAWN:_GetPrefixFromGroup(SpawnGroup) +self:F3({self.SpawnTemplatePrefix,self.SpawnAliasPrefix,SpawnGroup}) +local GroupName=SpawnGroup:GetName() +if GroupName then +local SpawnPrefix=string.match(GroupName,".*#") +if SpawnPrefix then +SpawnPrefix=SpawnPrefix:sub(1,-2) +end +return SpawnPrefix +end +return nil +end +function SPAWN:GetSpawnIndexFromGroup(SpawnGroup) +self:F2({self.SpawnTemplatePrefix,self.SpawnAliasPrefix,SpawnGroup}) +local IndexString=string.match(SpawnGroup:GetName(),"#(%d*)$"):sub(2) +local Index=tonumber(IndexString) +self:T3(IndexString,Index) +return Index +end +function SPAWN:_GetLastIndex() +self:F({self.SpawnTemplatePrefix,self.SpawnAliasPrefix}) +return self.SpawnMaxGroups +end +function SPAWN:_InitializeSpawnGroups(SpawnIndex) +self:F3({self.SpawnTemplatePrefix,self.SpawnAliasPrefix,SpawnIndex}) +if not self.SpawnGroups[SpawnIndex]then +self.SpawnGroups[SpawnIndex]={} +self.SpawnGroups[SpawnIndex].Visible=false +self.SpawnGroups[SpawnIndex].Spawned=false +self.SpawnGroups[SpawnIndex].UnControlled=false +self.SpawnGroups[SpawnIndex].SpawnTime=0 +self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix=self.SpawnTemplatePrefix +self.SpawnGroups[SpawnIndex].SpawnTemplate=self:_Prepare(self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix,SpawnIndex) +end +self:_RandomizeTemplate(SpawnIndex) +self:_RandomizeRoute(SpawnIndex) +return self.SpawnGroups[SpawnIndex] +end +function SPAWN:_GetGroupCategoryID(SpawnPrefix) +local TemplateGroup=Group.getByName(SpawnPrefix) +if TemplateGroup then +return TemplateGroup:getCategory() +else +return nil +end +end +function SPAWN:_GetGroupCoalitionID(SpawnPrefix) +local TemplateGroup=Group.getByName(SpawnPrefix) +if TemplateGroup then +return TemplateGroup:getCoalition() +else +return nil +end +end +function SPAWN:_GetGroupCountryID(SpawnPrefix) +self:F({self.SpawnTemplatePrefix,self.SpawnAliasPrefix,SpawnPrefix}) +local TemplateGroup=Group.getByName(SpawnPrefix) +if TemplateGroup then +local TemplateUnits=TemplateGroup:getUnits() +return TemplateUnits[1]:getCountry() +else +return nil +end +end +function SPAWN:_GetTemplate(SpawnTemplatePrefix) +self:F({self.SpawnTemplatePrefix,self.SpawnAliasPrefix,SpawnTemplatePrefix}) +local SpawnTemplate=nil +local Template=_DATABASE.Templates.Groups[SpawnTemplatePrefix].Template +self:F({Template=Template}) +SpawnTemplate=UTILS.DeepCopy(_DATABASE.Templates.Groups[SpawnTemplatePrefix].Template) +if SpawnTemplate==nil then +error('No Template returned for SpawnTemplatePrefix = '..SpawnTemplatePrefix) +end +self:T3({SpawnTemplate}) +return SpawnTemplate +end +function SPAWN:_Prepare(SpawnTemplatePrefix,SpawnIndex) +self:F({self.SpawnTemplatePrefix,self.SpawnAliasPrefix}) +local SpawnTemplate +if self.TweakedTemplate~=nil and self.TweakedTemplate==true then +BASE:I("WARNING: You are using a tweaked template.") +SpawnTemplate=self.SpawnTemplate +else +SpawnTemplate=self:_GetTemplate(SpawnTemplatePrefix) +SpawnTemplate.name=self:SpawnGroupName(SpawnIndex) +end +SpawnTemplate.groupId=nil +SpawnTemplate.lateActivation=self.LateActivated or false +if SpawnTemplate.CategoryID==Group.Category.GROUND then +self:T3("For ground units, visible needs to be false...") +SpawnTemplate.visible=false +end +if self.SpawnGrouping then +local UnitAmount=#SpawnTemplate.units +self:F({UnitAmount=UnitAmount,SpawnGrouping=self.SpawnGrouping}) +if UnitAmount>self.SpawnGrouping then +for UnitID=self.SpawnGrouping+1,UnitAmount do +SpawnTemplate.units[UnitID]=nil +end +else +if UnitAmount0 then +self.Tstop=timer.getTime()+Delay +else +if self.tid then +self:T(self.lid..string.format("Stopping timer by removing timer function after %d calls!",self.ncalls)) +timer.removeFunction(self.tid) +self.isrunning=false +end +end +return self +end +function TIMER:SetMaxFunctionCalls(Nmax) +self.ncallsMax=Nmax +return self +end +function TIMER:IsRunning() +return self.isrunning +end +function TIMER:_Function(time) +self.func(unpack(self.para)) +self.ncalls=self.ncalls+1 +local Tnext=self.dT and time+self.dT or nil +local stopme=false +if Tnext==nil then +self:T(self.lid..string.format("No next time as dT=nil ==> Stopping timer after %d function calls",self.ncalls)) +stopme=true +elseif self.Tstop and Tnext>self.Tstop then +self:T(self.lid..string.format("Stop time passed %.3f > %.3f ==> Stopping timer after %d function calls",Tnext,self.Tstop,self.ncalls)) +stopme=true +elseif self.ncallsMax and self.ncalls>=self.ncallsMax then +self:T(self.lid..string.format("Max function calls Nmax=%d reached ==> Stopping timer after %d function calls",self.ncallsMax,self.ncalls)) +stopme=true +end +if stopme then +self:Stop() +return nil +else +return Tnext +end +end +do +GOAL={ +ClassName="GOAL", +} +GOAL.Players={} +GOAL.TotalContributions=0 +function GOAL:New() +local self=BASE:Inherit(self,FSM:New()) +self:F({}) +self:SetStartState("Pending") +self:AddTransition("*","Achieved","Achieved") +self:SetEventPriority(5) +return self +end +function GOAL:AddPlayerContribution(PlayerName) +self:F({PlayerName}) +self.Players[PlayerName]=self.Players[PlayerName]or 0 +self.Players[PlayerName]=self.Players[PlayerName]+1 +self.TotalContributions=self.TotalContributions+1 +end +function GOAL:GetPlayerContribution(PlayerName) +return self.Players[PlayerName]or 0 +end +function GOAL:GetPlayerContributions() +return self.Players or{} +end +function GOAL:GetTotalContributions() +return self.TotalContributions or 0 +end +function GOAL:IsAchieved() +return self:Is("Achieved") +end +end +do +SPOT={ +ClassName="SPOT", +} +function SPOT:New(Recce) +local self=BASE:Inherit(self,FSM:New()) +self:F({}) +self:SetStartState("Off") +self:AddTransition("Off","LaseOn","On") +self:AddTransition("Off","LaseOnCoordinate","On") +self:AddTransition("On","Lasing","On") +self:AddTransition({"On","Destroyed"},"LaseOff","Off") +self:AddTransition("*","Destroyed","Destroyed") +self.Recce=Recce +self.LaseScheduler=SCHEDULER:New(self) +self:SetEventPriority(5) +self.Lasing=false +return self +end +function SPOT:onafterLaseOn(From,Event,To,Target,LaserCode,Duration) +self:F({"LaseOn",Target,LaserCode,Duration}) +local function StopLase(self) +self:LaseOff() +end +self.Target=Target +self.LaserCode=LaserCode +self.Lasing=true +local RecceDcsUnit=self.Recce:GetDCSObject() +self.SpotIR=Spot.createInfraRed(RecceDcsUnit,{x=0,y=2,z=0},Target:GetPointVec3():AddY(1):GetVec3()) +self.SpotLaser=Spot.createLaser(RecceDcsUnit,{x=0,y=2,z=0},Target:GetPointVec3():AddY(1):GetVec3(),LaserCode) +if Duration then +self.ScheduleID=self.LaseScheduler:Schedule(self,StopLase,{self},Duration) +end +self:HandleEvent(EVENTS.Dead) +self:__Lasing(-1) +end +function SPOT:onafterLaseOnCoordinate(From,Event,To,Coordinate,LaserCode,Duration) +self:F({"LaseOnCoordinate",Coordinate,LaserCode,Duration}) +local function StopLase(self) +self:LaseOff() +end +self.Target=nil +self.TargetCoord=Coordinate +self.LaserCode=LaserCode +self.Lasing=true +local RecceDcsUnit=self.Recce:GetDCSObject() +self.SpotIR=Spot.createInfraRed(RecceDcsUnit,{x=0,y=1,z=0},Coordinate:GetVec3()) +self.SpotLaser=Spot.createLaser(RecceDcsUnit,{x=0,y=1,z=0},Coordinate:GetVec3(),LaserCode) +if Duration then +self.ScheduleID=self.LaseScheduler:Schedule(self,StopLase,{self},Duration) +end +self:__Lasing(-1) +end +function SPOT:OnEventDead(EventData) +self:F({Dead=EventData.IniDCSUnitName,Target=self.Target}) +if self.Target then +if EventData.IniDCSUnitName==self.Target:GetName()then +self:F({"Target dead ",self.Target:GetName()}) +self:Destroyed() +self:LaseOff() +end +end +end +function SPOT:onafterLasing(From,Event,To) +if self.Target and self.Target:IsAlive()then +self.SpotIR:setPoint(self.Target:GetPointVec3():AddY(1):AddY(math.random(-100,100)/100):AddX(math.random(-100,100)/100):GetVec3()) +self.SpotLaser:setPoint(self.Target:GetPointVec3():AddY(1):GetVec3()) +self:__Lasing(-0.2) +elseif self.TargetCoord then +local irvec3={x=self.TargetCoord.x+math.random(-100,100)/100,y=self.TargetCoord.y+math.random(-100,100)/100,z=self.TargetCoord.z} +local lsvec3={x=self.TargetCoord.x,y=self.TargetCoord.y,z=self.TargetCoord.z} +self.SpotIR:setPoint(irvec3) +self.SpotLaser:setPoint(lsvec3) +self:__Lasing(-0.25) +else +self:F({"Target is not alive",self.Target:IsAlive()}) +end +end +function SPOT:onafterLaseOff(From,Event,To) +self:F({"Stopped lasing for ",self.Target and self.Target:GetName()or"coord",SpotIR=self.SportIR,SpotLaser=self.SpotLaser}) +self.Lasing=false +self.SpotIR:destroy() +self.SpotLaser:destroy() +self.SpotIR=nil +self.SpotLaser=nil +if self.ScheduleID then +self.LaseScheduler:Stop(self.ScheduleID) +end +self.ScheduleID=nil +self.Target=nil +return self +end +function SPOT:IsLasing() +return self.Lasing +end +end +ASTAR={ +ClassName="ASTAR", +Debug=nil, +lid=nil, +nodes={}, +counter=1, +Nnodes=0, +ncost=0, +ncostcache=0, +nvalid=0, +nvalidcache=0, +} +ASTAR.INF=1/0 +ASTAR.version="0.4.0" +function ASTAR:New() +local self=BASE:Inherit(self,BASE:New()) +self.lid="ASTAR | " +return self +end +function ASTAR:SetStartCoordinate(Coordinate) +self.startCoord=Coordinate +return self +end +function ASTAR:SetEndCoordinate(Coordinate) +self.endCoord=Coordinate +return self +end +function ASTAR:GetNodeFromCoordinate(Coordinate) +local node={} +node.coordinate=Coordinate +node.surfacetype=Coordinate:GetSurfaceType() +node.id=self.counter +node.valid={} +node.cost={} +self.counter=self.counter+1 +return node +end +function ASTAR:AddNode(Node) +self.nodes[Node.id]=Node +self.Nnodes=self.Nnodes+1 +return self +end +function ASTAR:AddNodeFromCoordinate(Coordinate) +local node=self:GetNodeFromCoordinate(Coordinate) +self:AddNode(node) +return node +end +function ASTAR:CheckValidSurfaceType(Node,SurfaceTypes) +if SurfaceTypes then +if type(SurfaceTypes)~="table"then +SurfaceTypes={SurfaceTypes} +end +for _,surface in pairs(SurfaceTypes)do +if surface==Node.surfacetype then +return true +end +end +return false +else +return true +end +end +function ASTAR:SetValidNeighbourFunction(NeighbourFunction,...) +self.ValidNeighbourFunc=NeighbourFunction +self.ValidNeighbourArg={} +if arg then +self.ValidNeighbourArg=arg +end +return self +end +function ASTAR:SetValidNeighbourLoS(CorridorWidth) +self:SetValidNeighbourFunction(ASTAR.LoS,CorridorWidth) +return self +end +function ASTAR:SetValidNeighbourDistance(MaxDistance) +self:SetValidNeighbourFunction(ASTAR.DistMax,MaxDistance) +return self +end +function ASTAR:SetValidNeighbourRoad(MaxDistance) +self:SetValidNeighbourFunction(ASTAR.Road,MaxDistance) +return self +end +function ASTAR:SetCostFunction(CostFunction,...) +self.CostFunc=CostFunction +self.CostArg={} +if arg then +self.CostArg=arg +end +return self +end +function ASTAR:SetCostDist2D() +self:SetCostFunction(ASTAR.Dist2D) +return self +end +function ASTAR:SetCostDist3D() +self:SetCostFunction(ASTAR.Dist3D) +return self +end +function ASTAR:SetCostRoad() +self:SetCostFunction(ASTAR) +return self +end +function ASTAR:CreateGrid(ValidSurfaceTypes,BoxHY,SpaceX,deltaX,deltaY,MarkGrid) +local Dz=SpaceX or 10000 +local Dx=BoxHY and BoxHY/2 or 20000 +local dz=deltaX or 2000 +local dx=deltaY or dz +local angle=self.startCoord:HeadingTo(self.endCoord) +local dist=self.startCoord:Get2DDistance(self.endCoord)+2*Dz +local co=COORDINATE:New(0,0,0) +local do1=co:Get2DDistance(self.startCoord) +local ho1=co:HeadingTo(self.startCoord) +local xmin=-Dx +local zmin=-Dz +local nz=dist/dz+1 +local nx=2*Dx/dx+1 +local text=string.format("Building grid with nx=%d ny=%d => total=%d nodes",nx,nz,nx*nz) +self:T(self.lid..text) +for i=1,nx do +local x=xmin+dx*(i-1) +for j=1,nz do +local z=zmin+dz*(j-1) +local vec3=UTILS.Rotate2D({x=x,y=0,z=z},angle) +local c=COORDINATE:New(vec3.z,vec3.y,vec3.x):Translate(do1,ho1,true) +local node=self:GetNodeFromCoordinate(c) +if self:CheckValidSurfaceType(node,ValidSurfaceTypes)then +if MarkGrid then +c:MarkToAll(string.format("i=%d, j=%d surface=%d",i,j,node.surfacetype)) +end +self:AddNode(node) +end +end +end +local text=string.format("Done building grid!") +self:T2(self.lid..text) +return self +end +function ASTAR.LoS(nodeA,nodeB,corridor) +local offset=1 +local dx=corridor and corridor/2 or nil +local dy=dx +local cA=nodeA.coordinate:GetVec3() +local cB=nodeB.coordinate:GetVec3() +cA.y=offset +cB.y=offset +local los=land.isVisible(cA,cB) +if los and corridor then +local heading=nodeA.coordinate:HeadingTo(nodeB.coordinate) +local Ap=UTILS.VecTranslate(cA,dx,heading+90) +local Bp=UTILS.VecTranslate(cB,dx,heading+90) +los=land.isVisible(Ap,Bp) +if los then +local Am=UTILS.VecTranslate(cA,dx,heading-90) +local Bm=UTILS.VecTranslate(cB,dx,heading-90) +los=land.isVisible(Am,Bm) +end +end +return los +end +function ASTAR.Road(nodeA,nodeB) +local path=land.findPathOnRoads("roads",nodeA.coordinate.x,nodeA.coordinate.z,nodeB.coordinate.x,nodeB.coordinate.z) +if path then +return true +else +return false +end +end +function ASTAR.DistMax(nodeA,nodeB,distmax) +distmax=distmax or 2000 +local dist=nodeA.coordinate:Get2DDistance(nodeB.coordinate) +return dist<=distmax +end +function ASTAR.Dist2D(nodeA,nodeB) +local dist=nodeA.coordinate:Get2DDistance(nodeB) +return dist +end +function ASTAR.Dist3D(nodeA,nodeB) +local dist=nodeA.coordinate:Get3DDistance(nodeB.coordinate) +return dist +end +function ASTAR.DistRoad(nodeA,nodeB) +local path=land.findPathOnRoads("roads",nodeA.coordinate.x,nodeA.coordinate.z,nodeB.coordinate.x,nodeB.coordinate.z) +if path then +local dist=0 +for i=2,#path do +local b=path[i] +local a=path[i-1] +dist=dist+UTILS.VecDist2D(a,b) +end +return dist +end +return math.huge +end +function ASTAR:FindClosestNode(Coordinate) +local distMin=math.huge +local closeNode=nil +for _,_node in pairs(self.nodes)do +local node=_node +local dist=node.coordinate:Get2DDistance(Coordinate) +if dist1000 then +self:T(self.lid.."Adding start node to node grid!") +self:AddNode(node) +end +return self +end +function ASTAR:FindEndNode() +local node,dist=self:FindClosestNode(self.endCoord) +self.endNode=node +if dist>1000 then +self:T(self.lid.."Adding end node to node grid!") +self:AddNode(node) +end +return self +end +function ASTAR:GetPath(ExcludeStartNode,ExcludeEndNode) +self:FindStartNode() +self:FindEndNode() +local nodes=self.nodes +local start=self.startNode +local goal=self.endNode +local openset={} +local closedset={} +local came_from={} +local g_score={} +local f_score={} +openset[start.id]=true +local Nopen=1 +g_score[start.id]=0 +f_score[start.id]=g_score[start.id]+self:_HeuristicCost(start,goal) +local T0=timer.getAbsTime() +local text=string.format("Starting A* pathfinding with %d Nodes",self.Nnodes) +self:T(self.lid..text) +local Tstart=UTILS.GetOSTime() +while Nopen>0 do +local current=self:_LowestFscore(openset,f_score) +if current.id==goal.id then +local path=self:_UnwindPath({},came_from,goal) +if not ExcludeEndNode then +table.insert(path,goal) +end +if ExcludeStartNode then +table.remove(path,1) +end +local Tstop=UTILS.GetOSTime() +local dT=nil +if Tstart and Tstop then +dT=Tstop-Tstart +end +local text=string.format("Found path with %d nodes (%d total)",#path,self.Nnodes) +if dT then +text=text..string.format(", OS Time %.6f sec",dT) +end +text=text..string.format(", Nvalid=%d [%d cached]",self.nvalid,self.nvalidcache) +text=text..string.format(", Ncost=%d [%d cached]",self.ncost,self.ncostcache) +self:T(self.lid..text) +return path +end +openset[current.id]=nil +Nopen=Nopen-1 +closedset[current.id]=true +local neighbors=self:_NeighbourNodes(current,nodes) +for _,neighbor in pairs(neighbors)do +if self:_NotIn(closedset,neighbor.id)then +local tentative_g_score=g_score[current.id]+self:_DistNodes(current,neighbor) +if self:_NotIn(openset,neighbor.id)or tentative_g_score0 then +AoA=-AoA +end +return math.deg(AoA) +end +end +return nil +end +function POSITIONABLE:GetClimbAngle() +local unitpos=self:GetPosition() +if unitpos then +local unitvel=self:GetVelocityVec3() +if unitvel and UTILS.VecNorm(unitvel)~=0 then +local angle=math.asin(unitvel.y/UTILS.VecNorm(unitvel)) +return math.deg(angle) +else +return 0 +end +end +return nil +end +function POSITIONABLE:GetPitch() +local unitpos=self:GetPosition() +if unitpos then +return math.deg(math.asin(unitpos.x.y)) +end +return nil +end +function POSITIONABLE:GetRoll() +local unitpos=self:GetPosition() +if unitpos then +local cp=UTILS.VecCross(unitpos.x,{x=0,y=1,z=0}) +local dp=UTILS.VecDot(cp,unitpos.z) +local Roll=math.acos(dp/(UTILS.VecNorm(cp)*UTILS.VecNorm(unitpos.z))) +if unitpos.z.y>0 then +Roll=-Roll +end +return math.deg(Roll) +end +end +function POSITIONABLE:GetYaw() +local unitpos=self:GetPosition() +if unitpos then +local unitvel=self:GetVelocityVec3() +if unitvel and UTILS.VecNorm(unitvel)~=0 then +local AxialVel={} +AxialVel.x=UTILS.VecDot(unitpos.x,unitvel) +AxialVel.y=UTILS.VecDot(unitpos.y,unitvel) +AxialVel.z=UTILS.VecDot(unitpos.z,unitvel) +local Yaw=math.acos(UTILS.VecDot({x=1,y=0,z=0},{x=AxialVel.x,y=0,z=AxialVel.z})/UTILS.VecNorm({x=AxialVel.x,y=0,z=AxialVel.z})) +if AxialVel.z>0 then +Yaw=-Yaw +end +return Yaw +end +end +end +function POSITIONABLE:GetMessageText(Message,Name) +local DCSObject=self:GetDCSObject() +if DCSObject then +local Callsign=string.format("%s",((Name~=""and Name)or self:GetCallsign()~=""and self:GetCallsign())or self:GetName()) +local MessageText=string.format("%s - %s",Callsign,Message) +return MessageText +end +return nil +end +function POSITIONABLE:GetMessage(Message,Duration,Name) +local DCSObject=self:GetDCSObject() +if DCSObject then +local MessageText=self:GetMessageText(Message,Name) +return MESSAGE:New(MessageText,Duration) +end +return nil +end +function POSITIONABLE:GetMessageType(Message,MessageType,Name) +local DCSObject=self:GetDCSObject() +if DCSObject then +local MessageText=self:GetMessageText(Message,Name) +return MESSAGE:NewType(MessageText,MessageType) +end +return nil +end +function POSITIONABLE:MessageToAll(Message,Duration,Name) +self:F2({Message,Duration}) +local DCSObject=self:GetDCSObject() +if DCSObject then +self:GetMessage(Message,Duration,Name):ToAll() +end +return nil +end +function POSITIONABLE:MessageToCoalition(Message,Duration,MessageCoalition,Name) +self:F2({Message,Duration}) +local Name=Name or"" +local DCSObject=self:GetDCSObject() +if DCSObject then +self:GetMessage(Message,Duration,Name):ToCoalition(MessageCoalition) +end +return nil +end +function POSITIONABLE:MessageTypeToCoalition(Message,MessageType,MessageCoalition,Name) +self:F2({Message,MessageType}) +local Name=Name or"" +local DCSObject=self:GetDCSObject() +if DCSObject then +self:GetMessageType(Message,MessageType,Name):ToCoalition(MessageCoalition) +end +return nil +end +function POSITIONABLE:MessageToRed(Message,Duration,Name) +self:F2({Message,Duration}) +local DCSObject=self:GetDCSObject() +if DCSObject then +self:GetMessage(Message,Duration,Name):ToRed() +end +return nil +end +function POSITIONABLE:MessageToBlue(Message,Duration,Name) +self:F2({Message,Duration}) +local DCSObject=self:GetDCSObject() +if DCSObject then +self:GetMessage(Message,Duration,Name):ToBlue() +end +return nil +end +function POSITIONABLE:MessageToClient(Message,Duration,Client,Name) +self:F2({Message,Duration}) +local DCSObject=self:GetDCSObject() +if DCSObject then +self:GetMessage(Message,Duration,Name):ToClient(Client) +end +return nil +end +function POSITIONABLE:MessageToGroup(Message,Duration,MessageGroup,Name) +self:F2({Message,Duration}) +local DCSObject=self:GetDCSObject() +if DCSObject then +if DCSObject:isExist()then +if MessageGroup:IsAlive()then +self:GetMessage(Message,Duration,Name):ToGroup(MessageGroup) +else +BASE:E({"Message not sent to Group; Group is not alive...",Message=Message,MessageGroup=MessageGroup}) +end +else +BASE:E({"Message not sent to Group; Positionable is not alive ...",Message=Message,Positionable=self,MessageGroup=MessageGroup}) +end +end +return nil +end +function POSITIONABLE:MessageTypeToGroup(Message,MessageType,MessageGroup,Name) +self:F2({Message,MessageType}) +local DCSObject=self:GetDCSObject() +if DCSObject then +if DCSObject:isExist()then +self:GetMessageType(Message,MessageType,Name):ToGroup(MessageGroup) +end +end +return nil +end +function POSITIONABLE:MessageToSetGroup(Message,Duration,MessageSetGroup,Name) +self:F2({Message,Duration}) +local DCSObject=self:GetDCSObject() +if DCSObject then +if DCSObject:isExist()then +MessageSetGroup:ForEachGroupAlive( +function(MessageGroup) +self:GetMessage(Message,Duration,Name):ToGroup(MessageGroup) +end +) +end +end +return nil +end +function POSITIONABLE:Message(Message,Duration,Name) +self:F2({Message,Duration}) +local DCSObject=self:GetDCSObject() +if DCSObject then +self:GetMessage(Message,Duration,Name):ToGroup(self) +end +return nil +end +function POSITIONABLE:GetRadio() +self:F2(self) +return RADIO:New(self) +end +function POSITIONABLE:GetBeacon() +self:F2(self) +return BEACON:New(self) +end +function POSITIONABLE:LaseUnit(Target,LaserCode,Duration) +self:F2() +LaserCode=LaserCode or math.random(1000,9999) +local RecceDcsUnit=self:GetDCSObject() +local TargetVec3=Target:GetVec3() +self:F("bulding spot") +self.Spot=SPOT:New(self) +self.Spot:LaseOn(Target,LaserCode,Duration) +self.LaserCode=LaserCode +return self.Spot +end +function POSITIONABLE:LaseCoordinate(Coordinate,LaserCode,Duration) +self:F2() +LaserCode=LaserCode or math.random(1000,9999) +self.Spot=SPOT:New(self) +self.Spot:LaseOnCoordinate(Coordinate,LaserCode,Duration) +self.LaserCode=LaserCode +return self.Spot +end +function POSITIONABLE:LaseOff() +self:F2() +if self.Spot then +self.Spot:LaseOff() +self.Spot=nil +end +return self +end +function POSITIONABLE:IsLasing() +self:F2() +local Lasing=false +if self.Spot then +Lasing=self.Spot:IsLasing() +end +return Lasing +end +function POSITIONABLE:GetSpot() +return self.Spot +end +function POSITIONABLE:GetLaserCode() +return self.LaserCode +end +do +function POSITIONABLE:AddCargo(Cargo) +self.__.Cargo[Cargo]=Cargo +return self +end +function POSITIONABLE:GetCargo() +return self.__.Cargo +end +function POSITIONABLE:RemoveCargo(Cargo) +self.__.Cargo[Cargo]=nil +return self +end +function POSITIONABLE:HasCargo(Cargo) +return self.__.Cargo[Cargo] +end +function POSITIONABLE:ClearCargo() +self.__.Cargo={} +end +function POSITIONABLE:IsCargoEmpty() +local IsEmpty=true +for _,Cargo in pairs(self.__.Cargo)do +IsEmpty=false +break +end +return IsEmpty +end +function POSITIONABLE:CargoItemCount() +local ItemCount=0 +for CargoName,Cargo in pairs(self.__.Cargo)do +ItemCount=ItemCount+Cargo:GetCount() +end +return ItemCount +end +function POSITIONABLE:GetCargoBayFreeWeight() +if not self.__.CargoBayWeightLimit then +self:SetCargoBayWeightLimit() +end +local CargoWeight=0 +for CargoName,Cargo in pairs(self.__.Cargo)do +CargoWeight=CargoWeight+Cargo:GetWeight() +end +return self.__.CargoBayWeightLimit-CargoWeight +end +function POSITIONABLE:SetCargoBayWeightLimit(WeightLimit) +if WeightLimit then +self.__.CargoBayWeightLimit=WeightLimit +elseif self.__.CargoBayWeightLimit~=nil then +else +if self:IsAir()then +local Desc=self:GetDesc() +self:F({Desc=Desc}) +local Weights={ +["C-17A"]=35000, +["C-130"]=22000 +} +self.__.CargoBayWeightLimit=Weights[Desc.typeName]or(Desc.massMax-(Desc.massEmpty+Desc.fuelMassMax)) +elseif self:IsShip()then +local Desc=self:GetDesc() +self:F({Desc=Desc}) +local Weights={ +["Type_071"]=245000, +["LHA_Tarawa"]=500000, +["Ropucha-class"]=150000, +["Dry-cargo ship-1"]=70000, +["Dry-cargo ship-2"]=70000, +["Higgins_boat"]=3700, +["USS_Samuel_Chase"]=25000, +["LST_Mk2"]=2100000, +} +self.__.CargoBayWeightLimit=(Weights[Desc.typeName]or 50000) +else +local Desc=self:GetDesc() +local Weights={ +["AAV7"]=25, +["Bedford_MWD"]=8, +["Blitz_36-6700A"]=10, +["BMD-1"]=9, +["BMP-1"]=8, +["BMP-2"]=7, +["BMP-3"]=8, +["Boman"]=25, +["BTR-80"]=9, +["BTR-82A"]=9, +["BTR_D"]=12, +["Cobra"]=8, +["Land_Rover_101_FC"]=11, +["Land_Rover_109_S3"]=7, +["LAV-25"]=6, +["M-2 Bradley"]=6, +["M1043 HMMWV Armament"]=4, +["M1045 HMMWV TOW"]=4, +["M1126 Stryker ICV"]=9, +["M1134 Stryker ATGM"]=9, +["M2A1_halftrack"]=9, +["M-113"]=9, +["Marder"]=6, +["MCV-80"]=9, +["MLRS FDDM"]=4, +["MTLB"]=25, +["GAZ-66"]=8, +["GAZ-3307"]=12, +["GAZ-3308"]=14, +["Grad_FDDM"]=6, +["KAMAZ Truck"]=12, +["KrAZ6322"]=12, +["M 818"]=12, +["Tigr_233036"]=6, +["TPZ"]=10, +["UAZ-469"]=4, +["Ural-375"]=12, +["Ural-4320-31"]=14, +["Ural-4320 APA-5D"]=10, +["Ural-4320T"]=14, +["ZBD04A"]=7, +} +local CargoBayWeightLimit=(Weights[Desc.typeName]or 0)*95 +self.__.CargoBayWeightLimit=CargoBayWeightLimit +end +end +self:F({CargoBayWeightLimit=self.__.CargoBayWeightLimit}) +end +end +function POSITIONABLE:Flare(FlareColor) +self:F2() +trigger.action.signalFlare(self:GetVec3(),FlareColor,0) +end +function POSITIONABLE:FlareWhite() +self:F2() +trigger.action.signalFlare(self:GetVec3(),trigger.flareColor.White,0) +end +function POSITIONABLE:FlareYellow() +self:F2() +trigger.action.signalFlare(self:GetVec3(),trigger.flareColor.Yellow,0) +end +function POSITIONABLE:FlareGreen() +self:F2() +trigger.action.signalFlare(self:GetVec3(),trigger.flareColor.Green,0) +end +function POSITIONABLE:FlareRed() +self:F2() +local Vec3=self:GetVec3() +if Vec3 then +trigger.action.signalFlare(Vec3,trigger.flareColor.Red,0) +end +end +function POSITIONABLE:Smoke(SmokeColor,Range,AddHeight) +self:F2() +if Range then +local Vec3=self:GetRandomVec3(Range) +Vec3.y=Vec3.y+AddHeight or 0 +trigger.action.smoke(Vec3,SmokeColor) +else +local Vec3=self:GetVec3() +Vec3.y=Vec3.y+AddHeight or 0 +trigger.action.smoke(self:GetVec3(),SmokeColor) +end +end +function POSITIONABLE:SmokeGreen() +self:F2() +trigger.action.smoke(self:GetVec3(),trigger.smokeColor.Green) +end +function POSITIONABLE:SmokeRed() +self:F2() +trigger.action.smoke(self:GetVec3(),trigger.smokeColor.Red) +end +function POSITIONABLE:SmokeWhite() +self:F2() +trigger.action.smoke(self:GetVec3(),trigger.smokeColor.White) +end +function POSITIONABLE:SmokeOrange() +self:F2() +trigger.action.smoke(self:GetVec3(),trigger.smokeColor.Orange) +end +function POSITIONABLE:SmokeBlue() +self:F2() +trigger.action.smoke(self:GetVec3(),trigger.smokeColor.Blue) +end +function POSITIONABLE:IsInZone(Zone) +self:F2({self.PositionableName,Zone}) +if self:IsAlive()then +local IsInZone=Zone:IsVec3InZone(self:GetVec3()) +return IsInZone +end +return false +end +function POSITIONABLE:IsNotInZone(Zone) +self:F2({self.PositionableName,Zone}) +if self:IsAlive()then +local IsNotInZone=not Zone:IsVec3InZone(self:GetVec3()) +return IsNotInZone +else +return false +end +end +CONTROLLABLE={ +ClassName="CONTROLLABLE", +ControllableName="", +WayPointFunctions={}, +} +function CONTROLLABLE:New(ControllableName) +local self=BASE:Inherit(self,POSITIONABLE:New(ControllableName)) +self.ControllableName=ControllableName +self.TaskScheduler=SCHEDULER:New(self) +return self +end +function CONTROLLABLE:_GetController() +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local ControllableController=DCSControllable:getController() +return ControllableController +end +return nil +end +function CONTROLLABLE:GetLife() +self:F2(self.ControllableName) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local UnitLife=0 +local Units=self:GetUnits() +if#Units==1 then +local Unit=Units[1] +UnitLife=Unit:GetLife() +else +local UnitLifeTotal=0 +for UnitID,Unit in pairs(Units)do +local Unit=Unit +UnitLifeTotal=UnitLifeTotal+Unit:GetLife() +end +UnitLife=UnitLifeTotal/#Units +end +return UnitLife +end +return nil +end +function CONTROLLABLE:GetLife0() +self:F2(self.ControllableName) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local UnitLife=0 +local Units=self:GetUnits() +if#Units==1 then +local Unit=Units[1] +UnitLife=Unit:GetLife0() +else +local UnitLifeTotal=0 +for UnitID,Unit in pairs(Units)do +local Unit=Unit +UnitLifeTotal=UnitLifeTotal+Unit:GetLife0() +end +UnitLife=UnitLifeTotal/#Units +end +return UnitLife +end +return nil +end +function CONTROLLABLE:GetFuelMin() +self:F(self.ControllableName) +return nil +end +function CONTROLLABLE:GetFuelAve() +self:F(self.ControllableName) +return nil +end +function CONTROLLABLE:GetFuel() +self:F(self.ControllableName) +return nil +end +function CONTROLLABLE:ClearTasks() +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local Controller=self:_GetController() +Controller:resetTask() +return self +end +return nil +end +function CONTROLLABLE:PopCurrentTask() +self:F2() +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local Controller=self:_GetController() +Controller:popTask() +return self +end +return nil +end +function CONTROLLABLE:PushTask(DCSTask,WaitTime) +self:F2() +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local DCSControllableName=self:GetName() +local function PushTask(Controller,DCSTask) +if self and self:IsAlive()then +local Controller=self:_GetController() +Controller:pushTask(DCSTask) +else +BASE:E({DCSControllableName.." is not alive anymore.",DCSTask=DCSTask}) +end +end +if not WaitTime or WaitTime==0 then +PushTask(self,DCSTask) +else +self.TaskScheduler:Schedule(self,PushTask,{DCSTask},WaitTime) +end +return self +end +return nil +end +function CONTROLLABLE:SetTask(DCSTask,WaitTime) +self:F({"SetTask",WaitTime,DCSTask=DCSTask}) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local DCSControllableName=self:GetName() +self:T2("Controllable Name = "..DCSControllableName) +local function SetTask(Controller,DCSTask) +if self and self:IsAlive()then +local Controller=self:_GetController() +Controller:setTask(DCSTask) +self:T({ControllableName=self:GetName(),DCSTask=DCSTask}) +else +BASE:E({DCSControllableName.." is not alive anymore.",DCSTask=DCSTask}) +end +end +if not WaitTime or WaitTime==0 then +SetTask(self,DCSTask) +self:T({ControllableName=self:GetName(),DCSTask=DCSTask}) +else +self.TaskScheduler:Schedule(self,SetTask,{DCSTask},WaitTime) +end +return self +end +return nil +end +function CONTROLLABLE:HasTask() +local HasTaskResult=false +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local Controller=self:_GetController() +HasTaskResult=Controller:hasTask() +end +return HasTaskResult +end +function CONTROLLABLE:TaskCondition(time,userFlag,userFlagValue,condition,duration,lastWayPoint) +local DCSStopCondition={} +DCSStopCondition.time=time +DCSStopCondition.userFlag=userFlag +DCSStopCondition.userFlagValue=userFlagValue +DCSStopCondition.condition=condition +DCSStopCondition.duration=duration +DCSStopCondition.lastWayPoint=lastWayPoint +return DCSStopCondition +end +function CONTROLLABLE:TaskControlled(DCSTask,DCSStopCondition) +local DCSTaskControlled={ +id='ControlledTask', +params={ +task=DCSTask, +stopCondition=DCSStopCondition +} +} +return DCSTaskControlled +end +function CONTROLLABLE:TaskCombo(DCSTasks) +local DCSTaskCombo={ +id='ComboTask', +params={ +tasks=DCSTasks +} +} +return DCSTaskCombo +end +function CONTROLLABLE:TaskWrappedAction(DCSCommand,Index) +local DCSTaskWrappedAction={ +id="WrappedAction", +enabled=true, +number=Index or 1, +auto=false, +params={ +action=DCSCommand, +}, +} +return DCSTaskWrappedAction +end +function CONTROLLABLE:SetTaskWaypoint(Waypoint,Task) +Waypoint.task=self:TaskCombo({Task}) +self:F({Waypoint.task}) +return Waypoint.task +end +function CONTROLLABLE:SetCommand(DCSCommand) +self:F2(DCSCommand) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local Controller=self:_GetController() +Controller:setCommand(DCSCommand) +return self +end +return nil +end +function CONTROLLABLE:CommandSwitchWayPoint(FromWayPoint,ToWayPoint) +self:F2({FromWayPoint,ToWayPoint}) +local CommandSwitchWayPoint={ +id='SwitchWaypoint', +params={ +fromWaypointIndex=FromWayPoint, +goToWaypointIndex=ToWayPoint, +}, +} +self:T3({CommandSwitchWayPoint}) +return CommandSwitchWayPoint +end +function CONTROLLABLE:CommandStopRoute(StopRoute) +self:F2({StopRoute}) +local CommandStopRoute={ +id='StopRoute', +params={ +value=StopRoute, +}, +} +self:T3({CommandStopRoute}) +return CommandStopRoute +end +function CONTROLLABLE:StartUncontrolled(delay) +if delay and delay>0 then +SCHEDULER:New(nil,CONTROLLABLE.StartUncontrolled,{self},delay) +else +self:SetCommand({id='Start',params={}}) +end +return self +end +function CONTROLLABLE:CommandActivateBeacon(Type,System,Frequency,UnitID,Channel,ModeChannel,AA,Callsign,Bearing,Delay) +AA=AA or self:IsAir() +UnitID=UnitID or self:GetID() +local CommandActivateBeacon={ +id="ActivateBeacon", +params={ +["type"]=Type, +["system"]=System, +["frequency"]=Frequency, +["unitId"]=UnitID, +["channel"]=Channel, +["modeChannel"]=ModeChannel, +["AA"]=AA, +["callsign"]=Callsign, +["bearing"]=Bearing, +} +} +if Delay and Delay>0 then +SCHEDULER:New(nil,self.CommandActivateBeacon,{self,Type,System,Frequency,UnitID,Channel,ModeChannel,AA,Callsign,Bearing},Delay) +else +self:SetCommand(CommandActivateBeacon) +end +return self +end +function CONTROLLABLE:CommandActivateICLS(Channel,UnitID,Callsign,Delay) +local CommandActivateICLS={ +id="ActivateICLS", +params={ +["type"]=BEACON.Type.ICLS, +["channel"]=Channel, +["unitId"]=UnitID, +["callsign"]=Callsign, +} +} +if Delay and Delay>0 then +SCHEDULER:New(nil,self.CommandActivateICLS,{self},Delay) +else +self:SetCommand(CommandActivateICLS) +end +return self +end +function CONTROLLABLE:CommandDeactivateBeacon(Delay) +local CommandDeactivateBeacon={id='DeactivateBeacon',params={}} +if Delay and Delay>0 then +SCHEDULER:New(nil,self.CommandActivateBeacon,{self},Delay) +else +self:SetCommand(CommandDeactivateBeacon) +end +return self +end +function CONTROLLABLE:CommandDeactivateICLS(Delay) +local CommandDeactivateICLS={id='DeactivateICLS',params={}} +if Delay and Delay>0 then +SCHEDULER:New(nil,self.CommandDeactivateICLS,{self},Delay) +else +self:SetCommand(CommandDeactivateICLS) +end +return self +end +function CONTROLLABLE:CommandSetCallsign(CallName,CallNumber,Delay) +local CommandSetCallsign={id='SetCallsign',params={callname=CallName,number=CallNumber or 1}} +if Delay and Delay>0 then +SCHEDULER:New(nil,self.CommandSetCallsign,{self,CallName,CallNumber},Delay) +else +self:SetCommand(CommandSetCallsign) +end +return self +end +function CONTROLLABLE:CommandEPLRS(SwitchOnOff,Delay) +if SwitchOnOff==nil then +SwitchOnOff=true +end +local CommandEPLRS={ +id='EPLRS', +params={ +value=SwitchOnOff, +groupId=self:GetID() +} +} +if Delay and Delay>0 then +SCHEDULER:New(nil,self.CommandEPLRS,{self,SwitchOnOff},Delay) +else +self:T(string.format("EPLRS=%s for controllable %s (id=%s)",tostring(SwitchOnOff),tostring(self:GetName()),tostring(self:GetID()))) +self:SetCommand(CommandEPLRS) +end +return self +end +function CONTROLLABLE:CommandSetFrequency(Frequency,Modulation,Delay) +local CommandSetFrequency={ +id='SetFrequency', +params={ +frequency=Frequency*1000000, +modulation=Modulation or radio.modulation.AM, +} +} +if Delay and Delay>0 then +SCHEDULER:New(nil,self.CommandSetFrequency,{self,Frequency,Modulation},Delay) +else +self:SetCommand(CommandSetFrequency) +end +return self +end +function CONTROLLABLE:TaskEPLRS(SwitchOnOff,idx) +if SwitchOnOff==nil then +SwitchOnOff=true +end +local CommandEPLRS={ +id='EPLRS', +params={ +value=SwitchOnOff, +groupId=self:GetID() +} +} +return self:TaskWrappedAction(CommandEPLRS,idx or 1) +end +function CONTROLLABLE:TaskAttackGroup(AttackGroup,WeaponType,WeaponExpend,AttackQty,Direction,Altitude,AttackQtyLimit,GroupAttack) +local DCSTask={id='AttackGroup', +params={ +groupId=AttackGroup:GetID(), +weaponType=WeaponType or 1073741822, +expend=WeaponExpend or"Auto", +attackQtyLimit=AttackQty and true or false, +attackQty=AttackQty or 1, +directionEnabled=Direction and true or false, +direction=Direction and math.rad(Direction)or 0, +altitudeEnabled=Altitude and true or false, +altitude=Altitude, +groupAttack=GroupAttack and true or false, +}, +} +return DCSTask +end +function CONTROLLABLE:TaskAttackUnit(AttackUnit,GroupAttack,WeaponExpend,AttackQty,Direction,Altitude,WeaponType) +local DCSTask={ +id='AttackUnit', +params={ +unitId=AttackUnit:GetID(), +groupAttack=GroupAttack and GroupAttack or false, +expend=WeaponExpend or"Auto", +directionEnabled=Direction and true or false, +direction=Direction and math.rad(Direction)or 0, +altitudeEnabled=Altitude and true or false, +altitude=Altitude, +attackQtyLimit=AttackQty and true or false, +attackQty=AttackQty, +weaponType=WeaponType or 1073741822, +} +} +return DCSTask +end +function CONTROLLABLE:TaskBombing(Vec2,GroupAttack,WeaponExpend,AttackQty,Direction,Altitude,WeaponType,Divebomb) +local DCSTask={ +id='Bombing', +params={ +point=Vec2, +x=Vec2.x, +y=Vec2.y, +groupAttack=GroupAttack and GroupAttack or false, +expend=WeaponExpend or"Auto", +attackQtyLimit=AttackQty and true or false, +attackQty=AttackQty or 1, +directionEnabled=Direction and true or false, +direction=Direction and math.rad(Direction)or 0, +altitudeEnabled=Altitude and true or false, +altitude=Altitude or 2000, +weaponType=WeaponType or 1073741822, +attackType=Divebomb and"Dive"or nil, +}, +} +return DCSTask +end +function CONTROLLABLE:TaskAttackMapObject(Vec2,GroupAttack,WeaponExpend,AttackQty,Direction,Altitude,WeaponType) +local DCSTask={ +id='AttackMapObject', +params={ +point=Vec2, +x=Vec2.x, +y=Vec2.y, +groupAttack=GroupAttack or false, +expend=WeaponExpend or"Auto", +attackQtyLimit=AttackQty and true or false, +attackQty=AttackQty, +directionEnabled=Direction and true or false, +direction=Direction and math.rad(Direction)or 0, +altitudeEnabled=Altitude and true or false, +altitude=Altitude, +weaponType=WeaponType or 1073741822, +}, +} +return DCSTask +end +function CONTROLLABLE:TaskCarpetBombing(Vec2,GroupAttack,WeaponExpend,AttackQty,Direction,Altitude,WeaponType,CarpetLength) +local DCSTask={ +id='CarpetBombing', +params={ +attackType="Carpet", +x=Vec2.x, +y=Vec2.y, +groupAttack=GroupAttack and GroupAttack or false, +carpetLength=CarpetLength or 500, +weaponType=WeaponType or ENUMS.WeaponFlag.AnyBomb, +expend=WeaponExpend or"All", +attackQtyLimit=AttackQty and true or false, +attackQty=AttackQty or 1, +directionEnabled=Direction and true or false, +direction=Direction and math.rad(Direction)or 0, +altitudeEnabled=Altitude and true or false, +altitude=Altitude, +} +} +return DCSTask +end +function CONTROLLABLE:TaskFollowBigFormation(FollowControllable,Vec3,LastWaypointIndex) +local DCSTask={ +id='FollowBigFormation', +params={ +groupId=FollowControllable:GetID(), +pos=Vec3, +lastWptIndexFlag=LastWaypointIndex and true or false, +lastWptIndex=LastWaypointIndex +} +} +return DCSTask +end +function CONTROLLABLE:TaskEmbarking(Coordinate,GroupSetForEmbarking,Duration,Distribution) +local g4e={} +if GroupSetForEmbarking then +for _,_group in pairs(GroupSetForEmbarking:GetSet())do +local group=_group +table.insert(g4e,group:GetID()) +end +else +self:E("ERROR: No groups for embarking specified!") +return nil +end +local groupID=self and self:GetID() +local DCSTask={ +id='Embarking', +params={ +selectedTransport=groupID, +x=Coordinate.x, +y=Coordinate.z, +groupsForEmbarking=g4e, +durationFlag=Duration and true or false, +duration=Duration, +distributionFlag=Distribution and true or false, +distribution=Distribution, +} +} +return DCSTask +end +function CONTROLLABLE:TaskEmbarkToTransport(Coordinate,Radius,UnitType) +local EmbarkToTransport={ +id="EmbarkToTransport", +params={ +x=Coordinate.x, +y=Coordinate.z, +zoneRadius=Radius or 200, +selectedType=UnitType, +} +} +return EmbarkToTransport +end +function CONTROLLABLE:TaskDisembarking(Coordinate,GroupSetToDisembark) +local g4e={} +if GroupSetToDisembark then +for _,_group in pairs(GroupSetToDisembark:GetSet())do +local group=_group +table.insert(g4e,group:GetID()) +end +else +self:E("ERROR: No groups for disembarking specified!") +return nil +end +local Disembarking={ +id="Disembarking", +params={ +x=Coordinate.x, +y=Coordinate.z, +groupsForEmbarking=g4e, +} +} +return Disembarking +end +function CONTROLLABLE:TaskOrbitCircleAtVec2(Point,Altitude,Speed) +self:F2({self.ControllableName,Point,Altitude,Speed}) +local DCSTask={ +id='Orbit', +params={ +pattern=AI.Task.OrbitPattern.CIRCLE, +point=Point, +speed=Speed, +altitude=Altitude+land.getHeight(Point) +} +} +return DCSTask +end +function CONTROLLABLE:TaskOrbit(Coord,Altitude,Speed,CoordRaceTrack) +local Pattern=AI.Task.OrbitPattern.CIRCLE +local P1=Coord:GetVec2() +local P2=nil +if CoordRaceTrack then +Pattern=AI.Task.OrbitPattern.RACE_TRACK +P2=CoordRaceTrack:GetVec2() +end +local Task={ +id='Orbit', +params={ +pattern=Pattern, +point=P1, +point2=P2, +speed=Speed or UTILS.KnotsToMps(250), +altitude=Altitude or Coord.y, +} +} +return Task +end +function CONTROLLABLE:TaskOrbitCircle(Altitude,Speed,Coordinate) +self:F2({self.ControllableName,Altitude,Speed}) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local OrbitVec2=Coordinate and Coordinate:GetVec2()or self:GetVec2() +return self:TaskOrbitCircleAtVec2(OrbitVec2,Altitude,Speed) +end +return nil +end +function CONTROLLABLE:TaskHoldPosition() +self:F2({self.ControllableName}) +return self:TaskOrbitCircle(30,10) +end +function CONTROLLABLE:TaskBombingRunway(Airbase,WeaponType,WeaponExpend,AttackQty,Direction,GroupAttack) +local DCSTask={ +id='BombingRunway', +params={ +runwayId=Airbase:GetID(), +weaponType=WeaponType or ENUMS.WeaponFlag.AnyBomb, +expend=WeaponExpend or AI.Task.WeaponExpend.ALL, +attackQty=AttackQty or 1, +direction=Direction and math.rad(Direction)or 0, +groupAttack=GroupAttack and true or false, +}, +} +return DCSTask +end +function CONTROLLABLE:TaskRefueling() +local DCSTask={ +id='Refueling', +params={} +} +return DCSTask +end +function CONTROLLABLE:TaskLandAtVec2(Vec2,Duration) +local DCSTask={ +id='Land', +params={ +point=Vec2, +durationFlag=Duration and true or false, +duration=Duration, +}, +} +return DCSTask +end +function CONTROLLABLE:TaskLandAtZone(Zone,Duration,RandomPoint) +local Point=RandomPoint and Zone:GetRandomVec2()or Zone:GetVec2() +local DCSTask=CONTROLLABLE.TaskLandAtVec2(self,Point,Duration) +return DCSTask +end +function CONTROLLABLE:TaskFollow(FollowControllable,Vec3,LastWaypointIndex) +self:F2({self.ControllableName,FollowControllable,Vec3,LastWaypointIndex}) +local LastWaypointIndexFlag=false +local lastWptIndexFlagChangedManually=false +if LastWaypointIndex then +LastWaypointIndexFlag=true +lastWptIndexFlagChangedManually=true +end +local DCSTask={ +id='Follow', +params={ +groupId=FollowControllable:GetID(), +pos=Vec3, +lastWptIndexFlag=LastWaypointIndexFlag, +lastWptIndex=LastWaypointIndex, +lastWptIndexFlagChangedManually=lastWptIndexFlagChangedManually, +} +} +self:T3({DCSTask}) +return DCSTask +end +function CONTROLLABLE:TaskEscort(FollowControllable,Vec3,LastWaypointIndex,EngagementDistance,TargetTypes) +local DCSTask +DCSTask={ +id='Escort', +params={ +groupId=FollowControllable:GetID(), +pos=Vec3, +lastWptIndexFlag=LastWaypointIndex and true or false, +lastWptIndex=LastWaypointIndex, +engagementDistMax=EngagementDistance, +targetTypes=TargetTypes or{"Air"}, +}, +} +return DCSTask +end +function CONTROLLABLE:TaskFireAtPoint(Vec2,Radius,AmmoCount,WeaponType,Altitude,ASL) +local DCSTask={ +id='FireAtPoint', +params={ +point=Vec2, +x=Vec2.x, +y=Vec2.y, +zoneRadius=Radius, +radius=Radius, +expendQty=100, +expendQtyEnabled=false, +alt_type=ASL and 0 or 1 +} +} +if AmmoCount then +DCSTask.params.expendQty=AmmoCount +DCSTask.params.expendQtyEnabled=true +end +if Altitude then +DCSTask.params.altitude=Altitude +end +if WeaponType then +DCSTask.params.weaponType=WeaponType +end +return DCSTask +end +function CONTROLLABLE:TaskHold() +local DCSTask={id='Hold',params={}} +return DCSTask +end +function CONTROLLABLE:TaskFAC_AttackGroup(AttackGroup,WeaponType,Designation,Datalink,Frequency,Modulation,CallsignName,CallsignNumber) +local DCSTask={ +id='FAC_AttackGroup', +params={ +groupId=AttackGroup:GetID(), +weaponType=WeaponType or ENUMS.WeaponFlag.AutoDCS, +designation=Designation or"Auto", +datalink=Datalink and Datalink or true, +frequency=(Frequency or 133)*1000000, +modulation=Modulation or radio.modulation.AM, +callname=CallsignName, +number=CallsignNumber, +} +} +return DCSTask +end +function CONTROLLABLE:EnRouteTaskEngageTargets(Distance,TargetTypes,Priority) +local DCSTask={ +id='EngageTargets', +params={ +maxDistEnabled=Distance and true or false, +maxDist=Distance, +targetTypes=TargetTypes or{"Air"}, +priority=Priority or 0, +} +} +return DCSTask +end +function CONTROLLABLE:EnRouteTaskEngageTargetsInZone(Vec2,Radius,TargetTypes,Priority) +local DCSTask={ +id='EngageTargetsInZone', +params={ +point=Vec2, +zoneRadius=Radius, +targetTypes=TargetTypes or{"Air"}, +priority=Priority or 0 +} +} +return DCSTask +end +function CONTROLLABLE:EnRouteTaskEngageGroup(AttackGroup,Priority,WeaponType,WeaponExpend,AttackQty,Direction,Altitude,AttackQtyLimit) +local DCSTask={ +id='EngageControllable', +params={ +groupId=AttackGroup:GetID(), +weaponType=WeaponType, +expend=WeaponExpend or"Auto", +directionEnabled=Direction and true or false, +direction=Direction, +altitudeEnabled=Altitude and true or false, +altitude=Altitude, +attackQtyLimit=AttackQty and true or false, +attackQty=AttackQty, +priority=Priority or 1, +}, +} +return DCSTask +end +function CONTROLLABLE:EnRouteTaskEngageUnit(EngageUnit,Priority,GroupAttack,WeaponExpend,AttackQty,Direction,Altitude,Visible,ControllableAttack) +local DCSTask={ +id='EngageUnit', +params={ +unitId=EngageUnit:GetID(), +priority=Priority or 1, +groupAttack=GroupAttack and GroupAttack or false, +visible=Visible and Visible or false, +expend=WeaponExpend or"Auto", +directionEnabled=Direction and true or false, +direction=Direction and math.rad(Direction)or nil, +altitudeEnabled=Altitude and true or false, +altitude=Altitude, +attackQtyLimit=AttackQty and true or false, +attackQty=AttackQty, +controllableAttack=ControllableAttack, +}, +} +return DCSTask +end +function CONTROLLABLE:EnRouteTaskAWACS() +local DCSTask={ +id='AWACS', +params={}, +} +return DCSTask +end +function CONTROLLABLE:EnRouteTaskTanker() +local DCSTask={ +id='Tanker', +params={}, +} +return DCSTask +end +function CONTROLLABLE:EnRouteTaskEWR() +local DCSTask={ +id='EWR', +params={}, +} +return DCSTask +end +function CONTROLLABLE:EnRouteTaskFAC_EngageGroup(AttackGroup,Priority,WeaponType,Designation,Datalink) +local DCSTask={ +id='FAC_EngageControllable', +params={ +groupId=AttackGroup:GetID(), +weaponType=WeaponType or"Auto", +designation=Designation, +datalink=Datalink and Datalink or false, +priority=Priority or 0, +} +} +return DCSTask +end +function CONTROLLABLE:EnRouteTaskFAC(Radius,Priority) +local DCSTask={ +id='FAC', +params={ +radius=Radius, +priority=Priority +} +} +return DCSTask +end +function CONTROLLABLE:TaskFunction(FunctionString,...) +local DCSScript={} +DCSScript[#DCSScript+1]="local MissionControllable = GROUP:Find( ... ) " +if arg and arg.n>0 then +local ArgumentKey='_'..tostring(arg):match("table: (.*)") +self:SetState(self,ArgumentKey,arg) +DCSScript[#DCSScript+1]="local Arguments = MissionControllable:GetState( MissionControllable, '"..ArgumentKey.."' ) " +DCSScript[#DCSScript+1]=FunctionString.."( MissionControllable, unpack( Arguments ) )" +else +DCSScript[#DCSScript+1]=FunctionString.."( MissionControllable )" +end +local DCSTask=self:TaskWrappedAction(self:CommandDoScript(table.concat(DCSScript))) +return DCSTask +end +function CONTROLLABLE:TaskMission(TaskMission) +local DCSTask={ +id='Mission', +params={TaskMission,}, +} +return DCSTask +end +do +function CONTROLLABLE:PatrolRoute() +local PatrolGroup=self +if not self:IsInstanceOf("GROUP")then +PatrolGroup=self:GetGroup() +end +self:F({PatrolGroup=PatrolGroup:GetName()}) +if PatrolGroup:IsGround()or PatrolGroup:IsShip()then +local Waypoints=PatrolGroup:GetTemplateRoutePoints() +local FromCoord=PatrolGroup:GetCoordinate() +local From=FromCoord:WaypointGround(120) +table.insert(Waypoints,1,From) +local TaskRoute=PatrolGroup:TaskFunction("CONTROLLABLE.PatrolRoute") +self:F({Waypoints=Waypoints}) +local Waypoint=Waypoints[#Waypoints] +PatrolGroup:SetTaskWaypoint(Waypoint,TaskRoute) +PatrolGroup:Route(Waypoints) +end +end +function CONTROLLABLE:PatrolRouteRandom(Speed,Formation,ToWaypoint) +local PatrolGroup=self +if not self:IsInstanceOf("GROUP")then +PatrolGroup=self:GetGroup() +end +self:F({PatrolGroup=PatrolGroup:GetName()}) +if PatrolGroup:IsGround()or PatrolGroup:IsShip()then +local Waypoints=PatrolGroup:GetTemplateRoutePoints() +local FromCoord=PatrolGroup:GetCoordinate() +local FromWaypoint=1 +if ToWaypoint then +FromWaypoint=ToWaypoint +end +local ToWaypoint +repeat +ToWaypoint=math.random(1,#Waypoints) +until(ToWaypoint~=FromWaypoint) +self:F({FromWaypoint=FromWaypoint,ToWaypoint=ToWaypoint}) +local Waypoint=Waypoints[ToWaypoint] +local ToCoord=COORDINATE:NewFromVec2({x=Waypoint.x,y=Waypoint.y}) +local Route={} +Route[#Route+1]=FromCoord:WaypointGround(Speed,Formation) +Route[#Route+1]=ToCoord:WaypointGround(Speed,Formation) +local TaskRouteToZone=PatrolGroup:TaskFunction("CONTROLLABLE.PatrolRouteRandom",Speed,Formation,ToWaypoint) +PatrolGroup:SetTaskWaypoint(Route[#Route],TaskRouteToZone) +PatrolGroup:Route(Route,1) +end +end +function CONTROLLABLE:PatrolZones(ZoneList,Speed,Formation,DelayMin,DelayMax) +if not type(ZoneList)=="table"then +ZoneList={ZoneList} +end +local PatrolGroup=self +if not self:IsInstanceOf("GROUP")then +PatrolGroup=self:GetGroup() +end +DelayMin=DelayMin or 1 +if not DelayMax or DelayMaxLengthDirect*10)or(LengthRoad/LengthOnRoad*100<5)) +self:T(string.format("Length on road = %.3f km",LengthOnRoad/1000)) +self:T(string.format("Length directly = %.3f km",LengthDirect/1000)) +self:T(string.format("Length fraction = %.3f km",LengthOnRoad/LengthDirect)) +self:T(string.format("Length only road = %.3f km",LengthRoad/1000)) +self:T(string.format("Length off road = %.3f km",LengthOffRoad/1000)) +self:T(string.format("Percent on road = %.1f",LengthRoad/LengthOnRoad*100)) +end +local route={} +local canroad=false +if GotPath and LengthRoad and LengthDirect>2000 then +if LongRoad and Shortcut then +table.insert(route,FromCoordinate:WaypointGround(Speed,OffRoadFormation)) +table.insert(route,ToCoordinate:WaypointGround(Speed,OffRoadFormation)) +else +table.insert(route,FromCoordinate:WaypointGround(Speed,OffRoadFormation)) +table.insert(route,PathOnRoad[2]:WaypointGround(Speed,"On Road")) +table.insert(route,PathOnRoad[#PathOnRoad-1]:WaypointGround(Speed,"On Road")) +local dist=ToCoordinate:Get2DDistance(PathOnRoad[#PathOnRoad-1]) +if dist>10 then +table.insert(route,ToCoordinate:WaypointGround(Speed,OffRoadFormation)) +table.insert(route,ToCoordinate:GetRandomCoordinateInRadius(10,5):WaypointGround(5,OffRoadFormation)) +table.insert(route,ToCoordinate:GetRandomCoordinateInRadius(10,5):WaypointGround(5,OffRoadFormation)) +end +end +canroad=true +else +table.insert(route,FromCoordinate:WaypointGround(Speed,OffRoadFormation)) +table.insert(route,ToCoordinate:WaypointGround(Speed,OffRoadFormation)) +end +if WaypointFunction then +local N=#route +for n,waypoint in pairs(route)do +waypoint.task={} +waypoint.task.id="ComboTask" +waypoint.task.params={} +waypoint.task.params.tasks={self:TaskFunction("CONTROLLABLE.___PassingWaypoint",n,N,WaypointFunction,unpack(WaypointFunctionArguments or{}))} +end +end +return route,canroad +end +function CONTROLLABLE:TaskGroundOnRailRoads(ToCoordinate,Speed,WaypointFunction,WaypointFunctionArguments) +self:F2({ToCoordinate=ToCoordinate,Speed=Speed}) +Speed=Speed or 20 +local FromCoordinate=self:GetCoordinate() +local PathOnRail,LengthOnRail=FromCoordinate:GetPathOnRoad(ToCoordinate,false,true) +self:T(string.format("Length on railroad = %.3f km",LengthOnRail/1000)) +local route={} +if PathOnRail then +table.insert(route,PathOnRail[1]:WaypointGround(Speed,"On Railroad")) +table.insert(route,PathOnRail[2]:WaypointGround(Speed,"On Railroad")) +end +if WaypointFunction then +local N=#route +for n,waypoint in pairs(route)do +waypoint.task={} +waypoint.task.id="ComboTask" +waypoint.task.params={} +waypoint.task.params.tasks={self:TaskFunction("CONTROLLABLE.___PassingWaypoint",n,N,WaypointFunction,unpack(WaypointFunctionArguments or{}))} +end +end +return route +end +function CONTROLLABLE.___PassingWaypoint(controllable,n,N,waypointfunction,...) +waypointfunction(controllable,n,N,...) +end +function CONTROLLABLE:RouteAirTo(ToCoordinate,AltType,Type,Action,Speed,DelaySeconds) +local FromCoordinate=self:GetCoordinate() +local FromWP=FromCoordinate:WaypointAir() +local ToWP=ToCoordinate:WaypointAir(AltType,Type,Action,Speed) +self:Route({FromWP,ToWP},DelaySeconds) +return self +end +function CONTROLLABLE:TaskRouteToZone(Zone,Randomize,Speed,Formation) +self:F2(Zone) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local ControllablePoint=self:GetVec2() +local PointFrom={} +PointFrom.x=ControllablePoint.x +PointFrom.y=ControllablePoint.y +PointFrom.type="Turning Point" +PointFrom.action=Formation or"Cone" +PointFrom.speed=20/3.6 +local PointTo={} +local ZonePoint +if Randomize then +ZonePoint=Zone:GetRandomVec2() +else +ZonePoint=Zone:GetVec2() +end +PointTo.x=ZonePoint.x +PointTo.y=ZonePoint.y +PointTo.type="Turning Point" +if Formation then +PointTo.action=Formation +else +PointTo.action="Cone" +end +if Speed then +PointTo.speed=Speed +else +PointTo.speed=20/3.6 +end +local Points={PointFrom,PointTo} +self:T3(Points) +self:Route(Points) +return self +end +return nil +end +function CONTROLLABLE:TaskRouteToVec2(Vec2,Speed,Formation) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local ControllablePoint=self:GetVec2() +local PointFrom={} +PointFrom.x=ControllablePoint.x +PointFrom.y=ControllablePoint.y +PointFrom.type="Turning Point" +PointFrom.action=Formation or"Cone" +PointFrom.speed=20/3.6 +local PointTo={} +PointTo.x=Vec2.x +PointTo.y=Vec2.y +PointTo.type="Turning Point" +if Formation then +PointTo.action=Formation +else +PointTo.action="Cone" +end +if Speed then +PointTo.speed=Speed +else +PointTo.speed=20/3.6 +end +local Points={PointFrom,PointTo} +self:T3(Points) +self:Route(Points) +return self +end +return nil +end +end +function CONTROLLABLE:CommandDoScript(DoScript) +local DCSDoScript={ +id="Script", +params={ +command=DoScript, +}, +} +self:T3(DCSDoScript) +return DCSDoScript +end +function CONTROLLABLE:GetTaskMission() +self:F2(self.ControllableName) +return routines.utils.deepCopy(_DATABASE.Templates.Controllables[self.ControllableName].Template) +end +function CONTROLLABLE:GetTaskRoute() +self:F2(self.ControllableName) +return routines.utils.deepCopy(_DATABASE.Templates.Controllables[self.ControllableName].Template.route.points) +end +function CONTROLLABLE:CopyRoute(Begin,End,Randomize,Radius) +self:F2({Begin,End}) +local Points={} +local ControllableName=string.match(self:GetName(),".*#") +if ControllableName then +ControllableName=ControllableName:sub(1,-2) +else +ControllableName=self:GetName() +end +self:T3({ControllableName}) +local Template=_DATABASE.Templates.Controllables[ControllableName].Template +if Template then +if not Begin then +Begin=0 +end +if not End then +End=0 +end +for TPointID=Begin+1,#Template.route.points-End do +if Template.route.points[TPointID]then +Points[#Points+1]=routines.utils.deepCopy(Template.route.points[TPointID]) +if Randomize then +if not Radius then +Radius=500 +end +Points[#Points].x=Points[#Points].x+math.random(Radius*-1,Radius) +Points[#Points].y=Points[#Points].y+math.random(Radius*-1,Radius) +end +end +end +return Points +else +error("Template not found for Controllable : "..ControllableName) +end +return nil +end +function CONTROLLABLE:GetDetectedTargets(DetectVisual,DetectOptical,DetectRadar,DetectIRST,DetectRWR,DetectDLINK) +self:F2(self.ControllableName) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local DetectionVisual=(DetectVisual and DetectVisual==true)and Controller.Detection.VISUAL or nil +local DetectionOptical=(DetectOptical and DetectOptical==true)and Controller.Detection.OPTICAL or nil +local DetectionRadar=(DetectRadar and DetectRadar==true)and Controller.Detection.RADAR or nil +local DetectionIRST=(DetectIRST and DetectIRST==true)and Controller.Detection.IRST or nil +local DetectionRWR=(DetectRWR and DetectRWR==true)and Controller.Detection.RWR or nil +local DetectionDLINK=(DetectDLINK and DetectDLINK==true)and Controller.Detection.DLINK or nil +local Params={} +if DetectionVisual then +Params[#Params+1]=DetectionVisual +end +if DetectionOptical then +Params[#Params+1]=DetectionOptical +end +if DetectionRadar then +Params[#Params+1]=DetectionRadar +end +if DetectionIRST then +Params[#Params+1]=DetectionIRST +end +if DetectionRWR then +Params[#Params+1]=DetectionRWR +end +if DetectionDLINK then +Params[#Params+1]=DetectionDLINK +end +self:T2({DetectionVisual,DetectionOptical,DetectionRadar,DetectionIRST,DetectionRWR,DetectionDLINK}) +return self:_GetController():getDetectedTargets(Params[1],Params[2],Params[3],Params[4],Params[5],Params[6]) +end +return nil +end +function CONTROLLABLE:IsTargetDetected(DCSObject,DetectVisual,DetectOptical,DetectRadar,DetectIRST,DetectRWR,DetectDLINK) +self:F2(self.ControllableName) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local DetectionVisual=(DetectVisual and DetectVisual==true)and Controller.Detection.VISUAL or nil +local DetectionOptical=(DetectOptical and DetectOptical==true)and Controller.Detection.OPTICAL or nil +local DetectionRadar=(DetectRadar and DetectRadar==true)and Controller.Detection.RADAR or nil +local DetectionIRST=(DetectIRST and DetectIRST==true)and Controller.Detection.IRST or nil +local DetectionRWR=(DetectRWR and DetectRWR==true)and Controller.Detection.RWR or nil +local DetectionDLINK=(DetectDLINK and DetectDLINK==true)and Controller.Detection.DLINK or nil +local Controller=self:_GetController() +local TargetIsDetected,TargetIsVisible,TargetLastTime,TargetKnowType,TargetKnowDistance,TargetLastPos,TargetLastVelocity +=Controller:isTargetDetected(DCSObject,DetectionVisual,DetectionOptical,DetectionRadar,DetectionIRST,DetectionRWR,DetectionDLINK) +return TargetIsDetected,TargetIsVisible,TargetLastTime,TargetKnowType,TargetKnowDistance,TargetLastPos,TargetLastVelocity +end +return nil +end +function CONTROLLABLE:IsUnitDetected(Unit,DetectVisual,DetectOptical,DetectRadar,DetectIRST,DetectRWR,DetectDLINK) +self:F2(self.ControllableName) +if Unit and Unit:IsAlive()then +return self:IsTargetDetected(Unit:GetDCSObject(),DetectVisual,DetectOptical,DetectRadar,DetectIRST,DetectRWR,DetectDLINK) +end +return nil +end +function CONTROLLABLE:IsGroupDetected(Group,DetectVisual,DetectOptical,DetectRadar,DetectIRST,DetectRWR,DetectDLINK) +self:F2(self.ControllableName) +if Group and Group:IsAlive()then +for _,_unit in pairs(Group:GetUnits())do +local unit=_unit +if unit and unit:IsAlive()then +local isdetected=self:IsUnitDetected(unit,DetectVisual,DetectOptical,DetectRadar,DetectIRST,DetectRWR,DetectDLINK) +if isdetected then +return true +end +end +end +return false +end +return nil +end +function CONTROLLABLE:GetDetectedUnitSet(DetectVisual,DetectOptical,DetectRadar,DetectIRST,DetectRWR,DetectDLINK) +local detectedtargets=self:GetDetectedTargets(DetectVisual,DetectOptical,DetectRadar,DetectIRST,DetectRWR,DetectDLINK) +local unitset=SET_UNIT:New() +for DetectionObjectID,Detection in pairs(detectedtargets or{})do +local DetectedObject=Detection.object +if DetectedObject and DetectedObject:isExist()and DetectedObject.id_<50000000 then +local unit=UNIT:Find(DetectedObject) +if unit and unit:IsAlive()then +if not unitset:FindUnit(unit:GetName())then +unitset:AddUnit(unit) +end +end +end +end +return unitset +end +function CONTROLLABLE:GetDetectedGroupSet(DetectVisual,DetectOptical,DetectRadar,DetectIRST,DetectRWR,DetectDLINK) +local detectedtargets=self:GetDetectedTargets(DetectVisual,DetectOptical,DetectRadar,DetectIRST,DetectRWR,DetectDLINK) +local groupset=SET_GROUP:New() +for DetectionObjectID,Detection in pairs(detectedtargets or{})do +local DetectedObject=Detection.object +if DetectedObject and DetectedObject:isExist()and DetectedObject.id_<50000000 then +local unit=UNIT:Find(DetectedObject) +if unit and unit:IsAlive()then +local group=unit:GetGroup() +if group and not groupset:FindGroup(group:GetName())then +groupset:AddGroup(group) +end +end +end +end +return groupset +end +function CONTROLLABLE:SetOption(OptionID,OptionValue) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local Controller=self:_GetController() +Controller:setOption(OptionID,OptionValue) +return self +end +return nil +end +function CONTROLLABLE:OptionROE(ROEvalue) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local Controller=self:_GetController() +if self:IsAir()then +Controller:setOption(AI.Option.Air.id.ROE,ROEvalue) +elseif self:IsGround()then +Controller:setOption(AI.Option.Ground.id.ROE,ROEvalue) +elseif self:IsShip()then +Controller:setOption(AI.Option.Naval.id.ROE,ROEvalue) +end +return self +end +return nil +end +function CONTROLLABLE:OptionROEHoldFirePossible() +self:F2({self.ControllableName}) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +if self:IsAir()or self:IsGround()or self:IsShip()then +return true +end +return false +end +return nil +end +function CONTROLLABLE:OptionROEHoldFire() +self:F2({self.ControllableName}) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local Controller=self:_GetController() +if self:IsAir()then +Controller:setOption(AI.Option.Air.id.ROE,AI.Option.Air.val.ROE.WEAPON_HOLD) +elseif self:IsGround()then +Controller:setOption(AI.Option.Ground.id.ROE,AI.Option.Ground.val.ROE.WEAPON_HOLD) +elseif self:IsShip()then +Controller:setOption(AI.Option.Naval.id.ROE,AI.Option.Naval.val.ROE.WEAPON_HOLD) +end +return self +end +return nil +end +function CONTROLLABLE:OptionROEReturnFirePossible() +self:F2({self.ControllableName}) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +if self:IsAir()or self:IsGround()or self:IsShip()then +return true +end +return false +end +return nil +end +function CONTROLLABLE:OptionROEReturnFire() +self:F2({self.ControllableName}) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local Controller=self:_GetController() +if self:IsAir()then +Controller:setOption(AI.Option.Air.id.ROE,AI.Option.Air.val.ROE.RETURN_FIRE) +elseif self:IsGround()then +Controller:setOption(AI.Option.Ground.id.ROE,AI.Option.Ground.val.ROE.RETURN_FIRE) +elseif self:IsShip()then +Controller:setOption(AI.Option.Naval.id.ROE,AI.Option.Naval.val.ROE.RETURN_FIRE) +end +return self +end +return nil +end +function CONTROLLABLE:OptionROEOpenFirePossible() +self:F2({self.ControllableName}) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +if self:IsAir()or self:IsGround()or self:IsShip()then +return true +end +return false +end +return nil +end +function CONTROLLABLE:OptionROEOpenFire() +self:F2({self.ControllableName}) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local Controller=self:_GetController() +if self:IsAir()then +Controller:setOption(AI.Option.Air.id.ROE,AI.Option.Air.val.ROE.OPEN_FIRE) +elseif self:IsGround()then +Controller:setOption(AI.Option.Ground.id.ROE,AI.Option.Ground.val.ROE.OPEN_FIRE) +elseif self:IsShip()then +Controller:setOption(AI.Option.Naval.id.ROE,AI.Option.Naval.val.ROE.OPEN_FIRE) +end +return self +end +return nil +end +function CONTROLLABLE:OptionROEOpenFireWeaponFreePossible() +self:F2({self.ControllableName}) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +if self:IsAir()then +return true +end +return false +end +return nil +end +function CONTROLLABLE:OptionROEOpenFireWeaponFree() +self:F2({self.ControllableName}) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local Controller=self:_GetController() +if self:IsAir()then +Controller:setOption(AI.Option.Air.id.ROE,AI.Option.Air.val.ROE.OPEN_FIRE_WEAPON_FREE) +end +return self +end +return nil +end +function CONTROLLABLE:OptionROEWeaponFreePossible() +self:F2({self.ControllableName}) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +if self:IsAir()then +return true +end +return false +end +return nil +end +function CONTROLLABLE:OptionROEWeaponFree() +self:F2({self.ControllableName}) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local Controller=self:_GetController() +if self:IsAir()then +Controller:setOption(AI.Option.Air.id.ROE,AI.Option.Air.val.ROE.WEAPON_FREE) +end +return self +end +return nil +end +function CONTROLLABLE:OptionROTNoReactionPossible() +self:F2({self.ControllableName}) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +if self:IsAir()then +return true +end +return false +end +return nil +end +function CONTROLLABLE:OptionROTNoReaction() +self:F2({self.ControllableName}) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local Controller=self:_GetController() +if self:IsAir()then +Controller:setOption(AI.Option.Air.id.REACTION_ON_THREAT,AI.Option.Air.val.REACTION_ON_THREAT.NO_REACTION) +end +return self +end +return nil +end +function CONTROLLABLE:OptionROT(ROTvalue) +self:F2({self.ControllableName}) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local Controller=self:_GetController() +if self:IsAir()then +Controller:setOption(AI.Option.Air.id.REACTION_ON_THREAT,ROTvalue) +end +return self +end +return nil +end +function CONTROLLABLE:OptionROTPassiveDefensePossible() +self:F2({self.ControllableName}) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +if self:IsAir()then +return true +end +return false +end +return nil +end +function CONTROLLABLE:OptionROTPassiveDefense() +self:F2({self.ControllableName}) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local Controller=self:_GetController() +if self:IsAir()then +Controller:setOption(AI.Option.Air.id.REACTION_ON_THREAT,AI.Option.Air.val.REACTION_ON_THREAT.PASSIVE_DEFENCE) +end +return self +end +return nil +end +function CONTROLLABLE:OptionROTEvadeFirePossible() +self:F2({self.ControllableName}) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +if self:IsAir()then +return true +end +return false +end +return nil +end +function CONTROLLABLE:OptionROTEvadeFire() +self:F2({self.ControllableName}) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local Controller=self:_GetController() +if self:IsAir()then +Controller:setOption(AI.Option.Air.id.REACTION_ON_THREAT,AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE) +end +return self +end +return nil +end +function CONTROLLABLE:OptionROTVerticalPossible() +self:F2({self.ControllableName}) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +if self:IsAir()then +return true +end +return false +end +return nil +end +function CONTROLLABLE:OptionROTVertical() +self:F2({self.ControllableName}) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local Controller=self:_GetController() +if self:IsAir()then +Controller:setOption(AI.Option.Air.id.REACTION_ON_THREAT,AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE) +end +return self +end +return nil +end +function CONTROLLABLE:OptionAlarmStateAuto() +self:F2({self.ControllableName}) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local Controller=self:_GetController() +if self:IsGround()then +Controller:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.AUTO) +elseif self:IsShip()then +Controller:setOption(9,0) +end +return self +end +return nil +end +function CONTROLLABLE:OptionAlarmStateGreen() +self:F2({self.ControllableName}) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local Controller=self:_GetController() +if self:IsGround()then +Controller:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) +elseif self:IsShip()then +Controller:setOption(9,1) +end +return self +end +return nil +end +function CONTROLLABLE:OptionAlarmStateRed() +self:F2({self.ControllableName}) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local Controller=self:_GetController() +if self:IsGround()then +Controller:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.RED) +elseif self:IsShip()then +Controller:setOption(9,2) +end +return self +end +return nil +end +function CONTROLLABLE:OptionRTBBingoFuel(RTB) +self:F2({self.ControllableName}) +if RTB==nil then +RTB=true +end +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local Controller=self:_GetController() +if self:IsAir()then +Controller:setOption(AI.Option.Air.id.RTB_ON_BINGO,RTB) +end +return self +end +return nil +end +function CONTROLLABLE:OptionRTBAmmo(WeaponsFlag) +self:F2({self.ControllableName}) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local Controller=self:_GetController() +if self:IsAir()then +Controller:setOption(AI.Option.Air.id.RTB_ON_OUT_OF_AMMO,WeaponsFlag) +end +return self +end +return nil +end +function CONTROLLABLE:OptionAllowJettisonWeaponsOnThreat() +self:F2({self.ControllableName}) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local Controller=self:_GetController() +if self:IsAir()then +Controller:setOption(AI.Option.Air.id.PROHIBIT_JETT,false) +end +return self +end +return nil +end +function CONTROLLABLE:OptionKeepWeaponsOnThreat() +self:F2({self.ControllableName}) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local Controller=self:_GetController() +if self:IsAir()then +Controller:setOption(AI.Option.Air.id.PROHIBIT_JETT,true) +end +return self +end +return nil +end +function CONTROLLABLE:OptionProhibitAfterburner(Prohibit) +self:F2({self.ControllableName}) +if Prohibit==nil then +Prohibit=true +end +if self:IsAir()then +self:SetOption(AI.Option.Air.id.PROHIBIT_AB,Prohibit) +end +return self +end +function CONTROLLABLE:OptionECM_Never() +self:F2({self.ControllableName}) +if self:IsAir()then +self:SetOption(AI.Option.Air.id.ECM_USING,0) +end +return self +end +function CONTROLLABLE:OptionECM_OnlyLockByRadar() +self:F2({self.ControllableName}) +if self:IsAir()then +self:SetOption(AI.Option.Air.id.ECM_USING,1) +end +return self +end +function CONTROLLABLE:OptionECM_DetectedLockByRadar() +self:F2({self.ControllableName}) +if self:IsAir()then +self:SetOption(AI.Option.Air.id.ECM_USING,2) +end +return self +end +function CONTROLLABLE:OptionECM_AlwaysOn() +self:F2({self.ControllableName}) +if self:IsAir()then +self:SetOption(AI.Option.Air.id.ECM_USING,3) +end +return self +end +function CONTROLLABLE:WayPointInitialize(WayPoints) +self:F({WayPoints}) +if WayPoints then +self.WayPoints=WayPoints +else +self.WayPoints=self:GetTaskRoute() +end +return self +end +function CONTROLLABLE:GetWayPoints() +self:F() +if self.WayPoints then +return self.WayPoints +end +return nil +end +function CONTROLLABLE:WayPointFunction(WayPoint,WayPointIndex,WayPointFunction,...) +self:F2({WayPoint,WayPointIndex,WayPointFunction}) +table.insert(self.WayPoints[WayPoint].task.params.tasks,WayPointIndex) +self.WayPoints[WayPoint].task.params.tasks[WayPointIndex]=self:TaskFunction(WayPointFunction,arg) +return self +end +function CONTROLLABLE:WayPointExecute(WayPoint,WaitTime) +self:F({WayPoint,WaitTime}) +if not WayPoint then +WayPoint=1 +end +for TaskPointID=1,WayPoint-1 do +table.remove(self.WayPoints,1) +end +self:T3(self.WayPoints) +self:SetTask(self:TaskRoute(self.WayPoints),WaitTime) +return self +end +function CONTROLLABLE:IsAirPlane() +self:F2() +local DCSObject=self:GetDCSObject() +if DCSObject then +local Category=DCSObject:getDesc().category +return Category==Unit.Category.AIRPLANE +end +return nil +end +function CONTROLLABLE:IsHelicopter() +self:F2() +local DCSObject=self:GetDCSObject() +if DCSObject then +local Category=DCSObject:getDesc().category +return Category==Unit.Category.HELICOPTER +end +return nil +end +function CONTROLLABLE:OptionRestrictBurner(RestrictBurner) +self:F2({self.ControllableName}) +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local Controller=self:_GetController() +if Controller then +if RestrictBurner==true then +if self:IsAir()then +Controller:setOption(16,true) +end +else +if self:IsAir()then +Controller:setOption(16,false) +end +end +end +end +end +function CONTROLLABLE:OptionAAAttackRange(range) +self:F2({self.ControllableName}) +local range=range or 3 +if range<0 or range>4 then +range=3 +end +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local Controller=self:_GetController() +if Controller then +if self:IsAir()then +self:SetOption(AI.Option.Air.val.MISSILE_ATTACK,range) +end +end +return self +end +return nil +end +function CONTROLLABLE:OptionEngageRange(EngageRange) +self:F2({self.ControllableName}) +EngageRange=EngageRange or 100 +if EngageRange<0 or EngageRange>100 then +EngageRange=100 +end +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local Controller=self:_GetController() +if Controller then +if self:IsGround()then +self:SetOption(AI.Option.Ground.id.AC_ENGAGEMENT_RANGE_RESTRICTION,EngageRange) +end +end +return self +end +return nil +end +function CONTROLLABLE:RelocateGroundRandomInRadius(speed,radius,onroad,shortcut) +self:F2({self.ControllableName}) +local _coord=self:GetCoordinate() +local _radius=radius or 500 +local _speed=speed or 20 +local _tocoord=_coord:GetRandomCoordinateInRadius(_radius,100) +local _onroad=onroad or true +local _grptsk={} +local _candoroad=false +local _shortcut=shortcut or false +if onroad then +_grptsk,_candoroad=self:TaskGroundOnRoad(_tocoord,_speed,"Off Road",_shortcut) +self:Route(_grptsk,5) +else +self:TaskRouteToVec2(_tocoord:GetVec2(),_speed,"Off Road") +end +return self +end +function CONTROLLABLE:OptionDisperseOnAttack(Seconds) +self:F2({self.ControllableName}) +local seconds=Seconds or 0 +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local Controller=self:_GetController() +if Controller then +if self:IsGround()then +self:SetOption(AI.Option.GROUND.id.DISPERSE_ON_ATTACK,seconds) +end +end +return self +end +return nil +end +GROUP={ +ClassName="GROUP", +} +GROUP.Takeoff={ +Air=1, +Runway=2, +Hot=3, +Cold=4, +} +GROUPTEMPLATE={} +GROUPTEMPLATE.Takeoff={ +[GROUP.Takeoff.Air]={"Turning Point","Turning Point"}, +[GROUP.Takeoff.Runway]={"TakeOff","From Runway"}, +[GROUP.Takeoff.Hot]={"TakeOffParkingHot","From Parking Area Hot"}, +[GROUP.Takeoff.Cold]={"TakeOffParking","From Parking Area"} +} +GROUP.Attribute={ +AIR_TRANSPORTPLANE="Air_TransportPlane", +AIR_AWACS="Air_AWACS", +AIR_FIGHTER="Air_Fighter", +AIR_BOMBER="Air_Bomber", +AIR_TANKER="Air_Tanker", +AIR_TRANSPORTHELO="Air_TransportHelo", +AIR_ATTACKHELO="Air_AttackHelo", +AIR_UAV="Air_UAV", +AIR_OTHER="Air_OtherAir", +GROUND_APC="Ground_APC", +GROUND_TRUCK="Ground_Truck", +GROUND_INFANTRY="Ground_Infantry", +GROUND_ARTILLERY="Ground_Artillery", +GROUND_TANK="Ground_Tank", +GROUND_TRAIN="Ground_Train", +GROUND_EWR="Ground_EWR", +GROUND_AAA="Ground_AAA", +GROUND_SAM="Ground_SAM", +GROUND_OTHER="Ground_OtherGround", +NAVAL_AIRCRAFTCARRIER="Naval_AircraftCarrier", +NAVAL_WARSHIP="Naval_WarShip", +NAVAL_ARMEDSHIP="Naval_ArmedShip", +NAVAL_UNARMEDSHIP="Naval_UnarmedShip", +NAVAL_OTHER="Naval_OtherNaval", +OTHER_UNKNOWN="Other_Unknown", +} +function GROUP:NewTemplate(GroupTemplate,CoalitionSide,CategoryID,CountryID) +local GroupName=GroupTemplate.name +_DATABASE:_RegisterGroupTemplate(GroupTemplate,CoalitionSide,CategoryID,CountryID,GroupName) +local self=BASE:Inherit(self,CONTROLLABLE:New(GroupName)) +self.GroupName=GroupName +if not _DATABASE.GROUPS[GroupName]then +_DATABASE.GROUPS[GroupName]=self +end +self:SetEventPriority(4) +return self +end +function GROUP:Register(GroupName) +local self=BASE:Inherit(self,CONTROLLABLE:New(GroupName)) +self.GroupName=GroupName +self:SetEventPriority(4) +return self +end +function GROUP:Find(DCSGroup) +local GroupName=DCSGroup:getName() +local GroupFound=_DATABASE:FindGroup(GroupName) +return GroupFound +end +function GROUP:FindByName(GroupName) +local GroupFound=_DATABASE:FindGroup(GroupName) +return GroupFound +end +function GROUP:GetDCSObject() +local DCSGroup=Group.getByName(self.GroupName) +if DCSGroup then +return DCSGroup +end +return nil +end +function GROUP:GetPositionVec3() +self:F2(self.PositionableName) +local DCSPositionable=self:GetDCSObject() +if DCSPositionable then +local unit=DCSPositionable:getUnits()[1] +if unit then +local PositionablePosition=unit:getPosition().p +self:T3(PositionablePosition) +return PositionablePosition +end +end +return nil +end +function GROUP:IsAlive() +self:F2(self.GroupName) +local DCSGroup=self:GetDCSObject() +if DCSGroup then +if DCSGroup:isExist()then +local DCSUnit=DCSGroup:getUnit(1) +if DCSUnit then +local GroupIsAlive=DCSUnit:isActive() +self:T3(GroupIsAlive) +return GroupIsAlive +end +end +end +return nil +end +function GROUP:IsActive() +self:F2(self.GroupName) +local DCSGroup=self:GetDCSObject() +if DCSGroup then +local unit=DCSGroup:getUnit(1) +if unit then +local GroupIsActive=unit:isActive() +return GroupIsActive +end +end +return nil +end +function GROUP:Destroy(GenerateEvent,delay) +self:F2(self.GroupName) +if delay and delay>0 then +self:ScheduleOnce(delay,GROUP.Destroy,self,GenerateEvent) +else +local DCSGroup=self:GetDCSObject() +if DCSGroup then +for Index,UnitData in pairs(DCSGroup:getUnits())do +if GenerateEvent and GenerateEvent==true then +if self:IsAir()then +self:CreateEventCrash(timer.getTime(),UnitData) +else +self:CreateEventDead(timer.getTime(),UnitData) +end +elseif GenerateEvent==false then +else +self:CreateEventRemoveUnit(timer.getTime(),UnitData) +end +end +USERFLAG:New(self:GetName()):Set(100) +DCSGroup:destroy() +DCSGroup=nil +end +end +return nil +end +function GROUP:GetCategory() +self:F2(self.GroupName) +local DCSGroup=self:GetDCSObject() +if DCSGroup then +local GroupCategory=DCSGroup:getCategory() +self:T3(GroupCategory) +return GroupCategory +end +return nil +end +function GROUP:GetCategoryName() +self:F2(self.GroupName) +local DCSGroup=self:GetDCSObject() +if DCSGroup then +local CategoryNames={ +[Group.Category.AIRPLANE]="Airplane", +[Group.Category.HELICOPTER]="Helicopter", +[Group.Category.GROUND]="Ground Unit", +[Group.Category.SHIP]="Ship", +[Group.Category.TRAIN]="Train", +} +local GroupCategory=DCSGroup:getCategory() +self:T3(GroupCategory) +return CategoryNames[GroupCategory] +end +return nil +end +function GROUP:GetCoalition() +self:F2(self.GroupName) +local DCSGroup=self:GetDCSObject() +if DCSGroup then +local GroupCoalition=DCSGroup:getCoalition() +self:T3(GroupCoalition) +return GroupCoalition +end +return nil +end +function GROUP:GetCountry() +self:F2(self.GroupName) +local DCSGroup=self:GetDCSObject() +if DCSGroup then +local GroupCountry=DCSGroup:getUnit(1):getCountry() +self:T3(GroupCountry) +return GroupCountry +end +return nil +end +function GROUP:HasAttribute(attribute,all) +local _units=self:GetUnits() +local _allhave=true +local _onehas=false +for _,_unit in pairs(_units)do +local _unit=_unit +if _unit then +local _hastit=_unit:HasAttribute(attribute) +if _hastit==true then +_onehas=true +else +_allhave=false +end +end +end +if all==true then +return _allhave +else +return _onehas +end +end +function GROUP:GetSpeedMax() +self:F2(self.GroupName) +local DCSGroup=self:GetDCSObject() +if DCSGroup then +local Units=self:GetUnits() +local speedmax=nil +for _,unit in pairs(Units)do +local unit=unit +local speed=unit:GetSpeedMax() +if speedmax==nil then +speedmax=speed +elseif speed0 then +self:ScheduleOnce(delay,GROUP.Activate,self) +else +trigger.action.activateGroup(self:GetDCSObject()) +end +return self +end +function GROUP:GetTypeName() +self:F2(self.GroupName) +local DCSGroup=self:GetDCSObject() +if DCSGroup then +local GroupTypeName=DCSGroup:getUnit(1):getTypeName() +self:T3(GroupTypeName) +return(GroupTypeName) +end +return nil +end +function GROUP:GetPlayerName() +self:F2(self.GroupName) +local DCSGroup=self:GetDCSObject() +if DCSGroup then +local PlayerName=DCSGroup:getUnit(1):getPlayerName() +self:T3(PlayerName) +return(PlayerName) +end +return nil +end +function GROUP:GetCallsign() +self:F2(self.GroupName) +local DCSGroup=self:GetDCSObject() +if DCSGroup then +local GroupCallSign=DCSGroup:getUnit(1):getCallsign() +self:T3(GroupCallSign) +return GroupCallSign +end +BASE:E({"Cannot GetCallsign",Positionable=self,Alive=self:IsAlive()}) +return nil +end +function GROUP:GetVec2() +local Unit=self:GetUnit(1) +if Unit then +local vec2=Unit:GetVec2() +return vec2 +end +end +function GROUP:GetVec3() +local unit=self:GetUnit(1) +if unit then +local vec3=unit:GetVec3() +return vec3 +end +self:E("ERROR: Cannot get Vec3 of group "..tostring(self.GroupName)) +return nil +end +function GROUP:GetPointVec2() +self:F2(self.GroupName) +local FirstUnit=self:GetUnit(1) +if FirstUnit then +local FirstUnitPointVec2=FirstUnit:GetPointVec2() +self:T3(FirstUnitPointVec2) +return FirstUnitPointVec2 +end +BASE:E({"Cannot GetPointVec2",Group=self,Alive=self:IsAlive()}) +return nil +end +function GROUP:GetCoordinate() +local FirstUnit=self:GetUnit(1) +if FirstUnit then +local FirstUnitCoordinate=FirstUnit:GetCoordinate() +return FirstUnitCoordinate +end +BASE:E({"Cannot GetCoordinate",Group=self,Alive=self:IsAlive()}) +return nil +end +function GROUP:GetRandomVec3(Radius) +self:F2(self.GroupName) +local FirstUnit=self:GetUnit(1) +if FirstUnit then +local FirstUnitRandomPointVec3=FirstUnit:GetRandomVec3(Radius) +self:T3(FirstUnitRandomPointVec3) +return FirstUnitRandomPointVec3 +end +BASE:E({"Cannot GetRandomVec3",Group=self,Alive=self:IsAlive()}) +return nil +end +function GROUP:GetHeading() +self:F2(self.GroupName) +local GroupSize=self:GetSize() +local HeadingAccumulator=0 +local n=0 +if GroupSize then +for i=1,GroupSize do +local unit=self:GetUnit(i) +if unit and unit:IsAlive()then +HeadingAccumulator=HeadingAccumulator+unit:GetHeading() +n=n+1 +end +end +return math.floor(HeadingAccumulator/n) +end +BASE:E({"Cannot GetHeading",Group=self,Alive=self:IsAlive()}) +return nil +end +function GROUP:GetFuelMin() +self:F3(self.ControllableName) +if not self:GetDCSObject()then +BASE:E({"Cannot GetFuel",Group=self,Alive=self:IsAlive()}) +return 0 +end +local min=65535 +local unit=nil +local tmp=nil +for UnitID,UnitData in pairs(self:GetUnits())do +if UnitData and UnitData:IsAlive()then +tmp=UnitData:GetFuel() +if tmpGroupVelocityMax then +GroupVelocityMax=UnitVelocity +end +end +return GroupVelocityMax +end +return nil +end +function GROUP:GetMinHeight() +self:F2() +local DCSGroup=self:GetDCSObject() +if DCSGroup then +local GroupHeightMin=999999999 +for Index,UnitData in pairs(DCSGroup:getUnits())do +local UnitData=UnitData +local UnitHeight=UnitData:getPoint() +if UnitHeightGroupHeightMax then +GroupHeightMax=UnitHeight +end +end +return GroupHeightMax +end +return nil +end +function GROUP:GetTemplate() +local GroupName=self:GetName() +return UTILS.DeepCopy(_DATABASE:GetGroupTemplate(GroupName)) +end +function GROUP:GetTemplateRoutePoints() +local GroupName=self:GetName() +return UTILS.DeepCopy(_DATABASE:GetGroupTemplate(GroupName).route.points) +end +function GROUP:SetTemplateControlled(Template,Controlled) +Template.uncontrolled=not Controlled +return Template +end +function GROUP:SetTemplateCountry(Template,CountryID) +Template.CountryID=CountryID +return Template +end +function GROUP:SetTemplateCoalition(Template,CoalitionID) +Template.CoalitionID=CoalitionID +return Template +end +function GROUP:InitHeading(Heading) +self.InitRespawnHeading=Heading +return self +end +function GROUP:InitHeight(Height) +self.InitRespawnHeight=Height +return self +end +function GROUP:InitZone(Zone) +self.InitRespawnZone=Zone +return self +end +function GROUP:InitRandomizePositionZone(PositionZone) +self.InitRespawnRandomizePositionZone=PositionZone +self.InitRespawnRandomizePositionInner=nil +self.InitRespawnRandomizePositionOuter=nil +return self +end +function GROUP:InitRandomizePositionRadius(OuterRadius,InnerRadius) +self.InitRespawnRandomizePositionZone=nil +self.InitRespawnRandomizePositionOuter=OuterRadius +self.InitRespawnRandomizePositionInner=InnerRadius +return self +end +function GROUP:InitCoordinate(coordinate) +self:F({coordinate=coordinate}) +self.InitCoord=coordinate +return self +end +function GROUP:InitRadioCommsOnOff(switch) +self:F({switch=switch}) +if switch==true or switch==nil then +self.InitRespawnRadio=true +else +self.InitRespawnRadio=false +end +return self +end +function GROUP:InitRadioFrequency(frequency) +self:F({frequency=frequency}) +self.InitRespawnFreq=frequency +return self +end +function GROUP:InitRadioModulation(modulation) +self:F({modulation=modulation}) +if modulation and modulation:lower()=="fm"then +self.InitRespawnModu=radio.modulation.FM +else +self.InitRespawnModu=radio.modulation.AM +end +return self +end +function GROUP:InitModex(modex) +self:F({modex=modex}) +if modex then +self.InitRespawnModex=tonumber(modex) +end +return self +end +function GROUP:Respawn(Template,Reset) +Template=Template or self:GetTemplate() +local function _Heading(course) +local h +if course<=180 then +h=math.rad(course) +else +h=-math.rad(360-course) +end +return h +end +if self:IsAlive()then +local Zone=self.InitRespawnZone +local Vec3=Zone and Zone:GetVec3()or self:GetVec3() +local From={x=Template.x,y=Template.y} +Template.x=Vec3.x +Template.y=Vec3.z +self:F(#Template.units) +if Reset==true then +for UnitID,UnitData in pairs(self:GetUnits())do +local GroupUnit=UnitData +self:F(GroupUnit:GetName()) +if GroupUnit:IsAlive()then +self:I("FF Alive") +local GroupUnitVec3=GroupUnit:GetVec3() +if Zone then +if self.InitRespawnRandomizePositionZone then +GroupUnitVec3=Zone:GetRandomVec3() +else +if self.InitRespawnRandomizePositionInner and self.InitRespawnRandomizePositionOuter then +GroupUnitVec3=POINT_VEC3:NewFromVec2(From):GetRandomPointVec3InRadius(self.InitRespawnRandomizePositionsOuter,self.InitRespawnRandomizePositionsInner) +else +GroupUnitVec3=Zone:GetVec3() +end +end +end +if self.InitCoord then +GroupUnitVec3=self.InitCoord:GetVec3() +end +Template.units[UnitID].alt=self.InitRespawnHeight and self.InitRespawnHeight or GroupUnitVec3.y +if Zone then +Template.units[UnitID].x=(Template.units[UnitID].x-From.x)+GroupUnitVec3.x +Template.units[UnitID].y=(Template.units[UnitID].y-From.y)+GroupUnitVec3.z +else +Template.units[UnitID].x=GroupUnitVec3.x +Template.units[UnitID].y=GroupUnitVec3.z +end +Template.units[UnitID].heading=_Heading(self.InitRespawnHeading and self.InitRespawnHeading or GroupUnit:GetHeading()) +Template.units[UnitID].psi=-Template.units[UnitID].heading +self:F({UnitID,Template.units[UnitID],Template.units[UnitID]}) +end +end +elseif Reset==false then +for UnitID,TemplateUnitData in pairs(Template.units)do +self:F("Reset") +local GroupUnitVec3={x=TemplateUnitData.x,y=TemplateUnitData.alt,z=TemplateUnitData.y} +if Zone then +if self.InitRespawnRandomizePositionZone then +GroupUnitVec3=Zone:GetRandomVec3() +else +if self.InitRespawnRandomizePositionInner and self.InitRespawnRandomizePositionOuter then +GroupUnitVec3=POINT_VEC3:NewFromVec2(From):GetRandomPointVec3InRadius(self.InitRespawnRandomizePositionsOuter,self.InitRespawnRandomizePositionsInner) +else +GroupUnitVec3=Zone:GetVec3() +end +end +end +if self.InitCoord then +GroupUnitVec3=self.InitCoord:GetVec3() +end +Template.units[UnitID].alt=self.InitRespawnHeight and self.InitRespawnHeight or GroupUnitVec3.y +Template.units[UnitID].x=(Template.units[UnitID].x-From.x)+GroupUnitVec3.x +Template.units[UnitID].y=(Template.units[UnitID].y-From.y)+GroupUnitVec3.z +Template.units[UnitID].heading=self.InitRespawnHeading and self.InitRespawnHeading or TemplateUnitData.heading +self:F({UnitID,Template.units[UnitID],Template.units[UnitID]}) +end +else +local units=self:GetUnits() +for UnitID,Unit in pairs(Template.units)do +for _,_unit in pairs(units)do +local unit=_unit +if unit:GetName()==Unit.name then +local coord=unit:GetCoordinate() +local heading=unit:GetHeading() +Unit.x=coord.x +Unit.y=coord.z +Unit.alt=coord.y +Unit.heading=math.rad(heading) +Unit.psi=-Unit.heading +end +end +end +end +end +if self.InitRespawnModex then +for UnitID=1,#Template.units do +Template.units[UnitID].onboard_num=string.format("%03d",self.InitRespawnModex+(UnitID-1)) +end +end +if self.InitRespawnRadio then +Template.communication=self.InitRespawnRadio +end +if self.InitRespawnFreq then +Template.frequency=self.InitRespawnFreq +end +if self.InitRespawnModu then +Template.modulation=self.InitRespawnModu +end +self:Destroy(false) +self:T({Template=Template}) +_DATABASE:Spawn(Template) +self:ResetEvents() +return self +end +function GROUP:RespawnAtCurrentAirbase(SpawnTemplate,Takeoff,Uncontrolled) +self:F2({SpawnTemplate,Takeoff,Uncontrolled}) +if self and self:IsAlive()then +local airbase=self:GetCoordinate():GetClosestAirbase() +if airbase then +self:F2("Closest airbase = "..airbase:GetName()) +else +self:E("ERROR: could not find closest airbase!") +return nil +end +Takeoff=Takeoff or SPAWN.Takeoff.Hot +local AirbaseCoord=airbase:GetCoordinate() +SpawnTemplate=SpawnTemplate or self:GetTemplate() +if SpawnTemplate then +local SpawnPoint=SpawnTemplate.route.points[1] +SpawnPoint.linkUnit=nil +SpawnPoint.helipadId=nil +SpawnPoint.airdromeId=nil +local AirbaseID=airbase:GetID() +local AirbaseCategory=airbase:GetAirbaseCategory() +if AirbaseCategory==Airbase.Category.SHIP or AirbaseCategory==Airbase.Category.HELIPAD then +SpawnPoint.linkUnit=AirbaseID +SpawnPoint.helipadId=AirbaseID +elseif AirbaseCategory==Airbase.Category.AIRDROME then +SpawnPoint.airdromeId=AirbaseID +end +SpawnPoint.type=GROUPTEMPLATE.Takeoff[Takeoff][1] +SpawnPoint.action=GROUPTEMPLATE.Takeoff[Takeoff][2] +local units=self:GetUnits() +local x +local y +for UnitID=1,#units do +local unit=units[UnitID] +local Parkingspot,TermialID,Distance=unit:GetCoordinate():GetClosestParkingSpot(airbase) +self:T2(string.format("Closest parking spot distance = %s, terminal ID=%s",tostring(Distance),tostring(TermialID))) +local uc=unit:GetCoordinate() +SpawnTemplate.units[UnitID].x=uc.x +SpawnTemplate.units[UnitID].y=uc.z +SpawnTemplate.units[UnitID].alt=uc.y +SpawnTemplate.units[UnitID].parking=TermialID +SpawnTemplate.units[UnitID].parking_id=nil +end +SpawnPoint.x=SpawnTemplate.units[1].x +SpawnPoint.y=SpawnTemplate.units[1].y +SpawnPoint.alt=SpawnTemplate.units[1].alt +SpawnTemplate.x=SpawnTemplate.units[1].x +SpawnTemplate.y=SpawnTemplate.units[1].y +SpawnTemplate.uncontrolled=Uncontrolled +if self.InitRespawnRadio then +SpawnTemplate.communication=self.InitRespawnRadio +end +if self.InitRespawnFreq then +SpawnTemplate.frequency=self.InitRespawnFreq +end +if self.InitRespawnModu then +SpawnTemplate.modulation=self.InitRespawnModu +end +self:Destroy(false) +_DATABASE:Spawn(SpawnTemplate) +self:ResetEvents() +return self +end +else +self:E("WARNING: GROUP is not alive!") +end +return nil +end +function GROUP:GetTaskMission() +self:F2(self.GroupName) +return routines.utils.deepCopy(_DATABASE.Templates.Groups[self.GroupName].Template) +end +function GROUP:GetTaskRoute() +self:F2(self.GroupName) +return routines.utils.deepCopy(_DATABASE.Templates.Groups[self.GroupName].Template.route.points) +end +function GROUP:CopyRoute(Begin,End,Randomize,Radius) +self:F2({Begin,End}) +local Points={} +local GroupName=string.match(self:GetName(),".*#") +if GroupName then +GroupName=GroupName:sub(1,-2) +else +GroupName=self:GetName() +end +self:T3({GroupName}) +local Template=_DATABASE.Templates.Groups[GroupName].Template +if Template then +if not Begin then +Begin=0 +end +if not End then +End=0 +end +for TPointID=Begin+1,#Template.route.points-End do +if Template.route.points[TPointID]then +Points[#Points+1]=routines.utils.deepCopy(Template.route.points[TPointID]) +if Randomize then +if not Radius then +Radius=500 +end +Points[#Points].x=Points[#Points].x+math.random(Radius*-1,Radius) +Points[#Points].y=Points[#Points].y+math.random(Radius*-1,Radius) +end +end +end +return Points +else +error("Template not found for Group : "..GroupName) +end +return nil +end +function GROUP:CalculateThreatLevelA2G() +local MaxThreatLevelA2G=0 +for UnitName,UnitData in pairs(self:GetUnits())do +local ThreatUnit=UnitData +local ThreatLevelA2G=ThreatUnit:GetThreatLevel() +if ThreatLevelA2G>MaxThreatLevelA2G then +MaxThreatLevelA2G=ThreatLevelA2G +end +end +self:T3(MaxThreatLevelA2G) +return MaxThreatLevelA2G +end +function GROUP:GetThreatLevel() +local threatlevelMax=0 +for UnitName,UnitData in pairs(self:GetUnits())do +local ThreatUnit=UnitData +local threatlevel=ThreatUnit:GetThreatLevel() +if threatlevel>threatlevelMax then +threatlevelMax=threatlevel +end +end +return threatlevelMax +end +function GROUP:InAir() +self:F2(self.GroupName) +local DCSGroup=self:GetDCSObject() +if DCSGroup then +local DCSUnit=DCSGroup:getUnit(1) +if DCSUnit then +local GroupInAir=DCSGroup:getUnit(1):inAir() +self:T3(GroupInAir) +return GroupInAir +end +end +return nil +end +function GROUP:IsAirborne(AllUnits) +self:F2(self.GroupName) +local units=self:GetUnits() +if units then +if AllUnits then +for _,_unit in pairs(units)do +local unit=_unit +if unit then +local inair=unit:InAir() +if not inair then +return false +end +end +end +return true +else +for _,_unit in pairs(units)do +local unit=_unit +if unit then +local inair=unit:InAir() +if inair then +return true +end +end +return false +end +end +end +return nil +end +function GROUP:GetDCSDesc(n) +n=n or 1 +local unit=self:GetUnit(n) +if unit and unit:IsAlive()~=nil then +local desc=unit:GetDesc() +return desc +end +return nil +end +function GROUP:GetAttribute() +local attribute=GROUP.Attribute.OTHER_UNKNOWN +if self then +local transportplane=self:HasAttribute("Transports")and self:HasAttribute("Planes") +local awacs=self:HasAttribute("AWACS") +local fighter=self:HasAttribute("Fighters")or self:HasAttribute("Interceptors")or self:HasAttribute("Multirole fighters")or(self:HasAttribute("Bombers")and not self:HasAttribute("Strategic bombers")) +local bomber=self:HasAttribute("Strategic bombers") +local tanker=self:HasAttribute("Tankers") +local uav=self:HasAttribute("UAVs") +local transporthelo=self:HasAttribute("Transport helicopters") +local attackhelicopter=self:HasAttribute("Attack helicopters") +local apc=self:HasAttribute("Infantry carriers") +local truck=self:HasAttribute("Trucks")and self:GetCategory()==Group.Category.GROUND +local infantry=self:HasAttribute("Infantry") +local artillery=self:HasAttribute("Artillery") +local tank=self:HasAttribute("Old Tanks")or self:HasAttribute("Modern Tanks") +local aaa=self:HasAttribute("AAA") +local ewr=self:HasAttribute("EWR") +local sam=self:HasAttribute("SAM elements")and(not self:HasAttribute("AAA")) +local train=self:GetCategory()==Group.Category.TRAIN +local aircraftcarrier=self:HasAttribute("Aircraft Carriers") +local warship=self:HasAttribute("Heavy armed ships") +local armedship=self:HasAttribute("Armed ships") +local unarmedship=self:HasAttribute("Unarmed ships") +if transportplane then +attribute=GROUP.Attribute.AIR_TRANSPORTPLANE +elseif awacs then +attribute=GROUP.Attribute.AIR_AWACS +elseif fighter then +attribute=GROUP.Attribute.AIR_FIGHTER +elseif bomber then +attribute=GROUP.Attribute.AIR_BOMBER +elseif tanker then +attribute=GROUP.Attribute.AIR_TANKER +elseif transporthelo then +attribute=GROUP.Attribute.AIR_TRANSPORTHELO +elseif attackhelicopter then +attribute=GROUP.Attribute.AIR_ATTACKHELO +elseif uav then +attribute=GROUP.Attribute.AIR_UAV +elseif apc then +attribute=GROUP.Attribute.GROUND_APC +elseif infantry then +attribute=GROUP.Attribute.GROUND_INFANTRY +elseif artillery then +attribute=GROUP.Attribute.GROUND_ARTILLERY +elseif tank then +attribute=GROUP.Attribute.GROUND_TANK +elseif aaa then +attribute=GROUP.Attribute.GROUND_AAA +elseif ewr then +attribute=GROUP.Attribute.GROUND_EWR +elseif sam then +attribute=GROUP.Attribute.GROUND_SAM +elseif truck then +attribute=GROUP.Attribute.GROUND_TRUCK +elseif train then +attribute=GROUP.Attribute.GROUND_TRAIN +elseif aircraftcarrier then +attribute=GROUP.Attribute.NAVAL_AIRCRAFTCARRIER +elseif warship then +attribute=GROUP.Attribute.NAVAL_WARSHIP +elseif armedship then +attribute=GROUP.Attribute.NAVAL_ARMEDSHIP +elseif unarmedship then +attribute=GROUP.Attribute.NAVAL_UNARMEDSHIP +else +if self:IsGround()then +attribute=GROUP.Attribute.GROUND_OTHER +elseif self:IsShip()then +attribute=GROUP.Attribute.NAVAL_OTHER +elseif self:IsAir()then +attribute=GROUP.Attribute.AIR_OTHER +else +attribute=GROUP.Attribute.OTHER_UNKNOWN +end +end +end +return attribute +end +do +function GROUP:RouteRTB(RTBAirbase,Speed) +self:F({RTBAirbase:GetName(),Speed}) +local DCSGroup=self:GetDCSObject() +if DCSGroup then +if RTBAirbase then +local Speed=Speed or self:GetSpeedMax()*0.8 +local coord=self:GetCoordinate() +local PointFrom=coord:WaypointAirTurningPoint(nil,Speed) +local PointLanding=RTBAirbase:GetCoordinate():WaypointAirLanding(Speed,RTBAirbase) +local Points={PointFrom,PointLanding} +self:T3(Points) +local Template=self:GetTemplate() +Template.route.points=Points +self:Respawn(Template,true) +self:Route(Points) +else +self:ClearTasks() +end +end +return self +end +end +function GROUP:OnReSpawn(ReSpawnFunction) +self.ReSpawnFunction=ReSpawnFunction +end +do +function GROUP:HandleEvent(Event,EventFunction,...) +self:EventDispatcher():OnEventForGroup(self:GetName(),EventFunction,self,Event,...) +return self +end +function GROUP:UnHandleEvent(Event) +self:EventDispatcher():RemoveEvent(self,Event) +return self +end +function GROUP:ResetEvents() +self:EventDispatcher():Reset(self) +for UnitID,UnitData in pairs(self:GetUnits())do +UnitData:ResetEvents() +end +return self +end +end +do +function GROUP:GetPlayerNames() +local HasPlayers=false +local PlayerNames={} +local Units=self:GetUnits() +for UnitID,UnitData in pairs(Units)do +local Unit=UnitData +local PlayerName=Unit:GetPlayerName() +if PlayerName and PlayerName~=""then +PlayerNames=PlayerNames or{} +table.insert(PlayerNames,PlayerName) +HasPlayers=true +end +end +if HasPlayers==true then +self:F2(PlayerNames) +return PlayerNames +end +return nil +end +function GROUP:GetPlayerCount() +local PlayerCount=0 +local Units=self:GetUnits() +for UnitID,UnitData in pairs(Units or{})do +local Unit=UnitData +local PlayerName=Unit:GetPlayerName() +if PlayerName and PlayerName~=""then +PlayerCount=PlayerCount+1 +end +end +return PlayerCount +end +end +function GROUP:EnableEmission(switch) +self:F2(self.GroupName) +local switch=switch or false +local DCSUnit=self:GetDCSObject() +if DCSUnit then +DCSUnit:enableEmission(switch) +end +end +UNIT={ +ClassName="UNIT", +UnitName=nil, +} +function UNIT:Register(UnitName) +local self=BASE:Inherit(self,CONTROLLABLE:New(UnitName)) +self.UnitName=UnitName +self:SetEventPriority(3) +return self +end +function UNIT:Find(DCSUnit) +if DCSUnit then +local UnitName=DCSUnit:getName() +local UnitFound=_DATABASE:FindUnit(UnitName) +return UnitFound +end +return nil +end +function UNIT:FindByName(UnitName) +local UnitFound=_DATABASE:FindUnit(UnitName) +return UnitFound +end +function UNIT:Name() +return self.UnitName +end +function UNIT:GetDCSObject() +local DCSUnit=Unit.getByName(self.UnitName) +if DCSUnit then +return DCSUnit +end +return nil +end +function UNIT:ReSpawnAt(Coordinate,Heading) +self:T(self:Name()) +local SpawnGroupTemplate=UTILS.DeepCopy(_DATABASE:GetGroupTemplateFromUnitName(self:Name())) +self:T(SpawnGroupTemplate) +local SpawnGroup=self:GetGroup() +self:T({SpawnGroup=SpawnGroup}) +if SpawnGroup then +local Vec3=SpawnGroup:GetVec3() +SpawnGroupTemplate.x=Coordinate.x +SpawnGroupTemplate.y=Coordinate.z +self:F(#SpawnGroupTemplate.units) +for UnitID,UnitData in pairs(SpawnGroup:GetUnits())do +local GroupUnit=UnitData +self:F(GroupUnit:GetName()) +if GroupUnit:IsAlive()then +local GroupUnitVec3=GroupUnit:GetVec3() +local GroupUnitHeading=GroupUnit:GetHeading() +SpawnGroupTemplate.units[UnitID].alt=GroupUnitVec3.y +SpawnGroupTemplate.units[UnitID].x=GroupUnitVec3.x +SpawnGroupTemplate.units[UnitID].y=GroupUnitVec3.z +SpawnGroupTemplate.units[UnitID].heading=GroupUnitHeading +self:F({UnitID,SpawnGroupTemplate.units[UnitID],SpawnGroupTemplate.units[UnitID]}) +end +end +end +for UnitTemplateID,UnitTemplateData in pairs(SpawnGroupTemplate.units)do +self:T({UnitTemplateData.name,self:Name()}) +SpawnGroupTemplate.units[UnitTemplateID].unitId=nil +if UnitTemplateData.name==self:Name()then +self:T("Adjusting") +SpawnGroupTemplate.units[UnitTemplateID].alt=Coordinate.y +SpawnGroupTemplate.units[UnitTemplateID].x=Coordinate.x +SpawnGroupTemplate.units[UnitTemplateID].y=Coordinate.z +SpawnGroupTemplate.units[UnitTemplateID].heading=Heading +self:F({UnitTemplateID,SpawnGroupTemplate.units[UnitTemplateID],SpawnGroupTemplate.units[UnitTemplateID]}) +else +self:F(SpawnGroupTemplate.units[UnitTemplateID].name) +local GroupUnit=UNIT:FindByName(SpawnGroupTemplate.units[UnitTemplateID].name) +if GroupUnit and GroupUnit:IsAlive()then +local GroupUnitVec3=GroupUnit:GetVec3() +local GroupUnitHeading=GroupUnit:GetHeading() +UnitTemplateData.alt=GroupUnitVec3.y +UnitTemplateData.x=GroupUnitVec3.x +UnitTemplateData.y=GroupUnitVec3.z +UnitTemplateData.heading=GroupUnitHeading +else +if SpawnGroupTemplate.units[UnitTemplateID].name~=self:Name()then +self:T("nilling") +SpawnGroupTemplate.units[UnitTemplateID].delete=true +end +end +end +end +local i=1 +while i<=#SpawnGroupTemplate.units do +local UnitTemplateData=SpawnGroupTemplate.units[i] +self:T(UnitTemplateData.name) +if UnitTemplateData.delete then +table.remove(SpawnGroupTemplate.units,i) +else +i=i+1 +end +end +SpawnGroupTemplate.groupId=nil +self:T(SpawnGroupTemplate) +_DATABASE:Spawn(SpawnGroupTemplate) +end +function UNIT:IsActive() +self:F2(self.UnitName) +local DCSUnit=self:GetDCSObject() +if DCSUnit then +local UnitIsActive=DCSUnit:isActive() +return UnitIsActive +end +return nil +end +function UNIT:IsAlive() +self:F3(self.UnitName) +local DCSUnit=self:GetDCSObject() +if DCSUnit then +local UnitIsAlive=DCSUnit:isExist()and DCSUnit:isActive() +return UnitIsAlive +end +return nil +end +function UNIT:GetCallsign() +self:F2(self.UnitName) +local DCSUnit=self:GetDCSObject() +if DCSUnit then +local UnitCallSign=DCSUnit:getCallsign() +if UnitCallSign==""then +UnitCallSign=DCSUnit:getName() +end +return UnitCallSign +end +self:F(self.ClassName.." "..self.UnitName.." not found!") +return nil +end +function UNIT:IsPlayer() +local group=self:GetGroup() +local units=group:GetTemplate().units +for _,unit in pairs(units)do +if unit.name==self:GetName()and(unit.skill=="Client"or unit.skill=="Player")then +return true +end +end +return false +end +function UNIT:GetPlayerName() +self:F(self.UnitName) +local DCSUnit=self:GetDCSObject() +if DCSUnit then +local PlayerName=DCSUnit:getPlayerName() +return PlayerName +end +return nil +end +function UNIT:IsClient() +if _DATABASE.CLIENTS[self.UnitName]then +return true +end +return false +end +function UNIT:GetClient() +local client=_DATABASE.CLIENTS[self.UnitName] +if client then +return client +end +return nil +end +function UNIT:GetNumber() +self:F2(self.UnitName) +local DCSUnit=self:GetDCSObject() +if DCSUnit then +local UnitNumber=DCSUnit:getNumber() +return UnitNumber +end +return nil +end +function UNIT:GetSpeedMax() +self:F2(self.UnitName) +local Desc=self:GetDesc() +if Desc then +local SpeedMax=Desc.speedMax +return SpeedMax*3.6 +end +return nil +end +function UNIT:GetRange() +self:F2(self.UnitName) +local Desc=self:GetDesc() +if Desc then +local Range=Desc.range +if Range then +Range=Range*1000 +else +Range=10000000 +end +return Range +end +return nil +end +function UNIT:IsRefuelable() +self:F2(self.UnitName) +local refuelable=self:HasAttribute("Refuelable") +local system=nil +local Desc=self:GetDesc() +if Desc and Desc.tankerType then +system=Desc.tankerType +end +return refuelable,system +end +function UNIT:IsTanker() +self:F2(self.UnitName) +local tanker=self:HasAttribute("Tankers") +local system=nil +if tanker then +local Desc=self:GetDesc() +if Desc and Desc.tankerType then +system=Desc.tankerType +end +local typename=self:GetTypeName() +if typename=="IL-78M"then +system=1 +elseif typename=="KC130"then +system=1 +elseif typename=="KC135BDA"then +system=1 +elseif typename=="KC135MPRS"then +system=1 +elseif typename=="S-3B Tanker"then +system=1 +end +end +return tanker,system +end +function UNIT:GetGroup() +self:F2(self.UnitName) +local DCSUnit=self:GetDCSObject() +if DCSUnit then +local UnitGroup=GROUP:FindByName(DCSUnit:getGroup():getName()) +return UnitGroup +end +return nil +end +function UNIT:GetPrefix() +self:F2(self.UnitName) +local DCSUnit=self:GetDCSObject() +if DCSUnit then +local UnitPrefix=string.match(self.UnitName,".*#"):sub(1,-2) +self:T3(UnitPrefix) +return UnitPrefix +end +return nil +end +function UNIT:GetAmmo() +self:F2(self.UnitName) +local DCSUnit=self:GetDCSObject() +if DCSUnit then +local UnitAmmo=DCSUnit:getAmmo() +return UnitAmmo +end +return nil +end +function UNIT:GetAmmunition() +local nammo=0 +local nshells=0 +local nrockets=0 +local nmissiles=0 +local nbombs=0 +local unit=self +local ammotable=unit:GetAmmo() +if ammotable then +local weapons=#ammotable +for w=1,weapons do +local Nammo=ammotable[w]["count"] +local Tammo=ammotable[w]["desc"]["typeName"] +local _weaponString=UTILS.Split(Tammo,"%.") +local _weaponName=_weaponString[#_weaponString] +local Category=ammotable[w].desc.category +local MissileCategory=nil +if Category==Weapon.Category.MISSILE then +MissileCategory=ammotable[w].desc.missileCategory +end +if Category==Weapon.Category.SHELL then +nshells=nshells+Nammo +elseif Category==Weapon.Category.ROCKET then +nrockets=nrockets+Nammo +elseif Category==Weapon.Category.BOMB then +nbombs=nbombs+Nammo +elseif Category==Weapon.Category.MISSILE then +if MissileCategory==Weapon.MissileCategory.AAM then +nmissiles=nmissiles+Nammo +elseif MissileCategory==Weapon.MissileCategory.ANTI_SHIP then +nmissiles=nmissiles+Nammo +elseif MissileCategory==Weapon.MissileCategory.BM then +nmissiles=nmissiles+Nammo +elseif MissileCategory==Weapon.MissileCategory.OTHER then +nmissiles=nmissiles+Nammo +end +end +end +end +nammo=nshells+nrockets+nmissiles+nbombs +return nammo,nshells,nrockets,nbombs,nmissiles +end +function UNIT:GetSensors() +self:F2(self.UnitName) +local DCSUnit=self:GetDCSObject() +if DCSUnit then +local UnitSensors=DCSUnit:getSensors() +return UnitSensors +end +return nil +end +function UNIT:HasSensors(...) +self:F2(arg) +local DCSUnit=self:GetDCSObject() +if DCSUnit then +local HasSensors=DCSUnit:hasSensors(unpack(arg)) +return HasSensors +end +return nil +end +function UNIT:HasSEAD() +self:F2() +local DCSUnit=self:GetDCSObject() +if DCSUnit then +local UnitSEADAttributes=DCSUnit:getDesc().attributes +local HasSEAD=false +if UnitSEADAttributes["RADAR_BAND1_FOR_ARM"]and UnitSEADAttributes["RADAR_BAND1_FOR_ARM"]==true or +UnitSEADAttributes["RADAR_BAND2_FOR_ARM"]and UnitSEADAttributes["RADAR_BAND2_FOR_ARM"]==true then +HasSEAD=true +end +return HasSEAD +end +return nil +end +function UNIT:GetRadar() +self:F2(self.UnitName) +local DCSUnit=self:GetDCSObject() +if DCSUnit then +local UnitRadarOn,UnitRadarObject=DCSUnit:getRadar() +return UnitRadarOn,UnitRadarObject +end +return nil,nil +end +function UNIT:GetFuel() +self:F3(self.UnitName) +local DCSUnit=self:GetDCSObject() +if DCSUnit then +local UnitFuel=DCSUnit:getFuel() +return UnitFuel +end +return nil +end +function UNIT:SetEmission(Switch) +if Switch==nil then +Switch=true +end +local DCSUnit=self:GetDCSObject() +if DCSUnit then +DCSUnit:enableEmission(Switch) +end +return self +end +function UNIT:EnableEmission() +self:SetEmission(true) +return self +end +function UNIT:DisableEmission() +self:SetEmission(false) +return self +end +function UNIT:GetUnits() +self:F3({self.UnitName}) +local DCSUnit=self:GetDCSObject() +local Units={} +if DCSUnit then +Units[1]=UNIT:Find(DCSUnit) +self:T3(Units) +return Units +end +return nil +end +function UNIT:GetLife() +self:F2(self.UnitName) +local DCSUnit=self:GetDCSObject() +if DCSUnit then +local UnitLife=DCSUnit:getLife() +return UnitLife +end +return-1 +end +function UNIT:GetLife0() +self:F2(self.UnitName) +local DCSUnit=self:GetDCSObject() +if DCSUnit then +local UnitLife0=DCSUnit:getLife0() +return UnitLife0 +end +return 0 +end +function UNIT:GetLifeRelative() +self:F2(self.UnitName) +if self and self:IsAlive()then +local life0=self:GetLife0() +local lifeN=self:GetLife() +return lifeN/life0 +end +return-1 +end +function UNIT:GetDamageRelative() +self:F2(self.UnitName) +if self and self:IsAlive()then +return 1-self:GetLifeRelative() +end +return 1 +end +function UNIT:GetUnitCategory() +self:F3(self.UnitName) +local DCSUnit=self:GetDCSObject() +if DCSUnit then +return DCSUnit:getDesc().category +end +return nil +end +function UNIT:GetCategoryName() +self:F3(self.UnitName) +local DCSUnit=self:GetDCSObject() +if DCSUnit then +local CategoryNames={ +[Unit.Category.AIRPLANE]="Airplane", +[Unit.Category.HELICOPTER]="Helicopter", +[Unit.Category.GROUND_UNIT]="Ground Unit", +[Unit.Category.SHIP]="Ship", +[Unit.Category.STRUCTURE]="Structure", +} +local UnitCategory=DCSUnit:getDesc().category +self:T3(UnitCategory) +return CategoryNames[UnitCategory] +end +return nil +end +function UNIT:GetThreatLevel() +local ThreatLevel=0 +local ThreatText="" +local Descriptor=self:GetDesc() +if Descriptor then +local Attributes=Descriptor.attributes +if self:IsGround()then +local ThreatLevels={ +"Unarmed", +"Infantry", +"Old Tanks & APCs", +"Tanks & IFVs without ATGM", +"Tanks & IFV with ATGM", +"Modern Tanks", +"AAA", +"IR Guided SAMs", +"SR SAMs", +"MR SAMs", +"LR SAMs" +} +if Attributes["LR SAM"]then ThreatLevel=10 +elseif Attributes["MR SAM"]then ThreatLevel=9 +elseif Attributes["SR SAM"]and +not Attributes["IR Guided SAM"]then ThreatLevel=8 +elseif(Attributes["SR SAM"]or Attributes["MANPADS"])and +Attributes["IR Guided SAM"]then ThreatLevel=7 +elseif Attributes["AAA"]then ThreatLevel=6 +elseif Attributes["Modern Tanks"]then ThreatLevel=5 +elseif(Attributes["Tanks"]or Attributes["IFV"])and +Attributes["ATGM"]then ThreatLevel=4 +elseif(Attributes["Tanks"]or Attributes["IFV"])and +not Attributes["ATGM"]then ThreatLevel=3 +elseif Attributes["Old Tanks"]or Attributes["APC"]or Attributes["Artillery"]then ThreatLevel=2 +elseif Attributes["Infantry"]then ThreatLevel=1 +end +ThreatText=ThreatLevels[ThreatLevel+1] +end +if self:IsAir()then +local ThreatLevels={ +"Unarmed", +"Tanker", +"AWACS", +"Transport Helicopter", +"UAV", +"Bomber", +"Strategic Bomber", +"Attack Helicopter", +"Battleplane", +"Multirole Fighter", +"Fighter" +} +if Attributes["Fighters"]then ThreatLevel=10 +elseif Attributes["Multirole fighters"]then ThreatLevel=9 +elseif Attributes["Battleplanes"]then ThreatLevel=8 +elseif Attributes["Attack helicopters"]then ThreatLevel=7 +elseif Attributes["Strategic bombers"]then ThreatLevel=6 +elseif Attributes["Bombers"]then ThreatLevel=5 +elseif Attributes["UAVs"]then ThreatLevel=4 +elseif Attributes["Transport helicopters"]then ThreatLevel=3 +elseif Attributes["AWACS"]then ThreatLevel=2 +elseif Attributes["Tankers"]then ThreatLevel=1 +end +ThreatText=ThreatLevels[ThreatLevel+1] +end +if self:IsShip()then +local ThreatLevels={ +"Unarmed ship", +"Light armed ships", +"Corvettes", +"", +"Frigates", +"", +"Cruiser", +"", +"Destroyer", +"", +"Aircraft Carrier" +} +if Attributes["Aircraft Carriers"]then ThreatLevel=10 +elseif Attributes["Destroyers"]then ThreatLevel=8 +elseif Attributes["Cruisers"]then ThreatLevel=6 +elseif Attributes["Frigates"]then ThreatLevel=4 +elseif Attributes["Corvettes"]then ThreatLevel=2 +elseif Attributes["Light armed ships"]then ThreatLevel=1 +end +ThreatText=ThreatLevels[ThreatLevel+1] +end +end +return ThreatLevel,ThreatText +end +function UNIT:Explode(power,delay) +power=power or 100 +local DCSUnit=self:GetDCSObject() +if DCSUnit then +if delay and delay>0 then +SCHEDULER:New(nil,self.Explode,{self,power},delay) +else +self:GetCoordinate():Explosion(power) +end +return self +end +return nil +end +function UNIT:OtherUnitInRadius(AwaitUnit,Radius) +self:F2({self.UnitName,AwaitUnit.UnitName,Radius}) +local DCSUnit=self:GetDCSObject() +if DCSUnit then +local UnitVec3=self:GetVec3() +local AwaitUnitVec3=AwaitUnit:GetVec3() +if(((UnitVec3.x-AwaitUnitVec3.x)^2+(UnitVec3.z-AwaitUnitVec3.z)^2)^0.5<=Radius)then +self:T3("true") +return true +else +self:T3("false") +return false +end +end +return nil +end +function UNIT:IsFriendly(FriendlyCoalition) +self:F2() +local DCSUnit=self:GetDCSObject() +if DCSUnit then +local UnitCoalition=DCSUnit:getCoalition() +self:T3({UnitCoalition,FriendlyCoalition}) +local IsFriendlyResult=(UnitCoalition==FriendlyCoalition) +self:F(IsFriendlyResult) +return IsFriendlyResult +end +return nil +end +function UNIT:IsShip() +self:F2() +local DCSUnit=self:GetDCSObject() +if DCSUnit then +local UnitDescriptor=DCSUnit:getDesc() +self:T3({UnitDescriptor.category,Unit.Category.SHIP}) +local IsShipResult=(UnitDescriptor.category==Unit.Category.SHIP) +self:T3(IsShipResult) +return IsShipResult +end +return nil +end +function UNIT:InAir(NoHeloCheck) +self:F2(self.UnitName) +local DCSUnit=self:GetDCSObject() +if DCSUnit then +local UnitInAir=DCSUnit:inAir() +local UnitCategory=DCSUnit:getDesc().category +if UnitInAir==true and UnitCategory==Unit.Category.HELICOPTER and(not NoHeloCheck)then +local VelocityVec3=DCSUnit:getVelocity() +local Velocity=UTILS.VecNorm(VelocityVec3) +local Coordinate=DCSUnit:getPoint() +local LandHeight=land.getHeight({x=Coordinate.x,y=Coordinate.z}) +local Height=Coordinate.y-LandHeight +if Velocity<1 and Height<=60 then +UnitInAir=false +end +end +self:T3(UnitInAir) +return UnitInAir +end +return nil +end +do +function UNIT:HandleEvent(EventID,EventFunction) +self:EventDispatcher():OnEventForUnit(self:GetName(),EventFunction,self,EventID) +return self +end +function UNIT:UnHandleEvent(EventID) +self:EventDispatcher():RemoveEvent(self,EventID) +return self +end +function UNIT:ResetEvents() +self:EventDispatcher():Reset(self) +return self +end +end +do +function UNIT:IsDetected(TargetUnit) +local TargetIsDetected,TargetIsVisible,TargetLastTime,TargetKnowType,TargetKnowDistance,TargetLastPos,TargetLastVelocity=self:IsTargetDetected(TargetUnit:GetDCSObject()) +return TargetIsDetected +end +function UNIT:IsLOS(TargetUnit) +local IsLOS=self:GetPointVec3():IsLOS(TargetUnit:GetPointVec3()) +return IsLOS +end +function UNIT:KnowUnit(TargetUnit,TypeKnown,DistanceKnown) +if TypeKnown~=false then +TypeKnown=true +end +if DistanceKnown~=false then +DistanceKnown=true +end +local DCSControllable=self:GetDCSObject() +if DCSControllable then +local Controller=DCSControllable:getController() +if Controller then +local object=TargetUnit:GetDCSObject() +if object then +self:I(string.format("Unit %s now knows target unit %s. Type known=%s, distance known=%s",self:GetName(),TargetUnit:GetName(),tostring(TypeKnown),tostring(DistanceKnown))) +Controller:knowTarget(object,TypeKnown,DistanceKnown) +end +end +end +end +end +function UNIT:GetTemplate() +local group=self:GetGroup() +local name=self:GetName() +if group then +local template=group:GetTemplate() +if template then +for _,unit in pairs(template.units)do +if unit.name==name then +return UTILS.DeepCopy(unit) +end +end +end +end +return nil +end +function UNIT:GetTemplatePayload() +local unit=self:GetTemplate() +if unit then +return unit.payload +end +return nil +end +function UNIT:GetTemplatePylons() +local payload=self:GetTemplatePayload() +if payload then +return payload.pylons +end +return nil +end +function UNIT:GetTemplateFuel() +local payload=self:GetTemplatePayload() +if payload then +return payload.fuel +end +return nil +end +function UNIT:EnableEmission(switch) +self:F2(self.UnitName) +local switch=switch or false +local DCSUnit=self:GetDCSObject() +if DCSUnit then +DCSUnit:enableEmission(switch) +end +end +CLIENT={ +ClassName="CLIENT", +ClientName=nil, +ClientAlive=false, +ClientTransport=false, +ClientBriefingShown=false, +_Menus={}, +_Tasks={}, +Messages={}, +Players={}, +} +function CLIENT:Find(DCSUnit,Error) +local ClientName=DCSUnit:getName() +local ClientFound=_DATABASE:FindClient(ClientName) +if ClientFound then +ClientFound:F(ClientName) +return ClientFound +end +if not Error then +error("CLIENT not found for: "..ClientName) +end +end +function CLIENT:FindByName(ClientName,ClientBriefing,Error) +local ClientFound=_DATABASE:FindClient(ClientName) +if ClientFound then +ClientFound:F({ClientName,ClientBriefing}) +ClientFound:AddBriefing(ClientBriefing) +ClientFound.MessageSwitch=true +return ClientFound +end +if not Error then +error("CLIENT not found for: "..ClientName) +end +end +function CLIENT:Register(ClientName) +local self=BASE:Inherit(self,UNIT:Register(ClientName)) +self.ClientName=ClientName +self.MessageSwitch=true +self.ClientAlive2=false +return self +end +function CLIENT:Transport() +self.ClientTransport=true +return self +end +function CLIENT:AddBriefing(ClientBriefing) +self.ClientBriefing=ClientBriefing +self.ClientBriefingShown=false +return self +end +function CLIENT:AddPlayer(PlayerName) +table.insert(self.Players,PlayerName) +return self +end +function CLIENT:GetPlayers() +return self.Players +end +function CLIENT:GetPlayer() +if#self.Players>0 then +return self.Players[1] +end +return nil +end +function CLIENT:RemovePlayer(PlayerName) +for i,playername in pairs(self.Players)do +if PlayerName==playername then +table.remove(self.Players,i) +break +end +end +return self +end +function CLIENT:RemovePlayers() +self.Players={} +return self +end +function CLIENT:ShowBriefing() +if not self.ClientBriefingShown then +self.ClientBriefingShown=true +local Briefing="" +if self.ClientBriefing and self.ClientBriefing~=""then +Briefing=Briefing..self.ClientBriefing +self:Message(Briefing,60,"Briefing") +end +end +return self +end +function CLIENT:ShowMissionBriefing(MissionBriefing) +self:F({self.ClientName}) +if MissionBriefing then +self:Message(MissionBriefing,60,"Mission Briefing") +end +return self +end +function CLIENT:Reset(ClientName) +self:F() +self._Menus={} +end +function CLIENT:IsMultiSeated() +self:F(self.ClientName) +local ClientMultiSeatedTypes={ +["Mi-8MT"]="Mi-8MT", +["UH-1H"]="UH-1H", +["P-51B"]="P-51B" +} +if self:IsAlive()then +local ClientTypeName=self:GetClientGroupUnit():GetTypeName() +if ClientMultiSeatedTypes[ClientTypeName]then +return true +end +end +return false +end +function CLIENT:Alive(CallBackFunction,...) +self:F() +self.ClientCallBack=CallBackFunction +self.ClientParameters=arg +self.AliveCheckScheduler=SCHEDULER:New(self,self._AliveCheckScheduler,{"Client Alive "..self.ClientName},0.1,5,0.5) +self.AliveCheckScheduler:NoTrace() +return self +end +function CLIENT:_AliveCheckScheduler(SchedulerName) +self:F3({SchedulerName,self.ClientName,self.ClientAlive2,self.ClientBriefingShown,self.ClientCallBack}) +if self:IsAlive()then +if self.ClientAlive2==false then +self:ShowBriefing() +if self.ClientCallBack then +self:T("Calling Callback function") +self.ClientCallBack(self,unpack(self.ClientParameters)) +end +self.ClientAlive2=true +end +else +if self.ClientAlive2==true then +self.ClientAlive2=false +end +end +return true +end +function CLIENT:GetDCSGroup() +self:F3() +local ClientUnit=Unit.getByName(self.ClientName) +local CoalitionsData={AlivePlayersRed=coalition.getPlayers(coalition.side.RED),AlivePlayersBlue=coalition.getPlayers(coalition.side.BLUE)} +for CoalitionId,CoalitionData in pairs(CoalitionsData)do +self:T3({"CoalitionData:",CoalitionData}) +for UnitId,UnitData in pairs(CoalitionData)do +self:T3({"UnitData:",UnitData}) +if UnitData and UnitData:isExist()then +if ClientUnit then +local ClientGroup=ClientUnit:getGroup() +if ClientGroup then +self:T3("ClientGroup = "..self.ClientName) +if ClientGroup:isExist()and UnitData:getGroup():isExist()then +if ClientGroup:getID()==UnitData:getGroup():getID()then +self:T3("Normal logic") +self:T3(self.ClientName.." : group found!") +self.ClientGroupID=ClientGroup:getID() +self.ClientGroupName=ClientGroup:getName() +return ClientGroup +end +else +self:T3("Bug 1.5 logic") +local ClientGroupTemplate=_DATABASE.Templates.Units[self.ClientName].GroupTemplate +self.ClientGroupID=ClientGroupTemplate.groupId +self.ClientGroupName=_DATABASE.Templates.Units[self.ClientName].GroupName +self:T3(self.ClientName.." : group found in bug 1.5 resolvement logic!") +return ClientGroup +end +end +else +end +end +end +end +if ClientUnit then +local ClientGroup=ClientUnit:getGroup() +if ClientGroup then +self:T3("ClientGroup = "..self.ClientName) +if ClientGroup:isExist()then +self:T3("Normal logic") +self:T3(self.ClientName.." : group found!") +return ClientGroup +end +end +end +self.ClientGroupID=nil +self.ClientGroupName=nil +return nil +end +function CLIENT:GetClientGroupID() +self:GetDCSGroup() +return self.ClientGroupID +end +function CLIENT:GetClientGroupName() +self:GetDCSGroup() +return self.ClientGroupName +end +function CLIENT:GetClientGroupUnit() +self:F2() +local ClientDCSUnit=Unit.getByName(self.ClientName) +self:T(self.ClientDCSUnit) +if ClientDCSUnit and ClientDCSUnit:isExist()then +local ClientUnit=_DATABASE:FindUnit(self.ClientName) +return ClientUnit +end +return nil +end +function CLIENT:GetClientGroupDCSUnit() +self:F2() +local ClientDCSUnit=Unit.getByName(self.ClientName) +if ClientDCSUnit and ClientDCSUnit:isExist()then +self:T2(ClientDCSUnit) +return ClientDCSUnit +end +end +function CLIENT:IsTransport() +self:F() +return self.ClientTransport +end +function CLIENT:ShowCargo() +self:F() +local CargoMsg="" +for CargoName,Cargo in pairs(CARGOS)do +if self==Cargo:IsLoadedInClient()then +CargoMsg=CargoMsg..Cargo.CargoName.." Type:"..Cargo.CargoType.." Weight: "..Cargo.CargoWeight.."\n" +end +end +if CargoMsg==""then +CargoMsg="empty" +end +self:Message(CargoMsg,15,"Co-Pilot: Cargo Status",30) +end +function CLIENT:Message(Message,MessageDuration,MessageCategory,MessageInterval,MessageID) +self:F({Message,MessageDuration,MessageCategory,MessageInterval}) +if self.MessageSwitch==true then +if MessageCategory==nil then +MessageCategory="Messages" +end +if MessageID~=nil then +if self.Messages[MessageID]==nil then +self.Messages[MessageID]={} +self.Messages[MessageID].MessageId=MessageID +self.Messages[MessageID].MessageTime=timer.getTime() +self.Messages[MessageID].MessageDuration=MessageDuration +if MessageInterval==nil then +self.Messages[MessageID].MessageInterval=600 +else +self.Messages[MessageID].MessageInterval=MessageInterval +end +MESSAGE:New(Message,MessageDuration,MessageCategory):ToClient(self) +else +if self:GetClientGroupDCSUnit()and not self:GetClientGroupDCSUnit():inAir()then +if timer.getTime()-self.Messages[MessageID].MessageTime>=self.Messages[MessageID].MessageDuration+10 then +MESSAGE:New(Message,MessageDuration,MessageCategory):ToClient(self) +self.Messages[MessageID].MessageTime=timer.getTime() +end +else +if timer.getTime()-self.Messages[MessageID].MessageTime>=self.Messages[MessageID].MessageDuration+self.Messages[MessageID].MessageInterval then +MESSAGE:New(Message,MessageDuration,MessageCategory):ToClient(self) +self.Messages[MessageID].MessageTime=timer.getTime() +end +end +end +else +MESSAGE:New(Message,MessageDuration,MessageCategory):ToClient(self) +end +end +end +STATIC={ +ClassName="STATIC", +} +function STATIC:Register(StaticName) +local self=BASE:Inherit(self,POSITIONABLE:New(StaticName)) +self.StaticName=StaticName +return self +end +function STATIC:Find(DCSStatic) +local StaticName=DCSStatic:getName() +local StaticFound=_DATABASE:FindStatic(StaticName) +return StaticFound +end +function STATIC:FindByName(StaticName,RaiseError) +local StaticFound=_DATABASE:FindStatic(StaticName) +self.StaticName=StaticName +if StaticFound then +return StaticFound +end +if RaiseError==nil or RaiseError==true then +error("STATIC not found for: "..StaticName) +end +return nil +end +function STATIC:Destroy(GenerateEvent) +self:F2(self.ObjectName) +local DCSObject=self:GetDCSObject() +if DCSObject then +local StaticName=DCSObject:getName() +self:F({StaticName=StaticName}) +if GenerateEvent and GenerateEvent==true then +if self:IsAir()then +self:CreateEventCrash(timer.getTime(),DCSObject) +else +self:CreateEventDead(timer.getTime(),DCSObject) +end +elseif GenerateEvent==false then +else +self:CreateEventRemoveUnit(timer.getTime(),DCSObject) +end +DCSObject:destroy() +return true +end +return nil +end +function STATIC:GetDCSObject() +local DCSStatic=StaticObject.getByName(self.StaticName) +if DCSStatic then +return DCSStatic +end +return nil +end +function STATIC:GetUnits() +self:F2({self.StaticName}) +local DCSStatic=self:GetDCSObject() +local Statics={} +if DCSStatic then +Statics[1]=STATIC:Find(DCSStatic) +self:T3(Statics) +return Statics +end +return nil +end +function STATIC:GetThreatLevel() +return 1,"Static" +end +function STATIC:SpawnAt(Coordinate,Heading,Delay) +Heading=Heading or 0 +if Delay and Delay>0 then +SCHEDULER:New(nil,self.SpawnAt,{self,Coordinate,Heading},Delay) +else +local SpawnStatic=SPAWNSTATIC:NewFromStatic(self.StaticName) +SpawnStatic:SpawnFromPointVec2(Coordinate,Heading,self.StaticName) +end +return self +end +function STATIC:ReSpawn(CountryID,Delay) +if Delay and Delay>0 then +SCHEDULER:New(nil,self.ReSpawn,{self,CountryID},Delay) +else +CountryID=CountryID or self:GetCountry() +local SpawnStatic=SPAWNSTATIC:NewFromStatic(self.StaticName,CountryID) +SpawnStatic:Spawn(nil,self.StaticName) +end +return self +end +function STATIC:ReSpawnAt(Coordinate,Heading,Delay) +if Delay and Delay>0 then +SCHEDULER:New(nil,self.ReSpawnAt,{self,Coordinate,Heading},Delay) +else +local SpawnStatic=SPAWNSTATIC:NewFromStatic(self.StaticName,self:GetCountry()) +SpawnStatic:SpawnFromCoordinate(Coordinate,Heading,self.StaticName) +end +return self +end +AIRBASE={ +ClassName="AIRBASE", +CategoryName={ +[Airbase.Category.AIRDROME]="Airdrome", +[Airbase.Category.HELIPAD]="Helipad", +[Airbase.Category.SHIP]="Ship", +}, +activerwyno=nil, +} +AIRBASE.Caucasus={ +["Gelendzhik"]="Gelendzhik", +["Krasnodar_Pashkovsky"]="Krasnodar-Pashkovsky", +["Sukhumi_Babushara"]="Sukhumi-Babushara", +["Gudauta"]="Gudauta", +["Batumi"]="Batumi", +["Senaki_Kolkhi"]="Senaki-Kolkhi", +["Kobuleti"]="Kobuleti", +["Kutaisi"]="Kutaisi", +["Tbilisi_Lochini"]="Tbilisi-Lochini", +["Soganlug"]="Soganlug", +["Vaziani"]="Vaziani", +["Anapa_Vityazevo"]="Anapa-Vityazevo", +["Krasnodar_Center"]="Krasnodar-Center", +["Novorossiysk"]="Novorossiysk", +["Krymsk"]="Krymsk", +["Maykop_Khanskaya"]="Maykop-Khanskaya", +["Sochi_Adler"]="Sochi-Adler", +["Mineralnye_Vody"]="Mineralnye Vody", +["Nalchik"]="Nalchik", +["Mozdok"]="Mozdok", +["Beslan"]="Beslan", +} +AIRBASE.Nevada={ +["Creech_AFB"]="Creech AFB", +["Groom_Lake_AFB"]="Groom Lake AFB", +["McCarran_International_Airport"]="McCarran International Airport", +["Nellis_AFB"]="Nellis AFB", +["Beatty_Airport"]="Beatty Airport", +["Boulder_City_Airport"]="Boulder City Airport", +["Echo_Bay"]="Echo Bay", +["Henderson_Executive_Airport"]="Henderson Executive Airport", +["Jean_Airport"]="Jean Airport", +["Laughlin_Airport"]="Laughlin Airport", +["Lincoln_County"]="Lincoln County", +["Mesquite"]="Mesquite", +["Mina_Airport_3Q0"]="Mina Airport 3Q0", +["North_Las_Vegas"]="North Las Vegas", +["Pahute_Mesa_Airstrip"]="Pahute Mesa Airstrip", +["Tonopah_Airport"]="Tonopah Airport", +["Tonopah_Test_Range_Airfield"]="Tonopah Test Range Airfield", +} +AIRBASE.Normandy={ +["Saint_Pierre_du_Mont"]="Saint Pierre du Mont", +["Lignerolles"]="Lignerolles", +["Cretteville"]="Cretteville", +["Maupertus"]="Maupertus", +["Brucheville"]="Brucheville", +["Meautis"]="Meautis", +["Cricqueville_en_Bessin"]="Cricqueville-en-Bessin", +["Lessay"]="Lessay", +["Sainte_Laurent_sur_Mer"]="Sainte-Laurent-sur-Mer", +["Biniville"]="Biniville", +["Cardonville"]="Cardonville", +["Deux_Jumeaux"]="Deux Jumeaux", +["Chippelle"]="Chippelle", +["Beuzeville"]="Beuzeville", +["Azeville"]="Azeville", +["Picauville"]="Picauville", +["Le_Molay"]="Le Molay", +["Longues_sur_Mer"]="Longues-sur-Mer", +["Carpiquet"]="Carpiquet", +["Bazenville"]="Bazenville", +["Sainte_Croix_sur_Mer"]="Sainte-Croix-sur-Mer", +["Beny_sur_Mer"]="Beny-sur-Mer", +["Rucqueville"]="Rucqueville", +["Sommervieu"]="Sommervieu", +["Lantheuil"]="Lantheuil", +["Evreux"]="Evreux", +["Chailey"]="Chailey", +["Needs_Oar_Point"]="Needs Oar Point", +["Funtington"]="Funtington", +["Tangmere"]="Tangmere", +["Ford_AF"]="Ford_AF", +["Goulet"]="Goulet", +["Argentan"]="Argentan", +["Vrigny"]="Vrigny", +["Essay"]="Essay", +["Hauterive"]="Hauterive", +["Barville"]="Barville", +["Conches"]="Conches", +} +AIRBASE.PersianGulf={ +["Abu_Dhabi_International_Airport"]="Abu Dhabi Intl", +["Abu_Musa_Island_Airport"]="Abu Musa Island", +["Al_Ain_International_Airport"]="Al Ain Intl", +["Al_Bateen_Airport"]="Al-Bateen", +["Al_Dhafra_AB"]="Al Dhafra AFB", +["Al_Maktoum_Intl"]="Al Maktoum Intl", +["Al_Minhad_AB"]="Al Minhad AFB", +["Bandar_Abbas_Intl"]="Bandar Abbas Intl", +["Bandar_Lengeh"]="Bandar Lengeh", +["Bandar_e_Jask_airfield"]="Bandar-e-Jask", +["Dubai_Intl"]="Dubai Intl", +["Fujairah_Intl"]="Fujairah Intl", +["Havadarya"]="Havadarya", +["Jiroft_Airport"]="Jiroft", +["Kerman_Airport"]="Kerman", +["Khasab"]="Khasab", +["Kish_International_Airport"]="Kish Intl", +["Lar_Airbase"]="Lar", +["Lavan_Island_Airport"]="Lavan Island", +["Liwa_Airbase"]="Liwa AFB", +["Qeshm_Island"]="Qeshm Island", +["Ras_Al_Khaimah"]="Ras Al Khaimah Intl", +["Sas_Al_Nakheel_Airport"]="Sas Al Nakheel", +["Sharjah_Intl"]="Sharjah Intl", +["Shiraz_International_Airport"]="Shiraz Intl", +["Sir_Abu_Nuayr"]="Sir Abu Nuayr", +["Sirri_Island"]="Sirri Island", +["Tunb_Island_AFB"]="Tunb Island AFB", +["Tunb_Kochak"]="Tunb Kochak", +} +AIRBASE.TheChannel={ +["Abbeville_Drucat"]="Abbeville Drucat", +["Merville_Calonne"]="Merville Calonne", +["Saint_Omer_Longuenesse"]="Saint Omer Longuenesse", +["Dunkirk_Mardyck"]="Dunkirk Mardyck", +["Manston"]="Manston", +["Hawkinge"]="Hawkinge", +["Lympne"]="Lympne", +["Detling"]="Detling", +["High_Halden"]="High Halden", +} +AIRBASE.Syria={ +["Kuweires"]="Kuweires", +["Marj_Ruhayyil"]="Marj Ruhayyil", +["Kiryat_Shmona"]="Kiryat Shmona", +["Marj_as_Sultan_North"]="Marj as Sultan North", +["Eyn_Shemer"]="Eyn Shemer", +["Incirlik"]="Incirlik", +["Damascus"]="Damascus", +["Bassel_Al_Assad"]="Bassel Al-Assad", +["Aleppo"]="Aleppo", +["Qabr_as_Sitt"]="Qabr as Sitt", +["Wujah_Al_Hajar"]="Wujah Al Hajar", +["Al_Dumayr"]="Al-Dumayr", +["Hatay"]="Hatay", +["Haifa"]="Haifa", +["Khalkhalah"]="Khalkhalah", +["Megiddo"]="Megiddo", +["Rayak"]="Rayak", +["Mezzeh"]="Mezzeh", +["King_Hussein_Air_College"]="King Hussein Air College", +["Jirah"]="Jirah", +["Taftanaz"]="Taftanaz", +["Rene_Mouawad"]="Rene Mouawad", +["Ramat_David"]="Ramat David", +["Minakh"]="Minakh", +["Adana_Sakirpasa"]="Adana Sakirpasa", +["Marj_as_Sultan_South"]="Marj as Sultan South", +["Hama"]="Hama", +["Al_Qusayr"]="Al Qusayr", +["Palmyra"]="Palmyra", +["Tabqa"]="Tabqa", +["Beirut_Rafic_Hariri"]="Beirut-Rafic Hariri", +["An_Nasiriyah"]="An Nasiriyah", +["Abu_al_Duhur"]="Abu al-Duhur", +["H4"]="H4", +["Gaziantep"]="Gaziantep", +["Rosh_Pina"]="Rosh Pina", +["Sayqal"]="Sayqal", +["Shayrat"]="Shayrat", +["Tiyas"]="Tiyas", +["Tha_lah"]="Tha'lah", +["Naqoura"]="Naqoura", +} +AIRBASE.TerminalType={ +Runway=16, +HelicopterOnly=40, +Shelter=68, +OpenMed=72, +OpenBig=104, +OpenMedOrBig=176, +HelicopterUsable=216, +FighterAircraft=244, +} +function AIRBASE:Register(AirbaseName) +local self=BASE:Inherit(self,POSITIONABLE:New(AirbaseName)) +self.AirbaseName=AirbaseName +self.AirbaseID=self:GetID(true) +self.descriptors=self:GetDesc() +self.category=self.descriptors and self.descriptors.category or Airbase.Category.AIRDROME +if self.category==Airbase.Category.AIRDROME then +self.isAirdrome=true +elseif self.category==Airbase.Category.HELIPAD then +self.isHelipad=true +elseif self.category==Airbase.Category.SHIP then +self.isShip=true +else +self:E("ERROR: Unknown airbase category!") +end +self:_InitParkingSpots() +local vec2=self:GetVec2() +self:GetCoordinate() +if vec2 then +if self.isShip then +local unit=UNIT:FindByName(AirbaseName) +if unit then +self.AirbaseZone=ZONE_UNIT:New(AirbaseName,unit,2500) +end +else +self.AirbaseZone=ZONE_RADIUS:New(AirbaseName,vec2,2500) +end +else +self:E(string.format("ERROR: Cound not get position Vec2 of airbase %s",AirbaseName)) +end +return self +end +function AIRBASE:Find(DCSAirbase) +local AirbaseName=DCSAirbase:getName() +local AirbaseFound=_DATABASE:FindAirbase(AirbaseName) +return AirbaseFound +end +function AIRBASE:FindByName(AirbaseName) +local AirbaseFound=_DATABASE:FindAirbase(AirbaseName) +return AirbaseFound +end +function AIRBASE:FindByID(id) +for name,_airbase in pairs(_DATABASE.AIRBASES)do +local airbase=_airbase +local aid=tonumber(airbase:GetID(true)) +if aid==id then +return airbase +end +end +return nil +end +function AIRBASE:GetDCSObject() +local DCSAirbase=Airbase.getByName(self.AirbaseName) +if DCSAirbase then +return DCSAirbase +end +return nil +end +function AIRBASE:GetZone() +return self.AirbaseZone +end +function AIRBASE.GetAllAirbases(coalition,category) +local airbases={} +for _,_airbase in pairs(_DATABASE.AIRBASES)do +local airbase=_airbase +if coalition==nil or airbase:GetCoalition()==coalition then +if category==nil or category==airbase:GetAirbaseCategory()then +table.insert(airbases,airbase) +end +end +end +return airbases +end +function AIRBASE.GetAllAirbaseNames(coalition,category) +local airbases={} +for airbasename,_airbase in pairs(_DATABASE.AIRBASES)do +local airbase=_airbase +if coalition==nil or airbase:GetCoalition()==coalition then +if category==nil or category==airbase:GetAirbaseCategory()then +table.insert(airbases,airbasename) +end +end +end +return airbases +end +function AIRBASE:GetID(unique) +if self.AirbaseID then +return unique and self.AirbaseID or math.abs(self.AirbaseID) +else +for DCSAirbaseId,DCSAirbase in ipairs(world.getAirbases())do +local AirbaseName=DCSAirbase:getName() +local airbaseID=tonumber(DCSAirbase:getID()) +local airbaseCategory=self:GetAirbaseCategory() +if AirbaseName==self.AirbaseName then +if airbaseCategory==Airbase.Category.SHIP or airbaseCategory==Airbase.Category.HELIPAD then +return unique and-airbaseID or airbaseID +else +return airbaseID +end +end +end +end +return nil +end +function AIRBASE:SetParkingSpotWhitelist(TerminalIdWhitelist) +if TerminalIdWhitelist==nil then +self.parkingWhitelist={} +return self +end +if type(TerminalIdWhitelist)~="table"then +TerminalIdWhitelist={TerminalIdWhitelist} +end +self.parkingWhitelist=TerminalIdWhitelist +return self +end +function AIRBASE:SetParkingSpotBlacklist(TerminalIdBlacklist) +if TerminalIdBlacklist==nil then +self.parkingBlacklist={} +return self +end +if type(TerminalIdBlacklist)~="table"then +TerminalIdBlacklist={TerminalIdBlacklist} +end +self.parkingBlacklist=TerminalIdBlacklist +return self +end +function AIRBASE:GetAirbaseCategory() +return self.category +end +function AIRBASE:IsAirdrome() +return self.isAirdrome +end +function AIRBASE:IsHelipad() +return self.isHelipad +end +function AIRBASE:IsShip() +return self.isShip +end +function AIRBASE:GetParkingData(available) +self:F2(available) +local DCSAirbase=self:GetDCSObject() +local parkingdata=nil +if DCSAirbase then +parkingdata=DCSAirbase:getParking(available) +end +self:T2({parkingdata=parkingdata}) +return parkingdata +end +function AIRBASE:GetParkingSpotsNumber(termtype) +local parkingdata=self:GetParkingData(false) +local nspots=0 +for _,parkingspot in pairs(parkingdata)do +if AIRBASE._CheckTerminalType(parkingspot.Term_Type,termtype)then +nspots=nspots+1 +end +end +return nspots +end +function AIRBASE:GetFreeParkingSpotsNumber(termtype,allowTOAC) +local parkingdata=self:GetParkingData(true) +local nfree=0 +for _,parkingspot in pairs(parkingdata)do +if AIRBASE._CheckTerminalType(parkingspot.Term_Type,termtype)then +if(allowTOAC and allowTOAC==true)or parkingspot.TO_AC==false then +nfree=nfree+1 +end +end +end +return nfree +end +function AIRBASE:GetFreeParkingSpotsCoordinates(termtype,allowTOAC) +local parkingdata=self:GetParkingData(true) +local spots={} +for _,parkingspot in pairs(parkingdata)do +if AIRBASE._CheckTerminalType(parkingspot.Term_Type,termtype)then +if(allowTOAC and allowTOAC==true)or parkingspot.TO_AC==false then +table.insert(spots,COORDINATE:NewFromVec3(parkingspot.vTerminalPos)) +end +end +end +return spots +end +function AIRBASE:GetParkingSpotsCoordinates(termtype) +local parkingdata=self:GetParkingData(false) +local spots={} +for _,parkingspot in ipairs(parkingdata)do +if AIRBASE._CheckTerminalType(parkingspot.Term_Type,termtype)then +local _coord=COORDINATE:NewFromVec3(parkingspot.vTerminalPos) +table.insert(spots,_coord) +end +end +return spots +end +function AIRBASE:_InitParkingSpots() +local parkingdata=self:GetParkingData(false) +self.parking={} +self.parkingByID={} +self.NparkingTotal=0 +self.NparkingTerminal={} +for _,terminalType in pairs(AIRBASE.TerminalType)do +self.NparkingTerminal[terminalType]=0 +end +for _,spot in pairs(parkingdata)do +local park={} +park.Vec3=spot.vTerminalPos +park.Coordinate=COORDINATE:NewFromVec3(spot.vTerminalPos) +park.DistToRwy=spot.fDistToRW +park.Free=nil +park.TerminalID=spot.Term_Index +park.TerminalID0=spot.Term_Index_0 +park.TerminalType=spot.Term_Type +park.TOAC=spot.TO_AC +self.NparkingTotal=self.NparkingTotal+1 +for _,terminalType in pairs(AIRBASE.TerminalType)do +if self._CheckTerminalType(terminalType,park.TerminalType)then +self.NparkingTerminal[terminalType]=self.NparkingTerminal[terminalType]+1 +end +end +self.parkingByID[park.TerminalID]=park +table.insert(self.parking,park) +end +return self +end +function AIRBASE:_GetParkingSpotByID(TerminalID) +return self.parkingByID[TerminalID] +end +function AIRBASE:GetParkingSpotsTable(termtype) +local parkingdata=self:GetParkingData(false) +local parkingfree=self:GetParkingData(true) +local function _isfree(_tocheck) +for _,_spot in pairs(parkingfree)do +if _spot.Term_Index==_tocheck.Term_Index then +return true +end +end +return false +end +local spots={} +for _,_spot in pairs(parkingdata)do +if AIRBASE._CheckTerminalType(_spot.Term_Type,termtype)then +local spot=self:_GetParkingSpotByID(_spot.Term_Index) +if spot then +spot.Free=_isfree(_spot) +spot.TOAC=_spot.TO_AC +table.insert(spots,spot) +else +self:E(string.format("ERROR: Parking spot %s is nil!",tostring(_spot.Term_Index))) +end +end +end +return spots +end +function AIRBASE:GetFreeParkingSpotsTable(termtype,allowTOAC) +local parkingfree=self:GetParkingData(true) +local freespots={} +for _,_spot in pairs(parkingfree)do +if AIRBASE._CheckTerminalType(_spot.Term_Type,termtype)and _spot.Term_Index>0 then +if(allowTOAC and allowTOAC==true)or _spot.TO_AC==false then +local spot=self:_GetParkingSpotByID(_spot.Term_Index) +spot.Free=true +spot.TOAC=_spot.TO_AC +table.insert(freespots,spot) +end +end +end +return freespots +end +function AIRBASE:GetParkingSpotData(TerminalID) +local parkingdata=self:GetParkingSpotsTable() +for _,_spot in pairs(parkingdata)do +local spot=_spot +self:T({TerminalID=spot.TerminalID,TerminalType=spot.TerminalType}) +if TerminalID==spot.TerminalID then +return spot +end +end +self:E("ERROR: Could not find spot with Terminal ID="..tostring(TerminalID)) +return nil +end +function AIRBASE:MarkParkingSpots(termtype,mark) +if mark==nil then +mark=true +end +local parkingdata=self:GetParkingSpotsTable(termtype) +local airbasename=self:GetName() +self:E(string.format("Parking spots at %s for termial type %s:",airbasename,tostring(termtype))) +for _,_spot in pairs(parkingdata)do +local _text=string.format("Term Index=%d, Term Type=%d, Free=%s, TOAC=%s, Term ID0=%d, Dist2Rwy=%.1f m", +_spot.TerminalID,_spot.TerminalType,tostring(_spot.Free),tostring(_spot.TOAC),_spot.TerminalID0,_spot.DistToRwy) +if mark then +_spot.Coordinate:MarkToAll(_text) +end +local _text=string.format("%s, Term Index=%3d, Term Type=%03d, Free=%5s, TOAC=%5s, Term ID0=%3d, Dist2Rwy=%.1f m", +airbasename,_spot.TerminalID,_spot.TerminalType,tostring(_spot.Free),tostring(_spot.TOAC),_spot.TerminalID0,_spot.DistToRwy) +self:E(_text) +end +end +function AIRBASE:FindFreeParkingSpotForAircraft(group,terminaltype,scanradius,scanunits,scanstatics,scanscenery,verysafe,nspots,parkingdata) +scanradius=scanradius or 50 +if scanunits==nil then +scanunits=true +end +if scanstatics==nil then +scanstatics=true +end +if scanscenery==nil then +scanscenery=false +end +if verysafe==nil then +verysafe=false +end +local function _overlap(object1,object2,dist) +local pos1=object1 +local pos2=object2 +local r1=pos1:GetBoundingRadius() +local r2=pos2:GetBoundingRadius() +if r1 and r2 then +local safedist=(r1+r2)*1.1 +local safe=(dist>safedist) +self:T2(string.format("r1=%.1f r2=%.1f s=%.1f d=%.1f ==> safe=%s",r1,r2,safedist,dist,tostring(safe))) +return safe +else +return true +end +end +local airport=self:GetName() +parkingdata=parkingdata or self:GetParkingSpotsTable(terminaltype) +local aircraft=group:GetUnit(1) +local _aircraftsize,ax,ay,az=aircraft:GetObjectSize() +local _nspots=nspots or group:GetSize() +self:E(string.format("%s: Looking for %d parking spot(s) for aircraft of size %.1f m (x=%.1f,y=%.1f,z=%.1f) at termial type %s.",airport,_nspots,_aircraftsize,ax,ay,az,tostring(terminaltype))) +local validspots={} +local nvalid=0 +local _test=false +if _test then +return validspots +end +local markobstacles=false +for _,parkingspot in pairs(parkingdata)do +local _spot=parkingspot.Coordinate +local _termid=parkingspot.TerminalID +if AIRBASE._CheckTerminalType(parkingspot.TerminalType,terminaltype)and self:_CheckParkingLists(_termid)then +if verysafe and(parkingspot.Free==false or parkingspot.TOAC==true)then +self:T(string.format("%s: Parking spot id %d NOT free (or aircraft has not taken off yet). Free=%s, TOAC=%s.",airport,parkingspot.TerminalID,tostring(parkingspot.Free),tostring(parkingspot.TOAC))) +else +local _,_,_,_units,_statics,_sceneries=_spot:ScanObjects(scanradius,scanunits,scanstatics,scanscenery) +local occupied=false +for _,unit in pairs(_units)do +local _coord=unit:GetCoordinate() +local _dist=_coord:Get2DDistance(_spot) +local _safe=_overlap(aircraft,unit,_dist) +if markobstacles then +local l,x,y,z=unit:GetObjectSize() +_coord:MarkToAll(string.format("Unit %s\nx=%.1f y=%.1f z=%.1f\nl=%.1f d=%.1f\nspot %d safe=%s",unit:GetName(),x,y,z,l,_dist,_termid,tostring(_safe))) +end +if scanunits and not _safe then +occupied=true +end +end +for _,static in pairs(_statics)do +local _static=STATIC:Find(static) +local _vec3=static:getPoint() +local _coord=COORDINATE:NewFromVec3(_vec3) +local _dist=_coord:Get2DDistance(_spot) +local _safe=_overlap(aircraft,_static,_dist) +if markobstacles then +local l,x,y,z=_static:GetObjectSize() +_coord:MarkToAll(string.format("Static %s\nx=%.1f y=%.1f z=%.1f\nl=%.1f d=%.1f\nspot %d safe=%s",static:getName(),x,y,z,l,_dist,_termid,tostring(_safe))) +end +if scanstatics and not _safe then +occupied=true +end +end +for _,scenery in pairs(_sceneries)do +local _scenery=SCENERY:Register(scenery:getTypeName(),scenery) +local _vec3=scenery:getPoint() +local _coord=COORDINATE:NewFromVec3(_vec3) +local _dist=_coord:Get2DDistance(_spot) +local _safe=_overlap(aircraft,_scenery,_dist) +if markobstacles then +local l,x,y,z=scenery:GetObjectSize(scenery) +_coord:MarkToAll(string.format("Scenery %s\nx=%.1f y=%.1f z=%.1f\nl=%.1f d=%.1f\nspot %d safe=%s",scenery:getTypeName(),x,y,z,l,_dist,_termid,tostring(_safe))) +end +if scanscenery and not _safe then +occupied=true +end +end +for _,_takenspot in pairs(validspots)do +local _dist=_takenspot.Coordinate:Get2DDistance(_spot) +local _safe=_overlap(aircraft,aircraft,_dist) +if not _safe then +occupied=true +end +end +if occupied then +self:I(string.format("%s: Parking spot id %d occupied.",airport,_termid)) +else +self:I(string.format("%s: Parking spot id %d free.",airport,_termid)) +if nvalid<_nspots then +table.insert(validspots,{Coordinate=_spot,TerminalID=_termid}) +end +nvalid=nvalid+1 +self:I(string.format("%s: Parking spot id %d free. Nfree=%d/%d.",airport,_termid,nvalid,_nspots)) +end +end +if nvalid>=_nspots then +return validspots +end +end +end +return validspots +end +function AIRBASE:_CheckParkingLists(TerminalID) +if self.parkingBlacklist and#self.parkingBlacklist>0 then +for _,terminalID in pairs(self.parkingBlacklist or{})do +if terminalID==TerminalID then +return false +end +end +end +if self.parkingWhitelist and#self.parkingWhitelist>0 then +for _,terminalID in pairs(self.parkingWhitelist or{})do +if terminalID==TerminalID then +return true +end +end +return false +end +return true +end +function AIRBASE._CheckTerminalType(Term_Type,termtype) +if Term_Type==nil then +return false +end +if termtype==nil then +if Term_Type==AIRBASE.TerminalType.Runway then +return false +else +return true +end +end +local match=false +if Term_Type==termtype then +match=true +end +if termtype==AIRBASE.TerminalType.OpenMedOrBig then +if Term_Type==AIRBASE.TerminalType.OpenMed or Term_Type==AIRBASE.TerminalType.OpenBig then +match=true +end +elseif termtype==AIRBASE.TerminalType.HelicopterUsable then +if Term_Type==AIRBASE.TerminalType.OpenMed or Term_Type==AIRBASE.TerminalType.OpenBig or Term_Type==AIRBASE.TerminalType.HelicopterOnly then +match=true +end +elseif termtype==AIRBASE.TerminalType.FighterAircraft then +if Term_Type==AIRBASE.TerminalType.OpenMed or Term_Type==AIRBASE.TerminalType.OpenBig or Term_Type==AIRBASE.TerminalType.Shelter then +match=true +end +end +return match +end +function AIRBASE:GetRunwayData(magvar,mark) +local runways={} +if self:GetAirbaseCategory()~=Airbase.Category.AIRDROME then +return{} +end +local runwaycoords=self:GetParkingSpotsCoordinates(AIRBASE.TerminalType.Runway) +if false then +for i,_coord in pairs(runwaycoords)do +local coord=_coord +coord:Translate(100,0):MarkToAll("Runway i="..i) +end +end +magvar=magvar or UTILS.GetMagneticDeclination() +local N=#runwaycoords +local N2=N/2 +local exception=false +local name=self:GetName() +if name==AIRBASE.Nevada.Jean_Airport or +name==AIRBASE.Nevada.Creech_AFB or +name==AIRBASE.PersianGulf.Abu_Dhabi_International_Airport or +name==AIRBASE.PersianGulf.Dubai_Intl or +name==AIRBASE.PersianGulf.Shiraz_International_Airport or +name==AIRBASE.PersianGulf.Kish_International_Airport then +exception=1 +elseif UTILS.GetDCSMap()==DCSMAP.Syria and N>=2 and +name~=AIRBASE.Syria.Minakh and +name~=AIRBASE.Syria.Damascus and +name~=AIRBASE.Syria.Khalkhalah and +name~=AIRBASE.Syria.Marj_Ruhayyil and +name~=AIRBASE.Syria.Beirut_Rafic_Hariri then +exception=2 +end +local function f(i) +local j +if exception==1 then +j=N-(i-1) +elseif exception==2 then +if i<=N2 then +j=i+N2 +else +j=i-N2 +end +else +if i%2==0 then +j=i-1 +else +j=i+1 +end +end +if name==AIRBASE.Syria.Beirut_Rafic_Hariri then +if i==1 then +j=3 +elseif i==2 then +j=6 +elseif i==3 then +j=1 +elseif i==4 then +j=5 +elseif i==5 then +j=4 +elseif i==6 then +j=2 +end +end +if name==AIRBASE.Syria.Ramat_David then +if i==1 then +j=4 +elseif i==2 then +j=6 +elseif i==3 then +j=5 +elseif i==4 then +j=1 +elseif i==5 then +j=3 +elseif i==6 then +j=2 +end +end +return j +end +for i=1,N do +local j=f(i) +local c1=runwaycoords[i] +local c2=runwaycoords[j] +local hdg=c1:HeadingTo(c2) +local idx=string.format("%02d",UTILS.Round((hdg-magvar)/10,0)) +local runway={} +runway.heading=hdg +runway.idx=idx +runway.length=c1:Get2DDistance(c2) +runway.position=c1 +runway.endpoint=c2 +if mark then +runway.position:MarkToAll(string.format("Runway %s: true heading=%03d (magvar=%d), length=%d m, i=%d, j=%d",runway.idx,runway.heading,magvar,runway.length,i,j)) +end +table.insert(runways,runway) +end +return runways +end +function AIRBASE:SetActiveRunway(iactive) +self.activerwyno=iactive +end +function AIRBASE:GetActiveRunway(magvar) +local runways=self:GetRunwayData(magvar) +if self.activerwyno then +return runways[self.activerwyno] +end +local Vwind=self:GetCoordinate():GetWindWithTurbulenceVec3() +local norm=UTILS.VecNorm(Vwind) +local iact=1 +if norm>0 then +Vwind.x=Vwind.x/norm +Vwind.y=0 +Vwind.z=Vwind.z/norm +local dotmin=nil +for i,_runway in pairs(runways)do +local runway=_runway +local alpha=math.rad(runway.heading) +local Vrunway={x=math.cos(alpha),y=0,z=math.sin(alpha)} +local dot=UTILS.VecDot(Vwind,Vrunway) +if dotmin==nil or dot radius %.1f m. Despawn = %s.",self:GetName(),unit:GetName(),group:GetName(),_i,dist,radius,tostring(despawn))) +end +end +else +self:T(string.format("%s, checking if unit %s of group %s is on runway. Unit is NOT alive.",self:GetName(),unit:GetName(),group:GetName())) +end +end +else +self:T(string.format("%s, checking if group %s is on runway. Group is NOT alive.",self:GetName(),group:GetName())) +end +return false +end +SCENERY={ +ClassName="SCENERY", +} +function SCENERY:Register(SceneryName,SceneryObject) +local self=BASE:Inherit(self,POSITIONABLE:New(SceneryName)) +self.SceneryName=SceneryName +self.SceneryObject=SceneryObject +return self +end +function SCENERY:GetDCSObject() +return self.SceneryObject +end +function SCENERY:GetThreatLevel() +return 0,"Scenery" +end +function SCENERY:FindByName(name) +local findAirbase=function() +local airbases=AIRBASE.GetAllAirbases() +for index,airbase in pairs(airbases)do +local surftype=airbase:GetCoordinate():GetSurfaceType() +if surftype~=land.SurfaceType.SHALLOW_WATER and surftype~=land.SurfaceType.WATER then +return airbase:GetCoordinate() +end +end +return nil +end +local sceneryScan=function(scancoord) +if scancoord~=nil then +local _,_,sceneryfound,_,_,scenerylist=scancoord:ScanObjects(200,false,false,true) +if sceneryfound==true then +scenerylist[1].id_=name +SCENERY.SceneryObject=SCENERY:Register(scenerylist[1].id_,scenerylist[1]) +return SCENERY.SceneryObject +end +end +return nil +end +if SCENERY.SceneryObject then +SCENERY.SceneryObject.SceneryObject.id_=name +SCENERY.SceneryObject.SceneryName=name +return SCENERY:Register(SCENERY.SceneryObject.SceneryObject.id_,SCENERY.SceneryObject.SceneryObject) +else +return sceneryScan(findAirbase()) +end +end +MARKER={ +ClassName="MARKER", +Debug=false, +lid=nil, +mid=nil, +coordinate=nil, +text=nil, +message=nil, +readonly=nil, +coalition=nil, +} +_MARKERID=0 +MARKER.version="0.1.0" +function MARKER:New(Coordinate,Text) +local self=BASE:Inherit(self,FSM:New()) +self.coordinate=Coordinate +self.text=Text +self.readonly=false +self.message="" +_MARKERID=_MARKERID+1 +self.myid=_MARKERID +self.lid=string.format("Marker #%d | ",self.myid) +self:SetStartState("Invisible") +self:AddTransition("Invisible","Added","Visible") +self:AddTransition("Visible","Removed","Invisible") +self:AddTransition("*","Changed","*") +self:AddTransition("*","TextUpdate","*") +self:AddTransition("*","CoordUpdate","*") +self:HandleEvent(EVENTS.MarkAdded) +self:HandleEvent(EVENTS.MarkRemoved) +self:HandleEvent(EVENTS.MarkChange) +return self +end +function MARKER:ReadOnly() +self.readonly=true +return self +end +function MARKER:Message(Text) +self.message=Text or"" +return self +end +function MARKER:ToAll(Delay) +if Delay and Delay>0 then +self:ScheduleOnce(Delay,MARKER.ToAll,self) +else +self.toall=true +self.tocoaliton=nil +self.coalition=nil +self.togroup=nil +self.groupname=nil +self.groupid=nil +if self.shown then +self:Remove() +end +self.mid=UTILS.GetMarkID() +trigger.action.markToAll(self.mid,self.text,self.coordinate:GetVec3(),self.readonly,self.message) +end +return self +end +function MARKER:ToCoalition(Coalition,Delay) +if Delay and Delay>0 then +self:ScheduleOnce(Delay,MARKER.ToCoalition,self,Coalition) +else +self.coalition=Coalition +self.tocoaliton=true +self.toall=false +self.togroup=false +self.groupname=nil +self.groupid=nil +if self.shown then +self:Remove() +end +self.mid=UTILS.GetMarkID() +trigger.action.markToCoalition(self.mid,self.text,self.coordinate:GetVec3(),self.coalition,self.readonly,self.message) +end +return self +end +function MARKER:ToBlue(Delay) +self:ToCoalition(coalition.side.BLUE,Delay) +return self +end +function MARKER:ToRed(Delay) +self:ToCoalition(coalition.side.RED,Delay) +return self +end +function MARKER:ToNeutral(Delay) +self:ToCoalition(coalition.side.NEUTRAL,Delay) +return self +end +function MARKER:ToGroup(Group,Delay) +if Delay and Delay>0 then +self:ScheduleOnce(Delay,MARKER.ToGroup,self,Group) +else +if Group and Group:IsAlive()~=nil then +self.groupid=Group:GetID() +if self.groupid then +self.groupname=Group:GetName() +self.togroup=true +self.tocoaliton=nil +self.coalition=nil +self.toall=nil +if self.shown then +self:Remove() +end +self.mid=UTILS.GetMarkID() +trigger.action.markToGroup(self.mid,self.text,self.coordinate:GetVec3(),self.groupid,self.readonly,self.message) +end +else +end +end +return self +end +function MARKER:UpdateText(Text,Delay) +if Delay and Delay>0 then +self:ScheduleOnce(Delay,MARKER.UpdateText,self,Text) +else +self.text=tostring(Text) +self:Refresh() +self:TextUpdate(tostring(Text)) +end +return self +end +function MARKER:UpdateCoordinate(Coordinate,Delay) +if Delay and Delay>0 then +self:ScheduleOnce(Delay,MARKER.UpdateCoordinate,self,Coordinate) +else +self.coordinate=Coordinate +self:Refresh() +self:CoordUpdate(Coordinate) +end +return self +end +function MARKER:Refresh(Delay) +if Delay and Delay>0 then +self:ScheduleOnce(Delay,MARKER.Refresh,self) +else +if self.toall then +self:ToAll() +elseif self.tocoaliton then +self:ToCoalition(self.coalition) +elseif self.togroup then +local group=GROUP:FindByName(self.groupname) +self:ToGroup(group) +else +self:E(self.lid.."ERROR: unknown To in :Refresh()!") +end +end +return self +end +function MARKER:Remove(Delay) +if Delay and Delay>0 then +self:ScheduleOnce(Delay,MARKER.Remove,self) +else +if self.shown then +trigger.action.removeMark(self.mid) +end +end +return self +end +function MARKER:GetCoordinate() +return self.coordinate +end +function MARKER:GetText() +return self.text +end +function MARKER:SetText(Text) +self.text=Text and tostring(Text)or"" +return self +end +function MARKER:IsVisible() +return self:Is("Visible") +end +function MARKER:IsInvisible() +return self:Is("Invisible") +end +function MARKER:OnEventMarkAdded(EventData) +if EventData and EventData.MarkID then +local MarkID=EventData.MarkID +self:T3(self.lid..string.format("Captured event MarkAdded for Mark ID=%s",tostring(MarkID))) +if MarkID==self.mid then +self.shown=true +self:Added(EventData) +end +end +end +function MARKER:OnEventMarkRemoved(EventData) +if EventData and EventData.MarkID then +local MarkID=EventData.MarkID +self:T3(self.lid..string.format("Captured event MarkAdded for Mark ID=%s",tostring(MarkID))) +if MarkID==self.mid then +self.shown=false +self:Removed(EventData) +end +end +end +function MARKER:OnEventMarkChange(EventData) +if EventData and EventData.MarkID then +local MarkID=EventData.MarkID +self:T3(self.lid..string.format("Captured event MarkChange for Mark ID=%s",tostring(MarkID))) +if MarkID==self.mid then +self:Changed(EventData) +self:TextChanged(tostring(EventData.MarkText)) +end +end +end +function MARKER:onafterAdded(From,Event,To,EventData) +local text=string.format("Captured event MarkAdded for myself:\n") +text=text..string.format("Marker ID = %s\n",tostring(EventData.MarkID)) +text=text..string.format("Coalition = %s\n",tostring(EventData.MarkCoalition)) +text=text..string.format("Group ID = %s\n",tostring(EventData.MarkGroupID)) +text=text..string.format("Initiator = %s\n",EventData.IniUnit and EventData.IniUnit:GetName()or"Nobody") +text=text..string.format("Coordinate = %s\n",EventData.MarkCoordinate and EventData.MarkCoordinate:ToStringLLDMS()or"Nowhere") +text=text..string.format("Text: \n%s",tostring(EventData.MarkText)) +self:T2(self.lid..text) +end +function MARKER:onafterRemoved(From,Event,To,EventData) +local text=string.format("Captured event MarkRemoved for myself:\n") +text=text..string.format("Marker ID = %s\n",tostring(EventData.MarkID)) +text=text..string.format("Coalition = %s\n",tostring(EventData.MarkCoalition)) +text=text..string.format("Group ID = %s\n",tostring(EventData.MarkGroupID)) +text=text..string.format("Initiator = %s\n",EventData.IniUnit and EventData.IniUnit:GetName()or"Nobody") +text=text..string.format("Coordinate = %s\n",EventData.MarkCoordinate and EventData.MarkCoordinate:ToStringLLDMS()or"Nowhere") +text=text..string.format("Text: \n%s",tostring(EventData.MarkText)) +self:T2(self.lid..text) +end +function MARKER:onafterChanged(From,Event,To,EventData) +local text=string.format("Captured event MarkChange for myself:\n") +text=text..string.format("Marker ID = %s\n",tostring(EventData.MarkID)) +text=text..string.format("Coalition = %s\n",tostring(EventData.MarkCoalition)) +text=text..string.format("Group ID = %s\n",tostring(EventData.MarkGroupID)) +text=text..string.format("Initiator = %s\n",EventData.IniUnit and EventData.IniUnit:GetName()or"Nobody") +text=text..string.format("Coordinate = %s\n",EventData.MarkCoordinate and EventData.MarkCoordinate:ToStringLLDMS()or"Nowhere") +text=text..string.format("Text: \n%s",tostring(EventData.MarkText)) +self:T2(self.lid..text) +end +function MARKER:onafterTextUpdate(From,Event,To,Text) +self:T(self.lid..string.format("New Marker Text:\n%s",Text)) +end +function MARKER:onafterCoordUpdate(From,Event,To,Coordinate) +self:T(self.lid..string.format("New Marker Coordinate in LL DMS: %s",Coordinate:ToStringLLDMS())) +end +CARGOS={} +do +CARGO={ +ClassName="CARGO", +Type=nil, +Name=nil, +Weight=nil, +CargoObject=nil, +CargoCarrier=nil, +Representable=false, +Slingloadable=false, +Moveable=false, +Containable=false, +Reported={}, +} +function CARGO:New(Type,Name,Weight,LoadRadius,NearRadius) +local self=BASE:Inherit(self,FSM:New()) +self:F({Type,Name,Weight,LoadRadius,NearRadius}) +self:SetStartState("UnLoaded") +self:AddTransition({"UnLoaded","Boarding"},"Board","Boarding") +self:AddTransition("Boarding","Boarding","Boarding") +self:AddTransition("Boarding","CancelBoarding","UnLoaded") +self:AddTransition("Boarding","Load","Loaded") +self:AddTransition("UnLoaded","Load","Loaded") +self:AddTransition("Loaded","UnBoard","UnBoarding") +self:AddTransition("UnBoarding","UnBoarding","UnBoarding") +self:AddTransition("UnBoarding","UnLoad","UnLoaded") +self:AddTransition("Loaded","UnLoad","UnLoaded") +self:AddTransition("*","Damaged","Damaged") +self:AddTransition("*","Destroyed","Destroyed") +self:AddTransition("*","Respawn","UnLoaded") +self:AddTransition("*","Reset","UnLoaded") +self.Type=Type +self.Name=Name +self.Weight=Weight or 0 +self.CargoObject=nil +self.CargoCarrier=nil +self.Representable=false +self.Slingloadable=false +self.Moveable=false +self.Containable=false +self.CargoLimit=0 +self.LoadRadius=LoadRadius or 500 +self:SetDeployed(false) +self.CargoScheduler=SCHEDULER:New() +CARGOS[self.Name]=self +return self +end +function CARGO:FindByName(CargoName) +local CargoFound=_DATABASE:FindCargo(CargoName) +return CargoFound +end +function CARGO:GetX() +if self:IsLoaded()then +return self.CargoCarrier:GetCoordinate().x +else +return self.CargoObject:GetCoordinate().x +end +end +function CARGO:GetY() +if self:IsLoaded()then +return self.CargoCarrier:GetCoordinate().z +else +return self.CargoObject:GetCoordinate().z +end +end +function CARGO:GetHeading() +if self:IsLoaded()then +return self.CargoCarrier:GetHeading() +else +return self.CargoObject:GetHeading() +end +end +function CARGO:CanSlingload() +return false +end +function CARGO:CanBoard() +return true +end +function CARGO:CanUnboard() +return true +end +function CARGO:CanLoad() +return true +end +function CARGO:CanUnload() +return true +end +function CARGO:Destroy() +if self.CargoObject then +self.CargoObject:Destroy() +end +self:Destroyed() +end +function CARGO:GetName() +return self.Name +end +function CARGO:GetObject() +if self:IsLoaded()then +return self.CargoCarrier +else +return self.CargoObject +end +end +function CARGO:GetObjectName() +if self:IsLoaded()then +return self.CargoCarrier:GetName() +else +return self.CargoObject:GetName() +end +end +function CARGO:GetCount() +return 1 +end +function CARGO:GetType() +return self.Type +end +function CARGO:GetTransportationMethod() +return self.TransportationMethod +end +function CARGO:GetCoalition() +if self:IsLoaded()then +return self.CargoCarrier:GetCoalition() +else +return self.CargoObject:GetCoalition() +end +end +function CARGO:GetCoordinate() +return self.CargoObject:GetCoordinate() +end +function CARGO:IsDestroyed() +return self:Is("Destroyed") +end +function CARGO:IsLoaded() +return self:Is("Loaded") +end +function CARGO:IsLoadedInCarrier(Carrier) +return self.CargoCarrier and self.CargoCarrier:GetName()==Carrier:GetName() +end +function CARGO:IsUnLoaded() +return self:Is("UnLoaded") +end +function CARGO:IsBoarding() +return self:Is("Boarding") +end +function CARGO:IsUnboarding() +return self:Is("UnBoarding") +end +function CARGO:IsAlive() +if self:IsLoaded()then +return self.CargoCarrier:IsAlive() +else +return self.CargoObject:IsAlive() +end +end +function CARGO:SetDeployed(Deployed) +self.Deployed=Deployed +end +function CARGO:IsDeployed() +return self.Deployed +end +function CARGO:Spawn(PointVec2) +self:F() +end +function CARGO:Flare(FlareColor) +if self:IsUnLoaded()then +trigger.action.signalFlare(self.CargoObject:GetVec3(),FlareColor,0) +end +end +function CARGO:FlareWhite() +self:Flare(trigger.flareColor.White) +end +function CARGO:FlareYellow() +self:Flare(trigger.flareColor.Yellow) +end +function CARGO:FlareGreen() +self:Flare(trigger.flareColor.Green) +end +function CARGO:FlareRed() +self:Flare(trigger.flareColor.Red) +end +function CARGO:Smoke(SmokeColor,Radius) +if self:IsUnLoaded()then +if Radius then +trigger.action.smoke(self.CargoObject:GetRandomVec3(Radius),SmokeColor) +else +trigger.action.smoke(self.CargoObject:GetVec3(),SmokeColor) +end +end +end +function CARGO:SmokeGreen() +self:Smoke(trigger.smokeColor.Green,Range) +end +function CARGO:SmokeRed() +self:Smoke(trigger.smokeColor.Red,Range) +end +function CARGO:SmokeWhite() +self:Smoke(trigger.smokeColor.White,Range) +end +function CARGO:SmokeOrange() +self:Smoke(trigger.smokeColor.Orange,Range) +end +function CARGO:SmokeBlue() +self:Smoke(trigger.smokeColor.Blue,Range) +end +function CARGO:SetLoadRadius(LoadRadius) +self.LoadRadius=LoadRadius or 150 +end +function CARGO:GetLoadRadius() +return self.LoadRadius +end +function CARGO:IsInLoadRadius(Coordinate) +self:F({Coordinate,LoadRadius=self.LoadRadius}) +local Distance=0 +if self:IsUnLoaded()then +local CargoCoordinate=self.CargoObject:GetCoordinate() +Distance=Coordinate:Get2DDistance(CargoCoordinate) +self:T(Distance) +if Distance<=self.LoadRadius then +return true +end +end +return false +end +function CARGO:IsInReportRadius(Coordinate) +self:F({Coordinate}) +local Distance=0 +if self:IsUnLoaded()then +Distance=Coordinate:Get2DDistance(self.CargoObject:GetCoordinate()) +self:T(Distance) +if Distance<=self.LoadRadius then +return true +end +end +return false +end +function CARGO:IsNear(Coordinate,NearRadius) +if self.CargoObject:IsAlive()then +local Distance=Coordinate:Get2DDistance(self.CargoObject:GetCoordinate()) +if Distance<=NearRadius then +return true +end +end +return false +end +function CARGO:IsInZone(Zone) +if self:IsLoaded()then +return Zone:IsPointVec2InZone(self.CargoCarrier:GetPointVec2()) +else +if self.CargoObject:GetSize()~=0 then +return Zone:IsPointVec2InZone(self.CargoObject:GetPointVec2()) +else +return false +end +end +return nil +end +function CARGO:GetPointVec2() +return self.CargoObject:GetPointVec2() +end +function CARGO:GetCoordinate() +return self.CargoObject:GetCoordinate() +end +function CARGO:GetWeight() +return self.Weight +end +function CARGO:SetWeight(Weight) +self.Weight=Weight +return self +end +function CARGO:GetVolume() +return self.Volume +end +function CARGO:SetVolume(Volume) +self.Volume=Volume +return self +end +function CARGO:MessageToGroup(Message,CarrierGroup,Name) +MESSAGE:New(Message,20,"Cargo "..self:GetName()):ToGroup(CarrierGroup) +end +function CARGO:Report(ReportText,Action,CarrierGroup) +if not self.Reported[CarrierGroup]or not self.Reported[CarrierGroup][Action]then +self.Reported[CarrierGroup]={} +self.Reported[CarrierGroup][Action]=true +self:MessageToGroup(ReportText,CarrierGroup) +if self.ReportFlareColor then +if not self.Reported[CarrierGroup]["Flaring"]then +self:Flare(self.ReportFlareColor) +self.Reported[CarrierGroup]["Flaring"]=true +end +end +if self.ReportSmokeColor then +if not self.Reported[CarrierGroup]["Smoking"]then +self:Smoke(self.ReportSmokeColor) +self.Reported[CarrierGroup]["Smoking"]=true +end +end +end +end +function CARGO:ReportFlare(FlareColor) +self.ReportFlareColor=FlareColor +end +function CARGO:ReportSmoke(SmokeColor) +self.ReportSmokeColor=SmokeColor +end +function CARGO:ReportReset(Action,CarrierGroup) +self.Reported[CarrierGroup][Action]=nil +end +function CARGO:ReportResetAll(CarrierGroup) +self.Reported[CarrierGroup]=nil +end +function CARGO:RespawnOnDestroyed(RespawnDestroyed) +if RespawnDestroyed then +self.onenterDestroyed=function(self) +self:Respawn() +end +else +self.onenterDestroyed=nil +end +end +end +do +CARGO_REPRESENTABLE={ +ClassName="CARGO_REPRESENTABLE" +} +function CARGO_REPRESENTABLE:New(CargoObject,Type,Name,LoadRadius,NearRadius) +local self=BASE:Inherit(self,CARGO:New(Type,Name,0,LoadRadius,NearRadius)) +self:F({Type,Name,LoadRadius,NearRadius}) +local Desc=CargoObject:GetDesc() +self:T({Desc=Desc}) +local Weight=math.random(80,120) +if Desc then +if Desc.typeName=="2B11 mortar"then +Weight=210 +else +Weight=Desc.massEmpty +end +end +self:SetWeight(Weight) +return self +end +function CARGO_REPRESENTABLE:Destroy() +self:F({CargoName=self:GetName()}) +return self +end +function CARGO_REPRESENTABLE:RouteTo(ToPointVec2,Speed) +self:F2(ToPointVec2) +local Points={} +local PointStartVec2=self.CargoObject:GetPointVec2() +Points[#Points+1]=PointStartVec2:WaypointGround(Speed) +Points[#Points+1]=ToPointVec2:WaypointGround(Speed) +local TaskRoute=self.CargoObject:TaskRoute(Points) +self.CargoObject:SetTask(TaskRoute,2) +return self +end +function CARGO_REPRESENTABLE:MessageToGroup(Message,TaskGroup,Name) +local CoordinateZone=ZONE_RADIUS:New("Zone",self:GetCoordinate():GetVec2(),500) +CoordinateZone:Scan({Object.Category.UNIT}) +for _,DCSUnit in pairs(CoordinateZone:GetScannedUnits())do +local NearUnit=UNIT:Find(DCSUnit) +self:F({NearUnit=NearUnit}) +local NearUnitCoalition=NearUnit:GetCoalition() +local CargoCoalition=self:GetCoalition() +if NearUnitCoalition==CargoCoalition then +local Attributes=NearUnit:GetDesc() +self:F({Desc=Attributes}) +if NearUnit:HasAttribute("Trucks")then +MESSAGE:New(Message,20,NearUnit:GetCallsign().." reporting - Cargo "..self:GetName()):ToGroup(TaskGroup) +break +end +end +end +end +end +do +CARGO_REPORTABLE={ +ClassName="CARGO_REPORTABLE" +} +function CARGO_REPORTABLE:New(Type,Name,Weight,LoadRadius,NearRadius) +local self=BASE:Inherit(self,CARGO:New(Type,Name,Weight,LoadRadius,NearRadius)) +self:F({Type,Name,Weight,LoadRadius,NearRadius}) +return self +end +function CARGO_REPORTABLE:MessageToGroup(Message,TaskGroup,Name) +MESSAGE:New(Message,20,"Cargo "..self:GetName().." reporting"):ToGroup(TaskGroup) +end +end +do +CARGO_PACKAGE={ +ClassName="CARGO_PACKAGE" +} +function CARGO_PACKAGE:New(CargoCarrier,Type,Name,Weight,LoadRadius,NearRadius) +local self=BASE:Inherit(self,CARGO_REPRESENTABLE:New(CargoCarrier,Type,Name,Weight,LoadRadius,NearRadius)) +self:F({Type,Name,Weight,LoadRadius,NearRadius}) +self:T(CargoCarrier) +self.CargoCarrier=CargoCarrier +return self +end +function CARGO_PACKAGE:onafterOnBoard(From,Event,To,CargoCarrier,Speed,BoardDistance,LoadDistance,Angle) +self:F() +self.CargoInAir=self.CargoCarrier:InAir() +self:T(self.CargoInAir) +if not self.CargoInAir then +local Points={} +local StartPointVec2=self.CargoCarrier:GetPointVec2() +local CargoCarrierHeading=CargoCarrier:GetHeading() +local CargoDeployHeading=((CargoCarrierHeading+Angle)>=360)and(CargoCarrierHeading+Angle-360)or(CargoCarrierHeading+Angle) +self:T({CargoCarrierHeading,CargoDeployHeading}) +local CargoDeployPointVec2=CargoCarrier:GetPointVec2():Translate(BoardDistance,CargoDeployHeading) +Points[#Points+1]=StartPointVec2:WaypointGround(Speed) +Points[#Points+1]=CargoDeployPointVec2:WaypointGround(Speed) +local TaskRoute=self.CargoCarrier:TaskRoute(Points) +self.CargoCarrier:SetTask(TaskRoute,1) +end +self:Boarded(CargoCarrier,Speed,BoardDistance,LoadDistance,Angle) +end +function CARGO_PACKAGE:IsNear(CargoCarrier) +self:F() +local CargoCarrierPoint=CargoCarrier:GetCoordinate() +local Distance=CargoCarrierPoint:Get2DDistance(self.CargoCarrier:GetCoordinate()) +self:T(Distance) +if Distance<=self.NearRadius then +return true +else +return false +end +end +function CARGO_PACKAGE:onafterOnBoarded(From,Event,To,CargoCarrier,Speed,BoardDistance,LoadDistance,Angle) +self:F() +if self:IsNear(CargoCarrier)then +self:__Load(1,CargoCarrier,Speed,LoadDistance,Angle) +else +self:__Boarded(1,CargoCarrier,Speed,BoardDistance,LoadDistance,Angle) +end +end +function CARGO_PACKAGE:onafterUnBoard(From,Event,To,CargoCarrier,Speed,UnLoadDistance,UnBoardDistance,Radius,Angle) +self:F() +self.CargoInAir=self.CargoCarrier:InAir() +self:T(self.CargoInAir) +if not self.CargoInAir then +self:_Next(self.FsmP.UnLoad,UnLoadDistance,Angle) +local Points={} +local StartPointVec2=CargoCarrier:GetPointVec2() +local CargoCarrierHeading=self.CargoCarrier:GetHeading() +local CargoDeployHeading=((CargoCarrierHeading+Angle)>=360)and(CargoCarrierHeading+Angle-360)or(CargoCarrierHeading+Angle) +self:T({CargoCarrierHeading,CargoDeployHeading}) +local CargoDeployPointVec2=StartPointVec2:Translate(UnBoardDistance,CargoDeployHeading) +Points[#Points+1]=StartPointVec2:WaypointGround(Speed) +Points[#Points+1]=CargoDeployPointVec2:WaypointGround(Speed) +local TaskRoute=CargoCarrier:TaskRoute(Points) +CargoCarrier:SetTask(TaskRoute,1) +end +self:__UnBoarded(1,CargoCarrier,Speed) +end +function CARGO_PACKAGE:onafterUnBoarded(From,Event,To,CargoCarrier,Speed) +self:F() +if self:IsNear(CargoCarrier)then +self:__UnLoad(1,CargoCarrier,Speed) +else +self:__UnBoarded(1,CargoCarrier,Speed) +end +end +function CARGO_PACKAGE:onafterLoad(From,Event,To,CargoCarrier,Speed,LoadDistance,Angle) +self:F() +self.CargoCarrier=CargoCarrier +local StartPointVec2=self.CargoCarrier:GetPointVec2() +local CargoCarrierHeading=self.CargoCarrier:GetHeading() +local CargoDeployHeading=((CargoCarrierHeading+Angle)>=360)and(CargoCarrierHeading+Angle-360)or(CargoCarrierHeading+Angle) +local CargoDeployPointVec2=StartPointVec2:Translate(LoadDistance,CargoDeployHeading) +local Points={} +Points[#Points+1]=StartPointVec2:WaypointGround(Speed) +Points[#Points+1]=CargoDeployPointVec2:WaypointGround(Speed) +local TaskRoute=self.CargoCarrier:TaskRoute(Points) +self.CargoCarrier:SetTask(TaskRoute,1) +end +function CARGO_PACKAGE:onafterUnLoad(From,Event,To,CargoCarrier,Speed,Distance,Angle) +self:F() +local StartPointVec2=self.CargoCarrier:GetPointVec2() +local CargoCarrierHeading=self.CargoCarrier:GetHeading() +local CargoDeployHeading=((CargoCarrierHeading+Angle)>=360)and(CargoCarrierHeading+Angle-360)or(CargoCarrierHeading+Angle) +local CargoDeployPointVec2=StartPointVec2:Translate(Distance,CargoDeployHeading) +self.CargoCarrier=CargoCarrier +local Points={} +Points[#Points+1]=StartPointVec2:WaypointGround(Speed) +Points[#Points+1]=CargoDeployPointVec2:WaypointGround(Speed) +local TaskRoute=self.CargoCarrier:TaskRoute(Points) +self.CargoCarrier:SetTask(TaskRoute,1) +end +end +do +CARGO_UNIT={ +ClassName="CARGO_UNIT" +} +function CARGO_UNIT:New(CargoUnit,Type,Name,LoadRadius,NearRadius) +local self=BASE:Inherit(self,CARGO_REPRESENTABLE:New(CargoUnit,Type,Name,LoadRadius,NearRadius)) +self:T({Type=Type,Name=Name,LoadRadius=LoadRadius,NearRadius=NearRadius}) +self.CargoObject=CargoUnit +self:SetEventPriority(5) +return self +end +function CARGO_UNIT:onenterUnBoarding(From,Event,To,ToPointVec2,NearRadius) +self:F({From,Event,To,ToPointVec2,NearRadius}) +local Angle=180 +local Speed=60 +local DeployDistance=9 +local RouteDistance=60 +if From=="Loaded"then +if not self:IsDestroyed()then +local CargoCarrier=self.CargoCarrier +if CargoCarrier:IsAlive()then +local CargoCarrierPointVec2=CargoCarrier:GetPointVec2() +local CargoCarrierHeading=self.CargoCarrier:GetHeading() +local CargoDeployHeading=((CargoCarrierHeading+Angle)>=360)and(CargoCarrierHeading+Angle-360)or(CargoCarrierHeading+Angle) +local CargoRoutePointVec2=CargoCarrierPointVec2:Translate(RouteDistance,CargoDeployHeading) +local FromDirectionVec3=CargoCarrierPointVec2:GetDirectionVec3(ToPointVec2 or CargoRoutePointVec2) +local FromAngle=CargoCarrierPointVec2:GetAngleDegrees(FromDirectionVec3) +local FromPointVec2=CargoCarrierPointVec2:Translate(DeployDistance,FromAngle) +ToPointVec2=ToPointVec2 or CargoCarrierPointVec2:GetRandomCoordinateInRadius(NearRadius,DeployDistance) +if self.CargoObject then +if CargoCarrier:IsShip()then +self.CargoObject:ReSpawnAt(ToPointVec2,CargoDeployHeading) +else +self.CargoObject:ReSpawnAt(FromPointVec2,CargoDeployHeading) +end +self:F({"CargoUnits:",self.CargoObject:GetGroup():GetName()}) +self.CargoCarrier=nil +local Points={} +Points[#Points+1]=FromPointVec2:WaypointGround(Speed,"Vee") +Points[#Points+1]=ToPointVec2:WaypointGround(Speed,"Vee") +local TaskRoute=self.CargoObject:TaskRoute(Points) +self.CargoObject:SetTask(TaskRoute,1) +self:__UnBoarding(1,ToPointVec2,NearRadius) +end +else +self:Destroyed() +end +end +end +end +function CARGO_UNIT:onleaveUnBoarding(From,Event,To,ToPointVec2,NearRadius) +self:F({From,Event,To,ToPointVec2,NearRadius}) +local Angle=180 +local Speed=10 +local Distance=5 +if From=="UnBoarding"then +return true +end +end +function CARGO_UNIT:onafterUnBoarding(From,Event,To,ToPointVec2,NearRadius) +self:F({From,Event,To,ToPointVec2,NearRadius}) +self.CargoInAir=self.CargoObject:InAir() +self:T(self.CargoInAir) +if not self.CargoInAir then +end +self:__UnLoad(1,ToPointVec2,NearRadius) +end +function CARGO_UNIT:onenterUnLoaded(From,Event,To,ToPointVec2) +self:F({ToPointVec2,From,Event,To}) +local Angle=180 +local Speed=10 +local Distance=5 +if From=="Loaded"then +local StartPointVec2=self.CargoCarrier:GetPointVec2() +local CargoCarrierHeading=self.CargoCarrier:GetHeading() +local CargoDeployHeading=((CargoCarrierHeading+Angle)>=360)and(CargoCarrierHeading+Angle-360)or(CargoCarrierHeading+Angle) +local CargoDeployCoord=StartPointVec2:Translate(Distance,CargoDeployHeading) +ToPointVec2=ToPointVec2 or COORDINATE:New(CargoDeployCoord.x,CargoDeployCoord.z) +if self.CargoObject then +self.CargoObject:ReSpawnAt(ToPointVec2,0) +self.CargoCarrier=nil +end +end +if self.OnUnLoadedCallBack then +self.OnUnLoadedCallBack(self,unpack(self.OnUnLoadedParameters)) +self.OnUnLoadedCallBack=nil +end +end +function CARGO_UNIT:onafterBoard(From,Event,To,CargoCarrier,NearRadius,...) +self:F({From,Event,To,CargoCarrier,NearRadius=NearRadius}) +self.CargoInAir=self.CargoObject:InAir() +local Desc=self.CargoObject:GetDesc() +local MaxSpeed=Desc.speedMaxOffRoad +local TypeName=Desc.typeName +if not self.CargoInAir then +local NearRadius=NearRadius or CargoCarrier:GetBoundingRadius()+5 +if self:IsNear(CargoCarrier:GetPointVec2(),NearRadius)then +self:Load(CargoCarrier,NearRadius,...) +else +if MaxSpeed and MaxSpeed==0 or TypeName and TypeName=="Stinger comm"then +self:Load(CargoCarrier,NearRadius,...) +else +local Speed=90 +local Angle=180 +local Distance=0 +local CargoCarrierPointVec2=CargoCarrier:GetPointVec2() +local CargoCarrierHeading=CargoCarrier:GetHeading() +local CargoDeployHeading=((CargoCarrierHeading+Angle)>=360)and(CargoCarrierHeading+Angle-360)or(CargoCarrierHeading+Angle) +local CargoDeployPointVec2=CargoCarrierPointVec2:Translate(Distance,CargoDeployHeading) +self.CargoObject:OptionAlarmStateGreen() +local Points={} +local PointStartVec2=self.CargoObject:GetPointVec2() +Points[#Points+1]=PointStartVec2:WaypointGround(Speed) +Points[#Points+1]=CargoDeployPointVec2:WaypointGround(Speed) +local TaskRoute=self.CargoObject:TaskRoute(Points) +self.CargoObject:SetTask(TaskRoute,2) +self:__Boarding(-5,CargoCarrier,NearRadius,...) +self.RunCount=0 +end +end +end +end +function CARGO_UNIT:onafterBoarding(From,Event,To,CargoCarrier,NearRadius,...) +self:F({From,Event,To,CargoCarrier:GetName(),NearRadius=NearRadius}) +self:F({IsAlive=self.CargoObject:IsAlive()}) +if CargoCarrier and CargoCarrier:IsAlive()then +if(CargoCarrier:IsAir()and not CargoCarrier:InAir())or true then +local NearRadius=NearRadius or CargoCarrier:GetBoundingRadius(NearRadius)+5 +if self:IsNear(CargoCarrier:GetPointVec2(),NearRadius)then +self:__Load(-1,CargoCarrier,...) +else +if self:IsNear(CargoCarrier:GetPointVec2(),20)then +self:__Boarding(-1,CargoCarrier,NearRadius,...) +self.RunCount=self.RunCount+1 +else +self:__Boarding(-2,CargoCarrier,NearRadius,...) +self.RunCount=self.RunCount+2 +end +if self.RunCount>=40 then +self.RunCount=0 +local Speed=90 +local Angle=180 +local Distance=0 +local CargoCarrierPointVec2=CargoCarrier:GetPointVec2() +local CargoCarrierHeading=CargoCarrier:GetHeading() +local CargoDeployHeading=((CargoCarrierHeading+Angle)>=360)and(CargoCarrierHeading+Angle-360)or(CargoCarrierHeading+Angle) +local CargoDeployPointVec2=CargoCarrierPointVec2:Translate(Distance,CargoDeployHeading) +self.CargoObject:OptionAlarmStateGreen() +local Points={} +local PointStartVec2=self.CargoObject:GetPointVec2() +Points[#Points+1]=PointStartVec2:WaypointGround(Speed,"Off road") +Points[#Points+1]=CargoDeployPointVec2:WaypointGround(Speed,"Off road") +local TaskRoute=self.CargoObject:TaskRoute(Points) +self.CargoObject:SetTask(TaskRoute,0.2) +end +end +else +self.CargoObject:MessageToGroup("Cancelling Boarding... Get back on the ground!",5,CargoCarrier:GetGroup(),self:GetName()) +self:CancelBoarding(CargoCarrier,NearRadius,...) +self.CargoObject:SetCommand(self.CargoObject:CommandStopRoute(true)) +end +else +self:E("Something is wrong") +end +end +function CARGO_UNIT:onenterLoaded(From,Event,To,CargoCarrier) +self:F({From,Event,To,CargoCarrier}) +self.CargoCarrier=CargoCarrier +if self.CargoObject then +self.CargoObject:Destroy(false) +end +end +function CARGO_UNIT:GetTransportationMethod() +if self:IsLoaded()then +return"for unboarding" +else +if self:IsUnLoaded()then +return"for boarding" +else +if self:IsDeployed()then +return"delivered" +end +end +end +return"" +end +end +do +CARGO_SLINGLOAD={ +ClassName="CARGO_SLINGLOAD" +} +function CARGO_SLINGLOAD:New(CargoStatic,Type,Name,LoadRadius,NearRadius) +local self=BASE:Inherit(self,CARGO_REPRESENTABLE:New(CargoStatic,Type,Name,nil,LoadRadius,NearRadius)) +self:F({Type,Name,NearRadius}) +self.CargoObject=CargoStatic +_EVENTDISPATCHER:CreateEventNewCargo(self) +self:HandleEvent(EVENTS.Dead,self.OnEventCargoDead) +self:HandleEvent(EVENTS.Crash,self.OnEventCargoDead) +self:HandleEvent(EVENTS.PlayerLeaveUnit,self.OnEventCargoDead) +self:SetEventPriority(4) +self.NearRadius=NearRadius or 25 +return self +end +function CARGO_SLINGLOAD:OnEventCargoDead(EventData) +local Destroyed=false +if self:IsDestroyed()or self:IsUnLoaded()then +if self.CargoObject:GetName()==EventData.IniUnitName then +if not self.NoDestroy then +Destroyed=true +end +end +end +if Destroyed then +self:I({"Cargo crate destroyed: "..self.CargoObject:GetName()}) +self:Destroyed() +end +end +function CARGO_SLINGLOAD:CanSlingload() +return true +end +function CARGO_SLINGLOAD:CanBoard() +return false +end +function CARGO_SLINGLOAD:CanUnboard() +return false +end +function CARGO_SLINGLOAD:CanLoad() +return false +end +function CARGO_SLINGLOAD:CanUnload() +return false +end +function CARGO_SLINGLOAD:IsInReportRadius(Coordinate) +local Distance=0 +if self:IsUnLoaded()then +Distance=Coordinate:Get2DDistance(self.CargoObject:GetCoordinate()) +if Distance<=self.LoadRadius then +return true +end +end +return false +end +function CARGO_SLINGLOAD:IsInLoadRadius(Coordinate) +local Distance=0 +if self:IsUnLoaded()then +Distance=Coordinate:Get2DDistance(self.CargoObject:GetCoordinate()) +if Distance<=self.NearRadius then +return true +end +end +return false +end +function CARGO_SLINGLOAD:GetCoordinate() +return self.CargoObject:GetCoordinate() +end +function CARGO_SLINGLOAD:IsAlive() +local Alive=true +if self:IsLoaded()then +Alive=Alive==true and self.CargoCarrier:IsAlive() +else +Alive=Alive==true and self.CargoObject:IsAlive() +end +return Alive +end +function CARGO_SLINGLOAD:RouteTo(Coordinate) +end +function CARGO_SLINGLOAD:IsNear(CargoCarrier,NearRadius) +return self:IsNear(CargoCarrier:GetCoordinate(),NearRadius) +end +function CARGO_SLINGLOAD:Respawn() +if self.CargoObject then +self.CargoObject:ReSpawn() +self:__Reset(-0.1) +end +end +function CARGO_SLINGLOAD:onafterReset() +if self.CargoObject then +self:SetDeployed(false) +self:SetStartState("UnLoaded") +self.CargoCarrier=nil +_EVENTDISPATCHER:CreateEventNewCargo(self) +end +end +function CARGO_SLINGLOAD:GetTransportationMethod() +if self:IsLoaded()then +return"for sling loading" +else +if self:IsUnLoaded()then +return"for sling loading" +else +if self:IsDeployed()then +return"delivered" +end +end +end +return"" +end +end +do +CARGO_CRATE={ +ClassName="CARGO_CRATE" +} +function CARGO_CRATE:New(CargoStatic,Type,Name,LoadRadius,NearRadius) +local self=BASE:Inherit(self,CARGO_REPRESENTABLE:New(CargoStatic,Type,Name,nil,LoadRadius,NearRadius)) +self:F({Type,Name,NearRadius}) +self.CargoObject=CargoStatic +_EVENTDISPATCHER:CreateEventNewCargo(self) +self:HandleEvent(EVENTS.Dead,self.OnEventCargoDead) +self:HandleEvent(EVENTS.Crash,self.OnEventCargoDead) +self:HandleEvent(EVENTS.PlayerLeaveUnit,self.OnEventCargoDead) +self:SetEventPriority(4) +self.NearRadius=NearRadius or 25 +return self +end +function CARGO_CRATE:OnEventCargoDead(EventData) +local Destroyed=false +if self:IsDestroyed()or self:IsUnLoaded()or self:IsBoarding()then +if self.CargoObject:GetName()==EventData.IniUnitName then +if not self.NoDestroy then +Destroyed=true +end +end +else +if self:IsLoaded()then +local CarrierName=self.CargoCarrier:GetName() +if CarrierName==EventData.IniDCSUnitName then +MESSAGE:New("Cargo is lost from carrier "..CarrierName,15):ToAll() +Destroyed=true +self.CargoCarrier:ClearCargo() +end +end +end +if Destroyed then +self:I({"Cargo crate destroyed: "..self.CargoObject:GetName()}) +self:Destroyed() +end +end +function CARGO_CRATE:onenterUnLoaded(From,Event,To,ToPointVec2) +local Angle=180 +local Speed=10 +local Distance=10 +if From=="Loaded"then +local StartCoordinate=self.CargoCarrier:GetCoordinate() +local CargoCarrierHeading=self.CargoCarrier:GetHeading() +local CargoDeployHeading=((CargoCarrierHeading+Angle)>=360)and(CargoCarrierHeading+Angle-360)or(CargoCarrierHeading+Angle) +local CargoDeployCoord=StartCoordinate:Translate(Distance,CargoDeployHeading) +ToPointVec2=ToPointVec2 or COORDINATE:NewFromVec2({x=CargoDeployCoord.x,y=CargoDeployCoord.z}) +if self.CargoObject then +self.CargoObject:ReSpawnAt(ToPointVec2,0) +self.CargoCarrier=nil +end +end +if self.OnUnLoadedCallBack then +self.OnUnLoadedCallBack(self,unpack(self.OnUnLoadedParameters)) +self.OnUnLoadedCallBack=nil +end +end +function CARGO_CRATE:onenterLoaded(From,Event,To,CargoCarrier) +self.CargoCarrier=CargoCarrier +if self.CargoObject then +self:T("Destroying") +self.NoDestroy=true +self.CargoObject:Destroy(false) +end +end +function CARGO_CRATE:CanBoard() +return false +end +function CARGO_CRATE:CanUnboard() +return false +end +function CARGO_CRATE:CanSlingload() +return false +end +function CARGO_CRATE:IsInReportRadius(Coordinate) +local Distance=0 +if self:IsUnLoaded()then +Distance=Coordinate:Get2DDistance(self.CargoObject:GetCoordinate()) +if Distance<=self.LoadRadius then +return true +end +end +return false +end +function CARGO_CRATE:IsInLoadRadius(Coordinate) +local Distance=0 +if self:IsUnLoaded()then +Distance=Coordinate:Get2DDistance(self.CargoObject:GetCoordinate()) +if Distance<=self.NearRadius then +return true +end +end +return false +end +function CARGO_CRATE:GetCoordinate() +return self.CargoObject:GetCoordinate() +end +function CARGO_CRATE:IsAlive() +local Alive=true +if self:IsLoaded()then +Alive=Alive==true and self.CargoCarrier:IsAlive() +else +Alive=Alive==true and self.CargoObject:IsAlive() +end +return Alive +end +function CARGO_CRATE:RouteTo(Coordinate) +self:F({Coordinate=Coordinate}) +end +function CARGO_CRATE:IsNear(CargoCarrier,NearRadius) +self:F({NearRadius=NearRadius}) +return self:IsNear(CargoCarrier:GetCoordinate(),NearRadius) +end +function CARGO_CRATE:Respawn() +self:F({"Respawning crate "..self:GetName()}) +if self.CargoObject then +self.CargoObject:ReSpawn() +self:__Reset(-0.1) +end +end +function CARGO_CRATE:onafterReset() +self:F({"Reset crate "..self:GetName()}) +if self.CargoObject then +self:SetDeployed(false) +self:SetStartState("UnLoaded") +self.CargoCarrier=nil +_EVENTDISPATCHER:CreateEventNewCargo(self) +end +end +function CARGO_CRATE:GetTransportationMethod() +if self:IsLoaded()then +return"for unloading" +else +if self:IsUnLoaded()then +return"for loading" +else +if self:IsDeployed()then +return"delivered" +end +end +end +return"" +end +end +do +CARGO_GROUP={ +ClassName="CARGO_GROUP", +} +function CARGO_GROUP:New(CargoGroup,Type,Name,LoadRadius,NearRadius) +local self=BASE:Inherit(self,CARGO_REPORTABLE:New(Type,Name,0,LoadRadius,NearRadius)) +self:F({Type,Name,LoadRadius}) +self.CargoSet=SET_CARGO:New() +self.CargoGroup=CargoGroup +self.Grouped=true +self.CargoUnitTemplate={} +self.NearRadius=NearRadius +self:SetDeployed(false) +local WeightGroup=0 +local VolumeGroup=0 +self.CargoGroup:Destroy() +local GroupName=CargoGroup:GetName() +self.CargoName=Name +self.CargoTemplate=UTILS.DeepCopy(_DATABASE:GetGroupTemplate(GroupName)) +self.CargoTemplate.lateActivation=false +self.GroupTemplate=UTILS.DeepCopy(self.CargoTemplate) +self.GroupTemplate.name=self.CargoName.."#CARGO" +self.GroupTemplate.groupId=nil +self.GroupTemplate.units={} +for UnitID,UnitTemplate in pairs(self.CargoTemplate.units)do +UnitTemplate.name=UnitTemplate.name.."#CARGO" +local CargoUnitName=UnitTemplate.name +self.CargoUnitTemplate[CargoUnitName]=UnitTemplate +self.GroupTemplate.units[#self.GroupTemplate.units+1]=self.CargoUnitTemplate[CargoUnitName] +self.GroupTemplate.units[#self.GroupTemplate.units].unitId=nil +local Unit=UNIT:Register(CargoUnitName) +end +self.CargoGroup=GROUP:NewTemplate(self.GroupTemplate,self.GroupTemplate.CoalitionID,self.GroupTemplate.CategoryID,self.GroupTemplate.CountryID) +self.CargoObject=_DATABASE:Spawn(self.GroupTemplate) +for CargoUnitID,CargoUnit in pairs(self.CargoObject:GetUnits())do +local CargoUnitName=CargoUnit:GetName() +local Cargo=CARGO_UNIT:New(CargoUnit,Type,CargoUnitName,LoadRadius,NearRadius) +self.CargoSet:Add(CargoUnitName,Cargo) +WeightGroup=WeightGroup+Cargo:GetWeight() +end +self:SetWeight(WeightGroup) +self:T({"Weight Cargo",WeightGroup}) +_EVENTDISPATCHER:CreateEventNewCargo(self) +self:HandleEvent(EVENTS.Dead,self.OnEventCargoDead) +self:HandleEvent(EVENTS.Crash,self.OnEventCargoDead) +self:HandleEvent(EVENTS.PlayerLeaveUnit,self.OnEventCargoDead) +self:SetEventPriority(4) +return self +end +function CARGO_GROUP:Respawn() +self:F({"Respawning"}) +for CargoID,CargoData in pairs(self.CargoSet:GetSet())do +local Cargo=CargoData +Cargo:Destroy() +Cargo:SetStartState("UnLoaded") +end +_DATABASE:Spawn(self.GroupTemplate) +for CargoUnitID,CargoUnit in pairs(self.CargoObject:GetUnits())do +local CargoUnitName=CargoUnit:GetName() +local Cargo=CARGO_UNIT:New(CargoUnit,self.Type,CargoUnitName,self.LoadRadius) +self.CargoSet:Add(CargoUnitName,Cargo) +end +self:SetDeployed(false) +self:SetStartState("UnLoaded") +end +function CARGO_GROUP:Ungroup() +if self.Grouped==true then +self.Grouped=false +self.CargoGroup:Destroy() +for CargoUnitName,CargoUnit in pairs(self.CargoSet:GetSet())do +local CargoUnit=CargoUnit +if CargoUnit:IsUnLoaded()then +local GroupTemplate=UTILS.DeepCopy(self.CargoTemplate) +GroupTemplate.name=self.CargoName.."#CARGO#"..CargoUnitName +GroupTemplate.groupId=nil +if CargoUnit:IsUnLoaded()then +GroupTemplate.units={} +GroupTemplate.units[1]=self.CargoUnitTemplate[CargoUnitName] +GroupTemplate.units[#GroupTemplate.units].unitId=nil +GroupTemplate.units[#GroupTemplate.units].x=CargoUnit:GetX() +GroupTemplate.units[#GroupTemplate.units].y=CargoUnit:GetY() +GroupTemplate.units[#GroupTemplate.units].heading=CargoUnit:GetHeading() +end +local CargoGroup=GROUP:NewTemplate(GroupTemplate,GroupTemplate.CoalitionID,GroupTemplate.CategoryID,GroupTemplate.CountryID) +_DATABASE:Spawn(GroupTemplate) +end +end +self.CargoObject=nil +end +end +function CARGO_GROUP:Regroup() +self:F("Regroup") +if self.Grouped==false then +self.Grouped=true +local GroupTemplate=UTILS.DeepCopy(self.CargoTemplate) +GroupTemplate.name=self.CargoName.."#CARGO" +GroupTemplate.groupId=nil +GroupTemplate.units={} +for CargoUnitName,CargoUnit in pairs(self.CargoSet:GetSet())do +local CargoUnit=CargoUnit +self:F({CargoUnit:GetName(),UnLoaded=CargoUnit:IsUnLoaded()}) +if CargoUnit:IsUnLoaded()then +CargoUnit.CargoObject:Destroy() +GroupTemplate.units[#GroupTemplate.units+1]=self.CargoUnitTemplate[CargoUnitName] +GroupTemplate.units[#GroupTemplate.units].unitId=nil +GroupTemplate.units[#GroupTemplate.units].x=CargoUnit:GetX() +GroupTemplate.units[#GroupTemplate.units].y=CargoUnit:GetY() +GroupTemplate.units[#GroupTemplate.units].heading=CargoUnit:GetHeading() +end +end +self.CargoGroup=GROUP:NewTemplate(GroupTemplate,GroupTemplate.CoalitionID,GroupTemplate.CategoryID,GroupTemplate.CountryID) +self:F({"Regroup",GroupTemplate}) +self.CargoObject=_DATABASE:Spawn(GroupTemplate) +end +end +function CARGO_GROUP:OnEventCargoDead(EventData) +self:E(EventData) +local Destroyed=false +if self:IsDestroyed()or self:IsUnLoaded()or self:IsBoarding()or self:IsUnboarding()then +Destroyed=true +for CargoID,CargoData in pairs(self.CargoSet:GetSet())do +local Cargo=CargoData +if Cargo:IsAlive()then +Destroyed=false +else +Cargo:Destroyed() +end +end +else +local CarrierName=self.CargoCarrier:GetName() +if CarrierName==EventData.IniDCSUnitName then +MESSAGE:New("Cargo is lost from carrier "..CarrierName,15):ToAll() +Destroyed=true +self.CargoCarrier:ClearCargo() +end +end +if Destroyed then +self:Destroyed() +self:E({"Cargo group destroyed"}) +end +end +function CARGO_GROUP:onafterBoard(From,Event,To,CargoCarrier,NearRadius,...) +self:F({CargoCarrier.UnitName,From,Event,To,NearRadius=NearRadius}) +NearRadius=NearRadius or self.NearRadius +self.CargoSet:ForEach( +function(Cargo,...) +self:F({"Board Unit",Cargo:GetName(),Cargo:IsDestroyed(),Cargo.CargoObject:IsAlive()}) +local CargoGroup=Cargo.CargoObject +CargoGroup:OptionAlarmStateGreen() +Cargo:__Board(1,CargoCarrier,NearRadius,...) +end,... +) +self:__Boarding(-1,CargoCarrier,NearRadius,...) +end +function CARGO_GROUP:onafterLoad(From,Event,To,CargoCarrier,...) +if From=="UnLoaded"then +for CargoID,Cargo in pairs(self.CargoSet:GetSet())do +if not Cargo:IsDestroyed()then +Cargo:Load(CargoCarrier) +end +end +end +self.CargoCarrier=CargoCarrier +self.CargoCarrier:AddCargo(self) +end +function CARGO_GROUP:onafterBoarding(From,Event,To,CargoCarrier,NearRadius,...) +local Boarded=true +local Cancelled=false +local Dead=true +self.CargoSet:Flush() +for CargoID,Cargo in pairs(self.CargoSet:GetSet())do +if not Cargo:is("Loaded") +and(not Cargo:is("Destroyed"))then +Boarded=false +end +if Cargo:is("UnLoaded")then +Cancelled=true +end +if not Cargo:is("Destroyed")then +Dead=false +end +end +if not Dead then +if not Cancelled then +if not Boarded then +self:__Boarding(-5,CargoCarrier,NearRadius,...) +else +self:F("Group Cargo is loaded") +self:__Load(1,CargoCarrier,...) +end +else +self:__CancelBoarding(1,CargoCarrier,NearRadius,...) +end +else +self:__Destroyed(1,CargoCarrier,NearRadius,...) +end +end +function CARGO_GROUP:onafterUnBoard(From,Event,To,ToPointVec2,NearRadius,...) +self:F({From,Event,To,ToPointVec2,NearRadius}) +NearRadius=NearRadius or 25 +local Timer=1 +if From=="Loaded"then +if self.CargoObject then +self.CargoObject:Destroy() +end +self.CargoSet:ForEach( +function(Cargo,NearRadius) +if not Cargo:IsDestroyed()then +local ToVec=nil +if ToPointVec2==nil then +ToVec=self.CargoCarrier:GetPointVec2():GetRandomPointVec2InRadius(2*NearRadius,NearRadius) +else +ToVec=ToPointVec2 +end +Cargo:__UnBoard(Timer,ToVec,NearRadius) +Timer=Timer+1 +end +end,{NearRadius} +) +self:__UnBoarding(1,ToPointVec2,NearRadius,...) +end +end +function CARGO_GROUP:onafterUnBoarding(From,Event,To,ToPointVec2,NearRadius,...) +local Angle=180 +local Speed=10 +local Distance=5 +if From=="UnBoarding"then +local UnBoarded=true +for CargoID,Cargo in pairs(self.CargoSet:GetSet())do +self:T({Cargo:GetName(),Cargo.current}) +if not Cargo:is("UnLoaded")and not Cargo:IsDestroyed()then +UnBoarded=false +end +end +if UnBoarded then +self:__UnLoad(1,ToPointVec2,...) +else +self:__UnBoarding(1,ToPointVec2,NearRadius,...) +end +return false +end +end +function CARGO_GROUP:onafterUnLoad(From,Event,To,ToPointVec2,...) +if From=="Loaded"then +self.CargoSet:ForEach( +function(Cargo) +local RandomVec2=nil +if ToPointVec2 then +RandomVec2=ToPointVec2:GetRandomPointVec2InRadius(20,10) +end +Cargo:UnBoard(RandomVec2) +end +) +end +self.CargoCarrier:RemoveCargo(self) +self.CargoCarrier=nil +end +function CARGO_GROUP:GetCoordinate() +local Cargo=self:GetFirstAlive() +if Cargo then +return Cargo.CargoObject:GetCoordinate() +end +return nil +end +function CARGO:GetX() +local Cargo=self:GetFirstAlive() +if Cargo then +return Cargo:GetCoordinate().x +end +return nil +end +function CARGO:GetY() +local Cargo=self:GetFirstAlive() +if Cargo then +return Cargo:GetCoordinate().z +end +return nil +end +function CARGO_GROUP:IsAlive() +local Cargo=self:GetFirstAlive() +return Cargo~=nil +end +function CARGO_GROUP:GetFirstAlive() +local CargoFirstAlive=nil +for _,Cargo in pairs(self.CargoSet:GetSet())do +if not Cargo:IsDestroyed()then +CargoFirstAlive=Cargo +break +end +end +return CargoFirstAlive +end +function CARGO_GROUP:GetCount() +return self.CargoSet:Count() +end +function CARGO_GROUP:GetGroup(Cargo) +local Cargo=Cargo or self:GetFirstAlive() +return Cargo.CargoObject:GetGroup() +end +function CARGO_GROUP:RouteTo(Coordinate) +self.CargoSet:ForEach( +function(Cargo) +Cargo.CargoObject:RouteGroundTo(Coordinate,10,"vee",0) +end +) +end +function CARGO_GROUP:IsNear(CargoCarrier,NearRadius) +self:F({NearRadius=NearRadius}) +for _,Cargo in pairs(self.CargoSet:GetSet())do +local Cargo=Cargo +if Cargo:IsAlive()then +if Cargo:IsNear(CargoCarrier:GetCoordinate(),NearRadius)then +self:F("Near") +return true +end +end +end +return nil +end +function CARGO_GROUP:IsInLoadRadius(Coordinate) +local Cargo=self:GetFirstAlive() +if Cargo then +local Distance=0 +local CargoCoordinate +if Cargo:IsLoaded()then +CargoCoordinate=Cargo.CargoCarrier:GetCoordinate() +else +CargoCoordinate=Cargo.CargoObject:GetCoordinate() +end +if CargoCoordinate then +Distance=Coordinate:Get2DDistance(CargoCoordinate) +else +return false +end +self:F({Distance=Distance,LoadRadius=self.LoadRadius}) +if Distance<=self.LoadRadius then +return true +else +return false +end +end +return nil +end +function CARGO_GROUP:IsInReportRadius(Coordinate) +local Cargo=self:GetFirstAlive() +if Cargo then +self:F({Cargo}) +local Distance=0 +if Cargo:IsUnLoaded()then +Distance=Coordinate:Get2DDistance(Cargo.CargoObject:GetCoordinate()) +if Distance<=self.LoadRadius then +return true +end +end +end +return nil +end +function CARGO_GROUP:Flare(FlareColor) +local Cargo=self.CargoSet:GetFirst() +if Cargo then +Cargo:Flare(FlareColor) +end +end +function CARGO_GROUP:Smoke(SmokeColor,Radius) +local Cargo=self.CargoSet:GetFirst() +if Cargo then +Cargo:Smoke(SmokeColor,Radius) +end +end +function CARGO_GROUP:IsInZone(Zone) +local Cargo=self.CargoSet:GetFirst() +if Cargo then +return Cargo:IsInZone(Zone) +end +return nil +end +function CARGO_GROUP:GetTransportationMethod() +if self:IsLoaded()then +return"for unboarding" +else +if self:IsUnLoaded()then +return"for boarding" +else +if self:IsDeployed()then +return"delivered" +end +end +end +return"" +end +end +SCORING={ +ClassName="SCORING", +ClassID=0, +Players={}, +} +local _SCORINGCoalition= +{ +[1]="Red", +[2]="Blue", +} +local _SCORINGCategory= +{ +[Unit.Category.AIRPLANE]="Plane", +[Unit.Category.HELICOPTER]="Helicopter", +[Unit.Category.GROUND_UNIT]="Vehicle", +[Unit.Category.SHIP]="Ship", +[Unit.Category.STRUCTURE]="Structure", +} +function SCORING:New(GameName) +local self=BASE:Inherit(self,BASE:New()) +if GameName then +self.GameName=GameName +else +error("A game name must be given to register the scoring results") +end +self.ScoringObjects={} +self.ScoringZones={} +self:SetMessagesToAll() +self:SetMessagesHit(false) +self:SetMessagesDestroy(true) +self:SetMessagesScore(true) +self:SetMessagesZone(true) +self:SetScaleDestroyScore(10) +self:SetScaleDestroyPenalty(30) +self:SetFratricide(self.ScaleDestroyPenalty*3) +self:SetCoalitionChangePenalty(self.ScaleDestroyPenalty) +self:SetDisplayMessagePrefix() +self:HandleEvent(EVENTS.Dead,self._EventOnDeadOrCrash) +self:HandleEvent(EVENTS.Crash,self._EventOnDeadOrCrash) +self:HandleEvent(EVENTS.Hit,self._EventOnHit) +self:HandleEvent(EVENTS.Birth) +self:HandleEvent(EVENTS.PlayerLeaveUnit) +self.ScoringPlayerScan=BASE:ScheduleOnce(1, +function() +for PlayerName,PlayerUnit in pairs(_DATABASE:GetPlayerUnits())do +self:_AddPlayerFromUnit(PlayerUnit) +self:SetScoringMenu(PlayerUnit:GetGroup()) +end +end +) +self:OpenCSV(GameName) +return self +end +function SCORING:SetDisplayMessagePrefix(DisplayMessagePrefix) +self.DisplayMessagePrefix=DisplayMessagePrefix or"" +return self +end +function SCORING:SetScaleDestroyScore(Scale) +self.ScaleDestroyScore=Scale +return self +end +function SCORING:SetScaleDestroyPenalty(Scale) +self.ScaleDestroyPenalty=Scale +return self +end +function SCORING:AddUnitScore(ScoreUnit,Score) +local UnitName=ScoreUnit:GetName() +self.ScoringObjects[UnitName]=Score +return self +end +function SCORING:RemoveUnitScore(ScoreUnit) +local UnitName=ScoreUnit:GetName() +self.ScoringObjects[UnitName]=nil +return self +end +function SCORING:AddStaticScore(ScoreStatic,Score) +local StaticName=ScoreStatic:GetName() +self.ScoringObjects[StaticName]=Score +return self +end +function SCORING:RemoveStaticScore(ScoreStatic) +local StaticName=ScoreStatic:GetName() +self.ScoringObjects[StaticName]=nil +return self +end +function SCORING:AddScoreGroup(ScoreGroup,Score) +local ScoreUnits=ScoreGroup:GetUnits() +for ScoreUnitID,ScoreUnit in pairs(ScoreUnits)do +local UnitName=ScoreUnit:GetName() +self.ScoringObjects[UnitName]=Score +end +return self +end +function SCORING:AddZoneScore(ScoreZone,Score) +local ZoneName=ScoreZone:GetName() +self.ScoringZones[ZoneName]={} +self.ScoringZones[ZoneName].ScoreZone=ScoreZone +self.ScoringZones[ZoneName].Score=Score +return self +end +function SCORING:RemoveZoneScore(ScoreZone) +local ZoneName=ScoreZone:GetName() +self.ScoringZones[ZoneName]=nil +return self +end +function SCORING:SetMessagesHit(OnOff) +self.MessagesHit=OnOff +return self +end +function SCORING:IfMessagesHit() +return self.MessagesHit +end +function SCORING:SetMessagesDestroy(OnOff) +self.MessagesDestroy=OnOff +return self +end +function SCORING:IfMessagesDestroy() +return self.MessagesDestroy +end +function SCORING:SetMessagesScore(OnOff) +self.MessagesScore=OnOff +return self +end +function SCORING:IfMessagesScore() +return self.MessagesScore +end +function SCORING:SetMessagesZone(OnOff) +self.MessagesZone=OnOff +return self +end +function SCORING:IfMessagesZone() +return self.MessagesZone +end +function SCORING:SetMessagesToAll() +self.MessagesAudience=1 +return self +end +function SCORING:IfMessagesToAll() +return self.MessagesAudience==1 +end +function SCORING:SetMessagesToCoalition() +self.MessagesAudience=2 +return self +end +function SCORING:IfMessagesToCoalition() +return self.MessagesAudience==2 +end +function SCORING:SetFratricide(Fratricide) +self.Fratricide=Fratricide +return self +end +function SCORING:SetCoalitionChangePenalty(CoalitionChangePenalty) +self.CoalitionChangePenalty=CoalitionChangePenalty +return self +end +function SCORING:SetScoringMenu(ScoringGroup) +local Menu=MENU_GROUP:New(ScoringGroup,'Scoring and Statistics') +local ReportGroupSummary=MENU_GROUP_COMMAND:New(ScoringGroup,'Summary report players in group',Menu,SCORING.ReportScoreGroupSummary,self,ScoringGroup) +local ReportGroupDetailed=MENU_GROUP_COMMAND:New(ScoringGroup,'Detailed report players in group',Menu,SCORING.ReportScoreGroupDetailed,self,ScoringGroup) +local ReportToAllSummary=MENU_GROUP_COMMAND:New(ScoringGroup,'Summary report all players',Menu,SCORING.ReportScoreAllSummary,self,ScoringGroup) +self:SetState(ScoringGroup,"ScoringMenu",Menu) +return self +end +function SCORING:_AddPlayerFromUnit(UnitData) +self:F(UnitData) +if UnitData:IsAlive()then +local UnitName=UnitData:GetName() +local PlayerName=UnitData:GetPlayerName() +local UnitDesc=UnitData:GetDesc() +local UnitCategory=UnitDesc.category +local UnitCoalition=UnitData:GetCoalition() +local UnitTypeName=UnitData:GetTypeName() +local UnitThreatLevel,UnitThreatType=UnitData:GetThreatLevel() +self:T({PlayerName,UnitName,UnitCategory,UnitCoalition,UnitTypeName}) +if self.Players[PlayerName]==nil then +self.Players[PlayerName]={} +self.Players[PlayerName].Hit={} +self.Players[PlayerName].Destroy={} +self.Players[PlayerName].Goals={} +self.Players[PlayerName].Mission={} +self.Players[PlayerName].HitPlayers={} +self.Players[PlayerName].Score=0 +self.Players[PlayerName].Penalty=0 +self.Players[PlayerName].PenaltyCoalition=0 +self.Players[PlayerName].PenaltyWarning=0 +end +if not self.Players[PlayerName].UnitCoalition then +self.Players[PlayerName].UnitCoalition=UnitCoalition +else +if self.Players[PlayerName].UnitCoalition~=UnitCoalition then +self.Players[PlayerName].Penalty=self.Players[PlayerName].Penalty+50 +self.Players[PlayerName].PenaltyCoalition=self.Players[PlayerName].PenaltyCoalition+1 +MESSAGE:NewType(self.DisplayMessagePrefix.."Player '"..PlayerName.."' changed coalition from ".._SCORINGCoalition[self.Players[PlayerName].UnitCoalition].." to ".._SCORINGCoalition[UnitCoalition].. +"(changed "..self.Players[PlayerName].PenaltyCoalition.." times the coalition). 50 Penalty points added.", +MESSAGE.Type.Information +):ToAll() +self:ScoreCSV(PlayerName,"","COALITION_PENALTY",1,-50,self.Players[PlayerName].UnitName,_SCORINGCoalition[self.Players[PlayerName].UnitCoalition],_SCORINGCategory[self.Players[PlayerName].UnitCategory],self.Players[PlayerName].UnitType, +UnitName,_SCORINGCoalition[UnitCoalition],_SCORINGCategory[UnitCategory],UnitData:GetTypeName()) +end +end +self.Players[PlayerName].UnitName=UnitName +self.Players[PlayerName].UnitCoalition=UnitCoalition +self.Players[PlayerName].UnitCategory=UnitCategory +self.Players[PlayerName].UnitType=UnitTypeName +self.Players[PlayerName].UNIT=UnitData +self.Players[PlayerName].ThreatLevel=UnitThreatLevel +self.Players[PlayerName].ThreatType=UnitThreatType +end +end +function SCORING:AddGoalScorePlayer(PlayerName,GoalTag,Text,Score) +self:F({PlayerName,PlayerName,GoalTag,Text,Score}) +if PlayerName then +local PlayerData=self.Players[PlayerName] +PlayerData.Goals[GoalTag]=PlayerData.Goals[GoalTag]or{Score=0} +PlayerData.Goals[GoalTag].Score=PlayerData.Goals[GoalTag].Score+Score +PlayerData.Score=PlayerData.Score+Score +MESSAGE:NewType(self.DisplayMessagePrefix..Text,MESSAGE.Type.Information):ToAll() +self:ScoreCSV(PlayerName,"","GOAL_"..string.upper(GoalTag),1,Score,nil) +end +end +function SCORING:AddGoalScore(PlayerUnit,GoalTag,Text,Score) +local PlayerName=PlayerUnit:GetPlayerName() +self:F({PlayerUnit.UnitName,PlayerName,GoalTag,Text,Score}) +if PlayerName then +local PlayerData=self.Players[PlayerName] +PlayerData.Goals[GoalTag]=PlayerData.Goals[GoalTag]or{Score=0} +PlayerData.Goals[GoalTag].Score=PlayerData.Goals[GoalTag].Score+Score +PlayerData.Score=PlayerData.Score+Score +MESSAGE:NewType(self.DisplayMessagePrefix..Text,MESSAGE.Type.Information):ToAll() +self:ScoreCSV(PlayerName,"","GOAL_"..string.upper(GoalTag),1,Score,PlayerUnit:GetName()) +end +end +function SCORING:_AddMissionTaskScore(Mission,PlayerUnit,Text,Score) +local PlayerName=PlayerUnit:GetPlayerName() +local MissionName=Mission:GetName() +self:F({Mission:GetName(),PlayerUnit.UnitName,PlayerName,Text,Score}) +if PlayerName then +local PlayerData=self.Players[PlayerName] +if not PlayerData.Mission[MissionName]then +PlayerData.Mission[MissionName]={} +PlayerData.Mission[MissionName].ScoreTask=0 +PlayerData.Mission[MissionName].ScoreMission=0 +end +self:T(PlayerName) +self:T(PlayerData.Mission[MissionName]) +PlayerData.Score=self.Players[PlayerName].Score+Score +PlayerData.Mission[MissionName].ScoreTask=self.Players[PlayerName].Mission[MissionName].ScoreTask+Score +MESSAGE:NewType(self.DisplayMessagePrefix..Mission:GetText().." : "..Text.." Score: "..Score,MESSAGE.Type.Information):ToAll() +self:ScoreCSV(PlayerName,"","TASK_"..MissionName:gsub(' ','_'),1,Score,PlayerUnit:GetName()) +end +end +function SCORING:_AddMissionGoalScore(Mission,PlayerName,Text,Score) +local MissionName=Mission:GetName() +self:F({Mission:GetName(),PlayerName,Text,Score}) +if PlayerName then +local PlayerData=self.Players[PlayerName] +if not PlayerData.Mission[MissionName]then +PlayerData.Mission[MissionName]={} +PlayerData.Mission[MissionName].ScoreTask=0 +PlayerData.Mission[MissionName].ScoreMission=0 +end +self:T(PlayerName) +self:T(PlayerData.Mission[MissionName]) +PlayerData.Score=self.Players[PlayerName].Score+Score +PlayerData.Mission[MissionName].ScoreTask=self.Players[PlayerName].Mission[MissionName].ScoreTask+Score +MESSAGE:NewType(string.format("%s%s: %s! Player %s receives %d score!",self.DisplayMessagePrefix,Mission:GetText(),Text,PlayerName,Score),MESSAGE.Type.Information):ToAll() +self:ScoreCSV(PlayerName,"","TASK_"..MissionName:gsub(' ','_'),1,Score) +end +end +function SCORING:_AddMissionScore(Mission,Text,Score) +local MissionName=Mission:GetName() +self:F({Mission,Text,Score}) +self:F(self.Players) +for PlayerName,PlayerData in pairs(self.Players)do +self:F(PlayerData) +if PlayerData.Mission[MissionName]then +PlayerData.Score=PlayerData.Score+Score +PlayerData.Mission[MissionName].ScoreMission=PlayerData.Mission[MissionName].ScoreMission+Score +MESSAGE:NewType(self.DisplayMessagePrefix.."Player '"..PlayerName.."' has "..Text.." in "..Mission:GetText()..". ".. +Score.." mission score!", +MESSAGE.Type.Information):ToAll() +self:ScoreCSV(PlayerName,"","MISSION_"..MissionName:gsub(' ','_'),1,Score) +end +end +end +function SCORING:OnEventBirth(Event) +if Event.IniUnit then +if Event.IniObjectCategory==1 then +local PlayerName=Event.IniUnit:GetPlayerName() +if PlayerName then +self:_AddPlayerFromUnit(Event.IniUnit) +self:SetScoringMenu(Event.IniGroup) +end +end +end +end +function SCORING:OnEventPlayerLeaveUnit(Event) +if Event.IniUnit then +local Menu=self:GetState(Event.IniUnit:GetGroup(),"ScoringMenu") +if Menu then +end +end +end +function SCORING:_EventOnHit(Event) +self:F({Event}) +local InitUnit=nil +local InitUNIT=nil +local InitUnitName="" +local InitGroup=nil +local InitGroupName="" +local InitPlayerName=nil +local InitCoalition=nil +local InitCategory=nil +local InitType=nil +local InitUnitCoalition=nil +local InitUnitCategory=nil +local InitUnitType=nil +local TargetUnit=nil +local TargetUNIT=nil +local TargetUnitName="" +local TargetGroup=nil +local TargetGroupName="" +local TargetPlayerName=nil +local TargetCoalition=nil +local TargetCategory=nil +local TargetType=nil +local TargetUnitCoalition=nil +local TargetUnitCategory=nil +local TargetUnitType=nil +if Event.IniDCSUnit then +InitUnit=Event.IniDCSUnit +InitUNIT=Event.IniUnit +InitUnitName=Event.IniDCSUnitName +InitGroup=Event.IniDCSGroup +InitGroupName=Event.IniDCSGroupName +InitPlayerName=Event.IniPlayerName +InitCoalition=Event.IniCoalition +InitCategory=Event.IniCategory +InitType=Event.IniTypeName +InitUnitCoalition=_SCORINGCoalition[InitCoalition] +InitUnitCategory=_SCORINGCategory[InitCategory] +InitUnitType=InitType +self:T({InitUnitName,InitGroupName,InitPlayerName,InitCoalition,InitCategory,InitType,InitUnitCoalition,InitUnitCategory,InitUnitType}) +end +if Event.TgtDCSUnit then +TargetUnit=Event.TgtDCSUnit +TargetUNIT=Event.TgtUnit +TargetUnitName=Event.TgtDCSUnitName +TargetGroup=Event.TgtDCSGroup +TargetGroupName=Event.TgtDCSGroupName +TargetPlayerName=Event.TgtPlayerName +TargetCoalition=Event.TgtCoalition +TargetCategory=Event.TgtCategory +TargetType=Event.TgtTypeName +TargetUnitCoalition=_SCORINGCoalition[TargetCoalition] +TargetUnitCategory=_SCORINGCategory[TargetCategory] +TargetUnitType=TargetType +self:T({TargetUnitName,TargetGroupName,TargetPlayerName,TargetCoalition,TargetCategory,TargetType,TargetUnitCoalition,TargetUnitCategory,TargetUnitType}) +end +if InitPlayerName~=nil then +self:_AddPlayerFromUnit(InitUNIT) +if self.Players[InitPlayerName]then +if TargetPlayerName~=nil then +self:_AddPlayerFromUnit(TargetUNIT) +end +self:T("Hitting Something") +if TargetCategory then +local Player=self.Players[InitPlayerName] +Player.Hit[TargetCategory]=Player.Hit[TargetCategory]or{} +Player.Hit[TargetCategory][TargetUnitName]=Player.Hit[TargetCategory][TargetUnitName]or{} +local PlayerHit=Player.Hit[TargetCategory][TargetUnitName] +PlayerHit.Score=PlayerHit.Score or 0 +PlayerHit.Penalty=PlayerHit.Penalty or 0 +PlayerHit.ScoreHit=PlayerHit.ScoreHit or 0 +PlayerHit.PenaltyHit=PlayerHit.PenaltyHit or 0 +PlayerHit.TimeStamp=PlayerHit.TimeStamp or 0 +PlayerHit.UNIT=PlayerHit.UNIT or TargetUNIT +PlayerHit.ThreatLevel,PlayerHit.ThreatType=PlayerHit.UNIT:GetThreatLevel() +if timer.getTime()-PlayerHit.TimeStamp>1 then +PlayerHit.TimeStamp=timer.getTime() +if TargetPlayerName~=nil then +Player.HitPlayers[TargetPlayerName]=true +end +local Score=0 +if InitCoalition then +if InitCoalition==TargetCoalition then +Player.Penalty=Player.Penalty+10 +PlayerHit.Penalty=PlayerHit.Penalty+10 +PlayerHit.PenaltyHit=PlayerHit.PenaltyHit+1 +if TargetPlayerName~=nil then +MESSAGE +:NewType(self.DisplayMessagePrefix.."Player '"..InitPlayerName.."' hit friendly player '"..TargetPlayerName.."' ".. +TargetUnitCategory.." ( "..TargetType.." ) "..PlayerHit.PenaltyHit.." times. ".. +"Penalty: -"..PlayerHit.Penalty..". Score Total:"..Player.Score-Player.Penalty, +MESSAGE.Type.Update +) +:ToAllIf(self:IfMessagesHit()and self:IfMessagesToAll()) +:ToCoalitionIf(InitCoalition,self:IfMessagesHit()and self:IfMessagesToCoalition()) +else +MESSAGE +:NewType(self.DisplayMessagePrefix.."Player '"..InitPlayerName.."' hit friendly target ".. +TargetUnitCategory.." ( "..TargetType.." ) "..PlayerHit.PenaltyHit.." times. ".. +"Penalty: -"..PlayerHit.Penalty..". Score Total:"..Player.Score-Player.Penalty, +MESSAGE.Type.Update +) +:ToAllIf(self:IfMessagesHit()and self:IfMessagesToAll()) +:ToCoalitionIf(InitCoalition,self:IfMessagesHit()and self:IfMessagesToCoalition()) +end +self:ScoreCSV(InitPlayerName,TargetPlayerName,"HIT_PENALTY",1,-10,InitUnitName,InitUnitCoalition,InitUnitCategory,InitUnitType,TargetUnitName,TargetUnitCoalition,TargetUnitCategory,TargetUnitType) +else +Player.Score=Player.Score+1 +PlayerHit.Score=PlayerHit.Score+1 +PlayerHit.ScoreHit=PlayerHit.ScoreHit+1 +if TargetPlayerName~=nil then +MESSAGE +:NewType(self.DisplayMessagePrefix.."Player '"..InitPlayerName.."' hit enemy player '"..TargetPlayerName.."' ".. +TargetUnitCategory.." ( "..TargetType.." ) "..PlayerHit.ScoreHit.." times. ".. +"Score: "..PlayerHit.Score..". Score Total:"..Player.Score-Player.Penalty, +MESSAGE.Type.Update +) +:ToAllIf(self:IfMessagesHit()and self:IfMessagesToAll()) +:ToCoalitionIf(InitCoalition,self:IfMessagesHit()and self:IfMessagesToCoalition()) +else +MESSAGE +:NewType(self.DisplayMessagePrefix.."Player '"..InitPlayerName.."' hit enemy target ".. +TargetUnitCategory.." ( "..TargetType.." ) "..PlayerHit.ScoreHit.." times. ".. +"Score: "..PlayerHit.Score..". Score Total:"..Player.Score-Player.Penalty, +MESSAGE.Type.Update +) +:ToAllIf(self:IfMessagesHit()and self:IfMessagesToAll()) +:ToCoalitionIf(InitCoalition,self:IfMessagesHit()and self:IfMessagesToCoalition()) +end +self:ScoreCSV(InitPlayerName,TargetPlayerName,"HIT_SCORE",1,1,InitUnitName,InitUnitCoalition,InitUnitCategory,InitUnitType,TargetUnitName,TargetUnitCoalition,TargetUnitCategory,TargetUnitType) +end +else +MESSAGE +:NewType(self.DisplayMessagePrefix.."Player '"..InitPlayerName.."' hit scenery object.", +MESSAGE.Type.Update +) +:ToAllIf(self:IfMessagesHit()and self:IfMessagesToAll()) +:ToCoalitionIf(InitCoalition,self:IfMessagesHit()and self:IfMessagesToCoalition()) +self:ScoreCSV(InitPlayerName,"","HIT_SCORE",1,0,InitUnitName,InitUnitCoalition,InitUnitCategory,InitUnitType,TargetUnitName,"","Scenery",TargetUnitType) +end +end +end +end +elseif InitPlayerName==nil then +end +if Event.WeaponPlayerName~=nil then +self:_AddPlayerFromUnit(Event.WeaponUNIT) +if self.Players[Event.WeaponPlayerName]then +if TargetPlayerName~=nil then +self:_AddPlayerFromUnit(TargetUNIT) +end +self:T("Hitting Scenery") +if TargetCategory then +local Player=self.Players[Event.WeaponPlayerName] +Player.Hit[TargetCategory]=Player.Hit[TargetCategory]or{} +Player.Hit[TargetCategory][TargetUnitName]=Player.Hit[TargetCategory][TargetUnitName]or{} +local PlayerHit=Player.Hit[TargetCategory][TargetUnitName] +PlayerHit.Score=PlayerHit.Score or 0 +PlayerHit.Penalty=PlayerHit.Penalty or 0 +PlayerHit.ScoreHit=PlayerHit.ScoreHit or 0 +PlayerHit.PenaltyHit=PlayerHit.PenaltyHit or 0 +PlayerHit.TimeStamp=PlayerHit.TimeStamp or 0 +PlayerHit.UNIT=PlayerHit.UNIT or TargetUNIT +PlayerHit.ThreatLevel,PlayerHit.ThreatType=PlayerHit.UNIT:GetThreatLevel() +if timer.getTime()-PlayerHit.TimeStamp>1 then +PlayerHit.TimeStamp=timer.getTime() +local Score=0 +if InitCoalition then +if InitCoalition==TargetCoalition then +Player.Penalty=Player.Penalty+10 +PlayerHit.Penalty=PlayerHit.Penalty+10 +PlayerHit.PenaltyHit=PlayerHit.PenaltyHit+1 +MESSAGE +:NewType(self.DisplayMessagePrefix.."Player '"..Event.WeaponPlayerName.."' hit friendly target ".. +TargetUnitCategory.." ( "..TargetType.." ) ".. +"Penalty: -"..PlayerHit.Penalty.." = "..Player.Score-Player.Penalty, +MESSAGE.Type.Update +) +:ToAllIf(self:IfMessagesHit()and self:IfMessagesToAll()) +:ToCoalitionIf(Event.WeaponCoalition,self:IfMessagesHit()and self:IfMessagesToCoalition()) +self:ScoreCSV(Event.WeaponPlayerName,TargetPlayerName,"HIT_PENALTY",1,-10,Event.WeaponName,Event.WeaponCoalition,Event.WeaponCategory,Event.WeaponTypeName,TargetUnitName,TargetUnitCoalition,TargetUnitCategory,TargetUnitType) +else +Player.Score=Player.Score+1 +PlayerHit.Score=PlayerHit.Score+1 +PlayerHit.ScoreHit=PlayerHit.ScoreHit+1 +MESSAGE +:NewType(self.DisplayMessagePrefix.."Player '"..Event.WeaponPlayerName.."' hit enemy target ".. +TargetUnitCategory.." ( "..TargetType.." ) ".. +"Score: +"..PlayerHit.Score.." = "..Player.Score-Player.Penalty, +MESSAGE.Type.Update +) +:ToAllIf(self:IfMessagesHit()and self:IfMessagesToAll()) +:ToCoalitionIf(Event.WeaponCoalition,self:IfMessagesHit()and self:IfMessagesToCoalition()) +self:ScoreCSV(Event.WeaponPlayerName,TargetPlayerName,"HIT_SCORE",1,1,Event.WeaponName,Event.WeaponCoalition,Event.WeaponCategory,Event.WeaponTypeName,TargetUnitName,TargetUnitCoalition,TargetUnitCategory,TargetUnitType) +end +else +MESSAGE +:NewType(self.DisplayMessagePrefix.."Player '"..Event.WeaponPlayerName.."' hit scenery object.", +MESSAGE.Type.Update +) +:ToAllIf(self:IfMessagesHit()and self:IfMessagesToAll()) +:ToCoalitionIf(InitCoalition,self:IfMessagesHit()and self:IfMessagesToCoalition()) +self:ScoreCSV(Event.WeaponPlayerName,"","HIT_SCORE",1,0,Event.WeaponName,Event.WeaponCoalition,Event.WeaponCategory,Event.WeaponTypeName,TargetUnitName,"","Scenery",TargetUnitType) +end +end +end +end +end +end +function SCORING:_EventOnDeadOrCrash(Event) +self:F({Event}) +local TargetUnit=nil +local TargetGroup=nil +local TargetUnitName="" +local TargetGroupName="" +local TargetPlayerName="" +local TargetCoalition=nil +local TargetCategory=nil +local TargetType=nil +local TargetUnitCoalition=nil +local TargetUnitCategory=nil +local TargetUnitType=nil +if Event.IniDCSUnit then +TargetUnit=Event.IniUnit +TargetUnitName=Event.IniDCSUnitName +TargetGroup=Event.IniDCSGroup +TargetGroupName=Event.IniDCSGroupName +TargetPlayerName=Event.IniPlayerName +TargetCoalition=Event.IniCoalition +TargetCategory=Event.IniCategory +TargetType=Event.IniTypeName +TargetUnitCoalition=_SCORINGCoalition[TargetCoalition] +TargetUnitCategory=_SCORINGCategory[TargetCategory] +TargetUnitType=TargetType +self:T({TargetUnitName,TargetGroupName,TargetPlayerName,TargetCoalition,TargetCategory,TargetType}) +end +for PlayerName,Player in pairs(self.Players)do +if Player then +self:T("Something got destroyed") +local InitUnitName=Player.UnitName +local InitUnitType=Player.UnitType +local InitCoalition=Player.UnitCoalition +local InitCategory=Player.UnitCategory +local InitUnitCoalition=_SCORINGCoalition[InitCoalition] +local InitUnitCategory=_SCORINGCategory[InitCategory] +self:T({InitUnitName,InitUnitType,InitUnitCoalition,InitCoalition,InitUnitCategory,InitCategory}) +local Destroyed=false +if Player and Player.Hit and Player.Hit[TargetCategory]and Player.Hit[TargetCategory][TargetUnitName]and Player.Hit[TargetCategory][TargetUnitName].TimeStamp~=0 then +local TargetThreatLevel=Player.Hit[TargetCategory][TargetUnitName].ThreatLevel +local TargetThreatType=Player.Hit[TargetCategory][TargetUnitName].ThreatType +Player.Destroy[TargetCategory]=Player.Destroy[TargetCategory]or{} +Player.Destroy[TargetCategory][TargetType]=Player.Destroy[TargetCategory][TargetType]or{} +local TargetDestroy=Player.Destroy[TargetCategory][TargetType] +TargetDestroy.Score=TargetDestroy.Score or 0 +TargetDestroy.ScoreDestroy=TargetDestroy.ScoreDestroy or 0 +TargetDestroy.Penalty=TargetDestroy.Penalty or 0 +TargetDestroy.PenaltyDestroy=TargetDestroy.PenaltyDestroy or 0 +if TargetCoalition then +if InitCoalition==TargetCoalition then +local ThreatLevelTarget=TargetThreatLevel +local ThreatTypeTarget=TargetThreatType +local ThreatLevelPlayer=Player.ThreatLevel/10+1 +local ThreatPenalty=math.ceil((ThreatLevelTarget/ThreatLevelPlayer)*self.ScaleDestroyPenalty/10) +self:F({ThreatLevel=ThreatPenalty,ThreatLevelTarget=ThreatLevelTarget,ThreatTypeTarget=ThreatTypeTarget,ThreatLevelPlayer=ThreatLevelPlayer}) +Player.Penalty=Player.Penalty+ThreatPenalty +TargetDestroy.Penalty=TargetDestroy.Penalty+ThreatPenalty +TargetDestroy.PenaltyDestroy=TargetDestroy.PenaltyDestroy+1 +if Player.HitPlayers[TargetPlayerName]then +MESSAGE +:NewType(self.DisplayMessagePrefix.."Player '"..PlayerName.."' destroyed friendly player '"..TargetPlayerName.."' ".. +TargetUnitCategory.." ( "..ThreatTypeTarget.." ) ".. +"Penalty: -"..TargetDestroy.Penalty.." = "..Player.Score-Player.Penalty, +MESSAGE.Type.Information +) +:ToAllIf(self:IfMessagesDestroy()and self:IfMessagesToAll()) +:ToCoalitionIf(InitCoalition,self:IfMessagesDestroy()and self:IfMessagesToCoalition()) +else +MESSAGE +:NewType(self.DisplayMessagePrefix.."Player '"..PlayerName.."' destroyed friendly target ".. +TargetUnitCategory.." ( "..ThreatTypeTarget.." ) ".. +"Penalty: -"..TargetDestroy.Penalty.." = "..Player.Score-Player.Penalty, +MESSAGE.Type.Information +) +:ToAllIf(self:IfMessagesDestroy()and self:IfMessagesToAll()) +:ToCoalitionIf(InitCoalition,self:IfMessagesDestroy()and self:IfMessagesToCoalition()) +end +Destroyed=true +self:ScoreCSV(PlayerName,TargetPlayerName,"DESTROY_PENALTY",1,ThreatPenalty,InitUnitName,InitUnitCoalition,InitUnitCategory,InitUnitType,TargetUnitName,TargetUnitCoalition,TargetUnitCategory,TargetUnitType) +else +local ThreatLevelTarget=TargetThreatLevel +local ThreatTypeTarget=TargetThreatType +local ThreatLevelPlayer=Player.ThreatLevel/10+1 +local ThreatScore=math.ceil((ThreatLevelTarget/ThreatLevelPlayer)*self.ScaleDestroyScore/10) +self:F({ThreatLevel=ThreatScore,ThreatLevelTarget=ThreatLevelTarget,ThreatTypeTarget=ThreatTypeTarget,ThreatLevelPlayer=ThreatLevelPlayer}) +Player.Score=Player.Score+ThreatScore +TargetDestroy.Score=TargetDestroy.Score+ThreatScore +TargetDestroy.ScoreDestroy=TargetDestroy.ScoreDestroy+1 +if Player.HitPlayers[TargetPlayerName]then +MESSAGE +:NewType(self.DisplayMessagePrefix.."Player '"..PlayerName.."' destroyed enemy player '"..TargetPlayerName.."' ".. +TargetUnitCategory.." ( "..ThreatTypeTarget.." ) ".. +"Score: +"..TargetDestroy.Score.." = "..Player.Score-Player.Penalty, +MESSAGE.Type.Information +) +:ToAllIf(self:IfMessagesDestroy()and self:IfMessagesToAll()) +:ToCoalitionIf(InitCoalition,self:IfMessagesDestroy()and self:IfMessagesToCoalition()) +else +MESSAGE +:NewType(self.DisplayMessagePrefix.."Player '"..PlayerName.."' destroyed enemy ".. +TargetUnitCategory.." ( "..ThreatTypeTarget.." ) ".. +"Score: +"..TargetDestroy.Score.." = "..Player.Score-Player.Penalty, +MESSAGE.Type.Information +) +:ToAllIf(self:IfMessagesDestroy()and self:IfMessagesToAll()) +:ToCoalitionIf(InitCoalition,self:IfMessagesDestroy()and self:IfMessagesToCoalition()) +end +Destroyed=true +self:ScoreCSV(PlayerName,TargetPlayerName,"DESTROY_SCORE",1,ThreatScore,InitUnitName,InitUnitCoalition,InitUnitCategory,InitUnitType,TargetUnitName,TargetUnitCoalition,TargetUnitCategory,TargetUnitType) +local UnitName=TargetUnit:GetName() +local Score=self.ScoringObjects[UnitName] +if Score then +Player.Score=Player.Score+Score +TargetDestroy.Score=TargetDestroy.Score+Score +MESSAGE +:NewType(self.DisplayMessagePrefix.."Special target '"..TargetUnitCategory.." ( "..ThreatTypeTarget.." ) ".." destroyed! ".. +"Player '"..PlayerName.."' receives an extra "..Score.." points! Total: "..Player.Score-Player.Penalty, +MESSAGE.Type.Information +) +:ToAllIf(self:IfMessagesScore()and self:IfMessagesToAll()) +:ToCoalitionIf(InitCoalition,self:IfMessagesScore()and self:IfMessagesToCoalition()) +self:ScoreCSV(PlayerName,TargetPlayerName,"DESTROY_SCORE",1,Score,InitUnitName,InitUnitCoalition,InitUnitCategory,InitUnitType,TargetUnitName,TargetUnitCoalition,TargetUnitCategory,TargetUnitType) +Destroyed=true +end +for ZoneName,ScoreZoneData in pairs(self.ScoringZones)do +self:F({ScoringZone=ScoreZoneData}) +local ScoreZone=ScoreZoneData.ScoreZone +local Score=ScoreZoneData.Score +if ScoreZone:IsVec2InZone(TargetUnit:GetVec2())then +Player.Score=Player.Score+Score +TargetDestroy.Score=TargetDestroy.Score+Score +MESSAGE +:NewType(self.DisplayMessagePrefix.."Target destroyed in zone '"..ScoreZone:GetName().."'.".. +"Player '"..PlayerName.."' receives an extra "..Score.." points! ".. +"Total: "..Player.Score-Player.Penalty, +MESSAGE.Type.Information) +:ToAllIf(self:IfMessagesZone()and self:IfMessagesToAll()) +:ToCoalitionIf(InitCoalition,self:IfMessagesZone()and self:IfMessagesToCoalition()) +self:ScoreCSV(PlayerName,TargetPlayerName,"DESTROY_SCORE",1,Score,InitUnitName,InitUnitCoalition,InitUnitCategory,InitUnitType,TargetUnitName,TargetUnitCoalition,TargetUnitCategory,TargetUnitType) +Destroyed=true +end +end +end +else +for ZoneName,ScoreZoneData in pairs(self.ScoringZones)do +self:F({ScoringZone=ScoreZoneData}) +local ScoreZone=ScoreZoneData.ScoreZone +local Score=ScoreZoneData.Score +if ScoreZone:IsVec2InZone(TargetUnit:GetVec2())then +Player.Score=Player.Score+Score +TargetDestroy.Score=TargetDestroy.Score+Score +MESSAGE +:NewType(self.DisplayMessagePrefix.."Scenery destroyed in zone '"..ScoreZone:GetName().."'.".. +"Player '"..PlayerName.."' receives an extra "..Score.." points! ".. +"Total: "..Player.Score-Player.Penalty, +MESSAGE.Type.Information +) +:ToAllIf(self:IfMessagesZone()and self:IfMessagesToAll()) +:ToCoalitionIf(InitCoalition,self:IfMessagesZone()and self:IfMessagesToCoalition()) +Destroyed=true +self:ScoreCSV(PlayerName,"","DESTROY_SCORE",1,Score,InitUnitName,InitUnitCoalition,InitUnitCategory,InitUnitType,TargetUnitName,"","Scenery",TargetUnitType) +end +end +end +if Destroyed then +Player.Hit[TargetCategory][TargetUnitName].TimeStamp=0 +end +end +end +end +end +function SCORING:ReportDetailedPlayerHits(PlayerName) +local ScoreMessage="" +local PlayerScore=0 +local PlayerPenalty=0 +local PlayerData=self.Players[PlayerName] +if PlayerData then +self:T("Score Player: "..PlayerName) +local InitUnitCoalition=_SCORINGCoalition[PlayerData.UnitCoalition] +local InitUnitCategory=_SCORINGCategory[PlayerData.UnitCategory] +local InitUnitType=PlayerData.UnitType +local InitUnitName=PlayerData.UnitName +local ScoreMessageHits="" +for CategoryID,CategoryName in pairs(_SCORINGCategory)do +self:T(CategoryName) +if PlayerData.Hit[CategoryID]then +self:T("Hit scores exist for player "..PlayerName) +local Score=0 +local ScoreHit=0 +local Penalty=0 +local PenaltyHit=0 +for UnitName,UnitData in pairs(PlayerData.Hit[CategoryID])do +Score=Score+UnitData.Score +ScoreHit=ScoreHit+UnitData.ScoreHit +Penalty=Penalty+UnitData.Penalty +PenaltyHit=UnitData.PenaltyHit +end +local ScoreMessageHit=string.format("%s:%d ",CategoryName,Score-Penalty) +self:T(ScoreMessageHit) +ScoreMessageHits=ScoreMessageHits..ScoreMessageHit +PlayerScore=PlayerScore+Score +PlayerPenalty=PlayerPenalty+Penalty +else +end +end +if ScoreMessageHits~=""then +ScoreMessage="Hits: "..ScoreMessageHits +end +end +return ScoreMessage,PlayerScore,PlayerPenalty +end +function SCORING:ReportDetailedPlayerDestroys(PlayerName) +local ScoreMessage="" +local PlayerScore=0 +local PlayerPenalty=0 +local PlayerData=self.Players[PlayerName] +if PlayerData then +self:T("Score Player: "..PlayerName) +local InitUnitCoalition=_SCORINGCoalition[PlayerData.UnitCoalition] +local InitUnitCategory=_SCORINGCategory[PlayerData.UnitCategory] +local InitUnitType=PlayerData.UnitType +local InitUnitName=PlayerData.UnitName +local ScoreMessageDestroys="" +for CategoryID,CategoryName in pairs(_SCORINGCategory)do +if PlayerData.Destroy[CategoryID]then +self:T("Destroy scores exist for player "..PlayerName) +local Score=0 +local ScoreDestroy=0 +local Penalty=0 +local PenaltyDestroy=0 +for UnitName,UnitData in pairs(PlayerData.Destroy[CategoryID])do +self:F({UnitData=UnitData}) +if UnitData~={}then +Score=Score+UnitData.Score +ScoreDestroy=ScoreDestroy+UnitData.ScoreDestroy +Penalty=Penalty+UnitData.Penalty +PenaltyDestroy=PenaltyDestroy+UnitData.PenaltyDestroy +end +end +local ScoreMessageDestroy=string.format(" %s:%d ",CategoryName,Score-Penalty) +self:T(ScoreMessageDestroy) +ScoreMessageDestroys=ScoreMessageDestroys..ScoreMessageDestroy +PlayerScore=PlayerScore+Score +PlayerPenalty=PlayerPenalty+Penalty +else +end +end +if ScoreMessageDestroys~=""then +ScoreMessage="Destroys: "..ScoreMessageDestroys +end +end +return ScoreMessage,PlayerScore,PlayerPenalty +end +function SCORING:ReportDetailedPlayerCoalitionChanges(PlayerName) +local ScoreMessage="" +local PlayerScore=0 +local PlayerPenalty=0 +local PlayerData=self.Players[PlayerName] +if PlayerData then +self:T("Score Player: "..PlayerName) +local InitUnitCoalition=_SCORINGCoalition[PlayerData.UnitCoalition] +local InitUnitCategory=_SCORINGCategory[PlayerData.UnitCategory] +local InitUnitType=PlayerData.UnitType +local InitUnitName=PlayerData.UnitName +local ScoreMessageCoalitionChangePenalties="" +if PlayerData.PenaltyCoalition~=0 then +ScoreMessageCoalitionChangePenalties=ScoreMessageCoalitionChangePenalties..string.format(" -%d (%d changed)",PlayerData.Penalty,PlayerData.PenaltyCoalition) +PlayerPenalty=PlayerPenalty+PlayerData.Penalty +end +if ScoreMessageCoalitionChangePenalties~=""then +ScoreMessage=ScoreMessage.."Coalition Penalties: "..ScoreMessageCoalitionChangePenalties +end +end +return ScoreMessage,PlayerScore,PlayerPenalty +end +function SCORING:ReportDetailedPlayerGoals(PlayerName) +local ScoreMessage="" +local PlayerScore=0 +local PlayerPenalty=0 +local PlayerData=self.Players[PlayerName] +if PlayerData then +self:T("Score Player: "..PlayerName) +local InitUnitCoalition=_SCORINGCoalition[PlayerData.UnitCoalition] +local InitUnitCategory=_SCORINGCategory[PlayerData.UnitCategory] +local InitUnitType=PlayerData.UnitType +local InitUnitName=PlayerData.UnitName +local ScoreMessageGoal="" +local ScoreGoal=0 +local ScoreTask=0 +for GoalName,GoalData in pairs(PlayerData.Goals)do +ScoreGoal=ScoreGoal+GoalData.Score +ScoreMessageGoal=ScoreMessageGoal.."'"..GoalName.."':"..GoalData.Score.."; " +end +PlayerScore=PlayerScore+ScoreGoal +if ScoreMessageGoal~=""then +ScoreMessage="Goals: "..ScoreMessageGoal +end +end +return ScoreMessage,PlayerScore,PlayerPenalty +end +function SCORING:ReportDetailedPlayerMissions(PlayerName) +local ScoreMessage="" +local PlayerScore=0 +local PlayerPenalty=0 +local PlayerData=self.Players[PlayerName] +if PlayerData then +self:T("Score Player: "..PlayerName) +local InitUnitCoalition=_SCORINGCoalition[PlayerData.UnitCoalition] +local InitUnitCategory=_SCORINGCategory[PlayerData.UnitCategory] +local InitUnitType=PlayerData.UnitType +local InitUnitName=PlayerData.UnitName +local ScoreMessageMission="" +local ScoreMission=0 +local ScoreTask=0 +for MissionName,MissionData in pairs(PlayerData.Mission)do +ScoreMission=ScoreMission+MissionData.ScoreMission +ScoreTask=ScoreTask+MissionData.ScoreTask +ScoreMessageMission=ScoreMessageMission.."'"..MissionName.."'; " +end +PlayerScore=PlayerScore+ScoreMission+ScoreTask +if ScoreMessageMission~=""then +ScoreMessage="Tasks: "..ScoreTask.." Mission: "..ScoreMission.." ( "..ScoreMessageMission..")" +end +end +return ScoreMessage,PlayerScore,PlayerPenalty +end +function SCORING:ReportScoreGroupSummary(PlayerGroup) +local PlayerMessage="" +self:T("Report Score Group Summary") +local PlayerUnits=PlayerGroup:GetUnits() +for UnitID,PlayerUnit in pairs(PlayerUnits)do +local PlayerUnit=PlayerUnit +local PlayerName=PlayerUnit:GetPlayerName() +if PlayerName then +local ReportHits,ScoreHits,PenaltyHits=self:ReportDetailedPlayerHits(PlayerName) +ReportHits=ReportHits~=""and"\n- "..ReportHits or ReportHits +self:F({ReportHits,ScoreHits,PenaltyHits}) +local ReportDestroys,ScoreDestroys,PenaltyDestroys=self:ReportDetailedPlayerDestroys(PlayerName) +ReportDestroys=ReportDestroys~=""and"\n- "..ReportDestroys or ReportDestroys +self:F({ReportDestroys,ScoreDestroys,PenaltyDestroys}) +local ReportCoalitionChanges,ScoreCoalitionChanges,PenaltyCoalitionChanges=self:ReportDetailedPlayerCoalitionChanges(PlayerName) +ReportCoalitionChanges=ReportCoalitionChanges~=""and"\n- "..ReportCoalitionChanges or ReportCoalitionChanges +self:F({ReportCoalitionChanges,ScoreCoalitionChanges,PenaltyCoalitionChanges}) +local ReportGoals,ScoreGoals,PenaltyGoals=self:ReportDetailedPlayerGoals(PlayerName) +ReportGoals=ReportGoals~=""and"\n- "..ReportGoals or ReportGoals +self:F({ReportGoals,ScoreGoals,PenaltyGoals}) +local ReportMissions,ScoreMissions,PenaltyMissions=self:ReportDetailedPlayerMissions(PlayerName) +ReportMissions=ReportMissions~=""and"\n- "..ReportMissions or ReportMissions +self:F({ReportMissions,ScoreMissions,PenaltyMissions}) +local PlayerScore=ScoreHits+ScoreDestroys+ScoreCoalitionChanges+ScoreGoals+ScoreMissions +local PlayerPenalty=PenaltyHits+PenaltyDestroys+PenaltyCoalitionChanges+PenaltyGoals+PenaltyMissions +PlayerMessage= +string.format("Player '%s' Score = %d ( %d Score, -%d Penalties )", +PlayerName, +PlayerScore-PlayerPenalty, +PlayerScore, +PlayerPenalty +) +MESSAGE:NewType(PlayerMessage,MESSAGE.Type.Detailed):ToGroup(PlayerGroup) +end +end +end +function SCORING:ReportScoreGroupDetailed(PlayerGroup) +local PlayerMessage="" +self:T("Report Score Group Detailed") +local PlayerUnits=PlayerGroup:GetUnits() +for UnitID,PlayerUnit in pairs(PlayerUnits)do +local PlayerUnit=PlayerUnit +local PlayerName=PlayerUnit:GetPlayerName() +if PlayerName then +local ReportHits,ScoreHits,PenaltyHits=self:ReportDetailedPlayerHits(PlayerName) +ReportHits=ReportHits~=""and"\n- "..ReportHits or ReportHits +self:F({ReportHits,ScoreHits,PenaltyHits}) +local ReportDestroys,ScoreDestroys,PenaltyDestroys=self:ReportDetailedPlayerDestroys(PlayerName) +ReportDestroys=ReportDestroys~=""and"\n- "..ReportDestroys or ReportDestroys +self:F({ReportDestroys,ScoreDestroys,PenaltyDestroys}) +local ReportCoalitionChanges,ScoreCoalitionChanges,PenaltyCoalitionChanges=self:ReportDetailedPlayerCoalitionChanges(PlayerName) +ReportCoalitionChanges=ReportCoalitionChanges~=""and"\n- "..ReportCoalitionChanges or ReportCoalitionChanges +self:F({ReportCoalitionChanges,ScoreCoalitionChanges,PenaltyCoalitionChanges}) +local ReportGoals,ScoreGoals,PenaltyGoals=self:ReportDetailedPlayerGoals(PlayerName) +ReportGoals=ReportGoals~=""and"\n- "..ReportGoals or ReportGoals +self:F({ReportGoals,ScoreGoals,PenaltyGoals}) +local ReportMissions,ScoreMissions,PenaltyMissions=self:ReportDetailedPlayerMissions(PlayerName) +ReportMissions=ReportMissions~=""and"\n- "..ReportMissions or ReportMissions +self:F({ReportMissions,ScoreMissions,PenaltyMissions}) +local PlayerScore=ScoreHits+ScoreDestroys+ScoreCoalitionChanges+ScoreGoals+ScoreMissions +local PlayerPenalty=PenaltyHits+PenaltyDestroys+PenaltyCoalitionChanges+ScoreGoals+PenaltyMissions +PlayerMessage= +string.format("Player '%s' Score = %d ( %d Score, -%d Penalties )%s%s%s%s%s", +PlayerName, +PlayerScore-PlayerPenalty, +PlayerScore, +PlayerPenalty, +ReportHits, +ReportDestroys, +ReportCoalitionChanges, +ReportGoals, +ReportMissions +) +MESSAGE:NewType(PlayerMessage,MESSAGE.Type.Detailed):ToGroup(PlayerGroup) +end +end +end +function SCORING:ReportScoreAllSummary(PlayerGroup) +local PlayerMessage="" +self:T({"Summary Score Report of All Players",Players=self.Players}) +for PlayerName,PlayerData in pairs(self.Players)do +self:T({PlayerName=PlayerName,PlayerGroup=PlayerGroup}) +if PlayerName then +local ReportHits,ScoreHits,PenaltyHits=self:ReportDetailedPlayerHits(PlayerName) +ReportHits=ReportHits~=""and"\n- "..ReportHits or ReportHits +self:F({ReportHits,ScoreHits,PenaltyHits}) +local ReportDestroys,ScoreDestroys,PenaltyDestroys=self:ReportDetailedPlayerDestroys(PlayerName) +ReportDestroys=ReportDestroys~=""and"\n- "..ReportDestroys or ReportDestroys +self:F({ReportDestroys,ScoreDestroys,PenaltyDestroys}) +local ReportCoalitionChanges,ScoreCoalitionChanges,PenaltyCoalitionChanges=self:ReportDetailedPlayerCoalitionChanges(PlayerName) +ReportCoalitionChanges=ReportCoalitionChanges~=""and"\n- "..ReportCoalitionChanges or ReportCoalitionChanges +self:F({ReportCoalitionChanges,ScoreCoalitionChanges,PenaltyCoalitionChanges}) +local ReportGoals,ScoreGoals,PenaltyGoals=self:ReportDetailedPlayerGoals(PlayerName) +ReportGoals=ReportGoals~=""and"\n- "..ReportGoals or ReportGoals +self:F({ReportGoals,ScoreGoals,PenaltyGoals}) +local ReportMissions,ScoreMissions,PenaltyMissions=self:ReportDetailedPlayerMissions(PlayerName) +ReportMissions=ReportMissions~=""and"\n- "..ReportMissions or ReportMissions +self:F({ReportMissions,ScoreMissions,PenaltyMissions}) +local PlayerScore=ScoreHits+ScoreDestroys+ScoreCoalitionChanges+ScoreGoals+ScoreMissions +local PlayerPenalty=PenaltyHits+PenaltyDestroys+PenaltyCoalitionChanges+ScoreGoals+PenaltyMissions +PlayerMessage= +string.format("Player '%s' Score = %d ( %d Score, -%d Penalties )", +PlayerName, +PlayerScore-PlayerPenalty, +PlayerScore, +PlayerPenalty +) +MESSAGE:NewType(PlayerMessage,MESSAGE.Type.Overview):ToGroup(PlayerGroup) +end +end +end +function SCORING:SecondsToClock(sSeconds) +local nSeconds=sSeconds +if nSeconds==0 then +return"00:00:00"; +else +nHours=string.format("%02.f",math.floor(nSeconds/3600)); +nMins=string.format("%02.f",math.floor(nSeconds/60-(nHours*60))); +nSecs=string.format("%02.f",math.floor(nSeconds-nHours*3600-nMins*60)); +return nHours..":"..nMins..":"..nSecs +end +end +function SCORING:OpenCSV(ScoringCSV) +self:F(ScoringCSV) +if lfs and io and os then +if ScoringCSV then +self.ScoringCSV=ScoringCSV +local fdir=lfs.writedir()..[[Logs\]]..self.ScoringCSV.." "..os.date("%Y-%m-%d %H-%M-%S")..".csv" +self.CSVFile,self.err=io.open(fdir,"w+") +if not self.CSVFile then +error("Error: Cannot open CSV file in "..lfs.writedir()) +end +self.CSVFile:write('"GameName","RunTime","Time","PlayerName","TargetPlayerName","ScoreType","PlayerUnitCoaltion","PlayerUnitCategory","PlayerUnitType","PlayerUnitName","TargetUnitCoalition","TargetUnitCategory","TargetUnitType","TargetUnitName","Times","Score"\n') +self.RunTime=os.date("%y-%m-%d_%H-%M-%S") +else +error("A string containing the CSV file name must be given.") +end +else +self:F("The MissionScripting.lua file has not been changed to allow lfs, io and os modules to be used...") +end +return self +end +function SCORING:ScoreCSV(PlayerName,TargetPlayerName,ScoreType,ScoreTimes,ScoreAmount,PlayerUnitName,PlayerUnitCoalition,PlayerUnitCategory,PlayerUnitType,TargetUnitName,TargetUnitCoalition,TargetUnitCategory,TargetUnitType) +local ScoreTime=self:SecondsToClock(timer.getTime()) +PlayerName=PlayerName:gsub('"','_') +TargetPlayerName=TargetPlayerName or"" +TargetPlayerName=TargetPlayerName:gsub('"','_') +if PlayerUnitName and PlayerUnitName~=''then +local PlayerUnit=Unit.getByName(PlayerUnitName) +if PlayerUnit then +if not PlayerUnitCategory then +PlayerUnitCategory=_SCORINGCategory[PlayerUnit:getDesc().category] +end +if not PlayerUnitCoalition then +PlayerUnitCoalition=_SCORINGCoalition[PlayerUnit:getCoalition()] +end +if not PlayerUnitType then +PlayerUnitType=PlayerUnit:getTypeName() +end +else +PlayerUnitName='' +PlayerUnitCategory='' +PlayerUnitCoalition='' +PlayerUnitType='' +end +else +PlayerUnitName='' +PlayerUnitCategory='' +PlayerUnitCoalition='' +PlayerUnitType='' +end +TargetUnitCoalition=TargetUnitCoalition or"" +TargetUnitCategory=TargetUnitCategory or"" +TargetUnitType=TargetUnitType or"" +TargetUnitName=TargetUnitName or"" +if lfs and io and os then +self.CSVFile:write( +'"'..self.GameName..'"'..','.. +'"'..self.RunTime..'"'..','.. +''..ScoreTime..''..','.. +'"'..PlayerName..'"'..','.. +'"'..TargetPlayerName..'"'..','.. +'"'..ScoreType..'"'..','.. +'"'..PlayerUnitCoalition..'"'..','.. +'"'..PlayerUnitCategory..'"'..','.. +'"'..PlayerUnitType..'"'..','.. +'"'..PlayerUnitName..'"'..','.. +'"'..TargetUnitCoalition..'"'..','.. +'"'..TargetUnitCategory..'"'..','.. +'"'..TargetUnitType..'"'..','.. +'"'..TargetUnitName..'"'..','.. +''..ScoreTimes..''..','.. +''..ScoreAmount +) +self.CSVFile:write("\n") +end +end +function SCORING:CloseCSV() +if lfs and io and os then +self.CSVFile:close() +end +end +CLEANUP_AIRBASE={ +ClassName="CLEANUP_AIRBASE", +TimeInterval=0.2, +CleanUpList={}, +} +CLEANUP_AIRBASE.__={} +CLEANUP_AIRBASE.__.Airbases={} +function CLEANUP_AIRBASE:New(AirbaseNames) +local self=BASE:Inherit(self,BASE:New()) +self:F({AirbaseNames}) +if type(AirbaseNames)=='table'then +for AirbaseID,AirbaseName in pairs(AirbaseNames)do +self:AddAirbase(AirbaseName) +end +else +local AirbaseName=AirbaseNames +self:AddAirbase(AirbaseName) +end +self:HandleEvent(EVENTS.Birth,self.__.OnEventBirth) +self.__.CleanUpScheduler=SCHEDULER:New(self,self.__.CleanUpSchedule,{},1,self.TimeInterval) +self:HandleEvent(EVENTS.EngineShutdown,self.__.EventAddForCleanUp) +self:HandleEvent(EVENTS.EngineStartup,self.__.EventAddForCleanUp) +self:HandleEvent(EVENTS.Hit,self.__.EventAddForCleanUp) +self:HandleEvent(EVENTS.PilotDead,self.__.OnEventCrash) +self:HandleEvent(EVENTS.Dead,self.__.OnEventCrash) +self:HandleEvent(EVENTS.Crash,self.__.OnEventCrash) +for UnitName,Unit in pairs(_DATABASE.UNITS)do +local Unit=Unit +if Unit:IsAlive()~=nil then +if self:IsInAirbase(Unit:GetVec2())then +self:F({UnitName=UnitName}) +self.CleanUpList[UnitName]={} +self.CleanUpList[UnitName].CleanUpUnit=Unit +self.CleanUpList[UnitName].CleanUpGroup=Unit:GetGroup() +self.CleanUpList[UnitName].CleanUpGroupName=Unit:GetGroup():GetName() +self.CleanUpList[UnitName].CleanUpUnitName=Unit:GetName() +end +end +end +return self +end +function CLEANUP_AIRBASE:AddAirbase(AirbaseName) +self.__.Airbases[AirbaseName]=AIRBASE:FindByName(AirbaseName) +self:F({"Airbase:",AirbaseName,self.__.Airbases[AirbaseName]:GetDesc()}) +return self +end +function CLEANUP_AIRBASE:RemoveAirbase(AirbaseName) +self.__.Airbases[AirbaseName]=nil +return self +end +function CLEANUP_AIRBASE:SetCleanMissiles(CleanMissiles) +if CleanMissiles then +self:HandleEvent(EVENTS.Shot,self.__.OnEventShot) +else +self:UnHandleEvent(EVENTS.Shot) +end +end +function CLEANUP_AIRBASE.__:IsInAirbase(Vec2) +local InAirbase=false +for AirbaseName,Airbase in pairs(self.__.Airbases)do +local Airbase=Airbase +if Airbase:GetZone():IsVec2InZone(Vec2)then +InAirbase=true +break; +end +end +return InAirbase +end +function CLEANUP_AIRBASE.__:DestroyUnit(CleanUpUnit) +self:F({CleanUpUnit}) +if CleanUpUnit then +local CleanUpUnitName=CleanUpUnit:GetName() +local CleanUpGroup=CleanUpUnit:GetGroup() +if CleanUpGroup:IsAlive()then +local CleanUpGroupUnits=CleanUpGroup:GetUnits() +if#CleanUpGroupUnits==1 then +local CleanUpGroupName=CleanUpGroup:GetName() +CleanUpGroup:Destroy() +else +CleanUpUnit:Destroy() +end +self.CleanUpList[CleanUpUnitName]=nil +end +end +end +function CLEANUP_AIRBASE.__:DestroyMissile(MissileObject) +self:F({MissileObject}) +if MissileObject and MissileObject:isExist()then +MissileObject:destroy() +self:T("MissileObject Destroyed") +end +end +function CLEANUP_AIRBASE.__:OnEventBirth(EventData) +self:F({EventData}) +if EventData and EventData.IniUnit and EventData.IniUnit:IsAlive()~=nil then +if self:IsInAirbase(EventData.IniUnit:GetVec2())then +self.CleanUpList[EventData.IniDCSUnitName]={} +self.CleanUpList[EventData.IniDCSUnitName].CleanUpUnit=EventData.IniUnit +self.CleanUpList[EventData.IniDCSUnitName].CleanUpGroup=EventData.IniGroup +self.CleanUpList[EventData.IniDCSUnitName].CleanUpGroupName=EventData.IniDCSGroupName +self.CleanUpList[EventData.IniDCSUnitName].CleanUpUnitName=EventData.IniDCSUnitName +end +end +end +function CLEANUP_AIRBASE.__:OnEventCrash(Event) +self:F({Event}) +if Event.IniDCSUnitName and Event.IniCategory==Object.Category.UNIT then +self.CleanUpList[Event.IniDCSUnitName]={} +self.CleanUpList[Event.IniDCSUnitName].CleanUpUnit=Event.IniUnit +self.CleanUpList[Event.IniDCSUnitName].CleanUpGroup=Event.IniGroup +self.CleanUpList[Event.IniDCSUnitName].CleanUpGroupName=Event.IniDCSGroupName +self.CleanUpList[Event.IniDCSUnitName].CleanUpUnitName=Event.IniDCSUnitName +end +end +function CLEANUP_AIRBASE.__:OnEventShot(Event) +self:F({Event}) +if self:IsInAirbase(Event.IniUnit:GetVec2())then +self:DestroyMissile(Event.Weapon) +end +end +function CLEANUP_AIRBASE.__:OnEventHit(Event) +self:F({Event}) +if Event.IniUnit then +if self:IsInAirbase(Event.IniUnit:GetVec2())then +self:T({"Life: ",Event.IniDCSUnitName,' = ',Event.IniUnit:GetLife(),"/",Event.IniUnit:GetLife0()}) +if Event.IniUnit:GetLife()0 then +local MoveProbability=(self.MoveMaximum*100)/self.AliveUnits +self:T('Move Probability = '..MoveProbability) +for MovementUnitName,MovementGroupName in pairs(self.MoveUnits)do +local MovementGroup=Group.getByName(MovementGroupName) +if MovementGroup and MovementGroup:isExist()then +local MoveOrStop=math.random(1,100) +self:T('MoveOrStop = '..MoveOrStop) +if MoveOrStop<=MoveProbability then +self:T('Group continues moving = '..MovementGroupName) +trigger.action.groupContinueMoving(MovementGroup) +else +self:T('Group stops moving = '..MovementGroupName) +trigger.action.groupStopMoving(MovementGroup) +end +else +self.MoveUnits[MovementUnitName]=nil +end +end +end +return true +end +SEAD={ +ClassName="SEAD", +TargetSkill={ +Average={Evade=30,DelayOn={40,60}}, +Good={Evade=20,DelayOn={30,50}}, +High={Evade=15,DelayOn={20,40}}, +Excellent={Evade=10,DelayOn={10,30}} +}, +SEADGroupPrefixes={}, +SuppressedGroups={}, +EngagementRange=75 +} +SEAD.Harms={ +["AGM_88"]="AGM_88", +["AGM_45"]="AGM_45", +["AGM_122"]="AGM_122", +["AGM_84"]="AGM_84", +["AGM_45"]="AGM_45", +["ALARN"]="ALARM", +["LD-10"]="LD-10", +["X_58"]="X_58", +["X_28"]="X_28", +["X_25"]="X_25", +["X_31"]="X_31", +["Kh25"]="Kh25", +} +function SEAD:New(SEADGroupPrefixes) +local self=BASE:Inherit(self,BASE:New()) +self:F(SEADGroupPrefixes) +if type(SEADGroupPrefixes)=='table'then +for SEADGroupPrefixID,SEADGroupPrefix in pairs(SEADGroupPrefixes)do +self.SEADGroupPrefixes[SEADGroupPrefix]=SEADGroupPrefix +end +else +self.SEADGroupPrefixes[SEADGroupPrefixes]=SEADGroupPrefixes +end +self:HandleEvent(EVENTS.Shot) +self:I("*** SEAD - Started Version 0.2.7") +return self +end +function SEAD:UpdateSet(SEADGroupPrefixes) +self:F(SEADGroupPrefixes) +if type(SEADGroupPrefixes)=='table'then +for SEADGroupPrefixID,SEADGroupPrefix in pairs(SEADGroupPrefixes)do +self.SEADGroupPrefixes[SEADGroupPrefix]=SEADGroupPrefix +end +else +self.SEADGroupPrefixes[SEADGroupPrefixes]=SEADGroupPrefixes +end +return self +end +function SEAD:SetEngagementRange(range) +self:F({range}) +range=range or 75 +if range<0 or range>100 then +range=75 +end +self.EngagementRange=range +self:T(string.format("*** SEAD - Engagement range set to %s",range)) +return self +end +function SEAD:_CheckHarms(WeaponName) +self:F({WeaponName}) +local hit=false +for _,_name in pairs(SEAD.Harms)do +if string.find(WeaponName,_name,1)then hit=true end +end +return hit +end +function SEAD:OnEventShot(EventData) +self:T({EventData}) +local SEADUnit=EventData.IniDCSUnit +local SEADUnitName=EventData.IniDCSUnitName +local SEADWeapon=EventData.Weapon +local SEADWeaponName=EventData.WeaponName +self:T("*** SEAD - Missile Launched = "..SEADWeaponName) +self:T({SEADWeapon}) +if self:_CheckHarms(SEADWeaponName)then +local _targetskill="Random" +local _targetMimgroupName="none" +local _evade=math.random(1,100) +local _targetMim=EventData.Weapon:getTarget() +local _targetUnit=UNIT:Find(_targetMim) +if _targetUnit and _targetUnit:IsAlive()then +local _targetMimgroup=_targetUnit:GetGroup() +local _targetMimgroupName=_targetMimgroup:GetName() +self:T(self.SEADGroupPrefixes) +self:T(_targetMimgroupName) +end +local SEADGroupFound=false +for SEADGroupPrefixID,SEADGroupPrefix in pairs(self.SEADGroupPrefixes)do +if string.find(_targetMimgroupName,SEADGroupPrefix,1,true)then +SEADGroupFound=true +self:T('*** SEAD - Group Found') +break +end +end +if SEADGroupFound==true then +if _targetskill=="Random"then +local Skills={"Average","Good","High","Excellent"} +_targetskill=Skills[math.random(1,4)] +end +self:T(_targetskill) +if self.TargetSkill[_targetskill]then +if(_evade>self.TargetSkill[_targetskill].Evade)then +self:T(string.format("*** SEAD - Evading, target skill "..string.format(_targetskill))) +local _targetMimgroup=Unit.getGroup(Weapon.getTarget(SEADWeapon)) +local _targetMimcont=_targetMimgroup:getController() +routines.groupRandomDistSelf(_targetMimgroup,300,'Diamond',250,20) +local id={ +groupName=_targetMimgroup, +ctrl=_targetMimcont +} +local function SuppressionEnd(id) +local range=self.EngagementRange +self:T(string.format("*** SEAD - Engagement Range is %d",range)) +id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.RED) +id.ctrl:setOption(AI.Option.Ground.id.AC_ENGAGEMENT_RANGE_RESTRICTION,range) +self.SuppressedGroups[id.groupName]=nil +end +local delay=math.random(self.TargetSkill[_targetskill].DelayOn[1],self.TargetSkill[_targetskill].DelayOn[2]) +local SuppressionEndTime=timer.getTime()+delay +if self.SuppressedGroups[id.groupName]==nil then +self.SuppressedGroups[id.groupName]={ +SuppressionEndTime=delay +} +Controller.setOption(_targetMimcont,AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) +timer.scheduleFunction(SuppressionEnd,id,SuppressionEndTime) +end +end +end +end +end +end +ESCORT={ +ClassName="ESCORT", +EscortName=nil, +EscortClient=nil, +EscortGroup=nil, +EscortMode=1, +MODE={ +FOLLOW=1, +MISSION=2, +}, +Targets={}, +FollowScheduler=nil, +ReportTargets=true, +OptionROE=AI.Option.Air.val.ROE.OPEN_FIRE, +OptionReactionOnThreat=AI.Option.Air.val.REACTION_ON_THREAT.ALLOW_ABORT_MISSION, +SmokeDirectionVector=false, +TaskPoints={} +} +function ESCORT:New(EscortClient,EscortGroup,EscortName,EscortBriefing) +local self=BASE:Inherit(self,BASE:New()) +self:F({EscortClient,EscortGroup,EscortName}) +self.EscortClient=EscortClient +self.EscortGroup=EscortGroup +self.EscortName=EscortName +self.EscortBriefing=EscortBriefing +self.EscortSetGroup=SET_GROUP:New() +self.EscortSetGroup:AddObject(self.EscortGroup) +self.EscortSetGroup:Flush() +self.Detection=DETECTION_UNITS:New(self.EscortSetGroup,15000) +self.EscortGroup.Detection=self.Detection +if not self.EscortClient._EscortGroups then +self.EscortClient._EscortGroups={} +end +if not self.EscortClient._EscortGroups[EscortGroup:GetName()]then +self.EscortClient._EscortGroups[EscortGroup:GetName()]={} +self.EscortClient._EscortGroups[EscortGroup:GetName()].EscortGroup=self.EscortGroup +self.EscortClient._EscortGroups[EscortGroup:GetName()].EscortName=self.EscortName +self.EscortClient._EscortGroups[EscortGroup:GetName()].Detection=self.EscortGroup.Detection +end +self.EscortMenu=MENU_GROUP:New(self.EscortClient:GetGroup(),self.EscortName) +self.EscortGroup:WayPointInitialize(1) +self.EscortGroup:OptionROTVertical() +self.EscortGroup:OptionROEOpenFire() +if not EscortBriefing then +EscortGroup:MessageToClient(EscortGroup:GetCategoryName().." '"..EscortName.."' ("..EscortGroup:GetCallsign()..") reporting! ".. +"We're escorting your flight. ".. +"Use the Radio Menu and F10 and use the options under + "..EscortName.."\n", +60,EscortClient +) +else +EscortGroup:MessageToClient(EscortGroup:GetCategoryName().." '"..EscortName.."' ("..EscortGroup:GetCallsign()..") "..EscortBriefing, +60,EscortClient +) +end +self.FollowDistance=100 +self.CT1=0 +self.GT1=0 +self.FollowScheduler,self.FollowSchedule=SCHEDULER:New(self,self._FollowScheduler,{},1,.5,.01) +self.FollowScheduler:Stop(self.FollowSchedule) +self.EscortMode=ESCORT.MODE.MISSION +return self +end +function ESCORT:SetDetection(Detection) +self.Detection=Detection +self.EscortGroup.Detection=self.Detection +self.EscortClient._EscortGroups[self.EscortGroup:GetName()].Detection=self.EscortGroup.Detection +Detection:__Start(1) +end +function ESCORT:TestSmokeDirectionVector(SmokeDirection) +self.SmokeDirectionVector=(SmokeDirection==true)and true or false +end +function ESCORT:Menus() +self:F() +self:MenuFollowAt(100) +self:MenuFollowAt(200) +self:MenuFollowAt(300) +self:MenuFollowAt(400) +self:MenuScanForTargets(100,60) +self:MenuHoldAtEscortPosition(30) +self:MenuHoldAtLeaderPosition(30) +self:MenuFlare() +self:MenuSmoke() +self:MenuReportTargets(60) +self:MenuAssistedAttack() +self:MenuROE() +self:MenuEvasion() +self:MenuResumeMission() +return self +end +function ESCORT:MenuFollowAt(Distance) +self:F(Distance) +if self.EscortGroup:IsAir()then +if not self.EscortMenuReportNavigation then +self.EscortMenuReportNavigation=MENU_GROUP:New(self.EscortClient:GetGroup(),"Navigation",self.EscortMenu) +end +if not self.EscortMenuJoinUpAndFollow then +self.EscortMenuJoinUpAndFollow={} +end +self.EscortMenuJoinUpAndFollow[#self.EscortMenuJoinUpAndFollow+1]=MENU_GROUP_COMMAND:New(self.EscortClient:GetGroup(),"Join-Up and Follow at "..Distance,self.EscortMenuReportNavigation,ESCORT._JoinUpAndFollow,self,Distance) +self.EscortMode=ESCORT.MODE.FOLLOW +end +return self +end +function ESCORT:MenuHoldAtEscortPosition(Height,Seconds,MenuTextFormat) +self:F({Height,Seconds,MenuTextFormat}) +if self.EscortGroup:IsAir()then +if not self.EscortMenuHold then +self.EscortMenuHold=MENU_GROUP:New(self.EscortClient:GetGroup(),"Hold position",self.EscortMenu) +end +if not Height then +Height=30 +end +if not Seconds then +Seconds=0 +end +local MenuText="" +if not MenuTextFormat then +if Seconds==0 then +MenuText=string.format("Hold at %d meter",Height) +else +MenuText=string.format("Hold at %d meter for %d seconds",Height,Seconds) +end +else +if Seconds==0 then +MenuText=string.format(MenuTextFormat,Height) +else +MenuText=string.format(MenuTextFormat,Height,Seconds) +end +end +if not self.EscortMenuHoldPosition then +self.EscortMenuHoldPosition={} +end +self.EscortMenuHoldPosition[#self.EscortMenuHoldPosition+1]=MENU_GROUP_COMMAND +:New( +self.EscortClient:GetGroup(), +MenuText, +self.EscortMenuHold, +ESCORT._HoldPosition, +self, +self.EscortGroup, +Height, +Seconds +) +end +return self +end +function ESCORT:MenuHoldAtLeaderPosition(Height,Seconds,MenuTextFormat) +self:F({Height,Seconds,MenuTextFormat}) +if self.EscortGroup:IsAir()then +if not self.EscortMenuHold then +self.EscortMenuHold=MENU_GROUP:New(self.EscortClient:GetGroup(),"Hold position",self.EscortMenu) +end +if not Height then +Height=30 +end +if not Seconds then +Seconds=0 +end +local MenuText="" +if not MenuTextFormat then +if Seconds==0 then +MenuText=string.format("Rejoin and hold at %d meter",Height) +else +MenuText=string.format("Rejoin and hold at %d meter for %d seconds",Height,Seconds) +end +else +if Seconds==0 then +MenuText=string.format(MenuTextFormat,Height) +else +MenuText=string.format(MenuTextFormat,Height,Seconds) +end +end +if not self.EscortMenuHoldAtLeaderPosition then +self.EscortMenuHoldAtLeaderPosition={} +end +self.EscortMenuHoldAtLeaderPosition[#self.EscortMenuHoldAtLeaderPosition+1]=MENU_GROUP_COMMAND +:New( +self.EscortClient:GetGroup(), +MenuText, +self.EscortMenuHold, +ESCORT._HoldPosition, +{ParamSelf=self, +ParamOrbitGroup=self.EscortClient, +ParamHeight=Height, +ParamSeconds=Seconds +} +) +end +return self +end +function ESCORT:MenuScanForTargets(Height,Seconds,MenuTextFormat) +self:F({Height,Seconds,MenuTextFormat}) +if self.EscortGroup:IsAir()then +if not self.EscortMenuScan then +self.EscortMenuScan=MENU_GROUP:New(self.EscortClient:GetGroup(),"Scan for targets",self.EscortMenu) +end +if not Height then +Height=100 +end +if not Seconds then +Seconds=30 +end +local MenuText="" +if not MenuTextFormat then +if Seconds==0 then +MenuText=string.format("At %d meter",Height) +else +MenuText=string.format("At %d meter for %d seconds",Height,Seconds) +end +else +if Seconds==0 then +MenuText=string.format(MenuTextFormat,Height) +else +MenuText=string.format(MenuTextFormat,Height,Seconds) +end +end +if not self.EscortMenuScanForTargets then +self.EscortMenuScanForTargets={} +end +self.EscortMenuScanForTargets[#self.EscortMenuScanForTargets+1]=MENU_GROUP_COMMAND +:New( +self.EscortClient:GetGroup(), +MenuText, +self.EscortMenuScan, +ESCORT._ScanTargets, +self, +30 +) +end +return self +end +function ESCORT:MenuFlare(MenuTextFormat) +self:F() +if not self.EscortMenuReportNavigation then +self.EscortMenuReportNavigation=MENU_GROUP:New(self.EscortClient:GetGroup(),"Navigation",self.EscortMenu) +end +local MenuText="" +if not MenuTextFormat then +MenuText="Flare" +else +MenuText=MenuTextFormat +end +if not self.EscortMenuFlare then +self.EscortMenuFlare=MENU_GROUP:New(self.EscortClient:GetGroup(),MenuText,self.EscortMenuReportNavigation,ESCORT._Flare,self) +self.EscortMenuFlareGreen=MENU_GROUP_COMMAND:New(self.EscortClient:GetGroup(),"Release green flare",self.EscortMenuFlare,ESCORT._Flare,self,FLARECOLOR.Green,"Released a green flare!") +self.EscortMenuFlareRed=MENU_GROUP_COMMAND:New(self.EscortClient:GetGroup(),"Release red flare",self.EscortMenuFlare,ESCORT._Flare,self,FLARECOLOR.Red,"Released a red flare!") +self.EscortMenuFlareWhite=MENU_GROUP_COMMAND:New(self.EscortClient:GetGroup(),"Release white flare",self.EscortMenuFlare,ESCORT._Flare,self,FLARECOLOR.White,"Released a white flare!") +self.EscortMenuFlareYellow=MENU_GROUP_COMMAND:New(self.EscortClient:GetGroup(),"Release yellow flare",self.EscortMenuFlare,ESCORT._Flare,self,FLARECOLOR.Yellow,"Released a yellow flare!") +end +return self +end +function ESCORT:MenuSmoke(MenuTextFormat) +self:F() +if not self.EscortGroup:IsAir()then +if not self.EscortMenuReportNavigation then +self.EscortMenuReportNavigation=MENU_GROUP:New(self.EscortClient:GetGroup(),"Navigation",self.EscortMenu) +end +local MenuText="" +if not MenuTextFormat then +MenuText="Smoke" +else +MenuText=MenuTextFormat +end +if not self.EscortMenuSmoke then +self.EscortMenuSmoke=MENU_GROUP:New(self.EscortClient:GetGroup(),"Smoke",self.EscortMenuReportNavigation,ESCORT._Smoke,self) +self.EscortMenuSmokeGreen=MENU_GROUP_COMMAND:New(self.EscortClient:GetGroup(),"Release green smoke",self.EscortMenuSmoke,ESCORT._Smoke,self,SMOKECOLOR.Green,"Releasing green smoke!") +self.EscortMenuSmokeRed=MENU_GROUP_COMMAND:New(self.EscortClient:GetGroup(),"Release red smoke",self.EscortMenuSmoke,ESCORT._Smoke,self,SMOKECOLOR.Red,"Releasing red smoke!") +self.EscortMenuSmokeWhite=MENU_GROUP_COMMAND:New(self.EscortClient:GetGroup(),"Release white smoke",self.EscortMenuSmoke,ESCORT._Smoke,self,SMOKECOLOR.White,"Releasing white smoke!") +self.EscortMenuSmokeOrange=MENU_GROUP_COMMAND:New(self.EscortClient:GetGroup(),"Release orange smoke",self.EscortMenuSmoke,ESCORT._Smoke,self,SMOKECOLOR.Orange,"Releasing orange smoke!") +self.EscortMenuSmokeBlue=MENU_GROUP_COMMAND:New(self.EscortClient:GetGroup(),"Release blue smoke",self.EscortMenuSmoke,ESCORT._Smoke,self,SMOKECOLOR.Blue,"Releasing blue smoke!") +end +end +return self +end +function ESCORT:MenuReportTargets(Seconds) +self:F({Seconds}) +if not self.EscortMenuReportNearbyTargets then +self.EscortMenuReportNearbyTargets=MENU_GROUP:New(self.EscortClient:GetGroup(),"Report targets",self.EscortMenu) +end +if not Seconds then +Seconds=30 +end +self.EscortMenuReportNearbyTargetsNow=MENU_GROUP_COMMAND:New(self.EscortClient:GetGroup(),"Report targets now!",self.EscortMenuReportNearbyTargets,ESCORT._ReportNearbyTargetsNow,self) +self.EscortMenuReportNearbyTargetsOn=MENU_GROUP_COMMAND:New(self.EscortClient:GetGroup(),"Report targets on",self.EscortMenuReportNearbyTargets,ESCORT._SwitchReportNearbyTargets,self,true) +self.EscortMenuReportNearbyTargetsOff=MENU_GROUP_COMMAND:New(self.EscortClient:GetGroup(),"Report targets off",self.EscortMenuReportNearbyTargets,ESCORT._SwitchReportNearbyTargets,self,false) +self.EscortMenuAttackNearbyTargets=MENU_GROUP:New(self.EscortClient:GetGroup(),"Attack targets",self.EscortMenu) +self.ReportTargetsScheduler=SCHEDULER:New(self,self._ReportTargetsScheduler,{},1,Seconds) +return self +end +function ESCORT:MenuAssistedAttack() +self:F() +self.EscortMenuTargetAssistance=MENU_GROUP:New(self.EscortClient:GetGroup(),"Request assistance from",self.EscortMenu) +return self +end +function ESCORT:MenuROE(MenuTextFormat) +self:F(MenuTextFormat) +if not self.EscortMenuROE then +self.EscortMenuROE=MENU_GROUP:New(self.EscortClient:GetGroup(),"ROE",self.EscortMenu) +if self.EscortGroup:OptionROEHoldFirePossible()then +self.EscortMenuROEHoldFire=MENU_GROUP_COMMAND:New(self.EscortClient:GetGroup(),"Hold Fire",self.EscortMenuROE,ESCORT._ROE,self,self.EscortGroup:OptionROEHoldFire(),"Holding weapons!") +end +if self.EscortGroup:OptionROEReturnFirePossible()then +self.EscortMenuROEReturnFire=MENU_GROUP_COMMAND:New(self.EscortClient:GetGroup(),"Return Fire",self.EscortMenuROE,ESCORT._ROE,self,self.EscortGroup:OptionROEReturnFire(),"Returning fire!") +end +if self.EscortGroup:OptionROEOpenFirePossible()then +self.EscortMenuROEOpenFire=MENU_GROUP_COMMAND:New(self.EscortClient:GetGroup(),"Open Fire",self.EscortMenuROE,ESCORT._ROE,self,self.EscortGroup:OptionROEOpenFire(),"Opening fire on designated targets!!") +end +if self.EscortGroup:OptionROEWeaponFreePossible()then +self.EscortMenuROEWeaponFree=MENU_GROUP_COMMAND:New(self.EscortClient:GetGroup(),"Weapon Free",self.EscortMenuROE,ESCORT._ROE,self,self.EscortGroup:OptionROEWeaponFree(),"Opening fire on targets of opportunity!") +end +end +return self +end +function ESCORT:MenuEvasion(MenuTextFormat) +self:F(MenuTextFormat) +if self.EscortGroup:IsAir()then +if not self.EscortMenuEvasion then +self.EscortMenuEvasion=MENU_GROUP:New(self.EscortClient:GetGroup(),"Evasion",self.EscortMenu) +if self.EscortGroup:OptionROTNoReactionPossible()then +self.EscortMenuEvasionNoReaction=MENU_GROUP_COMMAND:New(self.EscortClient:GetGroup(),"Fight until death",self.EscortMenuEvasion,ESCORT._ROT,self,self.EscortGroup:OptionROTNoReaction(),"Fighting until death!") +end +if self.EscortGroup:OptionROTPassiveDefensePossible()then +self.EscortMenuEvasionPassiveDefense=MENU_GROUP_COMMAND:New(self.EscortClient:GetGroup(),"Use flares, chaff and jammers",self.EscortMenuEvasion,ESCORT._ROT,self,self.EscortGroup:OptionROTPassiveDefense(),"Defending using jammers, chaff and flares!") +end +if self.EscortGroup:OptionROTEvadeFirePossible()then +self.EscortMenuEvasionEvadeFire=MENU_GROUP_COMMAND:New(self.EscortClient:GetGroup(),"Evade enemy fire",self.EscortMenuEvasion,ESCORT._ROT,self,self.EscortGroup:OptionROTEvadeFire(),"Evading on enemy fire!") +end +if self.EscortGroup:OptionROTVerticalPossible()then +self.EscortMenuOptionEvasionVertical=MENU_GROUP_COMMAND:New(self.EscortClient:GetGroup(),"Go below radar and evade fire",self.EscortMenuEvasion,ESCORT._ROT,self,self.EscortGroup:OptionROTVertical(),"Evading on enemy fire with vertical manoeuvres!") +end +end +end +return self +end +function ESCORT:MenuResumeMission() +self:F() +if not self.EscortMenuResumeMission then +self.EscortMenuResumeMission=MENU_GROUP:New(self.EscortClient:GetGroup(),"Resume mission from",self.EscortMenu) +end +return self +end +function ESCORT:_HoldPosition(OrbitGroup,OrbitHeight,OrbitSeconds) +local EscortGroup=self.EscortGroup +local EscortClient=self.EscortClient +local OrbitUnit=OrbitGroup:GetUnit(1) +self.FollowScheduler:Stop(self.FollowSchedule) +local PointFrom={} +local GroupVec3=EscortGroup:GetUnit(1):GetVec3() +PointFrom={} +PointFrom.x=GroupVec3.x +PointFrom.y=GroupVec3.z +PointFrom.speed=250 +PointFrom.type=AI.Task.WaypointType.TURNING_POINT +PointFrom.alt=GroupVec3.y +PointFrom.alt_type=AI.Task.AltitudeType.BARO +local OrbitPoint=OrbitUnit:GetVec2() +local PointTo={} +PointTo.x=OrbitPoint.x +PointTo.y=OrbitPoint.y +PointTo.speed=250 +PointTo.type=AI.Task.WaypointType.TURNING_POINT +PointTo.alt=OrbitHeight +PointTo.alt_type=AI.Task.AltitudeType.BARO +PointTo.task=EscortGroup:TaskOrbitCircleAtVec2(OrbitPoint,OrbitHeight,0) +local Points={PointFrom,PointTo} +EscortGroup:OptionROEHoldFire() +EscortGroup:OptionROTPassiveDefense() +EscortGroup:SetTask(EscortGroup:TaskRoute(Points)) +EscortGroup:MessageToClient("Orbiting at location.",10,EscortClient) +end +function ESCORT:_JoinUpAndFollow(Distance) +local EscortGroup=self.EscortGroup +local EscortClient=self.EscortClient +self.Distance=Distance +self:JoinUpAndFollow(EscortGroup,EscortClient,self.Distance) +end +function ESCORT:JoinUpAndFollow(EscortGroup,EscortClient,Distance) +self:F({EscortGroup,EscortClient,Distance}) +self.FollowScheduler:Stop(self.FollowSchedule) +EscortGroup:OptionROEHoldFire() +EscortGroup:OptionROTPassiveDefense() +self.EscortMode=ESCORT.MODE.FOLLOW +self.CT1=0 +self.GT1=0 +self.FollowScheduler:Start(self.FollowSchedule) +EscortGroup:MessageToClient("Rejoining and Following at "..Distance.."!",30,EscortClient) +end +function ESCORT:_Flare(Color,Message) +local EscortGroup=self.EscortGroup +local EscortClient=self.EscortClient +EscortGroup:GetUnit(1):Flare(Color) +EscortGroup:MessageToClient(Message,10,EscortClient) +end +function ESCORT:_Smoke(Color,Message) +local EscortGroup=self.EscortGroup +local EscortClient=self.EscortClient +EscortGroup:GetUnit(1):Smoke(Color) +EscortGroup:MessageToClient(Message,10,EscortClient) +end +function ESCORT:_ReportNearbyTargetsNow() +local EscortGroup=self.EscortGroup +local EscortClient=self.EscortClient +self:_ReportTargetsScheduler() +end +function ESCORT:_SwitchReportNearbyTargets(ReportTargets) +local EscortGroup=self.EscortGroup +local EscortClient=self.EscortClient +self.ReportTargets=ReportTargets +if self.ReportTargets then +if not self.ReportTargetsScheduler then +self.ReportTargetsScheduler:Schedule(self,self._ReportTargetsScheduler,{},1,30) +end +else +routines.removeFunction(self.ReportTargetsScheduler) +self.ReportTargetsScheduler=nil +end +end +function ESCORT:_ScanTargets(ScanDuration) +local EscortGroup=self.EscortGroup +local EscortClient=self.EscortClient +self.FollowScheduler:Stop(self.FollowSchedule) +if EscortGroup:IsHelicopter()then +EscortGroup:PushTask( +EscortGroup:TaskControlled( +EscortGroup:TaskOrbitCircle(200,20), +EscortGroup:TaskCondition(nil,nil,nil,nil,ScanDuration,nil) +),1) +elseif EscortGroup:IsAirPlane()then +EscortGroup:PushTask( +EscortGroup:TaskControlled( +EscortGroup:TaskOrbitCircle(1000,500), +EscortGroup:TaskCondition(nil,nil,nil,nil,ScanDuration,nil) +),1) +end +EscortGroup:MessageToClient("Scanning targets for "..ScanDuration.." seconds.",ScanDuration,EscortClient) +if self.EscortMode==ESCORT.MODE.FOLLOW then +self.FollowScheduler:Start(self.FollowSchedule) +end +end +function _Resume(EscortGroup) +env.info('_Resume') +local Escort=EscortGroup:GetState(EscortGroup,"Escort") +env.info("EscortMode = "..Escort.EscortMode) +if Escort.EscortMode==ESCORT.MODE.FOLLOW then +Escort:JoinUpAndFollow(EscortGroup,Escort.EscortClient,Escort.Distance) +end +end +function ESCORT:_AttackTarget(DetectedItem) +local EscortGroup=self.EscortGroup +self:F(EscortGroup) +local EscortClient=self.EscortClient +self.FollowScheduler:Stop(self.FollowSchedule) +if EscortGroup:IsAir()then +EscortGroup:OptionROEOpenFire() +EscortGroup:OptionROTPassiveDefense() +EscortGroup:SetState(EscortGroup,"Escort",self) +local DetectedSet=self.Detection:GetDetectedItemSet(DetectedItem) +local Tasks={} +DetectedSet:ForEachUnit( +function(DetectedUnit,Tasks) +if DetectedUnit:IsAlive()then +Tasks[#Tasks+1]=EscortGroup:TaskAttackUnit(DetectedUnit) +end +end,Tasks +) +Tasks[#Tasks+1]=EscortGroup:TaskFunction("_Resume",{"''"}) +EscortGroup:SetTask( +EscortGroup:TaskCombo( +Tasks +),1 +) +else +local DetectedSet=self.Detection:GetDetectedItemSet(DetectedItem) +local Tasks={} +DetectedSet:ForEachUnit( +function(DetectedUnit,Tasks) +if DetectedUnit:IsAlive()then +Tasks[#Tasks+1]=EscortGroup:TaskFireAtPoint(DetectedUnit:GetVec2(),50) +end +end,Tasks +) +EscortGroup:SetTask( +EscortGroup:TaskCombo( +Tasks +),1 +) +end +EscortGroup:MessageToClient("Engaging Designated Unit!",10,EscortClient) +end +function ESCORT:_AssistTarget(EscortGroupAttack,DetectedItem) +local EscortGroup=self.EscortGroup +local EscortClient=self.EscortClient +self.FollowScheduler:Stop(self.FollowSchedule) +if EscortGroupAttack:IsAir()then +EscortGroupAttack:OptionROEOpenFire() +EscortGroupAttack:OptionROTVertical() +local DetectedSet=self.Detection:GetDetectedItemSet(DetectedItem) +local Tasks={} +DetectedSet:ForEachUnit( +function(DetectedUnit,Tasks) +if DetectedUnit:IsAlive()then +Tasks[#Tasks+1]=EscortGroupAttack:TaskAttackUnit(DetectedUnit) +end +end,Tasks +) +Tasks[#Tasks+1]=EscortGroupAttack:TaskOrbitCircle(500,350) +EscortGroupAttack:SetTask( +EscortGroupAttack:TaskCombo( +Tasks +),1 +) +else +local DetectedSet=self.Detection:GetDetectedItemSet(DetectedItem) +local Tasks={} +DetectedSet:ForEachUnit( +function(DetectedUnit,Tasks) +if DetectedUnit:IsAlive()then +Tasks[#Tasks+1]=EscortGroupAttack:TaskFireAtPoint(DetectedUnit:GetVec2(),50) +end +end,Tasks +) +EscortGroupAttack:SetTask( +EscortGroupAttack:TaskCombo( +Tasks +),1 +) +end +EscortGroupAttack:MessageToClient("Assisting with the destroying the enemy unit!",10,EscortClient) +end +function ESCORT:_ROE(EscortROEFunction,EscortROEMessage) +local EscortGroup=self.EscortGroup +local EscortClient=self.EscortClient +pcall(function()EscortROEFunction()end) +EscortGroup:MessageToClient(EscortROEMessage,10,EscortClient) +end +function ESCORT:_ROT(EscortROTFunction,EscortROTMessage) +local EscortGroup=self.EscortGroup +local EscortClient=self.EscortClient +pcall(function()EscortROTFunction()end) +EscortGroup:MessageToClient(EscortROTMessage,10,EscortClient) +end +function ESCORT:_ResumeMission(WayPoint) +local EscortGroup=self.EscortGroup +local EscortClient=self.EscortClient +self.FollowScheduler:Stop(self.FollowSchedule) +local WayPoints=EscortGroup:GetTaskRoute() +self:T(WayPoint,WayPoints) +for WayPointIgnore=1,WayPoint do +table.remove(WayPoints,1) +end +SCHEDULER:New(EscortGroup,EscortGroup.SetTask,{EscortGroup:TaskRoute(WayPoints)},1) +EscortGroup:MessageToClient("Resuming mission from waypoint "..WayPoint..".",10,EscortClient) +end +function ESCORT:RegisterRoute() +self:F() +local EscortGroup=self.EscortGroup +local TaskPoints=EscortGroup:GetTaskRoute() +self:T(TaskPoints) +return TaskPoints +end +function ESCORT:_FollowScheduler() +self:F({self.FollowDistance}) +self:T({self.EscortClient.UnitName,self.EscortGroup.GroupName}) +if self.EscortGroup:IsAlive()and self.EscortClient:IsAlive()then +local ClientUnit=self.EscortClient:GetClientGroupUnit() +local GroupUnit=self.EscortGroup:GetUnit(1) +local FollowDistance=self.FollowDistance +self:T({ClientUnit.UnitName,GroupUnit.UnitName}) +if self.CT1==0 and self.GT1==0 then +self.CV1=ClientUnit:GetVec3() +self:T({"self.CV1",self.CV1}) +self.CT1=timer.getTime() +self.GV1=GroupUnit:GetVec3() +self.GT1=timer.getTime() +else +local CT1=self.CT1 +local CT2=timer.getTime() +local CV1=self.CV1 +local CV2=ClientUnit:GetVec3() +self.CT1=CT2 +self.CV1=CV2 +local CD=((CV2.x-CV1.x)^2+(CV2.y-CV1.y)^2+(CV2.z-CV1.z)^2)^0.5 +local CT=CT2-CT1 +local CS=(3600/CT)*(CD/1000) +self:T2({"Client:",CS,CD,CT,CV2,CV1,CT2,CT1}) +local GT1=self.GT1 +local GT2=timer.getTime() +local GV1=self.GV1 +local GV2=GroupUnit:GetVec3() +self.GT1=GT2 +self.GV1=GV2 +local GD=((GV2.x-GV1.x)^2+(GV2.y-GV1.y)^2+(GV2.z-GV1.z)^2)^0.5 +local GT=GT2-GT1 +local GS=(3600/GT)*(GD/1000) +self:T2({"Group:",GS,GD,GT,GV2,GV1,GT2,GT1}) +local GV={x=GV2.x-CV2.x,y=GV2.y-CV2.y,z=GV2.z-CV2.z} +local GH2={x=GV2.x,y=CV2.y,z=GV2.z} +local alpha=math.atan2(GV.z,GV.x) +local CVI={x=CV2.x+FollowDistance*math.cos(alpha), +y=GH2.y, +z=CV2.z+FollowDistance*math.sin(alpha), +} +local DV={x=CV2.x-CVI.x,y=CV2.y-CVI.y,z=CV2.z-CVI.z} +local DVu={x=DV.x/FollowDistance,y=DV.y/FollowDistance,z=DV.z/FollowDistance} +local GDV={x=DVu.x*CS*8+CVI.x,y=CVI.y,z=DVu.z*CS*8+CVI.z} +if self.SmokeDirectionVector==true then +trigger.action.smoke(GDV,trigger.smokeColor.Red) +end +self:T2({"CV2:",CV2}) +self:T2({"CVI:",CVI}) +self:T2({"GDV:",GDV}) +local CatchUpDistance=((GDV.x-GV2.x)^2+(GDV.y-GV2.y)^2+(GDV.z-GV2.z)^2)^0.5 +local Time=10 +local CatchUpSpeed=(CatchUpDistance-(CS*8.4))/Time +local Speed=CS+CatchUpSpeed +if Speed<0 then +Speed=0 +end +self:T({"Client Speed, Escort Speed, Speed, FollowDistance, Time:",CS,GS,Speed,FollowDistance,Time}) +self.EscortGroup:RouteToVec3(GDV,Speed/3.6) +end +return true +end +return false +end +function ESCORT:_ReportTargetsScheduler() +self:F(self.EscortGroup:GetName()) +if self.EscortGroup:IsAlive()and self.EscortClient:IsAlive()then +if true then +local EscortGroupName=self.EscortGroup:GetName() +self.EscortMenuAttackNearbyTargets:RemoveSubMenus() +if self.EscortMenuTargetAssistance then +self.EscortMenuTargetAssistance:RemoveSubMenus() +end +local DetectedItems=self.Detection:GetDetectedItems() +self:F(DetectedItems) +local DetectedTargets=false +local DetectedMsgs={} +for ClientEscortGroupName,EscortGroupData in pairs(self.EscortClient._EscortGroups)do +local ClientEscortTargets=EscortGroupData.Detection +for DetectedItemIndex,DetectedItem in pairs(DetectedItems)do +self:F({DetectedItemIndex,DetectedItem}) +local DetectedItemReportSummary=self.Detection:DetectedItemReportSummary(DetectedItem,EscortGroupData.EscortGroup,_DATABASE:GetPlayerSettings(self.EscortClient:GetPlayerName())) +if ClientEscortGroupName==EscortGroupName then +local DetectedMsg=DetectedItemReportSummary:Text("\n") +DetectedMsgs[#DetectedMsgs+1]=DetectedMsg +self:T(DetectedMsg) +MENU_GROUP_COMMAND:New(self.EscortClient:GetGroup(), +DetectedMsg, +self.EscortMenuAttackNearbyTargets, +ESCORT._AttackTarget, +self, +DetectedItem +) +else +if self.EscortMenuTargetAssistance then +local DetectedMsg=DetectedItemReportSummary:Text("\n") +self:T(DetectedMsg) +local MenuTargetAssistance=MENU_GROUP:New(self.EscortClient:GetGroup(),EscortGroupData.EscortName,self.EscortMenuTargetAssistance) +MENU_GROUP_COMMAND:New(self.EscortClient:GetGroup(), +DetectedMsg, +MenuTargetAssistance, +ESCORT._AssistTarget, +self, +EscortGroupData.EscortGroup, +DetectedItem +) +end +end +DetectedTargets=true +end +end +self:F(DetectedMsgs) +if DetectedTargets then +self.EscortGroup:MessageToClient("Reporting detected targets:\n"..table.concat(DetectedMsgs,"\n"),20,self.EscortClient) +else +self.EscortGroup:MessageToClient("No targets detected.",10,self.EscortClient) +end +return true +else +end +end +return false +end +MISSILETRAINER={ +ClassName="MISSILETRAINER", +TrackingMissiles={}, +} +function MISSILETRAINER._Alive(Client,self) +if self.Briefing then +Client:Message(self.Briefing,15,"Trainer") +end +if self.MenusOnOff==true then +Client:Message("Use the 'Radio Menu' -> 'Other (F10)' -> 'Missile Trainer' menu options to change the Missile Trainer settings (for all players).",15,"Trainer") +Client.MainMenu=MENU_GROUP:New(Client:GetGroup(),"Missile Trainer",nil) +Client.MenuMessages=MENU_GROUP:New(Client:GetGroup(),"Messages",Client.MainMenu) +Client.MenuOn=MENU_GROUP_COMMAND:New(Client:GetGroup(),"Messages On",Client.MenuMessages,self._MenuMessages,{MenuSelf=self,MessagesOnOff=true}) +Client.MenuOff=MENU_GROUP_COMMAND:New(Client:GetGroup(),"Messages Off",Client.MenuMessages,self._MenuMessages,{MenuSelf=self,MessagesOnOff=false}) +Client.MenuTracking=MENU_GROUP:New(Client:GetGroup(),"Tracking",Client.MainMenu) +Client.MenuTrackingToAll=MENU_GROUP_COMMAND:New(Client:GetGroup(),"To All",Client.MenuTracking,self._MenuMessages,{MenuSelf=self,TrackingToAll=true}) +Client.MenuTrackingToTarget=MENU_GROUP_COMMAND:New(Client:GetGroup(),"To Target",Client.MenuTracking,self._MenuMessages,{MenuSelf=self,TrackingToAll=false}) +Client.MenuTrackOn=MENU_GROUP_COMMAND:New(Client:GetGroup(),"Tracking On",Client.MenuTracking,self._MenuMessages,{MenuSelf=self,TrackingOnOff=true}) +Client.MenuTrackOff=MENU_GROUP_COMMAND:New(Client:GetGroup(),"Tracking Off",Client.MenuTracking,self._MenuMessages,{MenuSelf=self,TrackingOnOff=false}) +Client.MenuTrackIncrease=MENU_GROUP_COMMAND:New(Client:GetGroup(),"Frequency Increase",Client.MenuTracking,self._MenuMessages,{MenuSelf=self,TrackingFrequency=-1}) +Client.MenuTrackDecrease=MENU_GROUP_COMMAND:New(Client:GetGroup(),"Frequency Decrease",Client.MenuTracking,self._MenuMessages,{MenuSelf=self,TrackingFrequency=1}) +Client.MenuAlerts=MENU_GROUP:New(Client:GetGroup(),"Alerts",Client.MainMenu) +Client.MenuAlertsToAll=MENU_GROUP_COMMAND:New(Client:GetGroup(),"To All",Client.MenuAlerts,self._MenuMessages,{MenuSelf=self,AlertsToAll=true}) +Client.MenuAlertsToTarget=MENU_GROUP_COMMAND:New(Client:GetGroup(),"To Target",Client.MenuAlerts,self._MenuMessages,{MenuSelf=self,AlertsToAll=false}) +Client.MenuHitsOn=MENU_GROUP_COMMAND:New(Client:GetGroup(),"Hits On",Client.MenuAlerts,self._MenuMessages,{MenuSelf=self,AlertsHitsOnOff=true}) +Client.MenuHitsOff=MENU_GROUP_COMMAND:New(Client:GetGroup(),"Hits Off",Client.MenuAlerts,self._MenuMessages,{MenuSelf=self,AlertsHitsOnOff=false}) +Client.MenuLaunchesOn=MENU_GROUP_COMMAND:New(Client:GetGroup(),"Launches On",Client.MenuAlerts,self._MenuMessages,{MenuSelf=self,AlertsLaunchesOnOff=true}) +Client.MenuLaunchesOff=MENU_GROUP_COMMAND:New(Client:GetGroup(),"Launches Off",Client.MenuAlerts,self._MenuMessages,{MenuSelf=self,AlertsLaunchesOnOff=false}) +Client.MenuDetails=MENU_GROUP:New(Client:GetGroup(),"Details",Client.MainMenu) +Client.MenuDetailsDistanceOn=MENU_GROUP_COMMAND:New(Client:GetGroup(),"Range On",Client.MenuDetails,self._MenuMessages,{MenuSelf=self,DetailsRangeOnOff=true}) +Client.MenuDetailsDistanceOff=MENU_GROUP_COMMAND:New(Client:GetGroup(),"Range Off",Client.MenuDetails,self._MenuMessages,{MenuSelf=self,DetailsRangeOnOff=false}) +Client.MenuDetailsBearingOn=MENU_GROUP_COMMAND:New(Client:GetGroup(),"Bearing On",Client.MenuDetails,self._MenuMessages,{MenuSelf=self,DetailsBearingOnOff=true}) +Client.MenuDetailsBearingOff=MENU_GROUP_COMMAND:New(Client:GetGroup(),"Bearing Off",Client.MenuDetails,self._MenuMessages,{MenuSelf=self,DetailsBearingOnOff=false}) +Client.MenuDistance=MENU_GROUP:New(Client:GetGroup(),"Set distance to plane",Client.MainMenu) +Client.MenuDistance50=MENU_GROUP_COMMAND:New(Client:GetGroup(),"50 meter",Client.MenuDistance,self._MenuMessages,{MenuSelf=self,Distance=50/1000}) +Client.MenuDistance100=MENU_GROUP_COMMAND:New(Client:GetGroup(),"100 meter",Client.MenuDistance,self._MenuMessages,{MenuSelf=self,Distance=100/1000}) +Client.MenuDistance150=MENU_GROUP_COMMAND:New(Client:GetGroup(),"150 meter",Client.MenuDistance,self._MenuMessages,{MenuSelf=self,Distance=150/1000}) +Client.MenuDistance200=MENU_GROUP_COMMAND:New(Client:GetGroup(),"200 meter",Client.MenuDistance,self._MenuMessages,{MenuSelf=self,Distance=200/1000}) +else +if Client.MainMenu then +Client.MainMenu:Remove() +end +end +local ClientID=Client:GetID() +self:T(ClientID) +if not self.TrackingMissiles[ClientID]then +self.TrackingMissiles[ClientID]={} +end +self.TrackingMissiles[ClientID].Client=Client +if not self.TrackingMissiles[ClientID].MissileData then +self.TrackingMissiles[ClientID].MissileData={} +end +end +function MISSILETRAINER:New(Distance,Briefing) +local self=BASE:Inherit(self,BASE:New()) +self:F(Distance) +if Briefing then +self.Briefing=Briefing +end +self.Schedulers={} +self.SchedulerID=0 +self.MessageInterval=2 +self.MessageLastTime=timer.getTime() +self.Distance=Distance/1000 +self:HandleEvent(EVENTS.Shot) +self.DBClients=SET_CLIENT:New():FilterStart() +self.DBClients:ForEachClient( +function(Client) +self:F("ForEach:"..Client.UnitName) +Client:Alive(self._Alive,self) +end +) +self.MessagesOnOff=true +self.TrackingToAll=false +self.TrackingOnOff=true +self.TrackingFrequency=3 +self.AlertsToAll=true +self.AlertsHitsOnOff=true +self.AlertsLaunchesOnOff=true +self.DetailsRangeOnOff=true +self.DetailsBearingOnOff=true +self.MenusOnOff=true +self.TrackingMissiles={} +self.TrackingScheduler=SCHEDULER:New(self,self._TrackMissiles,{},0.5,0.05,0) +return self +end +function MISSILETRAINER:InitMessagesOnOff(MessagesOnOff) +self:F(MessagesOnOff) +self.MessagesOnOff=MessagesOnOff +if self.MessagesOnOff==true then +MESSAGE:New("Messages ON",15,"Menu"):ToAll() +else +MESSAGE:New("Messages OFF",15,"Menu"):ToAll() +end +return self +end +function MISSILETRAINER:InitTrackingToAll(TrackingToAll) +self:F(TrackingToAll) +self.TrackingToAll=TrackingToAll +if self.TrackingToAll==true then +MESSAGE:New("Missile tracking to all players ON",15,"Menu"):ToAll() +else +MESSAGE:New("Missile tracking to all players OFF",15,"Menu"):ToAll() +end +return self +end +function MISSILETRAINER:InitTrackingOnOff(TrackingOnOff) +self:F(TrackingOnOff) +self.TrackingOnOff=TrackingOnOff +if self.TrackingOnOff==true then +MESSAGE:New("Missile tracking ON",15,"Menu"):ToAll() +else +MESSAGE:New("Missile tracking OFF",15,"Menu"):ToAll() +end +return self +end +function MISSILETRAINER:InitTrackingFrequency(TrackingFrequency) +self:F(TrackingFrequency) +self.TrackingFrequency=self.TrackingFrequency+TrackingFrequency +if self.TrackingFrequency<0.5 then +self.TrackingFrequency=0.5 +end +if self.TrackingFrequency then +MESSAGE:New("Missile tracking frequency is "..self.TrackingFrequency.." seconds.",15,"Menu"):ToAll() +end +return self +end +function MISSILETRAINER:InitAlertsToAll(AlertsToAll) +self:F(AlertsToAll) +self.AlertsToAll=AlertsToAll +if self.AlertsToAll==true then +MESSAGE:New("Alerts to all players ON",15,"Menu"):ToAll() +else +MESSAGE:New("Alerts to all players OFF",15,"Menu"):ToAll() +end +return self +end +function MISSILETRAINER:InitAlertsHitsOnOff(AlertsHitsOnOff) +self:F(AlertsHitsOnOff) +self.AlertsHitsOnOff=AlertsHitsOnOff +if self.AlertsHitsOnOff==true then +MESSAGE:New("Alerts Hits ON",15,"Menu"):ToAll() +else +MESSAGE:New("Alerts Hits OFF",15,"Menu"):ToAll() +end +return self +end +function MISSILETRAINER:InitAlertsLaunchesOnOff(AlertsLaunchesOnOff) +self:F(AlertsLaunchesOnOff) +self.AlertsLaunchesOnOff=AlertsLaunchesOnOff +if self.AlertsLaunchesOnOff==true then +MESSAGE:New("Alerts Launches ON",15,"Menu"):ToAll() +else +MESSAGE:New("Alerts Launches OFF",15,"Menu"):ToAll() +end +return self +end +function MISSILETRAINER:InitRangeOnOff(DetailsRangeOnOff) +self:F(DetailsRangeOnOff) +self.DetailsRangeOnOff=DetailsRangeOnOff +if self.DetailsRangeOnOff==true then +MESSAGE:New("Range display ON",15,"Menu"):ToAll() +else +MESSAGE:New("Range display OFF",15,"Menu"):ToAll() +end +return self +end +function MISSILETRAINER:InitBearingOnOff(DetailsBearingOnOff) +self:F(DetailsBearingOnOff) +self.DetailsBearingOnOff=DetailsBearingOnOff +if self.DetailsBearingOnOff==true then +MESSAGE:New("Bearing display OFF",15,"Menu"):ToAll() +else +MESSAGE:New("Bearing display OFF",15,"Menu"):ToAll() +end +return self +end +function MISSILETRAINER:InitMenusOnOff(MenusOnOff) +self:F(MenusOnOff) +self.MenusOnOff=MenusOnOff +if self.MenusOnOff==true then +MESSAGE:New("Menus are ENABLED (only when a player rejoins a slot)",15,"Menu"):ToAll() +else +MESSAGE:New("Menus are DISABLED",15,"Menu"):ToAll() +end +return self +end +function MISSILETRAINER._MenuMessages(MenuParameters) +local self=MenuParameters.MenuSelf +if MenuParameters.MessagesOnOff~=nil then +self:InitMessagesOnOff(MenuParameters.MessagesOnOff) +end +if MenuParameters.TrackingToAll~=nil then +self:InitTrackingToAll(MenuParameters.TrackingToAll) +end +if MenuParameters.TrackingOnOff~=nil then +self:InitTrackingOnOff(MenuParameters.TrackingOnOff) +end +if MenuParameters.TrackingFrequency~=nil then +self:InitTrackingFrequency(MenuParameters.TrackingFrequency) +end +if MenuParameters.AlertsToAll~=nil then +self:InitAlertsToAll(MenuParameters.AlertsToAll) +end +if MenuParameters.AlertsHitsOnOff~=nil then +self:InitAlertsHitsOnOff(MenuParameters.AlertsHitsOnOff) +end +if MenuParameters.AlertsLaunchesOnOff~=nil then +self:InitAlertsLaunchesOnOff(MenuParameters.AlertsLaunchesOnOff) +end +if MenuParameters.DetailsRangeOnOff~=nil then +self:InitRangeOnOff(MenuParameters.DetailsRangeOnOff) +end +if MenuParameters.DetailsBearingOnOff~=nil then +self:InitBearingOnOff(MenuParameters.DetailsBearingOnOff) +end +if MenuParameters.Distance~=nil then +self.Distance=MenuParameters.Distance +MESSAGE:New("Hit detection distance set to "..(self.Distance*1000).." meters",15,"Menu"):ToAll() +end +end +function MISSILETRAINER:OnEventShot(EVentData) +self:F({EVentData}) +local TrainerSourceDCSUnit=EVentData.IniDCSUnit +local TrainerSourceDCSUnitName=EVentData.IniDCSUnitName +local TrainerWeapon=EVentData.Weapon +local TrainerWeaponName=EVentData.WeaponName +self:T("Missile Launched = "..TrainerWeaponName) +local TrainerTargetDCSUnit=TrainerWeapon:getTarget() +if TrainerTargetDCSUnit then +local TrainerTargetDCSUnitName=Unit.getName(TrainerTargetDCSUnit) +local TrainerTargetSkill=_DATABASE.Templates.Units[TrainerTargetDCSUnitName].Template.skill +self:T(TrainerTargetDCSUnitName) +local Client=self.DBClients:FindClient(TrainerTargetDCSUnitName) +if Client then +local TrainerSourceUnit=UNIT:Find(TrainerSourceDCSUnit) +local TrainerTargetUnit=UNIT:Find(TrainerTargetDCSUnit) +if self.MessagesOnOff==true and self.AlertsLaunchesOnOff==true then +local Message=MESSAGE:New( +string.format("%s launched a %s", +TrainerSourceUnit:GetTypeName(), +TrainerWeaponName +)..self:_AddRange(Client,TrainerWeapon)..self:_AddBearing(Client,TrainerWeapon),5,"Launch Alert") +if self.AlertsToAll then +Message:ToAll() +else +Message:ToClient(Client) +end +end +local ClientID=Client:GetID() +self:T(ClientID) +local MissileData={} +MissileData.TrainerSourceUnit=TrainerSourceUnit +MissileData.TrainerWeapon=TrainerWeapon +MissileData.TrainerTargetUnit=TrainerTargetUnit +MissileData.TrainerWeaponTypeName=TrainerWeapon:getTypeName() +MissileData.TrainerWeaponLaunched=true +table.insert(self.TrackingMissiles[ClientID].MissileData,MissileData) +end +else +if(TrainerWeapon:getTypeName()=="9M311")then +SCHEDULER:New(TrainerWeapon,TrainerWeapon.destroy,{},1) +else +end +end +end +function MISSILETRAINER:_AddRange(Client,TrainerWeapon) +local RangeText="" +if self.DetailsRangeOnOff then +local PositionMissile=TrainerWeapon:getPoint() +local TargetVec3=Client:GetVec3() +local Range=((PositionMissile.x-TargetVec3.x)^2+ +(PositionMissile.y-TargetVec3.y)^2+ +(PositionMissile.z-TargetVec3.z)^2 +)^0.5/1000 +RangeText=string.format(", at %4.2fkm",Range) +end +return RangeText +end +function MISSILETRAINER:_AddBearing(Client,TrainerWeapon) +local BearingText="" +if self.DetailsBearingOnOff then +local PositionMissile=TrainerWeapon:getPoint() +local TargetVec3=Client:GetVec3() +self:T2({TargetVec3,PositionMissile}) +local DirectionVector={x=PositionMissile.x-TargetVec3.x,y=PositionMissile.y-TargetVec3.y,z=PositionMissile.z-TargetVec3.z} +local DirectionRadians=math.atan2(DirectionVector.z,DirectionVector.x) +if DirectionRadians<0 then +DirectionRadians=DirectionRadians+2*math.pi +end +local DirectionDegrees=DirectionRadians*180/math.pi +BearingText=string.format(", %d degrees",DirectionDegrees) +end +return BearingText +end +function MISSILETRAINER:_TrackMissiles() +self:F2() +local ShowMessages=false +if self.MessagesOnOff and self.MessageLastTime+self.TrackingFrequency<=timer.getTime()then +self.MessageLastTime=timer.getTime() +ShowMessages=true +end +for ClientDataID,ClientData in pairs(self.TrackingMissiles)do +local Client=ClientData.Client +if Client and Client:IsAlive()then +for MissileDataID,MissileData in pairs(ClientData.MissileData)do +self:T3(MissileDataID) +local TrainerSourceUnit=MissileData.TrainerSourceUnit +local TrainerWeapon=MissileData.TrainerWeapon +local TrainerTargetUnit=MissileData.TrainerTargetUnit +local TrainerWeaponTypeName=MissileData.TrainerWeaponTypeName +local TrainerWeaponLaunched=MissileData.TrainerWeaponLaunched +if Client and Client:IsAlive()and TrainerSourceUnit and TrainerSourceUnit:IsAlive()and TrainerWeapon and TrainerWeapon:isExist()and TrainerTargetUnit and TrainerTargetUnit:IsAlive()then +local PositionMissile=TrainerWeapon:getPosition().p +local TargetVec3=Client:GetVec3() +local Distance=((PositionMissile.x-TargetVec3.x)^2+ +(PositionMissile.y-TargetVec3.y)^2+ +(PositionMissile.z-TargetVec3.z)^2 +)^0.5/1000 +if Distance<=self.Distance then +TrainerWeapon:destroy() +if self.MessagesOnOff==true and self.AlertsHitsOnOff==true then +self:T("killed") +local Message=MESSAGE:New( +string.format("%s launched by %s killed %s", +TrainerWeapon:getTypeName(), +TrainerSourceUnit:GetTypeName(), +TrainerTargetUnit:GetPlayerName() +),15,"Hit Alert") +if self.AlertsToAll==true then +Message:ToAll() +else +Message:ToClient(Client) +end +MissileData=nil +table.remove(ClientData.MissileData,MissileDataID) +self:T(ClientData.MissileData) +end +end +else +if not(TrainerWeapon and TrainerWeapon:isExist())then +if self.MessagesOnOff==true and self.AlertsLaunchesOnOff==true then +local Message=MESSAGE:New( +string.format("%s launched by %s self destructed!", +TrainerWeaponTypeName, +TrainerSourceUnit:GetTypeName() +),5,"Tracking") +if self.AlertsToAll==true then +Message:ToAll() +else +Message:ToClient(Client) +end +end +MissileData=nil +table.remove(ClientData.MissileData,MissileDataID) +self:T(ClientData.MissileData) +end +end +end +else +self.TrackingMissiles[ClientDataID]=nil +end +end +if ShowMessages==true and self.MessagesOnOff==true and self.TrackingOnOff==true then +for ClientDataID,ClientData in pairs(self.TrackingMissiles)do +local Client=ClientData.Client +ClientData.MessageToClient="" +ClientData.MessageToAll="" +for TrackingDataID,TrackingData in pairs(self.TrackingMissiles)do +for MissileDataID,MissileData in pairs(TrackingData.MissileData)do +local TrainerSourceUnit=MissileData.TrainerSourceUnit +local TrainerWeapon=MissileData.TrainerWeapon +local TrainerTargetUnit=MissileData.TrainerTargetUnit +local TrainerWeaponTypeName=MissileData.TrainerWeaponTypeName +local TrainerWeaponLaunched=MissileData.TrainerWeaponLaunched +if Client and Client:IsAlive()and TrainerSourceUnit and TrainerSourceUnit:IsAlive()and TrainerWeapon and TrainerWeapon:isExist()and TrainerTargetUnit and TrainerTargetUnit:IsAlive()then +if ShowMessages==true then +local TrackingTo +TrackingTo=string.format(" -> %s", +TrainerWeaponTypeName +) +if ClientDataID==TrackingDataID then +if ClientData.MessageToClient==""then +ClientData.MessageToClient="Missiles to You:\n" +end +ClientData.MessageToClient=ClientData.MessageToClient..TrackingTo..self:_AddRange(ClientData.Client,TrainerWeapon)..self:_AddBearing(ClientData.Client,TrainerWeapon).."\n" +else +if self.TrackingToAll==true then +if ClientData.MessageToAll==""then +ClientData.MessageToAll="Missiles to other Players:\n" +end +ClientData.MessageToAll=ClientData.MessageToAll..TrackingTo..self:_AddRange(ClientData.Client,TrainerWeapon)..self:_AddBearing(ClientData.Client,TrainerWeapon).." ( "..TrainerTargetUnit:GetPlayerName().." )\n" +end +end +end +end +end +end +if ClientData.MessageToClient~=""or ClientData.MessageToAll~=""then +local Message=MESSAGE:New(ClientData.MessageToClient..ClientData.MessageToAll,1,"Tracking"):ToClient(Client) +end +end +end +return true +end +ATC_GROUND={ +ClassName="ATC_GROUND", +SetClient=nil, +Airbases=nil, +AirbaseNames=nil, +} +function ATC_GROUND:New(Airbases,AirbaseList) +local self=BASE:Inherit(self,BASE:New()) +self:E({self.ClassName,Airbases}) +self.Airbases=Airbases +self.AirbaseList=AirbaseList +self.SetClient=SET_CLIENT:New():FilterCategories("plane"):FilterStart() +for AirbaseID,Airbase in pairs(self.Airbases)do +Airbase.ZoneBoundary=_DATABASE:FindAirbase(AirbaseID):GetZone() +Airbase.ZoneRunways={} +for PointsRunwayID,PointsRunway in pairs(Airbase.PointsRunways)do +Airbase.ZoneRunways[PointsRunwayID]=ZONE_POLYGON_BASE:New("Runway "..PointsRunwayID,PointsRunway) +end +Airbase.Monitor=self.AirbaseList and false or true +end +for AirbaseID,AirbaseName in pairs(self.AirbaseList or{})do +self.Airbases[AirbaseName].Monitor=true +end +self.SetClient:ForEachClient( +function(Client) +Client:SetState(self,"Speeding",false) +Client:SetState(self,"Warnings",0) +Client:SetState(self,"IsOffRunway",false) +Client:SetState(self,"OffRunwayWarnings",0) +Client:SetState(self,"Taxi",false) +end +) +SSB=USERFLAG:New("SSB") +SSB:Set(100) +return self +end +function ATC_GROUND:SmokeRunways(SmokeColor) +for AirbaseID,Airbase in pairs(self.Airbases)do +for PointsRunwayID,PointsRunway in pairs(Airbase.PointsRunways)do +Airbase.ZoneRunways[PointsRunwayID]:SmokeZone(SmokeColor) +end +end +end +function ATC_GROUND:SetKickSpeed(KickSpeed,Airbase) +if not Airbase then +self.KickSpeed=KickSpeed +else +self.Airbases[Airbase].KickSpeed=KickSpeed +end +return self +end +function ATC_GROUND:SetKickSpeedKmph(KickSpeed,Airbase) +self:SetKickSpeed(UTILS.KmphToMps(KickSpeed),Airbase) +return self +end +function ATC_GROUND:SetKickSpeedMiph(KickSpeedMiph,Airbase) +self:SetKickSpeed(UTILS.MiphToMps(KickSpeedMiph),Airbase) +return self +end +function ATC_GROUND:SetMaximumKickSpeed(MaximumKickSpeed,Airbase) +if not Airbase then +self.MaximumKickSpeed=MaximumKickSpeed +else +self.Airbases[Airbase].MaximumKickSpeed=MaximumKickSpeed +end +return self +end +function ATC_GROUND:SetMaximumKickSpeedKmph(MaximumKickSpeed,Airbase) +self:SetMaximumKickSpeed(UTILS.KmphToMps(MaximumKickSpeed),Airbase) +return self +end +function ATC_GROUND:SetMaximumKickSpeedMiph(MaximumKickSpeedMiph,Airbase) +self:SetMaximumKickSpeed(UTILS.MiphToMps(MaximumKickSpeedMiph),Airbase) +return self +end +function ATC_GROUND:_AirbaseMonitor() +self.SetClient:ForEachClient( +function(Client) +if Client:IsAlive()then +local IsOnGround=Client:InAir()==false +for AirbaseID,AirbaseMeta in pairs(self.Airbases)do +self:E(AirbaseID,AirbaseMeta.KickSpeed) +if AirbaseMeta.Monitor==true and Client:IsInZone(AirbaseMeta.ZoneBoundary)then +local NotInRunwayZone=true +for ZoneRunwayID,ZoneRunway in pairs(AirbaseMeta.ZoneRunways)do +NotInRunwayZone=(Client:IsNotInZone(ZoneRunway)==true)and NotInRunwayZone or false +end +if NotInRunwayZone then +if IsOnGround then +local Taxi=Client:GetState(self,"Taxi") +self:E(Taxi) +if Taxi==false then +local Velocity=VELOCITY:New(AirbaseMeta.KickSpeed or self.KickSpeed) +Client:Message("Welcome to "..AirbaseID..". The maximum taxiing speed is ".. +Velocity:ToString(),20,"ATC") +Client:SetState(self,"Taxi",true) +end +local Velocity=VELOCITY_POSITIONABLE:New(Client) +local IsAboveRunway=Client:IsAboveRunway() +self:T(IsAboveRunway,IsOnGround) +if IsOnGround then +local Speeding=false +if AirbaseMeta.MaximumKickSpeed then +if Velocity:Get()>AirbaseMeta.MaximumKickSpeed then +Speeding=true +end +else +if Velocity:Get()>self.MaximumKickSpeed then +Speeding=true +end +end +if Speeding==true then +MESSAGE:New("Penalty! Player "..Client:GetPlayerName().. +" has been kicked, due to a severe airbase traffic rule violation ...",10,"ATC"):ToAll() +Client:Destroy() +Client:SetState(self,"Speeding",false) +Client:SetState(self,"Warnings",0) +end +end +if IsOnGround then +local Speeding=false +if AirbaseMeta.KickSpeed then +if Velocity:Get()>AirbaseMeta.KickSpeed then +Speeding=true +end +else +if Velocity:Get()>self.KickSpeed then +Speeding=true +end +end +if Speeding==true then +local IsSpeeding=Client:GetState(self,"Speeding") +if IsSpeeding==true then +local SpeedingWarnings=Client:GetState(self,"Warnings") +self:T(SpeedingWarnings) +if SpeedingWarnings<=3 then +Client:Message("Warning "..SpeedingWarnings.."/3! Airbase traffic rule violation! Slow down now! Your speed is ".. +Velocity:ToString(),5,"ATC") +Client:SetState(self,"Warnings",SpeedingWarnings+1) +else +MESSAGE:New("Penalty! Player "..Client:GetPlayerName().." has been kicked, due to a severe airbase traffic rule violation ...",10,"ATC"):ToAll() +Client:Destroy() +Client:SetState(self,"Speeding",false) +Client:SetState(self,"Warnings",0) +end +else +Client:Message("Attention! You are speeding on the taxiway, slow down! Your speed is ".. +Velocity:ToString(),5,"ATC") +Client:SetState(self,"Speeding",true) +Client:SetState(self,"Warnings",1) +end +else +Client:SetState(self,"Speeding",false) +Client:SetState(self,"Warnings",0) +end +end +if IsOnGround and not IsAboveRunway then +local IsOffRunway=Client:GetState(self,"IsOffRunway") +if IsOffRunway==true then +local OffRunwayWarnings=Client:GetState(self,"OffRunwayWarnings") +self:T(OffRunwayWarnings) +if OffRunwayWarnings<=3 then +Client:Message("Warning "..OffRunwayWarnings.."/3! Airbase traffic rule violation! Get back on the taxi immediately!",5,"ATC") +Client:SetState(self,"OffRunwayWarnings",OffRunwayWarnings+1) +else +MESSAGE:New("Penalty! Player "..Client:GetPlayerName().." has been kicked, due to a severe airbase traffic rule violation ...",10,"ATC"):ToAll() +Client:Destroy() +Client:SetState(self,"IsOffRunway",false) +Client:SetState(self,"OffRunwayWarnings",0) +end +else +Client:Message("Attention! You are off the taxiway. Get back on the taxiway immediately!",5,"ATC") +Client:SetState(self,"IsOffRunway",true) +Client:SetState(self,"OffRunwayWarnings",1) +end +else +Client:SetState(self,"IsOffRunway",false) +Client:SetState(self,"OffRunwayWarnings",0) +end +end +else +Client:SetState(self,"Speeding",false) +Client:SetState(self,"Warnings",0) +Client:SetState(self,"IsOffRunway",false) +Client:SetState(self,"OffRunwayWarnings",0) +local Taxi=Client:GetState(self,"Taxi") +if Taxi==true then +Client:Message("You have progressed to the runway ... Await take-off clearance ...",20,"ATC") +Client:SetState(self,"Taxi",false) +end +end +end +end +else +Client:SetState(self,"Taxi",false) +end +end +) +return true +end +ATC_GROUND_CAUCASUS={ +ClassName="ATC_GROUND_CAUCASUS", +Airbases={ +[AIRBASE.Caucasus.Anapa_Vityazevo]={ +PointsRunways={ +[1]={ +[1]={["y"]=242140.57142858,["x"]=-6478.8571428583,}, +[2]={["y"]=242188.57142858,["x"]=-6522.0000000011,}, +[3]={["y"]=244124.2857143,["x"]=-4344.0000000011,}, +[4]={["y"]=244068.2857143,["x"]=-4296.5714285726,}, +[5]={["y"]=242140.57142858,["x"]=-6480.0000000011,} +}, +}, +}, +[AIRBASE.Caucasus.Batumi]={ +PointsRunways={ +[1]={ +[1]={["y"]=616442.28571429,["x"]=-355090.28571429,}, +[2]={["y"]=618450.57142857,["x"]=-356522,}, +[3]={["y"]=618407.71428571,["x"]=-356584.85714286,}, +[4]={["y"]=618361.99999999,["x"]=-356554.85714286,}, +[5]={["y"]=618324.85714285,["x"]=-356599.14285715,}, +[6]={["y"]=618250.57142856,["x"]=-356543.42857143,}, +[7]={["y"]=618257.7142857,["x"]=-356496.28571429,}, +[8]={["y"]=618237.7142857,["x"]=-356459.14285715,}, +[9]={["y"]=616555.71428571,["x"]=-355258.85714286,}, +[10]={["y"]=616486.28571428,["x"]=-355280.57142858,}, +[11]={["y"]=616410.57142856,["x"]=-355227.71428572,}, +[12]={["y"]=616441.99999999,["x"]=-355179.14285715,}, +[13]={["y"]=616401.99999999,["x"]=-355147.71428572,}, +[14]={["y"]=616441.42857142,["x"]=-355092.57142858,}, +}, +}, +}, +[AIRBASE.Caucasus.Beslan]={ +PointsRunways={ +[1]={ +[1]={["y"]=842104.57142857,["x"]=-148460.57142857,}, +[2]={["y"]=845225.71428572,["x"]=-148656,}, +[3]={["y"]=845220.57142858,["x"]=-148750,}, +[4]={["y"]=842098.85714286,["x"]=-148556.28571429,}, +[5]={["y"]=842104,["x"]=-148460.28571429,}, +}, +}, +}, +[AIRBASE.Caucasus.Gelendzhik]={ +PointsRunways={ +[1]={ +[1]={["y"]=297834.00000001,["x"]=-51107.428571429,}, +[2]={["y"]=297786.57142858,["x"]=-51068.857142858,}, +[3]={["y"]=298946.57142858,["x"]=-49686.000000001,}, +[4]={["y"]=298993.14285715,["x"]=-49725.714285715,}, +[5]={["y"]=297835.14285715,["x"]=-51107.714285715,}, +}, +}, +}, +[AIRBASE.Caucasus.Gudauta]={ +PointsRunways={ +[1]={ +[1]={["y"]=517096.57142857,["x"]=-197804.57142857,}, +[2]={["y"]=515880.85714285,["x"]=-195590.28571429,}, +[3]={["y"]=515812.28571428,["x"]=-195628.85714286,}, +[4]={["y"]=517036.57142857,["x"]=-197834.57142857,}, +[5]={["y"]=517097.99999999,["x"]=-197807.42857143,}, +}, +}, +}, +[AIRBASE.Caucasus.Kobuleti]={ +PointsRunways={ +[1]={ +[1]={["y"]=634509.71428571,["x"]=-318339.42857144,}, +[2]={["y"]=636767.42857143,["x"]=-317516.57142858,}, +[3]={["y"]=636790,["x"]=-317575.71428572,}, +[4]={["y"]=634531.42857143,["x"]=-318398.00000001,}, +[5]={["y"]=634510.28571429,["x"]=-318339.71428572,}, +}, +}, +}, +[AIRBASE.Caucasus.Krasnodar_Center]={ +PointsRunways={ +[1]={ +[1]={["y"]=369205.42857144,["x"]=11789.142857142,}, +[2]={["y"]=369209.71428572,["x"]=11714.857142856,}, +[3]={["y"]=366699.71428572,["x"]=11581.714285713,}, +[4]={["y"]=366698.28571429,["x"]=11659.142857142,}, +[5]={["y"]=369208.85714286,["x"]=11788.57142857,}, +}, +}, +}, +[AIRBASE.Caucasus.Krasnodar_Pashkovsky]={ +PointsRunways={ +[1]={ +[1]={["y"]=385891.14285715,["x"]=8416.5714285703,}, +[2]={["y"]=385842.28571429,["x"]=8467.9999999989,}, +[3]={["y"]=384180.85714286,["x"]=6917.1428571417,}, +[4]={["y"]=384228.57142858,["x"]=6867.7142857132,}, +[5]={["y"]=385891.14285715,["x"]=8416.5714285703,}, +}, +[2]={ +[1]={["y"]=386714.85714286,["x"]=6674.857142856,}, +[2]={["y"]=386757.71428572,["x"]=6627.7142857132,}, +[3]={["y"]=389028.57142858,["x"]=8741.4285714275,}, +[4]={["y"]=388981.71428572,["x"]=8790.5714285703,}, +[5]={["y"]=386714.57142858,["x"]=6674.5714285703,}, +}, +}, +}, +[AIRBASE.Caucasus.Krymsk]={ +PointsRunways={ +[1]={ +[1]={["y"]=293522.00000001,["x"]=-7567.4285714297,}, +[2]={["y"]=293578.57142858,["x"]=-7616.0000000011,}, +[3]={["y"]=295246.00000001,["x"]=-5591.142857144,}, +[4]={["y"]=295187.71428573,["x"]=-5546.0000000011,}, +[5]={["y"]=293523.14285715,["x"]=-7568.2857142868,}, +}, +}, +}, +[AIRBASE.Caucasus.Kutaisi]={ +PointsRunways={ +[1]={ +[1]={["y"]=682638,["x"]=-285202.28571429,}, +[2]={["y"]=685050.28571429,["x"]=-284507.42857144,}, +[3]={["y"]=685068.85714286,["x"]=-284578.85714286,}, +[4]={["y"]=682657.42857143,["x"]=-285264.28571429,}, +[5]={["y"]=682638.28571429,["x"]=-285202.85714286,}, +}, +}, +}, +[AIRBASE.Caucasus.Maykop_Khanskaya]={ +PointsRunways={ +[1]={ +[1]={["y"]=457005.42857143,["x"]=-27668.000000001,}, +[2]={["y"]=459028.85714286,["x"]=-25168.857142858,}, +[3]={["y"]=459082.57142857,["x"]=-25216.857142858,}, +[4]={["y"]=457060,["x"]=-27714.285714287,}, +[5]={["y"]=457004.57142857,["x"]=-27669.714285715,}, +}, +}, +}, +[AIRBASE.Caucasus.Mineralnye_Vody]={ +PointsRunways={ +[1]={ +[1]={["y"]=703904,["x"]=-50352.571428573,}, +[2]={["y"]=707596.28571429,["x"]=-52094.571428573,}, +[3]={["y"]=707560.57142858,["x"]=-52161.714285716,}, +[4]={["y"]=703871.71428572,["x"]=-50420.571428573,}, +[5]={["y"]=703902,["x"]=-50352.000000002,}, +}, +}, +}, +[AIRBASE.Caucasus.Mozdok]={ +PointsRunways={ +[1]={ +[1]={["y"]=832201.14285715,["x"]=-83699.428571431,}, +[2]={["y"]=832212.57142857,["x"]=-83780.571428574,}, +[3]={["y"]=835730.28571429,["x"]=-83335.714285717,}, +[4]={["y"]=835718.85714286,["x"]=-83246.571428574,}, +[5]={["y"]=832200.57142857,["x"]=-83700.000000002,}, +}, +}, +}, +[AIRBASE.Caucasus.Nalchik]={ +PointsRunways={ +[1]={ +[1]={["y"]=759454.28571429,["x"]=-125551.42857143,}, +[2]={["y"]=759492.85714286,["x"]=-125610.85714286,}, +[3]={["y"]=761406.28571429,["x"]=-124304.28571429,}, +[4]={["y"]=761361.14285714,["x"]=-124239.71428572,}, +[5]={["y"]=759456,["x"]=-125552.57142857,}, +}, +}, +}, +[AIRBASE.Caucasus.Novorossiysk]={ +PointsRunways={ +[1]={ +[1]={["y"]=278673.14285716,["x"]=-41615.142857144,}, +[2]={["y"]=278625.42857144,["x"]=-41570.571428572,}, +[3]={["y"]=279835.42857144,["x"]=-40226.000000001,}, +[4]={["y"]=279882.2857143,["x"]=-40270.000000001,}, +[5]={["y"]=278672.00000001,["x"]=-41614.857142858,}, +}, +}, +}, +[AIRBASE.Caucasus.Senaki_Kolkhi]={ +PointsRunways={ +[1]={ +[1]={["y"]=646060.85714285,["x"]=-281736,}, +[2]={["y"]=646056.57142857,["x"]=-281631.71428571,}, +[3]={["y"]=648442.28571428,["x"]=-281840.28571428,}, +[4]={["y"]=648432.28571428,["x"]=-281918.85714286,}, +[5]={["y"]=646063.71428571,["x"]=-281738.85714286,}, +}, +}, +}, +[AIRBASE.Caucasus.Sochi_Adler]={ +PointsRunways={ +[1]={ +[1]={["y"]=460831.42857143,["x"]=-165180,}, +[2]={["y"]=460878.57142857,["x"]=-165257.14285714,}, +[3]={["y"]=463663.71428571,["x"]=-163793.14285714,}, +[4]={["y"]=463612.28571428,["x"]=-163697.42857143,}, +[5]={["y"]=460831.42857143,["x"]=-165177.14285714,}, +}, +[2]={ +[1]={["y"]=460831.42857143,["x"]=-165180,}, +[2]={["y"]=460878.57142857,["x"]=-165257.14285714,}, +[3]={["y"]=463663.71428571,["x"]=-163793.14285714,}, +[4]={["y"]=463612.28571428,["x"]=-163697.42857143,}, +[5]={["y"]=460831.42857143,["x"]=-165177.14285714,}, +}, +}, +}, +[AIRBASE.Caucasus.Soganlug]={ +PointsRunways={ +[1]={ +[1]={["y"]=894525.71428571,["x"]=-316964,}, +[2]={["y"]=896363.14285714,["x"]=-318634.28571428,}, +[3]={["y"]=896299.14285714,["x"]=-318702.85714286,}, +[4]={["y"]=894464,["x"]=-317031.71428571,}, +[5]={["y"]=894524.57142857,["x"]=-316963.71428571,}, +}, +}, +}, +[AIRBASE.Caucasus.Sukhumi_Babushara]={ +PointsRunways={ +[1]={ +[1]={["y"]=562684,["x"]=-219779.71428571,}, +[2]={["y"]=562717.71428571,["x"]=-219718,}, +[3]={["y"]=566046.85714286,["x"]=-221376.57142857,}, +[4]={["y"]=566012.28571428,["x"]=-221446.57142857,}, +[5]={["y"]=562684.57142857,["x"]=-219782.57142857,}, +}, +}, +}, +[AIRBASE.Caucasus.Tbilisi_Lochini]={ +PointsRunways={ +[1]={ +[1]={["y"]=895261.14285715,["x"]=-314652.28571428,}, +[2]={["y"]=897654.57142857,["x"]=-316523.14285714,}, +[3]={["y"]=897711.71428571,["x"]=-316450.28571429,}, +[4]={["y"]=895327.42857143,["x"]=-314568.85714286,}, +[5]={["y"]=895261.71428572,["x"]=-314656,}, +}, +[2]={ +[1]={["y"]=895605.71428572,["x"]=-314724.57142857,}, +[2]={["y"]=897639.71428572,["x"]=-316148,}, +[3]={["y"]=897683.42857143,["x"]=-316087.14285714,}, +[4]={["y"]=895650,["x"]=-314660,}, +[5]={["y"]=895606,["x"]=-314724.85714286,} +}, +}, +}, +[AIRBASE.Caucasus.Vaziani]={ +PointsRunways={ +[1]={ +[1]={["y"]=902239.14285714,["x"]=-318190.85714286,}, +[2]={["y"]=904014.28571428,["x"]=-319994.57142857,}, +[3]={["y"]=904064.85714285,["x"]=-319945.14285715,}, +[4]={["y"]=902294.57142857,["x"]=-318146,}, +[5]={["y"]=902247.71428571,["x"]=-318190.85714286,}, +}, +}, +}, +}, +} +function ATC_GROUND_CAUCASUS:New(AirbaseNames) +local self=BASE:Inherit(self,ATC_GROUND:New(self.Airbases,AirbaseNames)) +self:SetKickSpeedKmph(50) +self:SetMaximumKickSpeedKmph(150) +return self +end +function ATC_GROUND_CAUCASUS:Start(RepeatScanSeconds) +RepeatScanSeconds=RepeatScanSeconds or 0.05 +self.AirbaseMonitor=SCHEDULER:New(self,self._AirbaseMonitor,{self},0,2,RepeatScanSeconds) +end +ATC_GROUND_NEVADA={ +ClassName="ATC_GROUND_NEVADA", +Airbases={ +[AIRBASE.Nevada.Beatty_Airport]={ +PointsRunways={ +[1]={ +[1]={["y"]=-174950.05857143,["x"]=-329679.65,}, +[2]={["y"]=-174946.53828571,["x"]=-331394.03885715,}, +[3]={["y"]=-174967.10971429,["x"]=-331394.32457143,}, +[4]={["y"]=-174971.01828571,["x"]=-329682.59171429,}, +}, +}, +}, +[AIRBASE.Nevada.Boulder_City_Airport]={ +PointsRunways={ +[1]={ +[1]={["y"]=-1317.841714286,["x"]=-429014.92857142,}, +[2]={["y"]=-951.26228571458,["x"]=-430310.21142856,}, +[3]={["y"]=-978.11942857172,["x"]=-430317.06857142,}, +[4]={["y"]=-1347.5088571432,["x"]=-429023.98485713,}, +}, +[2]={ +[1]={["y"]=-1879.955714286,["x"]=-429783.83742856,}, +[2]={["y"]=-256.25257142886,["x"]=-430023.63542856,}, +[3]={["y"]=-260.25257142886,["x"]=-430048.77828571,}, +[4]={["y"]=-1883.955714286,["x"]=-429807.83742856,}, +}, +}, +}, +[AIRBASE.Nevada.Creech_AFB]={ +PointsRunways={ +[1]={ +[1]={["y"]=-74234.729142857,["x"]=-360501.80857143,}, +[2]={["y"]=-77606.122285714,["x"]=-360417.86542857,}, +[3]={["y"]=-77608.578,["x"]=-360486.13428571,}, +[4]={["y"]=-74237.930571428,["x"]=-360586.25628571,}, +}, +[2]={ +[1]={["y"]=-75807.571428572,["x"]=-359073.42857142,}, +[2]={["y"]=-74770.142857144,["x"]=-360581.71428571,}, +[3]={["y"]=-74641.285714287,["x"]=-360585.42857142,}, +[4]={["y"]=-75734.142857144,["x"]=-359023.14285714,}, +}, +}, +}, +[AIRBASE.Nevada.Echo_Bay]={ +PointsRunways={ +[1]={ +[1]={["y"]=33182.919428572,["x"]=-388698.21657142,}, +[2]={["y"]=34202.543142857,["x"]=-388469.55485714,}, +[3]={["y"]=34207.686,["x"]=-388488.69771428,}, +[4]={["y"]=33185.422285715,["x"]=-388717.82228571,}, +}, +}, +}, +[AIRBASE.Nevada.Groom_Lake_AFB]={ +PointsRunways={ +[1]={ +[1]={["y"]=-85971.465428571,["x"]=-290567.77,}, +[2]={["y"]=-87691.155428571,["x"]=-286637.75428571,}, +[3]={["y"]=-87756.714285715,["x"]=-286663.99999999,}, +[4]={["y"]=-86035.940285714,["x"]=-290598.81314286,}, +}, +[2]={ +[1]={["y"]=-86741.547142857,["x"]=-290353.31971428,}, +[2]={["y"]=-89672.714285714,["x"]=-283546.57142855,}, +[3]={["y"]=-89772.142857143,["x"]=-283587.71428569,}, +[4]={["y"]=-86799.623714285,["x"]=-290374.16771428,}, +}, +}, +}, +[AIRBASE.Nevada.Henderson_Executive_Airport]={ +PointsRunways={ +[1]={ +[1]={["y"]=-25837.500571429,["x"]=-426404.25257142,}, +[2]={["y"]=-25843.509428571,["x"]=-428752.67942856,}, +[3]={["y"]=-25902.343714286,["x"]=-428749.96399999,}, +[4]={["y"]=-25934.667142857,["x"]=-426411.45657142,}, +}, +[2]={ +[1]={["y"]=-25650.296285714,["x"]=-426510.17971428,}, +[2]={["y"]=-25632.443428571,["x"]=-428297.11428571,}, +[3]={["y"]=-25686.690285714,["x"]=-428299.37457142,}, +[4]={["y"]=-25708.296285714,["x"]=-426515.15114285,}, +}, +}, +}, +[AIRBASE.Nevada.Jean_Airport]={ +PointsRunways={ +[1]={ +[1]={["y"]=-42549.187142857,["x"]=-449663.23257143,}, +[2]={["y"]=-43367.466285714,["x"]=-451044.77657143,}, +[3]={["y"]=-43395.180571429,["x"]=-451028.20514286,}, +[4]={["y"]=-42579.893142857,["x"]=-449648.18371428,}, +}, +[2]={ +[1]={["y"]=-42588.359428572,["x"]=-449900.14342857,}, +[2]={["y"]=-43349.698285714,["x"]=-451185.46857143,}, +[3]={["y"]=-43369.624571429,["x"]=-451173.49342857,}, +[4]={["y"]=-42609.216571429,["x"]=-449891.28628571,}, +}, +}, +}, +[AIRBASE.Nevada.Laughlin_Airport]={ +PointsRunways={ +[1]={ +[1]={["y"]=28231.600857143,["x"]=-515555.94114286,}, +[2]={["y"]=28453.728285714,["x"]=-518170.78885714,}, +[3]={["y"]=28370.788285714,["x"]=-518176.25742857,}, +[4]={["y"]=28138.022857143,["x"]=-515573.07514286,}, +}, +[2]={ +[1]={["y"]=28231.600857143,["x"]=-515555.94114286,}, +[2]={["y"]=28453.728285714,["x"]=-518170.78885714,}, +[3]={["y"]=28370.788285714,["x"]=-518176.25742857,}, +[4]={["y"]=28138.022857143,["x"]=-515573.07514286,}, +}, +}, +}, +[AIRBASE.Nevada.Lincoln_County]={ +PointsRunways={ +[1]={ +[1]={["y"]=33222.34171429,["x"]=-223959.40171429,}, +[2]={["y"]=33200.040000004,["x"]=-225369.36828572,}, +[3]={["y"]=33177.634571428,["x"]=-225369.21485715,}, +[4]={["y"]=33201.198857147,["x"]=-223960.54457143,}, +}, +}, +}, +[AIRBASE.Nevada.McCarran_International_Airport]={ +PointsRunways={ +[1]={ +[1]={["y"]=-29406.035714286,["x"]=-416102.48199999,}, +[2]={["y"]=-24680.714285715,["x"]=-416003.14285713,}, +[3]={["y"]=-24681.857142858,["x"]=-415926.57142856,}, +[4]={["y"]=-29408.42857143,["x"]=-416016.57142856,}, +}, +[2]={ +[1]={["y"]=-28567.221714286,["x"]=-416378.61799999,}, +[2]={["y"]=-25109.912285714,["x"]=-416309.92914285,}, +[3]={["y"]=-25112.508,["x"]=-416240.78714285,}, +[4]={["y"]=-28576.247428571,["x"]=-416308.49514285,}, +}, +[3]={ +[1]={["y"]=-29255.953142857,["x"]=-416307.10657142,}, +[2]={["y"]=-28005.571428572,["x"]=-413449.7142857,}, +[3]={["y"]=-28068.714285715,["x"]=-413422.85714284,}, +[4]={["y"]=-29331.000000001,["x"]=-416275.7142857,}, +}, +[4]={ +[1]={["y"]=-28994.901714286,["x"]=-416423.0522857,}, +[2]={["y"]=-27697.571428572,["x"]=-413464.57142856,}, +[3]={["y"]=-27767.857142858,["x"]=-413434.28571427,}, +[4]={["y"]=-29073.000000001,["x"]=-416386.85714284,}, +}, +}, +}, +[AIRBASE.Nevada.Mesquite]={ +PointsRunways={ +[1]={ +[1]={["y"]=68188.340285714,["x"]=-330302.54742857,}, +[2]={["y"]=68911.303428571,["x"]=-328920.76571429,}, +[3]={["y"]=68936.927142857,["x"]=-328933.888,}, +[4]={["y"]=68212.460285714,["x"]=-330317.19171429,}, +}, +}, +}, +[AIRBASE.Nevada.Mina_Airport_3Q0]={ +PointsRunways={ +[1]={ +[1]={["y"]=-290054.57371429,["x"]=-160930.02228572,}, +[2]={["y"]=-289469.77457143,["x"]=-162048.73571429,}, +[3]={["y"]=-289520.06028572,["x"]=-162074.73571429,}, +[4]={["y"]=-290104.69085714,["x"]=-160956.19457143,}, +}, +}, +}, +[AIRBASE.Nevada.Nellis_AFB]={ +PointsRunways={ +[1]={ +[1]={["y"]=-18614.218571428,["x"]=-399437.91085714,}, +[2]={["y"]=-16217.857142857,["x"]=-396596.85714286,}, +[3]={["y"]=-16300.142857143,["x"]=-396530,}, +[4]={["y"]=-18692.543428571,["x"]=-399381.31114286,}, +}, +[2]={ +[1]={["y"]=-18388.948857143,["x"]=-399630.51828571,}, +[2]={["y"]=-16011,["x"]=-396806.85714286,}, +[3]={["y"]=-16074.714285714,["x"]=-396751.71428572,}, +[4]={["y"]=-18451.571428572,["x"]=-399580.85714285,}, +}, +}, +}, +[AIRBASE.Nevada.Pahute_Mesa_Airstrip]={ +PointsRunways={ +[1]={ +[1]={["y"]=-132690.40942857,["x"]=-302733.53085714,}, +[2]={["y"]=-133112.43228571,["x"]=-304499.70742857,}, +[3]={["y"]=-133179.91685714,["x"]=-304485.544,}, +[4]={["y"]=-132759.988,["x"]=-302723.326,}, +}, +}, +}, +[AIRBASE.Nevada.Tonopah_Test_Range_Airfield]={ +PointsRunways={ +[1]={ +[1]={["y"]=-175389.162,["x"]=-224778.07685715,}, +[2]={["y"]=-173942.15485714,["x"]=-228210.27571429,}, +[3]={["y"]=-174001.77085714,["x"]=-228233.60371429,}, +[4]={["y"]=-175452.38685714,["x"]=-224806.84200001,}, +}, +}, +}, +[AIRBASE.Nevada.Tonopah_Airport]={ +PointsRunways={ +[1]={ +[1]={["y"]=-202128.25228571,["x"]=-196701.34314286,}, +[2]={["y"]=-201562.40828571,["x"]=-198814.99714286,}, +[3]={["y"]=-201591.44828571,["x"]=-198820.93714286,}, +[4]={["y"]=-202156.06828571,["x"]=-196707.68714286,}, +}, +[2]={ +[1]={["y"]=-202084.57171428,["x"]=-196722.02228572,}, +[2]={["y"]=-200592.75485714,["x"]=-197768.05571429,}, +[3]={["y"]=-200605.37285714,["x"]=-197783.49228572,}, +[4]={["y"]=-202097.14314285,["x"]=-196739.16514286,}, +}, +}, +}, +[AIRBASE.Nevada.North_Las_Vegas]={ +PointsRunways={ +[1]={ +[1]={["y"]=-32599.017714286,["x"]=-400913.26485714,}, +[2]={["y"]=-30881.068857143,["x"]=-400837.94628571,}, +[3]={["y"]=-30879.354571428,["x"]=-400873.08914285,}, +[4]={["y"]=-32595.966285714,["x"]=-400947.13571428,}, +}, +[2]={ +[1]={["y"]=-32499.448571428,["x"]=-400690.99514285,}, +[2]={["y"]=-31247.514857143,["x"]=-401868.95571428,}, +[3]={["y"]=-31271.802857143,["x"]=-401894.97857142,}, +[4]={["y"]=-32520.02,["x"]=-400716.99514285,}, +}, +[3]={ +[1]={["y"]=-31865.254857143,["x"]=-400999.74057143,}, +[2]={["y"]=-30893.604,["x"]=-401908.85742857,}, +[3]={["y"]=-30915.578857143,["x"]=-401936.03685714,}, +[4]={["y"]=-31884.969142858,["x"]=-401020.59771429,}, +}, +}, +}, +}, +} +function ATC_GROUND_NEVADA:New(AirbaseNames) +local self=BASE:Inherit(self,ATC_GROUND:New(self.Airbases,AirbaseNames)) +self:SetKickSpeedKmph(50) +self:SetMaximumKickSpeedKmph(150) +return self +end +function ATC_GROUND_NEVADA:Start(RepeatScanSeconds) +RepeatScanSeconds=RepeatScanSeconds or 0.05 +self.AirbaseMonitor=SCHEDULER:New(self,self._AirbaseMonitor,{self},0,2,RepeatScanSeconds) +end +ATC_GROUND_NORMANDY={ +ClassName="ATC_GROUND_NORMANDY", +Airbases={ +[AIRBASE.Normandy.Azeville]={ +PointsRunways={ +[1]={ +[1]={["y"]=-74194.387714285,["x"]=-2691.1399999998,}, +[2]={["y"]=-73160.282571428,["x"]=-2310.0274285712,}, +[3]={["y"]=-73141.711142857,["x"]=-2357.7417142855,}, +[4]={["y"]=-74176.959142857,["x"]=-2741.997142857,}, +}, +}, +}, +[AIRBASE.Normandy.Bazenville]={ +PointsRunways={ +[1]={ +[1]={["y"]=-19246.209999999,["x"]=-21246.748,}, +[2]={["y"]=-17883.70142857,["x"]=-20219.009714285,}, +[3]={["y"]=-17855.415714285,["x"]=-20256.438285714,}, +[4]={["y"]=-19217.791999999,["x"]=-21283.597714285,}, +}, +}, +}, +[AIRBASE.Normandy.Beny_sur_Mer]={ +PointsRunways={ +[1]={ +[1]={["y"]=-8592.7442857133,["x"]=-20386.15542857,}, +[2]={["y"]=-8404.4931428561,["x"]=-21744.113142856,}, +[3]={["y"]=-8267.9917142847,["x"]=-21724.97742857,}, +[4]={["y"]=-8451.0482857133,["x"]=-20368.87542857,}, +}, +}, +}, +[AIRBASE.Normandy.Beuzeville]={ +PointsRunways={ +[1]={ +[1]={["y"]=-71552.573428571,["x"]=-8744.3688571427,}, +[2]={["y"]=-72577.765714285,["x"]=-9638.5682857141,}, +[3]={["y"]=-72609.304285714,["x"]=-9601.2954285712,}, +[4]={["y"]=-71585.849428571,["x"]=-8709.9648571426,}, +}, +}, +}, +[AIRBASE.Normandy.Biniville]={ +PointsRunways={ +[1]={ +[1]={["y"]=-84757.320285714,["x"]=-7377.1354285713,}, +[2]={["y"]=-84271.482,["x"]=-7956.4859999999,}, +[3]={["y"]=-84299.482,["x"]=-7981.6288571427,}, +[4]={["y"]=-84784.969714286,["x"]=-7402.0588571427,}, +}, +}, +}, +[AIRBASE.Normandy.Brucheville]={ +PointsRunways={ +[1]={ +[1]={["y"]=-65546.792857142,["x"]=-14615.640857143,}, +[2]={["y"]=-66914.692,["x"]=-15232.713714285,}, +[3]={["y"]=-66896.527714285,["x"]=-15271.948571428,}, +[4]={["y"]=-65528.393714285,["x"]=-14657.995714286,}, +}, +}, +}, +[AIRBASE.Normandy.Cardonville]={ +PointsRunways={ +[1]={ +[1]={["y"]=-54280.445428571,["x"]=-15843.749142857,}, +[2]={["y"]=-53646.998571428,["x"]=-17143.012285714,}, +[3]={["y"]=-53683.93,["x"]=-17161.317428571,}, +[4]={["y"]=-54323.354571428,["x"]=-15855.004,}, +}, +}, +}, +[AIRBASE.Normandy.Carpiquet]={ +PointsRunways={ +[1]={ +[1]={["y"]=-10751.325714285,["x"]=-34229.494,}, +[2]={["y"]=-9283.5279999993,["x"]=-35192.352857142,}, +[3]={["y"]=-9325.2005714274,["x"]=-35260.967714285,}, +[4]={["y"]=-10794.90942857,["x"]=-34287.041428571,}, +}, +}, +}, +[AIRBASE.Normandy.Chailey]={ +PointsRunways={ +[1]={ +[1]={["y"]=12895.585714292,["x"]=164683.05657144,}, +[2]={["y"]=11410.727142863,["x"]=163606.54485715,}, +[3]={["y"]=11363.012857149,["x"]=163671.97342858,}, +[4]={["y"]=12797.537142863,["x"]=164711.01857144,}, +[5]={["y"]=12862.902857149,["x"]=164726.99685715,}, +}, +[2]={ +[1]={["y"]=11805.316000006,["x"]=164502.90971429,}, +[2]={["y"]=11997.280857149,["x"]=163032.65542858,}, +[3]={["y"]=11918.640857149,["x"]=163023.04657144,}, +[4]={["y"]=11726.973428578,["x"]=164489.94257143,}, +}, +}, +}, +[AIRBASE.Normandy.Chippelle]={ +PointsRunways={ +[1]={ +[1]={["y"]=-48540.313999999,["x"]=-28884.795999999,}, +[2]={["y"]=-47251.820285713,["x"]=-28140.128571427,}, +[3]={["y"]=-47274.551714285,["x"]=-28103.758285713,}, +[4]={["y"]=-48555.657714285,["x"]=-28839.90142857,}, +}, +}, +}, +[AIRBASE.Normandy.Cretteville]={ +PointsRunways={ +[1]={ +[1]={["y"]=-78351.723142857,["x"]=-18177.725428571,}, +[2]={["y"]=-77220.322285714,["x"]=-19125.687714286,}, +[3]={["y"]=-77247.899428571,["x"]=-19158.49,}, +[4]={["y"]=-78380.008857143,["x"]=-18208.011142857,}, +}, +}, +}, +[AIRBASE.Normandy.Cricqueville_en_Bessin]={ +PointsRunways={ +[1]={ +[1]={["y"]=-50875.034571428,["x"]=-14322.404571428,}, +[2]={["y"]=-50681.148571428,["x"]=-15825.258,}, +[3]={["y"]=-50717.434285713,["x"]=-15829.829428571,}, +[4]={["y"]=-50910.569428571,["x"]=-14327.562857142,}, +}, +}, +}, +[AIRBASE.Normandy.Deux_Jumeaux]={ +PointsRunways={ +[1]={ +[1]={["y"]=-49575.410857142,["x"]=-16575.161142857,}, +[2]={["y"]=-48149.077999999,["x"]=-16952.193428571,}, +[3]={["y"]=-48159.935142856,["x"]=-16996.764857142,}, +[4]={["y"]=-49584.839428571,["x"]=-16617.732571428,}, +}, +}, +}, +[AIRBASE.Normandy.Evreux]={ +PointsRunways={ +[1]={ +[1]={["y"]=112906.84828572,["x"]=-45585.824857142,}, +[2]={["y"]=112050.38228572,["x"]=-46811.871999999,}, +[3]={["y"]=111980.05371429,["x"]=-46762.173142856,}, +[4]={["y"]=112833.54542857,["x"]=-45540.010571428,}, +}, +[2]={ +[1]={["y"]=112046.02085714,["x"]=-45091.056571428,}, +[2]={["y"]=112488.668,["x"]=-46623.617999999,}, +[3]={["y"]=112405.66914286,["x"]=-46647.419142856,}, +[4]={["y"]=111966.03657143,["x"]=-45112.604285713,}, +}, +}, +}, +[AIRBASE.Normandy.Ford_AF]={ +PointsRunways={ +[1]={ +[1]={["y"]=-26506.13971428,["x"]=147514.39971429,}, +[2]={["y"]=-25012.977428565,["x"]=147566.14485715,}, +[3]={["y"]=-25009.851428565,["x"]=147482.63600001,}, +[4]={["y"]=-26503.693999994,["x"]=147427.33228572,}, +}, +[2]={ +[1]={["y"]=-25169.701999994,["x"]=148421.09257143,}, +[2]={["y"]=-26092.421999994,["x"]=147190.89628572,}, +[3]={["y"]=-26158.136285708,["x"]=147240.89628572,}, +[4]={["y"]=-25252.357999994,["x"]=148448.64457143,}, +}, +}, +}, +[AIRBASE.Normandy.Funtington]={ +PointsRunways={ +[1]={ +[1]={["y"]=-44698.388571423,["x"]=152952.17257143,}, +[2]={["y"]=-46452.993142851,["x"]=152388.77885714,}, +[3]={["y"]=-46476.361142851,["x"]=152470.05885714,}, +[4]={["y"]=-44787.256571423,["x"]=153009.52,}, +[5]={["y"]=-44715.581428566,["x"]=153002.08714286,}, +}, +[2]={ +[1]={["y"]=-45792.665999994,["x"]=153123.894,}, +[2]={["y"]=-46068.084857137,["x"]=151665.98342857,}, +[3]={["y"]=-46148.632285708,["x"]=151681.58685714,}, +[4]={["y"]=-45871.25971428,["x"]=153136.82714286,}, +}, +}, +}, +[AIRBASE.Normandy.Lantheuil]={ +PointsRunways={ +[1]={ +[1]={["y"]=-17158.84542857,["x"]=-24602.999428571,}, +[2]={["y"]=-15978.59342857,["x"]=-23922.978571428,}, +[3]={["y"]=-15932.021999999,["x"]=-24004.121428571,}, +[4]={["y"]=-17090.734857142,["x"]=-24673.248,}, +}, +}, +}, +[AIRBASE.Normandy.Lessay]={ +PointsRunways={ +[1]={ +[1]={["y"]=-87667.304571429,["x"]=-33220.165714286,}, +[2]={["y"]=-86146.607714286,["x"]=-34248.483142857,}, +[3]={["y"]=-86191.538285714,["x"]=-34316.991142857,}, +[4]={["y"]=-87712.212,["x"]=-33291.774857143,}, +}, +[2]={ +[1]={["y"]=-87125.123142857,["x"]=-34183.682571429,}, +[2]={["y"]=-85803.278285715,["x"]=-33498.428857143,}, +[3]={["y"]=-85768.408285715,["x"]=-33570.13,}, +[4]={["y"]=-87087.688571429,["x"]=-34258.272285715,}, +}, +}, +}, +[AIRBASE.Normandy.Lignerolles]={ +PointsRunways={ +[1]={ +[1]={["y"]=-35279.611714285,["x"]=-35232.026857142,}, +[2]={["y"]=-33804.948857142,["x"]=-35770.713999999,}, +[3]={["y"]=-33789.876285713,["x"]=-35726.655714284,}, +[4]={["y"]=-35263.548285713,["x"]=-35192.75542857,}, +}, +}, +}, +[AIRBASE.Normandy.Longues_sur_Mer]={ +PointsRunways={ +[1]={ +[1]={["y"]=-29444.070285713,["x"]=-16334.105428571,}, +[2]={["y"]=-28265.52942857,["x"]=-17011.557999999,}, +[3]={["y"]=-28344.74742857,["x"]=-17143.587999999,}, +[4]={["y"]=-29529.616285713,["x"]=-16477.766571428,}, +}, +}, +}, +[AIRBASE.Normandy.Maupertus]={ +PointsRunways={ +[1]={ +[1]={["y"]=-85605.340857143,["x"]=16175.267714286,}, +[2]={["y"]=-84132.567142857,["x"]=15895.905714286,}, +[3]={["y"]=-84139.995142857,["x"]=15847.623714286,}, +[4]={["y"]=-85613.626571429,["x"]=16132.410571429,}, +}, +}, +}, +[AIRBASE.Normandy.Meautis]={ +PointsRunways={ +[1]={ +[1]={["y"]=-72642.527714286,["x"]=-24593.622285714,}, +[2]={["y"]=-71298.672571429,["x"]=-24352.651142857,}, +[3]={["y"]=-71290.101142857,["x"]=-24398.365428571,}, +[4]={["y"]=-72631.715714286,["x"]=-24639.966857143,}, +}, +}, +}, +[AIRBASE.Normandy.Le_Molay]={ +PointsRunways={ +[1]={ +[1]={["y"]=-41876.526857142,["x"]=-26701.052285713,}, +[2]={["y"]=-40979.545714285,["x"]=-25675.045999999,}, +[3]={["y"]=-41017.687428571,["x"]=-25644.272571427,}, +[4]={["y"]=-41913.638285713,["x"]=-26665.137999999,}, +}, +}, +}, +[AIRBASE.Normandy.Needs_Oar_Point]={ +PointsRunways={ +[1]={ +[1]={["y"]=-83882.441142851,["x"]=141429.83314286,}, +[2]={["y"]=-85138.159428566,["x"]=140187.52828572,}, +[3]={["y"]=-85208.323428566,["x"]=140161.04371429,}, +[4]={["y"]=-85245.751999994,["x"]=140201.61514286,}, +[5]={["y"]=-83939.966571423,["x"]=141485.22085714,}, +}, +[2]={ +[1]={["y"]=-84528.76571428,["x"]=141988.01428572,}, +[2]={["y"]=-84116.98971428,["x"]=140565.78685714,}, +[3]={["y"]=-84199.35771428,["x"]=140541.14685714,}, +[4]={["y"]=-84605.051428566,["x"]=141966.01428572,}, +}, +}, +}, +[AIRBASE.Normandy.Picauville]={ +PointsRunways={ +[1]={ +[1]={["y"]=-80808.838571429,["x"]=-11834.554571428,}, +[2]={["y"]=-79531.574285714,["x"]=-12311.274,}, +[3]={["y"]=-79549.355428571,["x"]=-12356.928285714,}, +[4]={["y"]=-80827.815142857,["x"]=-11901.835142857,}, +}, +}, +}, +[AIRBASE.Normandy.Rucqueville]={ +PointsRunways={ +[1]={ +[1]={["y"]=-20023.988857141,["x"]=-26569.565428571,}, +[2]={["y"]=-18688.92542857,["x"]=-26571.086571428,}, +[3]={["y"]=-18688.012571427,["x"]=-26611.252285713,}, +[4]={["y"]=-20022.218857141,["x"]=-26608.505428571,}, +}, +}, +}, +[AIRBASE.Normandy.Saint_Pierre_du_Mont]={ +PointsRunways={ +[1]={ +[1]={["y"]=-48015.384571428,["x"]=-11886.631714285,}, +[2]={["y"]=-46540.412285713,["x"]=-11945.226571428,}, +[3]={["y"]=-46541.349999999,["x"]=-11991.174571428,}, +[4]={["y"]=-48016.837142856,["x"]=-11929.371142857,}, +}, +}, +}, +[AIRBASE.Normandy.Sainte_Croix_sur_Mer]={ +PointsRunways={ +[1]={ +[1]={["y"]=-15877.817999999,["x"]=-18812.579999999,}, +[2]={["y"]=-14464.377142856,["x"]=-18807.46,}, +[3]={["y"]=-14463.879714285,["x"]=-18759.706857142,}, +[4]={["y"]=-15878.229142856,["x"]=-18764.071428571,}, +}, +}, +}, +[AIRBASE.Normandy.Sainte_Laurent_sur_Mer]={ +PointsRunways={ +[1]={ +[1]={["y"]=-41676.834857142,["x"]=-14475.109428571,}, +[2]={["y"]=-40566.11142857,["x"]=-14817.319999999,}, +[3]={["y"]=-40579.543999999,["x"]=-14860.059999999,}, +[4]={["y"]=-41687.120571427,["x"]=-14509.680857142,}, +}, +}, +}, +[AIRBASE.Normandy.Sommervieu]={ +PointsRunways={ +[1]={ +[1]={["y"]=-26821.913714284,["x"]=-21390.466571427,}, +[2]={["y"]=-25465.308857142,["x"]=-21296.859999999,}, +[3]={["y"]=-25462.451714284,["x"]=-21343.717142856,}, +[4]={["y"]=-26818.002285713,["x"]=-21440.532857142,}, +}, +}, +}, +[AIRBASE.Normandy.Tangmere]={ +PointsRunways={ +[1]={ +[1]={["y"]=-34684.581142851,["x"]=150459.61657143,}, +[2]={["y"]=-33250.625428566,["x"]=149954.17,}, +[3]={["y"]=-33275.724285708,["x"]=149874.69028572,}, +[4]={["y"]=-34709.020571423,["x"]=150377.93742857,}, +}, +[2]={ +[1]={["y"]=-33103.438857137,["x"]=150812.72542857,}, +[2]={["y"]=-34410.246285708,["x"]=150009.73142857,}, +[3]={["y"]=-34453.535142851,["x"]=150082.02685714,}, +[4]={["y"]=-33176.545999994,["x"]=150870.22542857,}, +}, +}, +}, +[AIRBASE.Normandy.Argentan]={ +PointsRunways={ +[1]={ +[1]={["y"]=22322.280338032,["x"]=-78607.309765269,}, +[2]={["y"]=23032.778713963,["x"]=-78967.17709893,}, +[3]={["y"]=23015.27074041,["x"]=-79008.02903722,}, +[4]={["y"]=22299.944963827,["x"]=-78650.366148928,}, +}, +}, +}, +[AIRBASE.Normandy.Goulet]={ +PointsRunways={ +[1]={ +[1]={["y"]=24901.788373185,["x"]=-89139.367511763,}, +[2]={["y"]=25459.965967043,["x"]=-89709.67940114,}, +[3]={["y"]=25422.459962713,["x"]=-89741.669816598,}, +[4]={["y"]=24857.663662208,["x"]=-89173.56416277,}, +}, +}, +}, +[AIRBASE.Normandy.Essay]={ +PointsRunways={ +[1]={ +[1]={["y"]=44610.072022849,["x"]=-105469.21149064,}, +[2]={["y"]=45417.939023956,["x"]=-105536.08535277,}, +[3]={["y"]=45412.558368383,["x"]=-105585.27991801,}, +[4]={["y"]=44602.38537203,["x"]=-105516.10006064,}, +}, +}, +}, +[AIRBASE.Normandy.Hauterive]={ +PointsRunways={ +[1]={ +[1]={["y"]=40617.185360953,["x"]=-107657.10147517,}, +[2]={["y"]=41114.628372034,["x"]=-108298.77015609,}, +[3]={["y"]=41080.006684855,["x"]=-108319.06562788,}, +[4]={["y"]=40584.558402807,["x"]=-107692.29370481,}, +}, +}, +}, +[AIRBASE.Normandy.Vrigny]={ +PointsRunways={ +[1]={ +[1]={["y"]=24892.131051827,["x"]=-89131.628297486,}, +[2]={["y"]=25469.738000575,["x"]=-89709.235246234,}, +[3]={["y"]=25418.869206793,["x"]=-89738.771965204,}, +[4]={["y"]=24859.312475193,["x"]=-89171.010589446,}, +}, +}, +}, +[AIRBASE.Normandy.Barville]={ +PointsRunways={ +[1]={ +[1]={["y"]=49027.850333166,["x"]=-109217.05049066,}, +[2]={["y"]=49755.022185805,["x"]=-110346.63783457,}, +[3]={["y"]=49682.657996586,["x"]=-110401.35222154,}, +[4]={["y"]=48921.951519675,["x"]=-109285.88471943,}, +}, +[2]={ +[1]={["y"]=48429.522036941,["x"]=-109818.90874734,}, +[2]={["y"]=49746.197284681,["x"]=-109954.81222465,}, +[3]={["y"]=49735.607403332,["x"]=-110032.47135455,}, +[4]={["y"]=48420.697135816,["x"]=-109900.09783768,}, +}, +}, +}, +[AIRBASE.Normandy.Conches]={ +PointsRunways={ +[1]={ +[1]={["y"]=95099.187473266,["x"]=-56389.619005858,}, +[2]={["y"]=95181.545025963,["x"]=-56465.440244849,}, +[3]={["y"]=94071.678958666,["x"]=-57627.596821795,}, +[4]={["y"]=94005.008558864,["x"]=-57558.31189651,}, +}, +}, +}, +}, +} +function ATC_GROUND_NORMANDY:New(AirbaseNames) +local self=BASE:Inherit(self,ATC_GROUND:New(self.Airbases,AirbaseNames)) +self:SetKickSpeedKmph(40) +self:SetMaximumKickSpeedKmph(100) +return self +end +function ATC_GROUND_NORMANDY:Start(RepeatScanSeconds) +RepeatScanSeconds=RepeatScanSeconds or 0.05 +self.AirbaseMonitor=SCHEDULER:New(self,self._AirbaseMonitor,{self},0,2,RepeatScanSeconds) +end +ATC_GROUND_PERSIANGULF={ +ClassName="ATC_GROUND_PERSIANGULF", +Airbases={ +[AIRBASE.PersianGulf.Abu_Musa_Island_Airport]={ +PointsRunways={ +[1]={ +[1]={["y"]=-122813.71002344,["x"]=-31689.936027827,}, +[2]={["y"]=-122827.82488722,["x"]=-31590.105445836,}, +[3]={["y"]=-122769.5689949,["x"]=-31583.176330891,}, +[4]={["y"]=-122726.96776968,["x"]=-31614.998932862,}, +[5]={["y"]=-121293.92414543,["x"]=-31467.947715689,}, +[6]={["y"]=-121296.4904843,["x"]=-31432.018971528,}, +[7]={["y"]=-121236.18152088,["x"]=-31424.576588809,}, +[8]={["y"]=-121190.50068902,["x"]=-31458.452261875,}, +[9]={["y"]=-119839.83654246,["x"]=-31319.356695194,}, +[10]={["y"]=-119824.69514313,["x"]=-31423.293419374,}, +[11]={["y"]=-119886.80054375,["x"]=-31430.22253432,}, +[12]={["y"]=-119932.22474173,["x"]=-31395.320325706,}, +[13]={["y"]=-122813.9472789,["x"]=-31689.81193251,}, +}, +}, +}, +[AIRBASE.PersianGulf.Al_Dhafra_AB]={ +PointsRunways={ +[1]={ +[1]={["y"]=-174672.06004916,["x"]=-209880.97145616,}, +[2]={["y"]=-174705.15693282,["x"]=-209923.15131918,}, +[3]={["y"]=-171819.05380065,["x"]=-212172.84298281,}, +[4]={["y"]=-171785.09826475,["x"]=-212129.87417284,}, +[5]={["y"]=-174671.96413454,["x"]=-209880.52453983,}, +}, +[2]={ +[1]={["y"]=-174351.95872272,["x"]=-211813.88516693,}, +[2]={["y"]=-174381.29169939,["x"]=-211851.81242636,}, +[3]={["y"]=-171493.65648904,["x"]=-214102.92235002,}, +[4]={["y"]=-171464.99693831,["x"]=-214062.78788361,}, +[5]={["y"]=-174351.8628081,["x"]=-211813.4382506,}, +}, +}, +}, +[AIRBASE.PersianGulf.Al_Maktoum_Intl]={ +PointsRunways={ +[1]={ +[1]={["y"]=-111879.49046471,["x"]=-138953.80105841,}, +[2]={["y"]=-111917.23447224,["x"]=-139018.2804046,}, +[3]={["y"]=-108092.98121312,["x"]=-141406.67838426,}, +[4]={["y"]=-108052.34416748,["x"]=-141341.82058294,}, +[5]={["y"]=-111879.5412879,["x"]=-138952.87693763,}, +}, +}, +}, +[AIRBASE.PersianGulf.Al_Minhad_AB]={ +PointsRunways={ +[1]={ +[1]={["y"]=-91070.628933035,["x"]=-125989.64095162,}, +[2]={["y"]=-91072.346560159,["x"]=-126040.59722299,}, +[3]={["y"]=-87098.282779771,["x"]=-126039.41747017,}, +[4]={["y"]=-87099.632735396,["x"]=-125991.26905291,}, +[5]={["y"]=-91071.031270042,["x"]=-125987.44617225,}, +}, +}, +}, +[AIRBASE.PersianGulf.Bandar_Abbas_Intl]={ +PointsRunways={ +[1]={ +[1]={["y"]=12988.484058788,["x"]=113979.99250505,}, +[2]={["y"]=13037.8836239,["x"]=113952.60241152,}, +[3]={["y"]=14877.313199902,["x"]=117414.37833333,}, +[4]={["y"]=14828.777486364,["x"]=117439.06043783,}, +[5]={["y"]=12988.939584604,["x"]=113979.52494386,}, +}, +[2]={ +[1]={["y"]=13203.406014284,["x"]=113848.44907555,}, +[2]={["y"]=13258.268500181,["x"]=113818.47303925,}, +[3]={["y"]=15315.015323566,["x"]=117694.27156647,}, +[4]={["y"]=15264.815746383,["x"]=117725.22168173,}, +[5]={["y"]=13203.861540099,["x"]=113847.98151436,}, +}, +}, +}, +[AIRBASE.PersianGulf.Bandar_Lengeh]={ +PointsRunways={ +[1]={ +[1]={["y"]=-142373.15541415,["x"]=41364.94047809,}, +[2]={["y"]=-142363.30071107,["x"]=41298.112282592,}, +[3]={["y"]=-142217.57151662,["x"]=41320.35666061,}, +[4]={["y"]=-142213.00856728,["x"]=41291.838227254,}, +[5]={["y"]=-142131.44584788,["x"]=41301.534494595,}, +[6]={["y"]=-142132.58658522,["x"]=41323.778872613,}, +[7]={["y"]=-142123.17550221,["x"]=41336.041798956,}, +[8]={["y"]=-139580.45381288,["x"]=41711.022304533,}, +[9]={["y"]=-139590.04241918,["x"]=41778.350996659,}, +[10]={["y"]=-139732.41237808,["x"]=41757.089304408,}, +[11]={["y"]=-139736.7897853,["x"]=41785.646675372,}, +[12]={["y"]=-139816.41690726,["x"]=41775.641173137,}, +[13]={["y"]=-139816.00001133,["x"]=41754.58792885,}, +[14]={["y"]=-139824.1294819,["x"]=41743.748634761,}, +[15]={["y"]=-142373.20183966,["x"]=41365.161507021,}, +}, +}, +}, +[AIRBASE.PersianGulf.Dubai_Intl]={ +PointsRunways={ +[1]={ +[1]={["y"]=-89693.511670714,["x"]=-100490.47082052,}, +[2]={["y"]=-89731.488328846,["x"]=-100555.50584758,}, +[3]={["y"]=-85706.437275049,["x"]=-103076.68123933,}, +[4]={["y"]=-85669.519216262,["x"]=-103010.44994755,}, +[5]={["y"]=-89693.036962487,["x"]=-100489.9961123,}, +}, +[2]={ +[1]={["y"]=-90797.505501889,["x"]=-99344.082465487,}, +[2]={["y"]=-90835.482160021,["x"]=-99409.11749254,}, +[3]={["y"]=-87210.216900398,["x"]=-101681.72494832,}, +[4]={["y"]=-87171.474397253,["x"]=-101619.20256393,}, +[5]={["y"]=-90797.030793662,["x"]=-99343.607757261,}, +}, +}, +}, +[AIRBASE.PersianGulf.Fujairah_Intl]={ +PointsRunways={ +[1]={ +[1]={["y"]=5808.8716147284,["x"]=-116602.15633995,}, +[2]={["y"]=5781.9885293892,["x"]=-116666.67574476,}, +[3]={["y"]=9435.1910907931,["x"]=-118192.91910235,}, +[4]={["y"]=9459.878635843,["x"]=-118134.40047704,}, +[5]={["y"]=5808.4078522575,["x"]=-116603.31550719,}, +}, +}, +}, +[AIRBASE.PersianGulf.Havadarya]={ +PointsRunways={ +[1]={ +[1]={["y"]=-7565.4887830428,["x"]=109074.13162774,}, +[2]={["y"]=-7557.8281079193,["x"]=109030.65729641,}, +[3]={["y"]=-4987.3556518085,["x"]=109524.49147773,}, +[4]={["y"]=-4996.215358578,["x"]=109566.57508489,}, +[5]={["y"]=-7565.4936338604,["x"]=109074.32262205,}, +}, +}, +}, +[AIRBASE.PersianGulf.Kerman_Airport]={ +PointsRunways={ +[1]={ +[1]={["y"]=70375.468628778,["x"]=456046.12685302,}, +[2]={["y"]=70297.050081575,["x"]=456015.1578105,}, +[3]={["y"]=71814.291673715,["x"]=452165.51037702,}, +[4]={["y"]=71902.918622452,["x"]=452188.46411914,}, +[5]={["y"]=70860.465673482,["x"]=454829.89695989,}, +[6]={["y"]=70862.525255971,["x"]=454892.77675983,}, +[7]={["y"]=70816.157465062,["x"]=454922.77944807,}, +[8]={["y"]=70462.749176371,["x"]=455833.38051827,}, +[9]={["y"]=70483.400377364,["x"]=455901.17880077,}, +[10]={["y"]=70453.787334431,["x"]=455974.8217628,}, +[11]={["y"]=70405.860962315,["x"]=455961.57382254,}, +[12]={["y"]=70374.689338175,["x"]=456046.51649833,}, +}, +}, +}, +[AIRBASE.PersianGulf.Khasab]={ +PointsRunways={ +[1]={ +[1]={["y"]=-534.81827307392,["x"]=-1495.070060483,}, +[2]={["y"]=-434.82912685139,["x"]=-1519.8421462589,}, +[3]={["y"]=-405.55302547993,["x"]=-1413.0969766429,}, +[4]={["y"]=-424.92029254105,["x"]=-1352.0675653224,}, +[5]={["y"]=216.05735069389,["x"]=1206.9187095195,}, +[6]={["y"]=116.42961315781,["x"]=1229.9576238247,}, +[7]={["y"]=88.253643635887,["x"]=1123.7918160128,}, +[8]={["y"]=101.1741158476,["x"]=1042.6886109249,}, +[9]={["y"]=-535.31436058928,["x"]=-1494.8762081291,}, +}, +}, +}, +[AIRBASE.PersianGulf.Lar_Airbase]={ +PointsRunways={ +[1]={ +[1]={["y"]=-183987.5454359,["x"]=169021.72039309,}, +[2]={["y"]=-183988.41292374,["x"]=168955.27082471,}, +[3]={["y"]=-180847.92031188,["x"]=168930.46175795,}, +[4]={["y"]=-180806.58653731,["x"]=168888.39641215,}, +[5]={["y"]=-180740.37934087,["x"]=168886.56748407,}, +[6]={["y"]=-180735.62412787,["x"]=168932.65647164,}, +[7]={["y"]=-180685.14571291,["x"]=168934.11961411,}, +[8]={["y"]=-180682.5852136,["x"]=169001.78995301,}, +[9]={["y"]=-183987.48111493,["x"]=169021.35002828,}, +}, +}, +}, +[AIRBASE.PersianGulf.Qeshm_Island]={ +PointsRunways={ +[1]={ +[1]={["y"]=-35140.372717152,["x"]=63373.658918509,}, +[2]={["y"]=-35098.556715749,["x"]=63320.377239302,}, +[3]={["y"]=-34991.318905699,["x"]=63408.730403557,}, +[4]={["y"]=-34984.574389344,["x"]=63401.311435566,}, +[5]={["y"]=-34991.993357335,["x"]=63313.632722947,}, +[6]={["y"]=-34956.921872287,["x"]=63265.746656824,}, +[7]={["y"]=-34917.129225791,["x"]=63261.699947011,}, +[8]={["y"]=-34832.822771349,["x"]=63337.23853019,}, +[9]={["y"]=-34915.105870884,["x"]=63436.382920614,}, +[10]={["y"]=-34906.337999622,["x"]=63478.198922017,}, +[11]={["y"]=-32728.533668488,["x"]=65307.986209216,}, +[12]={["y"]=-32676.600892552,["x"]=65299.218337954,}, +[13]={["y"]=-32623.99366498,["x"]=65334.964274638,}, +[14]={["y"]=-32626.691471522,["x"]=65388.92040548,}, +[15]={["y"]=-31822.745121968,["x"]=66067.418750826,}, +[16]={["y"]=-31777.556862387,["x"]=66068.767654097,}, +[17]={["y"]=-31691.227053039,["x"]=65974.344425122,}, +[18]={["y"]=-31606.246146962,["x"]=66042.464040311,}, +[19]={["y"]=-31602.199437148,["x"]=66084.280041714,}, +[20]={["y"]=-31632.549760747,["x"]=66124.747139846,}, +[21]={["y"]=-31727.647441358,["x"]=66134.189462744,}, +[22]={["y"]=-31734.391957713,["x"]=66141.608430735,}, +[23]={["y"]=-31632.549760747,["x"]=66225.914885176,}, +[24]={["y"]=-31673.691310515,["x"]=66277.173209477,}, +[25]={["y"]=-35140.880825624,["x"]=63373.905965825,}, +}, +}, +}, +[AIRBASE.PersianGulf.Sharjah_Intl]={ +PointsRunways={ +[1]={ +[1]={["y"]=-71668.808658476,["x"]=-93980.156242153,}, +[2]={["y"]=-75307.847363315,["x"]=-91617.097584505,}, +[3]={["y"]=-75280.458023829,["x"]=-91574.709321014,}, +[4]={["y"]=-72249.697184234,["x"]=-93529.134331507,}, +[5]={["y"]=-72179.919581256,["x"]=-93526.199759419,}, +[6]={["y"]=-72138.183444896,["x"]=-93597.933743788,}, +[7]={["y"]=-71638.654062835,["x"]=-93927.584008321,}, +[8]={["y"]=-71668.325847279,["x"]=-93979.428115206,}, +}, +[2]={ +[1]={["y"]=-71553.225408723,["x"]=-93775.312323319,}, +[2]={["y"]=-75168.13829548,["x"]=-91426.51571111,}, +[3]={["y"]=-75125.388157445,["x"]=-91363.754870166,}, +[4]={["y"]=-71510.511081666,["x"]=-93703.252275385,}, +[5]={["y"]=-71552.247218027,["x"]=-93775.638386885,}, +}, +}, +}, +[AIRBASE.PersianGulf.Shiraz_International_Airport]={ +PointsRunways={ +[1]={ +[1]={["y"]=-353995.75579778,["x"]=382327.42294273,}, +[2]={["y"]=-354029.77009807,["x"]=382265.46199492,}, +[3]={["y"]=-349407.98049238,["x"]=379941.14030526,}, +[4]={["y"]=-349376.87025024,["x"]=380004.69408564,}, +[5]={["y"]=-353995.71101815,["x"]=382327.59771695,}, +}, +[2]={ +[1]={["y"]=-354056.29510012,["x"]=381845.97598829,}, +[2]={["y"]=-354091.48797289,["x"]=381783.6025623,}, +[3]={["y"]=-349650.64038107,["x"]=379550.92898242,}, +[4]={["y"]=-349624.41889127,["x"]=379614.92719482,}, +[5]={["y"]=-354056.25032049,["x"]=381846.15076251,}, +}, +}, +}, +[AIRBASE.PersianGulf.Sir_Abu_Nuayr]={ +PointsRunways={ +[1]={ +[1]={["y"]=-203367.3128691,["x"]=-103017.22553918,}, +[2]={["y"]=-203373.59664477,["x"]=-103054.92819323,}, +[3]={["y"]=-202578.27577922,["x"]=-103188.26018333,}, +[4]={["y"]=-202571.37254488,["x"]=-103151.01482599,}, +[5]={["y"]=-203367.65259839,["x"]=-103016.48202662,}, +[6]={["y"]=-203291.39594004,["x"]=-102985.49774228,}, +}, +}, +}, +[AIRBASE.PersianGulf.Sirri_Island]={ +PointsRunways={ +[1]={ +[1]={["y"]=-169713.12842428,["x"]=-27766.658020853,}, +[2]={["y"]=-169682.02009414,["x"]=-27726.583172021,}, +[3]={["y"]=-169727.21866794,["x"]=-27691.632048154,}, +[4]={["y"]=-169694.28043602,["x"]=-27650.276268081,}, +[5]={["y"]=-169763.08474269,["x"]=-27598.490047901,}, +[6]={["y"]=-169825.30140298,["x"]=-27607.090586235,}, +[7]={["y"]=-171614.98889813,["x"]=-26246.247907014,}, +[8]={["y"]=-171620.85326172,["x"]=-26187.105176343,}, +[9]={["y"]=-171686.10990337,["x"]=-26138.56820961,}, +[10]={["y"]=-171716.55468456,["x"]=-26178.745338885,}, +[11]={["y"]=-171764.9668776,["x"]=-26142.810515186,}, +[12]={["y"]=-171796.29599657,["x"]=-26183.416460911,}, +[13]={["y"]=-169713.5628285,["x"]=-27766.883787223,}, +}, +}, +}, +[AIRBASE.PersianGulf.Tunb_Island_AFB]={ +PointsRunways={ +[1]={ +[1]={["y"]=-92923.634698863,["x"]=9547.6862547173,}, +[2]={["y"]=-92963.030803298,["x"]=9565.7274614215,}, +[3]={["y"]=-92934.128053782,["x"]=9619.2987996964,}, +[4]={["y"]=-92970.946842975,["x"]=9640.1014155901,}, +[5]={["y"]=-92949.591945243,["x"]=9682.8112110532,}, +[6]={["y"]=-92899.518391942,["x"]=9699.7478540817,}, +[7]={["y"]=-91969.13471408,["x"]=11464.627292768,}, +[8]={["y"]=-91983.666755417,["x"]=11515.293058512,}, +[9]={["y"]=-91960.101282978,["x"]=11557.710908902,}, +[10]={["y"]=-91921.021874517,["x"]=11539.251288825,}, +[11]={["y"]=-91893.725202275,["x"]=11589.720675632,}, +[12]={["y"]=-91859.751646175,["x"]=11571.850192366,}, +[13]={["y"]=-92922.149728329,["x"]=9547.2937058617,}, +}, +}, +}, +[AIRBASE.PersianGulf.Tunb_Kochak]={ +PointsRunways={ +[1]={ +[1]={["y"]=-109925.50271188,["x"]=8974.5666013181,}, +[2]={["y"]=-109905.7382908,["x"]=8937.53274444,}, +[3]={["y"]=-109009.93726324,["x"]=9072.2234968343,}, +[4]={["y"]=-109040.82867587,["x"]=9104.9871291834,}, +[5]={["y"]=-109925.26515172,["x"]=8974.091480998,}, +}, +}, +}, +[AIRBASE.PersianGulf.Sas_Al_Nakheel_Airport]={ +PointsRunways={ +[1]={ +[1]={["y"]=-176230.75865538,["x"]=-188732.01369812,}, +[2]={["y"]=-176274.78045186,["x"]=-188744.8049371,}, +[3]={["y"]=-175692.03171595,["x"]=-190564.17145168,}, +[4]={["y"]=-175649.7486572,["x"]=-190550.58435053,}, +[5]={["y"]=-176230.66274076,["x"]=-188731.5667818,}, +}, +}, +}, +[AIRBASE.PersianGulf.Bandar_e_Jask_airfield]={ +PointsRunways={ +[1]={ +[1]={["y"]=155156.73167657,["x"]=-57837.031277333,}, +[2]={["y"]=155130.38996239,["x"]=-57790.475605714,}, +[3]={["y"]=157137.17872571,["x"]=-56710.411783359,}, +[4]={["y"]=157148.46631801,["x"]=-56688.071756941,}, +[5]={["y"]=157220.07198163,["x"]=-56649.035500253,}, +[6]={["y"]=157227.83220133,["x"]=-56662.204357931,}, +[7]={["y"]=157359.6383572,["x"]=-56590.481115222,}, +[8]={["y"]=157383.03659539,["x"]=-56633.044744502,}, +[9]={["y"]=155156.7940421,["x"]=-57837.149989814,}, +}, +}, +}, +[AIRBASE.PersianGulf.Abu_Dhabi_International_Airport]={ +PointsRunways={ +[1]={ +[1]={["y"]=-163964.56943899,["x"]=-189427.63621921,}, +[2]={["y"]=-164005.96838287,["x"]=-189478.90226888,}, +[3]={["y"]=-160798.22080495,["x"]=-192054.59531727,}, +[4]={["y"]=-160755.05282258,["x"]=-192002.58569997,}, +[5]={["y"]=-163964.47352437,["x"]=-189427.18930288,}, +}, +[2]={ +[1]={["y"]=-163615.44952024,["x"]=-187144.00786922,}, +[2]={["y"]=-163656.84846411,["x"]=-187195.27391888,}, +[3]={["y"]=-160452.71811093,["x"]=-189764.86593382,}, +[4]={["y"]=-160411.94568221,["x"]=-189715.47961171,}, +[5]={["y"]=-163615.35360562,["x"]=-187143.56095289,}, +}, +}, +}, +[AIRBASE.PersianGulf.Al_Bateen_Airport]={ +PointsRunways={ +[1]={ +[1]={["y"]=-183207.51774197,["x"]=-189871.8319832,}, +[2]={["y"]=-183240.61462564,["x"]=-189914.01184622,}, +[3]={["y"]=-180748.88998479,["x"]=-191943.30402837,}, +[4]={["y"]=-180711.83076051,["x"]=-191896.52435182,}, +[5]={["y"]=-183207.42182735,["x"]=-189871.38506688,}, +}, +}, +}, +[AIRBASE.PersianGulf.Kish_International_Airport]={ +PointsRunways={ +[1]={ +[1]={["y"]=-227330.79164594,["x"]=42691.91536494,}, +[2]={["y"]=-227321.58531968,["x"]=42758.113234714,}, +[3]={["y"]=-223235.73004619,["x"]=42313.579195302,}, +[4]={["y"]=-223240.99080406,["x"]=42247.819722016,}, +[5]={["y"]=-227330.67774245,["x"]=42691.785682556,}, +}, +[2]={ +[1]={["y"]=-227283.77911886,["x"]=42987.748941936,}, +[2]={["y"]=-227274.5727926,["x"]=43053.946811711,}, +[3]={["y"]=-222907.94761294,["x"]=42580.826755904,}, +[4]={["y"]=-222915.76510871,["x"]=42514.58376547,}, +[5]={["y"]=-227283.66521537,["x"]=42987.619259553,}, +}, +}, +}, +[AIRBASE.PersianGulf.Al_Ain_International_Airport]={ +PointsRunways={ +[1]={ +[1]={["y"]=-65165.315648901,["x"]=-209042.45716363,}, +[2]={["y"]=-65112.933878375,["x"]=-209048.84518442,}, +[3]={["y"]=-65672.013626755,["x"]=-213019.66479976,}, +[4]={["y"]=-65722.555424932,["x"]=-213013.91596964,}, +[5]={["y"]=-65165.400582791,["x"]=-209042.15059908,}, +}, +}, +}, +[AIRBASE.PersianGulf.Lavan_Island_Airport]={ +PointsRunways={ +[1]={ +[1]={["y"]=-288099.83301495,["x"]=76353.443273049,}, +[2]={["y"]=-288119.51457685,["x"]=76302.756224611,}, +[3]={["y"]=-288070.96603401,["x"]=76283.898526152,}, +[4]={["y"]=-288085.61084238,["x"]=76247.386812114,}, +[5]={["y"]=-288032.04695421,["x"]=76224.316223573,}, +[6]={["y"]=-287991.12173627,["x"]=76245.38067398,}, +[7]={["y"]=-287489.96435675,["x"]=76037.610404141,}, +[8]={["y"]=-287497.65444594,["x"]=76017.686082159,}, +[9]={["y"]=-287453.61120787,["x"]=75998.111309685,}, +[10]={["y"]=-287419.70490555,["x"]=76007.199596905,}, +[11]={["y"]=-285642.24565503,["x"]=75279.787069797,}, +[12]={["y"]=-285625.46727862,["x"]=75239.239326815,}, +[13]={["y"]=-285570.23845628,["x"]=75217.217707782,}, +[14]={["y"]=-285555.20782742,["x"]=75252.172658628,}, +[15]={["y"]=-285505.92134673,["x"]=75231.199688121,}, +[16]={["y"]=-285484.28380792,["x"]=75284.258832895,}, +[17]={["y"]=-288099.97979219,["x"]=76354.32393647,}, +}, +}, +}, +[AIRBASE.PersianGulf.Jiroft_Airport]={ +PointsRunways={ +[1]={ +[1]={["y"]=140376.87310595,["x"]=283748.07558774,}, +[2]={["y"]=140299.43760975,["x"]=283655.81201779,}, +[3]={["y"]=143008.43807723,["x"]=281517.41347718,}, +[4]={["y"]=143052.6952428,["x"]=281573.25195709,}, +[5]={["y"]=142946.60213095,["x"]=281656.5960586,}, +[6]={["y"]=142975.14179847,["x"]=281687.20381796,}, +[7]={["y"]=142932.12548801,["x"]=281724.01585287,}, +[8]={["y"]=142870.49635092,["x"]=281719.05243244,}, +[9]={["y"]=140437.35783025,["x"]=283640.84253664,}, +[10]={["y"]=140433.27045062,["x"]=283705.80267729,}, +[11]={["y"]=140376.77702493,["x"]=283747.8442964,}, +}, +}, +}, +}, +} +function ATC_GROUND_PERSIANGULF:New(AirbaseNames) +local self=BASE:Inherit(self,ATC_GROUND:New(self.Airbases,AirbaseNames)) +self:SetKickSpeedKmph(50) +self:SetMaximumKickSpeedKmph(150) +return self +end +function ATC_GROUND_PERSIANGULF:Start(RepeatScanSeconds) +RepeatScanSeconds=RepeatScanSeconds or 0.05 +self.AirbaseMonitor=SCHEDULER:New(self,self._AirbaseMonitor,{self},0,2,RepeatScanSeconds) +end +do +DETECTION_BASE={ +ClassName="DETECTION_BASE", +DetectionSetGroup=nil, +DetectionRange=nil, +DetectedObjects={}, +DetectionRun=0, +DetectedObjectsIdentified={}, +DetectedItems={}, +DetectedItemsByIndex={}, +} +function DETECTION_BASE:New(DetectionSet) +local self=BASE:Inherit(self,FSM:New()) +self.DetectedItemCount=0 +self.DetectedItemMax=0 +self.DetectedItems={} +self.DetectionSet=DetectionSet +self.RefreshTimeInterval=30 +self:InitDetectVisual(nil) +self:InitDetectOptical(nil) +self:InitDetectRadar(nil) +self:InitDetectRWR(nil) +self:InitDetectIRST(nil) +self:InitDetectDLINK(nil) +self:FilterCategories({ +Unit.Category.AIRPLANE, +Unit.Category.GROUND_UNIT, +Unit.Category.HELICOPTER, +Unit.Category.SHIP, +Unit.Category.STRUCTURE +}) +self:SetFriendliesRange(6000) +self:SetStartState("Stopped") +self:AddTransition("Stopped","Start","Detecting") +self:AddTransition("Detecting","Detect","Detecting") +self:AddTransition("Detecting","Detection","Detecting") +self:AddTransition("Detecting","Detected","Detecting") +self:AddTransition("Detecting","DetectedItem","Detecting") +self:AddTransition("*","Stop","Stopped") +return self +end +do +function DETECTION_BASE:onafterStart(From,Event,To) +self:__Detect(1) +end +function DETECTION_BASE:onafterDetect(From,Event,To) +local DetectDelay=0.1 +self.DetectionCount=0 +self.DetectionRun=0 +self:UnIdentifyAllDetectedObjects() +local DetectionTimeStamp=timer.getTime() +for DetectionObjectName,DetectedObjectData in pairs(self.DetectedObjects)do +self.DetectedObjects[DetectionObjectName].IsDetected=false +self.DetectedObjects[DetectionObjectName].IsVisible=false +self.DetectedObjects[DetectionObjectName].KnowDistance=nil +self.DetectedObjects[DetectionObjectName].LastTime=nil +self.DetectedObjects[DetectionObjectName].LastPos=nil +self.DetectedObjects[DetectionObjectName].LastVelocity=nil +self.DetectedObjects[DetectionObjectName].Distance=10000000 +end +self.DetectionCount=self:CountAliveRecce() +local DetectionInterval=self.DetectionCount/(self.RefreshTimeInterval-1) +self:ForEachAliveRecce( +function(DetectionGroup) +self:__Detection(DetectDelay,DetectionGroup,DetectionTimeStamp) +DetectDelay=DetectDelay+DetectionInterval +end +) +self:__Detect(-self.RefreshTimeInterval) +end +function DETECTION_BASE:CountAliveRecce() +return self.DetectionSet:CountAlive() +end +function DETECTION_BASE:ForEachAliveRecce(IteratorFunction,...) +self:F2(arg) +self.DetectionSet:ForEachGroupAlive(IteratorFunction,arg) +return self +end +function DETECTION_BASE:onafterDetection(From,Event,To,Detection,DetectionTimeStamp) +self.DetectionRun=self.DetectionRun+1 +local HasDetectedObjects=false +if Detection and Detection:IsAlive()then +local DetectionGroupName=Detection:GetName() +local DetectionUnit=Detection:GetUnit(1) +local DetectedUnits={} +local DetectedTargets=Detection:GetDetectedTargets( +self.DetectVisual, +self.DetectOptical, +self.DetectRadar, +self.DetectIRST, +self.DetectRWR, +self.DetectDLINK +) +self:F({DetectedTargets=DetectedTargets}) +for DetectionObjectID,Detection in pairs(DetectedTargets)do +local DetectedObject=Detection.object +if DetectedObject and DetectedObject:isExist()and DetectedObject.id_<50000000 then +local DetectedObjectName=DetectedObject:getName() +if not self.DetectedObjects[DetectedObjectName]then +self.DetectedObjects[DetectedObjectName]=self.DetectedObjects[DetectedObjectName]or{} +self.DetectedObjects[DetectedObjectName].Name=DetectedObjectName +self.DetectedObjects[DetectedObjectName].Object=DetectedObject +end +end +end +for DetectionObjectName,DetectedObjectData in pairs(self.DetectedObjects)do +local DetectedObject=DetectedObjectData.Object +if DetectedObject:isExist()then +local TargetIsDetected,TargetIsVisible,TargetLastTime,TargetKnowType,TargetKnowDistance,TargetLastPos,TargetLastVelocity=DetectionUnit:IsTargetDetected( +DetectedObject, +self.DetectVisual, +self.DetectOptical, +self.DetectRadar, +self.DetectIRST, +self.DetectRWR, +self.DetectDLINK +) +local DetectionAccepted=true +local DetectedObjectName=DetectedObject:getName() +local DetectedObjectType=DetectedObject:getTypeName() +local DetectedObjectVec3=DetectedObject:getPoint() +local DetectedObjectVec2={x=DetectedObjectVec3.x,y=DetectedObjectVec3.z} +local DetectionGroupVec3=Detection:GetVec3() +local DetectionGroupVec2={x=DetectionGroupVec3.x,y=DetectionGroupVec3.z} +local Distance=((DetectedObjectVec3.x-DetectionGroupVec3.x)^2+ +(DetectedObjectVec3.y-DetectionGroupVec3.y)^2+ +(DetectedObjectVec3.z-DetectionGroupVec3.z)^2 +)^0.5/1000 +local DetectedUnitCategory=DetectedObject:getDesc().category +DetectionAccepted=self._.FilterCategories[DetectedUnitCategory]~=nil and DetectionAccepted or false +if self.AcceptRange and Distance*1000>self.AcceptRange then +DetectionAccepted=false +end +if self.AcceptZones then +local AnyZoneDetection=false +for AcceptZoneID,AcceptZone in pairs(self.AcceptZones)do +local AcceptZone=AcceptZone +if AcceptZone:IsVec2InZone(DetectedObjectVec2)then +AnyZoneDetection=true +end +end +if not AnyZoneDetection then +DetectionAccepted=false +end +end +if self.RejectZones then +for RejectZoneID,RejectZone in pairs(self.RejectZones)do +local RejectZone=RejectZone +if RejectZone:IsPointVec2InZone(DetectedObjectVec2)==true then +DetectionAccepted=false +end +end +end +if not self.DetectedObjects[DetectedObjectName]and TargetIsVisible and self.DistanceProbability then +local DistanceFactor=Distance/4 +local DistanceProbabilityReversed=(1-self.DistanceProbability)*DistanceFactor +local DistanceProbability=1-DistanceProbabilityReversed +DistanceProbability=DistanceProbability*30/300 +local Probability=math.random() +if Probability>DistanceProbability then +DetectionAccepted=false +end +end +if not self.DetectedObjects[DetectedObjectName]and TargetIsVisible and self.AlphaAngleProbability then +local NormalVec2={x=DetectedObjectVec2.x-DetectionGroupVec2.x,y=DetectedObjectVec2.y-DetectionGroupVec2.y} +local AlphaAngle=math.atan2(NormalVec2.y,NormalVec2.x) +local Sinus=math.sin(AlphaAngle) +local AlphaAngleProbabilityReversed=(1-self.AlphaAngleProbability)*(1-Sinus) +local AlphaAngleProbability=1-AlphaAngleProbabilityReversed +AlphaAngleProbability=AlphaAngleProbability*30/300 +local Probability=math.random() +if Probability>AlphaAngleProbability then +DetectionAccepted=false +end +end +if not self.DetectedObjects[DetectedObjectName]and TargetIsVisible and self.ZoneProbability then +for ZoneDataID,ZoneData in pairs(self.ZoneProbability)do +self:F({ZoneData}) +local ZoneObject=ZoneData[1] +local ZoneProbability=ZoneData[2] +ZoneProbability=ZoneProbability*30/300 +if ZoneObject:IsPointVec2InZone(DetectedObjectVec2)==true then +local Probability=math.random() +if Probability>ZoneProbability then +DetectionAccepted=false +break +end +end +end +end +if DetectionAccepted then +HasDetectedObjects=true +self.DetectedObjects[DetectedObjectName]=self.DetectedObjects[DetectedObjectName]or{} +self.DetectedObjects[DetectedObjectName].Name=DetectedObjectName +if TargetIsDetected and TargetIsDetected==true then +self.DetectedObjects[DetectedObjectName].IsDetected=TargetIsDetected +end +if TargetIsDetected and TargetIsVisible and TargetIsVisible==true then +self.DetectedObjects[DetectedObjectName].IsVisible=TargetIsDetected and TargetIsVisible +end +if TargetIsDetected and not self.DetectedObjects[DetectedObjectName].KnowType then +self.DetectedObjects[DetectedObjectName].KnowType=TargetIsDetected and TargetKnowType +end +self.DetectedObjects[DetectedObjectName].KnowDistance=TargetKnowDistance +self.DetectedObjects[DetectedObjectName].LastTime=(TargetIsDetected and TargetIsVisible==false)and TargetLastTime +self.DetectedObjects[DetectedObjectName].LastPos=(TargetIsDetected and TargetIsVisible==false)and TargetLastPos +self.DetectedObjects[DetectedObjectName].LastVelocity=(TargetIsDetected and TargetIsVisible==false)and TargetLastVelocity +if not self.DetectedObjects[DetectedObjectName].Distance or(Distance and self.DetectedObjects[DetectedObjectName].Distance>Distance)then +self.DetectedObjects[DetectedObjectName].Distance=Distance +end +self.DetectedObjects[DetectedObjectName].DetectionTimeStamp=DetectionTimeStamp +self:F({DetectedObject=self.DetectedObjects[DetectedObjectName]}) +local DetectedUnit=UNIT:FindByName(DetectedObjectName) +DetectedUnits[DetectedObjectName]=DetectedUnit +else +self:F({DetectedObject="No more detection for "..DetectedObjectName}) +if self.DetectedObjects[DetectedObjectName]then +self.DetectedObjects[DetectedObjectName]=nil +end +end +else +self:F("Removing from DetectedObjects: "..DetectionObjectName) +self.DetectedObjects[DetectionObjectName]=nil +end +end +if HasDetectedObjects then +self:__Detected(0.1,DetectedUnits) +end +end +if self.DetectionCount>0 and self.DetectionRun==self.DetectionCount then +for DetectedObjectName,DetectedObject in pairs(self.DetectedObjects)do +if self.DetectedObjects[DetectedObjectName].IsDetected==true and self.DetectedObjects[DetectedObjectName].DetectionTimeStamp+300<=DetectionTimeStamp then +self.DetectedObjects[DetectedObjectName].IsDetected=false +end +end +self:CreateDetectionItems() +for DetectedItemID,DetectedItem in pairs(self.DetectedItems)do +self:UpdateDetectedItemDetection(DetectedItem) +self:CleanDetectionItem(DetectedItem,DetectedItemID) +if DetectedItem then +self:__DetectedItem(0.1,DetectedItem) +end +end +end +end +end +do +function DETECTION_BASE:CleanDetectionItem(DetectedItem,DetectedItemID) +local DetectedSet=DetectedItem.Set +if DetectedSet:Count()==0 then +self:RemoveDetectedItem(DetectedItemID) +end +return self +end +function DETECTION_BASE:ForgetDetectedUnit(UnitName) +local DetectedItems=self:GetDetectedItems() +for DetectedItemIndex,DetectedItem in pairs(DetectedItems)do +local DetectedSet=self:GetDetectedItemSet(DetectedItem) +if DetectedSet then +DetectedSet:RemoveUnitsByName(UnitName) +end +end +return self +end +function DETECTION_BASE:CreateDetectionItems() +self:F("Error, in DETECTION_BASE class...") +return self +end +end +do +function DETECTION_BASE:InitDetectVisual(DetectVisual) +self.DetectVisual=DetectVisual +return self +end +function DETECTION_BASE:InitDetectOptical(DetectOptical) +self:F2() +self.DetectOptical=DetectOptical +return self +end +function DETECTION_BASE:InitDetectRadar(DetectRadar) +self:F2() +self.DetectRadar=DetectRadar +return self +end +function DETECTION_BASE:InitDetectIRST(DetectIRST) +self:F2() +self.DetectIRST=DetectIRST +return self +end +function DETECTION_BASE:InitDetectRWR(DetectRWR) +self:F2() +self.DetectRWR=DetectRWR +return self +end +function DETECTION_BASE:InitDetectDLINK(DetectDLINK) +self:F2() +self.DetectDLINK=DetectDLINK +return self +end +end +do +function DETECTION_BASE:FilterCategories(FilterCategories) +self:F2() +self._.FilterCategories={} +if type(FilterCategories)=="table"then +for CategoryID,Category in pairs(FilterCategories)do +self._.FilterCategories[Category]=Category +end +else +self._.FilterCategories[FilterCategories]=FilterCategories +end +return self +end +end +do +function DETECTION_BASE:SetRefreshTimeInterval(RefreshTimeInterval) +self:F2() +self.RefreshTimeInterval=RefreshTimeInterval +return self +end +end +do +function DETECTION_BASE:SetFriendliesRange(FriendliesRange) +self:F2() +self.FriendliesRange=FriendliesRange +return self +end +end +do +function DETECTION_BASE:SetIntercept(Intercept,InterceptDelay) +self:F2() +self.Intercept=Intercept +self.InterceptDelay=InterceptDelay +return self +end +end +do +function DETECTION_BASE:SetAcceptRange(AcceptRange) +self:F2() +self.AcceptRange=AcceptRange +return self +end +function DETECTION_BASE:SetAcceptZones(AcceptZones) +self:F2() +if type(AcceptZones)=="table"then +if AcceptZones.ClassName and AcceptZones:IsInstanceOf(ZONE_BASE)then +self.AcceptZones={AcceptZones} +else +self.AcceptZones=AcceptZones +end +else +self:F({"AcceptZones must be a list of ZONE_BASE derived objects or one ZONE_BASE derived object",AcceptZones}) +error() +end +return self +end +function DETECTION_BASE:SetRejectZones(RejectZones) +self:F2() +if type(RejectZones)=="table"then +if RejectZones.ClassName and RejectZones:IsInstanceOf(ZONE_BASE)then +self.RejectZones={RejectZones} +else +self.RejectZones=RejectZones +end +else +self:F({"RejectZones must be a list of ZONE_BASE derived objects or one ZONE_BASE derived object",RejectZones}) +error() +end +return self +end +end +do +function DETECTION_BASE:SetDistanceProbability(DistanceProbability) +self:F2() +self.DistanceProbability=DistanceProbability +return self +end +function DETECTION_BASE:SetAlphaAngleProbability(AlphaAngleProbability) +self:F2() +self.AlphaAngleProbability=AlphaAngleProbability +return self +end +function DETECTION_BASE:SetZoneProbability(ZoneArray) +self:F2() +self.ZoneProbability=ZoneArray +return self +end +end +do +function DETECTION_BASE:AcceptChanges(DetectedItem) +DetectedItem.Changed=false +DetectedItem.Changes={} +return self +end +function DETECTION_BASE:AddChangeItem(DetectedItem,ChangeCode,ItemUnitType) +DetectedItem.Changed=true +local ID=DetectedItem.ID +DetectedItem.Changes=DetectedItem.Changes or{} +DetectedItem.Changes[ChangeCode]=DetectedItem.Changes[ChangeCode]or{} +DetectedItem.Changes[ChangeCode].ID=ID +DetectedItem.Changes[ChangeCode].ItemUnitType=ItemUnitType +self:F({"Change on Detected Item:",DetectedItemID=DetectedItem.ID,ChangeCode=ChangeCode,ItemUnitType=ItemUnitType}) +return self +end +function DETECTION_BASE:AddChangeUnit(DetectedItem,ChangeCode,ChangeUnitType) +DetectedItem.Changed=true +local ID=DetectedItem.ID +DetectedItem.Changes=DetectedItem.Changes or{} +DetectedItem.Changes[ChangeCode]=DetectedItem.Changes[ChangeCode]or{} +DetectedItem.Changes[ChangeCode][ChangeUnitType]=DetectedItem.Changes[ChangeCode][ChangeUnitType]or 0 +DetectedItem.Changes[ChangeCode][ChangeUnitType]=DetectedItem.Changes[ChangeCode][ChangeUnitType]+1 +DetectedItem.Changes[ChangeCode].ID=ID +self:F({"Change on Detected Unit:",DetectedItemID=DetectedItem.ID,ChangeCode=ChangeCode,ChangeUnitType=ChangeUnitType}) +return self +end +end +do +function DETECTION_BASE:SetFriendlyPrefixes(FriendlyPrefixes) +self.FriendlyPrefixes=self.FriendlyPrefixes or{} +if type(FriendlyPrefixes)~="table"then +FriendlyPrefixes={FriendlyPrefixes} +end +for PrefixID,Prefix in pairs(FriendlyPrefixes)do +self:F({FriendlyPrefix=Prefix}) +self.FriendlyPrefixes[Prefix]=Prefix +end +return self +end +function DETECTION_BASE:IsFriendliesNearBy(DetectedItem,Category) +return(DetectedItem.FriendliesNearBy and DetectedItem.FriendliesNearBy[Category]~=nil)or false +end +function DETECTION_BASE:GetFriendliesNearBy(DetectedItem,Category) +return DetectedItem.FriendliesNearBy and DetectedItem.FriendliesNearBy[Category] +end +function DETECTION_BASE:IsFriendliesNearIntercept(DetectedItem) +return DetectedItem.FriendliesNearIntercept~=nil or false +end +function DETECTION_BASE:GetFriendliesNearIntercept(DetectedItem) +return DetectedItem.FriendliesNearIntercept +end +function DETECTION_BASE:GetFriendliesDistance(DetectedItem) +return DetectedItem.FriendliesDistance +end +function DETECTION_BASE:IsPlayersNearBy(DetectedItem) +return DetectedItem.PlayersNearBy~=nil +end +function DETECTION_BASE:GetPlayersNearBy(DetectedItem) +return DetectedItem.PlayersNearBy +end +function DETECTION_BASE:ReportFriendliesNearBy(TargetData) +local DetectedItem=TargetData.DetectedItem +local DetectedSet=TargetData.DetectedItem.Set +local DetectedUnit=DetectedSet:GetFirst() +DetectedItem.FriendliesNearBy=nil +if DetectedUnit and DetectedUnit:IsAlive()then +local DetectedUnitCoord=DetectedUnit:GetCoordinate() +local InterceptCoord=TargetData.InterceptCoord or DetectedUnitCoord +local SphereSearch={ +id=world.VolumeType.SPHERE, +params={ +point=InterceptCoord:GetVec3(), +radius=self.FriendliesRange, +} +} +local FindNearByFriendlies=function(FoundDCSUnit,ReportGroupData) +local DetectedItem=ReportGroupData.DetectedItem +local DetectedSet=ReportGroupData.DetectedItem.Set +local DetectedUnit=DetectedSet:GetFirst() +local DetectedUnitCoord=DetectedUnit:GetCoordinate() +local InterceptCoord=ReportGroupData.InterceptCoord or DetectedUnitCoord +local ReportSetGroup=ReportGroupData.ReportSetGroup +local EnemyCoalition=DetectedUnit:GetCoalition() +local FoundUnitCoalition=FoundDCSUnit:getCoalition() +local FoundUnitCategory=FoundDCSUnit:getDesc().category +local FoundUnitName=FoundDCSUnit:getName() +local FoundUnitGroupName=FoundDCSUnit:getGroup():getName() +local EnemyUnitName=DetectedUnit:GetName() +local FoundUnitInReportSetGroup=ReportSetGroup:FindGroup(FoundUnitGroupName)~=nil +if FoundUnitInReportSetGroup==true then +for PrefixID,Prefix in pairs(self.FriendlyPrefixes or{})do +if string.find(FoundUnitName,Prefix:gsub("-","%%-"),1)then +FoundUnitInReportSetGroup=false +break +end +end +end +if FoundUnitCoalition~=EnemyCoalition and FoundUnitInReportSetGroup==false then +local FriendlyUnit=UNIT:Find(FoundDCSUnit) +local FriendlyUnitName=FriendlyUnit:GetName() +local FriendlyUnitCategory=FriendlyUnit:GetDesc().category +DetectedItem.FriendliesNearBy=DetectedItem.FriendliesNearBy or{} +DetectedItem.FriendliesNearBy[FoundUnitCategory]=DetectedItem.FriendliesNearBy[FoundUnitCategory]or{} +DetectedItem.FriendliesNearBy[FoundUnitCategory][FriendlyUnitName]=FriendlyUnit +local Distance=DetectedUnitCoord:Get2DDistance(FriendlyUnit:GetCoordinate()) +DetectedItem.FriendliesDistance=DetectedItem.FriendliesDistance or{} +DetectedItem.FriendliesDistance[Distance]=FriendlyUnit +return true +end +return true +end +world.searchObjects(Object.Category.UNIT,SphereSearch,FindNearByFriendlies,TargetData) +DetectedItem.PlayersNearBy=nil +_DATABASE:ForEachPlayer( +function(PlayerUnitName) +local PlayerUnit=UNIT:FindByName(PlayerUnitName) +if PlayerUnit and PlayerUnit:IsAlive()then +local coord=PlayerUnit:GetCoordinate() +if coord and coord:IsInRadius(DetectedUnitCoord,self.FriendliesRange)then +local PlayerUnitCategory=PlayerUnit:GetDesc().category +if(not self.FriendliesCategory)or(self.FriendliesCategory and(self.FriendliesCategory==PlayerUnitCategory))then +local PlayerUnitName=PlayerUnit:GetName() +DetectedItem.PlayersNearBy=DetectedItem.PlayersNearBy or{} +DetectedItem.PlayersNearBy[PlayerUnitName]=PlayerUnit +DetectedItem.FriendliesNearBy=DetectedItem.FriendliesNearBy or{} +DetectedItem.FriendliesNearBy[PlayerUnitCategory]=DetectedItem.FriendliesNearBy[PlayerUnitCategory]or{} +DetectedItem.FriendliesNearBy[PlayerUnitCategory][PlayerUnitName]=PlayerUnit +local Distance=DetectedUnitCoord:Get2DDistance(PlayerUnit:GetCoordinate()) +DetectedItem.FriendliesDistance=DetectedItem.FriendliesDistance or{} +DetectedItem.FriendliesDistance[Distance]=PlayerUnit +end +end +end +end +) +end +self:F({Friendlies=DetectedItem.FriendliesNearBy,Players=DetectedItem.PlayersNearBy}) +end +end +function DETECTION_BASE:IsDetectedObjectIdentified(DetectedObject) +local DetectedObjectName=DetectedObject.Name +if DetectedObjectName then +local DetectedObjectIdentified=self.DetectedObjectsIdentified[DetectedObjectName]==true +return DetectedObjectIdentified +else +return nil +end +end +function DETECTION_BASE:IdentifyDetectedObject(DetectedObject) +local DetectedObjectName=DetectedObject.Name +self.DetectedObjectsIdentified[DetectedObjectName]=true +end +function DETECTION_BASE:UnIdentifyDetectedObject(DetectedObject) +local DetectedObjectName=DetectedObject.Name +self.DetectedObjectsIdentified[DetectedObjectName]=false +end +function DETECTION_BASE:UnIdentifyAllDetectedObjects() +self.DetectedObjectsIdentified={} +end +function DETECTION_BASE:GetDetectedObject(ObjectName) +self:F2({ObjectName=ObjectName}) +if ObjectName then +local DetectedObject=self.DetectedObjects[ObjectName] +if DetectedObject then +local DetectedUnit=UNIT:FindByName(ObjectName) +if DetectedUnit and DetectedUnit:IsAlive()then +if self:IsDetectedObjectIdentified(DetectedObject)==false then +return DetectedObject +end +end +end +end +return nil +end +function DETECTION_BASE:GetDetectedUnitTypeName(DetectedUnit) +if DetectedUnit and DetectedUnit:IsAlive()then +local DetectedUnitName=DetectedUnit:GetName() +local DetectedObject=self.DetectedObjects[DetectedUnitName] +if DetectedObject then +if DetectedObject.KnowType then +return DetectedUnit:GetTypeName() +else +return"Unknown" +end +else +return"Unknown" +end +else +return"Dead:"..DetectedUnit:GetName() +end +return"Undetected:"..DetectedUnit:GetName() +end +function DETECTION_BASE:AddDetectedItem(ItemPrefix,DetectedItemKey,Set) +local DetectedItem={} +self.DetectedItemCount=self.DetectedItemCount+1 +self.DetectedItemMax=self.DetectedItemMax+1 +DetectedItemKey=DetectedItemKey or self.DetectedItemMax +self.DetectedItems[DetectedItemKey]=DetectedItem +self.DetectedItemsByIndex[DetectedItemKey]=DetectedItem +DetectedItem.Index=DetectedItemKey +DetectedItem.Set=Set or SET_UNIT:New():FilterDeads():FilterCrashes() +DetectedItem.ItemID=ItemPrefix.."."..self.DetectedItemMax +DetectedItem.ID=self.DetectedItemMax +DetectedItem.Removed=false +if self.Locking then +self:LockDetectedItem(DetectedItem) +end +return DetectedItem +end +function DETECTION_BASE:AddDetectedItemZone(ItemPrefix,DetectedItemKey,Set,Zone) +self:F({ItemPrefix,DetectedItemKey,Set,Zone}) +local DetectedItem=self:AddDetectedItem(ItemPrefix,DetectedItemKey,Set) +DetectedItem.Zone=Zone +return DetectedItem +end +function DETECTION_BASE:RemoveDetectedItem(DetectedItemKey) +local DetectedItem=self.DetectedItems[DetectedItemKey] +if DetectedItem then +self.DetectedItemCount=self.DetectedItemCount-1 +local DetectedItemIndex=DetectedItem.Index +self.DetectedItemsByIndex[DetectedItemIndex]=nil +self.DetectedItems[DetectedItemKey]=nil +end +end +function DETECTION_BASE:GetDetectedItems() +return self.DetectedItems +end +function DETECTION_BASE:GetDetectedItemsByIndex() +return self.DetectedItemsByIndex +end +function DETECTION_BASE:GetDetectedItemsCount() +local DetectedCount=self.DetectedItemCount +return DetectedCount +end +function DETECTION_BASE:GetDetectedItemByKey(Key) +self:F({DetectedItems=self.DetectedItems}) +local DetectedItem=self.DetectedItems[Key] +if DetectedItem then +return DetectedItem +end +return nil +end +function DETECTION_BASE:GetDetectedItemByIndex(Index) +self:F({self.DetectedItemsByIndex}) +local DetectedItem=self.DetectedItemsByIndex[Index] +if DetectedItem then +return DetectedItem +end +return nil +end +function DETECTION_BASE:GetDetectedItemID(DetectedItem) +return DetectedItem and DetectedItem.ItemID or"" +end +function DETECTION_BASE:GetDetectedID(Index) +local DetectedItem=self.DetectedItemsByIndex[Index] +if DetectedItem then +return DetectedItem.ID +end +return"" +end +function DETECTION_BASE:GetDetectedItemSet(DetectedItem) +local DetectedSetUnit=DetectedItem and DetectedItem.Set +if DetectedSetUnit then +return DetectedSetUnit +end +return nil +end +function DETECTION_BASE:UpdateDetectedItemDetection(DetectedItem) +local IsDetected=false +for UnitName,UnitData in pairs(DetectedItem.Set:GetSet())do +local DetectedObject=self.DetectedObjects[UnitName] +self:F({UnitName=UnitName,IsDetected=DetectedObject.IsDetected}) +if DetectedObject.IsDetected then +IsDetected=true +break +end +end +self:F({IsDetected=DetectedItem.IsDetected}) +DetectedItem.IsDetected=IsDetected +return IsDetected +end +function DETECTION_BASE:IsDetectedItemDetected(DetectedItem) +return DetectedItem.IsDetected +end +do +function DETECTION_BASE:GetDetectedItemZone(DetectedItem) +local DetectedZone=DetectedItem and DetectedItem.Zone +if DetectedZone then +return DetectedZone +end +local Detected +return nil +end +end +function DETECTION_BASE:LockDetectedItems() +for DetectedItemID,DetectedItem in pairs(self.DetectedItems)do +self:LockDetectedItem(DetectedItem) +end +self.Locking=true +return self +end +function DETECTION_BASE:UnlockDetectedItems() +for DetectedItemID,DetectedItem in pairs(self.DetectedItems)do +self:UnlockDetectedItem(DetectedItem) +end +self.Locking=nil +return self +end +function DETECTION_BASE:IsDetectedItemLocked(DetectedItem) +return self.Locking and DetectedItem.Locked==true +end +function DETECTION_BASE:LockDetectedItem(DetectedItem) +DetectedItem.Locked=true +return self +end +function DETECTION_BASE:UnlockDetectedItem(DetectedItem) +DetectedItem.Locked=nil +return self +end +function DETECTION_BASE:SetDetectedItemCoordinate(DetectedItem,Coordinate,DetectedItemUnit) +self:F({Coordinate=Coordinate}) +if DetectedItem then +if DetectedItemUnit then +DetectedItem.Coordinate=Coordinate +DetectedItem.Coordinate:SetHeading(DetectedItemUnit:GetHeading()) +DetectedItem.Coordinate.y=DetectedItemUnit:GetAltitude() +DetectedItem.Coordinate:SetVelocity(DetectedItemUnit:GetVelocityMPS()) +end +end +end +function DETECTION_BASE:GetDetectedItemCoordinate(DetectedItem) +self:F({DetectedItem=DetectedItem}) +if DetectedItem then +return DetectedItem.Coordinate +end +return nil +end +function DETECTION_BASE:GetDetectedItemCoordinates() +local Coordinates={} +for DetectedItemID,DetectedItem in pairs(self:GetDetectedItems())do +Coordinates[DetectedItem]=self:GetDetectedItemCoordinate(DetectedItem) +end +return Coordinates +end +function DETECTION_BASE:SetDetectedItemThreatLevel(DetectedItem) +local DetectedSet=DetectedItem.Set +if DetectedItem then +DetectedItem.ThreatLevel,DetectedItem.ThreatText=DetectedSet:CalculateThreatLevelA2G() +end +end +function DETECTION_BASE:GetDetectedItemThreatLevel(DetectedItem) +self:F({DetectedItem=DetectedItem}) +if DetectedItem then +self:F({ThreatLevel=DetectedItem.ThreatLevel,ThreatText=DetectedItem.ThreatText}) +return DetectedItem.ThreatLevel or 0,DetectedItem.ThreatText or"" +end +return nil,"" +end +function DETECTION_BASE:DetectedItemReportSummary(DetectedItem,AttackGroup,Settings) +self:F() +return nil +end +function DETECTION_BASE:DetectedReportDetailed(AttackGroup) +self:F() +return nil +end +function DETECTION_BASE:GetDetectionSet() +local DetectionSet=self.DetectionSet +return DetectionSet +end +function DETECTION_BASE:NearestRecce(DetectedItem) +local NearestRecce=nil +local DistanceRecce=1000000000 +for RecceGroupName,RecceGroup in pairs(self.DetectionSet:GetSet())do +if RecceGroup and RecceGroup:IsAlive()then +for RecceUnit,RecceUnit in pairs(RecceGroup:GetUnits())do +if RecceUnit:IsActive()then +local RecceUnitCoord=RecceUnit:GetCoordinate() +local Distance=RecceUnitCoord:Get2DDistance(self:GetDetectedItemCoordinate(DetectedItem)) +if Distance0 then +DetectedItemCoordText=DetectedItemCoordinate:ToStringA2A(AttackGroup,Settings) +else +DetectedItemCoordText=DetectedItemCoordinate:ToStringA2G(AttackGroup,Settings) +end +local ThreatLevelA2G=self:GetDetectedItemThreatLevel(DetectedItem) +local DetectedItemsCount=DetectedSet:Count() +local DetectedItemsTypes=DetectedSet:GetTypeNames() +local Report=REPORT:New() +Report:Add(DetectedItemID..", "..DetectedItemCoordText) +Report:Add(string.format("Threat: [%s%s]",string.rep("■",ThreatLevelA2G),string.rep("□",10-ThreatLevelA2G))) +Report:Add(string.format("Type: %2d of %s",DetectedItemsCount,DetectedItemsTypes)) +return Report +end +return nil +end +function DETECTION_AREAS:DetectedReportDetailed(AttackGroup) +self:F() +local Report=REPORT:New() +for DetectedItemIndex,DetectedItem in pairs(self.DetectedItems)do +local DetectedItem=DetectedItem +local ReportSummary=self:DetectedItemReportSummary(DetectedItem,AttackGroup) +Report:SetTitle("Detected areas:") +Report:Add(ReportSummary:Text()) +end +local ReportText=Report:Text() +return ReportText +end +function DETECTION_AREAS:CalculateIntercept(DetectedItem) +local DetectedCoord=DetectedItem.Coordinate +local DetectedSpeed=DetectedCoord:GetVelocity() +local DetectedHeading=DetectedCoord:GetHeading() +if self.Intercept then +local DetectedSet=DetectedItem.Set +local TranslateDistance=DetectedSpeed*self.InterceptDelay +local InterceptCoord=DetectedCoord:Translate(TranslateDistance,DetectedHeading) +DetectedItem.InterceptCoord=InterceptCoord +else +DetectedItem.InterceptCoord=DetectedCoord +end +end +function DETECTION_AREAS:SmokeDetectedUnits() +self:F2() +self._SmokeDetectedUnits=true +return self +end +function DETECTION_AREAS:FlareDetectedUnits() +self:F2() +self._FlareDetectedUnits=true +return self +end +function DETECTION_AREAS:SmokeDetectedZones() +self:F2() +self._SmokeDetectedZones=true +return self +end +function DETECTION_AREAS:FlareDetectedZones() +self:F2() +self._FlareDetectedZones=true +return self +end +function DETECTION_AREAS:BoundDetectedZones() +self:F2() +self._BoundDetectedZones=true +return self +end +function DETECTION_AREAS:GetChangeText(DetectedItem) +self:F(DetectedItem) +local MT={} +for ChangeCode,ChangeData in pairs(DetectedItem.Changes)do +if ChangeCode=="AA"then +MT[#MT+1]="Detected new area "..ChangeData.ID..". The center target is a "..ChangeData.ItemUnitType.."." +end +if ChangeCode=="RAU"then +MT[#MT+1]="Changed area "..ChangeData.ID..". Removed the center target." +end +if ChangeCode=="AAU"then +MT[#MT+1]="Changed area "..ChangeData.ID..". The new center target is a "..ChangeData.ItemUnitType.."." +end +if ChangeCode=="RA"then +MT[#MT+1]="Removed old area "..ChangeData.ID..". No more targets in this area." +end +if ChangeCode=="AU"then +local MTUT={} +for ChangeUnitType,ChangeUnitCount in pairs(ChangeData)do +if ChangeUnitType~="ID"then +MTUT[#MTUT+1]=ChangeUnitCount.." of "..ChangeUnitType +end +end +MT[#MT+1]="Detected for area "..ChangeData.ID.." new target(s) "..table.concat(MTUT,", ").."." +end +if ChangeCode=="RU"then +local MTUT={} +for ChangeUnitType,ChangeUnitCount in pairs(ChangeData)do +if ChangeUnitType~="ID"then +MTUT[#MTUT+1]=ChangeUnitCount.." of "..ChangeUnitType +end +end +MT[#MT+1]="Removed for area "..ChangeData.ID.." invisible or destroyed target(s) "..table.concat(MTUT,", ").."." +end +end +return table.concat(MT,"\n") +end +function DETECTION_AREAS:CreateDetectionItems() +self:F("Checking Detected Items for new Detected Units ...") +for DetectedItemID,DetectedItemData in pairs(self.DetectedItems)do +local DetectedItem=DetectedItemData +if DetectedItem then +self:T2({"Detected Item ID: ",DetectedItemID}) +local DetectedSet=DetectedItem.Set +local AreaExists=false +self:T3({"Zone Center Unit:",DetectedItem.Zone.ZoneUNIT.UnitName}) +local DetectedZoneObject=self:GetDetectedObject(DetectedItem.Zone.ZoneUNIT.UnitName) +self:T3({"Detected Zone Object:",DetectedItem.Zone:GetName(),DetectedZoneObject}) +if DetectedZoneObject then +AreaExists=true +else +DetectedSet:RemoveUnitsByName(DetectedItem.Zone.ZoneUNIT.UnitName) +self:AddChangeItem(DetectedItem,'RAU',self:GetDetectedUnitTypeName(DetectedItem.Zone.ZoneUNIT)) +for DetectedUnitName,DetectedUnitData in pairs(DetectedSet:GetSet())do +local DetectedUnit=DetectedUnitData +local DetectedObject=self:GetDetectedObject(DetectedUnit.UnitName) +local DetectedUnitTypeName=self:GetDetectedUnitTypeName(DetectedUnit) +if DetectedObject then +self:IdentifyDetectedObject(DetectedObject) +AreaExists=true +DetectedItem.Zone=ZONE_UNIT:New(DetectedUnit:GetName(),DetectedUnit,self.DetectionZoneRange) +self:AddChangeItem(DetectedItem,"AAU",DetectedUnitTypeName) +break +else +DetectedSet:Remove(DetectedUnitName) +self:AddChangeUnit(DetectedItem,"RU",DetectedUnitTypeName) +end +end +end +if AreaExists then +for DetectedUnitName,DetectedUnitData in pairs(DetectedSet:GetSet())do +local DetectedUnit=DetectedUnitData +local DetectedUnitTypeName=self:GetDetectedUnitTypeName(DetectedUnit) +local DetectedObject=nil +if DetectedUnit:IsAlive()then +DetectedObject=self:GetDetectedObject(DetectedUnit:GetName()) +end +if DetectedObject then +if DetectedUnit:IsInZone(DetectedItem.Zone)then +self:IdentifyDetectedObject(DetectedObject) +DetectedSet:AddUnit(DetectedUnit) +else +DetectedSet:Remove(DetectedUnitName) +self:AddChangeUnit(DetectedItem,"RU",DetectedUnitTypeName) +end +else +self:AddChangeUnit(DetectedItem,"RU","destroyed target") +DetectedSet:Remove(DetectedUnitName) +end +end +else +self:RemoveDetectedItem(DetectedItemID) +self:AddChangeItem(DetectedItem,"RA") +end +end +end +for DetectedUnitName,DetectedObjectData in pairs(self.DetectedObjects)do +local DetectedObject=self:GetDetectedObject(DetectedUnitName) +if DetectedObject then +local DetectedUnit=UNIT:FindByName(DetectedUnitName) +local DetectedUnitTypeName=self:GetDetectedUnitTypeName(DetectedUnit) +local AddedToDetectionArea=false +for DetectedItemID,DetectedItemData in pairs(self.DetectedItems)do +local DetectedItem=DetectedItemData +if DetectedItem then +local DetectedSet=DetectedItem.Set +if not self:IsDetectedObjectIdentified(DetectedObject)and DetectedUnit:IsInZone(DetectedItem.Zone)then +self:IdentifyDetectedObject(DetectedObject) +DetectedSet:AddUnit(DetectedUnit) +AddedToDetectionArea=true +self:AddChangeUnit(DetectedItem,"AU",DetectedUnitTypeName) +end +end +end +if AddedToDetectionArea==false then +local DetectedItem=self:AddDetectedItemZone("AREA",nil, +SET_UNIT:New():FilterDeads():FilterCrashes(), +ZONE_UNIT:New(DetectedUnitName,DetectedUnit,self.DetectionZoneRange) +) +DetectedItem.Set:AddUnit(DetectedUnit) +self:AddChangeItem(DetectedItem,"AA",DetectedUnitTypeName) +end +end +end +for DetectedItemID,DetectedItemData in pairs(self.DetectedItems)do +local DetectedItem=DetectedItemData +local DetectedSet=DetectedItem.Set +local DetectedFirstUnit=DetectedSet:GetFirst() +local DetectedZone=DetectedItem.Zone +local DetectedZoneCoord=DetectedZone:GetCoordinate() +self:SetDetectedItemCoordinate(DetectedItem,DetectedZoneCoord,DetectedFirstUnit) +self:CalculateIntercept(DetectedItem) +local OldFriendliesNearbyGround=self:IsFriendliesNearBy(DetectedItem,Unit.Category.GROUND_UNIT) +self:ReportFriendliesNearBy({DetectedItem=DetectedItem,ReportSetGroup=self.DetectionSet}) +local NewFriendliesNearbyGround=self:IsFriendliesNearBy(DetectedItem,Unit.Category.GROUND_UNIT) +if OldFriendliesNearbyGround~=NewFriendliesNearbyGround then +DetectedItem.Changed=true +end +self:SetDetectedItemThreatLevel(DetectedItem) +self:NearestRecce(DetectedItem) +if DETECTION_AREAS._SmokeDetectedUnits or self._SmokeDetectedUnits then +DetectedZone.ZoneUNIT:SmokeRed() +end +DetectedSet:ForEachUnit( +function(DetectedUnit) +if DetectedUnit:IsAlive()then +if DETECTION_AREAS._FlareDetectedUnits or self._FlareDetectedUnits then +DetectedUnit:FlareGreen() +end +if DETECTION_AREAS._SmokeDetectedUnits or self._SmokeDetectedUnits then +DetectedUnit:SmokeGreen() +end +end +end +) +if DETECTION_AREAS._FlareDetectedZones or self._FlareDetectedZones then +DetectedZone:FlareZone(SMOKECOLOR.White,30,math.random(0,90)) +end +if DETECTION_AREAS._SmokeDetectedZones or self._SmokeDetectedZones then +DetectedZone:SmokeZone(SMOKECOLOR.White,30) +end +if DETECTION_AREAS._BoundDetectedZones or self._BoundDetectedZones then +self.CountryID=DetectedSet:GetFirst():GetCountry() +DetectedZone:BoundZone(12,self.CountryID) +end +end +end +end +do +DETECTION_ZONES={ +ClassName="DETECTION_ZONES", +DetectionZoneRange=nil, +} +function DETECTION_ZONES:New(DetectionSetZone,DetectionCoalition) +local self=BASE:Inherit(self,DETECTION_BASE:New(DetectionSetZone)) +self.DetectionSetZone=DetectionSetZone +self.DetectionCoalition=DetectionCoalition +self._SmokeDetectedUnits=false +self._FlareDetectedUnits=false +self._SmokeDetectedZones=false +self._FlareDetectedZones=false +self._BoundDetectedZones=false +return self +end +function DETECTION_ZONES:CountAliveRecce() +return self.DetectionSetZone:Count() +end +function DETECTION_ZONES:ForEachAliveRecce(IteratorFunction,...) +self:F2(arg) +self.DetectionSetZone:ForEachZone(IteratorFunction,arg) +return self +end +function DETECTION_ZONES:DetectedItemReportSummary(DetectedItem,AttackGroup,Settings) +self:F({DetectedItem=DetectedItem}) +local DetectedItemID=self:GetDetectedItemID(DetectedItem) +if DetectedItem then +local DetectedSet=self:GetDetectedItemSet(DetectedItem) +local ReportSummaryItem +local DetectedZone=self:GetDetectedItemZone(DetectedItem) +local DetectedItemCoordinate=DetectedZone:GetCoordinate() +local DetectedItemCoordText=DetectedItemCoordinate:ToString(AttackGroup,Settings) +local ThreatLevelA2G=self:GetDetectedItemThreatLevel(DetectedItem) +local DetectedItemsCount=DetectedSet:Count() +local DetectedItemsTypes=DetectedSet:GetTypeNames() +local Report=REPORT:New() +Report:Add(DetectedItemID..", "..DetectedItemCoordText) +Report:Add(string.format("Threat: [%s]",string.rep("■",ThreatLevelA2G),string.rep("□",10-ThreatLevelA2G))) +Report:Add(string.format("Type: %2d of %s",DetectedItemsCount,DetectedItemsTypes)) +Report:Add(string.format("Detected: %s",DetectedItem.IsDetected and"yes"or"no")) +return Report +end +return nil +end +function DETECTION_ZONES:DetectedReportDetailed(AttackGroup) +self:F() +local Report=REPORT:New() +for DetectedItemIndex,DetectedItem in pairs(self.DetectedItems)do +local DetectedItem=DetectedItem +local ReportSummary=self:DetectedItemReportSummary(DetectedItem,AttackGroup) +Report:SetTitle("Detected areas:") +Report:Add(ReportSummary:Text()) +end +local ReportText=Report:Text() +return ReportText +end +function DETECTION_ZONES:CalculateIntercept(DetectedItem) +local DetectedCoord=DetectedItem.Coordinate +DetectedItem.InterceptCoord=DetectedCoord +end +function DETECTION_ZONES:SmokeDetectedUnits() +self:F2() +self._SmokeDetectedUnits=true +return self +end +function DETECTION_ZONES:FlareDetectedUnits() +self:F2() +self._FlareDetectedUnits=true +return self +end +function DETECTION_ZONES:SmokeDetectedZones() +self:F2() +self._SmokeDetectedZones=true +return self +end +function DETECTION_ZONES:FlareDetectedZones() +self:F2() +self._FlareDetectedZones=true +return self +end +function DETECTION_ZONES:BoundDetectedZones() +self:F2() +self._BoundDetectedZones=true +return self +end +function DETECTION_ZONES:GetChangeText(DetectedItem) +self:F(DetectedItem) +local MT={} +for ChangeCode,ChangeData in pairs(DetectedItem.Changes)do +if ChangeCode=="AA"then +MT[#MT+1]="Detected new area "..ChangeData.ID..". The center target is a "..ChangeData.ItemUnitType.."." +end +if ChangeCode=="RAU"then +MT[#MT+1]="Changed area "..ChangeData.ID..". Removed the center target." +end +if ChangeCode=="AAU"then +MT[#MT+1]="Changed area "..ChangeData.ID..". The new center target is a "..ChangeData.ItemUnitType.."." +end +if ChangeCode=="RA"then +MT[#MT+1]="Removed old area "..ChangeData.ID..". No more targets in this area." +end +if ChangeCode=="AU"then +local MTUT={} +for ChangeUnitType,ChangeUnitCount in pairs(ChangeData)do +if ChangeUnitType~="ID"then +MTUT[#MTUT+1]=ChangeUnitCount.." of "..ChangeUnitType +end +end +MT[#MT+1]="Detected for area "..ChangeData.ID.." new target(s) "..table.concat(MTUT,", ").."." +end +if ChangeCode=="RU"then +local MTUT={} +for ChangeUnitType,ChangeUnitCount in pairs(ChangeData)do +if ChangeUnitType~="ID"then +MTUT[#MTUT+1]=ChangeUnitCount.." of "..ChangeUnitType +end +end +MT[#MT+1]="Removed for area "..ChangeData.ID.." invisible or destroyed target(s) "..table.concat(MTUT,", ").."." +end +end +return table.concat(MT,"\n") +end +function DETECTION_ZONES:CreateDetectionItems() +self:F("Checking Detected Items for new Detected Units ...") +local DetectedUnits=SET_UNIT:New() +for ZoneName,DetectionZone in pairs(self.DetectionSetZone:GetSet())do +local DetectedItem=self:GetDetectedItemByKey(ZoneName) +if DetectedItem==nil then +DetectedItem=self:AddDetectedItemZone("ZONE",ZoneName,nil,DetectionZone) +end +local DetectedItemSetUnit=self:GetDetectedItemSet(DetectedItem) +DetectionZone:Scan({Object.Category.UNIT},{Unit.Category.GROUND_UNIT}) +local ZoneUnits=DetectionZone:GetScannedUnits() +for DCSUnitID,DCSUnit in pairs(ZoneUnits)do +local UnitName=DCSUnit:getName() +local ZoneUnit=UNIT:FindByName(UnitName) +local ZoneUnitCoalition=ZoneUnit:GetCoalition() +if ZoneUnitCoalition==self.DetectionCoalition then +if DetectedItemSetUnit:FindUnit(UnitName)==nil and DetectedUnits:FindUnit(UnitName)==nil then +self:F("Adding "..UnitName) +DetectedItemSetUnit:AddUnit(ZoneUnit) +DetectedUnits:AddUnit(ZoneUnit) +end +end +end +end +for DetectedItemID,DetectedItemData in pairs(self.DetectedItems)do +local DetectedItem=DetectedItemData +local DetectedSet=self:GetDetectedItemSet(DetectedItem) +local DetectedFirstUnit=DetectedSet:GetFirst() +local DetectedZone=self:GetDetectedItemZone(DetectedItem) +local DetectedZoneCoord=DetectedZone:GetCoordinate() +self:SetDetectedItemCoordinate(DetectedItem,DetectedZoneCoord,DetectedFirstUnit) +self:CalculateIntercept(DetectedItem) +local OldFriendliesNearbyGround=self:IsFriendliesNearBy(DetectedItem,Unit.Category.GROUND_UNIT) +self:ReportFriendliesNearBy({DetectedItem=DetectedItem,ReportSetGroup=self.DetectionSetGroup}) +local NewFriendliesNearbyGround=self:IsFriendliesNearBy(DetectedItem,Unit.Category.GROUND_UNIT) +if OldFriendliesNearbyGround~=NewFriendliesNearbyGround then +DetectedItem.Changed=true +end +self:SetDetectedItemThreatLevel(DetectedItem) +if DETECTION_ZONES._SmokeDetectedUnits or self._SmokeDetectedUnits then +DetectedZone:SmokeZone(SMOKECOLOR.Red,30) +end +DetectedSet:ForEachUnit( +function(DetectedUnit) +if DetectedUnit:IsAlive()then +if DETECTION_ZONES._FlareDetectedUnits or self._FlareDetectedUnits then +DetectedUnit:FlareGreen() +end +if DETECTION_ZONES._SmokeDetectedUnits or self._SmokeDetectedUnits then +DetectedUnit:SmokeGreen() +end +end +end +) +if DETECTION_ZONES._FlareDetectedZones or self._FlareDetectedZones then +DetectedZone:FlareZone(SMOKECOLOR.White,30,math.random(0,90)) +end +if DETECTION_ZONES._SmokeDetectedZones or self._SmokeDetectedZones then +DetectedZone:SmokeZone(SMOKECOLOR.White,30) +end +if DETECTION_ZONES._BoundDetectedZones or self._BoundDetectedZones then +self.CountryID=DetectedSet:GetFirst():GetCountry() +DetectedZone:BoundZone(12,self.CountryID) +end +end +end +function DETECTION_ZONES:onafterDetection(From,Event,To,Detection,DetectionTimeStamp) +self.DetectionRun=self.DetectionRun+1 +if self.DetectionCount>0 and self.DetectionRun==self.DetectionCount then +self:CreateDetectionItems() +for DetectedItemID,DetectedItem in pairs(self.DetectedItems)do +self:UpdateDetectedItemDetection(DetectedItem) +self:CleanDetectionItem(DetectedItem,DetectedItemID) +if DetectedItem then +self:__DetectedItem(0.1,DetectedItem) +end +end +self:__Detect(-self.RefreshTimeInterval) +end +end +function DETECTION_ZONES:UpdateDetectedItemDetection(DetectedItem) +local IsDetected=true +DetectedItem.IsDetected=true +return IsDetected +end +end +do +DESIGNATE={ +ClassName="DESIGNATE", +} +function DESIGNATE:New(CC,Detection,AttackSet,Mission) +local self=BASE:Inherit(self,FSM:New()) +self:F({Detection}) +self:SetStartState("Designating") +self:AddTransition("*","Detect","*") +self:AddTransition("*","LaseOn","Lasing") +self:AddTransition("Lasing","Lasing","Lasing") +self:AddTransition("*","LaseOff","Designate") +self:AddTransition("*","Smoke","*") +self:AddTransition("*","Illuminate","*") +self:AddTransition("*","DoneSmoking","*") +self:AddTransition("*","DoneIlluminating","*") +self:AddTransition("*","Status","*") +self.CC=CC +self.Detection=Detection +self.AttackSet=AttackSet +self.RecceSet=Detection:GetDetectionSet() +self.Recces={} +self.Designating={} +self:SetDesignateName() +self:SetLaseDuration() +self:SetFlashStatusMenu(false) +self:SetFlashDetectionMessages(true) +self:SetMission(Mission) +self:SetLaserCodes({1688,1130,4785,6547,1465,4578}) +self:SetAutoLase(false,false) +self:SetThreatLevelPrioritization(false) +self:SetMaximumDesignations(5) +self:SetMaximumDistanceDesignations(8000) +self:SetMaximumMarkings(2) +self:SetDesignateMenu() +self.LaserCodesUsed={} +self.MenuLaserCodes={} +self.Detection:__Start(2) +self:__Detect(-15) +self.MarkScheduler=SCHEDULER:New(self) +return self +end +function DESIGNATE:SetFlashStatusMenu(FlashMenu) +self.FlashStatusMenu={} +self.AttackSet:ForEachGroupAlive( +function(AttackGroup) +self.FlashStatusMenu[AttackGroup]=FlashMenu +end +) +return self +end +function DESIGNATE:SetFlashDetectionMessages(FlashDetectionMessage) +self.FlashDetectionMessage={} +self.AttackSet:ForEachGroupAlive( +function(AttackGroup) +self.FlashDetectionMessage[AttackGroup]=FlashDetectionMessage +end +) +return self +end +function DESIGNATE:SetMaximumDesignations(MaximumDesignations) +self.MaximumDesignations=MaximumDesignations +return self +end +function DESIGNATE:SetMaximumDistanceGroundDesignation(MaximumDistanceGroundDesignation) +self.MaximumDistanceGroundDesignation=MaximumDistanceGroundDesignation +return self +end +function DESIGNATE:SetMaximumDistanceAirDesignation(MaximumDistanceAirDesignation) +self.MaximumDistanceAirDesignation=MaximumDistanceAirDesignation +return self +end +function DESIGNATE:SetMaximumDistanceDesignations(MaximumDistanceDesignations) +self.MaximumDistanceDesignations=MaximumDistanceDesignations +return self +end +function DESIGNATE:SetMaximumMarkings(MaximumMarkings) +self.MaximumMarkings=MaximumMarkings +return self +end +function DESIGNATE:SetLaserCodes(LaserCodes) +self.LaserCodes=(type(LaserCodes)=="table")and LaserCodes or{LaserCodes} +self:F({LaserCodes=self.LaserCodes}) +self.LaserCodesUsed={} +return self +end +function DESIGNATE:AddMenuLaserCode(LaserCode,MenuText) +self.MenuLaserCodes[LaserCode]=MenuText +self:SetDesignateMenu() +return self +end +function DESIGNATE:RemoveMenuLaserCode(LaserCode) +self.MenuLaserCodes[LaserCode]=nil +self:SetDesignateMenu() +return self +end +function DESIGNATE:SetDesignateName(DesignateName) +self.DesignateName="Designation"..(DesignateName and(" for "..DesignateName)or"") +return self +end +function DESIGNATE:SetLaseDuration(LaseDuration) +self.LaseDuration=LaseDuration or 120 +return self +end +function DESIGNATE:GenerateLaserCodes() +self.LaserCodes={} +local function containsDigit(_number,_numberToFind) +local _thisNumber=_number +local _thisDigit=0 +while _thisNumber~=0 do +_thisDigit=_thisNumber%10 +_thisNumber=math.floor(_thisNumber/10) +if _thisDigit==_numberToFind then +return true +end +end +return false +end +local _code=1111 +local _count=1 +while _code<1777 and _count<30 do +while true do +_code=_code+1 +if not containsDigit(_code,8) +and not containsDigit(_code,9) +and not containsDigit(_code,0)then +self:T(_code) +table.insert(self.LaserCodes,_code) +break +end +end +_count=_count+1 +end +self.LaserCodesUsed={} +return self +end +function DESIGNATE:SetAutoLase(AutoLase,Message) +self.AutoLase=AutoLase or false +if Message then +local AutoLaseOnOff=(self.AutoLase==true)and"On"or"Off" +local CC=self.CC:GetPositionable() +if CC then +CC:MessageToSetGroup(self.DesignateName..": Auto Lase "..AutoLaseOnOff..".",15,self.AttackSet) +end +end +self:CoordinateLase() +self:SetDesignateMenu() +return self +end +function DESIGNATE:SetThreatLevelPrioritization(Prioritize) +self.ThreatLevelPrioritization=Prioritize +return self +end +function DESIGNATE:SetMission(Mission) +self.Mission=Mission +return self +end +function DESIGNATE:onafterDetect() +self:__Detect(-math.random(60)) +self:DesignationScope() +self:CoordinateLase() +self:SendStatus() +self:SetDesignateMenu() +return self +end +function DESIGNATE:DesignationScope() +local DetectedItems=self.Detection:GetDetectedItemsByIndex() +local DetectedItemCount=0 +for DesignateIndex,Designating in pairs(self.Designating)do +local DetectedItem=self.Detection:GetDetectedItemByIndex(DesignateIndex) +if DetectedItem then +local IsDetected=self.Detection:IsDetectedItemDetected(DetectedItem) +self:F({IsDetected=IsDetected}) +if IsDetected==false then +self:F("Removing") +self.Designating[DesignateIndex]=nil +self.AttackSet:ForEachGroupAlive( +function(AttackGroup) +if AttackGroup:IsAlive()==true then +local DetectionText=self.Detection:DetectedItemReportSummary(DetectedItem,AttackGroup):Text(", ") +self.CC:GetPositionable():MessageToGroup("Targets out of LOS\n"..DetectionText,10,AttackGroup,self.DesignateName) +end +end +) +else +DetectedItemCount=DetectedItemCount+1 +end +else +self.Designating[DesignateIndex]=nil +end +end +if DetectedItemCount<5 then +for DesignateIndex,DetectedItem in pairs(DetectedItems)do +local IsDetected=self.Detection:IsDetectedItemDetected(DetectedItem) +if IsDetected==true then +self:F({DistanceRecce=DetectedItem.DistanceRecce}) +if DetectedItem.DistanceRecce<=self.MaximumDistanceDesignations then +if self.Designating[DesignateIndex]==nil then +self.AttackSet:ForEachGroupAlive( +function(AttackGroup) +if self.FlashDetectionMessage[AttackGroup]==true then +local DetectionText=self.Detection:DetectedItemReportSummary(DetectedItem,AttackGroup):Text(", ") +self.CC:GetPositionable():MessageToGroup("Targets detected at \n"..DetectionText,10,AttackGroup,self.DesignateName) +end +end +) +self.Designating[DesignateIndex]="" +break +end +end +end +end +end +return self +end +function DESIGNATE:CoordinateLase() +local DetectedItems=self.Detection:GetDetectedItemsByIndex() +for DesignateIndex,Designating in pairs(self.Designating)do +local DetectedItem=DetectedItems[DesignateIndex] +if DetectedItem then +if self.AutoLase then +self:LaseOn(DesignateIndex,self.LaseDuration) +end +end +end +return self +end +function DESIGNATE:SendStatus(MenuAttackGroup) +self.AttackSet:ForEachGroupAlive( +function(AttackGroup) +if self.FlashStatusMenu[AttackGroup]or(MenuAttackGroup and(AttackGroup:GetName()==MenuAttackGroup:GetName()))then +local DetectedReport=REPORT:New("Targets ready for Designation:") +local DetectedItems=self.Detection:GetDetectedItemsByIndex() +for DesignateIndex,Designating in pairs(self.Designating)do +local DetectedItem=DetectedItems[DesignateIndex] +if DetectedItem then +local Report=self.Detection:DetectedItemReportSummary(DetectedItem,AttackGroup):Text(", ") +DetectedReport:Add(string.rep("-",140)) +DetectedReport:Add(" - "..Report) +if string.find(Designating,"L")then +DetectedReport:Add(" - ".."Lasing Targets") +end +if string.find(Designating,"S")then +DetectedReport:Add(" - ".."Smoking Targets") +end +if string.find(Designating,"I")then +DetectedReport:Add(" - ".."Illuminating Area") +end +end +end +local CC=self.CC:GetPositionable() +CC:MessageTypeToGroup(DetectedReport:Text("\n"),MESSAGE.Type.Information,AttackGroup,self.DesignateName) +local DesignationReport=REPORT:New("Marking Targets:") +self.RecceSet:ForEachGroupAlive( +function(RecceGroup) +local RecceUnits=RecceGroup:GetUnits() +for UnitID,RecceData in pairs(RecceUnits)do +local Recce=RecceData +if Recce:IsLasing()then +DesignationReport:Add(" - "..Recce:GetMessageText("Marking "..Recce:GetSpot().Target:GetTypeName().." with laser "..Recce:GetSpot().LaserCode..".")) +end +end +end +) +CC:MessageTypeToGroup(DesignationReport:Text(),MESSAGE.Type.Information,AttackGroup,self.DesignateName) +end +end +) +return self +end +function DESIGNATE:SetMenu(AttackGroup) +self.MenuDesignate=self.MenuDesignate or{} +local MissionMenu=nil +if self.Mission then +MissionMenu=self.Mission:GetMenu(AttackGroup) +end +local MenuTime=timer.getTime() +self.MenuDesignate[AttackGroup]=MENU_GROUP_DELAYED:New(AttackGroup,self.DesignateName,MissionMenu):SetTime(MenuTime):SetTag(self.DesignateName) +local MenuDesignate=self.MenuDesignate[AttackGroup] +if self.AutoLase then +MENU_GROUP_COMMAND_DELAYED:New(AttackGroup,"Auto Lase Off",MenuDesignate,self.MenuAutoLase,self,false):SetTime(MenuTime):SetTag(self.DesignateName) +else +MENU_GROUP_COMMAND_DELAYED:New(AttackGroup,"Auto Lase On",MenuDesignate,self.MenuAutoLase,self,true):SetTime(MenuTime):SetTag(self.DesignateName) +end +local StatusMenu=MENU_GROUP_DELAYED:New(AttackGroup,"Status",MenuDesignate):SetTime(MenuTime):SetTag(self.DesignateName) +MENU_GROUP_COMMAND_DELAYED:New(AttackGroup,"Report Status",StatusMenu,self.MenuStatus,self,AttackGroup):SetTime(MenuTime):SetTag(self.DesignateName) +if self.FlashStatusMenu[AttackGroup]then +MENU_GROUP_COMMAND_DELAYED:New(AttackGroup,"Flash Status Report Off",StatusMenu,self.MenuFlashStatus,self,AttackGroup,false):SetTime(MenuTime):SetTag(self.DesignateName) +else +MENU_GROUP_COMMAND_DELAYED:New(AttackGroup,"Flash Status Report On",StatusMenu,self.MenuFlashStatus,self,AttackGroup,true):SetTime(MenuTime):SetTag(self.DesignateName) +end +local DesignateCount=0 +for DesignateIndex,Designating in pairs(self.Designating)do +local DetectedItem=self.Detection:GetDetectedItemByIndex(DesignateIndex) +if DetectedItem then +local Coord=self.Detection:GetDetectedItemCoordinate(DetectedItem) +local ID=self.Detection:GetDetectedItemID(DetectedItem) +local MenuText=ID +if DetectedItem.DesignateMenuName then +MenuText=string.format("(%3s) %s",Designating,DetectedItem.DesignateMenuName) +else +MenuText=string.format("(%3s) %s",Designating,MenuText) +end +local DetectedMenu=MENU_GROUP_DELAYED:New(AttackGroup,MenuText,MenuDesignate):SetTime(MenuTime):SetTag(self.DesignateName) +if string.find(Designating,"L",1,true)==nil then +MENU_GROUP_COMMAND_DELAYED:New(AttackGroup,"Search other target",DetectedMenu,self.MenuForget,self,DesignateIndex):SetTime(MenuTime):SetTag(self.DesignateName) +for LaserCode,MenuText in pairs(self.MenuLaserCodes)do +MENU_GROUP_COMMAND_DELAYED:New(AttackGroup,string.format(MenuText,LaserCode),DetectedMenu,self.MenuLaseCode,self,DesignateIndex,self.LaseDuration,LaserCode):SetTime(MenuTime):SetTag(self.DesignateName) +end +MENU_GROUP_COMMAND_DELAYED:New(AttackGroup,"Lase with random laser code(s)",DetectedMenu,self.MenuLaseOn,self,DesignateIndex,self.LaseDuration):SetTime(MenuTime):SetTag(self.DesignateName) +else +MENU_GROUP_COMMAND_DELAYED:New(AttackGroup,"Stop lasing",DetectedMenu,self.MenuLaseOff,self,DesignateIndex):SetTime(MenuTime):SetTag(self.DesignateName) +end +if string.find(Designating,"S",1,true)==nil then +MENU_GROUP_COMMAND_DELAYED:New(AttackGroup,"Smoke red",DetectedMenu,self.MenuSmoke,self,DesignateIndex,SMOKECOLOR.Red):SetTime(MenuTime):SetTag(self.DesignateName) +MENU_GROUP_COMMAND_DELAYED:New(AttackGroup,"Smoke blue",DetectedMenu,self.MenuSmoke,self,DesignateIndex,SMOKECOLOR.Blue):SetTime(MenuTime):SetTag(self.DesignateName) +MENU_GROUP_COMMAND_DELAYED:New(AttackGroup,"Smoke green",DetectedMenu,self.MenuSmoke,self,DesignateIndex,SMOKECOLOR.Green):SetTime(MenuTime):SetTag(self.DesignateName) +MENU_GROUP_COMMAND_DELAYED:New(AttackGroup,"Smoke white",DetectedMenu,self.MenuSmoke,self,DesignateIndex,SMOKECOLOR.White):SetTime(MenuTime):SetTag(self.DesignateName) +MENU_GROUP_COMMAND_DELAYED:New(AttackGroup,"Smoke orange",DetectedMenu,self.MenuSmoke,self,DesignateIndex,SMOKECOLOR.Orange):SetTime(MenuTime):SetTag(self.DesignateName) +end +if string.find(Designating,"I",1,true)==nil then +MENU_GROUP_COMMAND_DELAYED:New(AttackGroup,"Illuminate",DetectedMenu,self.MenuIlluminate,self,DesignateIndex):SetTime(MenuTime):SetTag(self.DesignateName) +end +end +DesignateCount=DesignateCount+1 +if DesignateCount>10 then +break +end +end +MenuDesignate:Remove(MenuTime,self.DesignateName) +MenuDesignate:Set() +end +function DESIGNATE:SetDesignateMenu() +self.AttackSet:Flush(self) +local Delay=1 +self.AttackSet:ForEachGroupAlive( +function(AttackGroup) +self:ScheduleOnce(Delay,self.SetMenu,self,AttackGroup) +Delay=Delay+1 +end +) +return self +end +function DESIGNATE:MenuStatus(AttackGroup) +self:F("Status") +self:SendStatus(AttackGroup) +end +function DESIGNATE:MenuFlashStatus(AttackGroup,Flash) +self:F("Flash Status") +self.FlashStatusMenu[AttackGroup]=Flash +self:SetDesignateMenu() +end +function DESIGNATE:MenuForget(Index) +self:F("Forget") +self.Designating[Index]="" +self:SetDesignateMenu() +end +function DESIGNATE:MenuAutoLase(AutoLase) +self:F("AutoLase") +self:SetAutoLase(AutoLase,true) +end +function DESIGNATE:MenuSmoke(Index,Color) +self:F("Designate through Smoke") +if string.find(self.Designating[Index],"S")==nil then +self.Designating[Index]=self.Designating[Index].."S" +end +self:Smoke(Index,Color) +self:SetDesignateMenu() +end +function DESIGNATE:MenuIlluminate(Index) +self:F("Designate through Illumination") +if string.find(self.Designating[Index],"I",1,true)==nil then +self.Designating[Index]=self.Designating[Index].."I" +end +self:__Illuminate(1,Index) +self:SetDesignateMenu() +end +function DESIGNATE:MenuLaseOn(Index,Duration) +self:F("Designate through Lase") +self:__LaseOn(1,Index,Duration) +self:SetDesignateMenu() +end +function DESIGNATE:MenuLaseCode(Index,Duration,LaserCode) +self:F("Designate through Lase using "..LaserCode) +self:__LaseOn(1,Index,Duration,LaserCode) +self:SetDesignateMenu() +end +function DESIGNATE:MenuLaseOff(Index,Duration) +self:F("Lasing off") +self.Designating[Index]=string.gsub(self.Designating[Index],"L","") +self:__LaseOff(1,Index) +self:SetDesignateMenu() +end +function DESIGNATE:onafterLaseOn(From,Event,To,Index,Duration,LaserCode) +if string.find(self.Designating[Index],"L",1,true)==nil then +self.Designating[Index]=self.Designating[Index].."L" +self.LaseStart=timer.getTime() +self.LaseDuration=Duration +self:Lasing(Index,Duration,LaserCode) +end +end +function DESIGNATE:onafterLasing(From,Event,To,Index,Duration,LaserCodeRequested) +local DetectedItem=self.Detection:GetDetectedItemByIndex(Index) +local TargetSetUnit=self.Detection:GetDetectedItemSet(DetectedItem) +local MarkingCount=0 +local MarkedTypes={} +local ReportTypes=REPORT:New() +local ReportLaserCodes=REPORT:New() +TargetSetUnit:Flush(self) +for TargetUnit,RecceData in pairs(self.Recces)do +local Recce=RecceData +self:F({TargetUnit=TargetUnit,Recce=Recce:GetName()}) +if not Recce:IsLasing()then +local LaserCode=Recce:GetLaserCode() +self:F({ClearingLaserCode=LaserCode}) +self.LaserCodesUsed[LaserCode]=nil +self.Recces[TargetUnit]=nil +end +end +if LaserCodeRequested then +for TargetUnit,RecceData in pairs(self.Recces)do +local Recce=RecceData +self:F({TargetUnit=TargetUnit,Recce=Recce:GetName()}) +if Recce:IsLasing()then +Recce:LaseOff() +local LaserCode=Recce:GetLaserCode() +self:F({ClearingLaserCode=LaserCode}) +self.LaserCodesUsed[LaserCode]=nil +self.Recces[TargetUnit]=nil +break +end +end +end +if self.AutoLase or(not self.AutoLase and(self.LaseStart+Duration>=timer.getTime()))then +TargetSetUnit:ForEachUnitPerThreatLevel(10,0, +function(TargetUnit) +self:F({TargetUnit=TargetUnit:GetName()}) +if MarkingCount0 and self.takeoff~=RAT.wp.air then +self.takeoff=RAT.wp.air +self:E(RAT.id..string.format("ERROR: At least one zone defined as departure and takeoff is NOT set to air. Enabling air start for RAT group %s!",self.alias)) +end +if self.Ndeparture_Airports==0 and self.Ndeparture_Zone==0 then +self.random_departure=true +local text=string.format("No airports or zones found given in SetDeparture(). Enabling random departure airports for RAT group %s!",self.alias) +self:E(RAT.id.."ERROR: "..text) +MESSAGE:New(text,30):ToAll() +end +end +if not self.random_destination then +for _,name in pairs(self.destination_ports)do +if self:_AirportExists(name)then +self.Ndestination_Airports=self.Ndestination_Airports+1 +elseif self:_ZoneExists(name)then +self.Ndestination_Zones=self.Ndestination_Zones+1 +end +end +if self.Ndestination_Zones>0 and self.landing~=RAT.wp.air and not self.returnzone then +self.landing=RAT.wp.air +self.destinationzone=true +self:E(RAT.id.."ERROR: At least one zone defined as destination and landing is NOT set to air. Enabling destination zone!") +end +if self.Ndestination_Airports==0 and self.Ndestination_Zones==0 then +self.random_destination=true +local text="No airports or zones found given in SetDestination(). Enabling random destination airports!" +self:E(RAT.id.."ERROR: "..text) +MESSAGE:New(text,30):ToAll() +end +end +if self.destinationzone and self.returnzone then +self:E(RAT.id.."ERROR: Destination zone _and_ return to zone not possible! Disabling return to zone.") +self.returnzone=false +end +if self.returnzone and self.takeoff==RAT.wp.air then +self.landing=RAT.wp.air +end +if self.FLminuser then +self.FLminuser=math.min(self.FLminuser,self.aircraft.ceiling) +end +if self.FLmaxuser then +self.FLmaxuser=math.min(self.FLmaxuser,self.aircraft.ceiling) +end +if self.FLcruise then +self.FLcruise=math.min(self.FLcruise,self.aircraft.ceiling) +end +if self.FLminuser and self.FLmaxuser then +if self.FLminuser>self.FLmaxuser then +local min=self.FLminuser +local max=self.FLmaxuser +self.FLminuser=max +self.FLmaxuser=min +end +end +if self.FLminuser and self.FLcruiseself.FLmaxuser then +self.FLcruise=self.FLmaxuser +end +if self.uncontrolled then +self.takeoff=RAT.wp.cold +end +end +function RAT:SetCoalition(friendly) +self:F2(friendly) +if friendly:lower()=="sameonly"then +self.friendly=RAT.coal.sameonly +elseif friendly:lower()=="neutral"then +self.friendly=RAT.coal.neutral +else +self.friendly=RAT.coal.same +end +return self +end +function RAT:SetCoalitionAircraft(color) +self:F2(color) +if color:lower()=="blue"then +self.coalition=coalition.side.BLUE +if not self.country then +self.country=country.id.USA +end +elseif color:lower()=="red"then +self.coalition=coalition.side.RED +if not self.country then +self.country=country.id.RUSSIA +end +elseif color:lower()=="neutral"then +self.coalition=coalition.side.NEUTRAL +if not self.country then +self.country=country.id.SWITZERLAND +end +end +return self +end +function RAT:SetCountry(id) +self:F2(id) +self.country=id +return self +end +function RAT:SetTerminalType(termtype) +self:F2(termtype) +self.termtype=termtype +return self +end +function RAT:SetParkingScanRadius(radius) +self:F2(radius) +self.parkingscanradius=radius or 50 +return self +end +function RAT:SetParkingScanSceneryON() +self:F2() +self.parkingscanscenery=true +return self +end +function RAT:SetParkingScanSceneryOFF() +self:F2() +self.parkingscanscenery=false +return self +end +function RAT:SetParkingSpotSafeON() +self:F2() +self.parkingverysafe=true +return self +end +function RAT:SetParkingSpotSafeOFF() +self:F2() +self.parkingverysafe=false +return self +end +function RAT:SetDespawnAirOFF() +self.despawnair=false +return self +end +function RAT:SetTakeoff(type) +self:F2(type) +local _Type +if type:lower()=="takeoff-cold"or type:lower()=="cold"then +_Type=RAT.wp.cold +elseif type:lower()=="takeoff-hot"or type:lower()=="hot"then +_Type=RAT.wp.hot +elseif type:lower()=="takeoff-runway"or type:lower()=="runway"then +_Type=RAT.wp.runway +elseif type:lower()=="air"then +_Type=RAT.wp.air +else +_Type=RAT.wp.coldorhot +end +self.takeoff=_Type +return self +end +function RAT:SetTakeoffCold() +self.takeoff=RAT.wp.cold +return self +end +function RAT:SetTakeoffHot() +self.takeoff=RAT.wp.hot +return self +end +function RAT:SetTakeoffRunway() +self.takeoff=RAT.wp.runway +return self +end +function RAT:SetTakeoffColdOrHot() +self.takeoff=RAT.wp.coldorhot +return self +end +function RAT:SetTakeoffAir() +self.takeoff=RAT.wp.air +return self +end +function RAT:SetDeparture(departurenames) +self:F2(departurenames) +self.random_departure=false +local names +if type(departurenames)=="table"then +names=departurenames +elseif type(departurenames)=="string"then +names={departurenames} +else +self:E(RAT.id.."ERROR: Input parameter must be a string or a table in SetDeparture()!") +end +for _,name in pairs(names)do +if self:_AirportExists(name)then +table.insert(self.departure_ports,name) +elseif self:_ZoneExists(name)then +table.insert(self.departure_ports,name) +else +self:E(RAT.id.."ERROR: No departure airport or zone found with name "..name) +end +end +return self +end +function RAT:SetDestination(destinationnames) +self:F2(destinationnames) +self.random_destination=false +local names +if type(destinationnames)=="table"then +names=destinationnames +elseif type(destinationnames)=="string"then +names={destinationnames} +else +self:E(RAT.id.."ERROR: Input parameter must be a string or a table in SetDestination()!") +end +for _,name in pairs(names)do +if self:_AirportExists(name)then +table.insert(self.destination_ports,name) +elseif self:_ZoneExists(name)then +table.insert(self.destination_ports,name) +else +self:E(RAT.id.."ERROR: No destination airport or zone found with name "..name) +end +end +return self +end +function RAT:DestinationZone() +self:F2() +self.destinationzone=true +self.landing=RAT.wp.air +return self +end +function RAT:ReturnZone() +self:F2() +self.returnzone=true +return self +end +function RAT:SetDestinationsFromZone(zone) +self:F2(zone) +self.random_destination=false +self.destination_Azone=zone +return self +end +function RAT:SetDeparturesFromZone(zone) +self:F2(zone) +self.random_departure=false +self.departure_Azone=zone +return self +end +function RAT:AddFriendlyAirportsToDepartures() +self:F2() +self.addfriendlydepartures=true +return self +end +function RAT:AddFriendlyAirportsToDestinations() +self:F2() +self.addfriendlydestinations=true +return self +end +function RAT:ExcludedAirports(ports) +self:F2(ports) +if type(ports)=="string"then +self.excluded_ports={ports} +else +self.excluded_ports=ports +end +return self +end +function RAT:SetAISkill(skill) +self:F2(skill) +if skill:lower()=="average"then +self.skill="Average" +elseif skill:lower()=="good"then +self.skill="Good" +elseif skill:lower()=="excellent"then +self.skill="Excellent" +elseif skill:lower()=="random"then +self.skill="Random" +else +self.skill="High" +end +return self +end +function RAT:Livery(skins) +self:F2(skins) +if type(skins)=="string"then +self.livery={skins} +else +self.livery=skins +end +return self +end +function RAT:ChangeAircraft(actype) +self:F2(actype) +self.actype=actype +return self +end +function RAT:ContinueJourney() +self:F2() +self.continuejourney=true +self.commute=false +return self +end +function RAT:Commute(starshape) +self:F2() +self.commute=true +self.continuejourney=false +if starshape then +self.starshape=starshape +else +self.starshape=false +end +return self +end +function RAT:SetSpawnDelay(delay) +self:F2(delay) +delay=delay or 5 +self.spawndelay=math.max(0.5,delay) +return self +end +function RAT:SetSpawnInterval(interval) +self:F2(interval) +interval=interval or 5 +self.spawninterval=math.max(0.5,interval) +return self +end +function RAT:RespawnAfterLanding(delay) +self:F2(delay) +delay=delay or 180 +self.respawn_at_landing=true +delay=math.max(1.0,delay) +self.respawn_delay=delay +return self +end +function RAT:SetRespawnDelay(delay) +self:F2(delay) +delay=delay or 1.0 +delay=math.max(1.0,delay) +self.respawn_delay=delay +return self +end +function RAT:NoRespawn() +self:F2() +self.norespawn=true +return self +end +function RAT:SetMaxRespawnTriedWhenSpawnedOnRunway(n) +self:F2(n) +n=n or 3 +self.onrunwaymaxretry=n +return self +end +function RAT:RespawnAfterTakeoff() +self:F2() +self.respawn_after_takeoff=true +return self +end +function RAT:RespawnAfterCrashON() +self:F2() +self.respawn_after_crash=true +return self +end +function RAT:RespawnAfterCrashOFF() +self:F2() +self.respawn_after_crash=false +return self +end +function RAT:RespawnInAirAllowed() +self:F2() +self.respawn_inair=true +return self +end +function RAT:RespawnInAirNotAllowed() +self:F2() +self.respawn_inair=false +return self +end +function RAT:CheckOnRunway(switch,distance) +self:F2(switch) +if switch==nil then +switch=true +end +self.checkonrunway=switch +self.onrunwayradius=distance or 75 +return self +end +function RAT:CheckOnTop(switch,radius) +self:F2(switch) +if switch==nil then +switch=true +end +self.checkontop=switch +self.ontopradius=radius or 2 +return self +end +function RAT:ParkingSpotDB(switch) +self:E("RAT ParkingSpotDB function is obsolete and will be removed soon!") +return self +end +function RAT:RadioON() +self:F2() +self.radio=true +return self +end +function RAT:RadioOFF() +self:F2() +self.radio=false +return self +end +function RAT:RadioFrequency(frequency) +self:F2(frequency) +self.frequency=frequency +return self +end +function RAT:RadioModulation(modulation) +self:F2(modulation) +if modulation=="AM"then +self.modulation=radio.modulation.AM +elseif modulation=="FM"then +self.modulation=radio.modulation.FM +else +self.modulation=radio.modulation.AM +end +return self +end +function RAT:RadioMenuON() +self:F2() +self.f10menu=true +return self +end +function RAT:RadioMenuOFF() +self:F2() +self.f10menu=false +return self +end +function RAT:Invisible() +self:F2() +self.invisible=true +return self +end +function RAT:SetEPLRS(switch) +if switch==nil or switch==true then +self.eplrs=true +else +self.eplrs=false +end +return self +end +function RAT:Immortal() +self:F2() +self.immortal=true +return self +end +function RAT:Uncontrolled() +self:F2() +self.uncontrolled=true +return self +end +function RAT:ActivateUncontrolled(maxactivated,delay,delta,frand) +self:F2({max=maxactivated,delay=delay,delta=delta,rand=frand}) +self.activate_uncontrolled=true +self.activate_max=maxactivated or 1 +self.activate_delay=delay or 1 +self.activate_delta=delta or 1 +self.activate_frand=frand or 0 +self.activate_delay=math.max(self.activate_delay,1) +self.activate_delta=math.max(self.activate_delta,0) +self.activate_frand=math.max(self.activate_frand,0) +self.activate_frand=math.min(self.activate_frand,1) +return self +end +function RAT:TimeDestroyInactive(time) +self:F2(time) +time=time or self.Tinactive +time=math.max(time,60) +self.Tinactive=time +return self +end +function RAT:SetMaxCruiseSpeed(speed) +self:F2(speed) +self.Vcruisemax=speed/3.6 +return self +end +function RAT:SetClimbRate(rate) +self:F2(rate) +rate=rate or self.Vclimb +rate=math.max(rate,100) +rate=math.min(rate,15000) +self.Vclimb=rate +return self +end +function RAT:SetDescentAngle(angle) +self:F2(angle) +angle=angle or self.AlphaDescent +angle=math.max(angle,0.5) +angle=math.min(angle,50) +self.AlphaDescent=angle +return self +end +function RAT:SetROE(roe) +self:F2(roe) +if roe=="return"then +self.roe=RAT.ROE.returnfire +elseif roe=="free"then +self.roe=RAT.ROE.weaponfree +else +self.roe=RAT.ROE.weaponhold +end +return self +end +function RAT:SetROT(rot) +self:F2(rot) +if rot=="passive"then +self.rot=RAT.ROT.passive +elseif rot=="evade"then +self.rot=RAT.ROT.evade +else +self.rot=RAT.ROT.noreaction +end +return self +end +function RAT:MenuName(name) +self:F2(name) +self.SubMenuName=tostring(name) +return self +end +function RAT:EnableATC(switch) +self:F2(switch) +if switch==nil then +switch=true +end +self.ATCswitch=switch +return self +end +function RAT:ATC_Messages(switch) +self:F2(switch) +if switch==nil then +switch=true +end +RAT.ATC.messages=switch +return self +end +function RAT:ATC_Clearance(n) +self:F2(n) +RAT.ATC.Nclearance=n or 2 +return self +end +function RAT:ATC_Delay(time) +self:F2(time) +RAT.ATC.delay=time or 240 +return self +end +function RAT:SetMinDistance(dist) +self:F2(dist) +self.mindist=math.max(100,dist*1000) +return self +end +function RAT:SetMaxDistance(dist) +self:F2(dist) +self.maxdist=dist*1000 +return self +end +function RAT:_Debug(switch) +self:F2(switch) +if switch==nil then +switch=true +end +self.Debug=switch +return self +end +function RAT:Debugmode() +self:F2() +self.Debug=true +return self +end +function RAT:StatusReports(switch) +self:F2(switch) +if switch==nil then +switch=true +end +self.reportstatus=switch +return self +end +function RAT:PlaceMarkers(switch) +self:F2(switch) +if switch==nil then +switch=true +end +self.placemarkers=switch +return self +end +function RAT:SetFL(FL) +self:F2(FL) +FL=FL or self.FLcruise +FL=math.max(FL,0) +self.FLuser=FL*RAT.unit.FL2m +return self +end +function RAT:SetFLmax(FL) +self:F2(FL) +self.FLmaxuser=FL*RAT.unit.FL2m +return self +end +function RAT:SetMaxCruiseAltitude(alt) +self:F2(alt) +self.FLmaxuser=alt +return self +end +function RAT:SetFLmin(FL) +self:F2(FL) +self.FLminuser=FL*RAT.unit.FL2m +return self +end +function RAT:SetMinCruiseAltitude(alt) +self:F2(alt) +self.FLminuser=alt +return self +end +function RAT:SetFLcruise(FL) +self:F2(FL) +self.FLcruise=FL*RAT.unit.FL2m +return self +end +function RAT:SetCruiseAltitude(alt) +self:F2(alt) +self.FLcruise=alt +return self +end +function RAT:SetOnboardNum(tailnumprefix,zero) +self:F2({tailnumprefix=tailnumprefix,zero=zero}) +self.onboardnum=tailnumprefix +if zero~=nil then +self.onboardnum0=zero +end +return self +end +function RAT:_InitAircraft(DCSgroup) +self:F2(DCSgroup) +local DCSunit=DCSgroup:getUnit(1) +local DCSdesc=DCSunit:getDesc() +local DCScategory=DCSgroup:getCategory() +local DCStype=DCSunit:getTypeName() +if DCScategory==Group.Category.AIRPLANE then +self.category=RAT.cat.plane +elseif DCScategory==Group.Category.HELICOPTER then +self.category=RAT.cat.heli +else +self.category="other" +self:E(RAT.id.."ERROR: Group of RAT is neither airplane nor helicopter!") +end +self.aircraft.type=DCStype +self.aircraft.fuel=DCSunit:getFuel() +self.aircraft.Rmax=DCSdesc.range*RAT.unit.nm2m +self.aircraft.Reff=self.aircraft.Rmax*self.aircraft.fuel*0.95 +self.aircraft.Vmax=DCSdesc.speedMax +self.aircraft.Vymax=DCSdesc.VyMax +self.aircraft.ceiling=DCSdesc.Hmax +self.aircraft.length=DCSdesc.box.max.x +self.aircraft.height=DCSdesc.box.max.y +self.aircraft.width=DCSdesc.box.max.z +self.aircraft.box=math.max(self.aircraft.length,self.aircraft.width) +local text=string.format("\n******************************************************\n") +text=text..string.format("Aircraft parameters:\n") +text=text..string.format("Template group = %s\n",self.SpawnTemplatePrefix) +text=text..string.format("Alias = %s\n",self.alias) +text=text..string.format("Category = %s\n",self.category) +text=text..string.format("Type = %s\n",self.aircraft.type) +text=text..string.format("Length (x) = %6.1f m\n",self.aircraft.length) +text=text..string.format("Width (z) = %6.1f m\n",self.aircraft.width) +text=text..string.format("Height (y) = %6.1f m\n",self.aircraft.height) +text=text..string.format("Max air speed = %6.1f m/s\n",self.aircraft.Vmax) +text=text..string.format("Max climb speed = %6.1f m/s\n",self.aircraft.Vymax) +text=text..string.format("Initial Fuel = %6.1f\n",self.aircraft.fuel*100) +text=text..string.format("Max range = %6.1f km\n",self.aircraft.Rmax/1000) +text=text..string.format("Eff range = %6.1f km (with 95 percent initial fuel amount)\n",self.aircraft.Reff/1000) +text=text..string.format("Ceiling = %6.1f km = FL%3.0f\n",self.aircraft.ceiling/1000,self.aircraft.ceiling/RAT.unit.FL2m) +text=text..string.format("******************************************************\n") +self:T(RAT.id..text) +end +function RAT:_SpawnWithRoute(_departure,_destination,_takeoff,_landing,_livery,_waypoint,_lastpos,_nrespawn,parkingdata) +self:F({rat=RAT.id,departure=_departure,destination=_destination,takeoff=_takeoff,landing=_landing,livery=_livery,waypoint=_waypoint,lastpos=_lastpos,nrespawn=_nrespawn}) +local takeoff=self.takeoff +local landing=self.landing +if _takeoff then +takeoff=_takeoff +end +if _landing then +landing=_landing +end +if takeoff==RAT.wp.coldorhot then +local temp={RAT.wp.cold,RAT.wp.hot} +takeoff=temp[math.random(2)] +end +local nrespawn=0 +if _nrespawn then +nrespawn=_nrespawn +end +local departure,destination,waypoints,WPholding,WPfinal=self:_SetRoute(takeoff,landing,_departure,_destination,_waypoint) +if not(departure and destination and waypoints)then +return nil +end +local livery +if _livery then +livery=_livery +elseif self.livery then +livery=self.livery[math.random(#self.livery)] +local text=string.format("Chosen livery for group %s: %s",self:_AnticipatedGroupName(),livery) +self:T(RAT.id..text) +else +livery=nil +end +local successful=self:_ModifySpawnTemplate(waypoints,livery,_lastpos,departure,takeoff,parkingdata) +if not successful then +return nil +end +local group=self:SpawnWithIndex(self.SpawnIndex) +self.alive=self.alive+1 +self:T(RAT.id..string.format("Alive groups counter now = %d.",self.alive)) +if self.ATCswitch and landing==RAT.wp.landing then +if self.returnzone then +self:_ATCAddFlight(group:GetName(),departure:GetName()) +else +self:_ATCAddFlight(group:GetName(),destination:GetName()) +end +end +if self.placemarkers then +self:_PlaceMarkers(waypoints,self.SpawnIndex) +end +if self.invisible then +self:_CommandInvisible(group,true) +end +if self.immortal then +self:_CommandImmortal(group,true) +end +if self.eplrs then +group:CommandEPLRS(true,1) +end +self:_SetROE(group,self.roe) +self:_SetROT(group,self.rot) +self.ratcraft[self.SpawnIndex]={} +self.ratcraft[self.SpawnIndex]["group"]=group +self.ratcraft[self.SpawnIndex]["destination"]=destination +self.ratcraft[self.SpawnIndex]["departure"]=departure +self.ratcraft[self.SpawnIndex]["waypoints"]=waypoints +self.ratcraft[self.SpawnIndex]["airborne"]=group:InAir() +self.ratcraft[self.SpawnIndex]["nunits"]=group:GetInitialSize() +if group:InAir()then +self.ratcraft[self.SpawnIndex]["Tground"]=nil +self.ratcraft[self.SpawnIndex]["Pground"]=nil +self.ratcraft[self.SpawnIndex]["Uground"]=nil +self.ratcraft[self.SpawnIndex]["Tlastcheck"]=nil +else +self.ratcraft[self.SpawnIndex]["Tground"]=timer.getTime() +self.ratcraft[self.SpawnIndex]["Pground"]=group:GetCoordinate() +self.ratcraft[self.SpawnIndex]["Uground"]={} +for _,_unit in pairs(group:GetUnits())do +local _unitname=_unit:GetName() +self.ratcraft[self.SpawnIndex]["Uground"][_unitname]=_unit:GetCoordinate() +end +self.ratcraft[self.SpawnIndex]["Tlastcheck"]=timer.getTime() +end +self.ratcraft[self.SpawnIndex]["P0"]=group:GetCoordinate() +self.ratcraft[self.SpawnIndex]["Pnow"]=group:GetCoordinate() +self.ratcraft[self.SpawnIndex]["Distance"]=0 +self.ratcraft[self.SpawnIndex].takeoff=takeoff +self.ratcraft[self.SpawnIndex].landing=landing +self.ratcraft[self.SpawnIndex].wpholding=WPholding +self.ratcraft[self.SpawnIndex].wpfinal=WPfinal +self.ratcraft[self.SpawnIndex].active=not self.uncontrolled +self.ratcraft[self.SpawnIndex]["status"]=RAT.status.Spawned +self.ratcraft[self.SpawnIndex].livery=livery +self.ratcraft[self.SpawnIndex].despawnme=false +self.ratcraft[self.SpawnIndex].nrespawn=nrespawn +if self.f10menu then +local name=self.aircraft.type.." ID "..tostring(self.SpawnIndex) +self.Menu[self.SubMenuName].groups[self.SpawnIndex]=MENU_MISSION:New(name,self.Menu[self.SubMenuName].groups) +self.Menu[self.SubMenuName].groups[self.SpawnIndex]["roe"]=MENU_MISSION:New("Set ROE",self.Menu[self.SubMenuName].groups[self.SpawnIndex]) +MENU_MISSION_COMMAND:New("Weapons hold",self.Menu[self.SubMenuName].groups[self.SpawnIndex]["roe"],self._SetROE,self,group,RAT.ROE.weaponhold) +MENU_MISSION_COMMAND:New("Weapons free",self.Menu[self.SubMenuName].groups[self.SpawnIndex]["roe"],self._SetROE,self,group,RAT.ROE.weaponfree) +MENU_MISSION_COMMAND:New("Return fire",self.Menu[self.SubMenuName].groups[self.SpawnIndex]["roe"],self._SetROE,self,group,RAT.ROE.returnfire) +self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"]=MENU_MISSION:New("Set ROT",self.Menu[self.SubMenuName].groups[self.SpawnIndex]) +MENU_MISSION_COMMAND:New("No reaction",self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"],self._SetROT,self,group,RAT.ROT.noreaction) +MENU_MISSION_COMMAND:New("Passive defense",self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"],self._SetROT,self,group,RAT.ROT.passive) +MENU_MISSION_COMMAND:New("Evade on fire",self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"],self._SetROT,self,group,RAT.ROT.evade) +MENU_MISSION_COMMAND:New("Despawn group",self.Menu[self.SubMenuName].groups[self.SpawnIndex],self._Despawn,self,group) +MENU_MISSION_COMMAND:New("Place markers",self.Menu[self.SubMenuName].groups[self.SpawnIndex],self._PlaceMarkers,self,waypoints,self.SpawnIndex) +MENU_MISSION_COMMAND:New("Status report",self.Menu[self.SubMenuName].groups[self.SpawnIndex],self.Status,self,true,self.SpawnIndex) +end +return self.SpawnIndex +end +function RAT:ClearForLanding(name) +trigger.action.setUserFlag(name,1) +local flagvalue=trigger.misc.getUserFlag(name) +self:T(RAT.id.."ATC: User flag value (landing) for "..name.." set to "..flagvalue) +end +function RAT:_Respawn(index,lastpos,delay) +local departure=self.ratcraft[index].departure +local destination=self.ratcraft[index].destination +local takeoff=self.ratcraft[index].takeoff +local landing=self.ratcraft[index].landing +local livery=self.ratcraft[index].livery +local lastwp=self.ratcraft[index].waypoints[#self.ratcraft[index].waypoints] +local _departure=nil +local _destination=nil +local _takeoff=nil +local _landing=nil +local _livery=nil +local _lastwp=nil +local _lastpos=nil +if self.continuejourney then +_departure=destination:GetName() +_livery=livery +if landing==RAT.wp.landing and lastpos and not(self.respawn_at_landing or self.respawn_after_takeoff)then +if destination:GetCategory()==4 then +_lastpos=lastpos +end +end +if self.destinationzone then +_takeoff=RAT.wp.air +_landing=RAT.wp.air +elseif self.returnzone then +_takeoff=self.takeoff +if self.takeoff==RAT.wp.air then +_landing=RAT.wp.air +else +_landing=RAT.wp.landing +end +_departure=departure:GetName() +else +_takeoff=self.takeoff +_landing=self.landing +end +elseif self.commute then +if self.starshape==true then +if destination:GetName()==self.homebase then +_departure=self.homebase +_destination=nil +else +_departure=destination:GetName() +_destination=self.homebase +end +else +_departure=destination:GetName() +_destination=departure:GetName() +end +_livery=livery +if landing==RAT.wp.landing and lastpos and not(self.respawn_at_landing or self.respawn_after_takeoff)then +if destination:GetCategory()==4 then +_lastpos=lastpos +end +end +if self.destinationzone then +if self.takeoff==RAT.wp.air then +_takeoff=RAT.wp.air +_landing=RAT.wp.air +else +if takeoff==RAT.wp.air then +_takeoff=self.takeoff +_landing=RAT.wp.air +else +_takeoff=RAT.wp.air +_landing=RAT.wp.landing +end +end +elseif self.returnzone then +_departure=departure:GetName() +_destination=destination:GetName() +_takeoff=self.takeoff +_landing=self.landing +end +end +if _takeoff==RAT.wp.air and(self.continuejourney or self.commute)then +_lastwp=lastwp +end +self:T2({departure=_departure,destination=_destination,takeoff=_takeoff,landing=_landing,livery=_livery,lastwp=_lastwp}) +local respawndelay +if delay then +respawndelay=delay +elseif self.respawn_delay then +respawndelay=self.respawn_delay+3 +else +respawndelay=3 +end +local arg={} +arg.self=self +arg.departure=_departure +arg.destination=_destination +arg.takeoff=_takeoff +arg.landing=_landing +arg.livery=_livery +arg.lastwp=_lastwp +arg.lastpos=_lastpos +self:T(RAT.id..string.format("%s delayed respawn in %.1f seconds.",self.alias,respawndelay)) +SCHEDULER:New(nil,self._SpawnWithRouteTimer,{arg},respawndelay) +end +function RAT._SpawnWithRouteTimer(arg) +RAT._SpawnWithRoute(arg.self,arg.departure,arg.destination,arg.takeoff,arg.landing,arg.livery,arg.lastwp,arg.lastpos) +end +function RAT:_SetRoute(takeoff,landing,_departure,_destination,_waypoint) +local VxCruiseMax +if self.Vcruisemax then +VxCruiseMax=math.min(self.Vcruisemax,self.aircraft.Vmax) +else +VxCruiseMax=math.min(self.aircraft.Vmax*0.90,250) +end +local VxCruiseMin=math.min(VxCruiseMax*0.70,166) +local VxCruise=UTILS.RandomGaussian((VxCruiseMax-VxCruiseMin)/2+VxCruiseMin,(VxCruiseMax-VxCruiseMax)/4,VxCruiseMin,VxCruiseMax) +local VxClimb=math.min(self.aircraft.Vmax*0.90,200) +local VxDescent=math.min(self.aircraft.Vmax*0.60,140) +local VxHolding=VxDescent*0.9 +local VxFinal=VxHolding*0.9 +local VyClimb=math.min(self.Vclimb*RAT.unit.ft2meter/60,self.aircraft.Vymax) +local AlphaClimb=math.asin(VyClimb/VxClimb) +local AlphaDescent=math.rad(self.AlphaDescent) +local FLcruise_expect=self.FLcruise +local departure=nil +if _departure then +if self:_AirportExists(_departure)then +departure=AIRBASE:FindByName(_departure) +if takeoff==RAT.wp.air then +departure=departure:GetZone() +end +elseif self:_ZoneExists(_departure)then +departure=ZONE:New(_departure) +else +local text=string.format("ERROR! Specified departure airport %s does not exist for %s.",_departure,self.alias) +self:E(RAT.id..text) +end +else +departure=self:_PickDeparture(takeoff) +if self.commute and self.starshape==true and self.homebase==nil then +self.homebase=departure:GetName() +end +end +if not departure then +local text=string.format("ERROR! No valid departure airport could be found for %s.",self.alias) +self:E(RAT.id..text) +return nil +end +local Pdeparture +if takeoff==RAT.wp.air then +if _waypoint then +Pdeparture=COORDINATE:New(_waypoint.x,_waypoint.alt,_waypoint.y) +else +local vec2=departure:GetRandomVec2() +Pdeparture=COORDINATE:NewFromVec2(vec2) +end +else +Pdeparture=departure:GetCoordinate() +end +local H_departure +if takeoff==RAT.wp.air then +local Hmin +if self.category==RAT.cat.plane then +Hmin=1000 +else +Hmin=50 +end +H_departure=self:_Randomize(FLcruise_expect*0.7,0.3,Pdeparture.y+Hmin,FLcruise_expect) +if self.FLminuser then +H_departure=math.max(H_departure,self.FLminuser) +end +if _waypoint then +H_departure=_waypoint.alt +end +else +H_departure=Pdeparture.y +end +local mindist=self.mindist +if self.FLminuser then +local hclimb=self.FLminuser-H_departure +local hdescent=self.FLminuser-H_departure +local Dclimb,Ddescent,Dtot=self:_MinDistance(AlphaClimb,AlphaDescent,hclimb,hdescent) +if takeoff==RAT.wp.air and landing==RAT.wpair then +mindist=0 +elseif takeoff==RAT.wp.air then +mindist=Ddescent +elseif landing==RAT.wp.air then +mindist=Dclimb +else +mindist=Dtot +end +mindist=math.max(self.mindist,mindist) +local text=string.format("Adjusting min distance to %d km (for given min FL%03d)",mindist/1000,self.FLminuser/RAT.unit.FL2m) +self:T(RAT.id..text) +end +local destination=nil +if _destination then +if self:_AirportExists(_destination)then +destination=AIRBASE:FindByName(_destination) +if landing==RAT.wp.air or self.returnzone then +destination=destination:GetZone() +end +elseif self:_ZoneExists(_destination)then +destination=ZONE:New(_destination) +else +local text=string.format("ERROR: Specified destination airport/zone %s does not exist for %s!",_destination,self.alias) +self:E(RAT.id.."ERROR: "..text) +end +else +local random=self.random_destination +if self.continuejourney and _departure and#self.destination_ports<3 then +random=true +end +local mylanding=landing +local acrange=self.aircraft.Reff +if self.returnzone then +mylanding=RAT.wp.air +acrange=self.aircraft.Reff/2 +end +destination=self:_PickDestination(departure,Pdeparture,mindist,math.min(acrange,self.maxdist),random,mylanding) +end +if not destination then +local text=string.format("No valid destination airport could be found for %s!",self.alias) +MESSAGE:New(text,60):ToAll() +self:E(RAT.id.."ERROR: "..text) +return nil +end +if destination:GetName()==departure:GetName()then +local text=string.format("%s: Destination and departure are identical. Airport/zone %s.",self.alias,destination:GetName()) +MESSAGE:New(text,30):ToAll() +self:E(RAT.id.."ERROR: "..text) +end +local Preturn +local destination_returnzone +if self.returnzone then +local vec2=destination:GetRandomVec2() +Preturn=COORDINATE:NewFromVec2(vec2) +destination_returnzone=destination +destination=departure +end +local Pdestination +if landing==RAT.wp.air then +local vec2=destination:GetRandomVec2() +Pdestination=COORDINATE:NewFromVec2(vec2) +else +Pdestination=destination:GetCoordinate() +end +local H_destination=Pdestination.y +local Rhmin=8000 +local Rhmax=20000 +if self.category==RAT.cat.heli then +Rhmin=500 +Rhmax=1000 +end +local Vholding=Pdestination:GetRandomVec2InRadius(Rhmax,Rhmin) +local Pholding=COORDINATE:NewFromVec2(Vholding) +local H_holding=Pholding.y +local h_holding +if self.category==RAT.cat.plane then +h_holding=1200 +else +h_holding=150 +end +h_holding=self:_Randomize(h_holding,0.2) +local Hh_holding=H_holding+h_holding +if landing==RAT.wp.air then +Hh_holding=H_departure +end +local d_holding=Pholding:Get2DDistance(Pdestination) +local heading +local d_total +if self.returnzone then +heading=self:_Course(Pdeparture,Preturn) +d_total=Pdeparture:Get2DDistance(Preturn)+Preturn:Get2DDistance(Pholding) +else +heading=self:_Course(Pdeparture,Pholding) +d_total=Pdeparture:Get2DDistance(Pholding) +end +if takeoff==RAT.wp.air then +local H_departure_max +if landing==RAT.wp.air then +H_departure_max=H_departure +else +H_departure_max=d_total*math.tan(AlphaDescent)+Hh_holding +end +H_departure=math.min(H_departure,H_departure_max) +end +local deltaH=math.abs(H_departure-Hh_holding) +local phi=math.atan(deltaH/d_total) +local phi_climb +local phi_descent +if(H_departure>Hh_holding)then +phi_climb=AlphaClimb+phi +phi_descent=AlphaDescent-phi +else +phi_climb=AlphaClimb-phi +phi_descent=AlphaDescent+phi +end +local D_total +if self.returnzone then +D_total=math.sqrt(deltaH*deltaH+d_total/2*d_total/2) +else +D_total=math.sqrt(deltaH*deltaH+d_total*d_total) +end +local gamma=math.rad(180)-phi_climb-phi_descent +local a=D_total*math.sin(phi_climb)/math.sin(gamma) +local b=D_total*math.sin(phi_descent)/math.sin(gamma) +local hphi_max=b*math.sin(phi_climb) +local hphi_max2=a*math.sin(phi_descent) +local h_max1=b*math.sin(AlphaClimb) +local h_max2=a*math.sin(AlphaDescent) +local h_max +if(H_departure>Hh_holding)then +h_max=math.min(h_max1,h_max2) +else +h_max=math.max(h_max1,h_max2) +end +local FLmax=h_max+H_departure +local FLmin=math.max(H_departure,Hh_holding) +if self.category==RAT.cat.heli then +FLmin=math.max(H_departure,H_destination)+50 +FLmax=math.max(H_departure,H_destination)+1000 +end +FLmax=math.min(FLmax,self.aircraft.ceiling) +if self.FLminuser then +FLmin=math.max(self.FLminuser,FLmin) +end +if self.FLmaxuser then +FLmax=math.min(self.FLmaxuser,FLmax) +end +if FLmin>FLmax then +FLmin=FLmax +end +if FLcruise_expectFLmax then +FLcruise_expect=FLmax +end +local FLcruise=UTILS.RandomGaussian(FLcruise_expect,math.abs(FLmax-FLmin)/4,FLmin,FLmax) +if self.FLuser then +FLcruise=self.FLuser +FLcruise=math.max(FLcruise,FLmin) +FLcruise=math.min(FLcruise,FLmax) +end +local h_climb=FLcruise-H_departure +local h_descent=FLcruise-Hh_holding +local d_climb=h_climb/math.tan(AlphaClimb) +local d_descent=h_descent/math.tan(AlphaDescent) +local d_cruise=d_total-d_climb-d_descent +local text=string.format("\n******************************************************\n") +text=text..string.format("Template = %s\n",self.SpawnTemplatePrefix) +text=text..string.format("Alias = %s\n",self.alias) +text=text..string.format("Group name = %s\n\n",self:_AnticipatedGroupName()) +text=text..string.format("Speeds:\n") +text=text..string.format("VxCruiseMin = %6.1f m/s = %5.1f km/h\n",VxCruiseMin,VxCruiseMin*3.6) +text=text..string.format("VxCruiseMax = %6.1f m/s = %5.1f km/h\n",VxCruiseMax,VxCruiseMax*3.6) +text=text..string.format("VxCruise = %6.1f m/s = %5.1f km/h\n",VxCruise,VxCruise*3.6) +text=text..string.format("VxClimb = %6.1f m/s = %5.1f km/h\n",VxClimb,VxClimb*3.6) +text=text..string.format("VxDescent = %6.1f m/s = %5.1f km/h\n",VxDescent,VxDescent*3.6) +text=text..string.format("VxHolding = %6.1f m/s = %5.1f km/h\n",VxHolding,VxHolding*3.6) +text=text..string.format("VxFinal = %6.1f m/s = %5.1f km/h\n",VxFinal,VxFinal*3.6) +text=text..string.format("VyClimb = %6.1f m/s\n",VyClimb) +text=text..string.format("\nDistances:\n") +text=text..string.format("d_climb = %6.1f km\n",d_climb/1000) +text=text..string.format("d_cruise = %6.1f km\n",d_cruise/1000) +text=text..string.format("d_descent = %6.1f km\n",d_descent/1000) +text=text..string.format("d_holding = %6.1f km\n",d_holding/1000) +text=text..string.format("d_total = %6.1f km\n",d_total/1000) +text=text..string.format("\nHeights:\n") +text=text..string.format("H_departure = %6.1f m ASL\n",H_departure) +text=text..string.format("H_destination = %6.1f m ASL\n",H_destination) +text=text..string.format("H_holding = %6.1f m ASL\n",H_holding) +text=text..string.format("h_climb = %6.1f m\n",h_climb) +text=text..string.format("h_descent = %6.1f m\n",h_descent) +text=text..string.format("h_holding = %6.1f m\n",h_holding) +text=text..string.format("delta H = %6.1f m\n",deltaH) +text=text..string.format("FLmin = %6.1f m ASL = FL%03d\n",FLmin,FLmin/RAT.unit.FL2m) +text=text..string.format("FLcruise = %6.1f m ASL = FL%03d\n",FLcruise,FLcruise/RAT.unit.FL2m) +text=text..string.format("FLmax = %6.1f m ASL = FL%03d\n",FLmax,FLmax/RAT.unit.FL2m) +text=text..string.format("\nAngles:\n") +text=text..string.format("Alpha climb = %6.2f Deg\n",math.deg(AlphaClimb)) +text=text..string.format("Alpha descent = %6.2f Deg\n",math.deg(AlphaDescent)) +text=text..string.format("Phi (slope) = %6.2f Deg\n",math.deg(phi)) +text=text..string.format("Phi climb = %6.2f Deg\n",math.deg(phi_climb)) +text=text..string.format("Phi descent = %6.2f Deg\n",math.deg(phi_descent)) +if self.Debug then +local h_climb_max=FLmax-H_departure +local h_descent_max=FLmax-Hh_holding +local d_climb_max=h_climb_max/math.tan(AlphaClimb) +local d_descent_max=h_descent_max/math.tan(AlphaDescent) +local d_cruise_max=d_total-d_climb_max-d_descent_max +text=text..string.format("Heading = %6.1f Deg\n",heading) +text=text..string.format("\nSSA triangle:\n") +text=text..string.format("D_total = %6.1f km\n",D_total/1000) +text=text..string.format("gamma = %6.1f Deg\n",math.deg(gamma)) +text=text..string.format("a = %6.1f m\n",a) +text=text..string.format("b = %6.1f m\n",b) +text=text..string.format("hphi_max = %6.1f m\n",hphi_max) +text=text..string.format("hphi_max2 = %6.1f m\n",hphi_max2) +text=text..string.format("h_max1 = %6.1f m\n",h_max1) +text=text..string.format("h_max2 = %6.1f m\n",h_max2) +text=text..string.format("h_max = %6.1f m\n",h_max) +text=text..string.format("\nMax heights and distances:\n") +text=text..string.format("d_climb_max = %6.1f km\n",d_climb_max/1000) +text=text..string.format("d_cruise_max = %6.1f km\n",d_cruise_max/1000) +text=text..string.format("d_descent_max = %6.1f km\n",d_descent_max/1000) +text=text..string.format("h_climb_max = %6.1f m\n",h_climb_max) +text=text..string.format("h_descent_max = %6.1f m\n",h_descent_max) +end +text=text..string.format("******************************************************\n") +self:T2(RAT.id..text) +if d_cruise<0 then +d_cruise=100 +end +local wp={} +local c={} +local wpholding=nil +local wpfinal=nil +c[#c+1]=Pdeparture +wp[#wp+1]=self:_Waypoint(#wp+1,"Departure",takeoff,c[#wp+1],VxClimb,H_departure,departure) +self.waypointdescriptions[#wp]="Departure" +self.waypointstatus[#wp]=RAT.status.Departure +if takeoff==RAT.wp.air then +if d_climb<5000 or d_cruise<5000 then +d_cruise=d_cruise+d_climb +else +c[#c+1]=c[#c]:Translate(d_climb,heading) +wp[#wp+1]=self:_Waypoint(#wp+1,"Begin of Cruise",RAT.wp.cruise,c[#wp+1],VxCruise,FLcruise) +self.waypointdescriptions[#wp]="Begin of Cruise" +self.waypointstatus[#wp]=RAT.status.Cruise +end +else +c[#c+1]=c[#c]:Translate(d_climb/2,heading) +c[#c+1]=c[#c]:Translate(d_climb/2,heading) +wp[#wp+1]=self:_Waypoint(#wp+1,"Climb",RAT.wp.climb,c[#wp+1],VxClimb,H_departure+(FLcruise-H_departure)/2) +self.waypointdescriptions[#wp]="Climb" +self.waypointstatus[#wp]=RAT.status.Climb +wp[#wp+1]=self:_Waypoint(#wp+1,"Begin of Cruise",RAT.wp.cruise,c[#wp+1],VxCruise,FLcruise) +self.waypointdescriptions[#wp]="Begin of Cruise" +self.waypointstatus[#wp]=RAT.status.Cruise +end +if self.returnzone then +c[#c+1]=Preturn +wp[#wp+1]=self:_Waypoint(#wp+1,"Return Zone",RAT.wp.cruise,c[#wp+1],VxCruise,FLcruise) +self.waypointdescriptions[#wp]="Return Zone" +self.waypointstatus[#wp]=RAT.status.Uturn +end +if landing==RAT.wp.air then +c[#c+1]=Pdestination +wp[#wp+1]=self:_Waypoint(#wp+1,"Final Destination",RAT.wp.finalwp,c[#wp+1],VxCruise,FLcruise) +self.waypointdescriptions[#wp]="Final Destination" +self.waypointstatus[#wp]=RAT.status.Destination +elseif self.returnzone then +c[#c+1]=c[#c]:Translate(d_cruise/2,heading-180) +wp[#wp+1]=self:_Waypoint(#wp+1,"End of Cruise",RAT.wp.cruise,c[#wp+1],VxCruise,FLcruise) +self.waypointdescriptions[#wp]="End of Cruise" +self.waypointstatus[#wp]=RAT.status.Descent +else +c[#c+1]=c[#c]:Translate(d_cruise,heading) +wp[#wp+1]=self:_Waypoint(#wp+1,"End of Cruise",RAT.wp.cruise,c[#wp+1],VxCruise,FLcruise) +self.waypointdescriptions[#wp]="End of Cruise" +self.waypointstatus[#wp]=RAT.status.Descent +end +if landing==RAT.wp.landing then +if self.returnzone then +c[#c+1]=c[#c]:Translate(d_descent/2,heading-180) +wp[#wp+1]=self:_Waypoint(#wp+1,"Descent",RAT.wp.descent,c[#wp+1],VxDescent,FLcruise-(FLcruise-(h_holding+H_holding))/2) +self.waypointdescriptions[#wp]="Descent" +self.waypointstatus[#wp]=RAT.status.DescentHolding +else +c[#c+1]=c[#c]:Translate(d_descent/2,heading) +wp[#wp+1]=self:_Waypoint(#wp+1,"Descent",RAT.wp.descent,c[#wp+1],VxDescent,FLcruise-(FLcruise-(h_holding+H_holding))/2) +self.waypointdescriptions[#wp]="Descent" +self.waypointstatus[#wp]=RAT.status.DescentHolding +end +end +if landing==RAT.wp.landing then +c[#c+1]=Pholding +wp[#wp+1]=self:_Waypoint(#wp+1,"Holding Point",RAT.wp.holding,c[#wp+1],VxHolding,H_holding+h_holding) +self.waypointdescriptions[#wp]="Holding Point" +self.waypointstatus[#wp]=RAT.status.Holding +wpholding=#wp +c[#c+1]=Pdestination +wp[#wp+1]=self:_Waypoint(#wp+1,"Final Destination",landing,c[#wp+1],VxFinal,H_destination,destination) +self.waypointdescriptions[#wp]="Final Destination" +self.waypointstatus[#wp]=RAT.status.Destination +end +wpfinal=#wp +local waypoints={} +for _,p in ipairs(wp)do +table.insert(waypoints,p) +end +self:_Routeinfo(waypoints,"Waypoint info in set_route:") +if self.returnzone then +return departure,destination_returnzone,waypoints,wpholding,wpfinal +else +return departure,destination,waypoints,wpholding,wpfinal +end +end +function RAT:_PickDeparture(takeoff) +local departures={} +if self.random_departure then +for _,_airport in pairs(self.airports)do +local airport=_airport +local name=airport:GetName() +if not self:_Excluded(name)then +if takeoff==RAT.wp.air then +table.insert(departures,airport:GetZone()) +else +local nspots=1 +if self.termtype~=nil then +nspots=airport:GetParkingSpotsNumber(self.termtype) +end +if nspots>0 then +table.insert(departures,airport) +end +end +end +end +else +for _,name in pairs(self.departure_ports)do +local dep=nil +if self:_AirportExists(name)then +if takeoff==RAT.wp.air then +dep=AIRBASE:FindByName(name):GetZone() +else +dep=AIRBASE:FindByName(name) +if self.termtype~=nil and dep~=nil then +local _dep=dep +local nspots=_dep:GetParkingSpotsNumber(self.termtype) +if nspots==0 then +dep=nil +end +end +end +elseif self:_ZoneExists(name)then +if takeoff==RAT.wp.air then +dep=ZONE:New(name) +else +self:E(RAT.id..string.format("ERROR! Takeoff is not in air. Cannot use %s as departure.",name)) +end +else +self:E(RAT.id..string.format("ERROR: No airport or zone found with name %s.",name)) +end +if dep then +table.insert(departures,dep) +end +end +end +self:T(RAT.id..string.format("Number of possible departures for %s= %d",self.alias,#departures)) +local departure=departures[math.random(#departures)] +local text +if departure and departure:GetName()then +if takeoff==RAT.wp.air then +text=string.format("%s: Chosen departure zone: %s",self.alias,departure:GetName()) +else +text=string.format("%s: Chosen departure airport: %s (ID %d)",self.alias,departure:GetName(),departure:GetID()) +end +self:T(RAT.id..text) +else +self:E(RAT.id..string.format("ERROR! No departure airport or zone found for %s.",self.alias)) +departure=nil +end +return departure +end +function RAT:_PickDestination(departure,q,minrange,maxrange,random,landing) +minrange=minrange or self.mindist +maxrange=maxrange or self.maxdist +local destinations={} +if random then +for _,_airport in pairs(self.airports)do +local airport=_airport +local name=airport:GetName() +if self:_IsFriendly(name)and not self:_Excluded(name)and name~=departure:GetName()then +local distance=q:Get2DDistance(airport:GetCoordinate()) +if distance>=minrange and distance<=maxrange then +if landing==RAT.wp.air then +table.insert(destinations,airport:GetZone()) +else +local nspot=1 +if self.termtype then +nspot=airport:GetParkingSpotsNumber(self.termtype) +end +if nspot>0 then +table.insert(destinations,airport) +end +end +end +end +end +else +for _,name in pairs(self.destination_ports)do +if name~=departure:GetName()then +local dest=nil +if self:_AirportExists(name)then +if landing==RAT.wp.air then +dest=AIRBASE:FindByName(name):GetZone() +else +dest=AIRBASE:FindByName(name) +local nspot=1 +if self.termtype then +nspot=dest:GetParkingSpotsNumber(self.termtype) +end +if nspot==0 then +dest=nil +end +end +elseif self:_ZoneExists(name)then +if landing==RAT.wp.air then +dest=ZONE:New(name) +else +self:E(RAT.id..string.format("ERROR! Landing is not in air. Cannot use zone %s as destination!",name)) +end +else +self:E(RAT.id..string.format("ERROR! No airport or zone found with name %s",name)) +end +if dest then +local distance=q:Get2DDistance(dest:GetCoordinate()) +if distance>=minrange and distance<=maxrange then +table.insert(destinations,dest) +else +local text=string.format("Destination %s is ouside range. Distance = %5.1f km, min = %5.1f km, max = %5.1f km.",name,distance,minrange,maxrange) +self:T(RAT.id..text) +end +end +end +end +end +self:T(RAT.id..string.format("Number of possible destinations = %s.",#destinations)) +if#destinations>0 then +local function compare(a,b) +local qa=q:Get2DDistance(a:GetCoordinate()) +local qb=q:Get2DDistance(b:GetCoordinate()) +return qa0 then +destination=destinations[math.random(#destinations)] +local text +if landing==RAT.wp.air then +text=string.format("%s: Chosen destination zone: %s.",self.alias,destination:GetName()) +else +text=string.format("%s Chosen destination airport: %s (ID %d).",self.alias,destination:GetName(),destination:GetID()) +end +self:T(RAT.id..text) +else +self:E(RAT.id.."ERROR! No destination airport or zone found.") +destination=nil +end +return destination +end +function RAT:_GetAirportsInZone(zone) +local airports={} +for _,airport in pairs(self.airports)do +local name=airport:GetName() +local coord=airport:GetCoordinate() +if zone:IsPointVec3InZone(coord)then +table.insert(airports,name) +end +end +return airports +end +function RAT:_Excluded(port) +for _,name in pairs(self.excluded_ports)do +if name==port then +return true +end +end +return false +end +function RAT:_IsFriendly(port) +for _,airport in pairs(self.airports)do +local name=airport:GetName() +if name==port then +return true +end +end +return false +end +function RAT:_GetAirportsOfMap() +local _coalition +for i=0,2 do +if i==0 then +_coalition=coalition.side.NEUTRAL +elseif i==1 then +_coalition=coalition.side.RED +elseif i==2 then +_coalition=coalition.side.BLUE +end +local ab=coalition.getAirbases(i) +for _,airbase in pairs(ab)do +local _id=airbase:getID() +local _p=airbase:getPosition().p +local _name=airbase:getName() +local _myab=AIRBASE:FindByName(_name) +if _myab then +table.insert(self.airports_map,_myab) +local text="MOOSE: Airport ID = ".._myab:GetID().." and Name = ".._myab:GetName()..", Category = ".._myab:GetCategory()..", TypeName = ".._myab:GetTypeName() +self:T(RAT.id..text) +else +self:E(RAT.id..string.format("WARNING: Airbase %s does not exsist as MOOSE object!",tostring(_name))) +end +end +end +end +function RAT:_GetAirportsOfCoalition() +for _,coalition in pairs(self.ctable)do +for _,_airport in pairs(self.airports_map)do +local airport=_airport +local category=airport:GetAirbaseCategory() +if airport:GetCoalition()==coalition then +local condition1=self.category==RAT.cat.plane and category==Airbase.Category.HELIPAD +local condition2=self.category==RAT.cat.plane and category==Airbase.Category.SHIP +if not(condition1 or condition2)then +table.insert(self.airports,airport) +end +end +end +end +if#self.airports==0 then +local text=string.format("No possible departure/destination airports found for RAT %s.",tostring(self.alias)) +MESSAGE:New(text,10):ToAll() +self:E(RAT.id..text) +end +end +function RAT:Status(message,forID) +if message==nil then +message=false +end +if forID==nil then +forID=false +end +local Tnow=timer.getTime() +local nalive=0 +for spawnindex,ratcraft in ipairs(self.ratcraft)do +local group=ratcraft.group +if group and group:IsAlive()then +nalive=nalive+1 +local prefix=self:_GetPrefixFromGroup(group) +local life=self:_GetLife(group) +local fuel=group:GetFuel()*100.0 +local airborne=group:InAir() +local coords=group:GetCoordinate() +local alt=coords.y +local departure=ratcraft.departure:GetName() +local destination=ratcraft.destination:GetName() +local type=self.aircraft.type +local status=ratcraft.status +local active=ratcraft.active +local Nunits=ratcraft.nunits +local N0units=group:GetInitialSize() +local Tg=0 +local Dg=0 +local dTlast=0 +local stationary=false +if airborne then +ratcraft["Tground"]=nil +ratcraft["Pground"]=nil +ratcraft["Uground"]=nil +ratcraft["Tlastcheck"]=nil +else +if ratcraft["Tground"]then +Tg=Tnow-ratcraft["Tground"] +Dg=coords:Get2DDistance(ratcraft["Pground"]) +dTlast=Tnow-ratcraft["Tlastcheck"] +if dTlast>self.Tinactive then +for _,_unit in pairs(group:GetUnits())do +if _unit and _unit:IsAlive()then +local unitname=_unit:GetName() +local unitcoord=_unit:GetCoordinate() +local Ug=unitcoord:Get2DDistance(ratcraft.Uground[unitname]) +self:T2(RAT.id..string.format("Unit %s travelled distance on ground %.1f m since %d seconds.",unitname,Ug,dTlast)) +if Ug<50 and active and status~=RAT.status.EventBirth then +stationary=true +end +ratcraft["Uground"][unitname]=unitcoord +end +end +ratcraft["Tlastcheck"]=Tnow +ratcraft["Pground"]=coords +end +else +ratcraft["Tground"]=Tnow +ratcraft["Tlastcheck"]=Tnow +ratcraft["Pground"]=coords +ratcraft["Uground"]={} +for _,_unit in pairs(group:GetUnits())do +local unitname=_unit:GetName() +ratcraft.Uground[unitname]=_unit:GetCoordinate() +end +end +end +local Pn=coords +local Dtravel=Pn:Get2DDistance(ratcraft["Pnow"]) +ratcraft["Pnow"]=Pn +ratcraft["Distance"]=ratcraft["Distance"]+Dtravel +local Ddestination=Pn:Get2DDistance(ratcraft.destination:GetCoordinate()) +if(forID and spawnindex==forID)or(not forID)then +local text=string.format("ID %i of flight %s",spawnindex,prefix) +if N0units>1 then +text=text..string.format(" (%d/%d)\n",Nunits,N0units) +else +text=text.."\n" +end +if self.commute then +text=text..string.format("%s commuting between %s and %s\n",type,departure,destination) +elseif self.continuejourney then +text=text..string.format("%s travelling from %s to %s (and continueing form there)\n",type,departure,destination) +else +text=text..string.format("%s travelling from %s to %s\n",type,departure,destination) +end +text=text..string.format("Status: %s",status) +if airborne then +text=text.." [airborne]\n" +else +text=text.." [on ground]\n" +end +text=text..string.format("Fuel = %3.0f %%\n",fuel) +text=text..string.format("Life = %3.0f %%\n",life) +text=text..string.format("FL%03d = %i m ASL\n",alt/RAT.unit.FL2m,alt) +text=text..string.format("Distance travelled = %6.1f km\n",ratcraft["Distance"]/1000) +text=text..string.format("Distance to destination = %6.1f km",Ddestination/1000) +if not airborne then +text=text..string.format("\nTime on ground = %6.0f seconds\n",Tg) +text=text..string.format("Position change = %8.1f m since %3.0f seconds.",Dg,dTlast) +end +self:T(RAT.id..text) +if message then +MESSAGE:New(text,20):ToAll() +end +end +if not airborne then +if stationary then +local text=string.format("Group %s is despawned after being %d seconds inaktive on ground.",self.alias,dTlast) +self:T(RAT.id..text) +self:_Despawn(group) +end +if life<10 and Dtravel<100 then +local text=string.format("Damaged group %s is despawned. Life = %3.0f",self.alias,life) +self:T(RAT.id..text) +self:_Despawn(group) +end +end +if ratcraft.despawnme then +local text=string.format("Flight %s will be despawned NOW!",self.alias) +self:T(RAT.id..text) +if(not self.norespawn)and(not self.respawn_after_takeoff)then +local idx=self:GetSpawnIndexFromGroup(group) +local coord=group:GetCoordinate() +self:_Respawn(idx,coord,0) +end +if self.despawnair then +self:_Despawn(group,0) +end +end +else +local text=string.format("Group does not exist in loop ratcraft status.") +self:T2(RAT.id..text) +end +end +local text=string.format("Alive groups of %s: %d, nalive=%d/%d",self.alias,self.alive,nalive,self.ngroups) +self:T(RAT.id..text) +MESSAGE:New(text,20):ToAllIf(message and not forID) +end +function RAT:_GetLife(group) +local life=0.0 +if group and group:IsAlive()then +local unit=group:GetUnit(1) +if unit then +life=unit:GetLife()/unit:GetLife0()*100 +else +self:T2(RAT.id.."ERROR! Unit does not exist in RAT_Getlife(). Returning zero.") +end +else +self:T2(RAT.id.."ERROR! Group does not exist in RAT_Getlife(). Returning zero.") +end +return life +end +function RAT:_SetStatus(group,status) +if group and group:IsAlive()then +local index=self:GetSpawnIndexFromGroup(group) +if self.ratcraft[index]then +self.ratcraft[index].status=status +local no1=status==RAT.status.Departure +local no2=status==RAT.status.EventBirthAir +local no3=status==RAT.status.Holding +local text=string.format("Flight %s: %s.",group:GetName(),status) +self:T(RAT.id..text) +if not(no1 or no2 or no3)then +MESSAGE:New(text,10):ToAllIf(self.reportstatus) +end +end +end +end +function RAT:GetStatus(group) +if group and group:IsAlive()then +local index=self:GetSpawnIndexFromGroup(group) +if self.ratcraft[index]then +return self.ratcraft[index].status +end +end +return"nonexistant" +end +function RAT:_OnBirth(EventData) +self:F3(EventData) +self:T3(RAT.id.."Captured event birth!") +local SpawnGroup=EventData.IniGroup +if SpawnGroup then +local EventPrefix=self:_GetPrefixFromGroup(SpawnGroup) +if EventPrefix then +if EventPrefix==self.alias then +local text="Event: Group "..SpawnGroup:GetName().." was born." +self:T(RAT.id..text) +local status="unknown in birth" +if SpawnGroup:InAir()then +status=RAT.status.EventBirthAir +elseif self.uncontrolled then +status=RAT.status.Uncontrolled +else +status=RAT.status.EventBirth +end +self:_SetStatus(SpawnGroup,status) +local i=self:GetSpawnIndexFromGroup(SpawnGroup) +local _departure=self.ratcraft[i].departure:GetName() +local _destination=self.ratcraft[i].destination:GetName() +local _nrespawn=self.ratcraft[i].nrespawn +local _takeoff=self.ratcraft[i].takeoff +local _landing=self.ratcraft[i].landing +local _livery=self.ratcraft[i].livery +local _airbase=AIRBASE:FindByName(_departure) +local onrunway=false +if _airbase then +if self.checkonrunway and _takeoff~=RAT.wp.runway and _takeoff~=RAT.wp.air then +onrunway=_airbase:CheckOnRunWay(SpawnGroup,self.onrunwayradius,false) +end +end +if onrunway then +local text=string.format("ERROR: RAT group of %s was spawned on runway. Group #%d will be despawned immediately!",self.alias,i) +MESSAGE:New(text,30):ToAllIf(self.Debug) +self:E(RAT.id..text) +if self.Debug then +SpawnGroup:FlareRed() +end +self:_Despawn(SpawnGroup) +if(self.Ndeparture_Airports>=2 or self.random_departure)and _nrespawn new state %s.",SpawnGroup:GetName(),currentstate,status) +self:T(RAT.id..text) +local idx=self:GetSpawnIndexFromGroup(SpawnGroup) +local coord=SpawnGroup:GetCoordinate() +self:_Respawn(idx,coord) +end +text="Event: Group "..SpawnGroup:GetName().." will be destroyed now." +self:T(RAT.id..text) +self:_Despawn(SpawnGroup) +end +end +end +else +self:T2(RAT.id.."ERROR: Group does not exist in RAT:_OnEngineShutdown().") +end +end +function RAT:_OnHit(EventData) +self:F3(EventData) +self:T(RAT.id..string.format("Captured event Hit by %s! Initiator %s. Target %s",self.alias,tostring(EventData.IniUnitName),tostring(EventData.TgtUnitName))) +local SpawnGroup=EventData.TgtGroup +if SpawnGroup then +local EventPrefix=self:_GetPrefixFromGroup(SpawnGroup) +if EventPrefix and EventPrefix==self.alias then +self:T(RAT.id..string.format("Event: Group %s was hit. Unit %s.",SpawnGroup:GetName(),tostring(EventData.TgtUnitName))) +local text=string.format("%s, unit %s was hit!",self.alias,EventData.TgtUnitName) +MESSAGE:New(text,10):ToAllIf(self.reportstatus or self.Debug) +end +end +end +function RAT:_OnDeadOrCrash(EventData) +self:F3(EventData) +self:T3(RAT.id.."Captured event DeadOrCrash!") +local SpawnGroup=EventData.IniGroup +if SpawnGroup then +local EventPrefix=self:_GetPrefixFromGroup(SpawnGroup) +if EventPrefix then +if EventPrefix==self.alias then +self.alive=self.alive-1 +local text=string.format("Event: Group %s crashed or died. Alive counter = %d.",SpawnGroup:GetName(),self.alive) +self:T(RAT.id..text) +if EventData.id==world.event.S_EVENT_CRASH then +self:_OnCrash(EventData) +elseif EventData.id==world.event.S_EVENT_DEAD then +self:_OnDead(EventData) +end +end +end +end +end +function RAT:_OnDead(EventData) +self:F3(EventData) +self:T3(RAT.id.."Captured event Dead!") +local SpawnGroup=EventData.IniGroup +if SpawnGroup then +local EventPrefix=self:_GetPrefixFromGroup(SpawnGroup) +if EventPrefix then +if EventPrefix==self.alias then +local text=string.format("Event: Group %s died. Unit %s.",SpawnGroup:GetName(),EventData.IniUnitName) +self:T(RAT.id..text) +local status=RAT.status.EventDead +self:_SetStatus(SpawnGroup,status) +end +end +else +self:T2(RAT.id.."ERROR: Group does not exist in RAT:_OnDead().") +end +end +function RAT:_OnCrash(EventData) +self:F3(EventData) +self:T3(RAT.id.."Captured event Crash!") +local SpawnGroup=EventData.IniGroup +if SpawnGroup then +local EventPrefix=self:_GetPrefixFromGroup(SpawnGroup) +if EventPrefix and EventPrefix==self.alias then +local _i=self:GetSpawnIndexFromGroup(SpawnGroup) +self.ratcraft[_i].nunits=self.ratcraft[_i].nunits-1 +local _n=self.ratcraft[_i].nunits +local _n0=SpawnGroup:GetInitialSize() +local text=string.format("Event: Group %s crashed. Unit %s. Units still alive %d of %d.",SpawnGroup:GetName(),EventData.IniUnitName,_n,_n0) +self:T(RAT.id..text) +local status=RAT.status.EventCrash +self:_SetStatus(SpawnGroup,status) +if _n==0 and self.respawn_after_crash and not self.norespawn then +local text=string.format("No units left of group %s. Group will be respawned now.",SpawnGroup:GetName()) +self:T(RAT.id..text) +local idx=self:GetSpawnIndexFromGroup(SpawnGroup) +local coord=SpawnGroup:GetCoordinate() +self:_Respawn(idx,coord) +end +end +else +if self.Debug then +self:E(RAT.id.."ERROR: Group does not exist in RAT:_OnCrash().") +end +end +end +function RAT:_Despawn(group,delay) +if group~=nil then +local index=self:GetSpawnIndexFromGroup(group) +if index~=nil then +self.ratcraft[index].group=nil +self.ratcraft[index]["status"]="Dead" +local despawndelay=0 +if delay then +despawndelay=delay +elseif self.respawn_delay then +despawndelay=self.respawn_delay +end +self:T(RAT.id..string.format("%s delayed despawn in %.1f seconds.",self.alias,despawndelay)) +SCHEDULER:New(nil,self._Destroy,{self,group},despawndelay) +if self.f10menu and self.SubMenuName~=nil then +self.Menu[self.SubMenuName]["groups"][index]:Remove() +end +end +end +end +function RAT:_Destroy(group) +self:F2(group) +local DCSGroup=group:GetDCSObject() +if DCSGroup and DCSGroup:isExist()then +local triggerdead=true +for _,DCSUnit in pairs(DCSGroup:getUnits())do +if DCSUnit then +if triggerdead then +self:_CreateEventDead(timer.getTime(),DCSUnit) +triggerdead=false +end +_DATABASE:DeleteUnit(DCSUnit:getName()) +end +end +DCSGroup:destroy() +DCSGroup=nil +end +return nil +end +function RAT:_CreateEventDead(EventTime,Initiator) +self:F({EventTime,Initiator}) +local Event={ +id=world.event.S_EVENT_DEAD, +time=EventTime, +initiator=Initiator, +} +world.onEvent(Event) +end +function RAT:_Waypoint(index,description,Type,Coord,Speed,Altitude,Airport) +local _Altitude=Altitude or Coord.y +local Hland=Coord:GetLandHeight() +local _Type=nil +local _Action=nil +local _alttype="RADIO" +if Type==RAT.wp.cold then +_Type="TakeOffParking" +_Action="From Parking Area" +_Altitude=10 +_alttype="RADIO" +elseif Type==RAT.wp.hot then +_Type="TakeOffParkingHot" +_Action="From Parking Area Hot" +_Altitude=10 +_alttype="RADIO" +elseif Type==RAT.wp.runway then +_Type="TakeOff" +_Action="From Parking Area" +_Altitude=10 +_alttype="RADIO" +elseif Type==RAT.wp.air then +_Type="Turning Point" +_Action="Turning Point" +_alttype="BARO" +elseif Type==RAT.wp.climb then +_Type="Turning Point" +_Action="Turning Point" +_alttype="BARO" +elseif Type==RAT.wp.cruise then +_Type="Turning Point" +_Action="Turning Point" +_alttype="BARO" +elseif Type==RAT.wp.descent then +_Type="Turning Point" +_Action="Turning Point" +_alttype="BARO" +elseif Type==RAT.wp.holding then +_Type="Turning Point" +_Action="Turning Point" +_alttype="BARO" +elseif Type==RAT.wp.landing then +_Type="Land" +_Action="Landing" +_Altitude=10 +_alttype="RADIO" +elseif Type==RAT.wp.finalwp then +_Type="Turning Point" +_Action="Turning Point" +_alttype="BARO" +else +self:E(RAT.id.."ERROR: Unknown waypoint type in RAT:Waypoint() function!") +_Type="Turning Point" +_Action="Turning Point" +_alttype="RADIO" +end +local text=string.format("\n******************************************************\n") +text=text..string.format("Waypoint = %d\n",index) +text=text..string.format("Template = %s\n",self.SpawnTemplatePrefix) +text=text..string.format("Alias = %s\n",self.alias) +text=text..string.format("Type: %i - %s\n",Type,_Type) +text=text..string.format("Action: %s\n",_Action) +text=text..string.format("Coord: x = %6.1f km, y = %6.1f km, alt = %6.1f m\n",Coord.x/1000,Coord.z/1000,Coord.y) +text=text..string.format("Speed = %6.1f m/s = %6.1f km/h = %6.1f knots\n",Speed,Speed*3.6,Speed*1.94384) +text=text..string.format("Land = %6.1f m ASL\n",Hland) +text=text..string.format("Altitude = %6.1f m (%s)\n",_Altitude,_alttype) +if Airport then +if Type==RAT.wp.air then +text=text..string.format("Zone = %s\n",Airport:GetName()) +else +text=text..string.format("Airport = %s\n",Airport:GetName()) +end +else +text=text..string.format("No airport/zone specified\n") +end +text=text.."******************************************************\n" +self:T2(RAT.id..text) +local RoutePoint={} +RoutePoint.x=Coord.x +RoutePoint.y=Coord.z +RoutePoint.alt=_Altitude +RoutePoint.alt_type=_alttype +RoutePoint.type=_Type +RoutePoint.action=_Action +RoutePoint.speed=Speed +RoutePoint.speed_locked=true +RoutePoint.ETA=nil +RoutePoint.ETA_locked=false +RoutePoint.name=description +if(Airport~=nil)and(Type~=RAT.wp.air)then +local AirbaseID=Airport:GetID() +local AirbaseCategory=Airport:GetAirbaseCategory() +if AirbaseCategory==Airbase.Category.SHIP then +RoutePoint.linkUnit=AirbaseID +RoutePoint.helipadId=AirbaseID +elseif AirbaseCategory==Airbase.Category.HELIPAD then +RoutePoint.linkUnit=AirbaseID +RoutePoint.helipadId=AirbaseID +elseif AirbaseCategory==Airbase.Category.AIRDROME then +RoutePoint.airdromeId=AirbaseID +else +self:T(RAT.id.."Unknown Airport category in _Waypoint()!") +end +end +RoutePoint.properties={ +["vnav"]=1, +["scale"]=0, +["angle"]=0, +["vangle"]=0, +["steer"]=2, +} +local TaskCombo={} +local TaskHolding=self:_TaskHolding({x=Coord.x,y=Coord.z},Altitude,Speed,self:_Randomize(90,0.9)) +local TaskWaypoint=self:_TaskFunction("RAT._WaypointFunction",self,index) +RoutePoint.task={} +RoutePoint.task.id="ComboTask" +RoutePoint.task.params={} +TaskCombo[#TaskCombo+1]=TaskWaypoint +if Type==RAT.wp.holding then +TaskCombo[#TaskCombo+1]=TaskHolding +end +RoutePoint.task.params.tasks=TaskCombo +return RoutePoint +end +function RAT:_Routeinfo(waypoints,comment) +local text=string.format("\n******************************************************\n") +text=text..string.format("Template = %s\n",self.SpawnTemplatePrefix) +if comment then +text=text..comment.."\n" +end +text=text..string.format("Number of waypoints = %i\n",#waypoints) +for i=1,#waypoints do +local p=waypoints[i] +text=text..string.format("WP #%i: x = %6.1f km, y = %6.1f km, alt = %6.1f m %s\n",i-1,p.x/1000,p.y/1000,p.alt,self.waypointdescriptions[i]) +end +local total=0.0 +for i=1,#waypoints-1 do +local point1=waypoints[i] +local point2=waypoints[i+1] +local x1=point1.x +local y1=point1.y +local x2=point2.x +local y2=point2.y +local d=math.sqrt((x1-x2)^2+(y1-y2)^2) +local heading=self:_Course(point1,point2) +total=total+d +text=text..string.format("Distance from WP %i-->%i = %6.1f km. Heading = %03d : %s - %s\n",i-1,i,d/1000,heading,self.waypointdescriptions[i],self.waypointdescriptions[i+1]) +end +text=text..string.format("Total distance = %6.1f km\n",total/1000) +text=text..string.format("******************************************************\n") +self:T2(RAT.id..text) +return total +end +function RAT:_TaskHolding(P1,Altitude,Speed,Duration) +local dx=3000 +local dy=0 +if self.category==RAT.cat.heli then +dx=200 +dy=0 +end +local P2={} +P2.x=P1.x+dx +P2.y=P1.y+dy +local Task={ +id='Orbit', +params={ +pattern=AI.Task.OrbitPattern.RACE_TRACK, +point=P1, +point2=P2, +speed=Speed, +altitude=Altitude +} +} +local DCSTask={} +DCSTask.id="ControlledTask" +DCSTask.params={} +DCSTask.params.task=Task +if self.ATCswitch then +local userflagname=string.format("%s#%03d",self.alias,self.SpawnIndex+1) +local maxholdingduration=60*120 +DCSTask.params.stopCondition={userFlag=userflagname,userFlagValue=1,duration=maxholdingduration} +else +DCSTask.params.stopCondition={duration=Duration} +end +return DCSTask +end +function RAT._WaypointFunction(group,rat,wp) +local Tnow=timer.getTime() +local sdx=rat:GetSpawnIndexFromGroup(group) +local departure=rat.ratcraft[sdx].departure:GetName() +local destination=rat.ratcraft[sdx].destination:GetName() +local landing=rat.ratcraft[sdx].landing +local WPholding=rat.ratcraft[sdx].wpholding +local WPfinal=rat.ratcraft[sdx].wpfinal +local text +text=string.format("Flight %s passing waypoint #%d %s.",group:GetName(),wp,rat.waypointdescriptions[wp]) +BASE.T(rat,RAT.id..text) +local status=rat.waypointstatus[wp] +rat:_SetStatus(group,status) +if wp==WPholding then +text=string.format("Flight %s to %s ATC: Holding and awaiting landing clearance.",group:GetName(),destination) +MESSAGE:New(text,10):ToAllIf(rat.reportstatus) +if rat.ATCswitch then +if rat.f10menu then +MENU_MISSION_COMMAND:New("Clear for landing",rat.Menu[rat.SubMenuName].groups[sdx],rat.ClearForLanding,rat,group:GetName()) +end +rat._ATCRegisterFlight(rat,group:GetName(),Tnow) +end +end +if wp==WPfinal then +text=string.format("Flight %s arrived at final destination %s.",group:GetName(),destination) +MESSAGE:New(text,10):ToAllIf(rat.reportstatus) +BASE.T(rat,RAT.id..text) +if landing==RAT.wp.air then +text=string.format("Activating despawn switch for flight %s! Group will be detroyed soon.",group:GetName()) +MESSAGE:New(text,10):ToAllIf(rat.Debug) +BASE.T(rat,RAT.id..text) +rat.ratcraft[sdx].despawnme=true +end +end +end +function RAT:_TaskFunction(FunctionString,...) +self:F2({FunctionString,arg}) +local DCSTask +local ArgumentKey +local templatename=self.templategroup:GetName() +local groupname=self:_AnticipatedGroupName() +local DCSScript={} +DCSScript[#DCSScript+1]="local MissionControllable = GROUP:FindByName(\""..groupname.."\") " +DCSScript[#DCSScript+1]="local RATtemplateControllable = GROUP:FindByName(\""..templatename.."\") " +if arg and arg.n>0 then +ArgumentKey='_'..tostring(arg):match("table: (.*)") +self.templategroup:SetState(self.templategroup,ArgumentKey,arg) +DCSScript[#DCSScript+1]="local Arguments = RATtemplateControllable:GetState(RATtemplateControllable, '"..ArgumentKey.."' ) " +DCSScript[#DCSScript+1]=FunctionString.."( MissionControllable, unpack( Arguments ) )" +else +DCSScript[#DCSScript+1]=FunctionString.."( MissionControllable )" +end +DCSTask=self.templategroup:TaskWrappedAction(self.templategroup:CommandDoScript(table.concat(DCSScript))) +return DCSTask +end +function RAT:_AnticipatedGroupName(index) +local index=index or self.SpawnIndex+1 +return string.format("%s#%03d",self.alias,index) +end +function RAT:_ActivateUncontrolled() +self:F() +local idx={} +local rat={} +local nactive=0 +for spawnindex,ratcraft in pairs(self.ratcraft)do +local group=ratcraft.group +if group and group:IsAlive()then +local text=string.format("Uncontrolled: Group = %s (spawnindex = %d), active = %s.",ratcraft.group:GetName(),spawnindex,tostring(ratcraft.active)) +self:T2(RAT.id..text) +if ratcraft.active then +nactive=nactive+1 +else +table.insert(idx,spawnindex) +end +end +end +local text=string.format("Uncontrolled: Ninactive = %d, Nactive = %d (of max %d).",#idx,nactive,self.activate_max) +self:T(RAT.id..text) +if#idx>0 and nactive=1 then +for i=1,nunits do +table.insert(parkingspots,spots[1].Coordinate) +table.insert(parkingindex,spots[1].TerminalID) +end +PointVec3=spots[1].Coordinate +else +_notenough=true +end +elseif spawnonairport then +if nfree>=nunits then +for i=1,nunits do +table.insert(parkingspots,spots[i].Coordinate) +table.insert(parkingindex,spots[i].TerminalID) +end +else +_notenough=true +end +end +if _notenough then +if self.respawn_inair and not self.SpawnUnControlled then +self:E(RAT.id..string.format("WARNING: Group %s has no parking spots at %s ==> air start!",self.SpawnTemplatePrefix,departure:GetName())) +spawnonground=false +spawnonship=false +spawnonfarp=false +spawnonrunway=false +waypoints[1].type=GROUPTEMPLATE.Takeoff[GROUP.Takeoff.Air][1] +waypoints[1].action=GROUPTEMPLATE.Takeoff[GROUP.Takeoff.Air][2] +PointVec3.x=PointVec3.x+math.random(-1500,1500) +PointVec3.z=PointVec3.z+math.random(-1500,1500) +if self.category==RAT.cat.heli then +PointVec3.y=PointVec3:GetLandHeight()+math.random(100,1000) +else +PointVec3.y=PointVec3:GetLandHeight()+math.random(500,3000) +end +else +self:E(RAT.id..string.format("WARNING: Group %s has no parking spots at %s ==> No emergency air start or uncontrolled spawning ==> No spawn!",self.SpawnTemplatePrefix,departure:GetName())) +return nil +end +end +else +end +for UnitID=1,nunits do +local UnitTemplate=SpawnTemplate.units[UnitID] +local SX=UnitTemplate.x +local SY=UnitTemplate.y +local BX=SpawnTemplate.route.points[1].x +local BY=SpawnTemplate.route.points[1].y +local TX=PointVec3.x+(SX-BX) +local TY=PointVec3.z+(SY-BY) +if spawnonground then +if spawnonship or spawnonfarp or spawnonrunway or automatic then +self:T(RAT.id..string.format("RAT group %s spawning at farp, ship or runway %s.",self.alias,departure:GetName())) +SpawnTemplate.units[UnitID].x=PointVec3.x +SpawnTemplate.units[UnitID].y=PointVec3.z +SpawnTemplate.units[UnitID].alt=PointVec3.y +else +self:T(RAT.id..string.format("RAT group %s spawning at airbase %s on parking spot id %d",self.alias,departure:GetName(),parkingindex[UnitID])) +SpawnTemplate.units[UnitID].x=parkingspots[UnitID].x +SpawnTemplate.units[UnitID].y=parkingspots[UnitID].z +SpawnTemplate.units[UnitID].alt=parkingspots[UnitID].y +end +else +self:T(RAT.id..string.format("RAT group %s spawning in air at %s.",self.alias,departure:GetName())) +SpawnTemplate.units[UnitID].x=TX +SpawnTemplate.units[UnitID].y=TY +SpawnTemplate.units[UnitID].alt=PointVec3.y +end +if self.Debug then +local unitspawn=COORDINATE:New(SpawnTemplate.units[UnitID].x,SpawnTemplate.units[UnitID].alt,SpawnTemplate.units[UnitID].y) +unitspawn:MarkToAll(string.format("RAT %s Spawnplace unit #%d",self.alias,UnitID)) +end +UnitTemplate.parking=nil +UnitTemplate.parking_id=nil +if parkingindex[UnitID]and not automatic then +UnitTemplate.parking=parkingindex[UnitID] +end +self:T2(RAT.id..string.format("RAT group %s unit number %d: Parking = %s",self.alias,UnitID,tostring(UnitTemplate.parking))) +self:T2(RAT.id..string.format("RAT group %s unit number %d: Parking ID = %s",self.alias,UnitID,tostring(UnitTemplate.parking_id))) +SpawnTemplate.units[UnitID].heading=heading +SpawnTemplate.units[UnitID].psi=-heading +if livery then +SpawnTemplate.units[UnitID].livery_id=livery +end +if self.actype then +SpawnTemplate.units[UnitID]["type"]=self.actype +end +SpawnTemplate.units[UnitID]["skill"]=self.skill +if self.onboardnum then +SpawnTemplate.units[UnitID]["onboard_num"]=string.format("%s%d%02d",self.onboardnum,(self.SpawnIndex-1)%10,(self.onboardnum0-1)+UnitID) +end +SpawnTemplate.CoalitionID=self.coalition +if self.country then +SpawnTemplate.CountryID=self.country +end +end +for i,wp in ipairs(waypoints)do +SpawnTemplate.route.points[i]=wp +end +SpawnTemplate.x=PointVec3.x +SpawnTemplate.y=PointVec3.z +if self.radio then +SpawnTemplate.communication=self.radio +end +if self.frequency then +SpawnTemplate.frequency=self.frequency +end +if self.modulation then +SpawnTemplate.modulation=self.modulation +end +self:T(SpawnTemplate) +end +end +return true +end +function RAT:_ATCInit(airports_map) +if not RAT.ATC.init then +local text +text="Starting RAT ATC.\nSimultanious = "..RAT.ATC.Nclearance.."\n".."Delay = "..RAT.ATC.delay +BASE:T(RAT.id..text) +RAT.ATC.init=true +for _,ap in pairs(airports_map)do +local name=ap:GetName() +RAT.ATC.airport[name]={} +RAT.ATC.airport[name].queue={} +RAT.ATC.airport[name].busy=false +RAT.ATC.airport[name].onfinal={} +RAT.ATC.airport[name].Nonfinal=0 +RAT.ATC.airport[name].traffic=0 +RAT.ATC.airport[name].Tlastclearance=nil +end +SCHEDULER:New(nil,RAT._ATCCheck,{self},5,15) +SCHEDULER:New(nil,RAT._ATCStatus,{self},5,60) +RAT.ATC.T0=timer.getTime() +end +end +function RAT:_ATCAddFlight(name,dest) +BASE:T(string.format("%sATC %s: Adding flight %s with destination %s.",RAT.id,dest,name,dest)) +RAT.ATC.flight[name]={} +RAT.ATC.flight[name].destination=dest +RAT.ATC.flight[name].Tarrive=-1 +RAT.ATC.flight[name].holding=-1 +RAT.ATC.flight[name].Tonfinal=-1 +end +function RAT:_ATCDelFlight(t,entry) +for k,_ in pairs(t)do +if k==entry then +t[entry]=nil +end +end +end +function RAT:_ATCRegisterFlight(name,time) +BASE:T(RAT.id.."Flight "..name.." registered at ATC for landing clearance.") +RAT.ATC.flight[name].Tarrive=time +RAT.ATC.flight[name].holding=0 +end +function RAT:_ATCStatus() +local Tnow=timer.getTime() +for name,_ in pairs(RAT.ATC.flight)do +local hold=RAT.ATC.flight[name].holding +local dest=RAT.ATC.flight[name].destination +if hold>=0 then +local busy="Runway state is unknown" +if RAT.ATC.airport[dest].Nonfinal>0 then +busy="Runway is occupied by "..RAT.ATC.airport[dest].Nonfinal +else +busy="Runway is currently clear" +end +local text=string.format("ATC %s: Flight %s is holding for %i:%02d. %s.",dest,name,hold/60,hold%60,busy) +BASE:T(RAT.id..text) +elseif hold==RAT.ATC.onfinal then +local Tfinal=Tnow-RAT.ATC.flight[name].Tonfinal +local text=string.format("ATC %s: Flight %s is on final. Waiting %i:%02d for landing event.",dest,name,Tfinal/60,Tfinal%60) +BASE:T(RAT.id..text) +elseif hold==RAT.ATC.unregistered then +else +BASE:E(RAT.id.."ERROR: Unknown holding time in RAT:_ATCStatus().") +end +end +end +function RAT:_ATCCheck() +RAT:_ATCQueue() +local Tnow=timer.getTime() +for name,_ in pairs(RAT.ATC.airport)do +for qID,flight in ipairs(RAT.ATC.airport[name].queue)do +local nqueue=#RAT.ATC.airport[name].queue +local landing1 +if RAT.ATC.airport[name].Tlastclearance then +landing1=(Tnow-RAT.ATC.airport[name].Tlastclearance>RAT.ATC.delay)and RAT.ATC.airport[name].Nonfinal=0 then +RAT.ATC.flight[name].holding=Tnow-RAT.ATC.flight[name].Tarrive +end +local hold=RAT.ATC.flight[name].holding +local dest=RAT.ATC.flight[name].destination +if hold>=0 and airport==dest then +_queue[#_queue+1]={name,hold} +end +end +local function compare(a,b) +return a[2]>b[2] +end +table.sort(_queue,compare) +RAT.ATC.airport[airport].queue={} +for k,v in ipairs(_queue)do +table.insert(RAT.ATC.airport[airport].queue,v[1]) +end +end +end +RATMANAGER={ +ClassName="RATMANAGER", +Debug=false, +rat={}, +name={}, +alive={}, +min={}, +nrat=0, +ntot=nil, +Tcheck=60, +dTspawn=1.0, +manager=nil, +managerid=nil, +} +RATMANAGER.id="RATMANAGER | " +function RATMANAGER:New(ntot) +local self=BASE:Inherit(self,BASE:New()) +self.ntot=ntot or 1 +self:E(RATMANAGER.id..string.format("Creating manager for %d groups.",ntot)) +return self +end +function RATMANAGER:Add(ratobject,min) +ratobject.norespawn=true +ratobject.f10menu=false +self.nrat=self.nrat+1 +self.rat[self.nrat]=ratobject +self.alive[self.nrat]=0 +self.name[self.nrat]=ratobject.alias +self.min[self.nrat]=min or 1 +self:T(RATMANAGER.id..string.format("Adding ratobject %s with min flights = %d",self.name[self.nrat],self.min[self.nrat])) +ratobject:Spawn(0) +return self +end +function RATMANAGER:Start(delay) +local delay=delay or 5 +local text=string.format(RATMANAGER.id.."RAT manager will be started in %d seconds.\n",delay) +text=text..string.format("Managed groups:\n") +for i=1,self.nrat do +text=text..string.format("- %s with min groups %d\n",self.name[i],self.min[i]) +end +text=text..string.format("Number of constantly alive groups %d",self.ntot) +self:E(text) +SCHEDULER:New(nil,self._Start,{self},delay) +return self +end +function RATMANAGER:_Start() +local n=0 +for i=1,self.nrat do +n=n+self.min[i] +end +self.ntot=math.max(self.ntot,n) +local N=self:_RollDice(self.nrat,self.ntot,self.min,self.alive) +local time=0.0 +for i=1,self.nrat do +for j=1,N[i]do +time=time+self.dTspawn +SCHEDULER:New(nil,RAT._SpawnWithRoute,{self.rat[i]},time) +end +end +for i=1,self.nrat do +if self.rat[i].uncontrolled and self.rat[i].activate_uncontrolled then +local Tactivate=math.max(time+1,self.rat[i].activate_delay) +SCHEDULER:New(self.rat[i],self.rat[i]._ActivateUncontrolled,{self.rat[i]},Tactivate,self.rat[i].activate_delta,self.rat[i].activate_frand) +end +end +local TstartManager=math.max(time+1,self.Tcheck) +self.manager,self.managerid=SCHEDULER:New(self,self._Manage,{self},TstartManager,self.Tcheck) +local text=string.format(RATMANAGER.id.."Starting RAT manager with scheduler ID %s in %d seconds. Repeat interval %d seconds.",self.managerid,TstartManager,self.Tcheck) +self:E(text) +return self +end +function RATMANAGER:Stop(delay) +delay=delay or 1 +self:E(string.format(RATMANAGER.id.."Manager will be stopped in %d seconds.",delay)) +SCHEDULER:New(nil,self._Stop,{self},delay) +return self +end +function RATMANAGER:_Stop() +self:E(string.format(RATMANAGER.id.."Stopping manager with scheduler ID %s.",self.managerid)) +self.manager:Stop(self.managerid) +return self +end +function RATMANAGER:SetTcheck(dt) +self.Tcheck=dt or 60 +return self +end +function RATMANAGER:SetTspawn(dt) +self.dTspawn=dt or 1.0 +return self +end +function RATMANAGER:_Manage() +local ntot=self:_Count() +local text=string.format("Number of alive groups %d. New groups to be spawned %d.",ntot,self.ntot-ntot) +self:T(RATMANAGER.id..text) +local N=self:_RollDice(self.nrat,self.ntot,self.min,self.alive) +local time=0.0 +for i=1,self.nrat do +for j=1,N[i]do +time=time+self.dTspawn +SCHEDULER:New(nil,RAT._SpawnWithRoute,{self.rat[i]},time) +end +end +end +function RATMANAGER:_Count() +local ntotal=0 +for i=1,self.nrat do +local n=0 +local ratobject=self.rat[i] +for spawnindex,ratcraft in pairs(ratobject.ratcraft)do +local group=ratcraft.group +if group and group:IsAlive()then +n=n+1 +end +end +self.alive[i]=n +ntotal=ntotal+n +local text=string.format("Number of alive groups of %s = %d",self.name[i],n) +self:T(RATMANAGER.id..text) +end +return ntotal +end +function RATMANAGER:_RollDice(nrat,ntot,min,alive) +local function sum(A,index) +local summe=0 +for _,i in ipairs(index)do +summe=summe+A[i] +end +return summe +end +local N={} +local M={} +local P={} +for i=1,nrat do +N[#N+1]=0 +M[#M+1]=math.max(alive[i],min[i]) +P[#P+1]=math.max(min[i]-alive[i],0) +end +local mini={} +local maxi={} +local rattab={} +for i=1,nrat do +table.insert(rattab,i) +end +local done={} +local nnew=ntot +for i=1,nrat do +nnew=nnew-alive[i] +end +for i=1,nrat-1 do +local r=math.random(#rattab) +local j=rattab[r] +table.remove(rattab,r) +table.insert(done,j) +local sN=sum(N,done) +local sP=sum(P,rattab) +maxi[j]=nnew-sN-sP +mini[j]=P[j] +if maxi[j]>=mini[j]then +N[j]=math.random(mini[j],maxi[j]) +else +N[j]=0 +end +self:T3(string.format("RATMANAGER: i=%d, alive=%d, min=%d, mini=%d, maxi=%d, add=%d, sumN=%d, sumP=%d",j,alive[j],min[j],mini[j],maxi[j],N[j],sN,sP)) +end +local j=rattab[1] +N[j]=nnew-sum(N,done) +mini[j]=nnew-sum(N,done) +maxi[j]=nnew-sum(N,done) +table.remove(rattab,1) +table.insert(done,j) +local text=RATMANAGER.id.."\n" +for i=1,nrat do +text=text..string.format("%s: i=%d, alive=%d, min=%d, mini=%d, maxi=%d, add=%d\n",self.name[i],i,alive[i],min[i],mini[i],maxi[i],N[i]) +end +text=text..string.format("Total # of groups to add = %d",sum(N,done)) +self:T(text) +return N +end +RANGE={ +ClassName="RANGE", +Debug=false, +verbose=0, +id=nil, +rangename=nil, +location=nil, +messages=true, +rangeradius=5000, +rangezone=nil, +strafeTargets={}, +bombingTargets={}, +nbombtargets=0, +nstrafetargets=0, +MenuAddedTo={}, +planes={}, +strafeStatus={}, +strafePlayerResults={}, +bombPlayerResults={}, +PlayerSettings={}, +dtBombtrack=0.005, +BombtrackThreshold=25000, +Tmsg=30, +examinergroupname=nil, +examinerexclusive=nil, +strafemaxalt=914, +ndisplayresult=10, +BombSmokeColor=SMOKECOLOR.Red, +StrafeSmokeColor=SMOKECOLOR.Green, +StrafePitSmokeColor=SMOKECOLOR.White, +illuminationminalt=500, +illuminationmaxalt=1000, +scorebombdistance=1000, +TdelaySmoke=3.0, +eventmoose=true, +trackbombs=true, +trackrockets=true, +trackmissiles=true, +defaultsmokebomb=true, +autosave=false, +instructorfreq=nil, +instructor=nil, +rangecontrolfreq=nil, +rangecontrol=nil, +soundpath="Range Soundfiles/" +} +RANGE.Defaults={ +goodhitrange=25, +strafemaxalt=914, +dtBombtrack=0.005, +Tmsg=30, +ndisplayresult=10, +rangeradius=5000, +TdelaySmoke=3.0, +boxlength=3000, +boxwidth=300, +goodpass=20, +goodhitrange=25, +foulline=610, +} +RANGE.TargetType={ +UNIT="Unit", +STATIC="Static", +COORD="Coordinate", +} +RANGE.Sound={ +RC0={filename="RC-0.ogg",duration=0.60}, +RC1={filename="RC-1.ogg",duration=0.47}, +RC2={filename="RC-2.ogg",duration=0.43}, +RC3={filename="RC-3.ogg",duration=0.50}, +RC4={filename="RC-4.ogg",duration=0.58}, +RC5={filename="RC-5.ogg",duration=0.54}, +RC6={filename="RC-6.ogg",duration=0.61}, +RC7={filename="RC-7.ogg",duration=0.53}, +RC8={filename="RC-8.ogg",duration=0.34}, +RC9={filename="RC-9.ogg",duration=0.54}, +RCAccuracy={filename="RC-Accuracy.ogg",duration=0.67}, +RCDegrees={filename="RC-Degrees.ogg",duration=0.59}, +RCExcellentHit={filename="RC-ExcellentHit.ogg",duration=0.76}, +RCExcellentPass={filename="RC-ExcellentPass.ogg",duration=0.89}, +RCFeet={filename="RC-Feet.ogg",duration=0.49}, +RCFor={filename="RC-For.ogg",duration=0.64}, +RCGoodHit={filename="RC-GoodHit.ogg",duration=0.52}, +RCGoodPass={filename="RC-GoodPass.ogg",duration=0.62}, +RCHitsOnTarget={filename="RC-HitsOnTarget.ogg",duration=0.88}, +RCImpact={filename="RC-Impact.ogg",duration=0.61}, +RCIneffectiveHit={filename="RC-IneffectiveHit.ogg",duration=0.86}, +RCIneffectivePass={filename="RC-IneffectivePass.ogg",duration=0.99}, +RCInvalidHit={filename="RC-InvalidHit.ogg",duration=2.97}, +RCLeftStrafePitTooQuickly={filename="RC-LeftStrafePitTooQuickly.ogg",duration=3.09}, +RCPercent={filename="RC-Percent.ogg",duration=0.56}, +RCPoorHit={filename="RC-PoorHit.ogg",duration=0.54}, +RCPoorPass={filename="RC-PoorPass.ogg",duration=0.68}, +RCRollingInOnStrafeTarget={filename="RC-RollingInOnStrafeTarget.ogg",duration=1.38}, +RCTotalRoundsFired={filename="RC-TotalRoundsFired.ogg",duration=1.22}, +RCWeaponImpactedTooFar={filename="RC-WeaponImpactedTooFar.ogg",duration=3.73}, +IR0={filename="IR-0.ogg",duration=0.55}, +IR1={filename="IR-1.ogg",duration=0.41}, +IR2={filename="IR-2.ogg",duration=0.37}, +IR3={filename="IR-3.ogg",duration=0.41}, +IR4={filename="IR-4.ogg",duration=0.37}, +IR5={filename="IR-5.ogg",duration=0.43}, +IR6={filename="IR-6.ogg",duration=0.55}, +IR7={filename="IR-7.ogg",duration=0.43}, +IR8={filename="IR-8.ogg",duration=0.38}, +IR9={filename="IR-9.ogg",duration=0.55}, +IRDecimal={filename="IR-Decimal.ogg",duration=0.54}, +IRMegaHertz={filename="IR-MegaHertz.ogg",duration=0.87}, +IREnterRange={filename="IR-EnterRange.ogg",duration=4.83}, +IRExitRange={filename="IR-ExitRange.ogg",duration=3.10}, +} +RANGE.Names={} +RANGE.MenuF10={} +RANGE.MenuF10Root=nil +RANGE.version="2.3.0" +function RANGE:New(rangename) +BASE:F({rangename=rangename}) +local self=BASE:Inherit(self,FSM:New()) +self.rangename=rangename or"Practice Range" +self.id=string.format("RANGE %s | ",self.rangename) +local text=string.format("Script version %s - creating new RANGE object %s.",RANGE.version,self.rangename) +self:I(self.id..text) +self:SetDefaultPlayerSmokeBomb() +self:SetStartState("Stopped") +self:AddTransition("Stopped","Start","Running") +self:AddTransition("*","Status","*") +self:AddTransition("*","Impact","*") +self:AddTransition("*","EnterRange","*") +self:AddTransition("*","ExitRange","*") +self:AddTransition("*","Save","*") +self:AddTransition("*","Load","*") +return self +end +function RANGE:onafterStart() +local _location=nil +local _count=0 +for _,_target in pairs(self.bombingTargets)do +_count=_count+1 +if _location==nil then +_location=self:_GetBombTargetCoordinate(_target) +end +end +self.nbombtargets=_count +_count=0 +for _,_target in pairs(self.strafeTargets)do +_count=_count+1 +for _,_unit in pairs(_target.targets)do +if _location==nil then +_location=_unit:GetCoordinate() +end +end +end +self.nstrafetargets=_count +if self.location==nil then +self.location=_location +end +if self.location==nil then +local text=string.format("ERROR! No range location found. Number of strafe targets = %d. Number of bomb targets = %d.",self.nstrafetargets,self.nbombtargets) +self:E(self.id..text) +return +end +if self.rangezone==nil then +self.rangezone=ZONE_RADIUS:New(self.rangename,{x=self.location.x,y=self.location.z},self.rangeradius) +end +local text=string.format("Starting RANGE %s. Number of strafe targets = %d. Number of bomb targets = %d.",self.rangename,self.nstrafetargets,self.nbombtargets) +self:I(self.id..text) +if self.eventmoose then +self:T(self.id.."Events are handled by MOOSE.") +self:HandleEvent(EVENTS.Birth) +self:HandleEvent(EVENTS.Hit) +self:HandleEvent(EVENTS.Shot) +else +self:T(self.id.."Events are handled directly by DCS.") +world.addEventHandler(self) +end +for _,_target in pairs(self.bombingTargets)do +local _static=_target.type==RANGE.TargetType.STATIC +if _target.move and _static==false and _target.speed>1 then +local unit=_target.target +_target.target:PatrolZones({self.rangezone},_target.speed*0.75,"Off road") +end +end +if self.rangecontrolfreq then +self.rangecontrol=RADIOQUEUE:New(self.rangecontrolfreq,nil,self.rangename) +self.rangecontrol.schedonce=true +self.rangecontrol:SetDigit(0,RANGE.Sound.RC0.filename,RANGE.Sound.RC0.duration,self.soundpath) +self.rangecontrol:SetDigit(1,RANGE.Sound.RC1.filename,RANGE.Sound.RC1.duration,self.soundpath) +self.rangecontrol:SetDigit(2,RANGE.Sound.RC2.filename,RANGE.Sound.RC2.duration,self.soundpath) +self.rangecontrol:SetDigit(3,RANGE.Sound.RC3.filename,RANGE.Sound.RC3.duration,self.soundpath) +self.rangecontrol:SetDigit(4,RANGE.Sound.RC4.filename,RANGE.Sound.RC4.duration,self.soundpath) +self.rangecontrol:SetDigit(5,RANGE.Sound.RC5.filename,RANGE.Sound.RC5.duration,self.soundpath) +self.rangecontrol:SetDigit(6,RANGE.Sound.RC6.filename,RANGE.Sound.RC6.duration,self.soundpath) +self.rangecontrol:SetDigit(7,RANGE.Sound.RC7.filename,RANGE.Sound.RC7.duration,self.soundpath) +self.rangecontrol:SetDigit(8,RANGE.Sound.RC8.filename,RANGE.Sound.RC8.duration,self.soundpath) +self.rangecontrol:SetDigit(9,RANGE.Sound.RC9.filename,RANGE.Sound.RC9.duration,self.soundpath) +self.rangecontrol:SetSenderCoordinate(self.location) +self.rangecontrol:SetSenderUnitName(self.rangecontrolrelayname) +self.rangecontrol:Start(1,0.1) +if self.instructorfreq then +self.instructor=RADIOQUEUE:New(self.instructorfreq,nil,self.rangename) +self.instructor.schedonce=true +self.instructor:SetDigit(0,RANGE.Sound.IR0.filename,RANGE.Sound.IR0.duration,self.soundpath) +self.instructor:SetDigit(1,RANGE.Sound.IR1.filename,RANGE.Sound.IR1.duration,self.soundpath) +self.instructor:SetDigit(2,RANGE.Sound.IR2.filename,RANGE.Sound.IR2.duration,self.soundpath) +self.instructor:SetDigit(3,RANGE.Sound.IR3.filename,RANGE.Sound.IR3.duration,self.soundpath) +self.instructor:SetDigit(4,RANGE.Sound.IR4.filename,RANGE.Sound.IR4.duration,self.soundpath) +self.instructor:SetDigit(5,RANGE.Sound.IR5.filename,RANGE.Sound.IR5.duration,self.soundpath) +self.instructor:SetDigit(6,RANGE.Sound.IR6.filename,RANGE.Sound.IR6.duration,self.soundpath) +self.instructor:SetDigit(7,RANGE.Sound.IR7.filename,RANGE.Sound.IR7.duration,self.soundpath) +self.instructor:SetDigit(8,RANGE.Sound.IR8.filename,RANGE.Sound.IR8.duration,self.soundpath) +self.instructor:SetDigit(9,RANGE.Sound.IR9.filename,RANGE.Sound.IR9.duration,self.soundpath) +self.instructor:SetSenderCoordinate(self.location) +self.instructor:SetSenderUnitName(self.instructorrelayname) +self.instructor:Start(1,0.1) +end +end +if self.autosave then +self:Load() +end +if self.Debug then +self:_MarkTargetsOnMap() +self:_SmokeBombTargets() +self:_SmokeStrafeTargets() +self:_SmokeStrafeTargetBoxes() +self.rangezone:SmokeZone(SMOKECOLOR.White) +end +self:__Status(-60) +end +function RANGE:SetMaxStrafeAlt(maxalt) +self.strafemaxalt=maxalt or RANGE.Defaults.strafemaxalt +return self +end +function RANGE:SetBombtrackTimestep(dt) +self.dtBombtrack=dt or RANGE.Defaults.dtBombtrack +return self +end +function RANGE:SetMessageTimeDuration(time) +self.Tmsg=time or RANGE.Defaults.Tmsg +return self +end +function RANGE:SetAutosaveOn() +self.autosave=true +return self +end +function RANGE:SetAutosaveOff() +self.autosave=false +return self +end +function RANGE:SetMessageToExaminer(examinergroupname,exclusively) +self.examinergroupname=examinergroupname +self.examinerexclusive=exclusively +return self +end +function RANGE:SetDisplayedMaxPlayerResults(nmax) +self.ndisplayresult=nmax or RANGE.Defaults.ndisplayresult +return self +end +function RANGE:SetRangeRadius(radius) +self.rangeradius=radius*1000 or RANGE.Defaults.rangeradius +return self +end +function RANGE:SetDefaultPlayerSmokeBomb(switch) +if switch==true or switch==nil then +self.defaultsmokebomb=true +else +self.defaultsmokebomb=false +end +return self +end +function RANGE:SetBombtrackThreshold(distance) +self.BombtrackThreshold=(distance or 25)*1000 +return self +end +function RANGE:SetRangeLocation(coordinate) +self.location=coordinate +return self +end +function RANGE:SetRangeZone(zone) +self.rangezone=zone +return self +end +function RANGE:SetBombTargetSmokeColor(colorid) +self.BombSmokeColor=colorid or SMOKECOLOR.Red +return self +end +function RANGE:SetScoreBombDistance(distance) +self.scorebombdistance=distance or 1000 +return self +end +function RANGE:SetStrafeTargetSmokeColor(colorid) +self.StrafeSmokeColor=colorid or SMOKECOLOR.Green +return self +end +function RANGE:SetStrafePitSmokeColor(colorid) +self.StrafePitSmokeColor=colorid or SMOKECOLOR.White +return self +end +function RANGE:SetSmokeTimeDelay(delay) +self.TdelaySmoke=delay or RANGE.Defaults.TdelaySmoke +return self +end +function RANGE:DebugON() +self.Debug=true +return self +end +function RANGE:DebugOFF() +self.Debug=false +return self +end +function RANGE:SetMessagesOFF() +self.messages=false +return self +end +function RANGE:SetMessagesON() +self.messages=true +return self +end +function RANGE:TrackBombsON() +self.trackbombs=true +return self +end +function RANGE:TrackBombsOFF() +self.trackbombs=false +return self +end +function RANGE:TrackRocketsON() +self.trackrockets=true +return self +end +function RANGE:TrackRocketsOFF() +self.trackrockets=false +return self +end +function RANGE:TrackMissilesON() +self.trackmissiles=true +return self +end +function RANGE:TrackMissilesOFF() +self.trackmissiles=false +return self +end +function RANGE:SetRangeControl(frequency,relayunitname) +self.rangecontrolfreq=frequency or 256 +self.rangecontrolrelayname=relayunitname +return self +end +function RANGE:SetInstructorRadio(frequency,relayunitname) +self.instructorfreq=frequency or 305 +self.instructorrelayname=relayunitname +return self +end +function RANGE:SetSoundfilesPath(path) +self.soundpath=tostring(path or"Range Soundfiles/") +self:I(self.id..string.format("Setting sound files path to %s",self.soundpath)) +return self +end +function RANGE:AddStrafePit(targetnames,boxlength,boxwidth,heading,inverseheading,goodpass,foulline) +self:F({targetnames=targetnames,boxlength=boxlength,boxwidth=boxwidth,heading=heading,inverseheading=inverseheading,goodpass=goodpass,foulline=foulline}) +if type(targetnames)~="table"then +targetnames={targetnames} +end +local _targets={} +local center=nil +local ntargets=0 +for _i,_name in ipairs(targetnames)do +local _isstatic=self:_CheckStatic(_name) +local unit=nil +if _isstatic==true then +self:T(self.id..string.format("Adding STATIC object %s as strafe target #%d.",_name,_i)) +unit=STATIC:FindByName(_name,false) +elseif _isstatic==false then +self:T(self.id..string.format("Adding UNIT object %s as strafe target #%d.",_name,_i)) +unit=UNIT:FindByName(_name) +else +local text=string.format("ERROR! Could not find ANY strafe target object with name %s.",_name) +self:E(self.id..text) +end +if unit then +table.insert(_targets,unit) +if center==nil then +center=unit +end +ntargets=ntargets+1 +end +end +if ntargets==0 then +local text=string.format("ERROR! No strafe target could be found when calling RANGE:AddStrafePit() for range %s",self.rangename) +self:E(self.id..text) +return +end +local l=boxlength or RANGE.Defaults.boxlength +local w=(boxwidth or RANGE.Defaults.boxwidth)/2 +local heading=heading or center:GetHeading() +if inverseheading~=nil then +if inverseheading then +heading=heading-180 +end +end +if heading<0 then +heading=heading+360 +end +if heading>360 then +heading=heading-360 +end +goodpass=goodpass or RANGE.Defaults.goodpass +foulline=foulline or RANGE.Defaults.foulline +local Ccenter=center:GetCoordinate() +local _name=center:GetName() +local p={} +p[#p+1]=Ccenter:Translate(w,heading+90) +p[#p+1]=p[#p]:Translate(l,heading) +p[#p+1]=p[#p]:Translate(2*w,heading-90) +p[#p+1]=p[#p]:Translate(-l,heading) +local pv2={} +for i,p in ipairs(p)do +pv2[i]={x=p.x,y=p.z} +end +local _polygon=ZONE_POLYGON_BASE:New(_name,pv2) +local st={} +st.name=_name +st.polygon=_polygon +st.coordinate=Ccenter +st.goodPass=goodpass +st.targets=_targets +st.foulline=foulline +st.smokepoints=p +st.heading=heading +table.insert(self.strafeTargets,st) +local text=string.format("Adding new strafe target %s with %d targets: heading = %03d, box_L = %.1f, box_W = %.1f, goodpass = %d, foul line = %.1f",_name,ntargets,heading,l,w,goodpass,foulline) +self:T(self.id..text) +return self +end +function RANGE:AddStrafePitGroup(group,boxlength,boxwidth,heading,inverseheading,goodpass,foulline) +self:F({group=group,boxlength=boxlength,boxwidth=boxwidth,heading=heading,inverseheading=inverseheading,goodpass=goodpass,foulline=foulline}) +if group and group:IsAlive()then +local _units=group:GetUnits() +local _names={} +for _,_unit in ipairs(_units)do +local _unit=_unit +if _unit and _unit:IsAlive()then +local _name=_unit:GetName() +table.insert(_names,_name) +end +end +self:AddStrafePit(_names,boxlength,boxwidth,heading,inverseheading,goodpass,foulline) +end +return self +end +function RANGE:AddBombingTargets(targetnames,goodhitrange,randommove) +self:F({targetnames=targetnames,goodhitrange=goodhitrange,randommove=randommove}) +if type(targetnames)~="table"then +targetnames={targetnames} +end +goodhitrange=goodhitrange or RANGE.Defaults.goodhitrange +for _,name in pairs(targetnames)do +local _isstatic=self:_CheckStatic(name) +if _isstatic==true then +local _static=STATIC:FindByName(name) +self:T2(self.id..string.format("Adding static bombing target %s with hit range %d.",name,goodhitrange,false)) +self:AddBombingTargetUnit(_static,goodhitrange) +elseif _isstatic==false then +local _unit=UNIT:FindByName(name) +self:T2(self.id..string.format("Adding unit bombing target %s with hit range %d.",name,goodhitrange,randommove)) +self:AddBombingTargetUnit(_unit,goodhitrange) +else +self:E(self.id..string.format("ERROR! Could not find bombing target %s.",name)) +end +end +return self +end +function RANGE:AddBombingTargetUnit(unit,goodhitrange,randommove) +self:F({unit=unit,goodhitrange=goodhitrange,randommove=randommove}) +local name=unit:GetName() +local _isstatic=self:_CheckStatic(name) +goodhitrange=goodhitrange or RANGE.Defaults.goodhitrange +if randommove==nil or _isstatic==true then +randommove=false +end +if _isstatic==true then +self:I(self.id..string.format("Adding STATIC bombing target %s with good hit range %d. Random move = %s.",name,goodhitrange,tostring(randommove))) +elseif _isstatic==false then +self:I(self.id..string.format("Adding UNIT bombing target %s with good hit range %d. Random move = %s.",name,goodhitrange,tostring(randommove))) +else +self:E(self.id..string.format("ERROR! No bombing target with name %s could be found. Carefully check all UNIT and STATIC names defined in the mission editor!",name)) +end +local speed=0 +if _isstatic==false then +speed=self:_GetSpeed(unit) +end +local target={} +target.name=name +target.target=unit +target.goodhitrange=goodhitrange +target.move=randommove +target.speed=speed +target.coordinate=unit:GetCoordinate() +if _isstatic then +target.type=RANGE.TargetType.STATIC +else +target.type=RANGE.TargetType.UNIT +end +table.insert(self.bombingTargets,target) +return self +end +function RANGE:AddBombingTargetCoordinate(coord,name,goodhitrange) +local target={} +target.name=name or"Bomb Target" +target.target=nil +target.goodhitrange=goodhitrange or RANGE.Defaults.goodhitrange +target.move=false +target.speed=0 +target.coordinate=coord +target.type=RANGE.TargetType.COORD +table.insert(self.bombingTargets,target) +return self +end +function RANGE:AddBombingTargetGroup(group,goodhitrange,randommove) +self:F({group=group,goodhitrange=goodhitrange,randommove=randommove}) +if group then +local _units=group:GetUnits() +for _,_unit in pairs(_units)do +if _unit and _unit:IsAlive()then +self:AddBombingTargetUnit(_unit,goodhitrange,randommove) +end +end +end +return self +end +function RANGE:GetFoullineDistance(namepit,namefoulline) +self:F({namepit=namepit,namefoulline=namefoulline}) +local _staticpit=self:_CheckStatic(namepit) +local _staticfoul=self:_CheckStatic(namefoulline) +local pit=nil +if _staticpit==true then +pit=STATIC:FindByName(namepit,false) +elseif _staticpit==false then +pit=UNIT:FindByName(namepit) +else +self:E(self.id..string.format("ERROR! Pit object %s could not be found in GetFoullineDistance function. Check the name in the ME.",namepit)) +end +local foul=nil +if _staticfoul==true then +foul=STATIC:FindByName(namefoulline,false) +elseif _staticfoul==false then +foul=UNIT:FindByName(namefoulline) +else +self:E(self.id..string.format("ERROR! Foul line object %s could not be found in GetFoullineDistance function. Check the name in the ME.",namefoulline)) +end +local fouldist=0 +if pit~=nil and foul~=nil then +fouldist=pit:GetCoordinate():Get2DDistance(foul:GetCoordinate()) +else +self:E(self.id..string.format("ERROR! Foul line distance could not be determined. Check pit object name %s and foul line object name %s in the ME.",namepit,namefoulline)) +end +self:T(self.id..string.format("Foul line distance = %.1f m.",fouldist)) +return fouldist +end +function RANGE:onEvent(Event) +self:F3(Event) +if Event==nil or Event.initiator==nil then +self:T3("Skipping onEvent. Event or Event.initiator unknown.") +return true +end +if Unit.getByName(Event.initiator:getName())==nil then +self:T3("Skipping onEvent. Initiator unit name unknown.") +return true +end +local DCSiniunit=Event.initiator +local DCStgtunit=Event.target +local DCSweapon=Event.weapon +local EventData={} +local _playerunit=nil +local _playername=nil +if Event.initiator then +EventData.IniUnitName=Event.initiator:getName() +EventData.IniDCSGroup=Event.initiator:getGroup() +EventData.IniGroupName=Event.initiator:getGroup():getName() +_playerunit,_playername=self:_GetPlayerUnitAndName(EventData.IniUnitName) +end +if Event.target then +EventData.TgtUnitName=Event.target:getName() +EventData.TgtUnit=UNIT:FindByName(EventData.TgtUnitName) +end +if Event.weapon then +EventData.Weapon=Event.weapon +EventData.weapon=Event.weapon +EventData.WeaponTypeName=Event.weapon:getTypeName() +end +self:T3(self.id..string.format("EVENT: Event in onEvent with ID = %s",tostring(Event.id))) +self:T3(self.id..string.format("EVENT: Ini unit = %s",tostring(EventData.IniUnitName))) +self:T3(self.id..string.format("EVENT: Ini group = %s",tostring(EventData.IniGroupName))) +self:T3(self.id..string.format("EVENT: Ini player = %s",tostring(_playername))) +self:T3(self.id..string.format("EVENT: Tgt unit = %s",tostring(EventData.TgtUnitName))) +self:T3(self.id..string.format("EVENT: Wpn type = %s",tostring(EventData.WeaponTypeName))) +if Event.id==world.event.S_EVENT_BIRTH and _playername then +self:OnEventBirth(EventData) +end +if Event.id==world.event.S_EVENT_SHOT and _playername and Event.weapon then +self:OnEventShot(EventData) +end +if Event.id==world.event.S_EVENT_HIT and _playername and DCStgtunit then +self:OnEventHit(EventData) +end +end +function RANGE:OnEventBirth(EventData) +self:F({eventbirth=EventData}) +local _unitName=EventData.IniUnitName +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +self:T3(self.id.."BIRTH: unit = "..tostring(EventData.IniUnitName)) +self:T3(self.id.."BIRTH: group = "..tostring(EventData.IniGroupName)) +self:T3(self.id.."BIRTH: player = "..tostring(_playername)) +if _unit and _playername then +local _uid=_unit:GetID() +local _group=_unit:GetGroup() +local _gid=_group:GetID() +local _callsign=_unit:GetCallsign() +local text=string.format("Player %s, callsign %s entered unit %s (UID %d) of group %s (GID %d)",_playername,_callsign,_unitName,_uid,_group:GetName(),_gid) +self:T(self.id..text) +self.strafeStatus[_uid]=nil +self:ScheduleOnce(0.1,self._AddF10Commands,self,_unitName) +self.PlayerSettings[_playername]={} +self.PlayerSettings[_playername].smokebombimpact=self.defaultsmokebomb +self.PlayerSettings[_playername].flaredirecthits=false +self.PlayerSettings[_playername].smokecolor=SMOKECOLOR.Blue +self.PlayerSettings[_playername].flarecolor=FLARECOLOR.Red +self.PlayerSettings[_playername].delaysmoke=true +self.PlayerSettings[_playername].messages=true +self.PlayerSettings[_playername].client=CLIENT:FindByName(_unitName,nil,true) +self.PlayerSettings[_playername].unitname=_unitName +self.PlayerSettings[_playername].playername=_playername +self.PlayerSettings[_playername].airframe=EventData.IniUnit:GetTypeName() +self.PlayerSettings[_playername].inzone=false +if self.planes[_uid]~=true then +self.timerCheckZone=TIMER:New(self._CheckInZone,self,EventData.IniUnitName):Start(1,1) +self.planes[_uid]=true +end +end +end +function RANGE:OnEventHit(EventData) +self:F({eventhit=EventData}) +self:T3(self.id.."HIT: Ini unit = "..tostring(EventData.IniUnitName)) +self:T3(self.id.."HIT: Ini group = "..tostring(EventData.IniGroupName)) +self:T3(self.id.."HIT: Tgt target = "..tostring(EventData.TgtUnitName)) +local _unitName=EventData.IniUnitName +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +if _unit==nil or _playername==nil then +return +end +local _unitID=_unit:GetID() +local target=EventData.TgtUnit +local targetname=EventData.TgtUnitName +local _currentTarget=self.strafeStatus[_unitID] +if _currentTarget and target:IsAlive()then +local playerPos=_unit:GetCoordinate() +local targetPos=target:GetCoordinate() +for _,_target in pairs(_currentTarget.zone.targets)do +if _target and _target:IsAlive()and _target:GetName()==targetname then +local dist=playerPos:Get2DDistance(targetPos) +if dist>_currentTarget.zone.foulline then +_currentTarget.hits=_currentTarget.hits+1 +if _unit and _playername and self.PlayerSettings[_playername].flaredirecthits then +targetPos:Flare(self.PlayerSettings[_playername].flarecolor) +end +else +if _currentTarget.pastfoulline==false and _unit and _playername then +local _d=_currentTarget.zone.foulline +local text=string.format("%s, Invalid hit!\nYou already passed foul line distance of %d m for target %s.",self:_myname(_unitName),_d,targetname) +self:_DisplayMessageToGroup(_unit,text) +self:T2(self.id..text) +_currentTarget.pastfoulline=true +end +end +end +end +end +for _,_bombtarget in pairs(self.bombingTargets)do +local _target=_bombtarget.target +if _target and _target:IsAlive()and _bombtarget.name==targetname then +if _unit and _playername then +if self.PlayerSettings[_playername].flaredirecthits then +local targetPos=_target:GetCoordinate() +targetPos:Flare(self.PlayerSettings[_playername].flarecolor) +end +end +end +end +end +function RANGE:OnEventShot(EventData) +self:F({eventshot=EventData}) +if EventData.Weapon==nil then +return +end +if EventData.IniDCSUnit==nil then +return +end +local _weapon=EventData.Weapon:getTypeName() +local _weaponStrArray=UTILS.Split(_weapon,"%.") +local _weaponName=_weaponStrArray[#_weaponStrArray] +local desc=EventData.Weapon:getDesc() +local weaponcategory=desc.category +self:T(self.id.."EVENT SHOT: Range "..self.rangename) +self:T(self.id.."EVENT SHOT: Ini unit = "..EventData.IniUnitName) +self:T(self.id.."EVENT SHOT: Ini group = "..EventData.IniGroupName) +self:T(self.id.."EVENT SHOT: Weapon type = ".._weapon) +self:T(self.id.."EVENT SHOT: Weapon name = ".._weaponName) +self:T(self.id.."EVENT SHOT: Weapon cate = "..weaponcategory) +local _bombs=weaponcategory==Weapon.Category.BOMB +local _rockets=weaponcategory==Weapon.Category.ROCKET +local _missiles=weaponcategory==Weapon.Category.MISSILE +local _track=(_bombs and self.trackbombs)or(_rockets and self.trackrockets)or(_missiles and self.trackmissiles) +local _unitName=EventData.IniUnitName +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +local dPR=self.BombtrackThreshold*2 +if _unit and _playername then +dPR=_unit:GetCoordinate():Get2DDistance(self.location) +self:T(self.id..string.format("Range %s, player %s, player-range distance = %d km.",self.rangename,_playername,dPR/1000)) +end +if _track and dPR<=self.BombtrackThreshold and _unit and _playername then +local playerData=self.PlayerSettings[_playername] +self:T(self.id..string.format("RANGE %s: Tracking %s - %s.",self.rangename,_weapon,EventData.weapon:getName())) +local _lastBombPos={x=0,y=0,z=0} +local function trackBomb(_ordnance) +local _status,_bombPos=pcall( +function() +return _ordnance:getPoint() +end) +self:T2(self.id..string.format("Range %s: Bomb still in air: %s",self.rangename,tostring(_status))) +if _status then +_lastBombPos={x=_bombPos.x,y=_bombPos.y,z=_bombPos.z} +return timer.getTime()+self.dtBombtrack +else +local _closetTarget=nil +local _distance=nil +local _closeCoord=nil +local _hitquality="POOR" +local _callsign=self:_myname(_unitName) +local impactcoord=COORDINATE:NewFromVec3(_lastBombPos) +local insidezone=self.rangezone:IsCoordinateInZone(impactcoord) +if self.Debug then +impactcoord:MarkToAll("Bomb impact point") +end +if playerData.smokebombimpact and insidezone then +if playerData.delaysmoke then +timer.scheduleFunction(self._DelayedSmoke,{coord=impactcoord,color=playerData.smokecolor},timer.getTime()+self.TdelaySmoke) +else +impactcoord:Smoke(playerData.smokecolor) +end +end +for _,_bombtarget in pairs(self.bombingTargets)do +local targetcoord=self:_GetBombTargetCoordinate(_bombtarget) +if targetcoord then +local _temp=impactcoord:Get2DDistance(targetcoord) +if _distance==nil or _temp<_distance then +_distance=_temp +_closetTarget=_bombtarget +_closeCoord=targetcoord +if _distance<=0.5*_bombtarget.goodhitrange then +_hitquality="EXCELLENT" +elseif _distance<=_bombtarget.goodhitrange then +_hitquality="GOOD" +elseif _distance<=2*_bombtarget.goodhitrange then +_hitquality="INEFFECTIVE" +else +_hitquality="POOR" +end +end +end +end +if _distance and _distance<=self.scorebombdistance then +if not self.bombPlayerResults[_playername]then +self.bombPlayerResults[_playername]={} +end +local _results=self.bombPlayerResults[_playername] +local result={} +result.name=_closetTarget.name or"unknown" +result.distance=_distance +result.radial=_closeCoord:HeadingTo(impactcoord) +result.weapon=_weaponName or"unknown" +result.quality=_hitquality +result.player=playerData.playername +result.time=timer.getAbsTime() +result.airframe=playerData.airframe +table.insert(_results,result) +self:Impact(result,playerData) +elseif insidezone then +local _message=string.format("%s, weapon impacted too far from nearest range target (>%.1f km). No score!",_callsign,self.scorebombdistance/1000) +self:_DisplayMessageToGroup(_unit,_message,nil,false) +if self.rangecontrol then +self.rangecontrol:NewTransmission(RANGE.Sound.RCWeaponImpactedTooFar.filename,RANGE.Sound.RCWeaponImpactedTooFar.duration,self.soundpath,nil,nil,_message,self.subduration) +end +else +self:T(self.id.."Weapon impacted outside range zone.") +end +self:T(self.id..string.format("Range %s, player %s: Terminating bomb track timer.",self.rangename,_playername)) +return nil +end +end +self:T(self.id..string.format("Range %s, player %s: Tracking of weapon starts in 0.1 seconds.",self.rangename,_playername)) +timer.scheduleFunction(trackBomb,EventData.weapon,timer.getTime()+0.1) +end +end +function RANGE:onafterStatus(From,Event,To) +if self.verbose>0 then +local fsmstate=self:GetState() +local text=string.format("Range status: %s",fsmstate) +if self.instructor then +local alive="N/A" +if self.instructorrelayname then +local relay=UNIT:FindByName(self.instructorrelayname) +if relay then +alive=tostring(relay:IsAlive()) +end +end +text=text..string.format(", Instructor %.3f MHz (Relay=%s alive=%s)",self.instructorfreq,tostring(self.instructorrelayname),alive) +end +if self.rangecontrol then +local alive="N/A" +if self.rangecontrolrelayname then +local relay=UNIT:FindByName(self.rangecontrolrelayname) +if relay then +alive=tostring(relay:IsAlive()) +end +end +text=text..string.format(", Control %.3f MHz (Relay=%s alive=%s)",self.rangecontrolfreq,tostring(self.rangecontrolrelayname),alive) +end +self:I(self.id..text) +end +self:_CheckPlayers() +self:__Status(-10) +end +function RANGE:onafterEnterRange(From,Event,To,player) +if self.instructor and self.rangecontrol then +local RF=UTILS.Split(string.format("%.3f",self.rangecontrolfreq),".") +self.instructor:NewTransmission(RANGE.Sound.IREnterRange.filename,RANGE.Sound.IREnterRange.duration,self.soundpath) +self.instructor:Number2Transmission(RF[1]) +if tonumber(RF[2])>0 then +self.instructor:NewTransmission(RANGE.Sound.IRDecimal.filename,RANGE.Sound.IRDecimal.duration,self.soundpath) +self.instructor:Number2Transmission(RF[2]) +end +self.instructor:NewTransmission(RANGE.Sound.IRMegaHertz.filename,RANGE.Sound.IRMegaHertz.duration,self.soundpath) +end +end +function RANGE:onafterExitRange(From,Event,To,player) +if self.instructor then +self.instructor:NewTransmission(RANGE.Sound.IRExitRange.filename,RANGE.Sound.IRExitRange.duration,self.soundpath) +end +end +function RANGE:onafterImpact(From,Event,To,result,player) +local targetname=nil +if#self.bombingTargets>1 then +local targetname=result.name +end +local text=string.format("%s, impact %03d° for %d ft",player.playername,result.radial,UTILS.MetersToFeet(result.distance)) +if targetname then +text=text..string.format(" from bulls of target %s.") +else +text=text.."." +end +text=text..string.format(" %s hit.",result.quality) +if self.rangecontrol then +self.rangecontrol:NewTransmission(RANGE.Sound.RCImpact.filename,RANGE.Sound.RCImpact.duration,self.soundpath,nil,nil,text,self.subduration) +self.rangecontrol:Number2Transmission(string.format("%03d",result.radial),nil,0.1) +self.rangecontrol:NewTransmission(RANGE.Sound.RCDegrees.filename,RANGE.Sound.RCDegrees.duration,self.soundpath) +self.rangecontrol:NewTransmission(RANGE.Sound.RCFor.filename,RANGE.Sound.RCFor.duration,self.soundpath) +self.rangecontrol:Number2Transmission(string.format("%d",UTILS.MetersToFeet(result.distance))) +self.rangecontrol:NewTransmission(RANGE.Sound.RCFeet.filename,RANGE.Sound.RCFeet.duration,self.soundpath) +if result.quality=="POOR"then +self.rangecontrol:NewTransmission(RANGE.Sound.RCPoorHit.filename,RANGE.Sound.RCPoorHit.duration,self.soundpath,nil,0.5) +elseif result.quality=="INEFFECTIVE"then +self.rangecontrol:NewTransmission(RANGE.Sound.RCIneffectiveHit.filename,RANGE.Sound.RCIneffectiveHit.duration,self.soundpath,nil,0.5) +elseif result.quality=="GOOD"then +self.rangecontrol:NewTransmission(RANGE.Sound.RCGoodHit.filename,RANGE.Sound.RCGoodHit.duration,self.soundpath,nil,0.5) +elseif result.quality=="EXCELLENT"then +self.rangecontrol:NewTransmission(RANGE.Sound.RCExcellentHit.filename,RANGE.Sound.RCExcellentHit.duration,self.soundpath,nil,0.5) +end +end +local unit=UNIT:FindByName(player.unitname) +self:_DisplayMessageToGroup(unit,text,nil,true) +self:T(self.id..text) +if self.autosave then +self:Save() +end +end +function RANGE:onbeforeSave(From,Event,To) +if io and lfs then +return true +else +self:E(self.id..string.format("WARNING: io and/or lfs not desanitized. Cannot save player results.")) +return false +end +end +function RANGE:onafterSave(From,Event,To) +local function _savefile(filename,data) +local f=io.open(filename,"wb") +if f then +f:write(data) +f:close() +self:I(self.id..string.format("Saving player results to file %s",tostring(filename))) +else +self:E(self.id..string.format("ERROR: Could not save results to file %s",tostring(filename))) +end +end +local path=lfs.writedir()..[[Logs\]] +local filename=path..string.format("RANGE-%s_BombingResults.csv",self.rangename) +local scores="Name,Pass,Target,Distance,Radial,Quality,Weapon,Airframe,Mission Time" +for playername,results in pairs(self.bombPlayerResults)do +for i,_result in pairs(results)do +local result=_result +local distance=result.distance +local weapon=result.weapon +local target=result.name +local radial=result.radial +local quality=result.quality +local time=UTILS.SecondsToClock(result.time) +local airframe=result.airframe +local date="n/a" +if os then +date=os.date() +end +scores=scores..string.format("\n%s,%d,%s,%.2f,%03d,%s,%s,%s,%s,%s",playername,i,target,distance,radial,quality,weapon,airframe,time,date) +end +end +_savefile(filename,scores) +end +function RANGE:onbeforeLoad(From,Event,To) +if io and lfs then +return true +else +self:E(self.id..string.format("WARNING: io and/or lfs not desanitized. Cannot load player results.")) +return false +end +end +function RANGE:onafterLoad(From,Event,To) +local function _loadfile(filename) +local f=io.open(filename,"rb") +if f then +local data=f:read("*all") +f:close() +return data +else +self:E(self.id..string.format("WARNING: Could not load player results from file %s. File might not exist just yet.",tostring(filename))) +return nil +end +end +local path=lfs.writedir()..[[Logs\]] +local filename=path..string.format("RANGE-%s_BombingResults.csv",self.rangename) +local text=string.format("Loading player bomb results from file %s",filename) +self:I(self.id..text) +local data=_loadfile(filename) +if data then +local results=UTILS.Split(data,"\n") +table.remove(results,1) +self.bombPlayerResults={} +for _,_result in pairs(results)do +local resultdata=UTILS.Split(_result,",") +local result={} +local playername=resultdata[1] +result.player=playername +result.name=tostring(resultdata[3]) +result.distance=tonumber(resultdata[4]) +result.radial=tonumber(resultdata[5]) +result.quality=tostring(resultdata[6]) +result.weapon=tostring(resultdata[7]) +result.airframe=tostring(resultdata[8]) +result.time=UTILS.ClockToSeconds(resultdata[9]or"00:00:00") +result.date=resultdata[10]or"n/a" +self.bombPlayerResults[playername]=self.bombPlayerResults[playername]or{} +table.insert(self.bombPlayerResults[playername],result) +end +end +end +function RANGE._DelayedSmoke(_args) +trigger.action.smoke(_args.coord:GetVec3(),_args.color) +end +function RANGE:_DisplayMyStrafePitResults(_unitName) +self:F(_unitName) +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +if _unit and _playername then +local _message=string.format("My Top %d Strafe Pit Results:\n",self.ndisplayresult) +local _results=self.strafePlayerResults[_playername] +if _results==nil then +_message=string.format("%s: No Score yet.",_playername) +else +local _sort=function(a,b)return a.hits>b.hits end +table.sort(_results,_sort) +local _bestMsg="" +local _count=1 +for _,_result in pairs(_results)do +_message=_message..string.format("\n[%d] Hits %d - %s - %s",_count,_result.hits,_result.zone.name,_result.text) +if _bestMsg==""then +_bestMsg=string.format("Hits %d - %s - %s",_result.hits,_result.zone.name,_result.text) +end +if _count==self.ndisplayresult then +break +end +_count=_count+1 +end +_message=_message.."\n\nBEST: ".._bestMsg +end +self:_DisplayMessageToGroup(_unit,_message,nil,true,true) +end +end +function RANGE:_DisplayStrafePitResults(_unitName) +self:F(_unitName) +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +if _unit and _playername then +local _playerResults={} +local _message=string.format("Strafe Pit Results - Top %d Players:\n",self.ndisplayresult) +for _playerName,_results in pairs(self.strafePlayerResults)do +local _best=nil +for _,_result in pairs(_results)do +if _best==nil or _result.hits>_best.hits then +_best=_result +end +end +if _best~=nil then +local text=string.format("%s: Hits %i - %s - %s",_playerName,_best.hits,_best.zone.name,_best.text) +table.insert(_playerResults,{msg=text,hits=_best.hits}) +end +end +local _sort=function(a,b)return a.hits>b.hits end +table.sort(_playerResults,_sort) +for _i=1,math.min(#_playerResults,self.ndisplayresult)do +_message=_message..string.format("\n[%d] %s",_i,_playerResults[_i].msg) +end +if#_playerResults<1 then +_message=_message.."No player scored yet." +end +self:_DisplayMessageToGroup(_unit,_message,nil,true,true) +end +end +function RANGE:_DisplayMyBombingResults(_unitName) +self:F(_unitName) +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +if _unit and _playername then +local _message=string.format("My Top %d Bombing Results:\n",self.ndisplayresult) +local _results=self.bombPlayerResults[_playername] +if _results==nil then +_message=_playername..": No Score yet." +else +local _sort=function(a,b)return a.distance180 then +heading=heading-180 +else +heading=heading+180 +end +local mycoord=coord:ToStringA2G(_unit,_settings) +_text=_text..string.format("\n- %s: heading %03d°\n%s",_strafepit.name,heading,mycoord) +end +self:_DisplayMessageToGroup(_unit,_text,nil,true,true) +end +end +function RANGE:_DisplayRangeWeather(_unitname) +self:F(_unitname) +local unit,playername=self:_GetPlayerUnitAndName(_unitname) +if unit and playername then +local text="" +local coord=unit:GetCoordinate() +if self.location then +local position=self.location +local T=position:GetTemperature() +local P=position:GetPressure() +local Wd,Ws=position:GetWind() +local Bn,Bd=UTILS.BeaufortScale(Ws) +local WD=string.format('%03d°',Wd) +local Ts=string.format("%d°C",T) +local hPa2inHg=0.0295299830714 +local hPa2mmHg=0.7500615613030 +local settings=_DATABASE:GetPlayerSettings(playername)or _SETTINGS +local tT=string.format("%d°C",T) +local tW=string.format("%.1f m/s",Ws) +local tP=string.format("%.1f mmHg",P*hPa2mmHg) +if settings:IsImperial()then +tW=string.format("%.1f knots",UTILS.MpsToKnots(Ws)) +tP=string.format("%.2f inHg",P*hPa2inHg) +end +text=text..string.format("Weather Report at %s:\n",self.rangename) +text=text..string.format("--------------------------------------------------\n") +text=text..string.format("Temperature %s\n",tT) +text=text..string.format("Wind from %s at %s (%s)\n",WD,tW,Bd) +text=text..string.format("QFE %.1f hPa = %s",P,tP) +else +text=string.format("No range location defined for range %s.",self.rangename) +end +self:_DisplayMessageToGroup(unit,text,nil,true,true) +self:T2(self.id..text) +else +self:T(self.id..string.format("ERROR! Could not find player unit in RangeInfo! Name = %s",_unitname)) +end +end +function RANGE:_CheckPlayers() +for playername,_playersettings in pairs(self.PlayerSettings)do +local playersettings=_playersettings +local unitname=playersettings.unitname +local unit=UNIT:FindByName(unitname) +if unit and unit:IsAlive()then +if unit:IsInZone(self.rangezone)then +if not playersettings.inzone then +playersettings.inzone=true +self:EnterRange(playersettings) +end +else +if playersettings.inzone==true then +playersettings.inzone=false +self:ExitRange(playersettings) +end +end +end +end +end +function RANGE:_CheckInZone(_unitName) +self:F2(_unitName) +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +if _unit and _playername then +local function checkme(targetheading,_zone) +local zone=_zone +local unitheading=_unit:GetHeading() +local pitheading=targetheading-180 +local deltaheading=unitheading-pitheading +local towardspit=math.abs(deltaheading)<=90 or math.abs(deltaheading-360)<=90 +if towardspit then +local vec3=_unit:GetVec3() +local vec2={x=vec3.x,y=vec3.z} +local landheight=land.getHeight(vec2) +local unitalt=vec3.y-landheight +if unitalt<=self.strafemaxalt then +local unitinzone=zone:IsVec2InZone(vec2) +return unitinzone +end +end +return false +end +local _unitID=_unit:GetID() +local _currentStrafeRun=self.strafeStatus[_unitID] +if _currentStrafeRun then +local zone=_currentStrafeRun.zone.polygon +local unitinzone=checkme(_currentStrafeRun.zone.heading,zone) +if unitinzone then +_currentStrafeRun.time=_currentStrafeRun.time+1 +else +_currentStrafeRun.time=_currentStrafeRun.time+1 +if _currentStrafeRun.time<=3 then +self.strafeStatus[_unitID]=nil +local _msg=string.format("%s left strafing zone %s too quickly. No Score.",_playername,_currentStrafeRun.zone.name) +self:_DisplayMessageToGroup(_unit,_msg,nil,true) +if self.rangecontrol then +self.rangecontrol:NewTransmission(RANGE.Sound.RCLeftStrafePitTooQuickly.filename,RANGE.Sound.RCLeftStrafePitTooQuickly.duration,self.soundpath) +end +else +local _ammo=self:_GetAmmo(_unitName) +local _result=self.strafeStatus[_unitID] +local _sound=nil +if _result.hits>=_result.zone.goodPass*2 then +_result.text="EXCELLENT PASS" +_sound=RANGE.Sound.RCExcellentPass +elseif _result.hits>=_result.zone.goodPass then +_result.text="GOOD PASS" +_sound=RANGE.Sound.RCGoodPass +elseif _result.hits>=_result.zone.goodPass/2 then +_result.text="INEFFECTIVE PASS" +_sound=RANGE.Sound.RCIneffectivePass +else +_result.text="POOR PASS" +_sound=RANGE.Sound.RCPoorPass +end +local shots=_result.ammo-_ammo +local accur=0 +if shots>0 then +accur=_result.hits/shots*100 +end +local _text=string.format("%s, hits on target %s: %d",self:_myname(_unitName),_result.zone.name,_result.hits) +if shots and accur then +_text=_text..string.format("\nTotal rounds fired %d. Accuracy %.1f %%.",shots,accur) +end +_text=_text..string.format("\n%s",_result.text) +self:_DisplayMessageToGroup(_unit,_text) +if self.rangecontrol then +self.rangecontrol:NewTransmission(RANGE.Sound.RCHitsOnTarget.filename,RANGE.Sound.RCHitsOnTarget.duration,self.soundpath) +self.rangecontrol:Number2Transmission(string.format("%d",_result.hits)) +if shots and accur then +self.rangecontrol:NewTransmission(RANGE.Sound.RCTotalRoundsFired.filename,RANGE.Sound.RCTotalRoundsFired.duration,self.soundpath,nil,0.2) +self.rangecontrol:Number2Transmission(string.format("%d",shots),nil,0.2) +self.rangecontrol:NewTransmission(RANGE.Sound.RCAccuracy.filename,RANGE.Sound.RCAccuracy.duration,self.soundpath,nil,0.2) +self.rangecontrol:Number2Transmission(string.format("%d",UTILS.Round(accur,0))) +self.rangecontrol:NewTransmission(RANGE.Sound.RCPercent.filename,RANGE.Sound.RCPercent.duration,self.soundpath) +end +self.rangecontrol:NewTransmission(_sound.filename,_sound.duration,self.soundpath,nil,0.5) +end +self.strafeStatus[_unitID]=nil +local _stats=self.strafePlayerResults[_playername]or{} +table.insert(_stats,_result) +self.strafePlayerResults[_playername]=_stats +end +end +else +for _,_targetZone in pairs(self.strafeTargets)do +local zone=_targetZone.polygon +local unitinzone=checkme(_targetZone.heading,zone) +if unitinzone then +local _ammo=self:_GetAmmo(_unitName) +self.strafeStatus[_unitID]={hits=0,zone=_targetZone,time=1,ammo=_ammo,pastfoulline=false} +local _msg=string.format("%s, rolling in on strafe pit %s.",self:_myname(_unitName),_targetZone.name) +if self.rangecontrol then +self.rangecontrol:NewTransmission(RANGE.Sound.RCRollingInOnStrafeTarget.filename,RANGE.Sound.RCRollingInOnStrafeTarget.duration,self.soundpath) +end +self:_DisplayMessageToGroup(_unit,_msg,10,true) +break +end +end +end +end +end +function RANGE:_AddF10Commands(_unitName) +self:F(_unitName) +local _unit,playername=self:_GetPlayerUnitAndName(_unitName) +if _unit and playername then +local group=_unit:GetGroup() +local _gid=group:GetID() +if group and _gid then +if not self.MenuAddedTo[_gid]then +self.MenuAddedTo[_gid]=true +local _rangePath=nil +if RANGE.MenuF10Root then +_rangePath=missionCommands.addSubMenuForGroup(_gid,self.rangename,RANGE.MenuF10Root) +else +if RANGE.MenuF10[_gid]==nil then +RANGE.MenuF10[_gid]=missionCommands.addSubMenuForGroup(_gid,"On the Range") +end +_rangePath=missionCommands.addSubMenuForGroup(_gid,self.rangename,RANGE.MenuF10[_gid]) +end +local _statsPath=missionCommands.addSubMenuForGroup(_gid,"Statistics",_rangePath) +local _markPath=missionCommands.addSubMenuForGroup(_gid,"Mark Targets",_rangePath) +local _settingsPath=missionCommands.addSubMenuForGroup(_gid,"My Settings",_rangePath) +local _infoPath=missionCommands.addSubMenuForGroup(_gid,"Range Info",_rangePath) +local _mysmokePath=missionCommands.addSubMenuForGroup(_gid,"Smoke Color",_settingsPath) +local _myflarePath=missionCommands.addSubMenuForGroup(_gid,"Flare Color",_settingsPath) +missionCommands.addCommandForGroup(_gid,"Mark On Map",_markPath,self._MarkTargetsOnMap,self,_unitName) +missionCommands.addCommandForGroup(_gid,"Illuminate Range",_markPath,self._IlluminateBombTargets,self,_unitName) +missionCommands.addCommandForGroup(_gid,"Smoke Strafe Pits",_markPath,self._SmokeStrafeTargetBoxes,self,_unitName) +missionCommands.addCommandForGroup(_gid,"Smoke Strafe Tgts",_markPath,self._SmokeStrafeTargets,self,_unitName) +missionCommands.addCommandForGroup(_gid,"Smoke Bomb Tgts",_markPath,self._SmokeBombTargets,self,_unitName) +missionCommands.addCommandForGroup(_gid,"All Strafe Results",_statsPath,self._DisplayStrafePitResults,self,_unitName) +missionCommands.addCommandForGroup(_gid,"All Bombing Results",_statsPath,self._DisplayBombingResults,self,_unitName) +missionCommands.addCommandForGroup(_gid,"My Strafe Results",_statsPath,self._DisplayMyStrafePitResults,self,_unitName) +missionCommands.addCommandForGroup(_gid,"My Bomb Results",_statsPath,self._DisplayMyBombingResults,self,_unitName) +missionCommands.addCommandForGroup(_gid,"Reset All Stats",_statsPath,self._ResetRangeStats,self,_unitName) +missionCommands.addCommandForGroup(_gid,"Blue Smoke",_mysmokePath,self._playersmokecolor,self,_unitName,SMOKECOLOR.Blue) +missionCommands.addCommandForGroup(_gid,"Green Smoke",_mysmokePath,self._playersmokecolor,self,_unitName,SMOKECOLOR.Green) +missionCommands.addCommandForGroup(_gid,"Orange Smoke",_mysmokePath,self._playersmokecolor,self,_unitName,SMOKECOLOR.Orange) +missionCommands.addCommandForGroup(_gid,"Red Smoke",_mysmokePath,self._playersmokecolor,self,_unitName,SMOKECOLOR.Red) +missionCommands.addCommandForGroup(_gid,"White Smoke",_mysmokePath,self._playersmokecolor,self,_unitName,SMOKECOLOR.White) +missionCommands.addCommandForGroup(_gid,"Green Flares",_myflarePath,self._playerflarecolor,self,_unitName,FLARECOLOR.Green) +missionCommands.addCommandForGroup(_gid,"Red Flares",_myflarePath,self._playerflarecolor,self,_unitName,FLARECOLOR.Red) +missionCommands.addCommandForGroup(_gid,"White Flares",_myflarePath,self._playerflarecolor,self,_unitName,FLARECOLOR.White) +missionCommands.addCommandForGroup(_gid,"Yellow Flares",_myflarePath,self._playerflarecolor,self,_unitName,FLARECOLOR.Yellow) +missionCommands.addCommandForGroup(_gid,"Smoke Delay On/Off",_settingsPath,self._SmokeBombDelayOnOff,self,_unitName) +missionCommands.addCommandForGroup(_gid,"Smoke Impact On/Off",_settingsPath,self._SmokeBombImpactOnOff,self,_unitName) +missionCommands.addCommandForGroup(_gid,"Flare Hits On/Off",_settingsPath,self._FlareDirectHitsOnOff,self,_unitName) +missionCommands.addCommandForGroup(_gid,"All Messages On/Off",_settingsPath,self._MessagesToPlayerOnOff,self,_unitName) +missionCommands.addCommandForGroup(_gid,"General Info",_infoPath,self._DisplayRangeInfo,self,_unitName) +missionCommands.addCommandForGroup(_gid,"Weather Report",_infoPath,self._DisplayRangeWeather,self,_unitName) +missionCommands.addCommandForGroup(_gid,"Bombing Targets",_infoPath,self._DisplayBombTargets,self,_unitName) +missionCommands.addCommandForGroup(_gid,"Strafe Pits",_infoPath,self._DisplayStrafePits,self,_unitName) +end +else +self:E(self.id.."Could not find group or group ID in AddF10Menu() function. Unit name: ".._unitName) +end +else +self:E(self.id.."Player unit does not exist in AddF10Menu() function. Unit name: ".._unitName) +end +end +function RANGE:_GetBombTargetCoordinate(target) +local coord=nil +if target.type==RANGE.TargetType.UNIT then +if not target.move then +coord=target.coordinate +else +if target.target and target.target:IsAlive()then +coord=target.target:GetCoordinate() +end +end +elseif target.type==RANGE.TargetType.STATIC then +coord=target.coordinate +elseif target.type==RANGE.TargetType.COORD then +coord=target.coordinate +else +self:E(self.id.."ERROR: Unknown target type.") +end +return coord +end +function RANGE:_GetAmmo(unitname) +self:F2(unitname) +local ammo=0 +local unit,playername=self:_GetPlayerUnitAndName(unitname) +if unit and playername then +local has_ammo=false +local ammotable=unit:GetAmmo() +self:T2({ammotable=ammotable}) +if ammotable~=nil then +local weapons=#ammotable +self:T2(self.id..string.format("Number of weapons %d.",weapons)) +for w=1,weapons do +local Nammo=ammotable[w]["count"] +local Tammo=ammotable[w]["desc"]["typeName"] +if string.match(Tammo,"shell")then +ammo=ammo+Nammo +local text=string.format("Player %s has %d rounds ammo of type %s",playername,Nammo,Tammo) +self:T(self.id..text) +else +local text=string.format("Player %s has %d ammo of type %s",playername,Nammo,Tammo) +self:T(self.id..text) +end +end +end +end +return ammo +end +function RANGE:_MarkTargetsOnMap(_unitName) +self:F(_unitName) +local group=nil +if _unitName then +group=UNIT:FindByName(_unitName):GetGroup() +end +for _,_bombtarget in pairs(self.bombingTargets)do +local bombtarget=_bombtarget +local coord=self:_GetBombTargetCoordinate(_bombtarget) +if group then +coord:MarkToGroup(string.format("Bomb target %s:\n%s\n%s",bombtarget.name,coord:ToStringLLDMS(),coord:ToStringBULLS(group:GetCoalition())),group) +else +coord:MarkToAll(string.format("Bomb target %s",bombtarget.name)) +end +end +for _,_strafepit in pairs(self.strafeTargets)do +for _,_target in pairs(_strafepit.targets)do +local _target=_target +if _target and _target:IsAlive()then +local coord=_target:GetCoordinate() +if group then +coord:MarkToGroup(string.format("Strafe target %s:\n%s\n%s",_target:GetName(),coord:ToStringLLDMS(),coord:ToStringBULLS(group:GetCoalition())),group) +else +coord:MarkToAll("Strafe target ".._target:GetName()) +end +end +end +end +if _unitName then +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +local text=string.format("%s, %s, range targets are now marked on F10 map.",self.rangename,_playername) +self:_DisplayMessageToGroup(_unit,text,5) +end +end +function RANGE:_IlluminateBombTargets(_unitName) +self:F(_unitName) +local bomb={} +for _,_bombtarget in pairs(self.bombingTargets)do +local _target=_bombtarget.target +local coord=self:_GetBombTargetCoordinate(_bombtarget) +if coord then +table.insert(bomb,coord) +end +end +if#bomb>0 then +local coord=bomb[math.random(#bomb)] +local c=COORDINATE:New(coord.x,coord.y+math.random(self.illuminationminalt,self.illuminationmaxalt),coord.z) +c:IlluminationBomb() +end +local strafe={} +for _,_strafepit in pairs(self.strafeTargets)do +for _,_target in pairs(_strafepit.targets)do +local _target=_target +if _target and _target:IsAlive()then +local coord=_target:GetCoordinate() +table.insert(strafe,coord) +end +end +end +if#strafe>0 then +local coord=strafe[math.random(#strafe)] +local c=COORDINATE:New(coord.x,coord.y+math.random(self.illuminationminalt,self.illuminationmaxalt),coord.z) +c:IlluminationBomb() +end +if _unitName then +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +local text=string.format("%s, %s, range targets are illuminated.",self.rangename,_playername) +self:_DisplayMessageToGroup(_unit,text,5) +end +end +function RANGE:_ResetRangeStats(_unitName) +self:F(_unitName) +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +if _unit and _playername then +self.strafePlayerResults[_playername]=nil +self.bombPlayerResults[_playername]=nil +local text=string.format("%s, %s, your range stats were cleared.",self.rangename,_playername) +self:DisplayMessageToGroup(_unit,text,5,false,true) +end +end +function RANGE:_DisplayMessageToGroup(_unit,_text,_time,_clear,display) +self:F({unit=_unit,text=_text,time=_time,clear=_clear}) +_time=_time or self.Tmsg +if _clear==nil or _clear==false then +_clear=false +else +_clear=true +end +if self.messages==false then +return +end +if _unit and _unit:IsAlive()then +local _gid=_unit:GetGroup():GetID() +local _,playername=self:_GetPlayerUnitAndName(_unit:GetName()) +local playermessage=self.PlayerSettings[playername].messages +if _gid and(playermessage==true or display)and(not self.examinerexclusive)then +trigger.action.outTextForGroup(_gid,_text,_time,_clear) +end +if self.examinergroupname~=nil then +local _examinerid=GROUP:FindByName(self.examinergroupname):GetID() +if _examinerid then +trigger.action.outTextForGroup(_examinerid,_text,_time,_clear) +end +end +end +end +function RANGE:_SmokeBombImpactOnOff(unitname) +self:F(unitname) +local unit,playername=self:_GetPlayerUnitAndName(unitname) +if unit and playername then +local text +if self.PlayerSettings[playername].smokebombimpact==true then +self.PlayerSettings[playername].smokebombimpact=false +text=string.format("%s, %s, smoking impact points of bombs is now OFF.",self.rangename,playername) +else +self.PlayerSettigs[playername].smokebombimpact=true +text=string.format("%s, %s, smoking impact points of bombs is now ON.",self.rangename,playername) +end +self:_DisplayMessageToGroup(unit,text,5,false,true) +end +end +function RANGE:_SmokeBombDelayOnOff(unitname) +self:F(unitname) +local unit,playername=self:_GetPlayerUnitAndName(unitname) +if unit and playername then +local text +if self.PlayerSettings[playername].delaysmoke==true then +self.PlayerSettings[playername].delaysmoke=false +text=string.format("%s, %s, delayed smoke of bombs is now OFF.",self.rangename,playername) +else +self.PlayerSettigs[playername].delaysmoke=true +text=string.format("%s, %s, delayed smoke of bombs is now ON.",self.rangename,playername) +end +self:_DisplayMessageToGroup(unit,text,5,false,true) +end +end +function RANGE:_MessagesToPlayerOnOff(unitname) +self:F(unitname) +local unit,playername=self:_GetPlayerUnitAndName(unitname) +if unit and playername then +local text +if self.PlayerSettings[playername].messages==true then +text=string.format("%s, %s, display of ALL messages is now OFF.",self.rangename,playername) +else +text=string.format("%s, %s, display of ALL messages is now ON.",self.rangename,playername) +end +self:_DisplayMessageToGroup(unit,text,5,false,true) +self.PlayerSettings[playername].messages=not self.PlayerSettings[playername].messages +end +end +function RANGE:_FlareDirectHitsOnOff(unitname) +self:F(unitname) +local unit,playername=self:_GetPlayerUnitAndName(unitname) +if unit and playername then +local text +if self.PlayerSettings[playername].flaredirecthits==true then +self.PlayerSettings[playername].flaredirecthits=false +text=string.format("%s, %s, flaring direct hits is now OFF.",self.rangename,playername) +else +self.PlayerSettings[playername].flaredirecthits=true +text=string.format("%s, %s, flaring direct hits is now ON.",self.rangename,playername) +end +self:_DisplayMessageToGroup(unit,text,5,false,true) +end +end +function RANGE:_SmokeBombTargets(unitname) +self:F(unitname) +for _,_bombtarget in pairs(self.bombingTargets)do +local _target=_bombtarget.target +local coord=self:_GetBombTargetCoordinate(_bombtarget) +if coord then +coord:Smoke(self.BombSmokeColor) +end +end +if unitname then +local unit,playername=self:_GetPlayerUnitAndName(unitname) +local text=string.format("%s, %s, bombing targets are now marked with %s smoke.",self.rangename,playername,self:_smokecolor2text(self.BombSmokeColor)) +self:_DisplayMessageToGroup(unit,text,5) +end +end +function RANGE:_SmokeStrafeTargets(unitname) +self:F(unitname) +for _,_target in pairs(self.strafeTargets)do +_target.coordinate:Smoke(self.StrafeSmokeColor) +end +if unitname then +local unit,playername=self:_GetPlayerUnitAndName(unitname) +local text=string.format("%s, %s, strafing tragets are now marked with %s smoke.",self.rangename,playername,self:_smokecolor2text(self.StrafeSmokeColor)) +self:_DisplayMessageToGroup(unit,text,5) +end +end +function RANGE:_SmokeStrafeTargetBoxes(unitname) +self:F(unitname) +for _,_target in pairs(self.strafeTargets)do +local zone=_target.polygon +zone:SmokeZone(self.StrafePitSmokeColor,4) +for _,_point in pairs(_target.smokepoints)do +_point:SmokeOrange() +end +end +if unitname then +local unit,playername=self:_GetPlayerUnitAndName(unitname) +local text=string.format("%s, %s, strafing pit approach boxes are now marked with %s smoke.",self.rangename,playername,self:_smokecolor2text(self.StrafePitSmokeColor)) +self:_DisplayMessageToGroup(unit,text,5) +end +end +function RANGE:_playersmokecolor(_unitName,color) +self:F({unitname=_unitName,color=color}) +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +if _unit and _playername then +self.PlayerSettings[_playername].smokecolor=color +local text=string.format("%s, %s, your bomb impacts are now smoked in %s.",self.rangename,_playername,self:_smokecolor2text(color)) +self:_DisplayMessageToGroup(_unit,text,5) +end +end +function RANGE:_playerflarecolor(_unitName,color) +self:F({unitname=_unitName,color=color}) +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +if _unit and _playername then +self.PlayerSettings[_playername].flarecolor=color +local text=string.format("%s, %s, your direct hits are now flared in %s.",self.rangename,_playername,self:_flarecolor2text(color)) +self:_DisplayMessageToGroup(_unit,text,5) +end +end +function RANGE:_smokecolor2text(color) +self:F(color) +local txt="" +if color==SMOKECOLOR.Blue then +txt="blue" +elseif color==SMOKECOLOR.Green then +txt="green" +elseif color==SMOKECOLOR.Orange then +txt="orange" +elseif color==SMOKECOLOR.Red then +txt="red" +elseif color==SMOKECOLOR.White then +txt="white" +else +txt=string.format("unknown color (%s)",tostring(color)) +end +return txt +end +function RANGE:_flarecolor2text(color) +self:F(color) +local txt="" +if color==FLARECOLOR.Green then +txt="green" +elseif color==FLARECOLOR.Red then +txt="red" +elseif color==FLARECOLOR.White then +txt="white" +elseif color==FLARECOLOR.Yellow then +txt="yellow" +else +txt=string.format("unknown color (%s)",tostring(color)) +end +return txt +end +function RANGE:_CheckStatic(name) +self:F2(name) +local _DCSstatic=StaticObject.getByName(name) +if _DCSstatic and _DCSstatic:isExist()then +local _MOOSEstatic=STATIC:FindByName(name,false) +if not _MOOSEstatic then +self:T(self.id..string.format("Adding DCS static to MOOSE database. Name = %s.",name)) +_DATABASE:AddStatic(name) +end +return true +else +self:T3(self.id..string.format("No static object with name %s exists.",name)) +end +if UNIT:FindByName(name)then +return false +else +self:T3(self.id..string.format("No unit object with name %s exists.",name)) +end +return nil +end +function RANGE:_GetSpeed(controllable) +self:F2(controllable) +local desc=controllable:GetDesc() +local speed=0 +if desc then +speed=desc.speedMax*3.6 +self:T({speed=speed}) +end +return speed +end +function RANGE:_GetPlayerUnitAndName(_unitName) +self:F2(_unitName) +if _unitName~=nil then +local DCSunit=Unit.getByName(_unitName) +if DCSunit then +local playername=DCSunit:getPlayerName() +local unit=UNIT:Find(DCSunit) +self:T2({DCSunit=DCSunit,unit=unit,playername=playername}) +if DCSunit and unit and playername then +return unit,playername +end +end +end +return nil,nil +end +function RANGE:_myname(unitname) +self:F2(unitname) +local unit=UNIT:FindByName(unitname) +local pname=unit:GetPlayerName() +local csign=unit:GetCallsign() +return string.format("%s",pname) +end +do +ZONE_GOAL={ +ClassName="ZONE_GOAL", +Goal=nil, +SmokeTime=nil, +SmokeScheduler=nil, +SmokeColor=nil, +SmokeZone=nil, +} +function ZONE_GOAL:New(Zone) +local self=BASE:Inherit(self,ZONE_RADIUS:New(Zone:GetName(),Zone:GetVec2(),Zone:GetRadius())) +self:F({Zone=Zone}) +self.Goal=GOAL:New() +self.SmokeTime=nil +self:SetSmokeZone(true) +self:AddTransition("*","DestroyedUnit","*") +return self +end +function ZONE_GOAL:GetZone() +return self +end +function ZONE_GOAL:GetZoneName() +return self:GetName() +end +function ZONE_GOAL:SetSmokeZone(switch) +self.SmokeZone=switch +return self +end +function ZONE_GOAL:Smoke(SmokeColor) +self:F({SmokeColor=SmokeColor}) +self.SmokeColor=SmokeColor +end +function ZONE_GOAL:Flare(FlareColor) +self:FlareZone(FlareColor,30) +end +function ZONE_GOAL:onafterGuard() +self:F("Guard") +if self.SmokeZone and not self.SmokeScheduler then +self.SmokeScheduler=self:ScheduleRepeat(1,1,0.1,nil,self.StatusSmoke,self) +end +end +function ZONE_GOAL:StatusSmoke() +self:F({self.SmokeTime,self.SmokeColor}) +if self.SmokeZone then +local CurrentTime=timer.getTime() +if self.SmokeTime==nil or self.SmokeTime+300<=CurrentTime then +if self.SmokeColor then +self:GetCoordinate():Smoke(self.SmokeColor) +self.SmokeTime=CurrentTime +end +end +end +end +function ZONE_GOAL:__Destroyed(EventData) +self:F({"EventDead",EventData}) +self:F({EventData.IniUnit}) +if EventData.IniDCSUnit then +local Vec3=EventData.IniDCSUnit:getPosition().p +self:F({Vec3=Vec3}) +if Vec3 and self:IsVec3InZone(Vec3)then +local PlayerHits=_DATABASE.HITS[EventData.IniUnitName] +if PlayerHits then +for PlayerName,PlayerHit in pairs(PlayerHits.Players or{})do +self.Goal:AddPlayerContribution(PlayerName) +self:DestroyedUnit(EventData.IniUnitName,PlayerName) +end +end +end +end +end +function ZONE_GOAL:MonitorDestroyedUnits() +self:HandleEvent(EVENTS.Dead,self.__Destroyed) +self:HandleEvent(EVENTS.Crash,self.__Destroyed) +end +end +do +ZONE_GOAL_COALITION={ +ClassName="ZONE_GOAL_COALITION", +Coalition=nil, +PreviousCoaliton=nil, +UnitCategories=nil, +ObjectCategories=nil, +} +ZONE_GOAL_COALITION.States={} +function ZONE_GOAL_COALITION:New(Zone,Coalition,UnitCategories) +if not Zone then +BASE:E("ERROR: No Zone specified in ZONE_GOAL_COALITON!") +return nil +end +local self=BASE:Inherit(self,ZONE_GOAL:New(Zone)) +self:F({Zone=Zone,Coalition=Coalition}) +self:SetCoalition(Coalition or coalition.side.NEUTRAL) +self:SetUnitCategories(UnitCategories) +self:SetObjectCategories() +return self +end +function ZONE_GOAL_COALITION:SetCoalition(Coalition) +self.PreviousCoalition=self.Coalition or Coalition +self.Coalition=Coalition +return self +end +function ZONE_GOAL_COALITION:SetUnitCategories(UnitCategories) +if UnitCategories and type(UnitCategories)~="table"then +UnitCategories={UnitCategories} +end +self.UnitCategories=UnitCategories or{Unit.Category.GROUND_UNIT} +return self +end +function ZONE_GOAL_COALITION:SetObjectCategories(ObjectCategories) +if ObjectCategories and type(ObjectCategories)~="table"then +ObjectCategories={ObjectCategories} +end +self.ObjectCategories=ObjectCategories or{Object.Category.UNIT,Object.Category.STATIC} +return self +end +function ZONE_GOAL_COALITION:GetCoalition() +return self.Coalition +end +function ZONE_GOAL_COALITION:GetPreviousCoalition() +return self.PreviousCoalition +end +function ZONE_GOAL_COALITION:GetCoalitionName() +return UTILS.GetCoalitionName(self.Coalition) +end +function ZONE_GOAL_COALITION:StatusZone() +local State=self:GetState() +local text=string.format("Zone state=%s, Owner=%s, Scanning...",State,self:GetCoalitionName()) +self:F(text) +self:Scan(self.ObjectCategories,self.UnitCategories) +return self +end +end +do +ZONE_CAPTURE_COALITION={ +ClassName="ZONE_CAPTURE_COALITION", +MarkBlue=nil, +MarkRed=nil, +StartInterval=nil, +RepeatInterval=nil, +HitsOn=nil, +HitTimeLast=nil, +HitTimeAttackOver=nil, +MarkOn=nil, +} +function ZONE_CAPTURE_COALITION:New(Zone,Coalition,UnitCategories,ObjectCategories) +local self=BASE:Inherit(self,ZONE_GOAL_COALITION:New(Zone,Coalition,UnitCategories)) +self:F({Zone=Zone,Coalition=Coalition,UnitCategories=UnitCategories,ObjectCategories=ObjectCategories}) +self:SetObjectCategories(ObjectCategories) +self:SetSmokeZone(false) +self:SetMarkZone(true) +self:SetStartState("Empty") +do +end +do +end +do +end +do +end +self:AddTransition("*","Guard","Guarded") +self:AddTransition("*","Empty","Empty") +self:AddTransition({"Guarded","Empty"},"Attack","Attacked") +self:AddTransition({"Guarded","Attacked","Empty"},"Capture","Captured") +_EVENTDISPATCHER:CreateEventNewZoneGoal(self) +return self +end +function ZONE_CAPTURE_COALITION:Start(StartInterval,RepeatInterval) +self.StartInterval=StartInterval or 1 +self.RepeatInterval=RepeatInterval or 15 +if self.ScheduleStatusZone then +self:ScheduleStop(self.ScheduleStatusZone) +end +self.ScheduleStatusZone=self:ScheduleRepeat(self.StartInterval,self.RepeatInterval,0.1,nil,self.StatusZone,self) +self:HandleEvent(EVENTS.Hit,self.OnEventHit) +self:Mark() +return self +end +function ZONE_CAPTURE_COALITION:Stop() +if self.ScheduleStatusZone then +self:ScheduleStop(self.ScheduleStatusZone) +end +if self.SmokeScheduler then +self:ScheduleStop(self.SmokeScheduler) +end +self:UnHandleEvent(EVENTS.Hit) +end +function ZONE_CAPTURE_COALITION:SetMonitorHits(Switch,TimeAttackOver) +self.HitsOn=Switch +self.HitTimeAttackOver=TimeAttackOver or 5*60 +return self +end +function ZONE_CAPTURE_COALITION:SetMarkZone(Switch) +if Switch==nil or Switch==true then +self.MarkOn=true +else +self.MarkOn=false +end +return self +end +function ZONE_CAPTURE_COALITION:OnEventHit(EventData) +if self.HitsOn then +local UnitHit=EventData.TgtUnit +if UnitHit and UnitHit:IsInZone(self)and UnitHit:GetCoalition()==self.Coalition then +self.HitTimeLast=timer.getTime() +if self:GetState()~="Attacked"then +self:F2("Hit ==> Attack") +self:Attack() +end +end +end +end +function ZONE_CAPTURE_COALITION:onafterGuard() +self:F2("After Guard") +if self.SmokeZone and not self.SmokeScheduler then +self.SmokeScheduler=self:ScheduleRepeat(self.StartInterval,self.RepeatInterval,0.1,nil,self.StatusSmoke,self) +end +end +function ZONE_CAPTURE_COALITION:onenterGuarded() +self:F2("Enter Guarded") +self:Mark() +end +function ZONE_CAPTURE_COALITION:onenterCaptured() +self:F2("Enter Captured") +local NewCoalition=self:GetScannedCoalition() +self:F({NewCoalition=NewCoalition}) +self:SetCoalition(NewCoalition) +self:Mark() +self.Goal:Achieved() +end +function ZONE_CAPTURE_COALITION:onenterEmpty() +self:F2("Enter Empty") +self:Mark() +end +function ZONE_CAPTURE_COALITION:onenterAttacked() +self:F2("Enter Attacked") +self:Mark() +end +function ZONE_CAPTURE_COALITION:IsEmpty() +local IsEmpty=self:IsNoneInZone() +self:F({IsEmpty=IsEmpty}) +return IsEmpty +end +function ZONE_CAPTURE_COALITION:IsGuarded() +local IsGuarded=self:IsAllInZoneOfCoalition(self.Coalition) +self:F({IsGuarded=IsGuarded}) +return IsGuarded +end +function ZONE_CAPTURE_COALITION:IsCaptured() +local IsCaptured=self:IsAllInZoneOfOtherCoalition(self.Coalition) +self:F({IsCaptured=IsCaptured}) +return IsCaptured +end +function ZONE_CAPTURE_COALITION:IsAttacked() +local IsAttacked=self:IsSomeInZoneOfCoalition(self.Coalition) +self:F({IsAttacked=IsAttacked}) +return IsAttacked +end +function ZONE_CAPTURE_COALITION:StatusZone() +local State=self:GetState() +self:GetParent(self,ZONE_CAPTURE_COALITION).StatusZone(self) +local Tnow=timer.getTime() +if State~="Guarded"and self:IsGuarded()then +if self.HitTimeLast==nil or Tnow>=self.HitTimeLast+self.HitTimeAttackOver then +self:Guard() +self.HitTimeLast=nil +end +end +if State~="Empty"and self:IsEmpty()then +self:Empty() +end +if State~="Attacked"and self:IsAttacked()then +self:Attack() +end +if State~="Captured"and self:IsCaptured()then +self:Capture() +end +local unitset=self:GetScannedSetUnit() +local nRed=0 +local nBlue=0 +for _,object in pairs(unitset:GetSet())do +local coal=object:GetCoalition() +if object:IsAlive()then +if coal==coalition.side.RED then +nRed=nRed+1 +elseif coal==coalition.side.BLUE then +nBlue=nBlue+1 +end +end +end +if false then +local text=string.format("CAPTURE ZONE %s: Owner=%s (Previous=%s): #blue=%d, #red=%d, Status %s",self:GetZoneName(),self:GetCoalitionName(),UTILS.GetCoalitionName(self:GetPreviousCoalition()),nBlue,nRed,State) +local NewState=self:GetState() +if NewState~=State then +text=text..string.format(" --> %s",NewState) +end +self:I(text) +end +end +function ZONE_CAPTURE_COALITION:Mark() +if self.MarkOn then +local Coord=self:GetCoordinate() +local ZoneName=self:GetZoneName() +local State=self:GetState() +if self.MarkRed then +Coord:RemoveMark(self.MarkRed) +end +if self.MarkBlue then +Coord:RemoveMark(self.MarkBlue) +end +if self.Coalition==coalition.side.BLUE then +self.MarkBlue=Coord:MarkToCoalitionBlue("Coalition: Blue\nGuard Zone: "..ZoneName.."\nStatus: "..State) +self.MarkRed=Coord:MarkToCoalitionRed("Coalition: Blue\nCapture Zone: "..ZoneName.."\nStatus: "..State) +elseif self.Coalition==coalition.side.RED then +self.MarkRed=Coord:MarkToCoalitionRed("Coalition: Red\nGuard Zone: "..ZoneName.."\nStatus: "..State) +self.MarkBlue=Coord:MarkToCoalitionBlue("Coalition: Red\nCapture Zone: "..ZoneName.."\nStatus: "..State) +else +self.MarkRed=Coord:MarkToCoalitionRed("Coalition: Neutral\nCapture Zone: "..ZoneName.."\nStatus: "..State) +self.MarkBlue=Coord:MarkToCoalitionBlue("Coalition: Neutral\nCapture Zone: "..ZoneName.."\nStatus: "..State) +end +end +end +end +ARTY={ +ClassName="ARTY", +lid=nil, +Debug=false, +targets={}, +moves={}, +currentTarget=nil, +currentMove=nil, +Nammo0=0, +Nshells0=0, +Nrockets0=0, +Nmissiles0=0, +Nukes0=0, +Nillu0=0, +Nsmoke0=0, +StatusInterval=10, +WaitForShotTime=300, +DCSdesc=nil, +Type=nil, +DisplayName=nil, +groupname=nil, +alias=nil, +clusters={}, +ismobile=true, +iscargo=false, +cargogroup=nil, +IniGroupStrength=0, +IsArtillery=nil, +RearmingDistance=100, +RearmingGroup=nil, +RearmingGroupSpeed=nil, +RearmingGroupOnRoad=false, +RearmingGroupCoord=nil, +RearmingPlaceCoord=nil, +RearmingArtyOnRoad=false, +InitialCoord=nil, +report=true, +ammoshells={}, +ammorockets={}, +ammomissiles={}, +Nshots=0, +minrange=300, +maxrange=1000000, +nukewarhead=75000, +Nukes=nil, +nukefire=false, +nukefires=nil, +nukerange=nil, +Nillu=nil, +illuPower=1000000, +illuMinalt=500, +illuMaxalt=1000, +Nsmoke=nil, +smokeColor=SMOKECOLOR.Red, +relocateafterfire=false, +relocateRmin=300, +relocateRmax=800, +markallow=false, +markkey=nil, +markreadonly=false, +autorelocate=false, +autorelocatemaxdist=50000, +autorelocateonroad=false, +coalition=nil, +respawnafterdeath=false, +respawndelay=nil +} +ARTY.WeaponType={ +Auto=1073741822, +Cannon=805306368, +Rockets=30720, +CruiseMissile=2097152, +TacticalNukes=666, +IlluminationShells=667, +SmokeShells=668, +} +ARTY.db={ +["2B11 mortar"]={ +minrange=500, +maxrange=7000, +reloadtime=30, +}, +["SPH 2S1 Gvozdika"]={ +minrange=300, +maxrange=15000, +reloadtime=nil, +}, +["SPH 2S19 Msta"]={ +minrange=300, +maxrange=23500, +reloadtime=nil, +}, +["SPH 2S3 Akatsia"]={ +minrange=300, +maxrange=17000, +reloadtime=nil, +}, +["SPH 2S9 Nona"]={ +minrange=500, +maxrange=7000, +reloadtime=nil, +}, +["SPH M109 Paladin"]={ +minrange=300, +maxrange=22000, +reloadtime=nil, +}, +["SpGH Dana"]={ +minrange=300, +maxrange=18700, +reloadtime=nil, +}, +["MLRS BM-21 Grad"]={ +minrange=5000, +maxrange=19000, +reloadtime=420, +}, +["MLRS 9K57 Uragan BM-27"]={ +minrange=11500, +maxrange=35800, +reloadtime=840, +}, +["MLRS 9A52 Smerch"]={ +minrange=20000, +maxrange=70000, +reloadtime=2160, +}, +["MLRS M270"]={ +minrange=10000, +maxrange=32000, +reloadtime=540, +}, +} +ARTY.version="1.2.0" +function ARTY:New(group,alias) +local self=BASE:Inherit(self,FSM_CONTROLLABLE:New()) +if type(group)=="string"then +self.groupname=group +group=GROUP:FindByName(group) +if not group then +self:E(string.format("ERROR: Requested ARTY group %s does not exist! (Has to be a MOOSE group.)",self.groupname)) +return nil +end +end +if group then +self:T(string.format("ARTY script version %s. Added group %s.",ARTY.version,group:GetName())) +else +self:E("ERROR: Requested ARTY group does not exist! (Has to be a MOOSE group.)") +return nil +end +if not(group:IsGround()or group:IsShip())then +self:E(string.format("ERROR: ARTY group %s has to be a GROUND or SHIP group!",group:GetName())) +return nil +end +self:SetControllable(group) +self.groupname=group:GetName() +self.coalition=group:GetCoalition() +if alias~=nil then +self.alias=tostring(alias) +else +self.alias=self.groupname +end +self.lid=string.format("ARTY %s | ",self.alias) +self.InitialCoord=group:GetCoordinate() +local DCSgroup=Group.getByName(group:GetName()) +local DCSunit=DCSgroup:getUnit(1) +self.DCSdesc=DCSunit:getDesc() +self:T3(self.lid.."DCS descriptors for group "..group:GetName()) +for id,desc in pairs(self.DCSdesc)do +self:T3({id=id,desc=desc}) +end +self.SpeedMax=group:GetSpeedMax() +if self.SpeedMax>1 then +self.ismobile=true +else +self.ismobile=false +end +self.Speed=self.SpeedMax*0.7 +self.DisplayName=self.DCSdesc.displayName +self.IsArtillery=DCSunit:hasAttribute("Artillery") +self.Type=group:GetTypeName() +self.IniGroupStrength=#group:GetUnits() +self:AddTransition("*","Start","CombatReady") +self:AddTransition("CombatReady","OpenFire","Firing") +self:AddTransition("Firing","CeaseFire","CombatReady") +self:AddTransition("CombatReady","Winchester","OutOfAmmo") +self:AddTransition({"CombatReady","OutOfAmmo"},"Rearm","Rearming") +self:AddTransition("Rearming","Rearmed","Rearmed") +self:AddTransition("*","Move","Moving") +self:AddTransition("Moving","Arrived","Arrived") +self:AddTransition("*","NewTarget","*") +self:AddTransition("*","CombatReady","CombatReady") +self:AddTransition("*","Status","*") +self:AddTransition("*","NewMove","*") +self:AddTransition("*","Dead","*") +self:AddTransition("*","Respawn","CombatReady") +self:AddTransition("*","Loaded","InTransit") +self:AddTransition("InTransit","UnLoaded","CombatReady") +self:AddTransition("Rearming","Arrived","Rearming") +self:AddTransition("Rearming","Move","Rearming") +return self +end +function ARTY:NewFromCargoGroup(cargogroup,alias) +if cargogroup then +BASE:T(string.format("ARTY script version %s. Added CARGO group %s.",ARTY.version,cargogroup:GetName())) +else +BASE:E("ERROR: Requested ARTY CARGO GROUP does not exist! (Has to be a MOOSE CARGO(!) group.)") +return nil +end +local group=cargogroup:GetObject() +local arty=ARTY:New(group,alias) +arty.iscargo=true +arty.cargogroup=cargogroup +return arty +end +function ARTY:AssignTargetCoord(coord,prio,radius,nshells,maxengage,time,weapontype,name,unique) +self:F({coord=coord,prio=prio,radius=radius,nshells=nshells,maxengage=maxengage,time=time,weapontype=weapontype,name=name,unique=unique}) +nshells=nshells or 5 +radius=radius or 100 +maxengage=maxengage or 1 +prio=prio or 50 +prio=math.max(1,prio) +prio=math.min(100,prio) +if unique==nil then +unique=false +end +weapontype=weapontype or ARTY.WeaponType.Auto +local text=nil +if coord:IsInstanceOf("GROUP")then +text="WARNING: ARTY:AssignTargetCoordinate(coord, ...) needs a COORDINATE object as first parameter - you gave a GROUP. Converting to COORDINATE..." +coord=coord:GetCoordinate() +elseif coord:IsInstanceOf("UNIT")then +text="WARNING: ARTY:AssignTargetCoordinate(coord, ...) needs a COORDINATE object as first parameter - you gave a UNIT. Converting to COORDINATE..." +coord=coord:GetCoordinate() +elseif coord:IsInstanceOf("POSITIONABLE")then +text="WARNING: ARTY:AssignTargetCoordinate(coord, ...) needs a COORDINATE object as first parameter - you gave a POSITIONABLE. Converting to COORDINATE..." +coord=coord:GetCoordinate() +elseif coord:IsInstanceOf("COORDINATE")then +else +text="ERROR: ARTY:AssignTargetCoordinate(coord, ...) needs a COORDINATE object as first parameter!" +MESSAGE:New(text,30):ToAll() +self:E(self.lid..text) +return nil +end +if text~=nil then +self:E(self.lid..text) +end +local _name=name or coord:ToStringLLDMS() +local _unique=true +_name,_unique=self:_CheckName(self.targets,_name,not unique) +if unique==true and _unique==false then +self:T(self.lid..string.format("%s: target %s should have a unique name but name was already given. Rejecting target!",self.groupname,_name)) +return nil +end +local _time +if type(time)=="string"then +_time=self:_ClockToSeconds(time) +elseif type(time)=="number"then +_time=timer.getAbsTime()+time +else +_time=timer.getAbsTime() +end +local _target={name=_name,coord=coord,radius=radius,nshells=nshells,engaged=0,underfire=false,prio=prio,maxengage=maxengage,time=_time,weapontype=weapontype} +table.insert(self.targets,_target) +self:__NewTarget(1,_target) +return _name +end +function ARTY:AssignAttackGroup(group,prio,radius,nshells,maxengage,time,weapontype,name,unique) +nshells=nshells or 5 +radius=radius or 100 +maxengage=maxengage or 1 +prio=prio or 50 +prio=math.max(1,prio) +prio=math.min(100,prio) +if unique==nil then +unique=false +end +weapontype=weapontype or ARTY.WeaponType.Auto +if type(group)=="string"then +group=GROUP:FindByName(group) +end +if group and group:IsAlive()then +local coord=group:GetCoordinate() +local _name=group:GetName() +local _unique=true +_name,_unique=self:_CheckName(self.targets,_name,not unique) +if unique==true and _unique==false then +self:T(self.lid..string.format("%s: target %s should have a unique name but name was already given. Rejecting target!",self.groupname,_name)) +return nil +end +local _time +if type(time)=="string"then +_time=self:_ClockToSeconds(time) +elseif type(time)=="number"then +_time=timer.getAbsTime()+time +else +_time=timer.getAbsTime() +end +local target={} +target.attackgroup=true +target.name=_name +target.coord=coord +target.radius=radius +target.nshells=nshells +target.engaged=0 +target.underfire=false +target.prio=prio +target.time=_time +target.maxengage=maxengage +target.weapontype=weapontype +table.insert(self.targets,target) +self:__NewTarget(1,target) +return _name +else +self:E("ERROR: Group does not exist!") +end +return nil +end +function ARTY:AssignMoveCoord(coord,time,speed,onroad,cancel,name,unique) +self:F({coord=coord,time=time,speed=speed,onroad=onroad,cancel=cancel,name=name,unique=unique}) +if not self.ismobile then +self:T(self.lid..string.format("%s: group is immobile. Rejecting move request!",self.groupname)) +return nil +end +if unique==nil then +unique=false +end +local _name=name or coord:ToStringLLDMS() +local _unique=true +_name,_unique=self:_CheckName(self.moves,_name,not unique) +if unique==true and _unique==false then +self:T(self.lid..string.format("%s: move %s should have a unique name but name was already given. Rejecting move!",self.groupname,_name)) +return nil +end +if speed then +speed=math.min(speed,self.SpeedMax) +elseif self.Speed then +speed=self.Speed +else +speed=self.SpeedMax*0.7 +end +if onroad==nil then +onroad=false +end +if cancel==nil then +cancel=false +end +local _time +if type(time)=="string"then +_time=self:_ClockToSeconds(time) +elseif type(time)=="number"then +_time=timer.getAbsTime()+time +else +_time=timer.getAbsTime() +end +local _move={name=_name,coord=coord,time=_time,speed=speed,onroad=onroad,cancel=cancel} +table.insert(self.moves,_move) +return _name +end +function ARTY:SetAlias(alias) +self:F({alias=alias}) +self.alias=tostring(alias) +return self +end +function ARTY:AddToCluster(clusters) +self:F({clusters=clusters}) +local names +if type(clusters)=="table"then +names=clusters +elseif type(clusters)=="string"then +names={clusters} +else +self:E(self.lid.."ERROR: Input parameter must be a string or a table in ARTY:AddToCluster()!") +return +end +for _,cluster in pairs(names)do +table.insert(self.clusters,cluster) +end +return self +end +function ARTY:SetMinFiringRange(range) +self:F({range=range}) +self.minrange=range*1000 or 100 +return self +end +function ARTY:SetMaxFiringRange(range) +self:F({range=range}) +self.maxrange=range*1000 or 1000*1000 +return self +end +function ARTY:SetStatusInterval(interval) +self:F({interval=interval}) +self.StatusInterval=interval or 10 +return self +end +function ARTY:SetWaitForShotTime(waittime) +self:F({waittime=waittime}) +self.WaitForShotTime=waittime or 300 +return self +end +function ARTY:SetRearmingDistance(distance) +self:F({distance=distance}) +self.RearmingDistance=distance or 100 +return self +end +function ARTY:SetRearmingGroup(group) +self:F({group=group}) +self.RearmingGroup=group +return self +end +function ARTY:SetRearmingGroupSpeed(speed) +self:F({speed=speed}) +self.RearmingGroupSpeed=speed +return self +end +function ARTY:SetRearmingGroupOnRoad(onroad) +self:F({onroad=onroad}) +if onroad==nil then +onroad=true +end +self.RearmingGroupOnRoad=onroad +return self +end +function ARTY:SetRearmingArtyOnRoad(onroad) +self:F({onroad=onroad}) +if onroad==nil then +onroad=true +end +self.RearmingArtyOnRoad=onroad +return self +end +function ARTY:SetRearmingPlace(coord) +self:F({coord=coord}) +self.RearmingPlaceCoord=coord +return self +end +function ARTY:SetAutoRelocateToFiringRange(maxdistance,onroad) +self:F({distance=maxdistance,onroad=onroad}) +self.autorelocate=true +self.autorelocatemaxdist=maxdistance or 50 +self.autorelocatemaxdist=self.autorelocatemaxdist*1000 +if onroad==nil then +onroad=false +end +self.autorelocateonroad=onroad +return self +end +function ARTY:SetAutoRelocateAfterEngagement(rmax,rmin) +self.relocateafterfire=true +self.relocateRmax=rmax or 800 +self.relocateRmin=rmin or 300 +self.relocateRmin=math.min(self.relocateRmin,self.relocateRmax) +return self +end +function ARTY:SetReportON() +self.report=true +return self +end +function ARTY:SetReportOFF() +self.report=false +return self +end +function ARTY:SetRespawnOnDeath(delay) +self.respawnafterdeath=true +self.respawndelay=delay +return self +end +function ARTY:SetDebugON() +self.Debug=true +return self +end +function ARTY:SetDebugOFF() +self.Debug=false +return self +end +function ARTY:SetSpeed(speed) +self.Speed=speed +return self +end +function ARTY:RemoveTarget(name) +self:F2(name) +local id=self:_GetTargetIndexByName(name) +if id then +self:T(self.lid..string.format("Group %s: Removing target %s (id=%d).",self.groupname,name,id)) +table.remove(self.targets,id) +if self.markallow then +local batteryname,markTargetID,markMoveID=self:_GetMarkIDfromName(name) +if batteryname==self.groupname and markTargetID~=nil then +COORDINATE:RemoveMark(markTargetID) +end +end +end +self:T(self.lid..string.format("Group %s: Number of targets = %d.",self.groupname,#self.targets)) +end +function ARTY:RemoveMove(name) +self:F2(name) +local id=self:_GetMoveIndexByName(name) +if id then +self:T(self.lid..string.format("Group %s: Removing move %s (id=%d).",self.groupname,name,id)) +table.remove(self.moves,id) +if self.markallow then +local batteryname,markTargetID,markMoveID=self:_GetMarkIDfromName(name) +if batteryname==self.groupname and markMoveID~=nil then +COORDINATE:RemoveMark(markMoveID) +end +end +end +self:T(self.lid..string.format("Group %s: Number of moves = %d.",self.groupname,#self.moves)) +end +function ARTY:RemoveAllTargets() +self:F2() +for _,target in pairs(self.targets)do +self:RemoveTarget(target.name) +end +end +function ARTY:SetShellTypes(tableofnames) +self:F2(tableofnames) +self.ammoshells={} +for _,_type in pairs(tableofnames)do +table.insert(self.ammoshells,_type) +end +return self +end +function ARTY:SetRocketTypes(tableofnames) +self:F2(tableofnames) +self.ammorockets={} +for _,_type in pairs(tableofnames)do +table.insert(self.ammorockets,_type) +end +return self +end +function ARTY:SetMissileTypes(tableofnames) +self:F2(tableofnames) +self.ammomissiles={} +for _,_type in pairs(tableofnames)do +table.insert(self.ammomissiles,_type) +end +return self +end +function ARTY:SetTacNukeShells(n) +self.Nukes=n +return self +end +function ARTY:SetTacNukeWarhead(strength) +self.nukewarhead=strength or 0.075 +self.nukewarhead=self.nukewarhead*1000*1000 +return self +end +function ARTY:SetIlluminationShells(n,power) +self.Nillu=n +self.illuPower=power or 1.0 +self.illuPower=self.illuPower*1000000 +return self +end +function ARTY:SetIlluminationMinMaxAlt(minalt,maxalt) +self.illuMinalt=minalt or 500 +self.illuMaxalt=maxalt or 1000 +if self.illuMinalt>self.illuMaxalt then +self.illuMinalt=self.illuMaxalt +end +return self +end +function ARTY:SetSmokeShells(n,color) +self.Nsmoke=n +self.smokeColor=color or SMOKECOLOR.Red +return self +end +function ARTY:SetTacNukeFires(nfires,range) +self.nukefire=true +self.nukefires=nfires +self.nukerange=range +return self +end +function ARTY:SetMarkAssignmentsOn(key,readonly) +self.markkey=key +self.markallow=true +if readonly==nil then +self.markreadonly=false +end +return self +end +function ARTY:SetMarkTargetsOff() +self.markallow=false +self.markkey=nil +return self +end +function ARTY:onafterStart(Controllable,From,Event,To) +self:_EventFromTo("onafterStart",Event,From,To) +local text=string.format("Started ARTY version %s for group %s.",ARTY.version,Controllable:GetName()) +self:I(self.lid..text) +MESSAGE:New(text,5):ToAllIf(self.Debug) +self.Nammo0,self.Nshells0,self.Nrockets0,self.Nmissiles0=self:GetAmmo(self.Debug) +if self.nukerange==nil then +self.nukerange=1500/75000*self.nukewarhead +end +if self.nukefires==nil then +self.nukefires=20/1000/1000*self.nukerange*self.nukerange +end +if self.Nukes~=nil then +self.Nukes0=math.min(self.Nukes,self.Nshells0) +else +self.Nukes=0 +self.Nukes0=0 +end +if self.Nillu~=nil then +self.Nillu0=math.min(self.Nillu,self.Nshells0) +else +self.Nillu=0 +self.Nillu0=0 +end +if self.Nsmoke~=nil then +self.Nsmoke0=math.min(self.Nsmoke,self.Nshells0) +else +self.Nsmoke=0 +self.Nsmoke0=0 +end +local _dbproperties=self:_CheckDB(self.DisplayName) +self:T({dbproperties=_dbproperties}) +if _dbproperties~=nil then +for property,value in pairs(_dbproperties)do +self:T({property=property,value=value}) +self[property]=value +end +end +if not self.ismobile then +self.RearmingPlaceCoord=nil +self.relocateafterfire=false +self.autorelocate=false +end +self.Speed=math.min(self.Speed,self.SpeedMax) +if self.RearmingGroup then +local speedmax=self.RearmingGroup:GetSpeedMax() +self:T(self.lid..string.format("%s, rearming group %s max speed = %.1f km/h.",self.groupname,self.RearmingGroup:GetName(),speedmax)) +if self.RearmingGroupSpeed==nil then +self.RearmingGroupSpeed=speedmax*0.5 +else +self.RearmingGroupSpeed=math.min(self.RearmingGroupSpeed,self.RearmingGroup:GetSpeedMax()) +end +else +self.RearmingGroupSpeed=23 +end +local text=string.format("\n******************************************************\n") +text=text..string.format("Arty group = %s\n",self.groupname) +text=text..string.format("Arty alias = %s\n",self.alias) +text=text..string.format("Artillery attribute = %s\n",tostring(self.IsArtillery)) +text=text..string.format("Type = %s\n",self.Type) +text=text..string.format("Display Name = %s\n",self.DisplayName) +text=text..string.format("Number of units = %d\n",self.IniGroupStrength) +text=text..string.format("Speed max = %d km/h\n",self.SpeedMax) +text=text..string.format("Speed default = %d km/h\n",self.Speed) +text=text..string.format("Is mobile = %s\n",tostring(self.ismobile)) +text=text..string.format("Is cargo = %s\n",tostring(self.iscargo)) +text=text..string.format("Min range = %.1f km\n",self.minrange/1000) +text=text..string.format("Max range = %.1f km\n",self.maxrange/1000) +text=text..string.format("Total ammo count = %d\n",self.Nammo0) +text=text..string.format("Number of shells = %d\n",self.Nshells0) +text=text..string.format("Number of rockets = %d\n",self.Nrockets0) +text=text..string.format("Number of missiles = %d\n",self.Nmissiles0) +text=text..string.format("Number of nukes = %d\n",self.Nukes0) +text=text..string.format("Nuclear warhead = %d tons TNT\n",self.nukewarhead/1000) +text=text..string.format("Nuclear demolition = %d m\n",self.nukerange) +text=text..string.format("Nuclear fires = %d (active=%s)\n",self.nukefires,tostring(self.nukefire)) +text=text..string.format("Number of illum. = %d\n",self.Nillu0) +text=text..string.format("Illuminaton Power = %.3f mcd\n",self.illuPower/1000000) +text=text..string.format("Illuminaton Minalt = %d m\n",self.illuMinalt) +text=text..string.format("Illuminaton Maxalt = %d m\n",self.illuMaxalt) +text=text..string.format("Number of smoke = %d\n",self.Nsmoke0) +text=text..string.format("Smoke color = %d\n",self.smokeColor) +if self.RearmingGroup or self.RearmingPlaceCoord then +text=text..string.format("Rearming safe dist. = %d m\n",self.RearmingDistance) +end +if self.RearmingGroup then +text=text..string.format("Rearming group = %s\n",self.RearmingGroup:GetName()) +text=text..string.format("Rearming group speed= %d km/h\n",self.RearmingGroupSpeed) +text=text..string.format("Rearming group roads= %s\n",tostring(self.RearmingGroupOnRoad)) +end +if self.RearmingPlaceCoord then +local dist=self.InitialCoord:Get2DDistance(self.RearmingPlaceCoord) +text=text..string.format("Rearming coord dist = %d m\n",dist) +text=text..string.format("Rearming ARTY roads = %s\n",tostring(self.RearmingArtyOnRoad)) +end +text=text..string.format("Relocate after fire = %s\n",tostring(self.relocateafterfire)) +text=text..string.format("Relocate min dist. = %d m\n",self.relocateRmin) +text=text..string.format("Relocate max dist. = %d m\n",self.relocateRmax) +text=text..string.format("Auto move in range = %s\n",tostring(self.autorelocate)) +text=text..string.format("Auto move dist. max = %.1f km\n",self.autorelocatemaxdist/1000) +text=text..string.format("Auto move on road = %s\n",tostring(self.autorelocateonroad)) +text=text..string.format("Marker assignments = %s\n",tostring(self.markallow)) +text=text..string.format("Marker auth. key = %s\n",tostring(self.markkey)) +text=text..string.format("Marker readonly = %s\n",tostring(self.markreadonly)) +text=text..string.format("Clusters:\n") +for _,cluster in pairs(self.clusters)do +text=text..string.format("- %s\n",tostring(cluster)) +end +text=text..string.format("******************************************************\n") +text=text..string.format("Targets:\n") +for _,target in pairs(self.targets)do +text=text..string.format("- %s\n",self:_TargetInfo(target)) +local possible=self:_CheckWeaponTypePossible(target) +if not possible then +self:E(self.lid..string.format("WARNING: Selected weapon type %s is not possible",self:_WeaponTypeName(target.weapontype))) +end +if self.Debug then +local zone=ZONE_RADIUS:New(target.name,target.coord:GetVec2(),target.radius) +zone:BoundZone(180,coalition.side.NEUTRAL) +end +end +text=text..string.format("Moves:\n") +for i=1,#self.moves do +text=text..string.format("- %s\n",self:_MoveInfo(self.moves[i])) +end +text=text..string.format("******************************************************\n") +text=text..string.format("Shell types:\n") +for _,_type in pairs(self.ammoshells)do +text=text..string.format("- %s\n",_type) +end +text=text..string.format("Rocket types:\n") +for _,_type in pairs(self.ammorockets)do +text=text..string.format("- %s\n",_type) +end +text=text..string.format("Missile types:\n") +for _,_type in pairs(self.ammomissiles)do +text=text..string.format("- %s\n",_type) +end +text=text..string.format("******************************************************") +if self.Debug then +self:I(self.lid..text) +else +self:T(self.lid..text) +end +self.Controllable:OptionROEHoldFire() +self:HandleEvent(EVENTS.Shot) +self:HandleEvent(EVENTS.Dead) +if self.markallow then +world.addEventHandler(self) +end +self:__Status(self.StatusInterval) +end +function ARTY:_CheckDB(displayname) +for _type,_properties in pairs(ARTY.db)do +self:T({type=_type,properties=_properties}) +if _type==displayname then +self:T({type=_type,properties=_properties}) +return _properties +end +end +return nil +end +function ARTY:_StatusReport(display) +if display==nil then +display=false +end +local Nammo,Nshells,Nrockets,Nmissiles=self:GetAmmo() +local Nnukes=self.Nukes +local Nillu=self.Nillu +local Nsmoke=self.Nsmoke +local Tnow=timer.getTime() +local Clock=self:_SecondsToClock(timer.getAbsTime()) +local text=string.format("\n******************* STATUS ***************************\n") +text=text..string.format("ARTY group = %s\n",self.groupname) +text=text..string.format("Clock = %s\n",Clock) +text=text..string.format("FSM state = %s\n",self:GetState()) +text=text..string.format("Total ammo count = %d\n",Nammo) +text=text..string.format("Number of shells = %d\n",Nshells) +text=text..string.format("Number of rockets = %d\n",Nrockets) +text=text..string.format("Number of missiles = %d\n",Nmissiles) +text=text..string.format("Number of nukes = %d\n",Nnukes) +text=text..string.format("Number of illum. = %d\n",Nillu) +text=text..string.format("Number of smoke = %d\n",Nsmoke) +if self.currentTarget then +text=text..string.format("Current Target = %s\n",tostring(self.currentTarget.name)) +text=text..string.format("Curr. Tgt assigned = %d\n",Tnow-self.currentTarget.Tassigned) +else +text=text..string.format("Current Target = %s\n","none") +end +text=text..string.format("Nshots curr. Target = %d\n",self.Nshots) +text=text..string.format("Targets:\n") +for i=1,#self.targets do +text=text..string.format("- %s\n",self:_TargetInfo(self.targets[i])) +end +if self.currentMove then +text=text..string.format("Current Move = %s\n",tostring(self.currentMove.name)) +else +text=text..string.format("Current Move = %s\n","none") +end +text=text..string.format("Moves:\n") +for i=1,#self.moves do +text=text..string.format("- %s\n",self:_MoveInfo(self.moves[i])) +end +text=text..string.format("******************************************************") +env.info(self.lid..text) +MESSAGE:New(text,20):Clear():ToCoalitionIf(self.coalition,display) +end +function ARTY:OnEventShot(EventData) +self:F(EventData) +local _weapon=EventData.Weapon:getTypeName() +local _weaponStrArray=self:_split(_weapon,"%.") +local _weaponName=_weaponStrArray[#_weaponStrArray] +self:T3(self.lid.."EVENT SHOT: Ini unit = "..EventData.IniUnitName) +self:T3(self.lid.."EVENT SHOT: Ini group = "..EventData.IniGroupName) +self:T3(self.lid.."EVENT SHOT: Weapon type = ".._weapon) +self:T3(self.lid.."EVENT SHOT: Weapon name = ".._weaponName) +local group=EventData.IniGroup +if group and group:IsAlive()then +if EventData.IniGroupName==self.groupname then +if self.currentTarget then +self.Nshots=self.Nshots+1 +local text=string.format("%s, fired shot %d of %d with weapon %s on target %s.",self.alias,self.Nshots,self.currentTarget.nshells,_weaponName,self.currentTarget.name) +self:T(self.lid..text) +MESSAGE:New(text,5):Clear():ToAllIf(self.report or self.Debug) +local _lastpos={x=0,y=0,z=0} +local function _TrackWeapon(_data) +local _weaponalive,_currpos=pcall( +function() +return _data.weapon:getPoint() +end) +self:T3(self.lid..string.format("ARTY %s: Weapon still in air: %s",self.groupname,tostring(_weaponalive))) +local _destroyweapon=false +if _weaponalive then +_lastpos={x=_currpos.x,y=_currpos.y,z=_currpos.z} +local _coord=COORDINATE:NewFromVec3(_lastpos) +local _dist=_coord:Get2DDistance(_data.target.coord) +self:T3(self.lid..string.format("ARTY %s weapon to target dist = %d m",self.groupname,_dist)) +if _data.target.weapontype==ARTY.WeaponType.IlluminationShells then +if _dist<_data.target.radius then +local _cr=_data.target.coord:GetRandomCoordinateInRadius(_data.target.radius) +local _alt=_cr:GetLandHeight()+math.random(self.illuMinalt,self.illuMaxalt) +local _ci=COORDINATE:New(_cr.x,_alt,_cr.z) +_ci:IlluminationBomb(self.illuPower) +_destroyweapon=true +end +elseif _data.target.weapontype==ARTY.WeaponType.SmokeShells then +if _dist<_data.target.radius then +local _cr=_coord:GetRandomCoordinateInRadius(_data.target.radius) +_cr:Smoke(self.smokeColor) +_destroyweapon=true +end +end +if _destroyweapon then +self:T2(self.lid..string.format("ARTY %s destroying shell, stopping timer.",self.groupname)) +_data.weapon:destroy() +return nil +else +local dt=0.02 +self:T3(self.lid..string.format("ARTY %s tracking weapon again in %.3f seconds",self.groupname,dt)) +return timer.getTime()+dt +end +else +local _impactcoord=COORDINATE:NewFromVec3(_lastpos) +self:I(self.lid..string.format("ARTY %s weapon NOT ALIVE any more.",self.groupname)) +if _data.target.weapontype==ARTY.WeaponType.TacticalNukes then +self:T(self.lid..string.format("ARTY %s triggering nuclear explosion in one second.",self.groupname)) +SCHEDULER:New(nil,ARTY._NuclearBlast,{self,_impactcoord},1.0) +end +return nil +end +end +local _tracknuke=self.currentTarget.weapontype==ARTY.WeaponType.TacticalNukes and self.Nukes>0 +local _trackillu=self.currentTarget.weapontype==ARTY.WeaponType.IlluminationShells and self.Nillu>0 +local _tracksmoke=self.currentTarget.weapontype==ARTY.WeaponType.SmokeShells and self.Nsmoke>0 +if _tracknuke or _trackillu or _tracksmoke then +self:T(self.lid..string.format("ARTY %s: Tracking of weapon starts in two seconds.",self.groupname)) +local _peter={} +_peter.weapon=EventData.weapon +_peter.target=UTILS.DeepCopy(self.currentTarget) +timer.scheduleFunction(_TrackWeapon,_peter,timer.getTime()+2.0) +end +local _nammo,_nshells,_nrockets,_nmissiles=self:GetAmmo() +if self.currentTarget.weapontype==ARTY.WeaponType.TacticalNukes then +self.Nukes=self.Nukes-1 +end +if self.currentTarget.weapontype==ARTY.WeaponType.IlluminationShells then +self.Nillu=self.Nillu-1 +end +if self.currentTarget.weapontype==ARTY.WeaponType.SmokeShells then +self.Nsmoke=self.Nsmoke-1 +end +local _outofammo=false +if _nammo==0 then +self:T(self.lid..string.format("Group %s completely out of ammo.",self.groupname)) +_outofammo=true +end +local _partlyoutofammo=self:_CheckOutOfAmmo({self.currentTarget}) +local _weapontype=self:_WeaponTypeName(self.currentTarget.weapontype) +self:T(self.lid..string.format("Group %s ammo: total=%d, shells=%d, rockets=%d, missiles=%d",self.groupname,_nammo,_nshells,_nrockets,_nmissiles)) +self:T(self.lid..string.format("Group %s uses weapontype %s for current target.",self.groupname,_weapontype)) +local _ceasefire=false +local _relocate=false +if self.Nshots>=self.currentTarget.nshells then +local text=string.format("Group %s stop firing on target %s.",self.groupname,self.currentTarget.name) +self:T(self.lid..text) +MESSAGE:New(text,5):ToAllIf(self.Debug) +_ceasefire=true +_relocate=self.relocateafterfire +end +if _outofammo or _partlyoutofammo then +_ceasefire=true +end +if _relocate then +self:_Relocate() +end +if _ceasefire then +self:CeaseFire(self.currentTarget) +end +else +self:E(self.lid..string.format("WARNING: No current target for group %s?!",self.groupname)) +end +end +end +end +function ARTY:onEvent(Event) +if Event==nil or Event.idx==nil then +self:T3("Skipping onEvent. Event or Event.idx unknown.") +return true +end +self:T2(string.format("Event captured = %s",tostring(self.groupname))) +self:T2(string.format("Event id = %s",tostring(Event.id))) +self:T2(string.format("Event time = %s",tostring(Event.time))) +self:T2(string.format("Event idx = %s",tostring(Event.idx))) +self:T2(string.format("Event coalition = %s",tostring(Event.coalition))) +self:T2(string.format("Event group id = %s",tostring(Event.groupID))) +self:T2(string.format("Event text = %s",tostring(Event.text))) +if Event.initiator~=nil then +local _unitname=Event.initiator:getName() +self:T2(string.format("Event ini unit name = %s",tostring(_unitname))) +end +if Event.id==world.event.S_EVENT_MARK_ADDED then +self:T2({event="S_EVENT_MARK_ADDED",battery=self.groupname,vec3=Event.pos}) +elseif Event.id==world.event.S_EVENT_MARK_CHANGE then +self:T({event="S_EVENT_MARK_CHANGE",battery=self.groupname,vec3=Event.pos}) +self:_OnEventMarkChange(Event) +elseif Event.id==world.event.S_EVENT_MARK_REMOVED then +self:T2({event="S_EVENT_MARK_REMOVED",battery=self.groupname,vec3=Event.pos}) +self:_OnEventMarkRemove(Event) +end +end +function ARTY:_OnEventMarkRemove(Event) +local batterycoalition=self.coalition +if Event.text~=nil and Event.text:find("BATTERY")then +local _cancelmove=false +local _canceltarget=false +local _name="" +local _id=nil +if Event.text:find("Marked Relocation")then +_cancelmove=true +_name=self:_MarkMoveName(Event.idx) +_id=self:_GetMoveIndexByName(_name) +elseif Event.text:find("Marked Target")then +_canceltarget=true +_name=self:_MarkTargetName(Event.idx) +_id=self:_GetTargetIndexByName(_name) +else +return +end +if _id==nil then +return +end +if(batterycoalition==Event.coalition and self.markkey==nil)or self.markkey~=nil then +local _validkey=self:_MarkerKeyAuthentification(Event.text) +if _validkey then +if _cancelmove then +if self.currentMove and self.currentMove.name==_name then +self.Controllable:ClearTasks() +self:Arrived() +else +self:RemoveMove(_name) +end +elseif _canceltarget then +if self.currentTarget and self.currentTarget.name==_name then +self:CeaseFire(self.currentTarget) +self:RemoveTarget(_name) +else +self:RemoveTarget(_name) +end +end +end +end +end +end +function ARTY:_OnEventMarkChange(Event) +if Event.text~=nil and Event.text:lower():find("arty")then +local vec3={y=Event.pos.y,x=Event.pos.x,z=Event.pos.z} +local _coord=COORDINATE:NewFromVec3(vec3) +_coord.y=_coord:GetLandHeight() +local batterycoalition=self.coalition +local batteryname=self.groupname +if(batterycoalition==Event.coalition and self.markkey==nil)or self.markkey~=nil then +local _assign=self:_Markertext(Event.text) +if _assign==nil or not(_assign.engage or _assign.move or _assign.request or _assign.cancel or _assign.set)then +self:T(self.lid..string.format("WARNING: %s, no keyword ENGAGE, MOVE, REQUEST, CANCEL or SET in mark text! Command will not be executed. Text:\n%s",self.groupname,Event.text)) +return +end +local _assigned=false +if _assign.everyone then +_assigned=true +else +for _,bat in pairs(_assign.battery)do +if self.groupname==bat then +_assigned=true +end +end +for _,alias in pairs(_assign.aliases)do +if self.alias==alias then +_assigned=true +end +end +for _,bat in pairs(_assign.cluster)do +for _,cluster in pairs(self.clusters)do +if cluster==bat then +_assigned=true +end +end +end +end +if not _assigned then +self:T3(self.lid..string.format("INFO: ARTY group %s was not addressed! Mark text:\n%s",self.groupname,Event.text)) +return +else +if self.Controllable and self.Controllable:IsAlive()then +else +self:T3(self.lid..string.format("INFO: ARTY group %s was addressed but is NOT alive! Mark text:\n%s",self.groupname,Event.text)) +return +end +end +if _assign.coord then +_coord=_assign.coord +end +local _validkey=self:_MarkerKeyAuthentification(Event.text) +if _assign.request and _validkey then +if _assign.requestammo then +self:_MarkRequestAmmo() +end +if _assign.requestmoves then +self:_MarkRequestMoves() +end +if _assign.requesttargets then +self:_MarkRequestTargets() +end +if _assign.requeststatus then +self:_MarkRequestStatus() +end +if _assign.requestrearming then +self:Rearm() +end +return +end +if _assign.cancel and _validkey then +if _assign.cancelmove and self.currentMove then +self.Controllable:ClearTasks() +self:Arrived() +elseif _assign.canceltarget and self.currentTarget then +self.currentTarget.engaged=self.currentTarget.engaged+1 +self:CeaseFire(self.currentTarget) +elseif _assign.cancelrearm and self:is("Rearming")then +local nammo=self:GetAmmo() +if nammo>0 then +self:Rearmed() +else +self:Winchester() +end +end +return +end +if _assign.set and _validkey then +if _assign.setrearmingplace and self.ismobile then +self:SetRearmingPlace(_coord) +_coord:RemoveMark(Event.idx) +_coord:MarkToCoalition(string.format("Rearming place for battery %s",self.groupname),self.coalition,false,string.format("New rearming place for battery %s defined.",self.groupname)) +if self.Debug then +_coord:SmokeOrange() +end +end +if _assign.setrearminggroup then +_coord:RemoveMark(Event.idx) +local rearminggroupcoord=_assign.setrearminggroup:GetCoordinate() +rearminggroupcoord:MarkToCoalition(string.format("Rearming group for battery %s",self.groupname),self.coalition,false,string.format("New rearming group for battery %s defined.",self.groupname)) +self:SetRearmingGroup(_assign.setrearminggroup) +if self.Debug then +rearminggroupcoord:SmokeOrange() +end +end +return +end +if _validkey then +_coord:RemoveMark(Event.idx) +local _id=UTILS._MarkID+1 +if _assign.move then +local _name=self:_MarkMoveName(_id) +local text=string.format("%s, received new relocation assignment.",self.alias) +text=text..string.format("\nCoordinates %s",_coord:ToStringLLDMS()) +MESSAGE:New(text,10):ToCoalitionIf(batterycoalition,self.report or self.Debug) +local _movename=self:AssignMoveCoord(_coord,_assign.time,_assign.speed,_assign.onroad,_assign.movecanceltarget,_name,true) +if _movename~=nil then +local _mid=self:_GetMoveIndexByName(_movename) +local _move=self.moves[_mid] +local clock=tostring(self:_SecondsToClock(_move.time)) +local _markertext=_movename..string.format(", Time=%s, Speed=%d km/h, Use Roads=%s.",clock,_move.speed,tostring(_move.onroad)) +local _randomcoord=_coord:GetRandomCoordinateInRadius(100) +_randomcoord:MarkToCoalition(_markertext,batterycoalition,self.markreadonly or _assign.readonly) +else +local text=string.format("%s, relocation not possible.",self.alias) +MESSAGE:New(text,10):ToCoalitionIf(batterycoalition,self.report or self.Debug) +end +else +local _name=self:_MarkTargetName(_id) +local text=string.format("%s, received new target assignment.",self.alias) +text=text..string.format("\nCoordinates %s",_coord:ToStringLLDMS()) +if _assign.time then +text=text..string.format("\nTime %s",_assign.time) +end +if _assign.prio then +text=text..string.format("\nPrio %d",_assign.prio) +end +if _assign.radius then +text=text..string.format("\nRadius %d m",_assign.radius) +end +if _assign.nshells then +text=text..string.format("\nShots %d",_assign.nshells) +end +if _assign.maxengage then +text=text..string.format("\nEngagements %d",_assign.maxengage) +end +if _assign.weapontype then +text=text..string.format("\nWeapon %s",self:_WeaponTypeName(_assign.weapontype)) +end +MESSAGE:New(text,10):ToCoalitionIf(batterycoalition,self.report or self.Debug) +local _targetname=self:AssignTargetCoord(_coord,_assign.prio,_assign.radius,_assign.nshells,_assign.maxengage,_assign.time,_assign.weapontype,_name,true) +if _targetname~=nil then +local _tid=self:_GetTargetIndexByName(_targetname) +local _target=self.targets[_tid] +local clock=tostring(self:_SecondsToClock(_target.time)) +local weapon=self:_WeaponTypeName(_target.weapontype) +local _markertext=_targetname..string.format(", Priority=%d, Radius=%d m, Shots=%d, Engagements=%d, Weapon=%s, Time=%s",_target.prio,_target.radius,_target.nshells,_target.maxengage,weapon,clock) +local _randomcoord=_coord:GetRandomCoordinateInRadius(250) +_randomcoord:MarkToCoalition(_markertext,batterycoalition,self.markreadonly or _assign.readonly) +end +end +end +end +end +end +function ARTY:OnEventDead(EventData) +self:F(EventData) +local _name=self.groupname +if EventData and EventData.IniGroupName and EventData.IniGroupName==_name then +local unitname=tostring(EventData.IniUnitName) +self:T(self.lid..string.format("%s: Captured dead event for unit %s.",_name,unitname)) +self:Dead(unitname) +end +end +function ARTY:onafterStatus(Controllable,From,Event,To) +self:_EventFromTo("onafterStatus",Event,From,To) +local nammo,nshells,nrockets,nmissiles=self:GetAmmo() +if self.iscargo and self.cargogroup then +if self.cargogroup:IsLoaded()and not self:is("InTransit")then +self:T(self.lid..string.format("Group %s has been loaded into a carrier and is now transported.",self.alias)) +self:Loaded() +elseif self.cargogroup:IsUnLoaded()then +self:T(self.lid..string.format("Group %s has been unloaded from the carrier.",self.alias)) +self:UnLoaded() +end +end +local fsmstate=self:GetState() +self:T(self.lid..string.format("Status %s, Ammo total=%d: shells=%d [smoke=%d, illu=%d, nukes=%d*%.3f kT], rockets=%d, missiles=%d",fsmstate,nammo,nshells,self.Nsmoke,self.Nillu,self.Nukes,self.nukewarhead/1000000,nrockets,nmissiles)) +if self.Controllable and self.Controllable:IsAlive()then +if self.Debug then +self:_StatusReport() +end +if self:is("Moving")then +self:T2(self.lid..string.format("%s: Moving",Controllable:GetName())) +end +if self:is("Rearming")then +local _rearmed=self:_CheckRearmed() +if _rearmed then +self:T2(self.lid..string.format("%s: Rearming ==> Rearmed",Controllable:GetName())) +self:Rearmed() +end +end +if self:is("Rearmed")then +local distance=self.Controllable:GetCoordinate():Get2DDistance(self.InitialCoord) +self:T2(self.lid..string.format("%s: Rearmed. Distance ARTY to InitalCoord = %d m",Controllable:GetName(),distance)) +if distance<=self.RearmingDistance then +self:T2(self.lid..string.format("%s: Rearmed ==> CombatReady",Controllable:GetName())) +self:CombatReady() +end +end +if self:is("Arrived")then +self:T2(self.lid..string.format("%s: Arrived ==> CombatReady",Controllable:GetName())) +self:CombatReady() +end +if self:is("Firing")then +self:_CheckShootingStarted() +end +self:_CheckTargetsInRange() +local notpossible={} +for i=1,#self.targets do +local _target=self.targets[i] +local possible=self:_CheckWeaponTypePossible(_target) +if not possible then +table.insert(notpossible,_target.name) +end +end +for _,targetname in pairs(notpossible)do +self:E(self.lid..string.format("%s: Removing target %s because requested weapon is not possible with this type of unit.",self.groupname,targetname)) +self:RemoveTarget(targetname) +end +local _timedTarget=self:_CheckTimedTargets() +local _normalTarget=self:_CheckNormalTargets() +local _move=self:_CheckMoves() +if _move then +self:Move(_move) +elseif _timedTarget then +if self.currentTarget then +self:CeaseFire(self.currentTarget) +end +self:OpenFire(_timedTarget) +elseif _normalTarget then +self:OpenFire(_normalTarget) +end +local gotsome=false +if#self.targets>0 then +for i=1,#self.targets do +local _target=self.targets[i] +if self:_CheckWeaponTypeAvailable(_target)>0 then +gotsome=true +end +end +else +gotsome=true +end +if(nammo==0 or not gotsome)and not(self:is("Moving")or self:is("Rearming")or self:is("OutOfAmmo"))then +self:Winchester() +end +if self:is("OutOfAmmo")then +self:T2(self.lid..string.format("%s: OutOfAmmo ==> Rearm ==> Rearming",Controllable:GetName())) +self:Rearm() +end +self:__Status(self.StatusInterval) +elseif self.iscargo then +if self.cargogroup and self.cargogroup:IsAlive()then +if self:is("InTransit")then +self:__Status(-5) +end +end +else +self:E(self.lid..string.format("Arty group %s is not alive!",self.groupname)) +end +end +function ARTY:onbeforeLoaded(Controllable,From,Event,To) +if self.currentTarget then +self:CeaseFire(self.currentTarget) +end +return true +end +function ARTY:onafterUnLoaded(Controllable,From,Event,To) +self:CombatReady() +end +function ARTY:onenterCombatReady(Controllable,From,Event,To) +self:_EventFromTo("onenterCombatReady",Event,From,To) +self:T3(self.lid..string.format("onenterComabReady, from=%s, event=%s, to=%s",From,Event,To)) +end +function ARTY:onbeforeOpenFire(Controllable,From,Event,To,target) +self:_EventFromTo("onbeforeOpenFire",Event,From,To) +if self.currentTarget then +self:E(self.lid..string.format("ERROR: Group %s already has a target %s!",self.groupname,self.currentTarget.name)) +return false +end +if not self:_TargetInRange(target)then +self:E(self.lid..string.format("ERROR: Group %s, target %s is out of range!",self.groupname,self.currentTarget.name)) +return false +end +local nfire=self:_CheckWeaponTypeAvailable(target) +target.nshells=math.min(target.nshells,nfire) +if target.nshells<1 then +local text=string.format("%s, no ammo left to engage target %s with selected weapon type %s.") +return false +end +return true +end +function ARTY:onafterOpenFire(Controllable,From,Event,To,target) +self:_EventFromTo("onafterOpenFire",Event,From,To) +local id=self:_GetTargetIndexByName(target.name) +if id then +self.targets[id].underfire=true +self.currentTarget=target +self.currentTarget.Tassigned=timer.getTime() +end +local range=Controllable:GetCoordinate():Get2DDistance(target.coord) +local Nammo,Nshells,Nrockets,Nmissiles=self:GetAmmo() +local nfire=Nammo +local _type="shots" +if target.weapontype==ARTY.WeaponType.Auto then +nfire=Nammo +_type="shots" +elseif target.weapontype==ARTY.WeaponType.Cannon then +nfire=Nshells +_type="shells" +elseif target.weapontype==ARTY.WeaponType.TacticalNukes then +nfire=self.Nukes +_type="nuclear shells" +elseif target.weapontype==ARTY.WeaponType.IlluminationShells then +nfire=self.Nillu +_type="illumination shells" +elseif target.weapontype==ARTY.WeaponType.SmokeShells then +nfire=self.Nsmoke +_type="smoke shells" +elseif target.weapontype==ARTY.WeaponType.Rockets then +nfire=Nrockets +_type="rockets" +elseif target.weapontype==ARTY.WeaponType.CruiseMissile then +nfire=Nmissiles +_type="cruise missiles" +end +target.nshells=math.min(target.nshells,nfire) +local text=string.format("%s, opening fire on target %s with %d %s. Distance %.1f km.",Controllable:GetName(),target.name,target.nshells,_type,range/1000) +self:T(self.lid..text) +MESSAGE:New(text,10):ToCoalitionIf(self.coalition,self.report) +if target.attackgroup then +self:_AttackGroup(target) +else +self:_FireAtCoord(target.coord,target.radius,target.nshells,target.weapontype) +end +end +function ARTY:onafterCeaseFire(Controllable,From,Event,To,target) +self:_EventFromTo("onafterCeaseFire",Event,From,To) +if target then +local text=string.format("%s, ceasing fire on target %s.",Controllable:GetName(),target.name) +self:T(self.lid..text) +MESSAGE:New(text,10):ToCoalitionIf(self.coalition,self.report) +local id=self:_GetTargetIndexByName(target.name) +if id then +if self.Nshots>0 then +self.targets[id].engaged=self.targets[id].engaged+1 +self.targets[id].time=nil +end +self.targets[id].underfire=false +end +if target.engaged>=target.maxengage then +self:RemoveTarget(target.name) +end +self.Controllable:OptionROEHoldFire() +self.Controllable:ClearTasks() +else +self:E(self.lid..string.format("ERROR: No target in cease fire for group %s.",self.groupname)) +end +self.Nshots=0 +self.currentTarget=nil +end +function ARTY:onafterWinchester(Controllable,From,Event,To) +self:_EventFromTo("onafterWinchester",Event,From,To) +local text=string.format("%s, winchester!",Controllable:GetName()) +self:T(self.lid..text) +MESSAGE:New(text,10):ToCoalitionIf(self.coalition,self.report or self.Debug) +end +function ARTY:onbeforeRearm(Controllable,From,Event,To) +self:_EventFromTo("onbeforeRearm",Event,From,To) +local _rearmed=self:_CheckRearmed() +if _rearmed then +self:T(self.lid..string.format("%s, group is already armed to the teeth. Rearming request denied!",self.groupname)) +return false +else +self:T(self.lid..string.format("%s, group might be rearmed.",self.groupname)) +end +if self.RearmingGroup and self.RearmingGroup:IsAlive()then +return true +elseif self.RearmingPlaceCoord then +return true +else +return false +end +end +function ARTY:onafterRearm(Controllable,From,Event,To) +self:_EventFromTo("onafterRearm",Event,From,To) +local coordARTY=self.Controllable:GetCoordinate() +self.InitialCoord=coordARTY +local coordRARM=nil +if self.RearmingGroup then +coordRARM=self.RearmingGroup:GetCoordinate() +self.RearmingGroupCoord=coordRARM +end +if self.RearmingGroup and self.RearmingPlaceCoord and self.ismobile then +local text=string.format("%s, %s, request rearming at rearming place.",Controllable:GetName(),self.RearmingGroup:GetName()) +self:T(self.lid..text) +MESSAGE:New(text,10):ToCoalitionIf(self.coalition,self.report or self.Debug) +local dA=coordARTY:Get2DDistance(self.RearmingPlaceCoord) +local dR=coordRARM:Get2DDistance(self.RearmingPlaceCoord) +if dA>self.RearmingDistance then +local _tocoord=self:_VicinityCoord(self.RearmingPlaceCoord,self.RearmingDistance/4,self.RearmingDistance/2) +self:AssignMoveCoord(_tocoord,nil,nil,self.RearmingArtyOnRoad,false,"REARMING MOVE TO REARMING PLACE",true) +end +if dR>self.RearmingDistance then +local ToCoord=self:_VicinityCoord(self.RearmingPlaceCoord,self.RearmingDistance/4,self.RearmingDistance/2) +self:_Move(self.RearmingGroup,ToCoord,self.RearmingGroupSpeed,self.RearmingGroupOnRoad) +end +elseif self.RearmingGroup then +local text=string.format("%s, %s, request rearming.",Controllable:GetName(),self.RearmingGroup:GetName()) +self:T(self.lid..text) +MESSAGE:New(text,10):ToCoalitionIf(self.coalition,self.report or self.Debug) +local distance=coordARTY:Get2DDistance(coordRARM) +if distance>self.RearmingDistance then +self:_Move(self.RearmingGroup,self:_VicinityCoord(coordARTY),self.RearmingGroupSpeed,self.RearmingGroupOnRoad) +end +elseif self.RearmingPlaceCoord then +local text=string.format("%s, moving to rearming place.",Controllable:GetName()) +self:T(self.lid..text) +MESSAGE:New(text,10):ToCoalitionIf(self.coalition,self.report or self.Debug) +local dA=coordARTY:Get2DDistance(self.RearmingPlaceCoord) +if dA>self.RearmingDistance then +local _tocoord=self:_VicinityCoord(self.RearmingPlaceCoord) +self:AssignMoveCoord(_tocoord,nil,nil,self.RearmingArtyOnRoad,false,"REARMING MOVE TO REARMING PLACE",true) +end +end +end +function ARTY:onafterRearmed(Controllable,From,Event,To) +self:_EventFromTo("onafterRearmed",Event,From,To) +local text=string.format("%s, rearming complete.",Controllable:GetName()) +self:T(self.lid..text) +MESSAGE:New(text,10):ToCoalitionIf(self.coalition,self.report or self.Debug) +self.Nukes=self.Nukes0 +self.Nillu=self.Nillu0 +self.Nsmoke=self.Nsmoke0 +local dist=self.Controllable:GetCoordinate():Get2DDistance(self.InitialCoord) +if dist>self.RearmingDistance then +self:AssignMoveCoord(self.InitialCoord,nil,nil,self.RearmingArtyOnRoad,false,"REARMING MOVE REARMING COMPLETE",true) +end +if self.RearmingGroup and self.RearmingGroup:IsAlive()then +local d=self.RearmingGroup:GetCoordinate():Get2DDistance(self.RearmingGroupCoord) +if d>self.RearmingDistance then +self:_Move(self.RearmingGroup,self.RearmingGroupCoord,self.RearmingGroupSpeed,self.RearmingGroupOnRoad) +else +self.RearmingGroup:ClearTasks() +end +end +end +function ARTY:_CheckRearmed() +self:F2() +local nammo,nshells,nrockets,nmissiles=self:GetAmmo() +local units=self.Controllable:GetUnits() +local nunits=0 +if units then +nunits=#units +end +local FullAmmo=self.Nammo0*nunits/self.IniGroupStrength +local _rearmpc=nammo/FullAmmo*100 +if _rearmpc>1 then +local text=string.format("%s, rearming %d %% complete.",self.alias,_rearmpc) +self:T(self.lid..text) +MESSAGE:New(text,10):ToCoalitionIf(self.coalition,self.report or self.Debug) +end +if nammo>=FullAmmo then +return true +else +return false +end +end +function ARTY:onbeforeMove(Controllable,From,Event,To,move) +self:_EventFromTo("onbeforeMove",Event,From,To) +if not self.ismobile then +return false +end +if self.currentTarget then +if move.cancel then +self:CeaseFire(self.currentTarget) +else +return false +end +end +return true +end +function ARTY:onafterMove(Controllable,From,Event,To,move) +self:_EventFromTo("onafterMove",Event,From,To) +self.Controllable:OptionAlarmStateGreen() +self.Controllable:OptionROEHoldFire() +local _Speed=math.min(move.speed,self.SpeedMax) +if self.Debug then +move.coord:SmokeRed() +end +self.currentMove=move +self:_Move(self.Controllable,move.coord,move.speed,move.onroad) +end +function ARTY:onafterArrived(Controllable,From,Event,To) +self:_EventFromTo("onafterArrived",Event,From,To) +self.Controllable:OptionAlarmStateAuto() +local text=string.format("%s, arrived at destination.",Controllable:GetName()) +self:T(self.lid..text) +MESSAGE:New(text,10):ToCoalitionIf(self.coalition,self.report or self.Debug) +if self.currentMove then +self:RemoveMove(self.currentMove.name) +self.currentMove=nil +end +end +function ARTY:onafterNewTarget(Controllable,From,Event,To,target) +self:_EventFromTo("onafterNewTarget",Event,From,To) +local text=string.format("Adding new target %s.",target.name) +MESSAGE:New(text,5):ToAllIf(self.Debug) +self:T(self.lid..text) +end +function ARTY:onafterNewMove(Controllable,From,Event,To,move) +self:_EventFromTo("onafterNewTarget",Event,From,To) +local text=string.format("Adding new move %s.",move.name) +MESSAGE:New(text,5):ToAllIf(self.Debug) +self:T(self.lid..text) +end +function ARTY:onafterDead(Controllable,From,Event,To,Unitname) +self:_EventFromTo("onafterDead",Event,From,To) +local nunits=self.Controllable:CountAliveUnits() +local text=string.format("%s, our unit %s just died! %d units left.",self.groupname,Unitname,nunits) +MESSAGE:New(text,5):ToAllIf(self.Debug) +self:I(self.lid..text) +if nunits==0 then +if self.currentTarget then +self:CeaseFire(self.currentTarget) +end +if self.respawnafterdeath then +if not self.respawning then +self.respawning=true +self:__Respawn(self.respawndelay or 1) +end +else +self:Stop() +end +end +end +function ARTY:onafterRespawn(Controllable,From,Event,To) +self:_EventFromTo("onafterRespawn",Event,From,To) +env.info("FF Respawning arty group") +local group=self.Controllable +self.Controllable=group:Respawn() +self.respawning=false +self:__Status(-1) +end +function ARTY:onafterStop(Controllable,From,Event,To) +self:_EventFromTo("onafterStop",Event,From,To) +self:I(self.lid..string.format("Stopping ARTY FSM for group %s.",tostring(Controllable:GetName()))) +if self.currentTarget then +self:CeaseFire(self.currentTarget) +end +self:UnHandleEvent(EVENTS.Shot) +self:UnHandleEvent(EVENTS.Dead) +end +function ARTY:_FireAtCoord(coord,radius,nshells,weapontype) +self:F({coord=coord,radius=radius,nshells=nshells}) +local group=self.Controllable +if weapontype==ARTY.WeaponType.TacticalNukes or weapontype==ARTY.WeaponType.IlluminationShells or weapontype==ARTY.WeaponType.SmokeShells then +weapontype=ARTY.WeaponType.Cannon +end +group:OptionROEOpenFire() +local vec2=coord:GetVec2() +local fire=group:TaskFireAtPoint(vec2,radius,nshells,weapontype) +group:SetTask(fire) +end +function ARTY:_AttackGroup(target) +local group=self.Controllable +local weapontype=target.weapontype +if weapontype==ARTY.WeaponType.TacticalNukes or weapontype==ARTY.WeaponType.IlluminationShells or weapontype==ARTY.WeaponType.SmokeShells then +weapontype=ARTY.WeaponType.Cannon +end +group:OptionROEOpenFire() +local targetgroup=GROUP:FindByName(target.name) +local fire=group:TaskAttackGroup(targetgroup,weapontype,AI.Task.WeaponExpend.ONE,1) +group:SetTask(fire) +end +function ARTY:_NuclearBlast(_coord) +local S0=self.nukewarhead +local R0=self.nukerange +local N0=self.nukefires +_coord:Explosion(S0) +_coord:BigSmokeAndFireHuge() +local _fires={} +for i=1,N0 do +local _fire=_coord:GetRandomCoordinateInRadius(R0) +local _dist=_fire:Get2DDistance(_coord) +table.insert(_fires,{distance=_dist,coord=_fire}) +end +local _sort=function(a,b)return a.distance_nmax +if _gotit then +self:AssignMoveCoord(_new,nil,nil,false,false,"RELOCATION MOVE AFTER FIRING") +end +end +function ARTY:GetAmmo(display) +self:F3({display=display}) +if display==nil then +display=false +end +local nammo=0 +local nshells=0 +local nrockets=0 +local nmissiles=0 +local units=self.Controllable:GetUnits() +if units==nil then +return nammo,nshells,nrockets,nmissiles +end +for _,unit in pairs(units)do +if unit and unit:IsAlive()then +local text=string.format("ARTY group %s - unit %s:\n",self.groupname,unit:GetName()) +local ammotable=unit:GetAmmo() +if ammotable~=nil then +local weapons=#ammotable +if display then +self:I(self.lid..string.format("Number of weapons %d.",weapons)) +self:I({ammotable=ammotable}) +self:I(self.lid.."Ammotable:") +for id,bla in pairs(ammotable)do +self:I({id=id,ammo=bla}) +end +end +for w=1,weapons do +local Nammo=ammotable[w]["count"] +local Tammo=ammotable[w]["desc"]["typeName"] +local _weaponString=self:_split(Tammo,"%.") +local _weaponName=_weaponString[#_weaponString] +local Category=ammotable[w].desc.category +local MissileCategory=nil +if Category==Weapon.Category.MISSILE then +MissileCategory=ammotable[w].desc.missileCategory +end +local _gotshell=false +if#self.ammoshells>0 then +for _,_type in pairs(self.ammoshells)do +if string.match(Tammo,_type)and Category==Weapon.Category.SHELL then +_gotshell=true +end +end +else +if Category==Weapon.Category.SHELL then +_gotshell=true +end +end +local _gotrocket=false +if#self.ammorockets>0 then +for _,_type in pairs(self.ammorockets)do +if string.match(Tammo,_type)and Category==Weapon.Category.ROCKET then +_gotrocket=true +end +end +else +if Category==Weapon.Category.ROCKET then +_gotrocket=true +end +end +local _gotmissile=false +if#self.ammomissiles>0 then +for _,_type in pairs(self.ammomissiles)do +if string.match(Tammo,_type)and Category==Weapon.Category.MISSILE then +_gotmissile=true +end +end +else +if Category==Weapon.Category.MISSILE then +_gotmissile=true +end +end +if _gotshell then +nshells=nshells+Nammo +text=text..string.format("- %d shells of type %s\n",Nammo,_weaponName) +elseif _gotrocket then +nrockets=nrockets+Nammo +text=text..string.format("- %d rockets of type %s\n",Nammo,_weaponName) +elseif _gotmissile then +if MissileCategory==Weapon.MissileCategory.CRUISE then +nmissiles=nmissiles+Nammo +end +text=text..string.format("- %d %s missiles of type %s\n",Nammo,self:_MissileCategoryName(MissileCategory),_weaponName) +else +text=text..string.format("- %d unknown ammo of type %s (category=%d, missile category=%s)\n",Nammo,Tammo,Category,tostring(MissileCategory)) +end +end +end +if display then +self:I(self.lid..text) +else +self:T3(self.lid..text) +end +MESSAGE:New(text,10):ToAllIf(display) +end +end +nammo=nshells+nrockets+nmissiles +return nammo,nshells,nrockets,nmissiles +end +function ARTY:_MissileCategoryName(categorynumber) +local cat="unknown" +if categorynumber==Weapon.MissileCategory.AAM then +cat="air-to-air" +elseif categorynumber==Weapon.MissileCategory.SAM then +cat="surface-to-air" +elseif categorynumber==Weapon.MissileCategory.BM then +cat="ballistic" +elseif categorynumber==Weapon.MissileCategory.ANTI_SHIP then +cat="anti-ship" +elseif categorynumber==Weapon.MissileCategory.CRUISE then +cat="cruise" +elseif categorynumber==Weapon.MissileCategory.OTHER then +cat="other" +end +return cat +end +function ARTY:_MarkerKeyAuthentification(text) +local batterycoalition=self.coalition +local mykey=nil +if self.markkey~=nil then +local keywords=self:_split(text,",") +for _,key in pairs(keywords)do +local s=self:_split(key," ") +local val=s[2] +if key:lower():find("key")then +mykey=tonumber(val) +self:T(self.lid..string.format("Authorisation Key=%s.",val)) +end +end +end +local _validkey=true +if self.markkey~=nil then +_validkey=false +if mykey~=nil then +_validkey=self.markkey==mykey +end +self:T2(self.lid..string.format("%s, authkey=%s == %s=playerkey ==> valid=%s",self.groupname,tostring(self.markkey),tostring(mykey),tostring(_validkey))) +local text="" +if mykey==nil then +text=string.format("%s, authorization required but did not receive a key!",self.alias) +elseif _validkey==false then +text=string.format("%s, authorization required but did receive an incorrect key (key=%s)!",self.alias,tostring(mykey)) +elseif _validkey==true then +text=string.format("%s, authentification successful!",self.alias) +end +MESSAGE:New(text,10):ToCoalitionIf(batterycoalition,self.report or self.Debug) +end +return _validkey +end +function ARTY:_Markertext(text) +self:F(text) +local assignment={} +assignment.battery={} +assignment.aliases={} +assignment.cluster={} +assignment.everyone=false +assignment.move=false +assignment.engage=false +assignment.request=false +assignment.cancel=false +assignment.set=false +assignment.readonly=false +assignment.movecanceltarget=false +assignment.cancelmove=false +assignment.canceltarget=false +assignment.cancelrearm=false +assignment.setrearmingplace=false +assignment.setrearminggroup=false +if text:lower():find("arty engage")or text:lower():find("arty attack")then +assignment.engage=true +elseif text:lower():find("arty move")or text:lower():find("arty relocate")then +assignment.move=true +elseif text:lower():find("arty request")then +assignment.request=true +elseif text:lower():find("arty cancel")then +assignment.cancel=true +elseif text:lower():find("arty set")then +assignment.set=true +else +self:E(self.lid..'ERROR: Neither "ARTY ENGAGE" nor "ARTY MOVE" nor "ARTY RELOCATE" nor "ARTY REQUEST" nor "ARTY CANCEL" nor "ARTY SET" keyword specified!') +return nil +end +local keywords=self:_split(text,",") +self:T({keywords=keywords}) +for _,keyphrase in pairs(keywords)do +local str=self:_split(keyphrase," ") +local key=str[1] +local val=str[2] +self:T3(self.lid..string.format("%s, keyphrase = %s, key = %s, val = %s",self.groupname,tostring(keyphrase),tostring(key),tostring(val))) +if key:lower():find("battery")then +local v=self:_split(keyphrase,'"') +for i=2,#v,2 do +table.insert(assignment.battery,v[i]) +self:T2(self.lid..string.format("Key Battery=%s.",v[i])) +end +elseif key:lower():find("alias")then +local v=self:_split(keyphrase,'"') +for i=2,#v,2 do +table.insert(assignment.aliases,v[i]) +self:T2(self.lid..string.format("Key Aliases=%s.",v[i])) +end +elseif key:lower():find("cluster")then +local v=self:_split(keyphrase,'"') +for i=2,#v,2 do +table.insert(assignment.cluster,v[i]) +self:T2(self.lid..string.format("Key Cluster=%s.",v[i])) +end +elseif keyphrase:lower():find("everyone")or keyphrase:lower():find("all batteries")or keyphrase:lower():find("allbatteries")then +assignment.everyone=true +self:T(self.lid..string.format("Key Everyone=true.")) +elseif keyphrase:lower():find("irrevocable")or keyphrase:lower():find("readonly")then +assignment.readonly=true +self:T2(self.lid..string.format("Key Readonly=true.")) +elseif(assignment.engage or assignment.move)and key:lower():find("time")then +if val:lower():find("now")then +assignment.time=self:_SecondsToClock(timer.getTime0()+2) +else +assignment.time=val +end +self:T2(self.lid..string.format("Key Time=%s.",val)) +elseif assignment.engage and key:lower():find("shot")then +assignment.nshells=tonumber(val) +self:T(self.lid..string.format("Key Shot=%s.",val)) +elseif assignment.engage and key:lower():find("prio")then +assignment.prio=tonumber(val) +self:T2(string.format("Key Prio=%s.",val)) +elseif assignment.engage and key:lower():find("maxengage")then +assignment.maxengage=tonumber(val) +self:T2(self.lid..string.format("Key Maxengage=%s.",val)) +elseif assignment.engage and key:lower():find("radius")then +assignment.radius=tonumber(val) +self:T2(self.lid..string.format("Key Radius=%s.",val)) +elseif assignment.engage and key:lower():find("weapon")then +if val:lower():find("cannon")then +assignment.weapontype=ARTY.WeaponType.Cannon +elseif val:lower():find("rocket")then +assignment.weapontype=ARTY.WeaponType.Rockets +elseif val:lower():find("missile")then +assignment.weapontype=ARTY.WeaponType.CruiseMissile +elseif val:lower():find("nuke")then +assignment.weapontype=ARTY.WeaponType.TacticalNukes +elseif val:lower():find("illu")then +assignment.weapontype=ARTY.WeaponType.IlluminationShells +elseif val:lower():find("smoke")then +assignment.weapontype=ARTY.WeaponType.SmokeShells +else +assignment.weapontype=ARTY.WeaponType.Auto +end +self:T2(self.lid..string.format("Key Weapon=%s.",val)) +elseif(assignment.move or assignment.set)and key:lower():find("speed")then +assignment.speed=tonumber(val) +self:T2(self.lid..string.format("Key Speed=%s.",val)) +elseif(assignment.move or assignment.set)and(keyphrase:lower():find("on road")or keyphrase:lower():find("onroad")or keyphrase:lower():find("use road"))then +assignment.onroad=true +self:T2(self.lid..string.format("Key Onroad=true.")) +elseif assignment.move and(keyphrase:lower():find("cancel target")or keyphrase:lower():find("canceltarget"))then +assignment.movecanceltarget=true +self:T2(self.lid..string.format("Key Cancel Target (before move)=true.")) +elseif assignment.request and keyphrase:lower():find("rearm")then +assignment.requestrearming=true +self:T2(self.lid..string.format("Key Request Rearming=true.")) +elseif assignment.request and keyphrase:lower():find("ammo")then +assignment.requestammo=true +self:T2(self.lid..string.format("Key Request Ammo=true.")) +elseif assignment.request and keyphrase:lower():find("target")then +assignment.requesttargets=true +self:T2(self.lid..string.format("Key Request Targets=true.")) +elseif assignment.request and keyphrase:lower():find("status")then +assignment.requeststatus=true +self:T2(self.lid..string.format("Key Request Status=true.")) +elseif assignment.request and(keyphrase:lower():find("move")or keyphrase:lower():find("relocation"))then +assignment.requestmoves=true +self:T2(self.lid..string.format("Key Request Moves=true.")) +elseif assignment.cancel and(keyphrase:lower():find("engagement")or keyphrase:lower():find("attack")or keyphrase:lower():find("target"))then +assignment.canceltarget=true +self:T2(self.lid..string.format("Key Cancel Target=true.")) +elseif assignment.cancel and(keyphrase:lower():find("move")or keyphrase:lower():find("relocation"))then +assignment.cancelmove=true +self:T2(self.lid..string.format("Key Cancel Move=true.")) +elseif assignment.cancel and keyphrase:lower():find("rearm")then +assignment.cancelrearm=true +self:T2(self.lid..string.format("Key Cancel Rearm=true.")) +elseif assignment.set and keyphrase:lower():find("rearming place")then +assignment.setrearmingplace=true +self:T(self.lid..string.format("Key Set Rearming Place=true.")) +elseif assignment.set and keyphrase:lower():find("rearming group")then +local v=self:_split(keyphrase,'"') +local groupname=v[2] +local group=GROUP:FindByName(groupname) +if group and group:IsAlive()then +assignment.setrearminggroup=group +end +self:T2(self.lid..string.format("Key Set Rearming Group = %s.",tostring(groupname))) +elseif key:lower():find("lldms")then +local _flat="%d+:%d+:%d+%s*[N,S]" +local _flon="%d+:%d+:%d+%s*[W,E]" +local _lat=keyphrase:match(_flat) +local _lon=keyphrase:match(_flon) +self:T2(self.lid..string.format("Key LLDMS: lat=%s, long=%s format=DMS",_lat,_lon)) +if _lat and _lon then +local _latitude,_longitude=self:_LLDMS2DD(_lat,_lon) +self:T2(self.lid..string.format("Key LLDMS: lat=%.3f, long=%.3f format=DD",_latitude,_longitude)) +if _latitude and _longitude then +assignment.coord=COORDINATE:NewFromLLDD(_latitude,_longitude) +end +end +end +end +return assignment +end +function ARTY:_MarkRequestAmmo() +self:GetAmmo(true) +end +function ARTY:_MarkRequestStatus() +self:_StatusReport(true) +end +function ARTY:_MarkRequestMoves() +local text=string.format("%s, relocations:",self.groupname) +if#self.moves>0 then +for _,move in pairs(self.moves)do +if self.currentMove and move.name==self.currentMove.name then +text=text..string.format("\n- %s (current)",self:_MoveInfo(move)) +else +text=text..string.format("\n- %s",self:_MoveInfo(move)) +end +end +else +text=text..string.format("\n- no queued relocations") +end +MESSAGE:New(text,20):Clear():ToCoalition(self.coalition) +end +function ARTY:_MarkRequestTargets() +local text=string.format("%s, targets:",self.groupname) +if#self.targets>0 then +for _,target in pairs(self.targets)do +if self.currentTarget and target.name==self.currentTarget.name then +text=text..string.format("\n- %s (current)",self:_TargetInfo(target)) +else +text=text..string.format("\n- %s",self:_TargetInfo(target)) +end +end +else +text=text..string.format("\n- no queued targets") +end +MESSAGE:New(text,20):Clear():ToCoalition(self.coalition) +end +function ARTY:_MarkTargetName(markerid) +return string.format("BATTERY=%s, Marked Target ID=%d",self.groupname,markerid) +end +function ARTY:_MarkMoveName(markerid) +return string.format("BATTERY=%s, Marked Relocation ID=%d",self.groupname,markerid) +end +function ARTY:_GetMarkIDfromName(name) +local keywords=self:_split(name,",") +local battery=nil +local markTID=nil +local markMID=nil +for _,key in pairs(keywords)do +local str=self:_split(key,"=") +local par=str[1] +local val=str[2] +if par:find("BATTERY")then +battery=val +end +if par:find("Marked Target ID")then +markTID=tonumber(val) +end +if par:find("Marked Relocation ID")then +markMID=tonumber(val) +end +end +return battery,markTID,markMID +end +function ARTY:_SortTargetQueuePrio() +self:F2() +local function _sort(a,b) +return(a.engaged_target.engaged and self:_TargetInRange(_target)and self:_CheckWeaponTypeAvailable(_target)>0 then +self:T2(self.lid..string.format("Found NORMAL target %s",self:_TargetInfo(_target))) +return _target +end +end +return nil +end +function ARTY:_CheckTimedTargets() +self:F3() +local Tnow=timer.getAbsTime() +self:_SortQueueTime(self.targets) +if self:is("Rearming")then +return nil +end +for i=1,#self.targets do +local _target=self.targets[i] +self:T3(self.lid..string.format("Check TIMED target %d: %s",i,self:_TargetInfo(_target))) +if _target.time and Tnow>=_target.time and _target.underfire==false and self:_TargetInRange(_target)and self:_CheckWeaponTypeAvailable(_target)>0 then +if self.currentTarget then +if self.currentTarget.prio>_target.prio then +self:T2(self.lid..string.format("Found TIMED HIGH PRIO target %s.",self:_TargetInfo(_target))) +return _target +end +else +self:T2(self.lid..string.format("Found TIMED target %s.",self:_TargetInfo(_target))) +return _target +end +end +end +return nil +end +function ARTY:_CheckMoves() +self:F3() +local Tnow=timer.getAbsTime() +self:_SortQueueTime(self.moves) +local firing=false +if self.currentTarget then +firing=true +end +for i=1,#self.moves do +local _move=self.moves[i] +if string.find(_move.name,"REARMING MOVE")and((self.currentMove and self.currentMove.name~=_move.name)or self.currentMove==nil)then +return _move +elseif(Tnow>=_move.time)and(firing==false or _move.cancel)and(not self.currentMove)and(not self:is("Rearming"))then +return _move +end +end +return nil +end +function ARTY:_CheckShootingStarted() +self:F2() +if self.currentTarget then +local Tnow=timer.getTime() +local name=self.currentTarget.name +local dt=Tnow-self.currentTarget.Tassigned +if self.Nshots==0 then +self:T(self.lid..string.format("%s, waiting for %d seconds for first shot on target %s.",self.groupname,dt,name)) +end +if dt>self.WaitForShotTime and(self.Nshots==0 or self.currentTarget.nshells>=self.Nshots)then +self:T(self.lid..string.format("%s, no shot event after %d seconds. Removing current target %s from list.",self.groupname,self.WaitForShotTime,name)) +self:CeaseFire(self.currentTarget) +self:RemoveTarget(name) +end +end +end +function ARTY:_GetTargetIndexByName(name) +self:F2(name) +for i=1,#self.targets do +local targetname=self.targets[i].name +self:T3(self.lid..string.format("Have target with name %s. Index = %d",targetname,i)) +if targetname==name then +self:T2(self.lid..string.format("Found target with name %s. Index = %d",name,i)) +return i +end +end +self:T2(self.lid..string.format("WARNING: Target with name %s could not be found. (This can happen.)",name)) +return nil +end +function ARTY:_GetMoveIndexByName(name) +self:F2(name) +for i=1,#self.moves do +local movename=self.moves[i].name +self:T3(self.lid..string.format("Have move with name %s. Index = %d",movename,i)) +if movename==name then +self:T2(self.lid..string.format("Found move with name %s. Index = %d",name,i)) +return i +end +end +self:T2(self.lid..string.format("WARNING: Move with name %s could not be found. (This can happen.)",name)) +return nil +end +function ARTY:_CheckOutOfAmmo(targets) +local _nammo,_nshells,_nrockets,_nmissiles=self:GetAmmo() +local _partlyoutofammo=false +for _,Target in pairs(targets)do +if Target.weapontype==ARTY.WeaponType.Auto and _nammo==0 then +self:T(self.lid..string.format("Group %s, auto weapon requested for target %s but all ammo is empty.",self.groupname,Target.name)) +_partlyoutofammo=true +elseif Target.weapontype==ARTY.WeaponType.Cannon and _nshells==0 then +self:T(self.lid..string.format("Group %s, cannons requested for target %s but shells empty.",self.groupname,Target.name)) +_partlyoutofammo=true +elseif Target.weapontype==ARTY.WeaponType.TacticalNukes and self.Nukes<=0 then +self:T(self.lid..string.format("Group %s, tactical nukes requested for target %s but nukes empty.",self.groupname,Target.name)) +_partlyoutofammo=true +elseif Target.weapontype==ARTY.WeaponType.IlluminationShells and self.Nillu<=0 then +self:T(self.lid..string.format("Group %s, illumination shells requested for target %s but illumination shells empty.",self.groupname,Target.name)) +_partlyoutofammo=true +elseif Target.weapontype==ARTY.WeaponType.SmokeShells and self.Nsmoke<=0 then +self:T(self.lid..string.format("Group %s, smoke shells requested for target %s but smoke shells empty.",self.groupname,Target.name)) +_partlyoutofammo=true +elseif Target.weapontype==ARTY.WeaponType.Rockets and _nrockets==0 then +self:T(self.lid..string.format("Group %s, rockets requested for target %s but rockets empty.",self.groupname,Target.name)) +_partlyoutofammo=true +elseif Target.weapontype==ARTY.WeaponType.CruiseMissile and _nmissiles==0 then +self:T(self.lid..string.format("Group %s, cruise missiles requested for target %s but all missiles empty.",self.groupname,Target.name)) +_partlyoutofammo=true +end +end +return _partlyoutofammo +end +function ARTY:_CheckWeaponTypeAvailable(target) +local Nammo,Nshells,Nrockets,Nmissiles=self:GetAmmo() +local nfire=Nammo +if target.weapontype==ARTY.WeaponType.Auto then +nfire=Nammo +elseif target.weapontype==ARTY.WeaponType.Cannon then +nfire=Nshells +elseif target.weapontype==ARTY.WeaponType.TacticalNukes then +nfire=self.Nukes +elseif target.weapontype==ARTY.WeaponType.IlluminationShells then +nfire=self.Nillu +elseif target.weapontype==ARTY.WeaponType.SmokeShells then +nfire=self.Nsmoke +elseif target.weapontype==ARTY.WeaponType.Rockets then +nfire=Nrockets +elseif target.weapontype==ARTY.WeaponType.CruiseMissile then +nfire=Nmissiles +end +return nfire +end +function ARTY:_CheckWeaponTypePossible(target) +local possible=false +if target.weapontype==ARTY.WeaponType.Auto then +possible=self.Nammo0>0 +elseif target.weapontype==ARTY.WeaponType.Cannon then +possible=self.Nshells0>0 +elseif target.weapontype==ARTY.WeaponType.TacticalNukes then +possible=self.Nukes0>0 +elseif target.weapontype==ARTY.WeaponType.IlluminationShells then +possible=self.Nillu0>0 +elseif target.weapontype==ARTY.WeaponType.SmokeShells then +possible=self.Nsmoke0>0 +elseif target.weapontype==ARTY.WeaponType.Rockets then +possible=self.Nrockets0>0 +elseif target.weapontype==ARTY.WeaponType.CruiseMissile then +possible=self.Nmissiles0>0 +end +return possible +end +function ARTY:_CheckName(givennames,name,makeunique) +self:F2({givennames=givennames,name=name}) +local newname=name +local counter=1 +local n=1 +local nmax=100 +if makeunique==nil then +makeunique=true +end +repeat +local _unique=true +for _,_target in pairs(givennames)do +local _givenname=_target.name +if _givenname==newname then +_unique=false +end +self:T3(self.lid..string.format("%d: givenname = %s, newname=%s, unique = %s, makeunique = %s",n,tostring(_givenname),newname,tostring(_unique),tostring(makeunique))) +end +if _unique==false and makeunique==true then +newname=string.format("%s #%02d",name,counter) +counter=counter+1 +end +if _unique==false and makeunique==false then +self:T3(self.lid..string.format("Name %s is not unique. Return false.",tostring(newname))) +return name,false +end +n=n+1 +until(_unique or n==nmax) +self:T3(self.lid..string.format("Original name %s, new name = %s",name,newname)) +return newname,true +end +function ARTY:_TargetInRange(target,message) +self:F3(target) +if message==nil then +message=false +end +self:T3({controllable=self.Controllable,targetcoord=target.coord}) +local _dist=self.Controllable:GetCoordinate():Get2DDistance(target.coord) +local _inrange=true +local _tooclose=false +local _toofar=false +local text="" +if _distself.maxrange then +_inrange=false +_toofar=true +text=string.format("%s, target is out of range. Distance of %.1f km is greater than max range of %.1f km.",self.alias,_dist/1000,self.maxrange/1000) +end +if not _inrange then +self:T(self.lid..text) +MESSAGE:New(text,5):ToCoalitionIf(self.coalition,(self.report and message)or(self.Debug and message)) +end +local _remove=false +if not(self.ismobile or self.iscargo)and _inrange==false then +_remove=true +end +return _inrange,_toofar,_tooclose,_remove +end +function ARTY:_WeaponTypeName(tnumber) +self:F2(tnumber) +local name="unknown" +if tnumber==ARTY.WeaponType.Auto then +name="Auto" +elseif tnumber==ARTY.WeaponType.Cannon then +name="Cannons" +elseif tnumber==ARTY.WeaponType.Rockets then +name="Rockets" +elseif tnumber==ARTY.WeaponType.CruiseMissile then +name="Cruise Missiles" +elseif tnumber==ARTY.WeaponType.TacticalNukes then +name="Tactical Nukes" +elseif tnumber==ARTY.WeaponType.IlluminationShells then +name="Illumination Shells" +elseif tnumber==ARTY.WeaponType.SmokeShells then +name="Smoke Shells" +end +return name +end +function ARTY:_VicinityCoord(coord,rmin,rmax) +self:F2({coord=coord,rmin=rmin,rmax=rmax}) +rmin=rmin or 20 +rmax=rmax or 80 +local vec2=coord:GetRandomVec2InRadius(rmax,rmin) +local pops=COORDINATE:NewFromVec2(vec2) +self:T3(self.lid..string.format("Vicinity distance = %d (rmin=%d, rmax=%d)",pops:Get2DDistance(coord),rmin,rmax)) +return pops +end +function ARTY:_EventFromTo(BA,Event,From,To) +local text=string.format("%s: %s EVENT %s: %s --> %s",BA,self.groupname,Event,From,To) +self:T3(self.lid..text) +end +function ARTY:_split(str,sep) +self:F3({str=str,sep=sep}) +local result={} +local regex=("([^%s]+)"):format(sep) +for each in str:gmatch(regex)do +table.insert(result,each) +end +return result +end +function ARTY:_TargetInfo(target) +local clock=tostring(self:_SecondsToClock(target.time)) +local weapon=self:_WeaponTypeName(target.weapontype) +local _underfire=tostring(target.underfire) +return string.format("%s: prio=%d, radius=%d, nshells=%d, engaged=%d/%d, weapontype=%s, time=%s, underfire=%s, attackgroup=%s", +target.name,target.prio,target.radius,target.nshells,target.engaged,target.maxengage,weapon,clock,_underfire,tostring(target.attackgroup)) +end +function ARTY:_MoveInfo(move) +self:F3(move) +local _clock=self:_SecondsToClock(move.time) +return string.format("%s: time=%s, speed=%d, onroad=%s, cancel=%s",move.name,_clock,move.speed,tostring(move.onroad),tostring(move.cancel)) +end +function ARTY:_LLDMS2DD(l1,l2) +self:F2(l1,l2) +local _latlong={l1,l2} +local _latitude=nil +local _longitude=nil +for _,ll in pairs(_latlong)do +local _format="%d+:%d+:%d+" +local _ldms=ll:match(_format) +if _ldms then +local _dms=self:_split(_ldms,":") +local _deg=tonumber(_dms[1]) +local _min=tonumber(_dms[2]) +local _sec=tonumber(_dms[3]) +local function DMS2DD(d,m,s) +return d+m/60+s/3600 +end +if ll:match("N")then +_latitude=DMS2DD(_deg,_min,_sec) +elseif ll:match("S")then +_latitude=-DMS2DD(_deg,_min,_sec) +elseif ll:match("W")then +_longitude=-DMS2DD(_deg,_min,_sec) +elseif ll:match("E")then +_longitude=DMS2DD(_deg,_min,_sec) +end +local text=string.format("DMS %02d Deg %02d min %02d sec",_deg,_min,_sec) +self:T2(self.lid..text) +end +end +local text=string.format("\nLatitude %s",tostring(_latitude)) +text=text..string.format("\nLongitude %s",tostring(_longitude)) +self:T2(self.lid..text) +return _latitude,_longitude +end +function ARTY:_SecondsToClock(seconds) +self:F3({seconds=seconds}) +if seconds==nil then +return nil +end +local seconds=tonumber(seconds) +local _seconds=seconds%(60*60*24) +if seconds<=0 then +return nil +else +local hours=string.format("%02.f",math.floor(_seconds/3600)) +local mins=string.format("%02.f",math.floor(_seconds/60-(hours*60))) +local secs=string.format("%02.f",math.floor(_seconds-hours*3600-mins*60)) +local days=string.format("%d",seconds/(60*60*24)) +return hours..":"..mins..":"..secs.."+"..days +end +end +function ARTY:_ClockToSeconds(clock) +self:F3({clock=clock}) +if clock==nil then +return nil +end +local seconds=0 +local dsplit=self:_split(clock,"+") +if#dsplit>1 then +seconds=seconds+tonumber(dsplit[2])*60*60*24 +end +local tsplit=self:_split(dsplit[1],":") +local i=1 +for _,time in ipairs(tsplit)do +if i==1 then +seconds=seconds+tonumber(time)*60*60 +elseif i==2 then +seconds=seconds+tonumber(time)*60 +elseif i==3 then +seconds=seconds+tonumber(time) +end +i=i+1 +end +self:T3(self.lid..string.format("Clock %s = %d seconds",clock,seconds)) +return seconds +end +SUPPRESSION={ +ClassName="SUPPRESSION", +Debug=false, +lid=nil, +flare=false, +smoke=false, +DCSdesc=nil, +Type=nil, +IsInfantry=nil, +SpeedMax=nil, +Tsuppress_ave=15, +Tsuppress_min=5, +Tsuppress_max=25, +TsuppressOver=nil, +IniGroupStrength=nil, +Nhit=0, +Formation="Off road", +Speed=4, +MenuON=false, +FallbackON=false, +FallbackWait=60, +FallbackDist=100, +FallbackHeading=nil, +TakecoverON=false, +TakecoverWait=120, +TakecoverRange=300, +hideout=nil, +PminFlee=10, +PmaxFlee=90, +RetreatZone=nil, +RetreatDamage=nil, +RetreatWait=7200, +CurrentAlarmState="unknown", +CurrentROE="unknown", +DefaultAlarmState="Auto", +DefaultROE="Weapon Free", +eventmoose=true, +} +SUPPRESSION.ROE={ +Hold="Weapon Hold", +Free="Weapon Free", +Return="Return Fire", +} +SUPPRESSION.AlarmState={ +Auto="Auto", +Green="Green", +Red="Red", +} +SUPPRESSION.MenuF10=nil +SUPPRESSION.version="0.9.3" +function SUPPRESSION:New(group) +local self=BASE:Inherit(self,FSM_CONTROLLABLE:New()) +if group then +self.lid=string.format("SUPPRESSION %s | ",tostring(group:GetName())) +self:T(self.lid..string.format("SUPPRESSION version %s. Activating suppressive fire for group %s",SUPPRESSION.version,group:GetName())) +else +self:E(self.lid.."SUPPRESSION | Requested group does not exist! (Has to be a MOOSE group.)") +return nil +end +if group:IsGround()==false then +self:E(self.lid..string.format("SUPPRESSION fire group %s has to be a GROUND group!",group:GetName())) +return nil +end +self:SetControllable(group) +self.DCSdesc=group:GetDCSDesc(1) +self.SpeedMax=group:GetSpeedMax() +self.Speed=self.SpeedMax +self.IsInfantry=group:GetUnit(1):HasAttribute("Infantry") +self.Type=group:GetTypeName() +self.IniGroupStrength=#group:GetUnits() +self:SetDefaultROE("Free") +self:SetDefaultAlarmState("Auto") +self:AddTransition("*","Start","CombatReady") +self:AddTransition("*","Status","*") +self:AddTransition("CombatReady","Hit","Suppressed") +self:AddTransition("Suppressed","Hit","Suppressed") +self:AddTransition("Suppressed","Recovered","CombatReady") +self:AddTransition("Suppressed","TakeCover","TakingCover") +self:AddTransition("Suppressed","FallBack","FallingBack") +self:AddTransition("*","Retreat","Retreating") +self:AddTransition("TakingCover","FightBack","CombatReady") +self:AddTransition("FallingBack","FightBack","CombatReady") +self:AddTransition("Retreating","Retreated","Retreated") +self:AddTransition("*","OutOfAmmo","*") +self:AddTransition("*","Dead","*") +self:AddTransition("*","Stop","Stopped") +self:AddTransition("TakingCover","Hit","TakingCover") +self:AddTransition("FallingBack","Hit","FallingBack") +return self +end +function SUPPRESSION:SetSuppressionTime(Tave,Tmin,Tmax) +self:F({Tave=Tave,Tmin=Tmin,Tmax=Tmax}) +self.Tsuppress_min=Tmin or self.Tsuppress_min +self.Tsuppress_min=math.max(self.Tsuppress_min,1) +self.Tsuppress_max=Tmax or self.Tsuppress_max +self.Tsuppress_max=math.max(self.Tsuppress_max,self.Tsuppress_min) +self.Tsuppress_ave=Tave or self.Tsuppress_ave +self.Tsuppress_ave=math.max(self.Tsuppress_min) +self.Tsuppress_ave=math.min(self.Tsuppress_max) +self:T(self.lid..string.format("Set ave suppression time to %d seconds.",self.Tsuppress_ave)) +self:T(self.lid..string.format("Set min suppression time to %d seconds.",self.Tsuppress_min)) +self:T(self.lid..string.format("Set max suppression time to %d seconds.",self.Tsuppress_max)) +end +function SUPPRESSION:SetRetreatZone(zone) +self:F({zone=zone}) +self.RetreatZone=zone +end +function SUPPRESSION:DebugOn() +self:F() +self.Debug=true +end +function SUPPRESSION:FlareOn() +self:F() +self.flare=true +end +function SUPPRESSION:SmokeOn() +self:F() +self.smoke=true +end +function SUPPRESSION:SetFormation(formation) +self:F(formation) +self.Formation=formation or"Vee" +end +function SUPPRESSION:SetSpeed(speed) +self:F(speed) +self.Speed=speed or self.SpeedMax +self.Speed=math.min(self.Speed,self.SpeedMax) +end +function SUPPRESSION:Fallback(switch) +self:F(switch) +if switch==nil then +switch=true +end +self.FallbackON=switch +end +function SUPPRESSION:SetFallbackDistance(distance) +self:F(distance) +self.FallbackDist=distance +end +function SUPPRESSION:SetFallbackWait(time) +self:F(time) +self.FallbackWait=time +end +function SUPPRESSION:Takecover(switch) +self:F(switch) +if switch==nil then +switch=true +end +self.TakecoverON=switch +end +function SUPPRESSION:SetTakecoverWait(time) +self:F(time) +self.TakecoverWait=time +end +function SUPPRESSION:SetTakecoverRange(range) +self:F(range) +self.TakecoverRange=range +end +function SUPPRESSION:SetTakecoverPlace(Hideout) +self.hideout=Hideout +end +function SUPPRESSION:SetMinimumFleeProbability(probability) +self:F(probability) +self.PminFlee=probability or 10 +end +function SUPPRESSION:SetMaximumFleeProbability(probability) +self:F(probability) +self.PmaxFlee=probability or 90 +end +function SUPPRESSION:SetRetreatDamage(damage) +self:F(damage) +self.RetreatDamage=damage or 50 +end +function SUPPRESSION:SetRetreatWait(time) +self:F(time) +self.RetreatWait=time or 7200 +end +function SUPPRESSION:SetDefaultAlarmState(alarmstate) +self:F(alarmstate) +if alarmstate:lower()=="auto"then +self.DefaultAlarmState=SUPPRESSION.AlarmState.Auto +elseif alarmstate:lower()=="green"then +self.DefaultAlarmState=SUPPRESSION.AlarmState.Green +elseif alarmstate:lower()=="red"then +self.DefaultAlarmState=SUPPRESSION.AlarmState.Red +else +self.DefaultAlarmState=SUPPRESSION.AlarmState.Auto +end +end +function SUPPRESSION:SetDefaultROE(roe) +self:F(roe) +if roe:lower()=="free"then +self.DefaultROE=SUPPRESSION.ROE.Free +elseif roe:lower()=="hold"then +self.DefaultROE=SUPPRESSION.ROE.Hold +elseif roe:lower()=="return"then +self.DefaultROE=SUPPRESSION.ROE.Return +else +self.DefaultROE=SUPPRESSION.ROE.Free +end +end +function SUPPRESSION:MenuOn(switch) +self:F(switch) +if switch==nil then +switch=true +end +self.MenuON=switch +end +function SUPPRESSION:_CreateMenuGroup() +local SubMenuName=self.Controllable:GetName() +local MenuGroup=MENU_MISSION:New(SubMenuName,SUPPRESSION.MenuF10) +MENU_MISSION_COMMAND:New("Fallback!",MenuGroup,self.OrderFallBack,self) +MENU_MISSION_COMMAND:New("Take Cover!",MenuGroup,self.OrderTakeCover,self) +MENU_MISSION_COMMAND:New("Retreat!",MenuGroup,self.OrderRetreat,self) +MENU_MISSION_COMMAND:New("Report Status",MenuGroup,self.Status,self,true) +end +function SUPPRESSION:OrderFallBack() +local group=self.Controllable +local vicinity=group:GetCoordinate():GetRandomVec2InRadius(150,100) +local coord=COORDINATE:NewFromVec2(vicinity) +self:FallBack(self.Controllable) +end +function SUPPRESSION:OrderTakeCover() +local Hideout=self.hideout +if self.hideout==nil then +Hideout=self:_SearchHideout() +end +self:TakeCover(Hideout) +end +function SUPPRESSION:OrderRetreat() +self:Retreat() +end +function SUPPRESSION:StatusReport(message) +local group=self.Controllable +local nunits=group:CountAliveUnits() +local roe=self.CurrentROE +local state=self.CurrentAlarmState +local life_min,life_max,life_ave,life_ave0,groupstrength=self:_GetLife() +local ammotot=group:GetAmmunition() +local detectedG=group:GetDetectedGroupSet():CountAlive() +local detectedU=group:GetDetectedUnitSet():Count() +local text=string.format("State %s, Units=%d/%d, ROE=%s, AlarmState=%s, Hits=%d, Life(min/max/ave/ave0)=%d/%d/%d/%d, Total Ammo=%d, Detected=%d/%d", +self:GetState(),nunits,self.IniGroupStrength,self.CurrentROE,self.CurrentAlarmState,self.Nhit,life_min,life_max,life_ave,life_ave0,ammotot,detectedG,detectedU) +MESSAGE:New(text,10):ToAllIf(message or self.Debug) +self:I(self.lid..text) +end +function SUPPRESSION:onafterStart(Controllable,From,Event,To) +self:_EventFromTo("onafterStart",Event,From,To) +local text=string.format("Started SUPPRESSION for group %s.",Controllable:GetName()) +self:I(self.lid..text) +MESSAGE:New(text,10):ToAllIf(self.Debug) +local rzone="not defined" +if self.RetreatZone then +rzone=self.RetreatZone:GetName() +end +if self.RetreatDamage==nil then +if self.RetreatZone then +if self.IniGroupStrength==1 then +self.RetreatDamage=60.0 +elseif self.IniGroupStrength==2 then +self.RetreatDamage=50.0 +else +self.RetreatDamage=66.5 +end +else +self.RetreatDamage=100 +end +end +if self.MenuON then +if not SUPPRESSION.MenuF10 then +SUPPRESSION.MenuF10=MENU_MISSION:New("Suppression") +end +self:_CreateMenuGroup() +end +self:_SetAlarmState(self.DefaultAlarmState) +self:_SetROE(self.DefaultROE) +local text=string.format("\n******************************************************\n") +text=text..string.format("Suppressed group = %s\n",Controllable:GetName()) +text=text..string.format("Type = %s\n",self.Type) +text=text..string.format("IsInfantry = %s\n",tostring(self.IsInfantry)) +text=text..string.format("Group strength = %d\n",self.IniGroupStrength) +text=text..string.format("Average time = %5.1f seconds\n",self.Tsuppress_ave) +text=text..string.format("Minimum time = %5.1f seconds\n",self.Tsuppress_min) +text=text..string.format("Maximum time = %5.1f seconds\n",self.Tsuppress_max) +text=text..string.format("Default ROE = %s\n",self.DefaultROE) +text=text..string.format("Default AlarmState = %s\n",self.DefaultAlarmState) +text=text..string.format("Fall back ON = %s\n",tostring(self.FallbackON)) +text=text..string.format("Fall back distance = %5.1f m\n",self.FallbackDist) +text=text..string.format("Fall back wait = %5.1f seconds\n",self.FallbackWait) +text=text..string.format("Fall back heading = %s degrees\n",tostring(self.FallbackHeading)) +text=text..string.format("Take cover ON = %s\n",tostring(self.TakecoverON)) +text=text..string.format("Take cover search = %5.1f m\n",self.TakecoverRange) +text=text..string.format("Take cover wait = %5.1f seconds\n",self.TakecoverWait) +text=text..string.format("Min flee probability = %5.1f\n",self.PminFlee) +text=text..string.format("Max flee probability = %5.1f\n",self.PmaxFlee) +text=text..string.format("Retreat zone = %s\n",rzone) +text=text..string.format("Retreat damage = %5.1f %%\n",self.RetreatDamage) +text=text..string.format("Retreat wait = %5.1f seconds\n",self.RetreatWait) +text=text..string.format("Speed = %5.1f km/h\n",self.Speed) +text=text..string.format("Speed max = %5.1f km/h\n",self.SpeedMax) +text=text..string.format("Formation = %s\n",self.Formation) +text=text..string.format("******************************************************\n") +self:T(self.lid..text) +if self.eventmoose then +self:HandleEvent(EVENTS.Hit,self._OnEventHit) +self:HandleEvent(EVENTS.Dead,self._OnEventDead) +else +world.addEventHandler(self) +end +self:__Status(-1) +end +function SUPPRESSION:onafterStatus(Controllable,From,Event,To) +local group=self.Controllable +if group then +local nunits=group:CountAliveUnits() +if nunits>0 then +local nammo=group:GetAmmunition() +if nammo==0 then +self:OutOfAmmo() +end +self:StatusReport(false) +if self:GetState()~="Stopped"then +self:__Status(-30) +end +else +self:Stop() +end +else +self:Stop() +end +end +function SUPPRESSION:onafterHit(Controllable,From,Event,To,Unit,AttackUnit) +self:_EventFromTo("onafterHit",Event,From,To) +if From=="CombatReady"or From=="Suppressed"then +self:_Suppress() +end +local life_min,life_max,life_ave,life_ave0,groupstrength=self:_GetLife() +local Damage=100-life_ave0 +local RetreatCondition=Damage>=self.RetreatDamage-0.01 and self.RetreatZone +local Pflee=(self.PmaxFlee-self.PminFlee)/self.RetreatDamage*math.min(Damage,self.RetreatDamage)+self.PminFlee +local P=math.random(0,100) +local FleeCondition=P Prand ==> Flee)\n",Controllable:GetName(),Pflee,P) +self:T(self.lid..text) +if Damage>=99.9 then +return +end +if RetreatCondition then +self:Retreat() +elseif FleeCondition then +if self.FallbackON and AttackUnit:IsGround()then +self:FallBack(AttackUnit) +elseif self.TakecoverON then +local Hideout=self.hideout +if self.hideout==nil then +Hideout=self:_SearchHideout() +end +self:TakeCover(Hideout) +end +end +end +function SUPPRESSION:onbeforeRecovered(Controllable,From,Event,To) +self:_EventFromTo("onbeforeRecovered",Event,From,To) +local Tnow=timer.getTime() +self:T(self.lid..string.format("onbeforeRecovered: Time now: %d - Time over: %d",Tnow,self.TsuppressionOver)) +if Tnow>=self.TsuppressionOver then +return true +else +return false +end +end +function SUPPRESSION:onafterRecovered(Controllable,From,Event,To) +self:_EventFromTo("onafterRecovered",Event,From,To) +if Controllable and Controllable:IsAlive()then +local text=string.format("Group %s has recovered!",Controllable:GetName()) +MESSAGE:New(text,10):ToAllIf(self.Debug) +self:T(self.lid..text) +self:_SetROE() +if self.flare or self.Debug then +Controllable:FlareGreen() +end +end +end +function SUPPRESSION:onafterFightBack(Controllable,From,Event,To) +self:_EventFromTo("onafterFightBack",Event,From,To) +self:_SetROE() +self:_SetAlarmState() +end +function SUPPRESSION:onbeforeFallBack(Controllable,From,Event,To,AttackUnit) +self:_EventFromTo("onbeforeFallBack",Event,From,To) +if From=="FallingBack"then +return false +else +return true +end +end +function SUPPRESSION:onafterFallBack(Controllable,From,Event,To,AttackUnit) +self:_EventFromTo("onafterFallback",Event,From,To) +self:T(self.lid..string.format("Group %s is falling back after %d hits.",Controllable:GetName(),self.Nhit)) +local ACoord=AttackUnit:GetCoordinate() +local DCoord=Controllable:GetCoordinate() +local heading=self:_Heading(ACoord,DCoord) +if self.FallbackHeading then +heading=self.FallbackHeading +end +local Coord=DCoord:Translate(self.FallbackDist,heading) +if self.Debug then +local MarkerID=Coord:MarkToAll("Fall back position for group "..Controllable:GetName()) +end +if self.smoke or self.Debug then +Coord:SmokeBlue() +end +self:_SetROE(SUPPRESSION.ROE.Hold) +self:_SetAlarmState(SUPPRESSION.AlarmState.Green) +self:_Run(Coord,self.Speed,self.Formation,self.FallbackWait) +end +function SUPPRESSION:onbeforeTakeCover(Controllable,From,Event,To,Hideout) +self:_EventFromTo("onbeforeTakeCover",Event,From,To) +if From=="TakingCover"then +return false +end +if Hideout~=nil then +return true +else +return false +end +end +function SUPPRESSION:onafterTakeCover(Controllable,From,Event,To,Hideout) +self:_EventFromTo("onafterTakeCover",Event,From,To) +if self.Debug then +local MarkerID=Hideout:MarkToAll(string.format("Hideout for group %s",Controllable:GetName())) +end +if self.smoke or self.Debug then +Hideout:SmokeBlue() +end +self:_SetROE(SUPPRESSION.ROE.Hold) +self:_SetAlarmState(SUPPRESSION.AlarmState.Green) +self:_Run(Hideout,self.Speed,self.Formation,self.TakecoverWait) +end +function SUPPRESSION:onafterOutOfAmmo(Controllable,From,Event,To) +self:_EventFromTo("onafterOutOfAmmo",Event,From,To) +self:I(self.lid..string.format("Out of ammo!")) +if self.RetreatZone then +self:Retreat() +end +end +function SUPPRESSION:onbeforeRetreat(Controllable,From,Event,To) +self:_EventFromTo("onbeforeRetreat",Event,From,To) +if From=="Retreating"then +local text=string.format("Group %s is already retreating.",tostring(Controllable:GetName())) +self:T2(self.lid..text) +return false +else +return true +end +end +function SUPPRESSION:onafterRetreat(Controllable,From,Event,To) +self:_EventFromTo("onafterRetreat",Event,From,To) +local text=string.format("Group %s is retreating! Alarm state green.",Controllable:GetName()) +MESSAGE:New(text,10):ToAllIf(self.Debug) +self:T(self.lid..text) +local ZoneCoord=self.RetreatZone:GetRandomCoordinate() +local ZoneVec2=ZoneCoord:GetVec2() +if self.smoke or self.Debug then +ZoneCoord:SmokeBlue() +end +if self.Debug then +self.RetreatZone:SmokeZone(SMOKECOLOR.Red,12) +end +self:_SetROE(SUPPRESSION.ROE.Hold) +self:_SetAlarmState(SUPPRESSION.AlarmState.Green) +self:_Run(ZoneCoord,self.Speed,self.Formation,self.RetreatWait) +end +function SUPPRESSION:onbeforeRetreated(Controllable,From,Event,To) +self:_EventFromTo("onbeforeRetreated",Event,From,To) +local inzone=self.RetreatZone:IsVec3InZone(Controllable:GetVec3()) +return inzone +end +function SUPPRESSION:onafterRetreated(Controllable,From,Event,To) +self:_EventFromTo("onafterRetreated",Event,From,To) +self:_SetROE(SUPPRESSION.ROE.Return) +self:_SetAlarmState(SUPPRESSION.AlarmState.Auto) +end +function SUPPRESSION:onafterDead(Controllable,From,Event,To) +self:_EventFromTo("onafterDead",Event,From,To) +local group=self.Controllable +if group then +local nunits=group:CountAliveUnits() +local text=string.format("Group %s: One of our units just died! %d units left.",self.Controllable:GetName(),nunits) +MESSAGE:New(text,10):ToAllIf(self.Debug) +self:T(self.lid..text) +if nunits==0 then +self:Stop() +end +else +self:Stop() +end +end +function SUPPRESSION:onafterStop(Controllable,From,Event,To) +self:_EventFromTo("onafterStop",Event,From,To) +local text=string.format("Stopping SUPPRESSION for group %s",self.Controllable:GetName()) +MESSAGE:New(text,10):ToAllIf(self.Debug) +self:I(self.lid..text) +self.CallScheduler:Clear() +if self.mooseevents then +self:UnHandleEvent(EVENTS.Dead) +self:UnHandleEvent(EVENTS.Hit) +else +world.removeEventHandler(self) +end +end +function SUPPRESSION:onEvent(Event) +if Event==nil or Event.initiator==nil or Unit.getByName(Event.initiator:getName())==nil then +return true +end +local EventData={} +if Event.initiator then +EventData.IniDCSUnit=Event.initiator +EventData.IniUnitName=Event.initiator:getName() +EventData.IniDCSGroup=Event.initiator:getGroup() +EventData.IniGroupName=Event.initiator:getGroup():getName() +EventData.IniGroup=GROUP:FindByName(EventData.IniGroupName) +EventData.IniUnit=UNIT:FindByName(EventData.IniUnitName) +end +if Event.target then +EventData.TgtDCSUnit=Event.target +EventData.TgtUnitName=Event.target:getName() +EventData.TgtDCSGroup=Event.target:getGroup() +EventData.TgtGroupName=Event.target:getGroup():getName() +EventData.TgtGroup=GROUP:FindByName(EventData.TgtGroupName) +EventData.TgtUnit=UNIT:FindByName(EventData.TgtUnitName) +end +if Event.id==world.event.S_EVENT_HIT then +self:_OnEventHit(EventData) +end +if Event.id==world.event.S_EVENT_DEAD then +self:_OnEventDead(EventData) +end +end +function SUPPRESSION:_OnEventHit(EventData) +self:F(EventData) +local GroupNameSelf=self.Controllable:GetName() +local GroupNameTgt=EventData.TgtGroupName +local TgtUnit=EventData.TgtUnit +local tgt=EventData.TgtDCSUnit +local IniUnit=EventData.IniUnit +if GroupNameTgt==GroupNameSelf then +self:T(self.lid..string.format("Hit event at t = %5.1f",timer.getTime())) +if self.flare or self.Debug then +TgtUnit:FlareRed() +end +self.Nhit=self.Nhit+1 +self:T(self.lid..string.format("Group %s has just been hit %d times.",self.Controllable:GetName(),self.Nhit)) +local life=tgt:getLife()/(tgt:getLife0()+1)*100 +self:T2(self.lid..string.format("Target unit life = %5.1f",life)) +self:__Hit(3,TgtUnit,IniUnit) +end +end +function SUPPRESSION:_OnEventDead(EventData) +local GroupNameSelf=self.Controllable:GetName() +local GroupNameIni=EventData.IniGroupName +if GroupNameIni==GroupNameSelf then +local IniUnit=EventData.IniUnit +local IniUnitName=EventData.IniUnitName +if EventData.IniUnit then +self:T2(self.lid..string.format("Group %s: Dead MOOSE unit DOES exist! Unit name %s.",GroupNameIni,IniUnitName)) +else +self:T2(self.lid..string.format("Group %s: Dead MOOSE unit DOES NOT not exist! Unit name %s.",GroupNameIni,IniUnitName)) +end +if EventData.IniDCSUnit then +self:T2(self.lid..string.format("Group %s: Dead DCS unit DOES exist! Unit name %s.",GroupNameIni,IniUnitName)) +else +self:T2(self.lid..string.format("Group %s: Dead DCS unit DOES NOT exist! Unit name %s.",GroupNameIni,IniUnitName)) +end +if IniUnit and(self.flare or self.Debug)then +IniUnit:FlareWhite() +self:T(self.lid..string.format("Flare Dead MOOSE unit.")) +end +if EventData.IniDCSUnit and(self.flare or self.Debug)then +local p=EventData.IniDCSUnit:getPosition().p +trigger.action.signalFlare(p,trigger.flareColor.Yellow,0) +self:T(self.lid..string.format("Flare Dead DCS unit.")) +end +self:Status() +self:__Dead(0.1) +end +end +function SUPPRESSION:_Suppress() +local Tnow=timer.getTime() +local Controllable=self.Controllable +self:_SetROE(SUPPRESSION.ROE.Hold) +local sigma=(self.Tsuppress_max-self.Tsuppress_min)/4 +local Tsuppress=self:_Random_Gaussian(self.Tsuppress_ave,sigma,self.Tsuppress_min,self.Tsuppress_max) +local renew=true +if self.TsuppressionOver~=nil then +if Tsuppress+Tnow>self.TsuppressionOver then +self.TsuppressionOver=Tnow+Tsuppress +else +renew=false +end +else +self.TsuppressionOver=Tnow+Tsuppress +end +if renew then +self:__Recovered(self.TsuppressionOver-Tnow) +end +local text=string.format("Group %s is suppressed for %d seconds. Suppression ends at %d:%02d.",Controllable:GetName(),Tsuppress,self.TsuppressionOver/60,self.TsuppressionOver%60) +MESSAGE:New(text,10):ToAllIf(self.Debug) +self:T(self.lid..text) +end +function SUPPRESSION:_Run(fin,speed,formation,wait) +speed=speed or 20 +formation=formation or"Off road" +wait=wait or 30 +local group=self.Controllable +if group and group:IsAlive()then +group:ClearTasks() +local ini=group:GetCoordinate() +local dist=ini:Get2DDistance(fin) +local heading=self:_Heading(ini,fin) +local nx +if dist<=50 then +nx=2 +elseif dist<=100 then +nx=3 +elseif dist<=500 then +nx=4 +else +nx=5 +end +local dx=dist/(nx-1) +local wp={} +local tasks={} +wp[1]=ini:WaypointGround(speed,formation) +tasks[1]=group:TaskFunction("SUPPRESSION._Passing_Waypoint",self,1,false) +if self.Debug then +local MarkerID=ini:MarkToAll(string.format("Waypoing %d of group %s (initial)",#wp,self.Controllable:GetName())) +end +self:T2(self.lid..string.format("Number of waypoints %d",nx)) +for i=1,nx-2 do +local x=dx*i +local coord=ini:Translate(x,heading) +wp[#wp+1]=coord:WaypointGround(speed,formation) +tasks[#tasks+1]=group:TaskFunction("SUPPRESSION._Passing_Waypoint",self,#wp,false) +self:T2(self.lid..string.format("%d x = %4.1f",i,x)) +if self.Debug then +local MarkerID=coord:MarkToAll(string.format("Waypoing %d of group %s",#wp,self.Controllable:GetName())) +end +end +self:T2(self.lid..string.format("Total distance: %4.1f",dist)) +wp[#wp+1]=fin:WaypointGround(speed,formation) +if self.Debug then +local MarkerID=fin:MarkToAll(string.format("Waypoing %d of group %s (final)",#wp,self.Controllable:GetName())) +end +local ConditionWait=group:TaskCondition(nil,nil,nil,nil,wait,nil) +local TaskHold=group:TaskHold() +local TaskComboFin={} +TaskComboFin[#TaskComboFin+1]=group:TaskFunction("SUPPRESSION._Passing_Waypoint",self,#wp,true) +TaskComboFin[#TaskComboFin+1]=group:TaskControlled(TaskHold,ConditionWait) +tasks[#tasks+1]=group:TaskCombo(TaskComboFin) +local Waypoints=group:GetTemplateRoutePoints() +for i,p in ipairs(wp)do +table.insert(Waypoints,i,wp[i]) +end +for i,wp in ipairs(Waypoints)do +group:SetTaskWaypoint(Waypoints[i],tasks[i]) +end +group:Route(Waypoints) +else +self:E(self.lid..string.format("ERROR: Group is not alive!")) +end +end +function SUPPRESSION._Passing_Waypoint(group,Fsm,i,final) +local text=string.format("Group %s passing waypoint %d (final=%s)",group:GetName(),i,tostring(final)) +MESSAGE:New(text,10):ToAllIf(Fsm.Debug) +if Fsm.Debug then +env.info(self.lid..text) +end +if final then +if Fsm:is("Retreating")then +Fsm:Retreated() +else +Fsm:FightBack() +end +end +end +function SUPPRESSION:_SearchHideout() +local Zone=ZONE_GROUP:New("Zone_Hiding",self.Controllable,self.TakecoverRange) +local gpos=self.Controllable:GetCoordinate() +Zone:Scan(Object.Category.SCENERY) +local hideouts={} +for SceneryTypeName,SceneryData in pairs(Zone:GetScannedScenery())do +for SceneryName,SceneryObject in pairs(SceneryData)do +local SceneryObject=SceneryObject +local spos=SceneryObject:GetCoordinate() +local distance=spos:Get2DDistance(gpos) +if self.Debug then +local MarkerID=SceneryObject:GetCoordinate():MarkToAll(string.format("%s scenery object %s",self.Controllable:GetName(),SceneryObject:GetTypeName())) +local text=string.format("%s scenery: %s, Coord %s",self.Controllable:GetName(),SceneryObject:GetTypeName(),SceneryObject:GetCoordinate():ToStringLLDMS()) +self:T2(self.lid..text) +end +table.insert(hideouts,{object=SceneryObject,distance=distance}) +end +end +local Hideout=nil +if#hideouts>0 then +self:T(self.lid.."Number of hideouts "..#hideouts) +local _sort=function(a,b)return a.distancelife_max then +life_max=life +end +life_ave=life_ave+life +if self.Debug then +local text=string.format("n=%02d: Life = %3.1f, Life0 = %3.1f, min=%3.1f, max=%3.1f, ave=%3.1f, group=%3.1f",n,unit:GetLife(),unit:GetLife0(),life_min,life_max,life_ave/n,groupstrength) +self:T2(self.lid..text) +end +end +end +if n==0 then +return 0,0,0,0,0 +end +life_ave0=life_ave/self.IniGroupStrength +life_ave=life_ave/n +return life_min,life_max,life_ave,life_ave0,groupstrength +else +return 0,0,0,0,0 +end +end +function SUPPRESSION:_Heading(a,b) +local dx=b.x-a.x +local dy=b.z-a.z +local angle=math.deg(math.atan2(dy,dx)) +if angle<0 then +angle=360+angle +end +return angle +end +function SUPPRESSION:_Random_Gaussian(x0,sigma,xmin,xmax) +sigma=sigma or 5 +local r +local gotit=false +local i=0 +while not gotit do +local x1=math.random() +local x2=math.random() +r=math.sqrt(-2*sigma*sigma*math.log(x1))*math.cos(2*math.pi*x2)+x0 +i=i+1 +if(r>=xmin and r<=xmax)or i>100 then +gotit=true +end +end +return r +end +function SUPPRESSION:_SetROE(roe) +local group=self.Controllable +roe=roe or self.DefaultROE +self.CurrentROE=roe +if roe==SUPPRESSION.ROE.Free then +group:OptionROEOpenFire() +elseif roe==SUPPRESSION.ROE.Hold then +group:OptionROEHoldFire() +elseif roe==SUPPRESSION.ROE.Return then +group:OptionROEReturnFire() +else +self:E(self.lid.."Unknown ROE requested: "..tostring(roe)) +group:OptionROEOpenFire() +self.CurrentROE=SUPPRESSION.ROE.Free +end +local text=string.format("Group %s now has ROE %s.",self.Controllable:GetName(),self.CurrentROE) +self:T(self.lid..text) +end +function SUPPRESSION:_SetAlarmState(state) +local group=self.Controllable +state=state or self.DefaultAlarmState +self.CurrentAlarmState=state +if state==SUPPRESSION.AlarmState.Auto then +group:OptionAlarmStateAuto() +elseif state==SUPPRESSION.AlarmState.Green then +group:OptionAlarmStateGreen() +elseif state==SUPPRESSION.AlarmState.Red then +group:OptionAlarmStateRed() +else +self:E(self.lid.."Unknown alarm state requested: "..tostring(state)) +group:OptionAlarmStateAuto() +self.CurrentAlarmState=SUPPRESSION.AlarmState.Auto +end +local text=string.format("Group %s now has Alarm State %s.",self.Controllable:GetName(),self.CurrentAlarmState) +self:T(self.lid..text) +end +function SUPPRESSION:_EventFromTo(BA,Event,From,To) +local text=string.format("\n%s: %s EVENT %s: %s --> %s",BA,self.Controllable:GetName(),Event,From,To) +self:T2(self.lid..text) +end +PSEUDOATC={ +ClassName="PSEUDOATC", +group={}, +Debug=false, +mdur=30, +mrefresh=120, +talt=3, +chatty=true, +eventsmoose=true, +} +PSEUDOATC.id="PseudoATC | " +PSEUDOATC.version="0.9.2" +function PSEUDOATC:New() +local self=BASE:Inherit(self,BASE:New()) +self:E(PSEUDOATC.id..string.format("PseudoATC version %s",PSEUDOATC.version)) +return self +end +function PSEUDOATC:Start() +self:F() +self:E(PSEUDOATC.id.."Starting PseudoATC") +if self.eventsmoose then +self:T(PSEUDOATC.id.."Events are handled by MOOSE.") +self:HandleEvent(EVENTS.Birth,self._OnBirth) +self:HandleEvent(EVENTS.Land,self._PlayerLanded) +self:HandleEvent(EVENTS.Takeoff,self._PlayerTakeOff) +self:HandleEvent(EVENTS.PlayerLeaveUnit,self._PlayerLeft) +self:HandleEvent(EVENTS.Crash,self._PlayerLeft) +else +self:T(PSEUDOATC.id.."Events are handled by DCS.") +world.addEventHandler(self) +end +end +function PSEUDOATC:DebugOn() +self.Debug=true +end +function PSEUDOATC:DebugOff() +self.Debug=false +end +function PSEUDOATC:ChattyOn() +self.chatty=true +end +function PSEUDOATC:ChattyOff() +self.chatty=false +end +function PSEUDOATC:SetMessageDuration(duration) +self.mdur=duration or 30 +end +function PSEUDOATC:SetMenuRefresh(interval) +self.mrefresh=interval or 120 +end +function PSEUDOATC:SetEventsMoose(switch) +self.eventsmoose=switch +end +function PSEUDOATC:SetReportAltInterval(interval) +self.talt=interval or 3 +end +function PSEUDOATC:onEvent(Event) +if Event==nil or Event.initiator==nil or Unit.getByName(Event.initiator:getName())==nil then +return true +end +local DCSiniunit=Event.initiator +local DCSplace=Event.place +local DCSsubplace=Event.subplace +local EventData={} +local _playerunit=nil +local _playername=nil +if Event.initiator then +EventData.IniUnitName=Event.initiator:getName() +EventData.IniDCSGroup=Event.initiator:getGroup() +EventData.IniGroupName=Event.initiator:getGroup():getName() +_playerunit,_playername=self:_GetPlayerUnitAndName(EventData.IniUnitName) +end +if Event.place then +EventData.Place=Event.place +EventData.PlaceName=Event.place:getName() +end +if Event.subplace then +EventData.SubPlace=Event.subplace +EventData.SubPlaceName=Event.subplace:getName() +end +self:T3(PSEUDOATC.id..string.format("EVENT: Event in onEvent with ID = %s",tostring(Event.id))) +self:T3(PSEUDOATC.id..string.format("EVENT: Ini unit = %s",tostring(EventData.IniUnitName))) +self:T3(PSEUDOATC.id..string.format("EVENT: Ini group = %s",tostring(EventData.IniGroupName))) +self:T3(PSEUDOATC.id..string.format("EVENT: Ini player = %s",tostring(_playername))) +self:T3(PSEUDOATC.id..string.format("EVENT: Place = %s",tostring(EventData.PlaceName))) +self:T3(PSEUDOATC.id..string.format("EVENT: SubPlace = %s",tostring(EventData.SubPlaceName))) +if Event.id==world.event.S_EVENT_BIRTH and _playername then +self:_OnBirth(EventData) +end +if Event.id==world.event.S_EVENT_TAKEOFF and _playername and EventData.Place then +self:_PlayerTakeOff(EventData) +end +if Event.id==world.event.S_EVENT_LAND and _playername and EventData.Place then +self:_PlayerLanded(EventData) +end +if Event.id==world.event.S_EVENT_PLAYER_LEAVE_UNIT and _playername then +self:_PlayerLeft(EventData) +end +if Event.id==world.event.S_EVENT_CRASH and _playername then +self:_PlayerLeft(EventData) +end +end +function PSEUDOATC:_OnBirth(EventData) +self:F({EventData=EventData}) +local _unitName=EventData.IniUnitName +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +if _unit and _playername then +self:PlayerEntered(_unit) +end +end +function PSEUDOATC:_PlayerLeft(EventData) +self:F({EventData=EventData}) +local _unitName=EventData.IniUnitName +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +if _unit and _playername then +self:PlayerLeft(_unit) +end +end +function PSEUDOATC:_PlayerLanded(EventData) +self:F({EventData=EventData}) +local _unitName=EventData.IniUnitName +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +local _base=nil +local _baseName=nil +if EventData.place then +_base=EventData.place +_baseName=EventData.place:getName() +end +if _unit and _playername and _base then +self:PlayerLanded(_unit,_baseName) +end +end +function PSEUDOATC:_PlayerTakeOff(EventData) +self:F({EventData=EventData}) +local _unitName=EventData.IniUnitName +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +local _base=nil +local _baseName=nil +if EventData.place then +_base=EventData.place +_baseName=EventData.place:getName() +end +if _unit and _playername and _base then +self:PlayerTakeOff(_unit,_baseName) +end +end +function PSEUDOATC:PlayerEntered(unit) +self:F2({unit=unit}) +local group=unit:GetGroup() +local GID=group:GetID() +local GroupName=group:GetName() +local PlayerName=unit:GetPlayerName() +local UnitName=unit:GetName() +local CallSign=unit:GetCallsign() +local UID=unit:GetDCSObject():getID() +if not self.group[GID]then +self.group[GID]={} +self.group[GID].player={} +end +self.group[GID].player[UID]={} +self.group[GID].player[UID].group=group +self.group[GID].player[UID].unit=unit +self.group[GID].player[UID].groupname=GroupName +self.group[GID].player[UID].unitname=UnitName +self.group[GID].player[UID].playername=PlayerName +self.group[GID].player[UID].callsign=CallSign +self.group[GID].player[UID].waypoints=group:GetTaskRoute() +local text=string.format("Player %s entered unit %s of group %s (id=%d).",PlayerName,UnitName,GroupName,GID) +self:T(PSEUDOATC.id..text) +MESSAGE:New(text,30):ToAllIf(self.Debug) +local countPlayerInGroup=0 +for _ in pairs(self.group[GID].player)do countPlayerInGroup=countPlayerInGroup+1 end +if countPlayerInGroup<=1 then +self.group[GID].menu_main=missionCommands.addSubMenuForGroup(GID,"Pseudo ATC") +end +self:MenuCreatePlayer(GID,UID) +self:LocalAirports(GID,UID) +self:MenuAirports(GID,UID) +self:MenuWaypoints(GID,UID) +self.group[GID].player[UID].scheduler,self.group[GID].player[UID].schedulerid=SCHEDULER:New(nil,self.MenuRefresh,{self,GID,UID},self.mrefresh,self.mrefresh) +end +function PSEUDOATC:PlayerLanded(unit,place) +self:F2({unit=unit,place=place}) +local group=unit:GetGroup() +local GID=group:GetID() +local UID=unit:GetDCSObject():getID() +local PlayerName=self.group[GID].player[UID].playername +local UnitName=self.group[GID].player[UID].unitname +local GroupName=self.group[GID].player[UID].groupname +local text=string.format("Player %s in unit %s of group %s (id=%d) landed at %s.",PlayerName,UnitName,GroupName,GID,place) +self:T(PSEUDOATC.id..text) +MESSAGE:New(text,30):ToAllIf(self.Debug) +self:AltitudeTimerStop(GID,UID) +if place and self.chatty then +local text=string.format("Touchdown! Welcome to %s pilot %s. Have a nice day!",place,PlayerName) +MESSAGE:New(text,self.mdur):ToGroup(group) +end +end +function PSEUDOATC:PlayerTakeOff(unit,place) +self:F2({unit=unit,place=place}) +local group=unit:GetGroup() +local GID=group:GetID() +local UID=unit:GetDCSObject():getID() +local PlayerName=self.group[GID].player[UID].playername +local CallSign=self.group[GID].player[UID].callsign +local UnitName=self.group[GID].player[UID].unitname +local GroupName=self.group[GID].player[UID].groupname +local text=string.format("Player %s in unit %s of group %s (id=%d) took off at %s.",PlayerName,UnitName,GroupName,GID,place) +self:T(PSEUDOATC.id..text) +MESSAGE:New(text,30):ToAllIf(self.Debug) +if place and self.chatty then +local text=string.format("%s, %s, you are airborne. Have a safe trip!",place,CallSign) +MESSAGE:New(text,self.mdur):ToGroup(group) +end +end +function PSEUDOATC:PlayerLeft(unit) +self:F({unit=unit}) +local group=unit:GetGroup() +local GID=group:GetID() +local UID=unit:GetDCSObject():getID() +if self.group[GID].player[UID]then +local PlayerName=self.group[GID].player[UID].playername +local CallSign=self.group[GID].player[UID].callsign +local UnitName=self.group[GID].player[UID].unitname +local GroupName=self.group[GID].player[UID].groupname +local text=string.format("Player %s (callsign %s) of group %s just left unit %s.",PlayerName,CallSign,GroupName,UnitName) +self:T(PSEUDOATC.id..text) +MESSAGE:New(text,30):ToAllIf(self.Debug) +if self.group[GID].player[UID].schedulerid then +self.group[GID].player[UID].scheduler:Stop(self.group[GID].player[UID].schedulerid) +end +self:AltitudeTimerStop(GID,UID) +if self.group[GID].player[UID].menu_own then +missionCommands.removeItemForGroup(GID,self.group[GID].player[UID].menu_own) +end +local countPlayerInGroup=0 +for _ in pairs(self.group[GID].player)do countPlayerInGroup=countPlayerInGroup+1 end +if self.group[GID].menu_main and countPlayerInGroup==1 then +missionCommands.removeItemForGroup(GID,self.group[GID].menu_main) +end +self.group[GID].player[UID]=nil +end +end +function PSEUDOATC:MenuRefresh(GID,UID) +self:F({GID=GID,UID=UID}) +local text=string.format("Refreshing menues for player %s in group %s.",self.group[GID].player[UID].playername,self.group[GID].player[UID].groupname) +self:T(PSEUDOATC.id..text) +MESSAGE:New(text,30):ToAllIf(self.Debug) +self:MenuClear(GID,UID) +self:LocalAirports(GID,UID) +self:MenuAirports(GID,UID) +self:MenuWaypoints(GID,UID) +end +function PSEUDOATC:MenuCreatePlayer(GID,UID) +self:F({GID=GID,UID=UID}) +local PlayerName=self.group[GID].player[UID].playername +self.group[GID].player[UID].menu_own=missionCommands.addSubMenuForGroup(GID,PlayerName,self.group[GID].menu_main) +end +function PSEUDOATC:MenuClear(GID,UID) +self:F({GID=GID,UID=UID}) +local text=string.format("Clearing menus for player %s in group %s.",self.group[GID].player[UID].playername,self.group[GID].player[UID].groupname) +self:T(PSEUDOATC.id..text) +MESSAGE:New(text,30):ToAllIf(self.Debug) +if self.group[GID].player[UID].menu_airports then +missionCommands.removeItemForGroup(GID,self.group[GID].player[UID].menu_airports) +self.group[GID].player[UID].menu_airports=nil +else +self:T2(PSEUDOATC.id.."No airports to clear menus.") +end +if self.group[GID].player[UID].menu_waypoints then +missionCommands.removeItemForGroup(GID,self.group[GID].player[UID].menu_waypoints) +self.group[GID].player[UID].menu_waypoints=nil +end +if self.group[GID].player[UID].menu_reportalt then +missionCommands.removeItemForGroup(GID,self.group[GID].player[UID].menu_reportalt) +self.group[GID].player[UID].menu_reportalt=nil +end +if self.group[GID].player[UID].menu_requestalt then +missionCommands.removeItemForGroup(GID,self.group[GID].player[UID].menu_requestalt) +self.group[GID].player[UID].menu_requestalt=nil +end +end +function PSEUDOATC:MenuAirports(GID,UID) +self:F({GID=GID,UID=UID}) +self.group[GID].player[UID].menu_airports=missionCommands.addSubMenuForGroup(GID,"Local Airports",self.group[GID].player[UID].menu_own) +local i=0 +for _,airport in pairs(self.group[GID].player[UID].airports)do +i=i+1 +if i>10 then +break +end +local name=airport.name +local d=airport.distance +local pos=AIRBASE:FindByName(name):GetCoordinate() +local submenu=missionCommands.addSubMenuForGroup(GID,name,self.group[GID].player[UID].menu_airports) +missionCommands.addCommandForGroup(GID,"Weather Report",submenu,self.ReportWeather,self,GID,UID,pos,name) +missionCommands.addCommandForGroup(GID,"Request BR",submenu,self.ReportBR,self,GID,UID,pos,name) +self:T(string.format(PSEUDOATC.id.."Creating airport menu item %s for ID %d",name,GID)) +end +end +function PSEUDOATC:MenuWaypoints(GID,UID) +self:F({GID=GID,UID=UID}) +local callsign=self.group[GID].player[UID].callsign +self:T(PSEUDOATC.id..string.format("Creating waypoint menu for %s (ID %d).",callsign,GID)) +if#self.group[GID].player[UID].waypoints>0 then +self.group[GID].player[UID].menu_waypoints=missionCommands.addSubMenuForGroup(GID,"Waypoints",self.group[GID].player[UID].menu_own) +local j=0 +for i,wp in pairs(self.group[GID].player[UID].waypoints)do +j=j+1 +if j>10 then +break +end +local pos=COORDINATE:New(wp.x,wp.alt,wp.y) +local name=string.format("Waypoint %d",i-1) +local submenu=missionCommands.addSubMenuForGroup(GID,name,self.group[GID].player[UID].menu_waypoints) +missionCommands.addCommandForGroup(GID,"Weather Report",submenu,self.ReportWeather,self,GID,UID,pos,name) +missionCommands.addCommandForGroup(GID,"Request BR",submenu,self.ReportBR,self,GID,UID,pos,name) +end +end +self.group[GID].player[UID].menu_reportalt=missionCommands.addCommandForGroup(GID,"Talk me down",self.group[GID].player[UID].menu_own,self.AltidudeTimerToggle,self,GID,UID) +self.group[GID].player[UID].menu_requestalt=missionCommands.addCommandForGroup(GID,"Request altitude",self.group[GID].player[UID].menu_own,self.ReportHeight,self,GID,UID) +end +function PSEUDOATC:ReportWeather(GID,UID,position,location) +self:F({GID=GID,UID=UID,position=position,location=location}) +local settings=_DATABASE:GetPlayerSettings(self.group[GID].player[UID].playername)or _SETTINGS +local text=string.format("Local weather at %s:\n",location) +local Pqnh=position:GetPressure(0) +local Pqfe=position:GetPressure() +local hPa2inHg=0.0295299830714 +local hPa2mmHg=0.7500615613030 +local _Pqnh=string.format("%.2f inHg",Pqnh*hPa2inHg) +local _Pqfe=string.format("%.2f inHg",Pqfe*hPa2inHg) +if settings:IsMetric()then +_Pqnh=string.format("%.1f mmHg",Pqnh*hPa2mmHg) +_Pqfe=string.format("%.1f mmHg",Pqfe*hPa2mmHg) +end +text=text..string.format("QFE %.1f hPa = %s.\n",Pqfe,_Pqfe) +text=text..string.format("QNH %.1f hPa = %s.\n",Pqnh,_Pqnh) +local T=position:GetTemperature() +local _T=string.format('%d°F',UTILS.CelciusToFarenheit(T)) +if settings:IsMetric()then +_T=string.format('%d°C',T) +end +local text=text..string.format("Temperature %s\n",_T) +local Dir,Vel=position:GetWind() +local Bn,Bd=UTILS.BeaufortScale(Vel) +local Ds=string.format('%03d°',Dir) +local Vs=string.format("%.1f knots",UTILS.MpsToKnots(Vel)) +if settings:IsMetric()then +Vs=string.format('%.1f m/s',Vel) +end +local text=text..string.format("%s, Wind from %s at %s (%s).",self.group[GID].player[UID].playername,Ds,Vs,Bd) +self:_DisplayMessageToGroup(self.group[GID].player[UID].unit,text,self.mdur,true) +end +function PSEUDOATC:ReportBR(GID,UID,position,location) +self:F({GID=GID,UID=UID,position=position,location=location}) +local unit=self.group[GID].player[UID].unit +local coord=unit:GetCoordinate() +local angle=coord:HeadingTo(position) +local range=coord:Get2DDistance(position) +local Bs=string.format('%03d°',angle) +local settings=_DATABASE:GetPlayerSettings(self.group[GID].player[UID].playername)or _SETTINGS +local Rs=string.format("%.1f NM",UTILS.MetersToNM(range)) +if settings:IsMetric()then +Rs=string.format("%.1f km",range/1000) +end +local text=string.format("%s: Bearing %s, Range %s.",location,Bs,Rs) +self:_DisplayMessageToGroup(self.group[GID].player[UID].unit,text,self.mdur,true) +end +function PSEUDOATC:ReportHeight(GID,UID,dt,_clear) +self:F({GID=GID,UID=UID,dt=dt}) +local dt=dt or self.mdur +if _clear==nil then +_clear=false +end +local function get_AGL(p) +local agl=0 +local vec2={x=p.x,y=p.z} +local ground=land.getHeight(vec2) +local agl=p.y-ground +return agl +end +local unit=self.group[GID].player[UID].unit +if unit and unit:IsAlive()then +local position=unit:GetCoordinate() +local height=get_AGL(position) +local callsign=unit:GetCallsign() +local settings=_DATABASE:GetPlayerSettings(self.group[GID].player[UID].playername)or _SETTINGS +local Hs=string.format("%d ft",UTILS.MetersToFeet(height)) +if settings:IsMetric()then +Hs=string.format("%d m",height) +end +local _text=string.format("%s, your altitude is %s AGL.",callsign,Hs) +if _clear==false then +_text=_text..string.format(" FL%03d.",position.y/30.48) +end +self:_DisplayMessageToGroup(self.group[GID].player[UID].unit,_text,dt,_clear) +return height +end +return 0 +end +function PSEUDOATC:AltidudeTimerToggle(GID,UID) +self:F({GID=GID,UID=UID}) +if self.group[GID].player[UID].altimerid then +self:AltitudeTimerStop(GID,UID) +else +self:AltitudeTimeStart(GID,UID) +end +end +function PSEUDOATC:AltitudeTimeStart(GID,UID) +self:F({GID=GID,UID=UID}) +self:T(PSEUDOATC.id..string.format("Starting altitude report timer for player ID %d.",UID)) +self.group[GID].player[UID].altimer,self.group[GID].player[UID].altimerid=SCHEDULER:New(nil,self.ReportHeight,{self,GID,UID,0.1,true},1,3) +end +function PSEUDOATC:AltitudeTimerStop(GID,UID) +self:F({GID=GID,UID=UID}) +self:T(PSEUDOATC.id..string.format("Stopping altitude report timer for player ID %d.",UID)) +if self.group[GID].player[UID].altimerid then +self.group[GID].player[UID].altimer:Stop(self.group[GID].player[UID].altimerid) +end +self.group[GID].player[UID].altimer=nil +self.group[GID].player[UID].altimerid=nil +end +function PSEUDOATC:LocalAirports(GID,UID) +self:F({GID=GID,UID=UID}) +self.group[GID].player[UID].airports=nil +self.group[GID].player[UID].airports={} +local pos=self.group[GID].player[UID].unit:GetCoordinate() +for i=0,2 do +local airports=coalition.getAirbases(i) +for _,airbase in pairs(airports)do +local name=airbase:getName() +local q=AIRBASE:FindByName(name):GetCoordinate() +local d=q:Get2DDistance(pos) +table.insert(self.group[GID].player[UID].airports,{distance=d,name=name}) +end +end +local function compare(a,b) +return a.distance0 then +local samecoalition=anycoalition or Coalition==warehouse:GetCoalition() +if samecoalition and not(warehouse:IsNotReadyYet()or warehouse:IsStopped()or warehouse:IsDestroyed())then +local nassets=warehouse:GetNumberOfAssets(Descriptor,DescriptorValue) +local enough=true +if Descriptor and DescriptorValue then +enough=nassets>=MinAssets +end +if enough and(distmin==nil or dist=1 then +local FSMstate=self:GetState() +local coalition=self:GetCoalitionName() +local country=self:GetCountryName() +self:I(self.lid..string.format("State=%s %s [%s]: Assets=%d, Requests: waiting=%d, pending=%d",FSMstate,country,coalition,#self.stock,#self.queue,#self.pending)) +end +self:_JobDone() +self:_DisplayStatus() +self:_CheckConquered() +if self:IsRunwayOperational()==false then +local Trepair=self:GetRunwayRepairtime() +self:I(self.lid..string.format("Runway destroyed! Will be repaired in %d sec",Trepair)) +if Trepair==0 then +self:RunwayRepaired() +end +end +self:_CheckRequestConsistancy(self.queue) +if self:IsRunning()or self:IsAttacked()then +local request=self:_CheckQueue() +if request then +self:Request(request) +end +end +self:_PrintQueue(self.queue,"Queue waiting") +self:_PrintQueue(self.pending,"Queue pending") +self:_CheckFuel() +self:_UpdateWarehouseMarkText() +if self.Debug then +self:_DisplayStockItems(self.stock) +end +self:__Status(-self.dTstatus) +end +function WAREHOUSE:_JobDone() +local done={} +for _,request in pairs(self.pending)do +local request=request +if request.born then +local ncargo=0 +if request.cargogroupset then +ncargo=request.cargogroupset:Count() +end +local ntransport=0 +if request.transportgroupset then +ntransport=request.transportgroupset:Count() +end +local ncargotot=request.nasset +local ncargodelivered=request.ndelivered +local ncargodead=ncargotot-ncargodelivered-ncargo +local ntransporttot=request.ntransport +local ntransporthome=request.ntransporthome +local ntransportdead=ntransporttot-ntransporthome-ntransport +local text=string.format("Request id=%d: Cargo: Ntot=%d, Nalive=%d, Ndelivered=%d, Ndead=%d | Transport: Ntot=%d, Nalive=%d, Nhome=%d, Ndead=%d", +request.uid,ncargotot,ncargo,ncargodelivered,ncargodead,ntransporttot,ntransport,ntransporthome,ntransportdead) +self:T(self.lid..text) +if ncargo==0 then +if not self.delivered[request.uid]then +self:Delivered(request) +end +if ntransport==0 then +if self.verbosity>=1 then +local text=string.format("Warehouse %s: Job on request id=%d for warehouse %s done!\n",self.alias,request.uid,request.warehouse.alias) +text=text..string.format("- %d of %d assets delivered. Casualties %d.",ncargodelivered,ncargotot,ncargodead) +if request.ntransport>0 then +text=text..string.format("\n- %d of %d transports returned home. Casualties %d.",ntransporthome,ntransporttot,ntransportdead) +end +self:_InfoMessage(text,20) +end +table.insert(done,request) +else +for _,_group in pairs(request.transportgroupset:GetSetObjects())do +local group=_group +if group and group:IsAlive()then +local category=group:GetCategory() +local speed=group:GetVelocityKMH() +local notmoving=speed<1 +local airbase=group:GetCoordinate():GetClosestAirbase():GetName() +local athomebase=self.airbase and self.airbase:GetName()==airbase +local onground=not group:InAir() +local inspawnzone=group:IsPartlyOrCompletelyInZone(self.spawnzone) +local ishome=false +if category==Group.Category.GROUND or category==Group.Category.HELICOPTER then +ishome=inspawnzone and onground and notmoving +elseif category==Group.Category.AIRPLANE then +ishome=athomebase and onground and notmoving +end +local text=string.format("Group %s: speed=%d km/h, onground=%s , airbase=%s, spawnzone=%s ==> ishome=%s",group:GetName(),speed,tostring(onground),airbase,tostring(inspawnzone),tostring(ishome)) +self:T(self.lid..text) +if ishome then +local text=string.format("Warehouse %s: Transport group arrived back home and no cargo left for request id=%d.\nSending transport group %s back to stock.",self.alias,request.uid,group:GetName()) +self:T(self.lid..text) +if self.Debug then +group:SmokeRed() +end +self:Arrived(group) +end +end +end +end +else +if ntransport==0 and request.ntransport>0 then +local ncargoalive=0 +for _,_group in pairs(request.cargogroupset:GetSetObjects())do +local groupname=_group:GetName() +local group=GROUP:FindByName(groupname.."#CARGO") +if group and group:IsAlive()then +if group:IsPartlyOrCompletelyInZone(self.spawnzone)then +if self.Debug then +group:SmokeBlue() +end +self:AddAsset(group) +ncargoalive=ncargoalive+1 +end +end +end +self:_InfoMessage(string.format("Warehouse %s: All transports of request id=%s dead! Putting remaining %s cargo assets back into warehouse!",self.alias,request.uid,ncargoalive)) +end +end +end +end +for _,request in pairs(done)do +self:_DeleteQueueItem(request,self.pending) +end +end +function WAREHOUSE:_CheckAssetStatus() +local function _CheckGroup(_request,_group) +local request=_request +local group=_group +if group and group:IsAlive()then +local category=group:GetCategory() +for _,_unit in pairs(group:GetUnits())do +local unit=_unit +if unit and unit:IsAlive()then +local unitid=unit:GetID() +local life9=unit:GetLife() +local life0=unit:GetLife0() +local life=life9/life0*100 +local speed=unit:GetVelocityMPS() +local onground=unit:InAir() +local problem=false +if life<10 then +self:T(string.format("Unit %s is heavily damaged!",unit:GetName())) +end +if speed<1 and unit:GetSpeedMax()>1 and onground then +self:T(string.format("Unit %s is not moving!",unit:GetName())) +problem=true +end +if problem then +if request.assetproblem[unitid]then +local deltaT=timer.getAbsTime()-request.assetproblem[unitid] +if deltaT>300 then +unit:Destroy() +end +else +request.assetproblem[unitid]=timer.getAbsTime() +end +end +end +end +end +end +for _,request in pairs(self.pending)do +local request=request +if request.cargogroupset then +for _,_group in pairs(request.cargogroupset:GetSet())do +local group=_group +_CheckGroup(request,group) +end +end +if request.transportgroupset then +for _,group in pairs(request.transportgroupset:GetSet())do +_CheckGroup(request,group) +end +end +end +end +function WAREHOUSE:onafterAddAsset(From,Event,To,group,ngroups,forceattribute,forcecargobay,forceweight,loadradius,skill,liveries,assignment,other) +self:T({group=group,ngroups=ngroups,forceattribute=forceattribute,forcecargobay=forcecargobay,forceweight=forceweight}) +local n=ngroups or 1 +if type(group)=="string"then +group=GROUP:FindByName(group) +end +if liveries and type(liveries)=="string"then +liveries={liveries} +end +if group then +local wid,aid,rid=self:_GetIDsFromGroup(group) +if wid and aid and rid then +local warehouse=self:FindWarehouseInDB(wid) +if warehouse then +local request=warehouse:_GetRequestOfGroup(group,warehouse.pending) +if request then +local istransport=warehouse:_GroupIsTransport(group,request) +if istransport==true then +request.ntransporthome=request.ntransporthome+1 +request.transportgroupset:Remove(group:GetName(),true) +local ntrans=request.transportgroupset:Count() +self:T2(warehouse.lid..string.format("Transport %d of %s returned home. TransportSet=%d",request.ntransporthome,tostring(request.ntransport),ntrans)) +elseif istransport==false then +request.ndelivered=request.ndelivered+1 +local namewo=self:_GetNameWithOut(group) +request.cargogroupset:Remove(namewo,true) +local ncargo=request.cargogroupset:Count() +self:T2(warehouse.lid..string.format("Cargo %s: %d of %s delivered. CargoSet=%d",namewo,request.ndelivered,tostring(request.nasset),ncargo)) +else +self:E(warehouse.lid..string.format("WARNING: Group %s is neither cargo nor transport! Need to investigate...",group:GetName())) +end +if assignment==nil and request.assignment~=nil then +assignment=request.assignment +end +end +end +local asset=self:FindAssetInDB(group) +if asset~=nil then +self:_DebugMessage(string.format("Warehouse %s: Adding KNOWN asset uid=%d with attribute=%s to stock.",self.alias,asset.uid,asset.attribute),5) +if liveries then +if type(liveries)=="table"then +asset.livery=liveries[math.random(#liveries)] +else +asset.livery=liveries +end +end +asset.skill=skill or asset.skill +asset.wid=self.uid +asset.rid=nil +asset.spawned=false +asset.iscargo=nil +asset.arrived=nil +if group:IsAlive()==true then +asset.damage=asset.life0-group:GetLife() +end +table.insert(self.stock,asset) +self:__NewAsset(0.1,asset,assignment or"") +else +self:_ErrorMessage(string.format("ERROR: Known asset could not be found in global warehouse db!"),0) +end +else +self:_DebugMessage(string.format("Warehouse %s: Adding %d NEW assets of group %s to stock",self.alias,n,tostring(group:GetName())),5) +local assets=self:_RegisterAsset(group,n,forceattribute,forcecargobay,forceweight,loadradius,liveries,skill,assignment) +for _,asset in pairs(assets)do +asset.wid=self.uid +asset.rid=nil +table.insert(self.stock,asset) +self:__NewAsset(0.1,asset,assignment or"") +end +end +if group:IsAlive()==true then +self:_DebugMessage(string.format("Removing group %s",group:GetName()),5) +group:Destroy() +end +else +self:E(self.lid.."ERROR: Unknown group added as asset!") +self:E({unknowngroup=group}) +end +end +function WAREHOUSE:_RegisterAsset(group,ngroups,forceattribute,forcecargobay,forceweight,loadradius,liveries,skill,assignment) +self:F({groupname=group:GetName(),ngroups=ngroups,forceattribute=forceattribute,forcecargobay=forcecargobay,forceweight=forceweight}) +local n=ngroups or 1 +local function _GetObjectSize(DCSdesc) +if DCSdesc.box then +local x=DCSdesc.box.max.x+math.abs(DCSdesc.box.min.x) +local y=DCSdesc.box.max.y+math.abs(DCSdesc.box.min.y) +local z=DCSdesc.box.max.z+math.abs(DCSdesc.box.min.z) +return math.max(x,z),x,y,z +end +return 0,0,0,0 +end +local templategroupname=group:GetName() +local Descriptors=group:GetUnit(1):GetDesc() +local Category=group:GetCategory() +local TypeName=group:GetTypeName() +local SpeedMax=group:GetSpeedMax() +local RangeMin=group:GetRange() +local smax,sx,sy,sz=_GetObjectSize(Descriptors) +local weight=0 +local cargobay={} +local cargobaytot=0 +local cargobaymax=0 +for _i,_unit in pairs(group:GetUnits())do +local unit=_unit +local Desc=unit:GetDesc() +local unitweight=forceweight or Desc.massEmpty +if unitweight then +weight=weight+unitweight +end +local cargomax=0 +local massfuel=Desc.fuelMassMax or 0 +local massempty=Desc.massEmpty or 0 +local massmax=Desc.massMax or 0 +cargomax=massmax-massfuel-massempty +self:T3(self.lid..string.format("Unit name=%s: mass empty=%.1f kg, fuel=%.1f kg, max=%.1f kg ==> cargo=%.1f kg",unit:GetName(),unitweight,massfuel,massmax,cargomax)) +local bay=forcecargobay or unit:GetCargoBayFreeWeight() +table.insert(cargobay,bay) +cargobaytot=cargobaytot+bay +if bay>cargobaymax then +cargobaymax=bay +end +end +local attribute=forceattribute or self:_GetAttribute(group) +local assets={} +for i=1,n do +local asset={} +_WAREHOUSEDB.AssetID=_WAREHOUSEDB.AssetID+1 +asset.uid=_WAREHOUSEDB.AssetID +asset.templatename=templategroupname +asset.template=UTILS.DeepCopy(_DATABASE.Templates.Groups[templategroupname].Template) +asset.category=Category +asset.unittype=TypeName +asset.nunits=#asset.template.units +asset.range=RangeMin +asset.speedmax=SpeedMax +asset.size=smax +asset.weight=weight +asset.DCSdesc=Descriptors +asset.attribute=attribute +asset.cargobay=cargobay +asset.cargobaytot=cargobaytot +asset.cargobaymax=cargobaymax +asset.loadradius=loadradius +if liveries then +asset.livery=liveries[math.random(#liveries)] +end +asset.skill=skill +asset.assignment=assignment +asset.spawned=false +asset.life0=group:GetLife0() +asset.damage=0 +asset.spawngroupname=string.format("%s_AID-%d",templategroupname,asset.uid) +if i==1 then +self:_AssetItemInfo(asset) +end +_WAREHOUSEDB.Assets[asset.uid]=asset +table.insert(assets,asset) +end +return assets +end +function WAREHOUSE:_AssetItemInfo(asset) +local text=string.format("\nNew asset with id=%d for warehouse %s:\n",asset.uid,self.alias) +text=text..string.format("Spawngroup name= %s\n",asset.spawngroupname) +text=text..string.format("Template name = %s\n",asset.templatename) +text=text..string.format("Unit type = %s\n",asset.unittype) +text=text..string.format("Attribute = %s\n",asset.attribute) +text=text..string.format("Category = %d\n",asset.category) +text=text..string.format("Units # = %d\n",asset.nunits) +text=text..string.format("Speed max = %5.2f km/h\n",asset.speedmax) +text=text..string.format("Range max = %5.2f km\n",asset.range/1000) +text=text..string.format("Size max = %5.2f m\n",asset.size) +text=text..string.format("Weight total = %5.2f kg\n",asset.weight) +text=text..string.format("Cargo bay tot = %5.2f kg\n",asset.cargobaytot) +text=text..string.format("Cargo bay max = %5.2f kg\n",asset.cargobaymax) +text=text..string.format("Load radius = %s m\n",tostring(asset.loadradius)) +text=text..string.format("Skill = %s\n",tostring(asset.skill)) +text=text..string.format("Livery = %s",tostring(asset.livery)) +self:I(self.lid..text) +self:T({DCSdesc=asset.DCSdesc}) +self:T3({Template=asset.template}) +end +function WAREHOUSE:onafterNewAsset(From,Event,To,asset,assignment) +self:T(self.lid..string.format("New asset %s id=%d with assignment %s.",tostring(asset.templatename),asset.uid,tostring(assignment))) +end +function WAREHOUSE:onbeforeAddRequest(From,Event,To,warehouse,AssetDescriptor,AssetDescriptorValue,nAsset,TransportType,nTransport,Assignment,Prio) +local okay=true +if AssetDescriptor==WAREHOUSE.Descriptor.ATTRIBUTE then +local gotit=false +for _,attribute in pairs(WAREHOUSE.Attribute)do +if AssetDescriptorValue==attribute then +gotit=true +end +end +if not gotit then +self:_ErrorMessage("ERROR: Invalid request. Asset attribute is unknown!",5) +okay=false +end +elseif AssetDescriptor==WAREHOUSE.Descriptor.CATEGORY then +local gotit=false +for _,category in pairs(Group.Category)do +if AssetDescriptorValue==category then +gotit=true +end +end +if not gotit then +self:_ErrorMessage("ERROR: Invalid request. Asset category is unknown!",5) +okay=false +end +elseif AssetDescriptor==WAREHOUSE.Descriptor.GROUPNAME then +if type(AssetDescriptorValue)~="string"then +self:_ErrorMessage("ERROR: Invalid request. Asset template name must be passed as a string!",5) +okay=false +end +elseif AssetDescriptor==WAREHOUSE.Descriptor.UNITTYPE then +if type(AssetDescriptorValue)~="string"then +self:_ErrorMessage("ERROR: Invalid request. Asset unit type must be passed as a string!",5) +okay=false +end +elseif AssetDescriptor==WAREHOUSE.Descriptor.ASSIGNMENT then +if type(AssetDescriptorValue)~="string"then +self:_ErrorMessage("ERROR: Invalid request. Asset assignment type must be passed as a string!",5) +okay=false +end +elseif AssetDescriptor==WAREHOUSE.Descriptor.ASSETLIST then +if type(AssetDescriptorValue)~="table"then +self:_ErrorMessage("ERROR: Invalid request. Asset assignment type must be passed as a table!",5) +okay=false +end +else +self:_ErrorMessage("ERROR: Invalid request. Asset descriptor is not ATTRIBUTE, CATEGORY, GROUPNAME, UNITTYPE or ASSIGNMENT!",5) +okay=false +end +if self:IsStopped()then +self:_ErrorMessage("ERROR: Invalid request. Warehouse is stopped!",0) +okay=false +end +if self:IsDestroyed()and not self.respawnafterdestroyed then +self:_ErrorMessage("ERROR: Invalid request. Warehouse is destroyed!",0) +okay=false +end +return okay +end +function WAREHOUSE:onafterAddRequest(From,Event,To,warehouse,AssetDescriptor,AssetDescriptorValue,nAsset,TransportType,nTransport,Prio,Assignment) +nAsset=nAsset or 1 +TransportType=TransportType or WAREHOUSE.TransportType.SELFPROPELLED +Prio=Prio or 50 +if nTransport==nil then +if TransportType==WAREHOUSE.TransportType.SELFPROPELLED then +nTransport=0 +else +nTransport=1 +end +end +local toself=false +if self.warehouse:GetName()==warehouse.warehouse:GetName()then +toself=true +end +self.queueid=self.queueid+1 +local request={ +uid=self.queueid, +prio=Prio, +warehouse=warehouse, +assetdesc=AssetDescriptor, +assetdescval=AssetDescriptorValue, +nasset=nAsset, +transporttype=TransportType, +ntransport=nTransport, +assignment=tostring(Assignment), +airbase=warehouse:GetAirbase(), +category=warehouse:GetAirbaseCategory(), +ndelivered=0, +ntransporthome=0, +assets={}, +toself=toself, +} +table.insert(self.queue,request) +local text=string.format("Warehouse %s: New request from warehouse %s.\nDescriptor %s=%s, #assets=%s; Transport=%s, #transports =%s.", +self.alias,warehouse.alias,request.assetdesc,tostring(request.assetdescval),tostring(request.nasset),request.transporttype,tostring(request.ntransport)) +self:_DebugMessage(text,5) +end +function WAREHOUSE:onbeforeRequest(From,Event,To,Request) +self:T3({warehouse=self.alias,request=Request}) +local distance=self:GetCoordinate():Get2DDistance(Request.warehouse:GetCoordinate()) +local _assets=Request.cargoassets +if Request.nasset==0 then +local text=string.format("Warehouse %s: Request denied! Zero assets were requested.",self.alias) +self:_InfoMessage(text,10) +return false +end +for _,_asset in pairs(_assets)do +local asset=_asset +if asset.range=1 then +local text=string.format("Warehouse %s: Processing request id=%d from warehouse %s.\n",self.alias,Request.uid,Request.warehouse.alias) +text=text..string.format("Requested %s assets of %s=%s.\n",tostring(Request.nasset),Request.assetdesc,Request.assetdesc==WAREHOUSE.Descriptor.ASSETLIST and"Asset list"or Request.assetdescval) +text=text..string.format("Transports %s of type %s.",tostring(Request.ntransport),tostring(Request.transporttype)) +self:_InfoMessage(text,5) +end +Request.timestamp=timer.getAbsTime() +self:_SpawnAssetRequest(Request) +local _assetstock=Request.transportassets +local Parking={} +if Request.transportcategory==Group.Category.AIRPLANE or Request.transportcategory==Group.Category.HELICOPTER then +Parking=self:_FindParkingForAssets(self.airbase,_assetstock) +end +local _transportassets={} +for i=1,Request.ntransport do +local _assetitem=_assetstock[i] +local _alias=_assetitem.spawngroupname +_assetitem.rid=Request.uid +_assetitem.spawned=false +_assetitem.iscargo=false +_assetitem.arrived=false +local spawngroup=nil +Request.assets[_assetitem.uid]=_assetitem +if Request.transporttype==WAREHOUSE.TransportType.AIRPLANE then +spawngroup=self:_SpawnAssetAircraft(_alias,_assetitem,Request,Parking[_assetitem.uid],true) +elseif Request.transporttype==WAREHOUSE.TransportType.HELICOPTER then +spawngroup=self:_SpawnAssetAircraft(_alias,_assetitem,Request,Parking[_assetitem.uid],false) +elseif Request.transporttype==WAREHOUSE.TransportType.APC then +spawngroup=self:_SpawnAssetGroundNaval(_alias,_assetitem,Request,self.spawnzone) +elseif Request.transporttype==WAREHOUSE.TransportType.TRAIN then +self:_ErrorMessage("ERROR: Cargo transport by train not supported yet!") +return +elseif Request.transporttype==WAREHOUSE.TransportType.SHIP or Request.transporttype==WAREHOUSE.TransportType.NAVALCARRIER then +spawngroup=self:_SpawnAssetGroundNaval(_alias,_assetitem,Request,self.portzone) +elseif Request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then +self:_ErrorMessage("ERROR: Transport type selfpropelled was already handled above. We should not get here!") +return +else +self:_ErrorMessage("ERROR: Unknown transport type!") +return +end +end +Request.assetproblem={} +table.insert(self.pending,Request) +self:_DeleteQueueItem(Request,self.queue) +end +function WAREHOUSE:onafterRequestSpawned(From,Event,To,Request,CargoGroupSet,TransportGroupSet) +local _cargotype=Request.cargoattribute +local _cargocategory=Request.cargocategory +if Request.toself then +self:_DebugMessage(string.format("Selfrequest! Current status %s",self:GetState())) +self:__SelfRequest(1,CargoGroupSet,Request) +return +end +if Request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then +self:T2(self.lid..string.format("Got selfpropelled request for %d assets.",CargoGroupSet:Count())) +for _,_group in pairs(CargoGroupSet:GetSetObjects())do +local group=_group +if _cargocategory==Group.Category.GROUND then +self:T2(self.lid..string.format("Route ground group %s.",group:GetName())) +local ToCoordinate=Request.warehouse.spawnzone:GetRandomCoordinate() +if self.Debug then +ToCoordinate:MarkToAll(string.format("Destination of group %s",group:GetName())) +end +self:_RouteGround(group,Request) +elseif _cargocategory==Group.Category.AIRPLANE or _cargocategory==Group.Category.HELICOPTER then +self:T2(self.lid..string.format("Route airborne group %s.",group:GetName())) +self:_RouteAir(group) +elseif _cargocategory==Group.Category.SHIP then +self:T2(self.lid..string.format("Route naval group %s.",group:GetName())) +self:_RouteNaval(group,Request) +elseif _cargocategory==Group.Category.TRAIN then +self:T2(self.lid..string.format("Route train group %s.",group:GetName())) +self:_RouteTrain(group,Request.warehouse.rail) +else +self:E(self.lid..string.format("ERROR: unknown category %s for self propelled cargo %s!",tostring(_cargocategory),tostring(group:GetName()))) +end +end +Request.transportgroupset=TransportGroupSet +return +end +local _boardradius=500 +if Request.transporttype==WAREHOUSE.TransportType.AIRPLANE then +_boardradius=5000 +elseif Request.transporttype==WAREHOUSE.TransportType.HELICOPTER then +elseif Request.transporttype==WAREHOUSE.TransportType.APC then +elseif Request.transporttype==WAREHOUSE.TransportType.SHIP or Request.transporttype==WAREHOUSE.TransportType.AIRCRAFTCARRIER +or Request.transporttype==WAREHOUSE.TransportType.ARMEDSHIP or Request.transporttype==WAREHOUSE.TransportType.WARSHIP then +_boardradius=6000 +end +local CargoGroups=SET_CARGO:New() +for _,_group in pairs(CargoGroupSet:GetSetObjects())do +local asset=self:FindAssetInDB(_group) +local cargogroup=CARGO_GROUP:New(_group,_cargotype,_group:GetName(),_boardradius,asset.loadradius) +cargogroup:SetWeight(asset.weight) +CargoGroups:AddCargo(cargogroup) +end +local CargoTransport +if Request.transporttype==WAREHOUSE.TransportType.AIRPLANE then +local PickupAirbaseSet=SET_ZONE:New():AddZone(ZONE_AIRBASE:New(self.airbase:GetName())) +local DeployAirbaseSet=SET_ZONE:New():AddZone(ZONE_AIRBASE:New(Request.airbase:GetName())) +CargoTransport=AI_CARGO_DISPATCHER_AIRPLANE:New(TransportGroupSet,CargoGroups,PickupAirbaseSet,DeployAirbaseSet) +CargoTransport:SetHomeZone(ZONE_AIRBASE:New(self.airbase:GetName())) +elseif Request.transporttype==WAREHOUSE.TransportType.HELICOPTER then +local PickupZoneSet=SET_ZONE:New():AddZone(self.spawnzone) +local DeployZoneSet=SET_ZONE:New():AddZone(Request.warehouse.spawnzone) +CargoTransport=AI_CARGO_DISPATCHER_HELICOPTER:New(TransportGroupSet,CargoGroups,PickupZoneSet,DeployZoneSet) +CargoTransport:SetHomeZone(self.spawnzone) +elseif Request.transporttype==WAREHOUSE.TransportType.APC then +local PickupZoneSet=SET_ZONE:New():AddZone(self.spawnzone) +local DeployZoneSet=SET_ZONE:New():AddZone(Request.warehouse.spawnzone) +CargoTransport=AI_CARGO_DISPATCHER_APC:New(TransportGroupSet,CargoGroups,PickupZoneSet,DeployZoneSet,0) +CargoTransport:SetHomeZone(self.spawnzone) +elseif Request.transporttype==WAREHOUSE.TransportType.SHIP or Request.transporttype==WAREHOUSE.TransportType.AIRCRAFTCARRIER +or Request.transporttype==WAREHOUSE.TransportType.ARMEDSHIP or Request.transporttype==WAREHOUSE.TransportType.WARSHIP then +local PickupZoneSet=SET_ZONE:New():AddZone(self.portzone) +PickupZoneSet:AddZone(self.harborzone) +local DeployZoneSet=SET_ZONE:New():AddZone(Request.warehouse.harborzone) +local remotename=Request.warehouse.warehouse:GetName() +local ShippingLane=self.shippinglanes[remotename][math.random(#self.shippinglanes[remotename])] +CargoTransport=AI_CARGO_DISPATCHER_SHIP:New(TransportGroupSet,CargoGroups,PickupZoneSet,DeployZoneSet,ShippingLane) +CargoTransport:SetHomeZone(self.portzone) +else +self:E(self.lid.."ERROR: Unknown transporttype!") +end +local pickupouter=200 +local pickupinner=0 +local deployouter=200 +local deployinner=0 +if Request.transporttype==WAREHOUSE.TransportType.SHIP or Request.transporttype==WAREHOUSE.TransportType.AIRCRAFTCARRIER +or Request.transporttype==WAREHOUSE.TransportType.ARMEDSHIP or Request.transporttype==WAREHOUSE.TransportType.WARSHIP then +pickupouter=1000 +pickupinner=20 +deployouter=1000 +deployinner=0 +else +pickupouter=200 +pickupinner=0 +if self.spawnzone.Radius~=nil then +pickupouter=self.spawnzone.Radius +pickupinner=20 +end +deployouter=200 +deployinner=0 +if self.spawnzone.Radius~=nil then +deployouter=Request.warehouse.spawnzone.Radius +deployinner=20 +end +end +CargoTransport:SetPickupRadius(pickupouter,pickupinner) +CargoTransport:SetDeployRadius(deployouter,deployinner) +Request.carriercargo={} +for _,carriergroup in pairs(TransportGroupSet:GetSetObjects())do +local asset=self:FindAssetInDB(carriergroup) +for _i,_carrierunit in pairs(carriergroup:GetUnits())do +local carrierunit=_carrierunit +Request.carriercargo[carrierunit:GetName()]={} +local cargobay=asset.cargobay[_i] +carrierunit:SetCargoBayWeightLimit(cargobay) +self:T2(self.lid..string.format("Cargo bay weight limit of carrier unit %s: %.1f kg.",carrierunit:GetName(),carrierunit:GetCargoBayFreeWeight())) +end +end +function CargoTransport:OnAfterPickedUp(From,Event,To,Carrier,PickupZone) +local warehouse=Carrier:GetState(Carrier,"WAREHOUSE") +local text=string.format("Carrier group %s picked up at pickup zone %s.",Carrier:GetName(),PickupZone:GetName()) +warehouse:T(warehouse.lid..text) +end +function CargoTransport:OnAfterDeployed(From,Event,To,Carrier,DeployZone) +local warehouse=Carrier:GetState(Carrier,"WAREHOUSE") +end +function CargoTransport:OnAfterHome(From,Event,To,Carrier,Coordinate,Speed,Height,HomeZone) +local warehouse=Carrier:GetState(Carrier,"WAREHOUSE") +local text=string.format("Carrier group %s going home to zone %s.",Carrier:GetName(),HomeZone:GetName()) +warehouse:T(warehouse.lid..text) +end +function CargoTransport:OnAfterLoaded(From,Event,To,Carrier,Cargo,CarrierUnit,PickupZone) +local warehouse=Carrier:GetState(Carrier,"WAREHOUSE") +local text=string.format("Carrier group %s loaded cargo %s into unit %s in pickup zone %s",Carrier:GetName(),Cargo:GetName(),CarrierUnit:GetName(),PickupZone:GetName()) +warehouse:T(warehouse.lid..text) +local group=Cargo:GetObject() +local request=warehouse:_GetRequestOfGroup(group,warehouse.pending) +table.insert(request.carriercargo[CarrierUnit:GetName()],warehouse:_GetNameWithOut(Cargo:GetName())) +end +function CargoTransport:OnAfterUnloaded(From,Event,To,Carrier,Cargo,CarrierUnit,DeployZone) +local warehouse=Carrier:GetState(Carrier,"WAREHOUSE") +local group=Cargo:GetObject() +local text=string.format("Cargo group %s was unloaded from carrier unit %s.",tostring(group:GetName()),tostring(CarrierUnit:GetName())) +warehouse:T(warehouse.lid..text) +warehouse:Arrived(group) +end +function CargoTransport:OnAfterBackHome(From,Event,To,Carrier) +local carrier=Carrier +local warehouse=carrier:GetState(carrier,"WAREHOUSE") +carrier:SmokeWhite() +local text=string.format("Carrier %s is back home at warehouse %s.",tostring(Carrier:GetName()),tostring(warehouse.warehouse:GetName())) +MESSAGE:New(text,5):ToAllIf(warehouse.Debug) +warehouse:I(warehouse.lid..text) +warehouse:__Arrived(1,Carrier) +end +CargoTransport:__Start(5) +end +function WAREHOUSE:onafterUnloaded(From,Event,To,group) +self:_DebugMessage(string.format("Cargo %s unloaded!",tostring(group:GetName())),5) +if group and group:IsAlive()then +if self.Debug then +group:SmokeWhite() +end +local speedmax=group:GetSpeedMax() +if group:IsGround()then +if speedmax>1 then +group:RouteGroundTo(self.spawnzone:GetRandomCoordinate(),speedmax*0.5,AI.Task.VehicleFormation.RANK,3) +else +self:Arrived(group) +end +elseif group:IsAir()then +self:Arrived(group) +elseif group:IsShip()then +self:Arrived(group) +end +else +self:E(self.lid..string.format("ERROR unloaded Cargo group is not alive!")) +end +end +function WAREHOUSE:onbeforeArrived(From,Event,To,group) +local asset=self:FindAssetInDB(group) +if asset then +if asset.arrived==true then +return false +else +asset.arrived=true +return true +end +end +end +function WAREHOUSE:onafterArrived(From,Event,To,group) +if self.Debug then +group:SmokeOrange() +end +local request=self:_GetRequestOfGroup(group,self.pending) +if request then +local warehouse=request.warehouse +local istransport=self:_GroupIsTransport(group,request) +if istransport==true then +warehouse=self +elseif istransport==false then +warehouse=request.warehouse +else +self:E(self.lid..string.format("ERROR: Group %s is neither cargo nor transport",group:GetName())) +return +end +self:_DebugMessage(string.format("Group %s arrived at warehouse %s!",tostring(group:GetName()),warehouse.alias),5) +if group:IsGround()and group:GetSpeedMax()>1 then +group:RouteGroundTo(warehouse:GetCoordinate(),group:GetSpeedMax()*0.3,"Off Road") +end +self:T(self.lid.."Asset arrived at warehouse adding in 60 sec") +warehouse:__AddAsset(60,group) +end +end +function WAREHOUSE:onafterDelivered(From,Event,To,request) +if self.verbosity>=1 then +local text=string.format("Warehouse %s: All assets delivered to warehouse %s!",self.alias,request.warehouse.alias) +self:_InfoMessage(text,5) +end +if self.Debug then +self:_Fireworks(request.warehouse:GetCoordinate()) +end +self.delivered[request.uid]=true +end +function WAREHOUSE:onafterSelfRequest(From,Event,To,groupset,request) +self:_DebugMessage(string.format("Assets spawned at warehouse %s after self request!",self.alias)) +for _,_group in pairs(groupset:GetSetObjects())do +local group=_group +if self.Debug then +group:FlareGreen() +end +end +if self:IsAttacked()then +if self.autodefence then +for _,_group in pairs(groupset:GetSetObjects())do +local group=_group +local speedmax=group:GetSpeedMax() +if group:IsGround()and speedmax>1 and group:IsNotInZone(self.zone)then +group:RouteGroundTo(self.zone:GetRandomCoordinate(),0.8*speedmax,"Off Road") +end +end +end +table.insert(self.defending,request) +end +end +function WAREHOUSE:onafterAttacked(From,Event,To,Coalition,Country) +local text=string.format("Warehouse %s: We are under attack!",self.alias) +self:_InfoMessage(text) +if self.Debug then +self:GetCoordinate():SmokeOrange() +end +if self.autodefence then +local nground=self:GetNumberOfAssets(WAREHOUSE.Descriptor.CATEGORY,Group.Category.GROUND) +local text=string.format("Warehouse auto defence activated.\n") +if nground>0 then +text=text..string.format("Deploying all %d ground assets.",nground) +self:AddRequest(self,WAREHOUSE.Descriptor.CATEGORY,Group.Category.GROUND,WAREHOUSE.Quantity.ALL,nil,nil,0,"AutoDefence") +else +text=text..string.format("No ground assets currently available.") +end +self:_InfoMessage(text) +else +local text=string.format("Warehouse auto defence inactive.") +self:I(self.lid..text) +end +end +function WAREHOUSE:onafterDefeated(From,Event,To) +local text=string.format("Warehouse %s: Enemy attack was defeated!",self.alias) +self:_InfoMessage(text) +if self.Debug then +self:GetCoordinate():SmokeGreen() +end +if self.autodefence then +for _,request in pairs(self.defending)do +for _,_group in pairs(request.cargogroupset:GetSetObjects())do +local group=_group +local speed=group:GetSpeedMax() +if group:IsGround()and speed>1 then +group:RouteGroundTo(self:GetCoordinate(),speed*0.3) +end +self:__AddAsset(60,group) +end +end +self.defending=nil +self.defending={} +end +end +function WAREHOUSE:onafterRespawn(From,Event,To) +local text=string.format("Respawning warehouse %s",self.alias) +self:_InfoMessage(text) +self.warehouse:ReSpawn() +end +function WAREHOUSE:onbeforeChangeCountry(From,Event,To,Country) +local currentCountry=self:GetCountry() +local text=string.format("Warehouse %s: request to change country %d-->%d",self.alias,currentCountry,Country) +self:_DebugMessage(text,10) +if currentCountry~=Country then +return true +end +return false +end +function WAREHOUSE:onafterChangeCountry(From,Event,To,Country) +local CoalitionOld=self:GetCoalition() +self.warehouse:ReSpawn(Country) +local CoalitionNew=self:GetCoalition() +self.queue=nil +self.queue={} +if self.airbasename then +local airbase=AIRBASE:FindByName(self.airbasename) +local airbaseCoalition=airbase:GetCoalition() +if CoalitionNew==airbaseCoalition then +self.airbase=airbase +else +self.airbase=nil +end +end +if self.Debug then +if CoalitionNew==coalition.side.RED then +self:GetCoordinate():SmokeRed() +elseif CoalitionNew==coalition.side.BLUE then +self:GetCoordinate():SmokeBlue() +end +end +end +function WAREHOUSE:onbeforeCaptured(From,Event,To,Coalition,Country) +self:ChangeCountry(Country) +end +function WAREHOUSE:onafterCaptured(From,Event,To,Coalition,Country) +local text=string.format("Warehouse %s: We were captured by enemy coalition (side=%d)!",self.alias,Coalition) +self:_InfoMessage(text) +end +function WAREHOUSE:onafterAirbaseCaptured(From,Event,To,Coalition) +local text=string.format("Warehouse %s: Our airbase %s was captured by the enemy (coalition=%d)!",self.alias,self.airbasename,Coalition) +self:_InfoMessage(text) +if self.Debug then +if Coalition==coalition.side.RED then +self.airbase:GetCoordinate():SmokeRed() +elseif Coalition==coalition.side.BLUE then +self.airbase:GetCoordinate():SmokeBlue() +end +end +self.airbase=nil +end +function WAREHOUSE:onafterAirbaseRecaptured(From,Event,To,Coalition) +local text=string.format("Warehouse %s: We recaptured our airbase %s from the enemy (coalition=%d)!",self.alias,self.airbasename,Coalition) +self:_InfoMessage(text) +self.airbase=AIRBASE:FindByName(self.airbasename) +if self.Debug then +if Coalition==coalition.side.RED then +self.airbase:GetCoordinate():SmokeRed() +elseif Coalition==coalition.side.BLUE then +self.airbase:GetCoordinate():SmokeBlue() +end +end +end +function WAREHOUSE:onafterRunwayDestroyed(From,Event,To) +local text=string.format("Warehouse %s: Runway %s destroyed!",self.alias,self.airbasename) +self:_InfoMessage(text) +self.runwaydestroyed=timer.getAbsTime() +end +function WAREHOUSE:onafterRunwayRepaired(From,Event,To) +local text=string.format("Warehouse %s: Runway %s repaired!",self.alias,self.airbasename) +self:_InfoMessage(text) +self.runwaydestroyed=nil +end +function WAREHOUSE:onbeforeAssetSpawned(From,Event,To,group,asset,request) +if asset.spawned then +else +end +return true +end +function WAREHOUSE:onafterAssetSpawned(From,Event,To,group,asset,request) +local text=string.format("Asset %s from request id=%d was spawned!",asset.spawngroupname,request.uid) +self:T(self.lid..text) +asset.spawned=true +local n=0 +for _,_asset in pairs(request.assets)do +local assetitem=_asset +self:T(self.lid..string.format("Asset %s spawned %s as %s",assetitem.templatename,tostring(assetitem.spawned),tostring(assetitem.spawngroupname))) +if assetitem.spawned then +n=n+1 +else +end +end +if n==request.nasset+request.ntransport then +self:T(self.lid..string.format("All assets %d (ncargo=%d + ntransport=%d) of request rid=%d spawned. Calling RequestSpawned",n,request.nasset,request.ntransport,request.uid)) +self:RequestSpawned(request,request.cargogroupset,request.transportgroupset) +else +self:T(self.lid..string.format("Not all assets %d (ncargo=%d + ntransport=%d) of request rid=%d spawned YET",n,request.nasset,request.ntransport,request.uid)) +end +end +function WAREHOUSE:onafterAssetDead(From,Event,To,asset,request) +local text=string.format("Asset %s from request id=%d is dead!",asset.templatename,request.uid) +self:T(self.lid..text) +self:_DebugMessage(text) +end +function WAREHOUSE:onafterDestroyed(From,Event,To) +local text=string.format("Warehouse %s was destroyed! Assets lost %d. Respawn=%s",self.alias,#self.stock,tostring(self.respawnafterdestroyed)) +self:_InfoMessage(text) +if self.respawnafterdestroyed then +if self.respawndelay then +self:Pause() +self:__Respawn(self.respawndelay) +else +self:Respawn() +end +else +for k,_ in pairs(self.queue)do +self.queue[k]=nil +end +for k,_ in pairs(self.stock)do +end +for k=#self.stock,1,-1 do +self.stock[k]=nil +end +end +end +function WAREHOUSE:onafterSave(From,Event,To,path,filename) +local function _savefile(filename,data) +local f=assert(io.open(filename,"wb")) +f:write(data) +f:close() +end +filename=filename or string.format("WAREHOUSE-%d_%s.txt",self.uid,self.alias) +if path~=nil then +filename=path.."\\"..filename +end +local text=string.format("Saving warehouse assets to file %s",filename) +MESSAGE:New(text,30):ToAllIf(self.Debug or self.Report) +self:I(self.lid..text) +local warehouseassets="" +warehouseassets=warehouseassets..string.format("coalition=%d\n",self:GetCoalition()) +warehouseassets=warehouseassets..string.format("country=%d\n",self:GetCountry()) +for _,_asset in pairs(self.stock)do +local asset=_asset +local assetstring="" +for key,value in pairs(asset)do +if key=="templatename"or key=="attribute"or key=="cargobay"or key=="weight"or key=="loadradius"or key=="livery"or key=="skill"or key=="assignment"then +local name +if type(value)=="table"then +name=string.format("%s=%s;",key,value[1]) +else +name=string.format("%s=%s;",key,value) +end +assetstring=assetstring..name +end +self:I(string.format("Loaded asset: %s",assetstring)) +end +warehouseassets=warehouseassets..assetstring.."\n" +end +_savefile(filename,warehouseassets) +end +function WAREHOUSE:onbeforeLoad(From,Event,To,path,filename) +local function _fileexists(name) +local f=io.open(name,"r") +if f~=nil then +io.close(f) +return true +else +return false +end +end +filename=filename or string.format("WAREHOUSE-%d_%s.txt",self.uid,self.alias) +if path~=nil then +filename=path.."\\"..filename +end +local exists=_fileexists(filename) +if exists then +return true +else +self:_ErrorMessage(string.format("ERROR: file %s does not exist! Cannot load assets.",filename),60) +return false +end +end +function WAREHOUSE:onafterLoad(From,Event,To,path,filename) +local function _loadfile(filename) +local f=assert(io.open(filename,"rb")) +local data=f:read("*all") +f:close() +return data +end +filename=filename or string.format("WAREHOUSE-%d_%s.txt",self.uid,self.alias) +if path~=nil then +filename=path.."\\"..filename +end +local text=string.format("Loading warehouse assets from file %s",filename) +MESSAGE:New(text,30):ToAllIf(self.Debug or self.Report) +self:I(self.lid..text) +local data=_loadfile(filename) +local assetdata=UTILS.Split(data,"\n") +local Coalition +local Country +local assets={} +for _,asset in pairs(assetdata)do +local descriptors=UTILS.Split(asset,";") +local asset={} +local isasset=false +for _,descriptor in pairs(descriptors)do +local keyval=UTILS.Split(descriptor,"=") +if#keyval==2 then +if keyval[1]=="coalition"then +Coalition=tonumber(keyval[2]) +elseif keyval[1]=="country"then +Country=tonumber(keyval[2]) +else +isasset=true +local key=keyval[1] +local val=keyval[2] +if val=="nil"then +val=nil +end +if key=="cargobay"or key=="weight"or key=="loadradius"then +asset[key]=tonumber(val) +else +asset[key]=val +end +end +end +end +if isasset then +table.insert(assets,asset) +end +end +if Country~=self:GetCountry()then +self:T(self.lid..string.format("Changing warehouse country %d-->%d on loading assets.",self:GetCountry(),Country)) +self:ChangeCountry(Country) +end +for _,_asset in pairs(assets)do +local asset=_asset +local group=GROUP:FindByName(asset.templatename) +if group then +self:AddAsset(group,1,asset.attribute,asset.cargobay,asset.weight,asset.loadradius,asset.skill,asset.livery,asset.assignment) +else +self:E(string.format("ERROR: Group %s doest not exit. Cannot be loaded as asset.",tostring(asset.templatename))) +end +end +end +function WAREHOUSE:_SpawnAssetRequest(Request) +self:F2({requestUID=Request.uid}) +local cargoassets=Request.cargoassets +local Parking={} +if Request.cargocategory==Group.Category.AIRPLANE or Request.cargocategory==Group.Category.HELICOPTER then +Parking=self:_FindParkingForAssets(self.airbase,cargoassets)or{} +end +local UnControlled=true +for i=1,#cargoassets do +local asset=cargoassets[i] +asset.spawned=false +asset.iscargo=true +asset.rid=Request.uid +local _alias=asset.spawngroupname +Request.assets[asset.uid]=asset +local _group=nil +if asset.category==Group.Category.GROUND then +_group=self:_SpawnAssetGroundNaval(_alias,asset,Request,self.spawnzone) +elseif asset.category==Group.Category.AIRPLANE or asset.category==Group.Category.HELICOPTER then +if Parking[asset.uid]then +_group=self:_SpawnAssetAircraft(_alias,asset,Request,Parking[asset.uid],UnControlled) +else +_group=self:_SpawnAssetAircraft(_alias,asset,Request,nil,UnControlled) +end +elseif asset.category==Group.Category.TRAIN then +if self.rail then +_group=self:_SpawnAssetGroundNaval(_alias,asset,Request,self.spawnzone) +end +elseif asset.category==Group.Category.SHIP then +_group=self:_SpawnAssetGroundNaval(_alias,asset,Request,self.portzone) +else +self:E(self.lid.."ERROR: Unknown asset category!") +end +end +end +function WAREHOUSE:_SpawnAssetGroundNaval(alias,asset,request,spawnzone,aioff) +if asset and(asset.category==Group.Category.GROUND or asset.category==Group.Category.SHIP or asset.category==Group.Category.TRAIN)then +local template=self:_SpawnAssetPrepareTemplate(asset,alias) +template.route.points[1]={} +local coord=spawnzone:GetRandomCoordinate() +if asset.category==Group.Category.TRAIN then +coord=self.rail +end +for i=1,#template.units do +local unit=template.units[i] +local SX=unit.x or 0 +local SY=unit.y or 0 +local BX=asset.template.route.points[1].x +local BY=asset.template.route.points[1].y +local TX=coord.x+(SX-BX) +local TY=coord.z+(SY-BY) +template.units[i].x=TX +template.units[i].y=TY +if asset.livery then +unit.livery_id=asset.livery +end +if asset.skill then +unit.skill=asset.skill +end +end +template.route.points[1].x=coord.x +template.route.points[1].y=coord.z +template.x=coord.x +template.y=coord.z +template.alt=coord.y +local group=_DATABASE:Spawn(template) +if aioff then +group:SetAIOff() +end +return group +end +return nil +end +function WAREHOUSE:_SpawnAssetAircraft(alias,asset,request,parking,uncontrolled,hotstart) +if asset and asset.category==Group.Category.AIRPLANE or asset.category==Group.Category.HELICOPTER then +local template=self:_SpawnAssetPrepareTemplate(asset,alias) +if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then +if request.toself then +local wp=self.airbase:GetCoordinate():WaypointAir("RADIO",COORDINATE.WaypointType.TakeOffParking,COORDINATE.WaypointAction.FromParkingArea,0,false,self.airbase,{},"Parking") +template.route.points={wp} +else +template.route.points=self:_GetFlightplan(asset,self.airbase,request.warehouse.airbase) +end +else +local _type=COORDINATE.WaypointType.TakeOffParking +local _action=COORDINATE.WaypointAction.FromParkingArea +if hotstart then +_type=COORDINATE.WaypointType.TakeOffParkingHot +_action=COORDINATE.WaypointAction.FromParkingAreaHot +end +template.route.points[1]=self.airbase:GetCoordinate():WaypointAir("BARO",_type,_action,0,true,self.airbase,nil,"Spawnpoint") +end +local AirbaseID=self.airbase:GetID() +local AirbaseCategory=self:GetAirbaseCategory() +if AirbaseCategory==Airbase.Category.HELIPAD or AirbaseCategory==Airbase.Category.SHIP then +else +if#parking<#template.units then +local text=string.format("ERROR: Not enough parking! Free parking = %d < %d aircraft to be spawned.",#parking,#template.units) +self:_DebugMessage(text) +return nil +end +end +for i=1,#template.units do +local unit=template.units[i] +if AirbaseCategory==Airbase.Category.HELIPAD or AirbaseCategory==Airbase.Category.SHIP then +local coord=self.airbase:GetCoordinate() +unit.x=coord.x +unit.y=coord.z +unit.alt=coord.y +unit.parking_id=nil +unit.parking=nil +else +local coord=parking[i].Coordinate +local terminal=parking[i].TerminalID +if self.Debug then +coord:MarkToAll(string.format("Spawnplace unit %s terminal %d.",unit.name,terminal)) +end +unit.x=coord.x +unit.y=coord.z +unit.alt=coord.y +unit.parking_id=nil +unit.parking=terminal +end +if asset.livery then +unit.livery_id=asset.livery +end +if asset.skill then +unit.skill=asset.skill +end +if asset.payload then +unit.payload=asset.payload.pylons +end +if asset.modex then +unit.onboard_num=asset.modex[i] +end +if asset.callsign then +unit.callsign=asset.callsign[i] +end +end +template.x=template.units[1].x +template.y=template.units[1].y +template.uncontrolled=uncontrolled +self:T2({airtemplate=template}) +local group=_DATABASE:Spawn(template) +return group +end +return nil +end +function WAREHOUSE:_SpawnAssetPrepareTemplate(asset,alias) +local template=UTILS.DeepCopy(asset.template) +template.name=alias +template.CoalitionID=self:GetCoalition() +template.CountryID=self:GetCountry() +template.groupId=nil +template.lateActivation=false +if asset.missionTask then +self:T(self.lid..string.format("Setting mission task to %s",tostring(asset.missionTask))) +template.task=asset.missionTask +end +template.route={} +template.route.routeRelativeTOT=true +template.route.points={} +for i=1,#template.units do +local unit=template.units[i] +unit.unitId=nil +unit.name=string.format("%s-%02d",template.name,i) +end +return template +end +function WAREHOUSE:_RouteGround(group,request) +if group and group:IsAlive()then +local _speed=group:GetSpeedMax()*0.7 +local Waypoints={} +local hasoffroad=self:HasConnectionOffRoad(request.warehouse,self.Debug) +if hasoffroad then +local remotename=request.warehouse.warehouse:GetName() +local path=self.offroadpaths[remotename][math.random(#self.offroadpaths[remotename])] +for i=1,#path do +local coord=path[i] +local Waypoint=coord:WaypointGround(_speed,"Off Road") +table.insert(Waypoints,Waypoint) +end +else +Waypoints=group:TaskGroundOnRoad(request.warehouse.road,_speed,"Off Road",false,self.road) +local FromWP=group:GetCoordinate():WaypointGround(_speed,"Off Road") +table.insert(Waypoints,1,FromWP) +end +for n,wp in ipairs(Waypoints)do +env.info(n) +local tf=self:_SimpleTaskFunctionWP("warehouse:_PassingWaypoint",group,n,#Waypoints) +group:SetTaskWaypoint(wp,tf) +end +group:Route(Waypoints,1) +group:OptionROEReturnFire() +group:OptionAlarmStateGreen() +end +end +function WAREHOUSE:_RouteNaval(group,request) +if group and group:IsAlive()then +local _speed=group:GetSpeedMax()*0.8 +local remotename=request.warehouse.warehouse:GetName() +local lane=self.shippinglanes[remotename][math.random(#self.shippinglanes[remotename])] +if lane then +local Waypoints={} +for i=1,#lane do +local coord=lane[i] +local Waypoint=coord:WaypointGround(_speed) +table.insert(Waypoints,Waypoint) +end +local TaskFunction=self:_SimpleTaskFunction("warehouse:_Arrived",group) +local Waypoint=Waypoints[#Waypoints] +group:SetTaskWaypoint(Waypoint,TaskFunction) +group:Route(Waypoints,1) +group:OptionROEReturnFire() +else +self:E(self.lid..string.format("ERROR: No shipping lane defined for Naval asset!")) +end +end +end +function WAREHOUSE:_RouteAir(aircraft) +if aircraft and aircraft:IsAlive()~=nil then +self:T2(self.lid..string.format("RouteAir aircraft group %s alive=%s",aircraft:GetName(),tostring(aircraft:IsAlive()))) +local starttime=math.random(60) +aircraft:StartUncontrolled(starttime) +self:T2(self.lid..string.format("RouteAir aircraft group %s alive=%s (after start command)",aircraft:GetName(),tostring(aircraft:IsAlive()))) +aircraft:OptionROEReturnFire() +aircraft:OptionROTPassiveDefense() +else +self:E(string.format("ERROR: aircraft %s cannot be routed since it does not exist or is not alive %s!",tostring(aircraft:GetName()),tostring(aircraft:IsAlive()))) +end +end +function WAREHOUSE:_RouteTrain(Group,Coordinate,Speed) +if Group and Group:IsAlive()then +local _speed=Speed or Group:GetSpeedMax()*0.6 +local Waypoints=Group:TaskGroundOnRailRoads(Coordinate,Speed) +local TaskFunction=self:_SimpleTaskFunction("warehouse:_Arrived",Group) +local Waypoint=Waypoints[#Waypoints] +Group:SetTaskWaypoint(Waypoint,TaskFunction) +Group:Route(Waypoints,1) +end +end +function WAREHOUSE:_Arrived(group) +self:_DebugMessage(string.format("Group %s arrived!",tostring(group:GetName()))) +if group then +self:__Arrived(1,group) +end +end +function WAREHOUSE:_PassingWaypoint(group,n,N) +self:T(self.lid..string.format("Group %s passing waypoint %d of %d!",tostring(group:GetName()),n,N)) +if n==N then +self:__Arrived(1,group) +end +end +function WAREHOUSE:GetAssetByID(id) +if id then +return _WAREHOUSEDB.Assets[id] +else +return nil +end +end +function WAREHOUSE:GetAssetByName(GroupName) +local name=self:_GetNameWithOut(GroupName) +local _,aid,_=self:_GetIDsFromGroup(GROUP:FindByName(name)) +if aid then +return _WAREHOUSEDB.Assets[aid] +else +return nil +end +end +function WAREHOUSE:GetRequestByID(id) +if id then +for _,_request in pairs(self.queue)do +local request=_request +if request.uid==id then +return request,true +end +end +for _,_request in pairs(self.pending)do +local request=_request +if request.uid==id then +return request,false +end +end +end +return nil,nil +end +function WAREHOUSE:_OnEventBirth(EventData) +self:T3(self.lid..string.format("Warehouse %s (id=%s) captured event birth!",self.alias,self.uid)) +if EventData and EventData.IniGroup then +local group=EventData.IniGroup +local wid,aid,rid=self:_GetIDsFromGroup(group) +if wid==self.uid then +local asset=self:GetAssetByID(aid) +local request=self:GetRequestByID(rid) +if asset and request then +self:T(self.lid..string.format("Warehouse %s captured event birth of request ID=%d, asset ID=%d, unit %s spawned=%s",self.alias,request.uid,asset.uid,EventData.IniUnitName,tostring(asset.spawned))) +request.born=true +if not asset.spawned then +self:_DeleteStockItem(asset) +asset.spawned=true +asset.spawngroupname=group:GetName() +if asset.iscargo==true then +request.cargogroupset=request.cargogroupset or SET_GROUP:New() +request.cargogroupset:AddGroup(group) +else +request.transportgroupset=request.transportgroupset or SET_GROUP:New() +request.transportgroupset:AddGroup(group) +end +group:SetState(group,"WAREHOUSE",self) +self:AssetSpawned(group,asset,request) +end +else +self:E(self.lid..string.format("ERROR: Either asset AID=%s or request RID=%s are nil in event birth of unit %s",tostring(aid),tostring(rid),tostring(EventData.IniUnitName))) +end +else +end +end +end +function WAREHOUSE:_OnEventEngineStartup(EventData) +self:T3(self.lid..string.format("Warehouse %s captured event engine startup!",self.alias)) +if EventData and EventData.IniGroup then +local group=EventData.IniGroup +local wid,aid,rid=self:_GetIDsFromGroup(group) +if wid==self.uid then +self:T(self.lid..string.format("Warehouse %s captured event engine startup of its asset unit %s.",self.alias,EventData.IniUnitName)) +end +end +end +function WAREHOUSE:_OnEventTakeOff(EventData) +self:T3(self.lid..string.format("Warehouse %s captured event takeoff!",self.alias)) +if EventData and EventData.IniGroup then +local group=EventData.IniGroup +local wid,aid,rid=self:_GetIDsFromGroup(group) +if wid==self.uid then +self:T(self.lid..string.format("Warehouse %s captured event takeoff of its asset unit %s.",self.alias,EventData.IniUnitName)) +end +end +end +function WAREHOUSE:_OnEventLanding(EventData) +self:T3(self.lid..string.format("Warehouse %s captured event landing!",self.alias)) +if EventData and EventData.IniGroup then +local group=EventData.IniGroup +local wid,aid,rid=self:_GetIDsFromGroup(group) +if wid~=nil and wid==self.uid then +self:T(self.lid..string.format("Warehouse %s captured event landing of its asset unit %s.",self.alias,EventData.IniUnitName)) +end +end +end +function WAREHOUSE:_OnEventEngineShutdown(EventData) +self:T3(self.lid..string.format("Warehouse %s captured event engine shutdown!",self.alias)) +if EventData and EventData.IniGroup then +local group=EventData.IniGroup +local wid,aid,rid=self:_GetIDsFromGroup(group) +if wid==self.uid then +self:T(self.lid..string.format("Warehouse %s captured event engine shutdown of its asset unit %s.",self.alias,EventData.IniUnitName)) +end +end +end +function WAREHOUSE:_OnEventArrived(EventData) +if EventData and EventData.IniUnit then +local unit=EventData.IniUnit +if unit and unit:IsAlive()==true and unit:InAir()==false then +local group=EventData.IniGroup +local wid,aid,rid=self:_GetIDsFromGroup(group) +if wid~=nil and aid~=nil and rid~=nil then +if self.uid==wid then +local request=self:_GetRequestOfGroup(group,self.pending) +if request then +local istransport=self:_GroupIsTransport(group,request) +local closest=group:GetCoordinate():GetClosestAirbase() +local rightairbase=closest:GetName()==request.warehouse:GetAirbase():GetName() +if istransport==false and rightairbase then +local nunits=#group:GetUnits() +local dt=10*(nunits-1)+1 +if self.verbosity>=1 then +local text=string.format("Air asset group %s from warehouse %s arrived at its destination. Trigger Arrived event in %d sec",group:GetName(),self.alias,dt) +self:_InfoMessage(text) +end +self:__Arrived(dt,group) +end +end +end +else +self:T3(string.format("Group that arrived did not belong to a warehouse. Warehouse ID=%s, Asset ID=%s, Request ID=%s.",tostring(wid),tostring(aid),tostring(rid))) +end +end +end +end +function WAREHOUSE:_OnEventCrashOrDead(EventData) +self:T3(self.lid..string.format("Warehouse %s captured event dead or crash!",self.alias)) +if EventData then +if EventData.IniUnitName then +local warehousename=self.warehouse:GetName() +if EventData.IniUnitName==warehousename then +self:_DebugMessage(string.format("Warehouse %s alias %s was destroyed!",warehousename,self.alias)) +self:Destroyed() +end +if self.airbase and self.airbasename and self.airbasename==EventData.IniUnitName then +self:RunwayDestroyed() +end +end +if EventData.IniGroup then +local group=EventData.IniGroup +local wid,aid,rid=self:_GetIDsFromGroup(group) +if wid==self.uid then +self:T(self.lid..string.format("Warehouse %s captured event dead or crash of its asset unit %s.",self.alias,EventData.IniUnitName)) +for _,request in pairs(self.pending)do +local request=request +if request.uid==rid then +self:_UnitDead(EventData.IniUnit,request) +end +end +end +end +end +end +function WAREHOUSE:_UnitDead(deadunit,request) +if self.Debug then +deadunit:FlareRed() +end +local group=deadunit:GetGroup() +local nalive=group:CountAliveUnits() +local groupdead=true +if nalive>0 then +groupdead=false +end +local unitname=self:_GetNameWithOut(deadunit) +local groupname=self:_GetNameWithOut(group) +if groupdead then +self:T(self.lid..string.format("Group %s (transport=%s) is dead!",groupname,tostring(self:_GroupIsTransport(group,request)))) +if self.Debug then +group:SmokeWhite() +end +local asset=self:FindAssetInDB(group) +self:AssetDead(asset,request) +end +local NoTriggerEvent=true +if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then +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 +local istransport=self:_GroupIsTransport(group,request) +if istransport==true then +local cargogroupnames=request.carriercargo[unitname] +if cargogroupnames then +for _,cargoname in pairs(cargogroupnames)do +request.cargogroupset:Remove(cargoname,NoTriggerEvent) +self:T(self.lid..string.format("Removed transported cargo %s inside dead carrier %s: ncargo=%d",cargoname,unitname,request.cargogroupset:Count())) +end +end +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 +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())) +end +else +self:E(self.lid..string.format("ERROR: Group %s is neither cargo nor transport!",group:GetName())) +end +end +end +function WAREHOUSE:_OnEventBaseCaptured(EventData) +self:T3(self.lid..string.format("Warehouse %s captured event base captured!",self.alias)) +if self.airbasename==nil then +return +end +if EventData and EventData.Place then +local airbase=EventData.Place +if EventData.PlaceName==self.airbasename then +local NewCoalitionAirbase=airbase:GetCoalition() +self:T(self.lid..string.format("Airbase of warehouse %s (coalition ID=%d) was captured! New owner coalition ID=%d.",self.alias,self:GetCoalition(),NewCoalitionAirbase)) +if self.airbase==nil then +if NewCoalitionAirbase==self:GetCoalition()then +self:AirbaseRecaptured(NewCoalitionAirbase) +end +else +if NewCoalitionAirbase~=self:GetCoalition()then +self:AirbaseCaptured(NewCoalitionAirbase) +end +end +end +end +end +function WAREHOUSE:_OnEventMissionEnd(EventData) +self:T3(self.lid..string.format("Warehouse %s captured event mission end!",self.alias)) +if self.autosave then +self:Save(self.autosavepath,self.autosavefile) +end +end +function WAREHOUSE:_CheckConquered() +local coord=self.zone:GetCoordinate() +local radius=self.zone:GetRadius() +local gotunits,_,_,units,_,_=coord:ScanObjects(radius,true,false,false) +local Nblue=0 +local Nred=0 +local Nneutral=0 +local CountryBlue=nil +local CountryRed=nil +local CountryNeutral=nil +if gotunits then +for _,_unit in pairs(units)do +local unit=_unit +local distance=coord:Get2DDistance(unit:GetCoordinate()) +if unit:IsGround()and unit:IsAlive()and distance<=radius then +local _coalition=unit:GetCoalition() +local _country=unit:GetCountry() +self:T2(self.lid..string.format("Unit %s in warehouse zone of radius=%d m. Coalition=%d, country=%d. Distance = %d m.",unit:GetName(),radius,_coalition,_country,distance)) +if _coalition==coalition.side.BLUE then +Nblue=Nblue+1 +CountryBlue=_country +elseif _coalition==coalition.side.RED then +Nred=Nred+1 +CountryRed=_country +else +Nneutral=Nneutral+1 +CountryNeutral=_country +end +end +end +end +self:T(self.lid..string.format("Ground troops in warehouse zone: blue=%d, red=%d, neutral=%d",Nblue,Nred,Nneutral)) +local newcoalition=self:GetCoalition() +local newcountry=self:GetCountry() +if Nblue>0 and Nred==0 and Nneutral==0 then +newcoalition=coalition.side.BLUE +newcountry=CountryBlue +elseif Nblue==0 and Nred>0 and Nneutral==0 then +newcoalition=coalition.side.RED +newcountry=CountryRed +elseif Nblue==0 and Nred==0 and Nneutral>0 then +end +if self:IsAttacked()and newcoalition~=self:GetCoalition()then +self:Captured(newcoalition,newcountry) +return +end +if self:GetCoalition()==coalition.side.BLUE then +if self:IsRunning()and Nred>0 then +self:Attacked(coalition.side.RED,CountryRed) +end +if self:IsAttacked()and Nred==0 then +self:Defeated() +end +elseif self:GetCoalition()==coalition.side.RED then +if self:IsRunning()and Nblue>0 then +self:Attacked(coalition.side.BLUE,CountryBlue) +end +if self:IsAttacked()and Nblue==0 then +self:Defeated() +end +elseif self:GetCoalition()==coalition.side.NEUTRAL then +if self:IsRunning()and Nred>0 then +self:Attacked(coalition.side.RED,CountryRed) +elseif self:IsRunning()and Nblue>0 then +self:Attacked(coalition.side.BLUE,CountryBlue) +end +end +end +function WAREHOUSE:_CheckAirbaseOwner() +if self.airbasename then +local airbase=AIRBASE:FindByName(self.airbasename) +local airbasecurrentcoalition=airbase:GetCoalition() +if self.airbase then +if self:GetCoalition()~=airbasecurrentcoalition then +self.airbase=nil +end +else +if self:GetCoalition()==airbasecurrentcoalition then +self.airbase=airbase +end +end +end +end +function WAREHOUSE:_CheckRequestConsistancy(queue) +self:T3(self.lid..string.format("Number of queued requests = %d",#queue)) +local invalid={} +for _,_request in pairs(queue)do +local request=_request +self:T2(self.lid..string.format("Checking request id=%d.",request.uid)) +local valid=true +if request.nasset==0 then +self:E(self.lid..string.format("ERROR: INVALID request. Request for zero assets not possible. Can happen when, e.g. \"all\" ground assets are requests but none in stock.")) +valid=false +end +if self:GetCoalition()~=request.warehouse:GetCoalition()then +self:E(self.lid..string.format("ERROR: INVALID request. Requesting warehouse is of wrong coaltion! Own coalition %s != %s of requesting warehouse.",self:GetCoalitionName(),request.warehouse:GetCoalitionName())) +valid=false +end +if request.warehouse:IsStopped()then +self:E(self.lid..string.format("ERROR: INVALID request. Requesting warehouse is stopped!")) +valid=false +end +if request.warehouse:IsDestroyed()and not self.respawnafterdestroyed then +self:E(self.lid..string.format("ERROR: INVALID request. Requesting warehouse is destroyed!")) +valid=false +end +if valid==false then +self:E(self.lid..string.format("Got invalid request id=%d.",request.uid)) +table.insert(invalid,request) +else +self:T3(self.lid..string.format("Got valid request id=%d.",request.uid)) +end +end +for _,_request in pairs(invalid)do +self:E(self.lid..string.format("Deleting INVALID request id=%d.",_request.uid)) +self:_DeleteQueueItem(_request,self.queue) +end +end +function WAREHOUSE:_CheckRequestValid(request) +local _assets,_nassets,_enough=self:_FilterStock(self.stock,request.assetdesc,request.assetdescval,request.nasset) +if#_assets==0 then +return true +end +local nasset=request.nasset +if type(request.nasset)=="string"then +nasset=self:_QuantityRel2Abs(request.nasset,_nassets) +end +local text=string.format("Request valid? Number of assets: requested=%s=%d, selected=%d, total=%d, enough=%s.",tostring(request.nasset),nasset,#_assets,_nassets,tostring(_enough)) +self:T(text) +local asset=_assets[1] +local asset_plane=asset.category==Group.Category.AIRPLANE +local asset_helo=asset.category==Group.Category.HELICOPTER +local asset_ground=asset.category==Group.Category.GROUND +local asset_train=asset.category==Group.Category.TRAIN +local asset_naval=asset.category==Group.Category.SHIP +local asset_air=asset_helo or asset_plane +local valid=true +local requestcategory=request.warehouse:GetAirbaseCategory() +if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then +if asset_air then +if asset_plane then +if requestcategory==Airbase.Category.HELIPAD or self:GetAirbaseCategory()==Airbase.Category.HELIPAD then +self:E("ERROR: Incorrect request. Asset airplane requested but warehouse or requestor is HELIPAD/FARP!") +valid=false +end +elseif asset_helo then +if self:GetAirbaseCategory()==-1 or requestcategory==-1 then +self:E("ERROR: Incorrect request. Helos need a AIRBASE/HELIPAD/SHIP as home/destination base!") +valid=false +end +end +if self.airbase==nil or request.airbase==nil then +self:E("ERROR: Incorrect request. Either warehouse or requesting warehouse does not have any kind of airbase!") +valid=false +else +local termtype_dep=asset.terminalType or self:_GetTerminal(asset.attribute,self:GetAirbaseCategory()) +local termtype_des=asset.terminalType or self:_GetTerminal(asset.attribute,request.warehouse:GetAirbaseCategory()) +local np_departure=self.airbase:GetParkingSpotsNumber(termtype_dep) +local np_destination=request.airbase:GetParkingSpotsNumber(termtype_des) +self:T(string.format("Asset attribute = %s, DEPARTURE: terminal type = %d, spots = %d, DESTINATION: terminal type = %d, spots = %d",asset.attribute,termtype_dep,np_departure,termtype_des,np_destination)) +if np_departure0 then +_assetattribute=_assets[1].attribute +_assetcategory=_assets[1].category +if _assetcategory==Group.Category.AIRPLANE or _assetcategory==Group.Category.HELICOPTER then +if self.airbase and self.airbase:GetCoalition()==self:GetCoalition()then +if self:IsRunwayOperational()then +local Parking=self:_FindParkingForAssets(self.airbase,_assets) +if Parking==nil then +local text=string.format("Warehouse %s: Request denied! Not enough free parking spots for all requested assets at the moment.",self.alias) +self:_InfoMessage(text,5) +return false +end +else +local text=string.format("Warehouse %s: Request denied! Runway is still destroyed",self.alias) +self:_InfoMessage(text,5) +return false +end +else +local text=string.format("Warehouse %s: Request denied! No airbase",self.alias) +self:_InfoMessage(text,5) +return false +end +end +request.cargoassets=_assets +end +if request.transporttype~=WAREHOUSE.TransportType.SELFPROPELLED then +_transports=self:_GetTransportsForAssets(request) +if#_transports>0 then +local _transportattribute=_transports[1].attribute +local _transportcategory=_transports[1].category +if _transportcategory==Group.Category.AIRPLANE or _transportcategory==Group.Category.HELICOPTER then +if self.airbase and self.airbase:GetCoalition()==self:GetCoalition()then +if self:IsRunwayOperational()then +local Parking=self:_FindParkingForAssets(self.airbase,_transports) +if Parking==nil then +local text=string.format("Warehouse %s: Request denied! Not enough free parking spots for all transports at the moment.",self.alias) +self:_InfoMessage(text,5) +return false +end +else +local text=string.format("Warehouse %s: Request denied! Runway is still destroyed",self.alias) +self:_InfoMessage(text,5) +return false +end +else +local text=string.format("Warehouse %s: Request denied! No airbase currently!",self.alias) +self:_InfoMessage(text,5) +return false +end +end +else +local text=string.format("Warehouse %s: Request denied! Not enough transport carriers available at the moment.",self.alias) +self:_InfoMessage(text,5) +return false +end +else +if _assetcategory==Group.Category.GROUND then +local dist=self.warehouse:GetCoordinate():Get2DDistance(self.spawnzone:GetCoordinate()) +if dist>self.spawnzonemaxdist then +local text=string.format("Warehouse %s: Request denied! Not close enough to spawn zone. Distance = %d m. We need to be at least within %d m range to spawn.",self.alias,dist,self.spawnzonemaxdist) +self:_InfoMessage(text,5) +return false +end +end +end +request.cargoassets=_assets +request.cargoattribute=_assets[1].attribute +request.cargocategory=_assets[1].category +request.nasset=#_assets +local text=string.format("Selected cargo assets, attibute=%s, category=%d:\n",request.cargoattribute,request.cargocategory) +for _i,_asset in pairs(_assets)do +local asset=_asset +text=text..string.format("%d) name=%s, type=%s, category=%d, #units=%d",_i,asset.templatename,asset.unittype,asset.category,asset.nunits) +end +self:T(self.lid..text) +if request.transporttype~=WAREHOUSE.TransportType.SELFPROPELLED then +request.transportassets=_transports +request.transportattribute=_transports[1].attribute +request.transportcategory=_transports[1].category +request.ntransport=#_transports +local text=string.format("Selected transport assets, attibute=%s, category=%d:\n",request.transportattribute,request.transportcategory) +for _i,_asset in pairs(_transports)do +local asset=_asset +text=text..string.format("%d) name=%s, type=%s, category=%d, #units=%d\n",_i,asset.templatename,asset.unittype,asset.category,asset.nunits) +end +self:T(self.lid..text) +end +return true +end +function WAREHOUSE:_GetTransportsForAssets(request) +local transports=self:_FilterStock(self.stock,WAREHOUSE.Descriptor.ATTRIBUTE,request.transporttype,nil,true) +local cargoassets=UTILS.DeepCopy(request.cargoassets) +local cargoset=request.transportcargoset +local function sort_transports(a,b) +return a.cargobaymax>b.cargobaymax +end +local function sort_cargoassets(a,b) +return a.weight>b.weight +end +table.sort(transports,sort_transports) +table.sort(cargoassets,sort_cargoassets) +self:T2(self.lid.."Transport capability:") +local totalbay=0 +for i=1,#transports do +local transport=transports[i] +for j=1,transport.nunits do +totalbay=totalbay+transport.cargobay[j] +self:T2(self.lid..string.format("Cargo bay = %d (unit=%d)",transport.cargobay[j],j)) +end +end +self:T2(self.lid..string.format("Total capacity = %d",totalbay)) +self:T2(self.lid.."Cargo weight:") +local totalcargoweight=0 +for i=1,#cargoassets do +local asset=cargoassets[i] +totalcargoweight=totalcargoweight+asset.weight +self:T2(self.lid..string.format("weight = %d",asset.weight)) +end +self:T2(self.lid..string.format("Total weight = %d",totalcargoweight)) +local used_transports={} +for i=1,#transports do +local transport=transports[i] +local putintocarrier={} +local used=false +for k=1,transport.nunits do +local cargobay=transport.cargobay[k] +for j,asset in pairs(cargoassets)do +local asset=asset +local delta=cargobay-asset.weight +if delta>=0 then +cargobay=cargobay-asset.weight +self:T3(self.lid..string.format("%s unit %d loads cargo uid=%d: bayempty=%02d, bayloaded = %02d - weight=%02d",transport.templatename,k,asset.uid,transport.cargobay[k],cargobay,asset.weight)) +table.insert(putintocarrier,j) +used=true +else +self:T2(self.lid..string.format("Carrier unit %s too small for cargo asset %s ==> cannot be used! Cargo bay - asset weight = %d kg",transport.templatename,asset.templatename,delta)) +end +end +end +for j=#putintocarrier,1,-1 do +local nput=putintocarrier[j] +local cargo=cargoassets[nput] +if cargo then +self:T2(self.lid..string.format("Cargo id=%d assigned for carrier id=%d",cargo.uid,transport.uid)) +table.remove(cargoassets,nput) +end +end +if used then +table.insert(used_transports,transport) +end +local ntrans=self:_QuantityRel2Abs(request.ntransport,#transports) +if#used_transports>=ntrans then +request.ntransport=#used_transports +break +end +end +local text=string.format("Used Transports for request %d to warehouse %s:\n",request.uid,request.warehouse.alias) +local totalcargobay=0 +for _i,_transport in pairs(used_transports)do +local transport=_transport +text=text..string.format("%d) %s: cargobay tot = %d kg, cargobay max = %d kg, nunits=%d\n",_i,transport.unittype,transport.cargobaytot,transport.cargobaymax,transport.nunits) +totalcargobay=totalcargobay+transport.cargobaytot +end +text=text..string.format("Total cargo bay capacity = %.1f kg\n",totalcargobay) +text=text..string.format("Total cargo weight = %.1f kg\n",totalcargoweight) +text=text..string.format("Minimum number of runs = %.1f",totalcargoweight/totalcargobay) +self:_DebugMessage(text) +return used_transports +end +function WAREHOUSE:_QuantityRel2Abs(relative,ntot) +local nabs=0 +if type(relative)=="string"then +if relative==WAREHOUSE.Quantity.ALL then +nabs=ntot +elseif relative==WAREHOUSE.Quantity.THREEQUARTERS then +nabs=UTILS.Round(ntot*3/4) +elseif relative==WAREHOUSE.Quantity.HALF then +nabs=UTILS.Round(ntot/2) +elseif relative==WAREHOUSE.Quantity.THIRD then +nabs=UTILS.Round(ntot/3) +elseif relative==WAREHOUSE.Quantity.QUARTER then +nabs=UTILS.Round(ntot/4) +else +nabs=math.min(1,ntot) +end +else +nabs=relative +end +self:T2(self.lid..string.format("Relative %s: tot=%d, abs=%.2f",tostring(relative),ntot,nabs)) +return nabs +end +function WAREHOUSE:_CheckQueue() +self:_SortQueue() +local request=nil +local invalid={} +local gotit=false +for _,_qitem in ipairs(self.queue)do +local qitem=_qitem +local valid=self:_CheckRequestValid(qitem) +local okay=false +if valid then +okay=self:_CheckRequestNow(qitem) +else +table.insert(invalid,qitem) +end +if okay and valid and not gotit then +request=qitem +gotit=true +break +end +end +for _,_request in pairs(invalid)do +self:T(self.lid..string.format("Deleting invalid request id=%d.",_request.uid)) +self:_DeleteQueueItem(_request,self.queue) +end +return request +end +function WAREHOUSE:_SimpleTaskFunction(Function,group) +self:F2({Function}) +local warehouse=self.warehouse:GetName() +local groupname=group:GetName() +local DCSScript={} +DCSScript[#DCSScript+1]=string.format('local mygroup = GROUP:FindByName(\"%s\") ',groupname) +if self.isunit then +DCSScript[#DCSScript+1]=string.format("local mywarehouse = UNIT:FindByName(\"%s\") ",warehouse) +else +DCSScript[#DCSScript+1]=string.format("local mywarehouse = STATIC:FindByName(\"%s\") ",warehouse) +end +DCSScript[#DCSScript+1]=string.format('local warehouse = mywarehouse:GetState(mywarehouse, \"WAREHOUSE\") ') +DCSScript[#DCSScript+1]=string.format('%s(mygroup)',Function) +local DCSTask=CONTROLLABLE.TaskWrappedAction(self,CONTROLLABLE.CommandDoScript(self,table.concat(DCSScript))) +return DCSTask +end +function WAREHOUSE:_SimpleTaskFunctionWP(Function,group,n,N) +self:F2({Function}) +local warehouse=self.warehouse:GetName() +local groupname=group:GetName() +local DCSScript={} +DCSScript[#DCSScript+1]=string.format('local mygroup = GROUP:FindByName(\"%s\") ',groupname) +if self.isunit then +DCSScript[#DCSScript+1]=string.format("local mywarehouse = UNIT:FindByName(\"%s\") ",warehouse) +else +DCSScript[#DCSScript+1]=string.format("local mywarehouse = STATIC:FindByName(\"%s\") ",warehouse) +end +DCSScript[#DCSScript+1]=string.format('local warehouse = mywarehouse:GetState(mywarehouse, \"WAREHOUSE\") ') +DCSScript[#DCSScript+1]=string.format('%s(mygroup, %d, %d)',Function,n,N) +local DCSTask=CONTROLLABLE.TaskWrappedAction(self,CONTROLLABLE.CommandDoScript(self,table.concat(DCSScript))) +return DCSTask +end +function WAREHOUSE:_GetTerminal(_attribute,_category) +local _terminal=AIRBASE.TerminalType.OpenBig +if _attribute==WAREHOUSE.Attribute.AIR_FIGHTER then +_terminal=AIRBASE.TerminalType.FighterAircraft +elseif _attribute==WAREHOUSE.Attribute.AIR_BOMBER or _attribute==WAREHOUSE.Attribute.AIR_TRANSPORTPLANE or _attribute==WAREHOUSE.Attribute.AIR_TANKER or _attribute==WAREHOUSE.Attribute.AIR_AWACS then +_terminal=AIRBASE.TerminalType.OpenBig +elseif _attribute==WAREHOUSE.Attribute.AIR_TRANSPORTHELO or _attribute==WAREHOUSE.Attribute.AIR_ATTACKHELO then +_terminal=AIRBASE.TerminalType.HelicopterUsable +else +end +if _category==Airbase.Category.SHIP then +if not(_attribute==WAREHOUSE.Attribute.AIR_TRANSPORTHELO or _attribute==WAREHOUSE.Attribute.AIR_ATTACKHELO)then +_terminal=AIRBASE.TerminalType.OpenMedOrBig +end +end +return _terminal +end +function WAREHOUSE:_FindParkingForAssets(airbase,assets) +local scanradius=25 +local scanunits=true +local scanstatics=true +local scanscenery=false +local verysafe=false +local function _overlap(l1,l2,dist) +local safedist=(l1/2+l2/2)*1.05 +local safe=(dist>safedist) +self:T3(string.format("l1=%.1f l2=%.1f s=%.1f d=%.1f ==> safe=%s",l1,l2,safedist,dist,tostring(safe))) +return safe +end +local function _clients() +local clients=_DATABASE.CLIENTS +local coords={} +for clientname,client in pairs(clients)do +local template=_DATABASE:GetGroupTemplateFromUnitName(clientname) +local units=template.units +for i,unit in pairs(units)do +local coord=COORDINATE:New(unit.x,unit.alt,unit.y) +coords[unit.name]=coord +end +end +return coords +end +local parkingdata=airbase.parking +local obstacles={} +self.clientcoords=self.clientcoords or _clients() +for clientname,_coord in pairs(self.clientcoords)do +table.insert(obstacles,{coord=_coord,size=15,name=clientname,type="client"}) +end +for _,parkingspot in pairs(parkingdata)do +local _spot=parkingspot.Coordinate +local _termid=parkingspot.TerminalID +local _,_,_,_units,_statics,_sceneries=_spot:ScanObjects(scanradius,scanunits,scanstatics,scanscenery) +for _,_unit in pairs(_units)do +local unit=_unit +local _coord=unit:GetVec3() +local _size=self:_GetObjectSize(unit:GetDCSObject()) +local _name=unit:GetName() +table.insert(obstacles,{coord=_coord,size=_size,name=_name,type="unit"}) +end +for _,static in pairs(_statics)do +local _coord=static:getPoint() +local _name=static:getName() +local _size=self:_GetObjectSize(static) +table.insert(obstacles,{coord=_coord,size=_size,name=_name,type="static"}) +end +for _,scenery in pairs(_sceneries)do +local _coord=scenery:getPoint() +local _name=scenery:getTypeName() +local _size=self:_GetObjectSize(scenery) +table.insert(obstacles,{coord=_coord,size=_size,name=_name,type="scenery"}) +end +end +local parking={} +for _,asset in pairs(assets)do +local _asset=asset +local terminaltype=asset.terminalType or self:_GetTerminal(asset.attribute,self:GetAirbaseCategory()) +parking[_asset.uid]={} +for i=1,_asset.nunits do +local gotit=false +for _,_parkingspot in pairs(parkingdata)do +local parkingspot=_parkingspot +if AIRBASE._CheckTerminalType(parkingspot.TerminalType,terminaltype)and self:_CheckParkingValid(parkingspot,airbase)and airbase:_CheckParkingLists(parkingspot.TerminalID)then +local _spot=parkingspot.Coordinate +local _termid=parkingspot.TerminalID +local free=true +local problem=nil +for _,obstacle in pairs(obstacles)do +local dist=_spot:Get2DDistance(obstacle.coord) +local safe=_overlap(_asset.size,obstacle.size,dist) +if not safe then +free=false +problem=obstacle +problem.dist=dist +break +else +end +end +if free then +table.insert(parking[_asset.uid],parkingspot) +self:T(self.lid..string.format("Parking spot %d is free for asset id=%d!",_termid,_asset.uid)) +table.insert(obstacles,{coord=_spot,size=_asset.size,name=_asset.templatename,type="asset"}) +gotit=true +break +else +self:T(self.lid..string.format("Parking spot %d is occupied or not big enough!",_termid)) +if self.Debug then +local coord=problem.coord +local text=string.format("Obstacle blocking spot #%d is %s type %s with size=%.1f m and distance=%.1f m.",_termid,problem.name,problem.type,problem.size,problem.dist) +coord:MarkToAll(string.format(text)) +end +end +end +end +if not gotit then +self:I(self.lid..string.format("WARNING: No free parking spot for asset id=%d",_asset.uid)) +return nil +end +end +end +return parking +end +function WAREHOUSE:_GetRequestOfGroup(group,queue) +local wid,aid,rid=self:_GetIDsFromGroup(group) +for _,_request in pairs(queue)do +local request=_request +if request.uid==rid then +return request +end +end +end +function WAREHOUSE:_GroupIsTransport(group,request) +local asset=self:FindAssetInDB(group) +if asset and asset.iscargo~=nil then +return not asset.iscargo +else +local groupname=self:_GetNameWithOut(group) +if request.transportgroupset then +local transporters=request.transportgroupset:GetSetObjects() +for _,transport in pairs(transporters)do +if transport:GetName()==groupname then +return true +end +end +end +if request.cargogroupset then +local cargos=request.cargogroupset:GetSetObjects() +for _,cargo in pairs(cargos)do +if self:_GetNameWithOut(cargo)==groupname then +return false +end +end +end +end +return nil +end +function WAREHOUSE:_GetNameWithOut(group) +local groupname=type(group)=="string"and group or group:GetName() +if groupname:find("CARGO")then +local name=groupname:gsub("#CARGO","") +return name +else +return groupname +end +end +function WAREHOUSE:_GetIDsFromGroup(group) +local function analyse(text) +local unspawned=UTILS.Split(text,"#")[1] +local keywords=UTILS.Split(unspawned,"_") +local _wid=nil +local _aid=nil +local _rid=nil +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 +local name=group:GetName() +local wid,aid,rid=analyse(name) +local asset=self:GetAssetByID(aid) +if asset then +wid=asset.wid +rid=asset.rid +end +self:T(self.lid..string.format("Group Name = %s",tostring(name))) +self:T(self.lid..string.format("Warehouse ID = %s",tostring(wid))) +self:T(self.lid..string.format("Asset ID = %s",tostring(aid))) +self:T(self.lid..string.format("Request ID = %s",tostring(rid))) +return wid,aid,rid +else +self:E("WARNING: Group not found in GetIDsFromGroup() function!") +end +end +function WAREHOUSE:_GetIDsFromGroupOLD(group) +local function analyse(text) +local unspawned=UTILS.Split(text,"#")[1] +local keywords=UTILS.Split(unspawned,"_") +local _wid=nil +local _aid=nil +local _rid=nil +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 +local name=group:GetName() +local wid,aid,rid=analyse(name) +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!") +end +end +function WAREHOUSE:FilterStock(descriptor,attribute,nmax,mobile) +return self:_FilterStock(self.stock,descriptor,attribute,nmax,mobile) +end +function WAREHOUSE:_FilterStock(stock,descriptor,attribute,nmax,mobile) +nmax=nmax or WAREHOUSE.Quantity.ALL +if mobile==nil then +mobile=false +end +local filtered={} +if descriptor==WAREHOUSE.Descriptor.ASSETLIST then +local ntot=0 +for _,_rasset in pairs(attribute)do +local rasset=_rasset +for _,_asset in ipairs(stock)do +local asset=_asset +if rasset.uid==asset.uid then +table.insert(filtered,asset) +break +end +end +end +return filtered,#filtered,#filtered>=#attribute +end +local ntot=0 +for _,_asset in ipairs(stock)do +local asset=_asset +local ismobile=asset.speedmax>0 +if asset[descriptor]==attribute then +if(mobile==true and ismobile)or mobile==false then +ntot=ntot+1 +end +end +end +if ntot==0 then +return filtered,ntot,false +end +nmax=self:_QuantityRel2Abs(nmax,ntot) +for _i,_asset in ipairs(stock)do +local asset=_asset +if asset[descriptor]==attribute then +if(mobile and asset.speedmax>0)or(not mobile)then +table.insert(filtered,asset) +if nmax~=nil and#filtered>=nmax then +return filtered,ntot,true +end +end +end +end +return filtered,ntot,ntot>=nmax +end +function WAREHOUSE:_HasAttribute(group,attribute) +if group then +local groupattribute=self:_GetAttribute(group) +return groupattribute==attribute +end +return false +end +function WAREHOUSE:_GetAttribute(group) +local attribute=WAREHOUSE.Attribute.OTHER_UNKNOWN +if group then +local transportplane=group:HasAttribute("Transports")and group:HasAttribute("Planes") +local awacs=group:HasAttribute("AWACS") +local fighter=group:HasAttribute("Fighters")or group:HasAttribute("Interceptors")or group:HasAttribute("Multirole fighters")or(group:HasAttribute("Bombers")and not group:HasAttribute("Strategic bombers")) +local bomber=group:HasAttribute("Strategic bombers") +local tanker=group:HasAttribute("Tankers") +local uav=group:HasAttribute("UAVs") +local transporthelo=group:HasAttribute("Transport helicopters") +local attackhelicopter=group:HasAttribute("Attack helicopters") +local apc=group:HasAttribute("Infantry carriers") +local truck=group:HasAttribute("Trucks")and group:GetCategory()==Group.Category.GROUND +local infantry=group:HasAttribute("Infantry") +local artillery=group:HasAttribute("Artillery") +local tank=group:HasAttribute("Old Tanks")or group:HasAttribute("Modern Tanks") +local aaa=group:HasAttribute("AAA") +local ewr=group:HasAttribute("EWR") +local sam=group:HasAttribute("SAM elements")and(not group:HasAttribute("AAA")) +local train=group:GetCategory()==Group.Category.TRAIN +local aircraftcarrier=group:HasAttribute("Aircraft Carriers") +local warship=group:HasAttribute("Heavy armed ships") +local armedship=group:HasAttribute("Armed ships")or group:HasAttribute("Armed Ship") +local unarmedship=group:HasAttribute("Unarmed ships") +if transportplane then +attribute=WAREHOUSE.Attribute.AIR_TRANSPORTPLANE +elseif awacs then +attribute=WAREHOUSE.Attribute.AIR_AWACS +elseif fighter then +attribute=WAREHOUSE.Attribute.AIR_FIGHTER +elseif bomber then +attribute=WAREHOUSE.Attribute.AIR_BOMBER +elseif tanker then +attribute=WAREHOUSE.Attribute.AIR_TANKER +elseif transporthelo then +attribute=WAREHOUSE.Attribute.AIR_TRANSPORTHELO +elseif attackhelicopter then +attribute=WAREHOUSE.Attribute.AIR_ATTACKHELO +elseif uav then +attribute=WAREHOUSE.Attribute.AIR_UAV +elseif apc then +attribute=WAREHOUSE.Attribute.GROUND_APC +elseif infantry then +attribute=WAREHOUSE.Attribute.GROUND_INFANTRY +elseif artillery then +attribute=WAREHOUSE.Attribute.GROUND_ARTILLERY +elseif tank then +attribute=WAREHOUSE.Attribute.GROUND_TANK +elseif aaa then +attribute=WAREHOUSE.Attribute.GROUND_AAA +elseif ewr then +attribute=WAREHOUSE.Attribute.GROUND_EWR +elseif sam then +attribute=WAREHOUSE.Attribute.GROUND_SAM +elseif truck then +attribute=WAREHOUSE.Attribute.GROUND_TRUCK +elseif train then +attribute=WAREHOUSE.Attribute.GROUND_TRAIN +elseif aircraftcarrier then +attribute=WAREHOUSE.Attribute.NAVAL_AIRCRAFTCARRIER +elseif warship then +attribute=WAREHOUSE.Attribute.NAVAL_WARSHIP +elseif armedship then +attribute=WAREHOUSE.Attribute.NAVAL_ARMEDSHIP +elseif unarmedship then +attribute=WAREHOUSE.Attribute.NAVAL_UNARMEDSHIP +else +if group:IsGround()then +attribute=WAREHOUSE.Attribute.GROUND_OTHER +elseif group:IsShip()then +attribute=WAREHOUSE.Attribute.NAVAL_OTHER +elseif group:IsAir()then +attribute=WAREHOUSE.Attribute.AIR_OTHER +else +attribute=WAREHOUSE.Attribute.OTHER_UNKNOWN +end +end +end +return attribute +end +function WAREHOUSE:_GetObjectSize(DCSobject) +local DCSdesc=DCSobject:getDesc() +if DCSdesc.box then +local x=DCSdesc.box.max.x+math.abs(DCSdesc.box.min.x) +local y=DCSdesc.box.max.y+math.abs(DCSdesc.box.min.y) +local z=DCSdesc.box.max.z+math.abs(DCSdesc.box.min.z) +return math.max(x,z),x,y,z +end +return 0,0,0,0 +end +function WAREHOUSE:GetStockInfo(stock) +local _data={} +for _j,_attribute in pairs(WAREHOUSE.Attribute)do +local n=0 +for _i,_item in pairs(stock)do +local _ite=_item +if _ite.attribute==_attribute then +n=n+1 +end +end +_data[_attribute]=n +end +return _data +end +function WAREHOUSE:_DeleteStockItem(stockitem) +for i=1,#self.stock do +local item=self.stock[i] +if item.uid==stockitem.uid then +table.remove(self.stock,i) +break +end +end +end +function WAREHOUSE:_DeleteQueueItem(qitem,queue) +self:F({qitem=qitem,queue=queue}) +for i=1,#queue do +local _item=queue[i] +if _item.uid==qitem.uid then +self:T(self.lid..string.format("Deleting queue item id=%d.",qitem.uid)) +table.remove(queue,i) +break +end +end +end +function WAREHOUSE:_DeleteQueueItemByID(qitemID,queue) +for i=1,#queue do +local _item=queue[i] +if _item.uid==qitemID then +self:T(self.lid..string.format("Deleting queue item id=%d.",qitemID)) +table.remove(queue,i) +break +end +end +end +function WAREHOUSE:_SortQueue() +self:F3() +local function _sort(a,b) +return(a.prio=2 then +local total="Empty" +if#queue>0 then +total=string.format("Total = %d",#queue) +end +local text=string.format("%s at %s: %s",name,self.alias,total) +for i,qitem in ipairs(queue)do +local qitem=qitem +local uid=qitem.uid +local prio=qitem.prio +local clock="N/A" +if qitem.timestamp then +clock=tostring(UTILS.SecondsToClock(qitem.timestamp)) +end +local assignment=tostring(qitem.assignment) +local requestor=qitem.warehouse.alias +local airbasename=qitem.warehouse:GetAirbaseName() +local requestorAirbaseCat=qitem.warehouse:GetAirbaseCategory() +local assetdesc=qitem.assetdesc +local assetdescval=qitem.assetdescval +if assetdesc==WAREHOUSE.Descriptor.ASSETLIST then +assetdescval="Asset list" +end +local nasset=tostring(qitem.nasset) +local ndelivered=tostring(qitem.ndelivered) +local ncargogroupset="N/A" +if qitem.cargogroupset then +ncargogroupset=tostring(qitem.cargogroupset:Count()) +end +local transporttype="N/A" +if qitem.transporttype then +transporttype=qitem.transporttype +end +local ntransport="N/A" +if qitem.ntransport then +ntransport=tostring(qitem.ntransport) +end +local ntransportalive="N/A" +if qitem.transportgroupset then +ntransportalive=tostring(qitem.transportgroupset:Count()) +end +local ntransporthome="N/A" +if qitem.ntransporthome then +ntransporthome=tostring(qitem.ntransporthome) +end +text=text..string.format( +"\n%d) UID=%d, Prio=%d, Clock=%s, Assignment=%s | Requestor=%s [Airbase=%s, category=%d] | Assets(%s)=%s: #requested=%s / #alive=%s / #delivered=%s | Transport=%s: #requested=%s / #alive=%s / #home=%s", +i,uid,prio,clock,assignment,requestor,airbasename,requestorAirbaseCat,assetdesc,assetdescval,nasset,ncargogroupset,ndelivered,transporttype,ntransport,ntransportalive,ntransporthome) +end +if#queue==0 then +self:I(self.lid..text) +else +if total~="Empty"then +self:I(self.lid..text) +end +end +end +end +function WAREHOUSE:_DisplayStatus() +if self.verbosity>=3 then +local text=string.format("\n------------------------------------------------------\n") +text=text..string.format("Warehouse %s status: %s\n",self.alias,self:GetState()) +text=text..string.format("------------------------------------------------------\n") +text=text..string.format("Coalition name = %s\n",self:GetCoalitionName()) +text=text..string.format("Country name = %s\n",self:GetCountryName()) +text=text..string.format("Airbase name = %s (category=%d)\n",self:GetAirbaseName(),self:GetAirbaseCategory()) +text=text..string.format("Queued requests = %d\n",#self.queue) +text=text..string.format("Pending requests = %d\n",#self.pending) +text=text..string.format("------------------------------------------------------\n") +text=text..self:_GetStockAssetsText() +self:I(text) +end +end +function WAREHOUSE:_GetStockAssetsText(messagetoall) +local _data=self:GetStockInfo(self.stock) +local text="Stock:\n" +local total=0 +for _attribute,_count in pairs(_data)do +if _count>0 then +local attribute=tostring(UTILS.Split(_attribute,"_")[2]) +text=text..string.format("%s = %d\n",attribute,_count) +total=total+_count +end +end +text=text..string.format("===================\n") +text=text..string.format("Total = %d\n",total) +text=text..string.format("------------------------------------------------------\n") +MESSAGE:New(text,10):ToAllIf(messagetoall) +return text +end +function WAREHOUSE:_UpdateWarehouseMarkText() +if self.markerOn then +local text=string.format("Warehouse state: %s\nTotal assets in stock %d:\n",self:GetState(),#self.stock) +for _attribute,_count in pairs(self:GetStockInfo(self.stock)or{})do +if _count>0 then +local attribute=tostring(UTILS.Split(_attribute,"_")[2]) +text=text..string.format("%s=%d, ",attribute,_count) +end +end +local coordinate=self:GetCoordinate() +local coalition=self:GetCoalition() +if not self.markerWarehouse then +self.markerWarehouse=MARKER:New(coordinate,text):ToCoalition(coalition) +else +local refresh=false +if self.markerWarehouse.text~=text then +self.markerWarehouse.text=text +refresh=true +end +if self.markerWarehouse.coordinate~=coordinate then +self.markerWarehouse.coordinate=coordinate +refresh=true +end +if self.markerWarehouse.coalition~=coalition then +self.markerWarehouse.coalition=coalition +refresh=true +end +if refresh then +self.markerWarehouse:Refresh() +end +end +end +end +function WAREHOUSE:_DisplayStockItems(stock) +local text=self.lid..string.format("Warehouse %s stock assets:",self.alias) +for _i,_stock in pairs(stock)do +local mystock=_stock +local name=mystock.templatename +local category=mystock.category +local cargobaymax=mystock.cargobaymax +local cargobaytot=mystock.cargobaytot +local nunits=mystock.nunits +local range=mystock.range +local size=mystock.size +local speed=mystock.speedmax +local uid=mystock.uid +local unittype=mystock.unittype +local weight=mystock.weight +local attribute=mystock.attribute +text=text..string.format("\n%02d) uid=%d, name=%s, unittype=%s, category=%d, attribute=%s, nunits=%d, speed=%.1f km/h, range=%.1f km, size=%.1f m, weight=%.1f kg, cargobax max=%.1f kg tot=%.1f kg", +_i,uid,name,unittype,category,attribute,nunits,speed,range/1000,size,weight,cargobaymax,cargobaytot) +end +self:T3(text) +end +function WAREHOUSE:_Fireworks(coord) +coord=coord or self:GetCoordinate() +for i=1,91 do +local color=math.random(0,3) +coord:Flare(color,i-1) +end +end +function WAREHOUSE:_InfoMessage(text,duration) +duration=duration or 20 +if duration>0 and self.Debug or self.Report then +MESSAGE:New(text,duration):ToCoalition(self:GetCoalition()) +end +self:I(self.lid..text) +end +function WAREHOUSE:_DebugMessage(text,duration) +duration=duration or 20 +if duration>0 then +MESSAGE:New(text,duration):ToAllIf(self.Debug) +end +self:T(self.lid..text) +end +function WAREHOUSE:_ErrorMessage(text,duration) +duration=duration or 20 +if duration>0 then +MESSAGE:New(text,duration):ToAll() +end +self:E(self.lid..text) +end +function WAREHOUSE:_GetMaxHeight(D,alphaC,alphaD,Hdep,Hdest,Deltahhold) +local Hhold=Hdest+Deltahhold +local hdest=Hdest-Hdep +local hhold=hdest+Deltahhold +local Dp=math.sqrt(D^2+hhold^2) +local alphaS=math.atan(hdest/D) +local alphaH=math.atan(hhold/D) +local alphaCp=alphaC-alphaH +local alphaDp=alphaD+alphaH +local gammap=math.pi-alphaCp-alphaDp +local sCp=Dp*math.sin(alphaDp)/math.sin(gammap) +local sDp=Dp*math.sin(alphaCp)/math.sin(gammap) +local hmax=sCp*math.sin(alphaC) +if self.Debug then +env.info(string.format("Hdep = %.3f km",Hdep/1000)) +env.info(string.format("Hdest = %.3f km",Hdest/1000)) +env.info(string.format("DetaHold= %.3f km",Deltahhold/1000)) +env.info() +env.info(string.format("D = %.3f km",D/1000)) +env.info(string.format("Dp = %.3f km",Dp/1000)) +env.info() +env.info(string.format("alphaC = %.3f Deg",math.deg(alphaC))) +env.info(string.format("alphaCp = %.3f Deg",math.deg(alphaCp))) +env.info() +env.info(string.format("alphaD = %.3f Deg",math.deg(alphaD))) +env.info(string.format("alphaDp = %.3f Deg",math.deg(alphaDp))) +env.info() +env.info(string.format("alphaS = %.3f Deg",math.deg(alphaS))) +env.info(string.format("alphaH = %.3f Deg",math.deg(alphaH))) +env.info() +env.info(string.format("sCp = %.3f km",sCp/1000)) +env.info(string.format("sDp = %.3f km",sDp/1000)) +env.info() +env.info(string.format("hmax = %.3f km",hmax/1000)) +env.info() +local hdescent=hmax-hhold +local dClimb=hmax/math.tan(alphaC) +local dDescent=(hmax-hhold)/math.tan(alphaD) +local dCruise=D-dClimb-dDescent +env.info(string.format("hmax = %.3f km",hmax/1000)) +env.info(string.format("hdescent = %.3f km",hdescent/1000)) +env.info(string.format("Dclimb = %.3f km",dClimb/1000)) +env.info(string.format("Dcruise = %.3f km",dCruise/1000)) +env.info(string.format("Ddescent = %.3f km",dDescent/1000)) +env.info() +end +return hmax +end +function WAREHOUSE:_GetFlightplan(asset,departure,destination) +local Vmax=asset.speedmax/3.6 +local Range=asset.range +local category=asset.category +local ceiling=asset.DCSdesc.Hmax +local Vymax=asset.DCSdesc.VyMax +local VxCruiseMax=0.90*Vmax +local VxCruiseMin=math.min(VxCruiseMax*0.70,166) +local VxCruise=UTILS.RandomGaussian((VxCruiseMax-VxCruiseMin)/2+VxCruiseMin,(VxCruiseMax-VxCruiseMax)/4,VxCruiseMin,VxCruiseMax) +local VxClimb=math.min(Vmax*0.90,200) +local VxDescent=math.min(Vmax*0.60,140) +local VxHolding=VxDescent*0.9 +local VxFinal=VxHolding*0.9 +local VyClimb=math.min(7.6,Vymax) +local AlphaClimb=math.rad(4) +local AlphaDescent=math.rad(4) +local FLcruise_expect=150*RAT.unit.FL2m +if category==Group.Category.HELICOPTER then +FLcruise_expect=1000 +end +local Pdeparture=departure:GetCoordinate() +local H_departure=Pdeparture.y +local Pdestination=destination:GetCoordinate() +local H_destination=Pdestination.y +local Rhmin=5000 +local Rhmax=10000 +if category==Group.Category.HELICOPTER then +Rhmin=500 +Rhmax=1000 +end +local Pholding=Pdestination:GetRandomCoordinateInRadius(Rhmax,Rhmin) +local d_holding=Pholding:Get2DDistance(Pdestination) +local H_holding=Pholding.y +local heading=Pdeparture:HeadingTo(Pholding) +local d_total=Pdeparture:Get2DDistance(Pholding) +local h_holding=1200 +if category==Group.Category.HELICOPTER then +h_holding=150 +end +h_holding=UTILS.Randomize(h_holding,0.2) +local DeltaholdingMax=self:_GetMaxHeight(d_total,AlphaClimb,AlphaDescent,H_departure,H_holding,0) +if h_holding>DeltaholdingMax then +h_holding=math.abs(DeltaholdingMax) +end +local Hh_holding=H_holding+h_holding +local h_max=self:_GetMaxHeight(d_total,AlphaClimb,AlphaDescent,H_departure,H_holding,h_holding) +local FLmax=h_max+H_departure +local FLmin=math.max(H_departure,Hh_holding) +FLmax=math.min(FLmax,ceiling) +if FLmin>FLmax then +FLmin=FLmax +end +if FLcruise_expectFLmax then +FLcruise_expect=FLmax +end +local FLcruise=UTILS.RandomGaussian(FLcruise_expect,math.abs(FLmax-FLmin)/4,FLmin,FLmax) +local h_climb=FLcruise-H_departure +local h_descent=FLcruise-Hh_holding +local d_climb=h_climb/math.tan(AlphaClimb) +local d_descent=h_descent/math.tan(AlphaDescent) +local d_cruise=d_total-d_climb-d_descent +local text=string.format("Flight plan:\n") +text=text..string.format("Vx max = %.2f km/h\n",Vmax*3.6) +text=text..string.format("Vx climb = %.2f km/h\n",VxClimb*3.6) +text=text..string.format("Vx cruise = %.2f km/h\n",VxCruise*3.6) +text=text..string.format("Vx descent = %.2f km/h\n",VxDescent*3.6) +text=text..string.format("Vx holding = %.2f km/h\n",VxHolding*3.6) +text=text..string.format("Vx final = %.2f km/h\n",VxFinal*3.6) +text=text..string.format("Vy max = %.2f m/s\n",Vymax) +text=text..string.format("Vy climb = %.2f m/s\n",VyClimb) +text=text..string.format("Alpha Climb = %.2f Deg\n",math.deg(AlphaClimb)) +text=text..string.format("Alpha Descent = %.2f Deg\n",math.deg(AlphaDescent)) +text=text..string.format("Dist climb = %.3f km\n",d_climb/1000) +text=text..string.format("Dist cruise = %.3f km\n",d_cruise/1000) +text=text..string.format("Dist descent = %.3f km\n",d_descent/1000) +text=text..string.format("Dist total = %.3f km\n",d_total/1000) +text=text..string.format("h_climb = %.3f km\n",h_climb/1000) +text=text..string.format("h_desc = %.3f km\n",h_descent/1000) +text=text..string.format("h_holding = %.3f km\n",h_holding/1000) +text=text..string.format("h_max = %.3f km\n",h_max/1000) +text=text..string.format("FL min = %.3f km\n",FLmin/1000) +text=text..string.format("FL expect = %.3f km\n",FLcruise_expect/1000) +text=text..string.format("FL cruise * = %.3f km\n",FLcruise/1000) +text=text..string.format("FL max = %.3f km\n",FLmax/1000) +text=text..string.format("Ceiling = %.3f km\n",ceiling/1000) +text=text..string.format("Max range = %.3f km\n",Range/1000) +self:T(self.lid..text) +if d_cruise<0 then +d_cruise=100 +end +local wp={} +local c={} +c[#c+1]=Pdeparture +wp[#wp+1]=Pdeparture:WaypointAir("RADIO",COORDINATE.WaypointType.TakeOffParking,COORDINATE.WaypointAction.FromParkingArea,VxClimb*3.6,true,departure,nil,"Departure") +local Pcruise=Pdeparture:Translate(d_climb,heading) +Pcruise.y=FLcruise +c[#c+1]=Pcruise +wp[#wp+1]=Pcruise:WaypointAir("BARO",COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.TurningPoint,VxCruise*3.6,true,nil,nil,"Cruise") +local Pdescent=Pcruise:Translate(d_cruise,heading) +Pdescent.y=FLcruise +c[#c+1]=Pdescent +wp[#wp+1]=Pdescent:WaypointAir("BARO",COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.TurningPoint,VxDescent*3.6,true,nil,nil,"Descent") +Pholding.y=H_holding+h_holding +c[#c+1]=Pholding +wp[#wp+1]=Pholding:WaypointAir("BARO",COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.TurningPoint,VxHolding*3.6,true,nil,nil,"Holding") +c[#c+1]=Pdestination +wp[#wp+1]=Pdestination:WaypointAir("RADIO",COORDINATE.WaypointType.Land,COORDINATE.WaypointAction.Landing,VxFinal*3.6,true,destination,nil,"Final Destination") +if self.Debug then +for i,coord in pairs(c)do +local coord=coord +local dist=0 +if i>1 then +dist=coord:Get2DDistance(c[i-1]) +end +coord:MarkToAll(string.format("Waypoint %i, distance = %.2f km",i,dist/1000)) +end +end +return wp,c +end +FOX={ +ClassName="FOX", +Debug=false, +lid=nil, +menuadded={}, +menudisabled=nil, +destroy=nil, +launchalert=nil, +marklaunch=nil, +missiles={}, +players={}, +safezones={}, +launchzones={}, +protectedset=nil, +explosionpower=0.1, +explosiondist=200, +explosiondist2=500, +bigmissilemass=50, +destroy=nil, +dt50=5, +dt10=1, +dt05=0.5, +dt01=0.1, +dt00=0.01, +} +FOX.MenuF10={} +FOX.MenuF10Root=nil +FOX.version="0.6.1" +function FOX:New() +self.lid="FOX | " +local self=BASE:Inherit(self,FSM:New()) +self:SetDefaultMissileDestruction(true) +self:SetDefaultLaunchAlerts(true) +self:SetDefaultLaunchMarks(true) +self:SetExplosionDistance() +self:SetExplosionDistanceBigMissiles() +self:SetExplosionPower() +self:SetStartState("Stopped") +self:AddTransition("Stopped","Start","Running") +self:AddTransition("*","Status","*") +self:AddTransition("*","MissileLaunch","*") +self:AddTransition("*","MissileDestroyed","*") +self:AddTransition("*","EnterSafeZone","*") +self:AddTransition("*","ExitSafeZone","*") +self:AddTransition("Running","Stop","Stopped") +return self +end +function FOX:onafterStart(From,Event,To) +local text=string.format("Starting FOX Missile Trainer %s",FOX.version) +env.info(text) +self:HandleEvent(EVENTS.Birth) +self:HandleEvent(EVENTS.Shot) +if self.Debug then +self:HandleEvent(EVENTS.Hit) +end +if self.Debug then +self:TraceClass(self.ClassName) +self:TraceLevel(2) +end +self:__Status(-20) +end +function FOX:onafterStop(From,Event,To) +local text=string.format("Stopping FOX Missile Trainer %s",FOX.version) +env.info(text) +self:UnHandleEvent(EVENTS.Birth) +self:UnHandleEvent(EVENTS.Shot) +if self.Debug then +self:UnhandleEvent(EVENTS.Hit) +end +end +function FOX:AddSafeZone(zone) +table.insert(self.safezones,zone) +return self +end +function FOX:AddLaunchZone(zone) +table.insert(self.launchzones,zone) +return self +end +function FOX:SetProtectedGroupSet(groupset) +self.protectedset=groupset +return self +end +function FOX:AddProtectedGroup(group) +if not self.protectedset then +self.protectedset=SET_GROUP:New() +end +self.protectedset:AddGroup(group) +return self +end +function FOX:SetExplosionPower(power) +self.explosionpower=power or 0.1 +return self +end +function FOX:SetExplosionDistance(distance) +self.explosiondist=distance or 200 +return self +end +function FOX:SetExplosionDistanceBigMissiles(distance,explosivemass) +self.explosiondist2=distance or 500 +self.bigmissilemass=explosivemass or 50 +return self +end +function FOX:SetDisableF10Menu() +self.menudisabled=true +return self +end +function FOX:SetDefaultMissileDestruction(switch) +if switch==nil then +self.destroy=false +else +self.destroy=switch +end +return self +end +function FOX:SetDefaultLaunchAlerts(switch) +if switch==nil then +self.launchalert=false +else +self.launchalert=switch +end +return self +end +function FOX:SetDefaultLaunchMarks(switch) +if switch==nil then +self.marklaunch=false +else +self.marklaunch=switch +end +return self +end +function FOX:SetDebugOnOff(switch) +if switch==nil then +self.Debug=false +else +self.Debug=switch +end +return self +end +function FOX:SetDebugOn() +self:SetDebugOnOff(true) +return self +end +function FOX:SetDebugOff() +self:SetDebugOff(false) +return self +end +function FOX:onafterStatus(From,Event,To) +local fsmstate=self:GetState() +local time=timer.getAbsTime() +local clock=UTILS.SecondsToClock(time) +self:I(self.lid..string.format("Missile trainer status %s: %s",clock,fsmstate)) +self:_CheckMissileStatus() +self:_CheckPlayers() +if fsmstate=="Running"then +self:__Status(-10) +end +end +function FOX:_CheckPlayers() +for playername,_playersettings in pairs(self.players)do +local playersettings=_playersettings +local unitname=playersettings.unitname +local unit=UNIT:FindByName(unitname) +if unit and unit:IsAlive()then +local coord=unit:GetCoordinate() +local issafe=self:_CheckCoordSafe(coord) +if issafe then +if not playersettings.inzone then +self:EnterSafeZone(playersettings) +playersettings.inzone=true +end +else +if playersettings.inzone==true then +self:ExitSafeZone(playersettings) +playersettings.inzone=false +end +end +end +end +end +function FOX:_RemoveMissile(missile) +if missile then +for i,_missile in pairs(self.missiles)do +local m=_missile +if missile.missileName==m.missileName then +table.remove(self.missiles,i) +return +end +end +end +end +function FOX:_CheckMissileStatus() +local text="Missiles:" +local inactive={} +for i,_missile in pairs(self.missiles)do +local missile=_missile +local targetname="unkown" +if missile.targetUnit then +targetname=missile.targetUnit:GetName() +end +local playername="none" +if missile.targetPlayer then +playername=missile.targetPlayer.name +end +local active=tostring(missile.active) +local mtype=missile.missileType +local dtype=missile.missileType +local range=UTILS.MetersToNM(missile.missileRange) +if not active then +table.insert(inactive,i) +end +local heading=self:_GetWeapongHeading(missile.weapon) +text=text..string.format("\n[%d] %s: active=%s, range=%.1f NM, heading=%03d, target=%s, player=%s, missilename=%s",i,mtype,active,range,heading,targetname,playername,missile.missileName) +end +if#self.missiles==0 then +text=text.." none" +end +self:I(self.lid..text) +for i=#self.missiles,1,-1 do +local missile=self.missiles[i] +if missile and not missile.active then +table.remove(self.missiles,i) +end +end +end +function FOX:_IsProtected(targetunit) +if not self.protectedset then +return false +end +if targetunit and targetunit:IsAlive()then +local targetgroup=targetunit:GetGroup() +if targetgroup then +local targetname=targetgroup:GetName() +for _,_group in pairs(self.protectedset:GetSetObjects())do +local group=_group +if group then +local groupname=group:GetName() +if targetname==groupname then +return true +end +end +end +end +end +return false +end +function FOX:onafterMissileLaunch(From,Event,To,missile) +local text=string.format("FOX: Tracking missile %s(%s) - target %s - shooter %s",missile.missileType,missile.missileName,tostring(missile.targetName),missile.shooterName) +self:I(FOX.lid..text) +MESSAGE:New(text,10):ToAllIf(self.Debug) +for _,_player in pairs(self.players)do +local player=_player +local playerUnit=player.unit +if playerUnit and playerUnit:IsAlive()and player.coalition~=missile.shooterCoalition then +local distance=playerUnit:GetCoordinate():Get3DDistance(missile.shotCoord) +local bearing=playerUnit:GetCoordinate():HeadingTo(missile.shotCoord) +if player.launchalert then +if(missile.targetPlayer and player.unitname==missile.targetPlayer.unitname)or(distance=self.bigmissilemass +end +if destroymissile and self:_CheckCoordSafe(targetCoord)then +self:I(self.lid..string.format("Destroying missile %s(%s) fired by %s aimed at %s [player=%s] at distance %.1f m", +missile.missileType,missile.missileName,missile.shooterName,target:GetName(),tostring(missile.targetPlayer~=nil),distance)) +_ordnance:destroy() +missile.active=false +if self.Debug then +missileCoord:SmokeRed() +targetCoord:SmokeGreen() +end +self:MissileDestroyed(missile) +if self.explosionpower>0 and distance>50 and(distShooter==nil or(distShooter and distShooter>50))then +missileCoord:Explosion(self.explosionpower) +end +if missile.targetPlayer then +local text=string.format("Destroying missile. %s",self:_DeadText()) +MESSAGE:New(text,10):ToGroup(target:GetGroup()) +missile.targetPlayer.dead=missile.targetPlayer.dead+1 +end +return nil +else +local dt=1.0 +if distance>50000 then +dt=self.dt50 +elseif distance>10000 then +dt=self.dt10 +elseif distance>5000 then +dt=self.dt05 +elseif distance>1000 then +dt=self.dt01 +else +dt=self.dt00 +end +return timer.getTime()+dt +end +else +self:T(self.lid..string.format("Missile %s(%s) fired by %s has no current target. Checking back in 0.1 sec.",missile.missileType,missile.missileName,missile.shooterName)) +return timer.getTime()+0.1 +end +else +if target then +local player=self:_GetPlayerFromUnit(target) +if player and player.unit:IsAlive()then +local text=string.format("Missile defeated. Well done, %s!",player.name) +MESSAGE:New(text,10):ToClient(player.client) +player.defeated=player.defeated+1 +end +end +missile.active=false +self:T(FOX.lid..string.format("Terminating missile track timer.")) +return nil +end +end +self:T(FOX.lid..string.format("Tracking of missile starts in 0.0001 seconds.")) +timer.scheduleFunction(trackMissile,missile.weapon,timer.getTime()+0.0001) +end +function FOX:OnEventBirth(EventData) +self:F3({eventbirth=EventData}) +if EventData==nil then +self:E(self.lid.."ERROR: EventData=nil in event BIRTH!") +self:E(EventData) +return +end +if EventData.IniUnit==nil then +self:E(self.lid.."ERROR: EventData.IniUnit=nil in event BIRTH!") +self:E(EventData) +return +end +local _unitName=EventData.IniUnitName +local playerunit,playername=self:_GetPlayerUnitAndName(_unitName) +self:T(self.lid.."BIRTH: unit = "..tostring(EventData.IniUnitName)) +self:T(self.lid.."BIRTH: group = "..tostring(EventData.IniGroupName)) +self:T(self.lid.."BIRTH: player = "..tostring(playername)) +if playerunit and playername then +local _uid=playerunit:GetID() +local _group=playerunit:GetGroup() +local _callsign=playerunit:GetCallsign() +local text=string.format("Pilot %s, callsign %s entered unit %s of group %s.",playername,_callsign,_unitName,_group:GetName()) +self:T(self.lid..text) +MESSAGE:New(text,5):ToAllIf(self.Debug) +if not self.menudisabled then +SCHEDULER:New(nil,self._AddF10Commands,{self,_unitName},0.1) +end +local playerData={} +playerData.unit=playerunit +playerData.unitname=_unitName +playerData.group=_group +playerData.groupname=_group:GetName() +playerData.name=playername +playerData.callsign=playerData.unit:GetCallsign() +playerData.client=CLIENT:FindByName(_unitName,nil,true) +playerData.coalition=_group:GetCoalition() +playerData.destroy=playerData.destroy or self.destroy +playerData.launchalert=playerData.launchalert or self.launchalert +playerData.marklaunch=playerData.marklaunch or self.marklaunch +playerData.defeated=playerData.defeated or 0 +playerData.dead=playerData.dead or 0 +self.players[playername]=playerData +end +end +function FOX:GetMissileTarget(missile) +local target=nil +local targetName="unknown" +local targetUnit=nil +if missile.weapon and missile.weapon:isExist()then +target=missile.weapon:getTarget() +if target then +self:T2({missiletarget=target}) +targetUnit=UNIT:Find(target) +if targetUnit then +targetName=targetUnit:GetName() +missile.targetUnit=targetUnit +missile.targetPlayer=self:_GetPlayerFromUnit(missile.targetUnit) +end +end +end +if missile.targetName and missile.targetName~=targetName then +self:I(self.lid..string.format("Missile %s(%s) changed target to %s. Previous target was %s.",missile.missileType,missile.missileName,targetName,missile.targetName)) +end +missile.targetName=targetName +end +function FOX:OnEventShot(EventData) +self:T2({eventshot=EventData}) +if EventData.Weapon==nil then +return +end +if EventData.IniDCSUnit==nil then +return +end +local _weapon=EventData.WeaponName +local _target=EventData.Weapon:getTarget() +local _targetName="unknown" +local _targetUnit=nil +local desc=EventData.Weapon:getDesc() +self:T2({desc=desc}) +local weaponcategory=desc.category +local missilecategory=desc.missileCategory +local missilerange=nil +if missilecategory then +missilerange=desc.rangeMaxAltMax +end +self:T2(FOX.lid.."EVENT SHOT: FOX") +self:T2(FOX.lid..string.format("EVENT SHOT: Ini unit = %s",tostring(EventData.IniUnitName))) +self:T2(FOX.lid..string.format("EVENT SHOT: Ini group = %s",tostring(EventData.IniGroupName))) +self:T2(FOX.lid..string.format("EVENT SHOT: Weapon type = %s",tostring(_weapon))) +self:T2(FOX.lid..string.format("EVENT SHOT: Weapon categ = %s",tostring(weaponcategory))) +self:T2(FOX.lid..string.format("EVENT SHOT: Missil categ = %s",tostring(missilecategory))) +self:T2(FOX.lid..string.format("EVENT SHOT: Missil range = %s",tostring(missilerange))) +if not self:_CheckCoordLaunch(EventData.IniUnit:GetCoordinate())then +self:T(self.lid.."Missile was not fired in launch zone. No tracking!") +return +end +local _track=weaponcategory==1 and missilecategory and(missilecategory==1 or missilecategory==2 or missilecategory==6) +if _track then +local missile={} +missile.active=true +missile.weapon=EventData.weapon +missile.missileType=_weapon +missile.missileRange=missilerange +missile.missileName=EventData.weapon:getName() +missile.shooterUnit=EventData.IniUnit +missile.shooterGroup=EventData.IniGroup +missile.shooterCoalition=EventData.IniUnit:GetCoalition() +missile.shooterName=EventData.IniUnitName +missile.shotTime=timer.getAbsTime() +missile.shotCoord=EventData.IniUnit:GetCoordinate() +missile.fuseDist=desc.fuseDist +missile.explosive=desc.warhead.explosiveMass or desc.warhead.shapedExplosiveMass +missile.targetOrig=missile.targetName +self:GetMissileTarget(missile) +self:I(FOX.lid..string.format("EVENT SHOT: Shooter=%s %s(%s) ==> Target=%s, fuse dist=%s, explosive=%s", +tostring(missile.shooterName),tostring(missile.missileType),tostring(missile.missileName),tostring(missile.targetName),tostring(missile.fuseDist),tostring(missile.explosive))) +if missile.targetPlayer or self:_IsProtected(missile.targetUnit)or missile.targetName=="unknown"then +table.insert(self.missiles,missile) +self:__MissileLaunch(0.1,missile) +end +end +end +function FOX:OnEventHit(EventData) +self:T({eventhit=EventData}) +if EventData.Weapon==nil then +return +end +if EventData.IniUnit==nil then +return +end +if EventData.TgtUnit==nil then +return +end +local weapon=EventData.Weapon +local weaponname=weapon:getName() +for i,_missile in pairs(self.missiles)do +local missile=_missile +if missile.missileName==weaponname then +self:I(self.lid..string.format("WARNING: Missile %s (%s) hit target %s. Missile trainer target was %s.",missile.missileType,missile.missileName,EventData.TgtUnitName,missile.targetName)) +self:I({missile=missile}) +return +end +end +end +function FOX:_AddF10Commands(_unitName) +self:F(_unitName) +local _unit,playername=self:_GetPlayerUnitAndName(_unitName) +if _unit and playername then +local group=_unit:GetGroup() +local gid=group:GetID() +if group and gid then +if not self.menuadded[gid]then +self.menuadded[gid]=true +local _rootPath=nil +if FOX.MenuF10Root then +_rootPath=FOX.MenuF10Root +else +if FOX.MenuF10[gid]==nil then +FOX.MenuF10[gid]=missionCommands.addSubMenuForGroup(gid,"FOX") +end +_rootPath=FOX.MenuF10[gid] +end +missionCommands.addCommandForGroup(gid,"Destroy Missiles On/Off",_rootPath,self._ToggleDestroyMissiles,self,_unitName) +missionCommands.addCommandForGroup(gid,"Launch Alerts On/Off",_rootPath,self._ToggleLaunchAlert,self,_unitName) +missionCommands.addCommandForGroup(gid,"Mark Launch On/Off",_rootPath,self._ToggleLaunchMark,self,_unitName) +missionCommands.addCommandForGroup(gid,"My Status",_rootPath,self._MyStatus,self,_unitName) +end +else +self:E(self.lid..string.format("ERROR: Could not find group or group ID in AddF10Menu() function. Unit name: %s.",_unitName)) +end +else +self:E(self.lid..string.format("ERROR: Player unit does not exist in AddF10Menu() function. Unit name: %s.",_unitName)) +end +end +function FOX:_MyStatus(_unitname) +self:F2(_unitname) +local unit,playername=self:_GetPlayerUnitAndName(_unitname) +if unit and playername then +local playerData=self.players[playername] +if playerData then +local m,mtext=self:_GetTargetMissiles(playerData.name) +local text=string.format("Status of player %s:\n",playerData.name) +local safe=self:_CheckCoordSafe(playerData.unit:GetCoordinate()) +text=text..string.format("Destroy missiles? %s\n",tostring(playerData.destroy)) +text=text..string.format("Launch alert? %s\n",tostring(playerData.launchalert)) +text=text..string.format("Launch marks? %s\n",tostring(playerData.marklaunch)) +text=text..string.format("Am I safe? %s\n",tostring(safe)) +text=text..string.format("Missiles defeated: %d\n",playerData.defeated) +text=text..string.format("Missiles destroyed: %d\n",playerData.dead) +text=text..string.format("Me target: %d\n%s",m,mtext) +MESSAGE:New(text,10,nil,true):ToClient(playerData.client) +end +end +end +function FOX:_GetTargetMissiles(playername) +local text="" +local n=0 +for _,_missile in pairs(self.missiles)do +local missile=_missile +if missile.targetPlayer and missile.targetPlayer.name==playername then +n=n+1 +text=text..string.format("Type %s: active %s\n",missile.missileType,tostring(missile.active)) +end +end +return n,text +end +function FOX:_ToggleLaunchAlert(_unitname) +self:F2(_unitname) +local unit,playername=self:_GetPlayerUnitAndName(_unitname) +if unit and playername then +local playerData=self.players[playername] +if playerData then +playerData.launchalert=not playerData.launchalert +local text="" +if playerData.launchalert==true then +text=string.format("%s, missile launch alerts are now ENABLED.",playerData.name) +else +text=string.format("%s, missile launch alerts are now DISABLED.",playerData.name) +end +MESSAGE:New(text,5):ToClient(playerData.client) +end +end +end +function FOX:_ToggleLaunchMark(_unitname) +self:F2(_unitname) +local unit,playername=self:_GetPlayerUnitAndName(_unitname) +if unit and playername then +local playerData=self.players[playername] +if playerData then +playerData.marklaunch=not playerData.marklaunch +local text="" +if playerData.marklaunch==true then +text=string.format("%s, missile launch marks are now ENABLED.",playerData.name) +else +text=string.format("%s, missile launch marks are now DISABLED.",playerData.name) +end +MESSAGE:New(text,5):ToClient(playerData.client) +end +end +end +function FOX:_ToggleDestroyMissiles(_unitname) +self:F2(_unitname) +local unit,playername=self:_GetPlayerUnitAndName(_unitname) +if unit and playername then +local playerData=self.players[playername] +if playerData then +playerData.destroy=not playerData.destroy +local text="" +if playerData.destroy==true then +text=string.format("%s, incoming missiles will be DESTROYED.",playerData.name) +else +text=string.format("%s, incoming missiles will NOT be DESTROYED.",playerData.name) +end +MESSAGE:New(text,5):ToClient(playerData.client) +end +end +end +function FOX:_DeadText() +local texts={} +texts[1]="You're dead!" +texts[2]="Meet your maker!" +texts[3]="Time to meet your maker!" +texts[4]="Well, I guess that was it!" +texts[5]="Bye, bye!" +texts[6]="Cheers buddy, was nice knowing you!" +local r=math.random(#texts) +return texts[r] +end +function FOX:_CheckCoordSafe(coord) +if#self.safezones==0 then +return true +end +for _,_zone in pairs(self.safezones)do +local zone=_zone +local inzone=zone:IsCoordinateInZone(coord) +if inzone then +return true +end +end +return false +end +function FOX:_CheckCoordLaunch(coord) +if#self.launchzones==0 then +return true +end +for _,_zone in pairs(self.launchzones)do +local zone=_zone +local inzone=zone:IsCoordinateInZone(coord) +if inzone then +return true +end +end +return false +end +function FOX:_GetWeapongHeading(weapon) +if weapon and weapon:isExist()then +local wp=weapon:getPosition() +local wph=math.atan2(wp.x.z,wp.x.x) +if wph<0 then +wph=wph+2*math.pi +end +wph=math.deg(wph) +return wph +end +return-1 +end +function FOX:_SayNotchingHeadings(playerData,weapon) +if playerData and playerData.unit and playerData.unit:IsAlive()then +local nr,nl=self:_GetNotchingHeadings(weapon) +if nr and nl then +local text=string.format("Notching heading %03d° or %03d°",nr,nl) +MESSAGE:New(text,5,"FOX"):ToClient(playerData.client) +end +end +end +function FOX:_GetNotchingHeadings(weapon) +if weapon then +local hdg=self:_GetWeapongHeading(weapon) +local hdg1=hdg+90 +if hdg1>360 then +hdg1=hdg1-360 +end +local hdg2=hdg-90 +if hdg2<0 then +hdg2=hdg2+360 +end +return hdg1,hdg2 +end +return nil,nil +end +function FOX:_GetPlayerFromUnitname(unitName) +for _,_player in pairs(self.players)do +local player=_player +if player.unitname==unitName then +return player +end +end +return nil +end +function FOX:_GetPlayerFromUnit(unit) +if unit and unit:IsAlive()then +local unitname=unit:GetName() +for _,_player in pairs(self.players)do +local player=_player +if player.unitname==unitname then +return player +end +end +end +return nil +end +function FOX:_GetPlayerUnitAndName(_unitName) +self:F2(_unitName) +if _unitName~=nil then +local DCSunit=Unit.getByName(_unitName) +if DCSunit then +local playername=DCSunit:getPlayerName() +local unit=UNIT:Find(DCSunit) +self:T2({DCSunit=DCSunit,unit=unit,playername=playername}) +if DCSunit and unit and playername then +self:T(self.lid..string.format("Found DCS unit %s with player %s.",tostring(_unitName),tostring(playername))) +return unit,playername +end +end +end +return nil,nil +end +MANTIS={ +ClassName="MANTIS", +name="mymantis", +SAM_Templates_Prefix="", +SAM_Group=nil, +EWR_Templates_Prefix="", +EWR_Group=nil, +Adv_EWR_Group=nil, +HQ_Template_CC="", +HQ_CC=nil, +SAM_Table={}, +lid="", +Detection=nil, +AWACS_Detection=nil, +debug=false, +checkradius=25000, +grouping=5000, +acceptrange=80000, +detectinterval=30, +engagerange=75, +autorelocate=false, +advanced=false, +adv_ratio=100, +adv_state=0, +AWACS_Prefix="", +advAwacs=false, +verbose=false, +awacsrange=250000, +Shorad=nil, +ShoradLink=false, +ShoradTime=600, +ShoradActDistance=15000, +UseEmOnOff=true, +} +do +function MANTIS:New(name,samprefix,ewrprefix,hq,coaltion,dynamic,awacs,EmOnOff) +self.name=name or"mymantis" +self.SAM_Templates_Prefix=samprefix or"Red SAM" +self.EWR_Templates_Prefix=ewrprefix or"Red EWR" +self.HQ_Template_CC=hq or nil +self.Coalition=coaltion or"red" +self.SAM_Table={} +self.dynamic=dynamic or false +self.checkradius=25000 +self.grouping=5000 +self.acceptrange=80000 +self.detectinterval=30 +self.engagerange=75 +self.autorelocate=false +self.autorelocateunits={HQ=false,EWR=false} +self.advanced=false +self.adv_ratio=100 +self.adv_state=0 +self.verbose=false +self.Adv_EWR_Group=nil +self.AWACS_Prefix=awacs or nil +self.awacsrange=250000 +self.Shorad=nil +self.ShoradLink=false +self.ShoradTime=600 +self.ShoradActDistance=15000 +if EmOnOff then +if EmOnOff==false then +self.UseEmOnOff=false +end +end +if type(awacs)=="string"then +self.advAwacs=true +else +self.advAwacs=false +end +local self=BASE:Inherit(self,BASE:New()) +self.lid=string.format("MANTIS %s | ",self.name) +if self.debug then +BASE:TraceOnOff(true) +BASE:TraceClass(self.ClassName) +BASE:TraceLevel(1) +end +if self.dynamic then +self.SAM_Group=SET_GROUP:New():FilterPrefixes(self.SAM_Templates_Prefix):FilterCoalitions(self.Coalition):FilterStart() +self.EWR_Group=SET_GROUP:New():FilterPrefixes({self.SAM_Templates_Prefix,self.EWR_Templates_Prefix}):FilterCoalitions(self.Coalition):FilterStart() +else +self.SAM_Group=SET_GROUP:New():FilterPrefixes(self.SAM_Templates_Prefix):FilterCoalitions(self.Coalition):FilterOnce() +self.EWR_Group=SET_GROUP:New():FilterPrefixes({self.SAM_Templates_Prefix,self.EWR_Templates_Prefix}):FilterCoalitions(self.Coalition):FilterOnce() +end +if self.HQ_Template_CC then +self.HQ_CC=GROUP:FindByName(self.HQ_Template_CC) +end +self.version="0.4.1" +self:I(string.format("***** Starting MANTIS Version %s *****",self.version)) +return self +end +function MANTIS:_GetSAMTable() +return self.SAM_Table +end +function MANTIS:_SetSAMTable(table) +self.SAM_Table=table +return self +end +function MANTIS:SetEWRGrouping(radius) +local radius=radius or 5000 +self.grouping=radius +end +function MANTIS:SetEWRRange(radius) +local radius=radius or 80000 +self.acceptrange=radius +end +function MANTIS:SetSAMRadius(radius) +local radius=radius or 25000 +self.checkradius=radius +end +function MANTIS:SetSAMRange(range) +local range=range or 75 +if range<0 or range>100 then +range=75 +end +self.engagerange=range +end +function MANTIS:SetNewSAMRangeWhileRunning(range) +local range=range or 75 +if range<0 or range>100 then +range=75 +end +self.engagerange=range +self:_RefreshSAMTable() +self.mysead.EngagementRange=range +end +function MANTIS:Debug(onoff) +local onoff=onoff or false +self.debug=onoff +end +function MANTIS:GetCommandCenter() +if self.HQ_CC then +return self.HQ_CC +else +return nil +end +end +function MANTIS:SetAwacs(prefix) +if prefix~=nil then +if type(prefix)=="string"then +self.AWACS_Prefix=prefix +self.advAwacs=true +end +end +end +function MANTIS:SetAwacsRange(range) +local range=range or 250000 +self.awacsrange=range +end +function MANTIS:SetCommandCenter(group) +local group=group or nil +if group~=nil then +if type(group)=="string"then +self.HQ_CC=GROUP:FindByName(group) +self.HQ_Template_CC=group +else +self.HQ_CC=group +self.HQ_Template_CC=group:GetName() +end +end +end +function MANTIS:SetDetectInterval(interval) +local interval=interval or 30 +self.detectinterval=interval +end +function MANTIS:SetAdvancedMode(onoff,ratio) +self:F({onoff,ratio}) +local onoff=onoff or false +local ratio=ratio or 100 +if(type(self.HQ_Template_CC)=="string")and onoff and self.dynamic then +self.adv_ratio=ratio +self.advanced=true +self.adv_state=0 +self.Adv_EWR_Group=SET_GROUP:New():FilterPrefixes(self.EWR_Templates_Prefix):FilterCoalitions(self.Coalition):FilterStart() +env.info(string.format("***** Starting Advanced Mode MANTIS Version %s *****",self.version)) +else +local text=self.lid.." Advanced Mode requires a HQ and dynamic to be set. Revisit your MANTIS:New() statement to add both." +local m=MESSAGE:New(text,10,"MANTIS",true):ToAll() +BASE:E(text) +end +end +function MANTIS:SetUsingEmOnOff(switch) +self.UseEmOnOff=switch or false +end +function MANTIS:_CheckHQState() +local text=self.lid.." Checking HQ State" +self:T(text) +local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) +if self.verbose then env.info(text)end +if self.advanced then +local hq=self.HQ_Template_CC +local hqgrp=GROUP:FindByName(hq) +if hqgrp then +if hqgrp:IsAlive()then +env.info(self.lid.." HQ is alive!") +return true +else +env.info(self.lid.." HQ is dead!") +return false +end +end +end +end +function MANTIS:_CheckEWRState() +local text=self.lid.." Checking EWR State" +self:F(text) +local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) +if self.verbose then env.info(text)end +if self.advanced then +local EWR_Group=self.Adv_EWR_Group +local nalive=EWR_Group:CountAlive() +if self.advAwacs then +local awacs=GROUP:FindByName(self.AWACS_Prefix) +if awacs~=nil then +if awacs:IsAlive()then +nalive=nalive+1 +end +end +end +env.info(self.lid..string.format(" No of EWR alive is %d",nalive)) +if nalive>0 then +return true +else +return false +end +end +end +function MANTIS:_CheckAdvState() +local text=self.lid.." Checking Advanced State" +self:F(text) +local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) +if self.verbose then env.info(text)end +local currstate=self.adv_state +local EWR_State=self:_CheckEWRState() +local HQ_State=self:_CheckHQState() +if EWR_State and HQ_State then +self.adv_state=0 +elseif EWR_State or HQ_State then +self.adv_state=1 +else +self.adv_state=2 +end +local interval=self.detectinterval +local ratio=self.adv_ratio/100 +ratio=ratio*self.adv_state +local newinterval=interval+(interval*ratio) +local text=self.lid..string.format(" Calculated OldState/NewState/Interval: %d / %d / %d",currstate,self.adv_state,newinterval) +self:F(text) +local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) +if self.verbose then env.info(text)end +return newinterval,currstate +end +function MANTIS:SetAutoRelocate(hq,ewr) +self:F({hq,ewr}) +local hqrel=hq or false +local ewrel=ewr or false +if hqrel or ewrel then +self.autorelocate=true +self.autorelocateunits={HQ=hqrel,EWR=ewrel} +self:T({self.autorelocate,self.autorelocateunits}) +end +end +function MANTIS:_RelocateGroups() +self:T(self.lid.." Relocating Groups") +local text=self.lid.." Relocating Groups" +local m=MESSAGE:New(text,10,"MANTIS",true):ToAllIf(self.debug) +if self.verbose then env.info(text)end +if self.autorelocate then +if self.autorelocateunits.HQ and self.HQ_CC then +local _hqgrp=self.HQ_CC +self:T(self.lid.." Relocating HQ") +local text=self.lid.." Relocating HQ" +local m=MESSAGE:New(text,10,"MANTIS"):ToAll() +_hqgrp:RelocateGroundRandomInRadius(20,500,true,true) +end +if self.autorelocateunits.EWR then +local EWR_GRP=SET_GROUP:New():FilterPrefixes(self.EWR_Templates_Prefix):FilterCoalitions(self.Coalition):FilterOnce() +local EWR_Grps=EWR_GRP.Set +for _,_grp in pairs(EWR_Grps)do +if _grp:IsGround()then +self:T(self.lid.." Relocating EWR ".._grp:GetName()) +local text=self.lid.." Relocating EWR ".._grp:GetName() +local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) +if self.verbose then env.info(text)end +_grp:RelocateGroundRandomInRadius(20,500,true,true) +end +end +end +end +end +function MANTIS:CheckObjectInZone(dectset,samcoordinate) +self:F(self.lid.."CheckObjectInZone Called") +local radius=self.checkradius +local set=dectset +for _,_coord in pairs(set)do +local coord=_coord +local dectstring=coord:ToStringLLDMS() +local samstring=samcoordinate:ToStringLLDMS() +local targetdistance=samcoordinate:DistanceFromPointVec2(coord) +local text=string.format("Checking SAM at % s - Distance %d m - Target %s",samstring,targetdistance,dectstring) +local m=MESSAGE:New(text,10,"Check"):ToAllIf(self.debug) +if self.verbose then env.info(self.lid..text)end +if targetdistance<=radius then +return true,targetdistance +end +end +return false,0 +end +function MANTIS:StartDetection() +self:F(self.lid.."Starting Detection") +local groupset=self.EWR_Group +local grouping=self.grouping or 5000 +local acceptrange=self.acceptrange or 80000 +local interval=self.detectinterval or 60 +_MANTISdetection=DETECTION_AREAS:New(groupset,grouping) +_MANTISdetection:FilterCategories({Unit.Category.AIRPLANE,Unit.Category.HELICOPTER}) +_MANTISdetection:SetAcceptRange(acceptrange) +_MANTISdetection:SetRefreshTimeInterval(interval) +_MANTISdetection:Start() +function _MANTISdetection:OnAfterDetectedItem(From,Event,To,DetectedItem) +local debug=false +if DetectedItem.IsDetected and debug then +local Coordinate=DetectedItem.Coordinate +local text="MANTIS: Detection at "..Coordinate:ToStringLLDMS() +local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) +end +end +return _MANTISdetection +end +function MANTIS:StartAwacsDetection() +self:F(self.lid.."Starting Awacs Detection") +local group=self.AWACS_Prefix +local groupset=SET_GROUP:New():FilterPrefixes(group):FilterCoalitions(self.Coalition):FilterStart() +local grouping=self.grouping or 5000 +local interval=self.detectinterval or 60 +_MANTISAwacs=DETECTION_AREAS:New(groupset,grouping) +_MANTISAwacs:FilterCategories({Unit.Category.AIRPLANE,Unit.Category.HELICOPTER}) +_MANTISAwacs:SetAcceptRange(self.awacsrange) +_MANTISAwacs:SetRefreshTimeInterval(interval) +_MANTISAwacs:Start() +function _MANTISAwacs:OnAfterDetectedItem(From,Event,To,DetectedItem) +local debug=false +if DetectedItem.IsDetected and debug then +local Coordinate=DetectedItem.Coordinate +local text="Awacs Detection at "..Coordinate:ToStringLLDMS() +local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) +end +end +return _MANTISAwacs +end +function MANTIS:SetSAMStartState() +self:F(self.lid.."Setting SAM Start States") +local SAM_SET=self.SAM_Group +local SAM_Grps=SAM_SET.Set +local SAM_Tbl={} +local SEAD_Grps={} +local engagerange=self.engagerange +for _i,_group in pairs(SAM_Grps)do +local group=_group +if self.UseEmOnOff then +group:EnableEmission(false) +else +group:OptionAlarmStateGreen() +end +group:SetOption(AI.Option.Ground.id.AC_ENGAGEMENT_RANGE_RESTRICTION,engagerange) +if group:IsGround()then +local grpname=group:GetName() +local grpcoord=group:GetCoordinate() +table.insert(SAM_Tbl,{grpname,grpcoord}) +table.insert(SEAD_Grps,grpname) +end +end +self.SAM_Table=SAM_Tbl +local mysead=SEAD:New(SEAD_Grps) +mysead:SetEngagementRange(engagerange) +self.mysead=mysead +return self +end +function MANTIS:_RefreshSAMTable() +self:F(self.lid.."Setting SAM Start States") +local SAM_SET=self.SAM_Group +local SAM_Grps=SAM_SET.Set +local SAM_Tbl={} +local SEAD_Grps={} +local engagerange=self.engagerange +for _i,_group in pairs(SAM_Grps)do +local group=_group +group:SetOption(AI.Option.Ground.id.AC_ENGAGEMENT_RANGE_RESTRICTION,engagerange) +if group:IsGround()then +local grpname=group:GetName() +local grpcoord=group:GetCoordinate() +table.insert(SAM_Tbl,{grpname,grpcoord}) +table.insert(SEAD_Grps,grpname) +end +end +self.SAM_Table=SAM_Tbl +if self.mysead~=nil then +local mysead=self.mysead +mysead:UpdateSet(SEAD_Grps) +end +return self +end +function MANTIS:AddShorad(Shorad,Shoradtime) +local Shorad=Shorad or nil +local ShoradTime=Shoradtime or 600 +local ShoradLink=true +if Shorad:IsInstanceOf("SHORAD")then +self.ShoradLink=ShoradLink +self.Shorad=Shorad +self.ShoradTime=Shoradtime +end +end +function MANTIS:RemoveShorad() +self.ShoradLink=false +end +function MANTIS:Start() +self:F(self.lid.."Starting MANTIS") +self:SetSAMStartState() +self.Detection=self:StartDetection() +if self.advAwacs then +self.AWACS_Detection=self:StartAwacsDetection() +end +local function check(detection) +local detset=detection:GetDetectedItemCoordinates() +self:F("Check:",{detset}) +local rand=math.random(1,100) +if rand>65 then +self:_RefreshSAMTable() +end +local samset=self:_GetSAMTable() +for _,_data in pairs(samset)do +local samcoordinate=_data[2] +local name=_data[1] +local samgroup=GROUP:FindByName(name) +local IsInZone,Distance=self:CheckObjectInZone(detset,samcoordinate) +if IsInZone then +if samgroup:IsAlive()then +if self.UseEmOnOff then +samgroup:EnableEmission(true) +end +samgroup:OptionAlarmStateRed() +if self.ShoradLink and Distance100)or(low>high)then +low=70 +end +if(high<0)or(high>100)or(highTstop then +self:E(string.format("ERROR: Recovery stop time %s lies before recovery start time %s! Recovery window rejected.",UTILS.SecondsToClock(Tstart),UTILS.SecondsToClock(Tstop))) +return self +end +if Tstop<=Tnow then +self:I(string.format("WARNING: Recovery stop time %s already over. Tnow=%s! Recovery window rejected.",UTILS.SecondsToClock(Tstop),UTILS.SecondsToClock(Tnow))) +return self +end +case=case or self.defaultcase +holdingoffset=holdingoffset or self.defaultoffset +if case==1 then +holdingoffset=0 +end +self.windowcount=self.windowcount+1 +local recovery={} +recovery.START=Tstart +recovery.STOP=Tstop +recovery.CASE=case +recovery.OFFSET=holdingoffset +recovery.OPEN=false +recovery.OVER=false +recovery.WIND=turnintowind +recovery.SPEED=speed or 20 +recovery.ID=self.windowcount +if uturn==nil or uturn==true then +recovery.UTURN=true +else +recovery.UTURN=false +end +table.insert(self.recoverytimes,recovery) +return recovery +end +function AIRBOSS:SetSquadronAI(setgroup) +self.squadsetAI=setgroup +return self +end +function AIRBOSS:SetExcludeAI(setgroup) +self.excludesetAI=setgroup +return self +end +function AIRBOSS:AddExcludeAI(group) +self.excludesetAI=self.excludesetAI or SET_GROUP:New() +self.excludesetAI:AddGroup(group) +return self +end +function AIRBOSS:CloseCurrentRecoveryWindow(delay) +if delay and delay>0 then +self:ScheduleOnce(delay,self.CloseCurrentRecoveryWindow,self) +else +if self:IsRecovering()and self.recoverywindow and self.recoverywindow.OPEN then +self:RecoveryStop() +self.recoverywindow.OPEN=false +self.recoverywindow.OVER=true +self:DeleteRecoveryWindow(self.recoverywindow) +end +end +end +function AIRBOSS:DeleteAllRecoveryWindows(delay) +for _,recovery in pairs(self.recoverytimes)do +self:I(self.lid..string.format("Deleting recovery window ID %s",tostring(recovery.ID))) +self:DeleteRecoveryWindow(recovery,delay) +end +return self +end +function AIRBOSS:GetRecoveryWindowByID(id) +if id then +for _,_window in pairs(self.recoverytimes)do +local window=_window +if window and window.ID==id then +return window +end +end +end +return nil +end +function AIRBOSS:DeleteRecoveryWindow(window,delay) +if delay and delay>0 then +self:ScheduleOnce(delay,self.DeleteRecoveryWindow,self,window) +else +for i,_recovery in pairs(self.recoverytimes)do +local recovery=_recovery +if window and window.ID==recovery.ID then +if window.OPEN then +self:RecoveryStop() +else +table.remove(self.recoverytimes,i) +end +end +end +end +end +function AIRBOSS:SetRecoveryTurnTime(interval) +self.dTturn=interval or 300 +return self +end +function AIRBOSS:SetMPWireCorrection(Dcorr) +self.mpWireCorrection=Dcorr or 12 +return self +end +function AIRBOSS:SetQueueUpdateTime(interval) +self.dTqueue=interval or 30 +return self +end +function AIRBOSS:SetLSOCallInterval(timeinterval) +self.LSOdT=timeinterval or 4 +return self +end +function AIRBOSS:SetAirbossNiceGuy(switch) +if switch==true or switch==nil then +self.airbossnice=true +else +self.airbossnice=false +end +return self +end +function AIRBOSS:SetEmergencyLandings(switch) +if switch==true or switch==nil then +self.emergency=true +else +self.emergency=false +end +return self +end +function AIRBOSS:SetDespawnOnEngineShutdown(switch) +if switch==true or switch==nil then +self.despawnshutdown=true +else +self.despawnshutdown=false +end +return self +end +function AIRBOSS:SetRespawnAI(switch) +if switch==true or switch==nil then +self.respawnAI=true +else +self.respawnAI=false +end +return self +end +function AIRBOSS:SetRefuelAI(lowfuelthreshold) +self.lowfuelAI=lowfuelthreshold or 10 +return self +end +function AIRBOSS:SetInitialMaxAlt(altitude) +self.initialmaxalt=UTILS.FeetToMeters(altitude or 1300) +return self +end +function AIRBOSS:SetSoundfilesFolder(folderpath) +if folderpath then +local lastchar=string.sub(folderpath,-1) +if lastchar~="/"then +folderpath=folderpath.."/" +end +end +self.soundfolder=folderpath +self:I(self.lid..string.format("Setting sound files folder to: %s",self.soundfolder)) +return self +end +function AIRBOSS:SetStatusUpdateTime(interval) +self.dTstatus=interval or 0.5 +return self +end +function AIRBOSS:SetDefaultMessageDuration(duration) +self.Tmessage=duration or 10 +return self +end +function AIRBOSS:SetGlideslopeErrorThresholds(_max,_min,High,HIGH,Low,LOW) +self.gle._max=_max or 0.4 +self.gle.High=High or 0.8 +self.gle.HIGH=HIGH or 1.5 +self.gle._min=_min or-0.3 +self.gle.Low=Low or-0.6 +self.gle.LOW=LOW or-0.9 +return self +end +function AIRBOSS:SetLineupErrorThresholds(_max,_min,Left,LeftMed,LEFT,Right,RightMed,RIGHT) +self.lue._max=_max or 0.5 +self.lue._min=_min or-0.5 +self.lue.Left=Left or-1.0 +self.lue.LeftMed=LeftMed or-2.0 +self.lue.LEFT=LEFT or-3.0 +self.lue.Right=Right or 1.0 +self.lue.RightMed=RightMed or 2.0 +self.lue.RIGHT=RIGHT or 3.0 +return self +end +function AIRBOSS:SetMarshalRadius(radius) +self.marshalradius=UTILS.NMToMeters(radius or 2.8) +return self +end +function AIRBOSS:SetMenuSingleCarrier(switch) +if switch==true or switch==nil then +self.menusingle=true +else +self.menusingle=false +end +return self +end +function AIRBOSS:SetMenuMarkZones(switch) +if switch==nil or switch==true then +self.menumarkzones=true +else +self.menumarkzones=false +end +return self +end +function AIRBOSS:SetMenuSmokeZones(switch) +if switch==nil or switch==true then +self.menusmokezones=true +else +self.menusmokezones=false +end +return self +end +function AIRBOSS:SetTrapSheet(path,prefix) +if io then +self.trapsheet=true +self.trappath=path +self.trapprefix=prefix +else +self:E(self.lid.."ERROR: io is not desanitized. Cannot save trap sheet.") +end +return self +end +function AIRBOSS:SetStaticWeather(switch) +if switch==nil or switch==true then +self.staticweather=true +else +self.staticweather=false +end +return self +end +function AIRBOSS:SetTACANoff() +self.TACANon=false +return self +end +function AIRBOSS:SetTACAN(channel,mode,morsecode) +self.TACANchannel=channel or 74 +self.TACANmode=mode or"X" +self.TACANmorse=morsecode or"STN" +self.TACANon=true +return self +end +function AIRBOSS:SetICLSoff() +self.ICLSon=false +return self +end +function AIRBOSS:SetICLS(channel,morsecode) +self.ICLSchannel=channel or 1 +self.ICLSmorse=morsecode or"STN" +self.ICLSon=true +return self +end +function AIRBOSS:SetBeaconRefresh(interval) +self.dTbeacon=interval or 20*60 +return self +end +function AIRBOSS:SetLSORadio(frequency,modulation) +self.LSOFreq=(frequency or 264) +modulation=modulation or"AM" +if modulation=="FM"then +self.LSOModu=radio.modulation.FM +else +self.LSOModu=radio.modulation.AM +end +self.LSORadio={} +self.LSORadio.frequency=self.LSOFreq +self.LSORadio.modulation=self.LSOModu +self.LSORadio.alias="LSO" +return self +end +function AIRBOSS:SetMarshalRadio(frequency,modulation) +self.MarshalFreq=frequency or 305 +modulation=modulation or"AM" +if modulation=="FM"then +self.MarshalModu=radio.modulation.FM +else +self.MarshalModu=radio.modulation.AM +end +self.MarshalRadio={} +self.MarshalRadio.frequency=self.MarshalFreq +self.MarshalRadio.modulation=self.MarshalModu +self.MarshalRadio.alias="MARSHAL" +return self +end +function AIRBOSS:SetRadioUnitName(unitname) +self.senderac=unitname +return self +end +function AIRBOSS:SetRadioRelayLSO(unitname) +self.radiorelayLSO=unitname +return self +end +function AIRBOSS:SetRadioRelayMarshal(unitname) +self.radiorelayMSH=unitname +return self +end +function AIRBOSS:SetUserSoundRadio() +self.usersoundradio=true +return self +end +function AIRBOSS:SoundCheckLSO(delay) +if delay and delay>0 then +self:ScheduleOnce(delay,AIRBOSS.SoundCheckLSO,self) +else +local text="Playing LSO sound files:" +for _name,_call in pairs(self.LSOCall)do +local call=_call +text=text..string.format("\nFile=%s.%s, duration=%.2f sec, loud=%s, subtitle=\"%s\".",call.file,call.suffix,call.duration,tostring(call.loud),call.subtitle) +self:RadioTransmission(self.LSORadio,call,false) +if call.loud then +self:RadioTransmission(self.LSORadio,call,true) +end +end +self:I(self.lid..text) +end +end +function AIRBOSS:SoundCheckMarshal(delay) +if delay and delay>0 then +self:ScheduleOnce(delay,AIRBOSS.SoundCheckMarshal,self) +else +local text="Playing Marshal sound files:" +for _name,_call in pairs(self.MarshalCall)do +local call=_call +text=text..string.format("\nFile=%s.%s, duration=%.2f sec, loud=%s, subtitle=\"%s\".",call.file,call.suffix,call.duration,tostring(call.loud),call.subtitle) +self:RadioTransmission(self.MarshalRadio,call,false) +if call.loud then +self:RadioTransmission(self.MarshalRadio,call,true) +end +end +self:I(self.lid..text) +end +end +function AIRBOSS:SetMaxLandingPattern(nmax) +nmax=nmax or 4 +nmax=math.max(nmax,1) +nmax=math.min(nmax,6) +self.Nmaxpattern=nmax +return self +end +function AIRBOSS:SetMaxMarshalStacks(nmax) +self.Nmaxmarshal=nmax or 3 +self.Nmaxmarshal=math.max(self.Nmaxmarshal,1) +return self +end +function AIRBOSS:SetMaxSectionSize(nmax) +nmax=nmax or 2 +nmax=math.max(nmax,1) +nmax=math.min(nmax,4) +self.NmaxSection=nmax-1 +return self +end +function AIRBOSS:SetMaxFlightsPerStack(nmax) +nmax=nmax or 2 +nmax=math.max(nmax,1) +nmax=math.min(nmax,4) +self.NmaxStack=nmax +return self +end +function AIRBOSS:SetHandleAION() +self.handleai=true +return self +end +function AIRBOSS:SetHandleAIOFF() +self.handleai=false +return self +end +function AIRBOSS:SetRecoveryTanker(recoverytanker) +self.tanker=recoverytanker +return self +end +function AIRBOSS:SetAWACS(awacs) +self.awacs=awacs +return self +end +function AIRBOSS:SetDefaultPlayerSkill(skill) +self.defaultskill=skill or AIRBOSS.Difficulty.NORMAL +local gotit=false +for _,_skill in pairs(AIRBOSS.Difficulty)do +if _skill==self.defaultskill then +gotit=true +end +end +if not gotit then +self.defaultskill=AIRBOSS.Difficulty.NORMAL +self:E(self.lid..string.format("ERROR: Invalid default skill = %s. Resetting to Naval Aviator.",tostring(skill))) +end +return self +end +function AIRBOSS:SetAutoSave(path,filename) +self.autosave=true +self.autosavepath=path +self.autosavefile=filename +return self +end +function AIRBOSS:SetDebugModeON() +self.Debug=true +return self +end +function AIRBOSS:SetPatrolAdInfinitum(switch) +if switch==false then +self.adinfinitum=false +else +self.adinfinitum=true +end +return self +end +function AIRBOSS:SetMagneticDeclination(declination) +self.magvar=declination or UTILS.GetMagneticDeclination() +return self +end +function AIRBOSS:SetDebugModeOFF() +self.Debug=false +return self +end +function AIRBOSS:GetNextRecoveryTime(InSeconds) +if self.recoverywindow then +if InSeconds then +return self.recoverywindow.START,self.recoverywindow.STOP +else +return UTILS.SecondsToClock(self.recoverywindow.START),UTILS.SecondsToClock(self.recoverywindow.STOP) +end +else +if InSeconds then +return-1,-1 +else +return"?","?" +end +end +end +function AIRBOSS:IsRecovering() +return self:is("Recovering") +end +function AIRBOSS:IsIdle() +return self:is("Idle") +end +function AIRBOSS:IsPaused() +return self:is("Paused") +end +function AIRBOSS:_ActivateBeacons() +self:T(self.lid..string.format("Activating Beacons (TACAN=%s, ICLS=%s)",tostring(self.TACANon),tostring(self.ICLSon))) +if self.TACANon then +self:I(self.lid..string.format("Activating TACAN Channel %d%s (%s)",self.TACANchannel,self.TACANmode,self.TACANmorse)) +self.beacon:ActivateTACAN(self.TACANchannel,self.TACANmode,self.TACANmorse,true) +end +if self.ICLSon then +self:I(self.lid..string.format("Activating ICLS Channel %d (%s)",self.ICLSchannel,self.ICLSmorse)) +self.beacon:ActivateICLS(self.ICLSchannel,self.ICLSmorse) +end +self.Tbeacon=timer.getTime() +end +function AIRBOSS:onafterStart(From,Event,To) +self:I(self.lid..string.format("Starting AIRBOSS v%s for carrier unit %s of type %s on map %s",AIRBOSS.version,self.carrier:GetName(),self.carriertype,self.theatre)) +self:_ActivateBeacons() +self.Cposition=self:GetCoordinate() +self.Corientation=self.carrier:GetOrientationX() +self.Corientlast=self.Corientation +self.Tpupdate=timer.getTime() +if#self.recoverytimes==0 and false then +local Topen=timer.getAbsTime()+15*60 +local Tclose=Topen+3*60*60 +self:AddRecoveryWindow(UTILS.SecondsToClock(Topen),UTILS.SecondsToClock(Tclose)) +end +self:_CheckRecoveryTimes() +self.Tqueue=timer.getTime()-60 +self:HandleEvent(EVENTS.Birth) +self:HandleEvent(EVENTS.Land) +self:HandleEvent(EVENTS.EngineShutdown) +self:HandleEvent(EVENTS.Takeoff) +self:HandleEvent(EVENTS.Crash) +self:HandleEvent(EVENTS.Ejection) +self:HandleEvent(EVENTS.PlayerLeaveUnit,self._PlayerLeft) +self:HandleEvent(EVENTS.MissionEnd) +self:HandleEvent(EVENTS.RemoveUnit) +self.StatusTimer=TIMER:New(self._Status,self):Start(2,0.5) +self:__Status(1) +end +function AIRBOSS:onafterStatus(From,Event,To) +local time=timer.getTime() +if time-self.Tqueue>self.dTqueue then +local clock=UTILS.SecondsToClock(timer.getAbsTime()) +local eta=UTILS.SecondsToClock(self:_GetETAatNextWP()) +local hdg=self:GetHeading() +local pos=self:GetCoordinate() +local speed=self.carrier:GetVelocityKNOTS() +local collision=false +local holdtime=0 +if self.holdtimestamp then +holdtime=timer.getTime()-self.holdtimestamp +end +local NextWP=self:_GetNextWaypoint() +local ExpectedSpeed=UTILS.MpsToKnots(NextWP:GetVelocity()) +if speed<0.5 and ExpectedSpeed>0 and not(self.detour or self.turnintowind)then +if not self.holdtimestamp then +self:E(self.lid..string.format("Carrier came to an unexpected standstill. Trying to re-route in 3 min. Speed=%.1f knots, expected=%.1f knots",speed,ExpectedSpeed)) +self.holdtimestamp=timer.getTime() +else +if holdtime>3*60 then +local coord=self:GetCoordinate():Translate(500,hdg+10) +self:CarrierResumeRoute(coord) +self.holdtimestamp=nil +end +end +end +local text=string.format("Time %s - Status %s (case=%d) - Speed=%.1f kts - Heading=%d - WP=%d - ETA=%s - Turning=%s - Collision Warning=%s - Detour=%s - Turn Into Wind=%s - Holdtime=%d sec", +clock,self:GetState(),self.case,speed,hdg,self.currentwp,eta,tostring(self.turning),tostring(collision),tostring(self.detour),tostring(self.turnintowind),holdtime) +self:T(self.lid..text) +text="Players:" +local i=0 +for _name,_player in pairs(self.players)do +i=i+1 +local player=_player +text=text..string.format("\n%d.) %s: Step=%s, Unit=%s, Airframe=%s",i,tostring(player.name),tostring(player.step),tostring(player.unitname),tostring(player.actype)) +end +if i==0 then +text=text.." none" +end +self:I(self.lid..text) +if collision then +if self.turnintowind then +self:CarrierResumeRoute(self.Creturnto) +if self:IsRecovering()and self.recoverywindow and self.recoverywindow.WIND then +self.recoverywindow.WIND=false +end +end +end +self:_CheckRecoveryTimes() +self:_ScanCarrierZone() +self:_CheckQueue() +self:_CheckCarrierTurning() +self:_CheckPatternUpdate() +self.Tqueue=time +end +if time-self.Tbeacon>self.dTbeacon then +self:_ActivateBeacons() +end +self:__Status(-30) +end +function AIRBOSS:_Status() +self:_CheckPlayerStatus() +self:_CheckAIStatus() +end +function AIRBOSS:_CheckAIStatus() +for _,_flight in pairs(self.Qmarshal)do +local flight=_flight +if flight.ai then +local fuel=flight.group:GetFuelMin()*100 +local text=string.format("Group %s fuel=%.1f %%",flight.groupname,fuel) +self:T3(self.lid..text) +if self.lowfuelAI and fuel=recovery.START then +if time0 then +local extmin=5*npattern +recovery.STOP=recovery.STOP+extmin*60 +local text=string.format("We still got flights in the pattern.\nRecovery time prolonged by %d minutes.\nNow get your act together and no more bolters!",extmin) +self:MessageToPattern(text,"AIRBOSS","99",10,false,nil) +else +self:RecoveryStop() +state="closing now" +recovery.OPEN=false +recovery.OVER=true +end +else +state="closed" +end +end +else +state="in the future" +if nextwindow==nil then +nextwindow=recovery +state="next in line" +end +end +text=text..string.format("\n- Start=%s Stop=%s Case=%d Offset=%d Open=%s Closed=%s Status=\"%s\"",Cstart,Cstop,recovery.CASE,recovery.OFFSET,tostring(recovery.OPEN),tostring(recovery.OVER),state) +end +self:T(self.lid..text) +self.recoverywindow=nil +if self:IsIdle()then +if nextwindow then +self:RecoveryCase(nextwindow.CASE,nextwindow.OFFSET) +if nextwindow.WIND and nextwindow.START-time5 +local _,vwind=self:GetWind() +if vwind<0.1 then +uturn=false +end +if not nextwindow.UTURN then +uturn=false +end +self:T(self.lid..string.format("Heading=%03d°, Wind=%03d° %.1f kts, Delta=%03d° ==> U-turn=%s",hdg,wind,UTILS.MpsToKnots(vwind),delta,tostring(uturn))) +local t=math.max(nextwindow.STOP-nextwindow.START+self.dTturn,60*60*24) +local v=UTILS.KnotsToMps(nextwindow.SPEED) +local vmax=self.carrier:GetSpeedMax()/3.6 +v=math.min(v,vmax) +self:CarrierTurnIntoWind(t,v,uturn) +end +self.recoverywindow=nextwindow +else +self:RecoveryCase() +end +else +if currwindow then +self.recoverywindow=currwindow +else +self.recoverywindow=nextwindow +end +end +self:T2({"FF",recoverywindow=self.recoverywindow}) +end +function AIRBOSS:_GetFlightLead(flight) +if flight.name~=flight.seclead then +local lead=self.players[flight.seclead] +return lead,false +else +return flight,true +end +end +function AIRBOSS:onbeforeRecoveryCase(From,Event,To,Case,Offset) +Case=Case or self.defaultcase +Offset=Offset or self.defaultoffset +if Case==self.case and Offset==self.holdingoffset then +return false +end +return true +end +function AIRBOSS:onafterRecoveryCase(From,Event,To,Case,Offset) +Case=Case or self.defaultcase +Offset=Offset or self.defaultoffset +local text=string.format("Switching recovery case %d ==> %d",self.case,Case) +if Case>1 then +text=text..string.format(" Holding offset angle %d degrees.",Offset) +end +MESSAGE:New(text,20,self.alias):ToAllIf(self.Debug) +self:T(self.lid..text) +self.case=Case +self.holdingoffset=Offset +for _,_flight in pairs(self.flights)do +local flight=_flight +if not(self:_InQueue(self.Qmarshal,flight.group)or self:_InQueue(self.Qpattern,flight.group))then +if flight.name~=flight.seclead then +local lead=self.players[flight.seclead] +if lead and not(self:_InQueue(self.Qmarshal,lead.group)or self:_InQueue(self.Qpattern,lead.group))then +flight.case=self.case +end +else +flight.case=self.case +end +end +end +end +function AIRBOSS:onafterRecoveryStart(From,Event,To,Case,Offset) +Case=Case or self.defaultcase +Offset=Offset or self.defaultoffset +self:_MarshalCallRecoveryStart(Case) +self:RecoveryCase(Case,Offset) +end +function AIRBOSS:onafterRecoveryStop(From,Event,To) +self:T(self.lid..string.format("Stopping aircraft recovery.")) +self:_MarshalCallRecoveryStopped(self.case) +if self.turnintowind then +local coord=self.Creturnto +if self.recoverywindow and self.recoverywindow.UTURN==false then +coord=nil +end +self:CarrierResumeRoute(coord) +end +if self.recoverywindow and self.recoverywindow.OPEN==true then +self.recoverywindow.OPEN=false +self.recoverywindow.OVER=true +self:DeleteRecoveryWindow(self.recoverywindow) +end +self:_CheckRecoveryTimes() +end +function AIRBOSS:onafterRecoveryPause(From,Event,To,duration) +self:T(self.lid..string.format("Pausing aircraft recovery.")) +if duration then +self:__RecoveryUnpause(duration) +local clock=UTILS.SecondsToClock(timer.getAbsTime()+duration) +self:_MarshalCallRecoveryPausedResumedAt(clock) +else +local text=string.format("aircraft recovery is paused until further notice.") +self:_MarshalCallRecoveryPausedNotice() +end +end +function AIRBOSS:onafterRecoveryUnpause(From,Event,To) +self:T(self.lid..string.format("Unpausing aircraft recovery.")) +self:_MarshalCallResumeRecovery() +end +function AIRBOSS:onafterPassingWaypoint(From,Event,To,n) +self:I(self.lid..string.format("Carrier passed waypoint %d.",n)) +end +function AIRBOSS:onafterIdle(From,Event,To) +self:T(self.lid..string.format("Carrier goes to idle.")) +end +function AIRBOSS:onafterStop(From,Event,To) +self:I(self.lid..string.format("Stopping airboss script.")) +self:UnHandleEvent(EVENTS.Birth) +self:UnHandleEvent(EVENTS.Land) +self:UnHandleEvent(EVENTS.EngineShutdown) +self:UnHandleEvent(EVENTS.Takeoff) +self:UnHandleEvent(EVENTS.Crash) +self:UnHandleEvent(EVENTS.Ejection) +self:UnHandleEvent(EVENTS.PlayerLeaveUnit) +self:UnHandleEvent(EVENTS.MissionEnd) +self.CallScheduler:Clear() +end +function AIRBOSS:_InitStennis() +self.carrierparam.sterndist=-153 +self.carrierparam.deckheight=19.06 +self.carrierparam.totlength=310 +self.carrierparam.totwidthport=40 +self.carrierparam.totwidthstarboard=30 +self.carrierparam.rwyangle=-9.1359 +self.carrierparam.rwylength=225 +self.carrierparam.rwywidth=20 +self.carrierparam.wire1=46 +self.carrierparam.wire2=46+12 +self.carrierparam.wire3=46+24 +self.carrierparam.wire4=46+35 +self.Platform.name="Platform 5k" +self.Platform.Xmin=-UTILS.NMToMeters(22) +self.Platform.Xmax=nil +self.Platform.Zmin=-UTILS.NMToMeters(30) +self.Platform.Zmax=UTILS.NMToMeters(30) +self.Platform.LimitXmin=nil +self.Platform.LimitXmax=nil +self.Platform.LimitZmin=nil +self.Platform.LimitZmax=nil +self.DirtyUp.name="Dirty Up" +self.DirtyUp.Xmin=-UTILS.NMToMeters(21) +self.DirtyUp.Xmax=nil +self.DirtyUp.Zmin=-UTILS.NMToMeters(30) +self.DirtyUp.Zmax=UTILS.NMToMeters(30) +self.DirtyUp.LimitXmin=nil +self.DirtyUp.LimitXmax=nil +self.DirtyUp.LimitZmin=nil +self.DirtyUp.LimitZmax=nil +self.Bullseye.name="Bullseye" +self.Bullseye.Xmin=-UTILS.NMToMeters(11) +self.Bullseye.Xmax=nil +self.Bullseye.Zmin=-UTILS.NMToMeters(30) +self.Bullseye.Zmax=UTILS.NMToMeters(30) +self.Bullseye.LimitXmin=nil +self.Bullseye.LimitXmax=nil +self.Bullseye.LimitZmin=nil +self.Bullseye.LimitZmax=nil +self.BreakEntry.name="Break Entry" +self.BreakEntry.Xmin=-UTILS.NMToMeters(4) +self.BreakEntry.Xmax=nil +self.BreakEntry.Zmin=-UTILS.NMToMeters(0.5) +self.BreakEntry.Zmax=UTILS.NMToMeters(1.5) +self.BreakEntry.LimitXmin=0 +self.BreakEntry.LimitXmax=nil +self.BreakEntry.LimitZmin=nil +self.BreakEntry.LimitZmax=nil +self.BreakEarly.name="Early Break" +self.BreakEarly.Xmin=-UTILS.NMToMeters(1) +self.BreakEarly.Xmax=UTILS.NMToMeters(5) +self.BreakEarly.Zmin=-UTILS.NMToMeters(2) +self.BreakEarly.Zmax=UTILS.NMToMeters(1) +self.BreakEarly.LimitXmin=0 +self.BreakEarly.LimitXmax=nil +self.BreakEarly.LimitZmin=-UTILS.NMToMeters(0.2) +self.BreakEarly.LimitZmax=nil +self.BreakLate.name="Late Break" +self.BreakLate.Xmin=-UTILS.NMToMeters(1) +self.BreakLate.Xmax=UTILS.NMToMeters(5) +self.BreakLate.Zmin=-UTILS.NMToMeters(2) +self.BreakLate.Zmax=UTILS.NMToMeters(1) +self.BreakLate.LimitXmin=0 +self.BreakLate.LimitXmax=nil +self.BreakLate.LimitZmin=-UTILS.NMToMeters(0.8) +self.BreakLate.LimitZmax=nil +self.Abeam.name="Abeam Position" +self.Abeam.Xmin=-UTILS.NMToMeters(5) +self.Abeam.Xmax=UTILS.NMToMeters(5) +self.Abeam.Zmin=-UTILS.NMToMeters(2) +self.Abeam.Zmax=500 +self.Abeam.LimitXmin=-200 +self.Abeam.LimitXmax=nil +self.Abeam.LimitZmin=nil +self.Abeam.LimitZmax=nil +self.Ninety.name="Ninety" +self.Ninety.Xmin=-UTILS.NMToMeters(4) +self.Ninety.Xmax=0 +self.Ninety.Zmin=-UTILS.NMToMeters(2) +self.Ninety.Zmax=nil +self.Ninety.LimitXmin=nil +self.Ninety.LimitXmax=nil +self.Ninety.LimitZmin=nil +self.Ninety.LimitZmax=-UTILS.NMToMeters(0.6) +self.Wake.name="Wake" +self.Wake.Xmin=-UTILS.NMToMeters(4) +self.Wake.Xmax=0 +self.Wake.Zmin=-2000 +self.Wake.Zmax=nil +self.Wake.LimitXmin=nil +self.Wake.LimitXmax=nil +self.Wake.LimitZmin=0 +self.Wake.LimitZmax=nil +self.Final.name="Final" +self.Final.Xmin=-UTILS.NMToMeters(4) +self.Final.Xmax=0 +self.Final.Zmin=-2000 +self.Final.Zmax=nil +self.Final.LimitXmin=nil +self.Final.LimitXmax=nil +self.Final.LimitZmin=nil +self.Final.LimitZmax=nil +self.Groove.name="Groove" +self.Groove.Xmin=-UTILS.NMToMeters(4) +self.Groove.Xmax=nil +self.Groove.Zmin=-UTILS.NMToMeters(2) +self.Groove.Zmax=UTILS.NMToMeters(2) +self.Groove.LimitXmin=nil +self.Groove.LimitXmax=nil +self.Groove.LimitZmin=nil +self.Groove.LimitZmax=nil +end +function AIRBOSS:_InitNimitz() +self:_InitStennis() +self.carrierparam.sterndist=-164 +self.carrierparam.deckheight=20.1494 +self.carrierparam.totlength=332.8 +self.carrierparam.totwidthport=45 +self.carrierparam.totwidthstarboard=35 +self.carrierparam.rwyangle=-9.1359 +self.carrierparam.rwylength=250 +self.carrierparam.rwywidth=25 +self.carrierparam.wire1=55 +self.carrierparam.wire2=67 +self.carrierparam.wire3=79 +self.carrierparam.wire4=92 +end +function AIRBOSS:_InitTarawa() +self:_InitStennis() +self.carrierparam.sterndist=-125 +self.carrierparam.deckheight=21 +self.carrierparam.totlength=245 +self.carrierparam.totwidthport=10 +self.carrierparam.totwidthstarboard=25 +self.carrierparam.rwyangle=0 +self.carrierparam.rwylength=225 +self.carrierparam.rwywidth=15 +self.carrierparam.wire1=nil +self.carrierparam.wire2=nil +self.carrierparam.wire3=nil +self.carrierparam.wire4=nil +self.BreakLate.name="Late Break" +self.BreakLate.Xmin=-UTILS.NMToMeters(1) +self.BreakLate.Xmax=UTILS.NMToMeters(5) +self.BreakLate.Zmin=-UTILS.NMToMeters(1.6) +self.BreakLate.Zmax=UTILS.NMToMeters(1) +self.BreakLate.LimitXmin=0 +self.BreakLate.LimitXmax=nil +self.BreakLate.LimitZmin=-UTILS.NMToMeters(0.5) +self.BreakLate.LimitZmax=nil +end +function AIRBOSS:SetVoiceOversMarshalByGabriella(mizfolder) +if mizfolder then +local lastchar=string.sub(mizfolder,-1) +if lastchar~="/"then +mizfolder=mizfolder.."/" +end +self.soundfolderMSH=mizfolder +else +self.soundfolderMSH=self.soundfolder +end +self:I(self.lid..string.format("Marshal Gabriella reporting for duty! Soundfolder=%s",tostring(self.soundfolderMSH))) +self.MarshalCall.AFFIRMATIVE.duration=0.65 +self.MarshalCall.ALTIMETER.duration=0.60 +self.MarshalCall.BRC.duration=0.67 +self.MarshalCall.CARRIERTURNTOHEADING.duration=1.62 +self.MarshalCall.CASE.duration=0.30 +self.MarshalCall.CHARLIETIME.duration=0.77 +self.MarshalCall.CLEAREDFORRECOVERY.duration=0.93 +self.MarshalCall.DECKCLOSED.duration=0.73 +self.MarshalCall.DEGREES.duration=0.48 +self.MarshalCall.EXPECTED.duration=0.50 +self.MarshalCall.FLYNEEDLES.duration=0.89 +self.MarshalCall.HOLDATANGELS.duration=0.81 +self.MarshalCall.HOURS.duration=0.41 +self.MarshalCall.MARSHALRADIAL.duration=0.95 +self.MarshalCall.N0.duration=0.41 +self.MarshalCall.N1.duration=0.30 +self.MarshalCall.N2.duration=0.34 +self.MarshalCall.N3.duration=0.31 +self.MarshalCall.N4.duration=0.34 +self.MarshalCall.N5.duration=0.30 +self.MarshalCall.N6.duration=0.33 +self.MarshalCall.N7.duration=0.38 +self.MarshalCall.N8.duration=0.35 +self.MarshalCall.N9.duration=0.35 +self.MarshalCall.NEGATIVE.duration=0.60 +self.MarshalCall.NEWFB.duration=0.95 +self.MarshalCall.OPS.duration=0.23 +self.MarshalCall.POINT.duration=0.38 +self.MarshalCall.RADIOCHECK.duration=1.27 +self.MarshalCall.RECOVERY.duration=0.60 +self.MarshalCall.RECOVERYOPSSTOPPED.duration=1.25 +self.MarshalCall.RECOVERYPAUSEDNOTICE.duration=2.55 +self.MarshalCall.RECOVERYPAUSEDRESUMED.duration=2.55 +self.MarshalCall.REPORTSEEME.duration=0.87 +self.MarshalCall.RESUMERECOVERY.duration=1.55 +self.MarshalCall.ROGER.duration=0.50 +self.MarshalCall.SAYNEEDLES.duration=0.82 +self.MarshalCall.STACKFULL.duration=5.70 +self.MarshalCall.STARTINGRECOVERY.duration=1.61 +end +function AIRBOSS:SetVoiceOversMarshalByRaynor(mizfolder) +if mizfolder then +local lastchar=string.sub(mizfolder,-1) +if lastchar~="/"then +mizfolder=mizfolder.."/" +end +self.soundfolderMSH=mizfolder +else +self.soundfolderMSH=self.soundfolder +end +self:I(self.lid..string.format("Marshal Raynor reporting for duty! Soundfolder=%s",tostring(self.soundfolderMSH))) +self.MarshalCall.AFFIRMATIVE.duration=0.70 +self.MarshalCall.ALTIMETER.duration=0.60 +self.MarshalCall.BRC.duration=0.60 +self.MarshalCall.CARRIERTURNTOHEADING.duration=1.87 +self.MarshalCall.CASE.duration=0.60 +self.MarshalCall.CHARLIETIME.duration=0.81 +self.MarshalCall.CLEAREDFORRECOVERY.duration=1.21 +self.MarshalCall.DECKCLOSED.duration=0.86 +self.MarshalCall.DEGREES.duration=0.55 +self.MarshalCall.EXPECTED.duration=0.61 +self.MarshalCall.FLYNEEDLES.duration=0.90 +self.MarshalCall.HOLDATANGELS.duration=0.91 +self.MarshalCall.HOURS.duration=0.54 +self.MarshalCall.MARSHALRADIAL.duration=0.80 +self.MarshalCall.N0.duration=0.38 +self.MarshalCall.N1.duration=0.30 +self.MarshalCall.N2.duration=0.30 +self.MarshalCall.N3.duration=0.30 +self.MarshalCall.N4.duration=0.32 +self.MarshalCall.N5.duration=0.41 +self.MarshalCall.N6.duration=0.48 +self.MarshalCall.N7.duration=0.51 +self.MarshalCall.N8.duration=0.38 +self.MarshalCall.N9.duration=0.34 +self.MarshalCall.NEGATIVE.duration=0.60 +self.MarshalCall.NEWFB.duration=1.10 +self.MarshalCall.OPS.duration=0.46 +self.MarshalCall.POINT.duration=0.21 +self.MarshalCall.RADIOCHECK.duration=0.95 +self.MarshalCall.RECOVERY.duration=0.63 +self.MarshalCall.RECOVERYOPSSTOPPED.duration=1.36 +self.MarshalCall.RECOVERYPAUSEDNOTICE.duration=2.8 +self.MarshalCall.RECOVERYPAUSEDRESUMED.duration=2.75 +self.MarshalCall.REPORTSEEME.duration=1.06 +self.MarshalCall.RESUMERECOVERY.duration=1.41 +self.MarshalCall.ROGER.duration=0.41 +self.MarshalCall.SAYNEEDLES.duration=0.79 +self.MarshalCall.STACKFULL.duration=4.70 +self.MarshalCall.STARTINGRECOVERY.duration=2.06 +end +function AIRBOSS:SetVoiceOversLSOByRaynor(mizfolder) +if mizfolder then +local lastchar=string.sub(mizfolder,-1) +if lastchar~="/"then +mizfolder=mizfolder.."/" +end +self.soundfolderLSO=mizfolder +else +self.soundfolderLSO=self.soundfolder +end +self:I(self.lid..string.format("LSO Raynor reporting for duty! Soundfolder=%s",tostring(self.soundfolderLSO))) +self.LSOCall.BOLTER.duration=0.75 +self.LSOCall.CALLTHEBALL.duration=0.625 +self.LSOCall.CHECK.duration=0.40 +self.LSOCall.CLEAREDTOLAND.duration=0.85 +self.LSOCall.COMELEFT.duration=0.60 +self.LSOCall.DEPARTANDREENTER.duration=1.10 +self.LSOCall.EXPECTHEAVYWAVEOFF.duration=1.30 +self.LSOCall.EXPECTSPOT75.duration=1.85 +self.LSOCall.FAST.duration=0.75 +self.LSOCall.FOULDECK.duration=0.75 +self.LSOCall.HIGH.duration=0.65 +self.LSOCall.IDLE.duration=0.40 +self.LSOCall.LONGINGROOVE.duration=1.25 +self.LSOCall.LOW.duration=0.60 +self.LSOCall.N0.duration=0.38 +self.LSOCall.N1.duration=0.30 +self.LSOCall.N2.duration=0.30 +self.LSOCall.N3.duration=0.30 +self.LSOCall.N4.duration=0.32 +self.LSOCall.N5.duration=0.41 +self.LSOCall.N6.duration=0.48 +self.LSOCall.N7.duration=0.51 +self.LSOCall.N8.duration=0.38 +self.LSOCall.N9.duration=0.34 +self.LSOCall.PADDLESCONTACT.duration=0.91 +self.LSOCall.POWER.duration=0.45 +self.LSOCall.RADIOCHECK.duration=0.90 +self.LSOCall.RIGHTFORLINEUP.duration=0.70 +self.LSOCall.ROGERBALL.duration=0.72 +self.LSOCall.SLOW.duration=0.63 +self.LSOCall.STABILIZED.duration=0.75 +self.LSOCall.WAVEOFF.duration=0.55 +self.LSOCall.WELCOMEABOARD.duration=0.80 +end +function AIRBOSS:SetVoiceOversLSOByFF(mizfolder) +if mizfolder then +local lastchar=string.sub(mizfolder,-1) +if lastchar~="/"then +mizfolder=mizfolder.."/" +end +self.soundfolderLSO=mizfolder +else +self.soundfolderLSO=self.soundfolder +end +self:I(self.lid..string.format("LSO FF reporting for duty! Soundfolder=%s",tostring(self.soundfolderLSO))) +self.LSOCall.BOLTER.duration=0.75 +self.LSOCall.CALLTHEBALL.duration=0.60 +self.LSOCall.CHECK.duration=0.45 +self.LSOCall.CLEAREDTOLAND.duration=1.00 +self.LSOCall.COMELEFT.duration=0.60 +self.LSOCall.DEPARTANDREENTER.duration=1.10 +self.LSOCall.EXPECTHEAVYWAVEOFF.duration=1.20 +self.LSOCall.EXPECTSPOT75.duration=2.00 +self.LSOCall.FAST.duration=0.70 +self.LSOCall.FOULDECK.duration=0.62 +self.LSOCall.HIGH.duration=0.65 +self.LSOCall.IDLE.duration=0.45 +self.LSOCall.LONGINGROOVE.duration=1.20 +self.LSOCall.LOW.duration=0.50 +self.LSOCall.N0.duration=0.40 +self.LSOCall.N1.duration=0.25 +self.LSOCall.N2.duration=0.37 +self.LSOCall.N3.duration=0.37 +self.LSOCall.N4.duration=0.39 +self.LSOCall.N5.duration=0.39 +self.LSOCall.N6.duration=0.40 +self.LSOCall.N7.duration=0.40 +self.LSOCall.N8.duration=0.37 +self.LSOCall.N9.duration=0.40 +self.LSOCall.PADDLESCONTACT.duration=1.00 +self.LSOCall.POWER.duration=0.50 +self.LSOCall.RADIOCHECK.duration=1.10 +self.LSOCall.RIGHTFORLINEUP.duration=0.80 +self.LSOCall.ROGERBALL.duration=1.00 +self.LSOCall.SLOW.duration=0.65 +self.LSOCall.SLOW.duration=0.59 +self.LSOCall.STABILIZED.duration=0.90 +self.LSOCall.WAVEOFF.duration=0.60 +self.LSOCall.WELCOMEABOARD.duration=1.00 +end +function AIRBOSS:SetVoiceOversMarshalByFF(mizfolder) +if mizfolder then +local lastchar=string.sub(mizfolder,-1) +if lastchar~="/"then +mizfolder=mizfolder.."/" +end +self.soundfolderMSH=mizfolder +else +self.soundfolderMSH=self.soundfolder +end +self:I(self.lid..string.format("Marshal FF reporting for duty! Soundfolder=%s",tostring(self.soundfolderMSH))) +self.MarshalCall.AFFIRMATIVE.duration=0.90 +self.MarshalCall.ALTIMETER.duration=0.85 +self.MarshalCall.BRC.duration=0.80 +self.MarshalCall.CARRIERTURNTOHEADING.duration=2.48 +self.MarshalCall.CASE.duration=0.40 +self.MarshalCall.CHARLIETIME.duration=0.90 +self.MarshalCall.CLEAREDFORRECOVERY.duration=1.25 +self.MarshalCall.DECKCLOSED.duration=1.10 +self.MarshalCall.DEGREES.duration=0.60 +self.MarshalCall.EXPECTED.duration=0.55 +self.MarshalCall.FLYNEEDLES.duration=0.90 +self.MarshalCall.HOLDATANGELS.duration=1.10 +self.MarshalCall.HOURS.duration=0.60 +self.MarshalCall.MARSHALRADIAL.duration=1.10 +self.MarshalCall.N0.duration=0.40 +self.MarshalCall.N1.duration=0.25 +self.MarshalCall.N2.duration=0.37 +self.MarshalCall.N3.duration=0.37 +self.MarshalCall.N4.duration=0.39 +self.MarshalCall.N5.duration=0.39 +self.MarshalCall.N6.duration=0.40 +self.MarshalCall.N7.duration=0.40 +self.MarshalCall.N8.duration=0.37 +self.MarshalCall.N9.duration=0.40 +self.MarshalCall.NEGATIVE.duration=0.80 +self.MarshalCall.NEWFB.duration=1.35 +self.MarshalCall.OPS.duration=0.48 +self.MarshalCall.POINT.duration=0.33 +self.MarshalCall.RADIOCHECK.duration=1.20 +self.MarshalCall.RECOVERY.duration=0.70 +self.MarshalCall.RECOVERYOPSSTOPPED.duration=1.65 +self.MarshalCall.RECOVERYPAUSEDNOTICE.duration=2.9 +self.MarshalCall.RECOVERYPAUSEDRESUMED.duration=3.40 +self.MarshalCall.REPORTSEEME.duration=0.95 +self.MarshalCall.RESUMERECOVERY.duration=1.75 +self.MarshalCall.ROGER.duration=0.53 +self.MarshalCall.SAYNEEDLES.duration=0.90 +self.MarshalCall.STACKFULL.duration=6.35 +self.MarshalCall.STARTINGRECOVERY.duration=2.65 +end +function AIRBOSS:_InitVoiceOvers() +self.LSOCall={ +BOLTER={ +file="LSO-BolterBolter", +suffix="ogg", +loud=false, +subtitle="Bolter, Bolter", +duration=0.75, +subduration=5, +}, +CALLTHEBALL={ +file="LSO-CallTheBall", +suffix="ogg", +loud=false, +subtitle="Call the ball", +duration=0.6, +subduration=2, +}, +CHECK={ +file="LSO-Check", +suffix="ogg", +loud=false, +subtitle="Check", +duration=0.45, +subduration=2.5, +}, +CLEAREDTOLAND={ +file="LSO-ClearedToLand", +suffix="ogg", +loud=false, +subtitle="Cleared to land", +duration=1.0, +subduration=5, +}, +COMELEFT={ +file="LSO-ComeLeft", +suffix="ogg", +loud=true, +subtitle="Come left", +duration=0.60, +subduration=1, +}, +RADIOCHECK={ +file="LSO-RadioCheck", +suffix="ogg", +loud=false, +subtitle="Paddles, radio check", +duration=1.1, +subduration=5, +}, +RIGHTFORLINEUP={ +file="LSO-RightForLineup", +suffix="ogg", +loud=true, +subtitle="Right for line up", +duration=0.80, +subduration=1, +}, +HIGH={ +file="LSO-High", +suffix="ogg", +loud=true, +subtitle="You're high", +duration=0.65, +subduration=1, +}, +LOW={ +file="LSO-Low", +suffix="ogg", +loud=true, +subtitle="You're low", +duration=0.50, +subduration=1, +}, +POWER={ +file="LSO-Power", +suffix="ogg", +loud=true, +subtitle="Power", +duration=0.50, +subduration=1, +}, +SLOW={ +file="LSO-Slow", +suffix="ogg", +loud=true, +subtitle="You're slow", +duration=0.65, +subduration=1, +}, +FAST={ +file="LSO-Fast", +suffix="ogg", +loud=true, +subtitle="You're fast", +duration=0.70, +subduration=1, +}, +ROGERBALL={ +file="LSO-RogerBall", +suffix="ogg", +loud=false, +subtitle="Roger ball", +duration=1.00, +subduration=2, +}, +WAVEOFF={ +file="LSO-WaveOff", +suffix="ogg", +loud=false, +subtitle="Wave off", +duration=0.6, +subduration=5, +}, +LONGINGROOVE={ +file="LSO-LongInTheGroove", +suffix="ogg", +loud=false, +subtitle="You're long in the groove", +duration=1.2, +subduration=5, +}, +FOULDECK={ +file="LSO-FoulDeck", +suffix="ogg", +loud=false, +subtitle="Foul deck", +duration=0.62, +subduration=5, +}, +DEPARTANDREENTER={ +file="LSO-DepartAndReenter", +suffix="ogg", +loud=false, +subtitle="Depart and re-enter", +duration=1.1, +subduration=5, +}, +PADDLESCONTACT={ +file="LSO-PaddlesContact", +suffix="ogg", +loud=false, +subtitle="Paddles, contact", +duration=1.0, +subduration=5, +}, +WELCOMEABOARD={ +file="LSO-WelcomeAboard", +suffix="ogg", +loud=false, +subtitle="Welcome aboard", +duration=1.0, +subduration=5, +}, +EXPECTHEAVYWAVEOFF={ +file="LSO-ExpectHeavyWaveoff", +suffix="ogg", +loud=false, +subtitle="Expect heavy waveoff", +duration=1.2, +subduration=5, +}, +EXPECTSPOT75={ +file="LSO-ExpectSpot75", +suffix="ogg", +loud=false, +subtitle="Expect spot 7.5", +duration=2.0, +subduration=5, +}, +STABILIZED={ +file="LSO-Stabilized", +suffix="ogg", +loud=false, +subtitle="Stabilized", +duration=0.9, +subduration=5, +}, +IDLE={ +file="LSO-Idle", +suffix="ogg", +loud=false, +subtitle="Idle", +duration=0.45, +subduration=5, +}, +N0={ +file="LSO-N0", +suffix="ogg", +loud=false, +subtitle="", +duration=0.40, +}, +N1={ +file="LSO-N1", +suffix="ogg", +loud=false, +subtitle="", +duration=0.25, +}, +N2={ +file="LSO-N2", +suffix="ogg", +loud=false, +subtitle="", +duration=0.37, +}, +N3={ +file="LSO-N3", +suffix="ogg", +loud=false, +subtitle="", +duration=0.37, +}, +N4={ +file="LSO-N4", +suffix="ogg", +loud=false, +subtitle="", +duration=0.39, +}, +N5={ +file="LSO-N5", +suffix="ogg", +loud=false, +subtitle="", +duration=0.39, +}, +N6={ +file="LSO-N6", +suffix="ogg", +loud=false, +subtitle="", +duration=0.40, +}, +N7={ +file="LSO-N7", +suffix="ogg", +loud=false, +subtitle="", +duration=0.40, +}, +N8={ +file="LSO-N8", +suffix="ogg", +loud=false, +subtitle="", +duration=0.37, +}, +N9={ +file="LSO-N9", +suffix="ogg", +loud=false, +subtitle="", +duration=0.40, +}, +CLICK={ +file="AIRBOSS-RadioClick", +suffix="ogg", +loud=false, +subtitle="", +duration=0.35, +}, +NOISE={ +file="AIRBOSS-Noise", +suffix="ogg", +loud=false, +subtitle="", +duration=3.6, +}, +SPINIT={ +file="AIRBOSS-SpinIt", +suffix="ogg", +loud=false, +subtitle="", +duration=0.73, +subduration=5, +}, +} +self.PilotCall={ +N0={ +file="PILOT-N0", +suffix="ogg", +loud=false, +subtitle="", +duration=0.40, +}, +N1={ +file="PILOT-N1", +suffix="ogg", +loud=false, +subtitle="", +duration=0.25, +}, +N2={ +file="PILOT-N2", +suffix="ogg", +loud=false, +subtitle="", +duration=0.37, +}, +N3={ +file="PILOT-N3", +suffix="ogg", +loud=false, +subtitle="", +duration=0.37, +}, +N4={ +file="PILOT-N4", +suffix="ogg", +loud=false, +subtitle="", +duration=0.39, +}, +N5={ +file="PILOT-N5", +suffix="ogg", +loud=false, +subtitle="", +duration=0.39, +}, +N6={ +file="PILOT-N6", +suffix="ogg", +loud=false, +subtitle="", +duration=0.40, +}, +N7={ +file="PILOT-N7", +suffix="ogg", +loud=false, +subtitle="", +duration=0.40, +}, +N8={ +file="PILOT-N8", +suffix="ogg", +loud=false, +subtitle="", +duration=0.37, +}, +N9={ +file="PILOT-N9", +suffix="ogg", +loud=false, +subtitle="", +duration=0.40, +}, +POINT={ +file="PILOT-Point", +suffix="ogg", +loud=false, +subtitle="", +duration=0.33, +}, +SKYHAWK={ +file="PILOT-Skyhawk", +suffix="ogg", +loud=false, +subtitle="", +duration=0.95, +subduration=5, +}, +HARRIER={ +file="PILOT-Harrier", +suffix="ogg", +loud=false, +subtitle="", +duration=0.58, +subduration=5, +}, +HAWKEYE={ +file="PILOT-Hawkeye", +suffix="ogg", +loud=false, +subtitle="", +duration=0.63, +subduration=5, +}, +TOMCAT={ +file="PILOT-Tomcat", +suffix="ogg", +loud=false, +subtitle="", +duration=0.66, +subduration=5, +}, +HORNET={ +file="PILOT-Hornet", +suffix="ogg", +loud=false, +subtitle="", +duration=0.56, +subduration=5, +}, +VIKING={ +file="PILOT-Viking", +suffix="ogg", +loud=false, +subtitle="", +duration=0.61, +subduration=5, +}, +BALL={ +file="PILOT-Ball", +suffix="ogg", +loud=false, +subtitle="", +duration=0.50, +subduration=5, +}, +BINGOFUEL={ +file="PILOT-BingoFuel", +suffix="ogg", +loud=false, +subtitle="", +duration=0.80, +}, +GASATDIVERT={ +file="PILOT-GasAtDivert", +suffix="ogg", +loud=false, +subtitle="", +duration=1.80, +}, +GASATTANKER={ +file="PILOT-GasAtTanker", +suffix="ogg", +loud=false, +subtitle="", +duration=1.95, +}, +} +self.MarshalCall={ +AFFIRMATIVE={ +file="MARSHAL-Affirmative", +suffix="ogg", +loud=false, +subtitle="", +duration=0.90, +}, +ALTIMETER={ +file="MARSHAL-Altimeter", +suffix="ogg", +loud=false, +subtitle="", +duration=0.85, +}, +BRC={ +file="MARSHAL-BRC", +suffix="ogg", +loud=false, +subtitle="", +duration=0.80, +}, +CARRIERTURNTOHEADING={ +file="MARSHAL-CarrierTurnToHeading", +suffix="ogg", +loud=false, +subtitle="", +duration=2.48, +subduration=5, +}, +CASE={ +file="MARSHAL-Case", +suffix="ogg", +loud=false, +subtitle="", +duration=0.40, +}, +CHARLIETIME={ +file="MARSHAL-CharlieTime", +suffix="ogg", +loud=false, +subtitle="", +duration=0.90, +}, +CLEAREDFORRECOVERY={ +file="MARSHAL-ClearedForRecovery", +suffix="ogg", +loud=false, +subtitle="", +duration=1.25, +}, +DECKCLOSED={ +file="MARSHAL-DeckClosed", +suffix="ogg", +loud=false, +subtitle="", +duration=1.10, +subduration=5, +}, +DEGREES={ +file="MARSHAL-Degrees", +suffix="ogg", +loud=false, +subtitle="", +duration=0.60, +}, +EXPECTED={ +file="MARSHAL-Expected", +suffix="ogg", +loud=false, +subtitle="", +duration=0.55, +}, +FLYNEEDLES={ +file="MARSHAL-FlyYourNeedles", +suffix="ogg", +loud=false, +subtitle="Fly your needles", +duration=0.9, +subduration=5, +}, +HOLDATANGELS={ +file="MARSHAL-HoldAtAngels", +suffix="ogg", +loud=false, +subtitle="", +duration=1.10, +}, +HOURS={ +file="MARSHAL-Hours", +suffix="ogg", +loud=false, +subtitle="", +duration=0.60, +subduration=5, +}, +MARSHALRADIAL={ +file="MARSHAL-MarshalRadial", +suffix="ogg", +loud=false, +subtitle="", +duration=1.10, +}, +N0={ +file="MARSHAL-N0", +suffix="ogg", +loud=false, +subtitle="", +duration=0.40, +}, +N1={ +file="MARSHAL-N1", +suffix="ogg", +loud=false, +subtitle="", +duration=0.25, +}, +N2={ +file="MARSHAL-N2", +suffix="ogg", +loud=false, +subtitle="", +duration=0.37, +}, +N3={ +file="MARSHAL-N3", +suffix="ogg", +loud=false, +subtitle="", +duration=0.37, +}, +N4={ +file="MARSHAL-N4", +suffix="ogg", +loud=false, +subtitle="", +duration=0.39, +}, +N5={ +file="MARSHAL-N5", +suffix="ogg", +loud=false, +subtitle="", +duration=0.39, +}, +N6={ +file="MARSHAL-N6", +suffix="ogg", +loud=false, +subtitle="", +duration=0.40, +}, +N7={ +file="MARSHAL-N7", +suffix="ogg", +loud=false, +subtitle="", +duration=0.40, +}, +N8={ +file="MARSHAL-N8", +suffix="ogg", +loud=false, +subtitle="", +duration=0.37, +}, +N9={ +file="MARSHAL-N9", +suffix="ogg", +loud=false, +subtitle="", +duration=0.40, +}, +NEGATIVE={ +file="MARSHAL-Negative", +suffix="ogg", +loud=false, +subtitle="", +duration=0.80, +subduration=5, +}, +NEWFB={ +file="MARSHAL-NewFB", +suffix="ogg", +loud=false, +subtitle="", +duration=1.35, +}, +OPS={ +file="MARSHAL-Ops", +suffix="ogg", +loud=false, +subtitle="", +duration=0.48, +}, +POINT={ +file="MARSHAL-Point", +suffix="ogg", +loud=false, +subtitle="", +duration=0.33, +}, +RADIOCHECK={ +file="MARSHAL-RadioCheck", +suffix="ogg", +loud=false, +subtitle="Radio check", +duration=1.20, +subduration=5, +}, +RECOVERY={ +file="MARSHAL-Recovery", +suffix="ogg", +loud=false, +subtitle="", +duration=0.70, +subduration=5, +}, +RECOVERYOPSSTOPPED={ +file="MARSHAL-RecoveryOpsStopped", +suffix="ogg", +loud=false, +subtitle="", +duration=1.65, +subduration=5, +}, +RECOVERYPAUSEDNOTICE={ +file="MARSHAL-RecoveryPausedNotice", +suffix="ogg", +loud=false, +subtitle="aircraft recovery paused until further notice", +duration=2.90, +subduration=5, +}, +RECOVERYPAUSEDRESUMED={ +file="MARSHAL-RecoveryPausedResumed", +suffix="ogg", +loud=false, +subtitle="", +duration=3.40, +subduration=5, +}, +REPORTSEEME={ +file="MARSHAL-ReportSeeMe", +suffix="ogg", +loud=false, +subtitle="", +duration=0.95, +}, +RESUMERECOVERY={ +file="MARSHAL-ResumeRecovery", +suffix="ogg", +loud=false, +subtitle="resuming aircraft recovery", +duration=1.75, +subduraction=5, +}, +ROGER={ +file="MARSHAL-Roger", +suffix="ogg", +loud=false, +subtitle="", +duration=0.53, +subduration=5, +}, +SAYNEEDLES={ +file="MARSHAL-SayNeedles", +suffix="ogg", +loud=false, +subtitle="Say needles", +duration=0.90, +subduration=5, +}, +STACKFULL={ +file="MARSHAL-StackFull", +suffix="ogg", +loud=false, +subtitle="Marshal Stack is currently full. Hold outside 10 NM zone and wait for further instructions", +duration=6.35, +subduration=10, +}, +STARTINGRECOVERY={ +file="MARSHAL-StartingRecovery", +suffix="ogg", +loud=false, +subtitle="", +duration=2.65, +subduration=5, +}, +CLICK={ +file="AIRBOSS-RadioClick", +suffix="ogg", +loud=false, +subtitle="", +duration=0.35, +}, +NOISE={ +file="AIRBOSS-Noise", +suffix="ogg", +loud=false, +subtitle="", +duration=3.6, +}, +} +self:SetVoiceOversLSOByRaynor() +self:SetVoiceOversMarshalByRaynor() +end +function AIRBOSS:SetVoiceOver(radiocall,duration,subtitle,subduration,filename,suffix) +radiocall.duration=duration +radiocall.subtitle=subtitle or radiocall.subtitle +radiocall.file=filename +radiocall.suffix=suffix or".ogg" +end +function AIRBOSS:_GetAircraftAoA(playerData) +local hornet=playerData.actype==AIRBOSS.AircraftCarrier.HORNET +local goshawk=playerData.actype==AIRBOSS.AircraftCarrier.T45C +local skyhawk=playerData.actype==AIRBOSS.AircraftCarrier.A4EC +local harrier=playerData.actype==AIRBOSS.AircraftCarrier.AV8B +local tomcat=playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B +local aoa={} +if hornet then +aoa.SLOW=9.8 +aoa.Slow=9.3 +aoa.OnSpeedMax=8.8 +aoa.OnSpeed=8.1 +aoa.OnSpeedMin=7.4 +aoa.Fast=6.9 +aoa.FAST=6.3 +elseif tomcat then +aoa.SLOW=self:_AoAUnit2Deg(playerData,17.0) +aoa.Slow=self:_AoAUnit2Deg(playerData,16.0) +aoa.OnSpeedMax=self:_AoAUnit2Deg(playerData,15.5) +aoa.OnSpeed=self:_AoAUnit2Deg(playerData,15.0) +aoa.OnSpeedMin=self:_AoAUnit2Deg(playerData,14.5) +aoa.Fast=self:_AoAUnit2Deg(playerData,14.0) +aoa.FAST=self:_AoAUnit2Deg(playerData,13.0) +elseif goshawk then +aoa.SLOW=8.00 +aoa.Slow=7.75 +aoa.OnSpeedMax=7.25 +aoa.OnSpeed=7.00 +aoa.OnSpeedMin=6.75 +aoa.Fast=6.25 +aoa.FAST=6.00 +elseif skyhawk then +aoa.SLOW=9.50 +aoa.Slow=9.25 +aoa.OnSpeedMax=9.00 +aoa.OnSpeed=8.75 +aoa.OnSpeedMin=8.50 +aoa.Fast=8.25 +aoa.FAST=8.00 +elseif harrier then +aoa.SLOW=14.0 +aoa.Slow=13.0 +aoa.OnSpeedMax=12.0 +aoa.OnSpeed=11.0 +aoa.OnSpeedMin=10.0 +aoa.Fast=9.0 +aoa.FAST=8.0 +end +return aoa +end +function AIRBOSS:_AoAUnit2Deg(playerData,aoaunits) +local degrees=aoaunits +if playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then +degrees=-10+50/30*aoaunits +degrees=0.918*aoaunits-3.411 +elseif playerData.actype==AIRBOSS.AircraftCarrier.A4EC then +degrees=0.5*aoaunits +end +return degrees +end +function AIRBOSS:_AoADeg2Units(playerData,degrees) +local aoaunits=degrees +if playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then +aoaunits=(degrees+10)*30/50 +aoaunits=1.089*degrees+3.715 +elseif playerData.actype==AIRBOSS.AircraftCarrier.A4EC then +aoaunits=2*degrees +end +return aoaunits +end +function AIRBOSS:_GetAircraftParameters(playerData,step) +step=step or playerData.step +local hornet=playerData.actype==AIRBOSS.AircraftCarrier.HORNET +local skyhawk=playerData.actype==AIRBOSS.AircraftCarrier.A4EC +local tomcat=playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B +local harrier=playerData.actype==AIRBOSS.AircraftCarrier.AV8B +local alt +local aoa +local dist +local speed +local aoaac=self:_GetAircraftAoA(playerData) +if step==AIRBOSS.PatternStep.PLATFORM then +alt=UTILS.FeetToMeters(5000) +speed=UTILS.KnotsToMps(250) +elseif step==AIRBOSS.PatternStep.ARCIN then +if tomcat then +speed=UTILS.KnotsToMps(150) +else +speed=UTILS.KnotsToMps(250) +end +elseif step==AIRBOSS.PatternStep.ARCOUT then +if tomcat then +speed=UTILS.KnotsToMps(150) +else +speed=UTILS.KnotsToMps(250) +end +elseif step==AIRBOSS.PatternStep.DIRTYUP then +alt=UTILS.FeetToMeters(1200) +elseif step==AIRBOSS.PatternStep.BULLSEYE then +alt=UTILS.FeetToMeters(1200) +dist=-UTILS.NMToMeters(3) +aoa=aoaac.OnSpeed +elseif step==AIRBOSS.PatternStep.INITIAL then +if hornet or tomcat or harrier then +alt=UTILS.FeetToMeters(800) +speed=UTILS.KnotsToMps(350) +elseif skyhawk then +alt=UTILS.FeetToMeters(600) +speed=UTILS.KnotsToMps(250) +elseif goshawk then +alt=UTILS.FeetToMeters(800) +speed=UTILS.KnotsToMps(300) +end +elseif step==AIRBOSS.PatternStep.BREAKENTRY then +if hornet or tomcat or harrier then +alt=UTILS.FeetToMeters(800) +speed=UTILS.KnotsToMps(350) +elseif skyhawk then +alt=UTILS.FeetToMeters(600) +speed=UTILS.KnotsToMps(250) +elseif goshawk then +alt=UTILS.FeetToMeters(800) +speed=UTILS.KnotsToMps(300) +end +elseif step==AIRBOSS.PatternStep.EARLYBREAK then +if hornet or tomcat or harrier or goshawk then +alt=UTILS.FeetToMeters(800) +elseif skyhawk then +alt=UTILS.FeetToMeters(600) +end +elseif step==AIRBOSS.PatternStep.LATEBREAK then +if hornet or tomcat or harrier or goshawk then +alt=UTILS.FeetToMeters(800) +elseif skyhawk then +alt=UTILS.FeetToMeters(600) +end +elseif step==AIRBOSS.PatternStep.ABEAM then +if hornet or tomcat or harrier or goshawk then +alt=UTILS.FeetToMeters(600) +elseif skyhawk then +alt=UTILS.FeetToMeters(500) +end +aoa=aoaac.OnSpeed +if harrier then +dist=UTILS.NMToMeters(0.9) +else +dist=UTILS.NMToMeters(1.2) +end +if goshawk then +dist=UTILS.NMToMeters(0.9) +else +dist=UTILS.NMToMeters(1.1) +end +elseif step==AIRBOSS.PatternStep.NINETY then +if hornet or tomcat then +alt=UTILS.FeetToMeters(500) +elseif goshawk then +alt=UTILS.FeetToMeters(450) +elseif skyhawk then +alt=UTILS.FeetToMeters(500) +elseif harrier then +alt=UTILS.FeetToMeters(425) +end +aoa=aoaac.OnSpeed +elseif step==AIRBOSS.PatternStep.WAKE then +if hornet or goshawk then +alt=UTILS.FeetToMeters(370) +elseif tomcat then +alt=UTILS.FeetToMeters(430) +elseif skyhawk then +alt=UTILS.FeetToMeters(370) +end +aoa=aoaac.OnSpeed +elseif step==AIRBOSS.PatternStep.FINAL then +if hornet or goshawk then +alt=UTILS.FeetToMeters(300) +elseif tomcat then +alt=UTILS.FeetToMeters(360) +elseif skyhawk then +alt=UTILS.FeetToMeters(300) +elseif harrier then +alt=UTILS.FeetToMeters(300) +end +aoa=aoaac.OnSpeed +end +return alt,aoa,dist,speed +end +function AIRBOSS:_GetNextMarshalFight() +for _,_flight in pairs(self.Qmarshal)do +local flight=_flight +local stack=flight.flag +local Tmarshal=timer.getAbsTime()-flight.time +local TmarshalMin=2*60 +if flight.ai then +TmarshalMin=3*60 +end +if flight.holding~=nil and Tmarshal>=TmarshalMin then +if flight.case==1 and stack==1 or flight.case>1 then +if flight.ai then +return flight +else +if flight.step~=AIRBOSS.PatternStep.COMMENCING then +return flight +end +end +end +end +end +return nil +end +function AIRBOSS:_CheckQueue() +if self.Debug then +self:_PrintQueue(self.flights,"All Flights") +end +self:_PrintQueue(self.Qmarshal,"Marshal") +self:_PrintQueue(self.Qpattern,"Pattern") +self:_PrintQueue(self.Qwaiting,"Waiting") +self:_PrintQueue(self.Qspinning,"Spinning") +if self.case>1 then +for _,_flight in pairs(self.Qwaiting)do +local flight=_flight +local removed=self:_RemoveFlightFromQueue(self.Qwaiting,flight) +if removed then +local stack=self:_GetFreeStack(flight.ai) +self:T(self.lid..string.format("Moving flight %s onboard %s from Waiting queue to Case %d Marshal stack %d",flight.groupname,flight.onboard,self.case,stack)) +if flight.ai then +self:_MarshalAI(flight,stack) +else +self:_MarshalPlayer(flight,stack) +end +break +end +end +end +if not self:IsRecovering()then +for _,_flight in pairs(self.Qmarshal)do +local flight=_flight +if(flight.case==1 and self.case>1)or(flight.case>1 and self.case==1)then +local removed=self:_RemoveFlightFromQueue(self.Qmarshal,flight) +if removed then +local stack=self:_GetFreeStack(flight.ai) +self:T(self.lid..string.format("Moving flight %s onboard %s from Marshal Case %d ==> %d Marshal stack %d",flight.groupname,flight.onboard,flight.case,self.case,stack)) +if flight.ai then +self:_MarshalAI(flight,stack) +else +self:_MarshalPlayer(flight,stack) +end +break +elseif flight.case~=self.case then +flight.case=self.case +end +end +end +return +end +local _,npattern=self:_GetQueueInfo(self.Qpattern) +local _,nspinning=self:_GetQueueInfo(self.Qspinning) +local marshalflight=self:_GetNextMarshalFight() +if marshalflight and npattern0 then +local patternflight=self.Qpattern[#self.Qpattern] +pcase=patternflight.case +local npunits=self:_GetFlightUnits(patternflight,false) +Tpattern=timer.getAbsTime()-patternflight.time +self:T(self.lid..string.format("Pattern time of last group %s = %d seconds. # of units=%d.",patternflight.groupname,Tpattern,npunits)) +end +local TpatternMin +if pcase==1 then +TpatternMin=2*60*npunits +else +TpatternMin=2*60*npunits +end +if Tpattern>TpatternMin then +self:T(self.lid..string.format("Sending marshal flight %s to pattern.",marshalflight.groupname)) +self:_ClearForLanding(marshalflight) +end +end +end +function AIRBOSS:_ClearForLanding(flight) +if flight.ai then +self:_RemoveFlightFromMarshalQueue(flight,false) +self:_LandAI(flight) +self:_MarshalCallClearedForRecovery(flight.onboard,flight.case) +else +if flight.step~=AIRBOSS.PatternStep.COMMENCING then +self:_MarshalCallClearedForRecovery(flight.onboard,flight.case) +flight.time=timer.getAbsTime() +end +self:_SetPlayerStep(flight,AIRBOSS.PatternStep.COMMENCING,3) +end +end +function AIRBOSS:_SetPlayerStep(playerData,step,delay) +if delay and delay>0 then +self:ScheduleOnce(delay,self._SetPlayerStep,self,playerData,step) +else +if playerData then +playerData.step=step +playerData.warning=nil +self:_StepHint(playerData) +end +end +end +function AIRBOSS:_ScanCarrierZone() +local coord=self:GetCoordinate() +local RCCZ=self.zoneCCA:GetRadius() +self:T(self.lid..string.format("Scanning Carrier Controlled Area. Radius=%.1f NM.",UTILS.MetersToNM(RCCZ))) +local _,_,_,unitscan=coord:ScanObjects(RCCZ,true,false,false) +local insideCCA={} +for _,_unit in pairs(unitscan)do +local unit=_unit +local airborne=unit:IsAir() +local inzone=unit:IsInZone(self.zoneCCA) +local friendly=self:GetCoalition()==unit:GetCoalition() +local carrierac=self:_IsCarrierAircraft(unit) +if airborne and inzone and friendly and carrierac then +local group=unit:GetGroup() +local groupname=group:GetName() +if insideCCA[groupname]==nil then +insideCCA[groupname]=group +end +end +end +for groupname,_group in pairs(insideCCA)do +local group=_group +local knownflight=self:_GetFlightFromGroupInQueue(group,self.flights) +local actype=group:GetTypeName() +if knownflight then +if knownflight.ai and knownflight.flag==-100 and self.handleai then +local putintomarshal=false +local flight=_DATABASE:GetFlightGroup(groupname) +if flight and flight:IsInbound()and flight.destbase:GetName()==self.carrier:GetName()then +if flight.ishelo then +else +putintomarshal=true +end +flight.airboss=self +end +if putintomarshal then +local stack=self:_GetFreeStack(knownflight.ai) +local respawn=self.respawnAI +if stack then +self:_MarshalAI(knownflight,stack,respawn) +else +if not self:_InQueue(self.Qwaiting,knownflight.group)then +self:_WaitAI(knownflight,respawn) +end +end +break +end +end +else +if not self:_IsHuman(group)then +self:_CreateFlightGroup(group) +end +end +end +local remove={} +for _,_flight in pairs(self.flights)do +local flight=_flight +if insideCCA[flight.groupname]==nil then +if flight.ai and not(self:_InQueue(self.Qmarshal,flight.group)or self:_InQueue(self.Qpattern,flight.group))then +table.insert(remove,flight) +end +end +end +for _,flight in pairs(remove)do +self:_RemoveFlightFromQueue(self.flights,flight) +end +end +function AIRBOSS:_WaitPlayer(playerData) +if playerData then +local nwaiting=#self.Qwaiting +self:_MarshalCallStackFull(playerData.onboard,nwaiting) +table.insert(self.Qwaiting,playerData) +playerData.time=timer.getAbsTime() +playerData.step=AIRBOSS.PatternStep.WAITING +playerData.warning=nil +for _,_flight in pairs(playerData.section)do +local flight=_flight +flight.step=AIRBOSS.PatternStep.WAITING +flight.time=timer.getAbsTime() +flight.warning=nil +end +end +end +function AIRBOSS:_MarshalPlayer(playerData,stack) +if playerData then +self:_AddMarshalGroup(playerData,stack) +self:_SetPlayerStep(playerData,AIRBOSS.PatternStep.HOLDING) +playerData.holding=nil +for _,_flight in pairs(playerData.section)do +local flight=_flight +self:_SetPlayerStep(flight,AIRBOSS.PatternStep.HOLDING) +flight.holding=nil +flight.case=playerData.case +flight.flag=stack +self:Marshal(flight) +end +else +self:E(self.lid.."ERROR: Could not add player to Marshal stack! playerData=nil") +end +end +function AIRBOSS:_WaitAI(flight,respawn) +flight.flag=-99 +table.insert(self.Qwaiting,flight) +local group=flight.group +local groupname=flight.groupname +local speedOrbitMps=UTILS.KnotsToMps(274) +local speedOrbitKmh=UTILS.KnotsToKmph(274) +local speedTransit=UTILS.KnotsToKmph(370) +local cv=self:GetCoordinate() +local fc=group:GetCoordinate() +local hdg=self:GetHeading(false) +local hdgto=cv:HeadingTo(fc) +local angels=math.random(6,10) +local altitude=UTILS.FeetToMeters(angels*1000) +local p0=cv:Translate(UTILS.NMToMeters(11),hdgto):Translate(UTILS.NMToMeters(5),hdg):SetAltitude(altitude) +local wp={} +wp[1]=group:GetCoordinate():WaypointAirTurningPoint(nil,speedTransit,{},"Current Position") +local taskorbit=group:TaskOrbit(p0,altitude,speedOrbitMps) +wp[#wp+1]=p0:WaypointAirTurningPoint(nil,speedOrbitKmh,{taskorbit},string.format("Waiting Orbit at Angels %d",angels)) +if self.Debug then +p0:MarkToAll(string.format("Waiting Orbit of flight %s at Angels %s",groupname,angels)) +end +if respawn then +local Template=group:GetTemplate() +Template.route.points=wp +group=group:Respawn(Template,true) +end +group:WayPointInitialize(wp) +group:Route(wp,1) +end +function AIRBOSS:_MarshalAI(flight,nstack,respawn) +self:F2({flight=flight,nstack=nstack,respawn=respawn}) +if flight==nil or flight.group==nil then +self:E(self.lid.."ERROR: flight or flight.group is nil.") +return +end +if flight.group:GetCoordinate()==nil then +self:E(self.lid.."ERROR: cannot get coordinate of flight group.") +return +end +if not self:_InQueue(self.Qmarshal,flight.group)then +self:_AddMarshalGroup(flight,nstack) +end +local case=flight.case +local ostack=flight.flag +local group=flight.group +local groupname=flight.groupname +flight.flag=nstack +local Carrier=self:GetCoordinate() +local hdg=self:GetHeading() +local speedOrbitMps=UTILS.KnotsToMps(274) +local speedOrbitKmh=UTILS.KnotsToKmph(274) +local speedTransit=UTILS.KnotsToKmph(370) +local altitude +local p0 +local p1 +local p2 +altitude,p1,p2=self:_GetMarshalAltitude(nstack,case) +local wp={} +if not flight.holding then +self:T(self.lid..string.format("Guiding AI flight %s to marshal stack %d-->%d.",groupname,ostack,nstack)) +wp[1]=group:GetCoordinate():WaypointAirTurningPoint(nil,speedTransit,{},"Current Position") +local TaskArrivedHolding=flight.group:TaskFunction("AIRBOSS._ReachedHoldingZone",self,flight) +if case==1 then +local pE=Carrier:Translate(UTILS.NMToMeters(7),hdg-30):SetAltitude(altitude) +p0=Carrier:Translate(UTILS.NMToMeters(5),hdg-135):SetAltitude(altitude) +wp[#wp+1]=pE:WaypointAirTurningPoint(nil,speedTransit,{TaskArrivedHolding},"Entering Case I Marshal Pattern") +else +local radial=self:GetRadial(case,false,true) +p0=p2:Translate(UTILS.NMToMeters(5),radial+90):Translate(UTILS.NMToMeters(5),radial,true) +wp[#wp+1]=p0:WaypointAirTurningPoint(nil,speedTransit,{TaskArrivedHolding},"Entering Case II/III Marshal Pattern") +end +else +self:T(self.lid..string.format("Updating AI flight %s at marshal stack %d-->%d.",groupname,ostack,nstack)) +wp[1]=group:GetCoordinate():WaypointAirTurningPoint(nil,speedOrbitKmh,{},"Current Position") +p0=group:GetCoordinate():Translate(UTILS.NMToMeters(0.2),group:GetHeading(),true) +end +local taskorbit=group:TaskOrbit(p1,altitude,speedOrbitMps,p2) +wp[#wp+1]=p0:WaypointAirTurningPoint(nil,speedOrbitKmh,{taskorbit},string.format("Marshal Orbit Stack %d",nstack)) +if self.Debug then +p0:MarkToAll("WP P0 "..groupname) +p1:MarkToAll("RT P1 "..groupname) +p2:MarkToAll("RT P2 "..groupname) +end +if respawn then +local Template=group:GetTemplate() +Template.route.points=wp +flight.group=group:Respawn(Template,true) +end +flight.group:WayPointInitialize(wp) +flight.group:Route(wp,1) +self:Marshal(flight) +end +function AIRBOSS:_RefuelAI(flight) +local wp={} +local CurrentSpeed=flight.group:GetVelocityKMH() +wp[#wp+1]=flight.group:GetCoordinate():WaypointAirTurningPoint(nil,CurrentSpeed,{},"Current position") +local refuelac=false +local actype=flight.group:GetTypeName() +if actype==AIRBOSS.AircraftCarrier.AV8B or +actype==AIRBOSS.AircraftCarrier.F14A or +actype==AIRBOSS.AircraftCarrier.F14B or +actype==AIRBOSS.AircraftCarrier.F14A_AI or +actype==AIRBOSS.AircraftCarrier.HORNET or +actype==AIRBOSS.AircraftCarrier.FA18C or +actype==AIRBOSS.AircraftCarrier.S3B or +actype==AIRBOSS.AircraftCarrier.S3BTANKER then +refuelac=true +end +local text="" +if self.tanker and refuelac then +local tankerpos=self.tanker.tanker:GetCoordinate() +local TaskRefuel=flight.group:TaskRefueling() +local TaskMarshal=flight.group:TaskFunction("AIRBOSS._TaskFunctionMarshalAI",self,flight) +wp[#wp+1]=tankerpos:WaypointAirTurningPoint(nil,CurrentSpeed,{TaskRefuel,TaskMarshal},"Refueling") +self:_MarshalCallGasAtTanker(flight.onboard) +else +local divertfield=self:GetCoordinate():GetClosestAirbase(Airbase.Category.AIRDROME,self:GetCoalition()) +if divertfield==nil then +divertfield=self:GetCoordinate():GetClosestAirbase(Airbase.Category.AIRDROME,0) +end +if divertfield then +local divertcoord=divertfield:GetCoordinate() +wp[#wp+1]=divertcoord:WaypointAirLanding(UTILS.KnotsToKmph(300),divertfield,{},"Divert Field") +self:_MarshalCallGasAtDivert(flight.onboard,divertfield:GetName()) +local Template=flight.group:GetTemplate() +Template.route.points=wp +flight.group=flight.group:Respawn(Template,true) +else +self:E(self.lid..string.format("WARNING: No recovery tanker or divert field available for group %s.",flight.groupname)) +flight.refueling=true +return +end +end +flight.group:WayPointInitialize(wp) +flight.group:Route(wp,1) +flight.refueling=true +end +function AIRBOSS:_LandAI(flight) +self:T(self.lid..string.format("Landing AI flight %s.",flight.groupname)) +local Speed=UTILS.KnotsToKmph(200) +if flight.actype==AIRBOSS.AircraftCarrier.HORNET or flight.actype==AIRBOSS.AircraftCarrier.FA18C then +Speed=UTILS.KnotsToKmph(200) +elseif flight.actype==AIRBOSS.AircraftCarrier.E2D then +Speed=UTILS.KnotsToKmph(150) +elseif flight.actype==AIRBOSS.AircraftCarrier.F14A_AI or flight.actype==AIRBOSS.AircraftCarrier.F14A or flight.actype==AIRBOSS.AircraftCarrier.F14B then +Speed=UTILS.KnotsToKmph(175) +elseif flight.actype==AIRBOSS.AircraftCarrier.S3B or flight.actype==AIRBOSS.AircraftCarrier.S3BTANKER then +Speed=UTILS.KnotsToKmph(140) +end +local Carrier=self:GetCoordinate() +local hdg=self:GetHeading() +local wp={} +local CurrentSpeed=flight.group:GetVelocityKMH() +wp[#wp+1]=flight.group:GetCoordinate():WaypointAirTurningPoint(nil,CurrentSpeed,{},"Current position") +local alt=UTILS.FeetToMeters(800) +wp[#wp+1]=Carrier:Translate(UTILS.NMToMeters(4),hdg-160):SetAltitude(alt):WaypointAirLanding(Speed,self.airbase,nil,"Landing") +flight.group:WayPointInitialize(wp) +flight.group:Route(wp,0) +end +function AIRBOSS:_GetMarshalAltitude(stack,case) +if stack<=0 then +return 0,nil,nil +end +case=case or self.case +local Carrier=self:GetCoordinate() +local angels0 +local Dist +local p1=nil +local p2=nil +local nstack=stack-1 +if case==1 then +angels0=2 +local hdg=self.carrier:GetHeading() +p1=Carrier +p2=Carrier:Translate(UTILS.NMToMeters(1.5),hdg) +if self.carriertype==AIRBOSS.CarrierType.TARAWA then +p1=Carrier:Translate(UTILS.NMToMeters(1.0),hdg+90) +p2=p1:Translate(2.5,hdg) +end +else +angels0=6 +Dist=UTILS.NMToMeters(nstack+angels0+15) +local radial=self:GetRadial(case,false,true) +local l=UTILS.NMToMeters(10) +p1=Carrier:Translate(Dist+l,radial) +p2=Carrier:Translate(Dist,radial) +end +local altitude=UTILS.FeetToMeters((nstack+angels0)*1000) +p1:SetAltitude(altitude,true) +p2:SetAltitude(altitude,true) +return altitude,p1,p2 +end +function AIRBOSS:_GetCharlieTime(flightgroup) +local stack=flightgroup.flag +if stack<=0 then +return nil +end +local Tnow=timer.getAbsTime() +local Tcharlie=0 +local Trecovery=0 +if self.recoverywindow then +Trecovery=math.max(self.recoverywindow.START-Tnow,0) +else +Trecovery=7*60 +end +for _,_flight in pairs(self.Qmarshal)do +local flight=_flight +local mstack=flight.flag +local Tarrive=0 +local Tholding=3*60 +if stack>0 and mstack>0 and mstack<=stack then +if flight.holding==nil then +local holdingzone=self:_GetZoneHolding(flight.case,1):GetCoordinate() +local d0=holdingzone:Get2DDistance(flight.group:GetCoordinate()) +local v0=flight.group:GetVelocityMPS() +Tarrive=d0/v0 +self:T3(self.lid..string.format("Tarrive=%.1f seconds, Clock %s",Tarrive,UTILS.SecondsToClock(Tnow+Tarrive))) +else +if mstack==1 then +local tholding=timer.getAbsTime()-flight.time +Tholding=math.max(3*60-tholding,0) +end +end +local Tmin=math.max(Tarrive,Trecovery) +Tcharlie=math.max(Tmin,Tcharlie)+Tholding +end +end +Tcharlie=Tcharlie+Tnow +local text=string.format("Charlie time for flight %s (%s) %s",flightgroup.onboard,flightgroup.groupname,UTILS.SecondsToClock(Tcharlie)) +MESSAGE:New(text,10,"DEBUG"):ToAllIf(self.Debug) +self:T(self.lid..text) +return Tcharlie +end +function AIRBOSS:_AddMarshalGroup(flight,stack) +flight.flag=stack +flight.case=self.case +table.insert(self.Qmarshal,flight) +local P=UTILS.hPa2inHg(self:GetCoordinate():GetPressure()) +local alt=self:_GetMarshalAltitude(stack,flight.case) +local brc=self:GetBRC() +if self.recoverywindow and self.recoverywindow.WIND then +brc=self:GetBRCintoWind() +end +flight.Tcharlie=self:_GetCharlieTime(flight) +local Ccharlie=UTILS.SecondsToClock(flight.Tcharlie) +self:_MarshalCallArrived(flight.onboard,flight.case,brc,alt,Ccharlie,P) +if self.TACANon and(not flight.ai)and flight.difficulty==AIRBOSS.Difficulty.EASY then +local radial=self:GetRadial(flight.case,true,true,true) +if flight.case==1 then +radial=self:GetBRC() +end +local text=string.format("Select TACAN %03d°, channel %d%s (%s)",radial,self.TACANchannel,self.TACANmode,self.TACANmorse) +self:MessageToPlayer(flight,text,nil,"") +end +end +function AIRBOSS:_CollapseMarshalStack(flight,nopattern) +self:F2({flight=flight,nopattern=nopattern}) +local case=flight.case +local stack=flight.flag +if stack<=0 then +self:E(self.lid..string.format("ERROR: Flight %s is has stack value %d<0. Cannot collapse stack!",flight.groupname,stack)) +return +end +self.Tcollapse=timer.getTime() +for _,_flight in pairs(self.Qmarshal)do +local mflight=_flight +if(case==1 and mflight.case==1)then +local mstack=mflight.flag +if mstack>stack then +local newstack=self:_GetFreeStack(mflight.ai,mflight.case,true) +if newstack and newstack %d.",mflight.groupname,mflight.case,mstack,newstack)) +if mflight.ai then +self:_MarshalAI(mflight,newstack) +else +mflight.flag=newstack +local angels=self:_GetAngels(self:_GetMarshalAltitude(newstack,case)) +if mflight.difficulty~=AIRBOSS.Difficulty.HARD then +local text=string.format("descent to stack at Angels %d.",angels) +self:MessageToPlayer(mflight,text,"MARSHAL") +end +mflight.time=timer.getAbsTime() +for _,_sec in pairs(mflight.section)do +local sec=_sec +sec.flag=newstack +sec.time=timer.getAbsTime() +if sec.difficulty~=AIRBOSS.Difficulty.HARD then +local text=string.format("descent to stack at Angels %d.",angels) +self:MessageToPlayer(sec,text,"MARSHAL") +end +end +end +end +end +end +end +if nopattern then +self:T(self.lid..string.format("Flight %s is leaving stack but not going to pattern.",flight.groupname)) +else +local Tmarshal=UTILS.SecondsToClock(timer.getAbsTime()-flight.time) +self:T(self.lid..string.format("Flight %s is leaving marshal after %s and going pattern.",flight.groupname,Tmarshal)) +self:_AddFlightToPatternQueue(flight) +end +flight.flag=-1 +flight.time=timer.getAbsTime() +end +function AIRBOSS:_GetFreeStack(ai,case,empty) +case=case or self.case +if case==1 then +return self:_GetFreeStack_Old(ai,case,empty) +end +local nmaxstacks=100 +if case==1 then +nmaxstacks=self.Nmaxmarshal +end +local stack={} +for i=1,nmaxstacks do +stack[i]=self.NmaxStack +end +local nmax=1 +for _,_flight in pairs(self.Qmarshal)do +local flight=_flight +if flight.case==case then +local n=flight.flag +if n>nmax then +nmax=n +end +if n>0 then +if flight.ai or flight.case>1 then +stack[n]=0 +else +stack[n]=stack[n]-1 +end +else +self:E(string.format("ERROR: Flight %s in marshal stack has stack value <= 0. Stack value is %d.",flight.groupname,n)) +end +end +end +local nfree=nil +if stack[nmax]==0 then +if case==1 then +if nmax>=nmaxstacks then +nfree=nil +else +nfree=nmax+1 +end +else +nfree=nmax+1 +end +elseif stack[nmax]==self.NmaxStack then +self:E(self.lid..string.format("ERROR: Max occupied stack is empty. Should not happen! Nmax=%d, stack[nmax]=%d",nmax,stack[nmax])) +nfree=nmax +else +if ai or empty or case>1 then +nfree=nmax+1 +else +nfree=nmax +end +end +self:I(self.lid..string.format("Returning free stack %s",tostring(nfree))) +return nfree +end +function AIRBOSS:_GetFreeStack_Old(ai,case,empty) +case=case or self.case +local nmaxstacks=100 +if case==1 then +nmaxstacks=self.Nmaxmarshal +end +local stack={} +for i=1,nmaxstacks do +stack[i]=self.NmaxStack +end +for _,_flight in pairs(self.Qmarshal)do +local flight=_flight +if flight.case==case then +local n=flight.flag +if n>0 then +if flight.ai or flight.case>1 then +stack[n]=0 +else +stack[n]=stack[n]-1 +end +else +self:E(string.format("ERROR: Flight %s in marshal stack has stack value <= 0. Stack value is %d.",flight.groupname,n)) +end +end +end +local nfree=nil +for i=1,nmaxstacks do +self:T2(self.lid..string.format("FF Stack[%d]=%d",i,stack[i])) +if ai or empty or case>1 then +if stack[i]==self.NmaxStack then +nfree=i +return i +end +else +if stack[i]>0 then +nfree=i +return i +end +end +end +return nfree +end +function AIRBOSS:_GetFlightUnits(flight,onground) +local inair=true +if onground==true then +inair=false +end +local function countunits(_group,inair) +local group=_group +local units=group:GetUnits() +local n=0 +if units then +for _,_unit in pairs(units)do +local unit=_unit +if unit and unit:IsAlive()then +if inair then +if unit:InAir()then +self:T2(self.lid..string.format("Unit %s is in AIR",unit:GetName())) +n=n+1 +end +else +n=n+1 +end +end +end +end +return n +end +local nunits=countunits(flight.group,inair) +local nsection=0 +for _,sec in pairs(flight.section)do +local secflight=sec +nsection=nsection+countunits(secflight.group,inair) +end +return nunits+nsection,nunits,nsection +end +function AIRBOSS:_GetQueueInfo(queue,case) +local ngroup=0 +local Nunits=0 +for _,_flight in pairs(queue)do +local flight=_flight +if case then +if(flight.case==case)or(case==23 and(flight.case==2 or flight.case==3))then +local ntot,nunits,nsection=self:_GetFlightUnits(flight) +Nunits=Nunits+ntot +if ntot>0 then +ngroup=ngroup+1 +end +end +else +local ntot,nunits,nsection=self:_GetFlightUnits(flight) +Nunits=Nunits+ntot +if ntot>0 then +ngroup=ngroup+1 +end +end +end +return ngroup,Nunits +end +function AIRBOSS:_PrintQueue(queue,name) +local Nqueue,nqueue=self:_GetQueueInfo(queue) +local text=string.format("%s Queue N=%d (#%d), n=%d:",name,Nqueue,#queue,nqueue) +if#queue==0 then +text=text.." empty." +else +for i,_flight in pairs(queue)do +local flight=_flight +local clock=UTILS.SecondsToClock(timer.getAbsTime()-flight.time) +local case=flight.case +local stack=flight.flag +local fuel=flight.group:GetFuelMin()*100 +local ai=tostring(flight.ai) +local lead=flight.seclead +local Nsec=#flight.section +local actype=self:_GetACNickname(flight.actype) +local onboard=flight.onboard +local holding=tostring(flight.holding) +local _,nunits,nsec=self:_GetFlightUnits(flight,false) +text=text..string.format("\n[%d] %s*%d (%s): lead=%s (%d/%d), onboard=%s, flag=%d, case=%d, time=%s, fuel=%d, ai=%s, holding=%s", +i,flight.groupname,nunits,actype,lead,nsec,Nsec,onboard,stack,case,clock,fuel,ai,holding) +if stack>0 then +local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack,case)) +text=text..string.format(" stackalt=%d ft",alt) +end +for j,_element in pairs(flight.elements)do +local element=_element +text=text..string.format("\n (%d) %s (%s): ai=%s, ballcall=%s, recovered=%s", +j,element.onboard,element.unitname,tostring(element.ai),tostring(element.ballcall),tostring(element.recovered)) +end +end +end +self:T(self.lid..text) +end +function AIRBOSS:_CreateFlightGroup(group) +self:T(self.lid..string.format("Creating new flight for group %s of aircraft type %s.",group:GetName(),group:GetTypeName())) +local flight={} +if not self:_InQueue(self.flights,group)then +local groupname=group:GetName() +local human,playername=self:_IsHuman(group) +flight.group=group +flight.groupname=group:GetName() +flight.nunits=#group:GetUnits() +flight.time=timer.getAbsTime() +flight.dist0=group:GetCoordinate():Get2DDistance(self:GetCoordinate()) +flight.flag=-100 +flight.ai=not human +flight.actype=group:GetTypeName() +flight.onboardnumbers=self:_GetOnboardNumbers(group) +flight.seclead=flight.group:GetUnit(1):GetName() +flight.section={} +flight.ballcall=false +flight.refueling=false +flight.holding=nil +flight.name=flight.group:GetUnit(1):GetName() +flight.case=self.case +local text=string.format("Flight elements of group %s:",flight.groupname) +flight.elements={} +local units=group:GetUnits() +for i,_unit in pairs(units)do +local unit=_unit +local element={} +element.unit=unit +element.unitname=unit:GetName() +element.onboard=flight.onboardnumbers[element.unitname] +element.ballcall=false +element.ai=not self:_IsHumanUnit(unit) +element.recovered=nil +text=text..string.format("\n[%d] %s onboard #%s, AI=%s",i,element.unitname,tostring(element.onboard),tostring(element.ai)) +table.insert(flight.elements,element) +end +self:T(self.lid..text) +if flight.ai then +local onboard=flight.onboardnumbers[flight.seclead] +flight.onboard=onboard +else +flight.onboard=self:_GetOnboardNumberPlayer(group) +end +table.insert(self.flights,flight) +else +self:E(self.lid..string.format("ERROR: Flight group %s already exists in self.flights!",group:GetName())) +return nil +end +return flight +end +function AIRBOSS:_NewPlayer(unitname) +local playerunit,playername=self:_GetPlayerUnitAndName(unitname) +if playerunit and playername then +local group=playerunit:GetGroup() +local playerData +playerData=self:_CreateFlightGroup(group) +if playerData then +playerData.unit=playerunit +playerData.unitname=unitname +playerData.name=playername +playerData.callsign=playerData.unit:GetCallsign() +playerData.client=CLIENT:FindByName(unitname,nil,true) +playerData.seclead=playername +playerData.passes=0 +playerData.messages={} +playerData.lastdebrief=playerData.lastdebrief or{} +playerData.attitudemonitor=false +if playerData.trapon==nil then +playerData.trapon=self.trapsheet +end +playerData.difficulty=playerData.difficulty or self.defaultskill +if playerData.subtitles==nil then +playerData.subtitles=true +end +if playerData.showhints==nil then +if playerData.difficulty==AIRBOSS.Difficulty.HARD then +playerData.showhints=false +else +playerData.showhints=true +end +end +playerData.points={} +playerData=self:_InitPlayer(playerData) +self.players[playername]=playerData +self.playerscores[playername]=self.playerscores[playername]or{} +if self.welcome then +self:MessageToPlayer(playerData,string.format("Welcome, %s %s!",playerData.difficulty,playerData.name),string.format("AIRBOSS %s",self.alias),"",5) +end +end +return playerData +end +return nil +end +function AIRBOSS:_InitPlayer(playerData,step) +self:T(self.lid..string.format("Initializing player data for %s callsign %s.",playerData.name,playerData.callsign)) +playerData.step=step or AIRBOSS.PatternStep.UNDEFINED +playerData.groove={} +playerData.debrief={} +playerData.trapsheet={} +playerData.warning=nil +playerData.holding=nil +playerData.refueling=false +playerData.valid=false +playerData.lig=false +playerData.wop=false +playerData.waveoff=false +playerData.wofd=false +playerData.owo=false +playerData.boltered=false +playerData.landed=false +playerData.Tlso=timer.getTime() +playerData.Tgroove=nil +playerData.TIG0=nil +playerData.wire=nil +playerData.flag=-100 +playerData.debriefschedulerID=nil +if playerData.group:GetName():match("Groove")and playerData.passes==0 then +self:MessageToPlayer(playerData,"Group name contains \"Groove\". Happy groove testing.") +playerData.attitudemonitor=true +playerData.step=AIRBOSS.PatternStep.FINAL +self:_AddFlightToPatternQueue(playerData) +self.dTstatus=0.1 +end +return playerData +end +function AIRBOSS:_GetFlightFromGroupInQueue(group,queue) +if group then +local name=group:GetName() +for i,_flight in pairs(queue)do +local flight=_flight +if flight.groupname==name then +return flight,i +end +end +self:T2(self.lid..string.format("WARNING: Flight group %s could not be found in queue.",name)) +end +self:T2(self.lid..string.format("WARNING: Flight group could not be found in queue. Group is nil!")) +return nil,nil +end +function AIRBOSS:_GetFlightElement(unitname) +local unit=UNIT:FindByName(unitname) +if unit then +local flight=self:_GetFlightFromGroupInQueue(unit:GetGroup(),self.flights) +if flight then +for i,_element in pairs(flight.elements)do +local element=_element +if element.unit:GetName()==unitname then +return element,i,flight +end +end +self:T2(self.lid..string.format("WARNING: Flight element %s could not be found in flight group.",unitname,flight.groupname)) +end +end +return nil,nil,nil +end +function AIRBOSS:_RemoveFlightElement(unitname) +local element,idx,flight=self:_GetFlightElement(unitname) +if idx then +table.remove(flight.elements,idx) +return true +else +self:T("WARNING: Flight element could not be removed from flight group. Index=nil!") +return nil +end +end +function AIRBOSS:_InQueue(queue,group) +local name=group:GetName() +for _,_flight in pairs(queue)do +local flight=_flight +if name==flight.groupname then +return true +end +end +return false +end +function AIRBOSS:_RemoveDeadFlightGroups() +for i=#self.flight,1,-1 do +local flight=self.flights[i] +if not flight.group:IsAlive()then +self:T(string.format("Removing dead flight group %s from ALL flights table.",flight.groupname)) +table.remove(self.flights,i) +end +end +for i=#self.Qmarshal,1,-1 do +local flight=self.Qmarshal[i] +if not flight.group:IsAlive()then +self:T(string.format("Removing dead flight group %s from Marshal Queue table.",flight.groupname)) +table.remove(self.Qmarshal,i) +end +end +for i=#self.Qpattern,1,-1 do +local flight=self.Qpattern[i] +if not flight.group:IsAlive()then +self:T(string.format("Removing dead flight group %s from Pattern Queue table.",flight.groupname)) +table.remove(self.Qpattern,i) +end +end +end +function AIRBOSS:_GetLeadFlight(flight) +local lead=flight +if flight.name~=flight.seclead then +lead=self.players[flight.seclead] +end +return lead +end +function AIRBOSS:_CheckSectionRecovered(flight) +if flight==nil then +return true +end +local lead=self:_GetLeadFlight(flight) +for _,_element in pairs(lead.elements)do +local element=_element +if not element.recovered then +return false +end +end +for _,_section in pairs(lead.section)do +local sectionmember=_section +for _,_element in pairs(sectionmember.elements)do +local element=_element +if not element.recovered then +return false +end +end +end +self:_RemoveFlightFromQueue(self.Qpattern,lead) +if self:_InQueue(self.Qmarshal,lead.group)then +self:E(self.lid..string.format("ERROR: lead flight group %s should not be in marshal queue",lead.groupname)) +self:_RemoveFlightFromMarshalQueue(lead,true) +end +if self:_InQueue(self.Qwaiting,lead.group)then +self:E(self.lid..string.format("ERROR: lead flight group %s should not be in pattern queue",lead.groupname)) +self:_RemoveFlightFromQueue(self.Qwaiting,lead) +end +return true +end +function AIRBOSS:_AddFlightToPatternQueue(flight) +table.insert(self.Qpattern,flight) +flight.flag=-1 +flight.time=timer.getAbsTime() +flight.recovered=false +for _,elem in pairs(flight.elements)do +elem.recoverd=false +end +for _,sec in pairs(flight.section)do +sec.flag=-1 +sec.time=timer.getAbsTime() +for _,elem in pairs(sec.elements)do +elem.recoverd=false +end +end +end +function AIRBOSS:_RecoveredElement(unit) +local element,idx,flight=self:_GetFlightElement(unit:GetName()) +if element then +element.recovered=true +end +return flight +end +function AIRBOSS:_RemoveFlightFromMarshalQueue(flight,nopattern) +local removed,idx=self:_RemoveFlightFromQueue(self.Qmarshal,flight) +if removed then +flight.holding=nil +self:_CollapseMarshalStack(flight,nopattern) +if flight.case==1 and#self.Qwaiting>0 then +local nextflight=self.Qwaiting[1] +local freestack=self:_GetFreeStack(nextflight.ai) +if nextflight.ai then +self:_MarshalAI(nextflight,freestack) +else +self:_MarshalPlayer(nextflight,freestack) +end +self:_RemoveFlightFromQueue(self.Qwaiting,nextflight) +end +end +return removed,idx +end +function AIRBOSS:_RemoveFlightFromQueue(queue,flight) +for i,_flight in pairs(queue)do +local qflight=_flight +if qflight.groupname==flight.groupname then +self:T(self.lid..string.format("Removing flight group %s from queue.",flight.groupname)) +table.remove(queue,i) +return true,i +end +end +return false,nil +end +function AIRBOSS:_RemoveUnitFromFlight(unit) +if unit and unit:IsInstanceOf("UNIT")then +local group=unit:GetGroup() +if group then +local flight=self:_GetFlightFromGroupInQueue(group,self.flights) +if flight then +local removed=self:_RemoveFlightElement(unit:GetName()) +if removed then +local _,nunits=self:_GetFlightUnits(flight,not flight.ai) +local nelements=#flight.elements +self:T(self.lid..string.format("Removed unit %s: nunits=%d, nelements=%d",unit:GetName(),nunits,nelements)) +if nunits==0 or nelements==0 then +self:_RemoveFlight(flight) +end +end +end +end +end +end +function AIRBOSS:_RemoveFlightFromSection(flight) +if flight.name~=flight.seclead then +local lead=self.players[flight.seclead] +if lead then +for i,sec in pairs(lead.section)do +local sectionmember=sec +if sectionmember.name==flight.name then +table.remove(lead.section,i) +break +end +end +end +end +end +function AIRBOSS:_UpdateFlightSection(flight) +if flight.seclead==flight.name then +if#flight.section>=1 then +local newlead=flight.section[1] +newlead.seclead=newlead.name +for i=2,#flight.section do +local member=flight.section[i] +table.insert(newlead.section,member) +member.seclead=newlead.name +end +end +flight.section={} +else +self:_RemoveFlightFromSection(flight) +end +end +function AIRBOSS:_RemoveFlight(flight,completely) +self:F(self.lid..string.format("Removing flight %s, ai=%s completely=%s.",tostring(flight.groupname),tostring(flight.ai),tostring(completely))) +self:_RemoveFlightFromMarshalQueue(flight,true) +self:_RemoveFlightFromQueue(self.Qpattern,flight) +self:_RemoveFlightFromQueue(self.Qwaiting,flight) +self:_RemoveFlightFromQueue(self.Qspinning,flight) +if flight.ai then +self:_RemoveFlightFromQueue(self.flights,flight) +else +local grades=self.playerscores[flight.name] +if grades and#grades>0 then +while#grades>0 and grades[#grades].finalscore==nil do +table.remove(grades,#grades) +end +end +if completely then +self:_UpdateFlightSection(flight) +self:_RemoveFlightFromQueue(self.flights,flight) +local playerdata=self.players[flight.name] +if playerdata then +self:I(self.lid..string.format("Removing player %s completely.",flight.name)) +self.players[flight.name]=nil +end +flight=nil +else +self:_SetPlayerStep(flight,AIRBOSS.PatternStep.UNDEFINED) +for _,sectionmember in pairs(flight.section)do +self:_SetPlayerStep(sectionmember,AIRBOSS.PatternStep.UNDEFINED) +self:_RemoveFlightFromQueue(self.Qspinning,sectionmember) +end +self:_RemoveFlightFromSection(flight) +end +end +end +function AIRBOSS:_CheckPlayerStatus() +for _playerName,_playerData in pairs(self.players)do +local playerData=_playerData +if playerData then +local unit=playerData.unit +if unit and unit:IsAlive()then +if unit:IsInZone(self.zoneCCA)then +if playerData.attitudemonitor then +self:_AttitudeMonitor(playerData) +end +self:_CheckPlayerPatternDistance(playerData) +self:_CheckFoulDeck(playerData) +if playerData.step==AIRBOSS.PatternStep.UNDEFINED then +elseif playerData.step==AIRBOSS.PatternStep.REFUELING then +elseif playerData.step==AIRBOSS.PatternStep.SPINNING then +self:_Spinning(playerData) +elseif playerData.step==AIRBOSS.PatternStep.HOLDING then +self:_Holding(playerData) +elseif playerData.step==AIRBOSS.PatternStep.WAITING then +self:_Waiting(playerData) +elseif playerData.step==AIRBOSS.PatternStep.COMMENCING then +self:_Commencing(playerData,true) +elseif playerData.step==AIRBOSS.PatternStep.BOLTER then +self:_BolterPattern(playerData) +elseif playerData.step==AIRBOSS.PatternStep.PLATFORM then +self:_Platform(playerData) +elseif playerData.step==AIRBOSS.PatternStep.ARCIN then +self:_ArcInTurn(playerData) +elseif playerData.step==AIRBOSS.PatternStep.ARCOUT then +self:_ArcOutTurn(playerData) +elseif playerData.step==AIRBOSS.PatternStep.DIRTYUP then +self:_DirtyUp(playerData) +elseif playerData.step==AIRBOSS.PatternStep.BULLSEYE then +self:_Bullseye(playerData) +elseif playerData.step==AIRBOSS.PatternStep.INITIAL then +self:_Initial(playerData) +elseif playerData.step==AIRBOSS.PatternStep.BREAKENTRY then +self:_BreakEntry(playerData) +elseif playerData.step==AIRBOSS.PatternStep.EARLYBREAK then +self:_Break(playerData,AIRBOSS.PatternStep.EARLYBREAK) +elseif playerData.step==AIRBOSS.PatternStep.LATEBREAK then +self:_Break(playerData,AIRBOSS.PatternStep.LATEBREAK) +elseif playerData.step==AIRBOSS.PatternStep.ABEAM then +self:_Abeam(playerData) +elseif playerData.step==AIRBOSS.PatternStep.NINETY then +self:_CheckForLongDownwind(playerData) +self:_Ninety(playerData) +elseif playerData.step==AIRBOSS.PatternStep.WAKE then +self:_Wake(playerData) +elseif playerData.step==AIRBOSS.PatternStep.EMERGENCY then +self:_Final(playerData,true) +elseif playerData.step==AIRBOSS.PatternStep.FINAL then +self:_Final(playerData) +elseif playerData.step==AIRBOSS.PatternStep.GROOVE_XX or +playerData.step==AIRBOSS.PatternStep.GROOVE_IM or +playerData.step==AIRBOSS.PatternStep.GROOVE_IC or +playerData.step==AIRBOSS.PatternStep.GROOVE_AR or +playerData.step==AIRBOSS.PatternStep.GROOVE_AL or +playerData.step==AIRBOSS.PatternStep.GROOVE_LC or +playerData.step==AIRBOSS.PatternStep.GROOVE_IW then +self:_Groove(playerData) +elseif playerData.step==AIRBOSS.PatternStep.DEBRIEF then +playerData.debriefschedulerID=self:ScheduleOnce(5,self._Debrief,self,playerData) +playerData.step=AIRBOSS.PatternStep.UNDEFINED +else +self:E(self.lid..string.format("ERROR: Unknown player step %s. Please report!",tostring(playerData.step))) +end +self:_CheckMissedStepOnEntry(playerData) +else +self:T2(self.lid.."WARNING: Player unit not inside the CCA!") +end +else +self:T(self.lid.."WARNING: Player unit is not alive!") +end +end +end +end +function AIRBOSS:_CheckMissedStepOnEntry(playerData) +local rightcase=playerData.case>1 +local rightqueue=self:_InQueue(self.Qpattern,playerData.group) +local rightflag=playerData.flag~=-42 +local step=playerData.step +local missedstep=step==AIRBOSS.PatternStep.PLATFORM or step==AIRBOSS.PatternStep.ARCIN or step==AIRBOSS.PatternStep.ARCOUT or step==AIRBOSS.PatternStep.DIRTYUP +if rightcase and rightqueue and rightflag then +local zone=nil +if playerData.case==2 and missedstep then +zone=self:_GetZoneInitial(playerData.case) +elseif playerData.case==3 and missedstep then +zone=self:_GetZoneBullseye(playerData.case) +end +if zone then +local inzone=playerData.unit:IsInZone(zone) +local relheading=self:_GetRelativeHeading(playerData.unit,false) +if inzone and math.abs(relheading)<60 then +local text=string.format("you missed an important step in the pattern!\nYour next step would have been %s.",playerData.step) +self:MessageToPlayer(playerData,text,"AIRBOSS",nil,5) +if playerData.case==2 then +playerData.step=AIRBOSS.PatternStep.INITIAL +elseif playerData.case==3 then +playerData.step=AIRBOSS.PatternStep.BULLSEYE +end +playerData.flag=-42 +end +end +end +end +function AIRBOSS:_SetTimeInGroove(playerData) +if playerData.TIG0 then +playerData.Tgroove=timer.getTime()-playerData.TIG0 +else +playerData.Tgroove=999 +end +end +function AIRBOSS:_GetTimeInGroove(playerData) +local Tgroove=999 +if playerData.TIG0 then +Tgroove=timer.getTime()-playerData.TIG0 +end +return Tgroove +end +function AIRBOSS:OnEventBirth(EventData) +self:F3({eventbirth=EventData}) +if EventData==nil then +self:E(self.lid.."ERROR: EventData=nil in event BIRTH!") +self:E(EventData) +return +end +if EventData.IniUnit==nil then +self:E(self.lid.."ERROR: EventData.IniUnit=nil in event BIRTH!") +self:E(EventData) +return +end +local _unitName=EventData.IniUnitName +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +self:T(self.lid.."BIRTH: unit = "..tostring(EventData.IniUnitName)) +self:T(self.lid.."BIRTH: group = "..tostring(EventData.IniGroupName)) +self:T(self.lid.."BIRTH: player = "..tostring(_playername)) +if _unit and _playername then +local _uid=_unit:GetID() +local _group=_unit:GetGroup() +local _callsign=_unit:GetCallsign() +local text=string.format("Pilot %s, callsign %s entered unit %s of group %s.",_playername,_callsign,_unitName,_group:GetName()) +self:T(self.lid..text) +MESSAGE:New(text,5):ToAllIf(self.Debug) +local rightaircraft=self:_IsCarrierAircraft(_unit) +if rightaircraft==false then +local text=string.format("Player aircraft type %s not supported by AIRBOSS class.",_unit:GetTypeName()) +MESSAGE:New(text,30):ToAllIf(self.Debug) +self:T2(self.lid..text) +return +end +if self:GetCoalition()~=_unit:GetCoalition()then +local text=string.format("Player entered aircraft of other coalition.") +MESSAGE:New(text,30):ToAllIf(self.Debug) +self:T(self.lid..text) +return +end +self:_AddF10Commands(_unitName) +self:ScheduleOnce(1,self._NewPlayer,self,_unitName) +end +end +function AIRBOSS:OnEventLand(EventData) +self:F3({eventland=EventData}) +if EventData==nil then +self:E(self.lid.."ERROR: EventData=nil in event LAND!") +self:E(EventData) +return +end +if EventData.IniUnit==nil then +self:E(self.lid.."ERROR: EventData.IniUnit=nil in event LAND!") +self:E(EventData) +return +end +local _unitName=EventData.IniUnitName +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +self:T(self.lid.."LAND: unit = "..tostring(EventData.IniUnitName)) +self:T(self.lid.."LAND: group = "..tostring(EventData.IniGroupName)) +self:T(self.lid.."LAND: player = "..tostring(_playername)) +local airbase=EventData.Place +if airbase==nil then +return +end +local airbasename=tostring(airbase:GetName()) +if airbasename==self.airbase:GetName()then +local stern=self:_GetSternCoord() +local zoneCarrier=self:_GetZoneCarrierBox() +if _unit and _playername then +local _uid=_unit:GetID() +local _group=_unit:GetGroup() +local _callsign=_unit:GetCallsign() +local text=string.format("Player %s, callsign %s unit %s (ID=%d) of group %s landed at airbase %s",_playername,_callsign,_unitName,_uid,_group:GetName(),airbasename) +self:T(self.lid..text) +MESSAGE:New(text,5,"DEBUG"):ToAllIf(self.Debug) +local playerData=self.players[_playername] +if playerData==nil then +self:E(self.lid..string.format("ERROR: playerData nil in landing event. unit=%s player=%s",tostring(_unitName),tostring(_playername))) +return +end +if _unit:IsInZone(zoneCarrier)then +if not playerData.valid then +local text=string.format("you missed at least one important step in the pattern!\nYour next step would have been %s.\nThis pass is INVALID.",playerData.step) +self:MessageToPlayer(playerData,text,"AIRBOSS",nil,30,true,5) +self:_RemoveFlightFromMarshalQueue(playerData,true) +self:_RemoveFlightFromQueue(self.Qpattern,playerData) +self:_RemoveFlightFromQueue(self.Qwaiting,playerData) +self:_RemoveFlightFromQueue(self.Qspinning,playerData) +self:_InitPlayer(playerData) +return +end +if playerData.landed then +self:E(self.lid..string.format("Player %s just landed a second time.",_playername)) +else +playerData.landed=true +playerData.attitudemonitor=false +local coord=playerData.unit:GetCoordinate() +local X,Z,rho,phi=self:_GetDistances(_unit) +local dist=coord:Get2DDistance(stern) +if self.Debug and false then +local lp=coord:MarkToAll("Landing coord.") +coord:SmokeGreen() +end +self:_SetTimeInGroove(playerData) +local text=string.format("Player %s AC type %s landed at dist=%.1f m. Tgroove=%.1f sec.",playerData.name,playerData.actype,dist,self:_GetTimeInGroove(playerData)) +text=text..string.format(" X=%.1f m, Z=%.1f m, rho=%.1f m.",X,Z,rho) +self:T(self.lid..text) +if self.carriertype==AIRBOSS.CarrierType.TARAWA then +self:RadioTransmission(self.LSORadio,self.LSOCall.IDLE,false,1,nil,true) +self:_SetPlayerStep(playerData,AIRBOSS.PatternStep.DEBRIEF) +else +self:_SetPlayerStep(playerData,AIRBOSS.PatternStep.UNDEFINED) +self:ScheduleOnce(1,self._Trapped,self,playerData) +end +end +else +if playerData then +self:E(self.lid..string.format("Player %s did not land in carrier box zone. Maybe in the water near the carrier?",playerData.name)) +end +end +else +if self.carriertype~=AIRBOSS.CarrierType.TARAWA then +local coord=EventData.IniUnit:GetCoordinate() +local dist=coord:Get2DDistance(self:GetCoordinate()) +local wire=self:_GetWire(coord,0) +local _type=EventData.IniUnit:GetTypeName() +local text=string.format("AI unit %s of type %s landed at dist=%.1f m. Trapped wire=%d.",_unitName,_type,dist,wire) +self:T(self.lid..text) +end +local flight=self:_RecoveredElement(EventData.IniUnit) +self:_CheckSectionRecovered(flight) +end +end +end +function AIRBOSS:OnEventEngineShutdown(EventData) +self:F3({eventengineshutdown=EventData}) +if EventData==nil then +self:E(self.lid.."ERROR: EventData=nil in event ENGINESHUTDOWN!") +self:E(EventData) +return +end +if EventData.IniUnit==nil then +self:E(self.lid.."ERROR: EventData.IniUnit=nil in event ENGINESHUTDOWN!") +self:E(EventData) +return +end +local _unitName=EventData.IniUnitName +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +self:T3(self.lid.."ENGINESHUTDOWN: unit = "..tostring(EventData.IniUnitName)) +self:T3(self.lid.."ENGINESHUTDOWN: group = "..tostring(EventData.IniGroupName)) +self:T3(self.lid.."ENGINESHUTDOWN: player = "..tostring(_playername)) +if _unit and _playername then +self:T(self.lid..string.format("Player %s shut down its engines!",_playername)) +else +self:T(self.lid..string.format("AI unit %s shut down its engines!",_unitName)) +local flight=self:_GetFlightFromGroupInQueue(EventData.IniGroup,self.flights) +if flight and flight.ai then +local recovered=self:_CheckSectionRecovered(flight) +if recovered then +self:T(self.lid..string.format("AI group %s completely recovered. Despawning group after engine shutdown event as requested in 5 seconds.",tostring(EventData.IniGroupName))) +self:_RemoveFlight(flight) +local istanker=self.tanker and self.tanker.tanker:GetName()==EventData.IniGroupName +local isawacs=self.awacs and self.awacs.tanker:GetName()==EventData.IniGroupName +if self.despawnshutdown and not(istanker or isawacs)then +EventData.IniGroup:Destroy(nil,5) +end +end +end +end +end +function AIRBOSS:OnEventTakeoff(EventData) +self:F3({eventtakeoff=EventData}) +if EventData==nil then +self:E(self.lid.."ERROR: EventData=nil in event TAKEOFF!") +self:E(EventData) +return +end +if EventData.IniUnit==nil then +self:E(self.lid.."ERROR: EventData.IniUnit=nil in event TAKEOFF!") +self:E(EventData) +return +end +local _unitName=EventData.IniUnitName +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +self:T3(self.lid.."TAKEOFF: unit = "..tostring(EventData.IniUnitName)) +self:T3(self.lid.."TAKEOFF: group = "..tostring(EventData.IniGroupName)) +self:T3(self.lid.."TAKEOFF: player = "..tostring(_playername)) +local airbase=EventData.Place +local airbasename="unknown" +if airbase then +airbasename=airbase:GetName() +end +if airbasename==self.airbase:GetName()then +if _unit and _playername then +self:T(self.lid..string.format("Player %s took off at %s!",_playername,airbasename)) +else +self:T2(self.lid..string.format("AI unit %s took off at %s!",_unitName,airbasename)) +local flight=self:_GetFlightFromGroupInQueue(EventData.IniGroup,self.flights) +if flight then +for _,elem in pairs(flight.elements)do +local element=elem +element.ballcall=false +element.recovered=nil +end +end +end +end +end +function AIRBOSS:OnEventCrash(EventData) +self:F3({eventcrash=EventData}) +if EventData==nil then +self:E(self.lid.."ERROR: EventData=nil in event CRASH!") +self:E(EventData) +return +end +if EventData.IniUnit==nil then +self:E(self.lid.."ERROR: EventData.IniUnit=nil in event CRASH!") +self:E(EventData) +return +end +local _unitName=EventData.IniUnitName +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +self:T3(self.lid.."CRASH: unit = "..tostring(EventData.IniUnitName)) +self:T3(self.lid.."CRASH: group = "..tostring(EventData.IniGroupName)) +self:T3(self.lid.."CARSH: player = "..tostring(_playername)) +if _unit and _playername then +self:T(self.lid..string.format("Player %s crashed!",_playername)) +local flight=self.players[_playername] +if flight then +self:_RemoveFlight(flight,true) +end +else +self:T2(self.lid..string.format("AI unit %s crashed!",EventData.IniUnitName)) +self:_RemoveUnitFromFlight(EventData.IniUnit) +end +end +function AIRBOSS:OnEventEjection(EventData) +self:F3({eventland=EventData}) +if EventData==nil then +self:E(self.lid.."ERROR: EventData=nil in event EJECTION!") +self:E(EventData) +return +end +if EventData.IniUnit==nil then +self:E(self.lid.."ERROR: EventData.IniUnit=nil in event EJECTION!") +self:E(EventData) +return +end +local _unitName=EventData.IniUnitName +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +self:T3(self.lid.."EJECT: unit = "..tostring(EventData.IniUnitName)) +self:T3(self.lid.."EJECT: group = "..tostring(EventData.IniGroupName)) +self:T3(self.lid.."EJECT: player = "..tostring(_playername)) +if _unit and _playername then +self:T(self.lid..string.format("Player %s ejected!",_playername)) +local flight=self.players[_playername] +if flight then +self:_RemoveFlight(flight,true) +end +else +self:T(self.lid..string.format("AI unit %s ejected!",EventData.IniUnitName)) +self:_RemoveUnitFromFlight(EventData.IniUnit) +local flight=self:_GetFlightFromGroupInQueue(EventData.IniGroup,self.flights) +self:_CheckSectionRecovered(flight) +end +end +function AIRBOSS:OnEventRemoveUnit(EventData) +self:F3({eventland=EventData}) +if EventData==nil then +self:E(self.lid.."ERROR: EventData=nil in event REMOVEUNIT!") +self:E(EventData) +return +end +if EventData.IniUnit==nil then +self:E(self.lid.."ERROR: EventData.IniUnit=nil in event REMOVEUNIT!") +self:E(EventData) +return +end +local _unitName=EventData.IniUnitName +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +self:T3(self.lid.."EJECT: unit = "..tostring(EventData.IniUnitName)) +self:T3(self.lid.."EJECT: group = "..tostring(EventData.IniGroupName)) +self:T3(self.lid.."EJECT: player = "..tostring(_playername)) +if _unit and _playername then +self:T(self.lid..string.format("Player %s removed!",_playername)) +local flight=self.players[_playername] +if flight then +self:_RemoveFlight(flight,true) +end +else +self:T(self.lid..string.format("AI unit %s removed!",EventData.IniUnitName)) +self:_RemoveUnitFromFlight(EventData.IniUnit) +local flight=self:_GetFlightFromGroupInQueue(EventData.IniGroup,self.flights) +self:_CheckSectionRecovered(flight) +end +end +function AIRBOSS:_PlayerLeft(EventData) +self:F3({eventleave=EventData}) +if EventData==nil then +self:E(self.lid.."ERROR: EventData=nil in event PLAYERLEFTUNIT!") +self:E(EventData) +return +end +if EventData.IniUnit==nil then +self:E(self.lid.."ERROR: EventData.IniUnit=nil in event PLAYERLEFTUNIT!") +self:E(EventData) +return +end +local _unitName=EventData.IniUnitName +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +self:T3(self.lid.."PLAYERLEAVEUNIT: unit = "..tostring(EventData.IniUnitName)) +self:T3(self.lid.."PLAYERLEAVEUNIT: group = "..tostring(EventData.IniGroupName)) +self:T3(self.lid.."PLAYERLEAVEUNIT: player = "..tostring(_playername)) +if _unit and _playername then +self:T(self.lid..string.format("Player %s left unit %s!",_playername,_unitName)) +local flight=self.players[_playername] +if flight then +self:_RemoveFlight(flight,true) +end +end +end +function AIRBOSS:OnEventMissionEnd(EventData) +self:T3(self.lid.."Mission Ended") +end +function AIRBOSS:_Spinning(playerData) +local SpinIt={} +SpinIt.name="Spinning" +SpinIt.Xmin=-UTILS.NMToMeters(6) +SpinIt.Xmax=UTILS.NMToMeters(5) +SpinIt.Zmin=-UTILS.NMToMeters(6) +SpinIt.Zmax=UTILS.NMToMeters(2) +SpinIt.LimitXmin=-100 +SpinIt.LimitXmax=nil +SpinIt.LimitZmin=-UTILS.NMToMeters(1) +SpinIt.LimitZmax=nil +local X,Z,rho,phi=self:_GetDistances(playerData.unit) +if self:_CheckLimits(X,Z,SpinIt)then +self:_SetPlayerStep(playerData,AIRBOSS.PatternStep.INITIAL) +self:_RemoveFlightFromQueue(self.Qspinning,playerData) +end +end +function AIRBOSS:_Waiting(playerData) +local radius=UTILS.NMToMeters(10) +local zone=ZONE_RADIUS:New("Carrier 10 NM Zone",self.carrier:GetVec2(),radius) +local inzone=playerData.unit:IsInZone(zone) +local Twaiting=timer.getAbsTime()-playerData.time +if inzone and Twaiting>3*60 and not playerData.warning then +local text=string.format("You are supposed to wait outside the 10 NM zone.") +self:MessageToPlayer(playerData,text,"AIRBOSS") +playerData.warning=true +end +if inzone==false and playerData.warning==true then +playerData.warning=nil +end +end +function AIRBOSS:_Holding(playerData) +local unit=playerData.unit +local stack=playerData.flag +if stack<=0 then +local text=string.format("ERROR: player %s in step %s is holding but has stack=%s (<=0)",playerData.name,playerData.step,tostring(stack)) +self:E(self.lid..text) +end +local patternalt=self:_GetMarshalAltitude(stack,playerData.case) +local playeralt=unit:GetAltitude() +local zoneHolding=self:_GetZoneHolding(playerData.case,stack) +if zoneHolding==nil then +self:E(self.lid.."ERROR: zoneHolding is nil!") +self:E({playerData=playerData}) +return +end +local inholdingzone=unit:IsInZone(zoneHolding) +local altdiff=playeralt-patternalt +local altgood=UTILS.FeetToMeters(500) +if playerData.difficulty==AIRBOSS.Difficulty.HARD then +altgood=UTILS.FeetToMeters(200) +elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then +altgood=UTILS.FeetToMeters(350) +elseif playerData.difficulty==AIRBOSS.Difficulty.EASY then +altgood=UTILS.FeetToMeters(500) +end +local altback=altgood*0.5 +local justcollapsed=false +if self.Tcollapse then +local dT=timer.getTime()-self.Tcollapse +if dT<=90 then +justcollapsed=true +end +end +local goodalt=math.abs(altdiff)altgood then +if not playerData.warning then +text=text..string.format("You left your assigned altitude. Descent to angels %d.",angels) +playerData.warning=true +end +elseif altdiff<-altgood then +if not playerData.warning then +text=text..string.format("You left your assigned altitude. Climb to angels %d.",angels) +playerData.warning=true +end +end +end +if playerData.warning and math.abs(altdiff)<=altback then +text=text..string.format("Altitude is looking good again.") +playerData.warning=nil +end +elseif playerData.holding==false then +if inholdingzone then +text=text..string.format("You are back in the holding zone. Now stay there!") +playerData.holding=true +else +self:T3("Player still outside the holding zone. What are you doing man?!") +end +elseif playerData.holding==nil then +if inholdingzone then +playerData.holding=true +text=text..string.format("You arrived at the holding zone.") +if goodalt then +text=text..string.format(" Altitude is good.") +else +if altdiff<0 then +text=text..string.format(" But you're too low.") +else +text=text..string.format(" But you're too high.") +end +text=text..string.format("\nCurrently assigned altitude is %d ft.",UTILS.MetersToFeet(patternalt)) +playerData.warning=true +end +else +self:T3("Waiting for player to arrive in the holding zone.") +end +end +if playerData.showhints then +self:MessageToPlayer(playerData,text,"MARSHAL") +end +end +function AIRBOSS:_Commencing(playerData,zonecheck) +if zonecheck then +local zoneCommence=self:_GetZoneCommence(playerData.case,playerData.flag) +local inzone=playerData.unit:IsInZone(zoneCommence) +if not inzone then +if timer.getAbsTime()-playerData.time>180 then +self:_MarshalCallClearedForRecovery(playerData.onboard,playerData.case) +playerData.time=timer.getAbsTime() +end +return +end +end +self:_RemoveFlightFromMarshalQueue(playerData) +self:_InitPlayer(playerData) +if playerData.difficulty~=AIRBOSS.Difficulty.HARD then +local text="" +if playerData.case==1 then +text=text.."Proceed to initial." +else +text=text.."Descent to platform." +if playerData.difficulty==AIRBOSS.Difficulty.EASY and playerData.showhints then +text=text.." VSI 4000 ft/min until you reach 5000 ft." +end +end +self:MessageToPlayer(playerData,text,"MARSHAL") +end +local nextstep +if playerData.case==1 then +nextstep=AIRBOSS.PatternStep.INITIAL +else +nextstep=AIRBOSS.PatternStep.PLATFORM +end +self:_SetPlayerStep(playerData,nextstep) +for i,_flight in pairs(playerData.section)do +local flight=_flight +self:_Commencing(flight,false) +end +end +function AIRBOSS:_Initial(playerData) +local inzone=playerData.unit:IsInZone(self:_GetZoneInitial(playerData.case)) +local relheading=self:_GetRelativeHeading(playerData.unit,false) +local altitude=playerData.unit:GetAltitude() +if inzone and math.abs(relheading)<60 and altitude<=self.initialmaxalt then +if playerData.showhints then +local hint=string.format("Initial") +if playerData.difficulty==AIRBOSS.Difficulty.EASY and playerData.actype~=AIRBOSS.AircraftCarrier.AV8B then +if playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then +hint=hint.." - Hook down, SAS on, Wing Sweep 68°!" +else +hint=hint.." - Hook down!" +end +end +self:MessageToPlayer(playerData,hint,"MARSHAL") +end +self:_SetPlayerStep(playerData,AIRBOSS.PatternStep.BREAKENTRY) +return true +end +return false +end +function AIRBOSS:_CheckCorridor(playerData) +local validzone=self:_GetZoneCorridor(playerData.case) +local invalid=playerData.unit:IsNotInZone(validzone) +if invalid and(not playerData.warning)then +self:MessageToPlayer(playerData,"you left the approach corridor!","AIRBOSS") +playerData.warning=true +end +if(not invalid)and playerData.warning then +self:MessageToPlayer(playerData,"you're back in the approach corridor.","AIRBOSS") +playerData.warning=false +end +end +function AIRBOSS:_Platform(playerData) +self:_CheckCorridor(playerData) +local inzone=playerData.unit:IsInZone(self:_GetZonePlatform(playerData.case)) +if inzone then +self:_PlayerHint(playerData) +local nextstep +if math.abs(self.holdingoffset)>0 and playerData.case>1 then +nextstep=AIRBOSS.PatternStep.ARCIN +else +if playerData.case==2 then +nextstep=AIRBOSS.PatternStep.INITIAL +elseif playerData.case==3 then +nextstep=AIRBOSS.PatternStep.DIRTYUP +end +end +self:_SetPlayerStep(playerData,nextstep) +end +end +function AIRBOSS:_ArcInTurn(playerData) +self:_CheckCorridor(playerData) +local inzone=playerData.unit:IsInZone(self:_GetZoneArcIn(playerData.case)) +if inzone then +self:_PlayerHint(playerData) +self:_SetPlayerStep(playerData,AIRBOSS.PatternStep.ARCOUT) +end +end +function AIRBOSS:_ArcOutTurn(playerData) +self:_CheckCorridor(playerData) +local inzone=playerData.unit:IsInZone(self:_GetZoneArcOut(playerData.case)) +if inzone then +self:_PlayerHint(playerData) +local nextstep +if playerData.case==3 then +nextstep=AIRBOSS.PatternStep.DIRTYUP +else +nextstep=AIRBOSS.PatternStep.INITIAL +end +self:_SetPlayerStep(playerData,nextstep) +end +end +function AIRBOSS:_DirtyUp(playerData) +self:_CheckCorridor(playerData) +local inzone=playerData.unit:IsInZone(self:_GetZoneDirtyUp(playerData.case)) +if inzone then +self:_PlayerHint(playerData) +if playerData.actype==AIRBOSS.AircraftCarrier.HORNET or playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then +local callsay=self:_NewRadioCall(self.MarshalCall.SAYNEEDLES,nil,nil,5,playerData.onboard) +local callfly=self:_NewRadioCall(self.MarshalCall.FLYNEEDLES,nil,nil,5,playerData.onboard) +self:RadioTransmission(self.MarshalRadio,callsay,false,55,nil,true) +self:RadioTransmission(self.MarshalRadio,callfly,false,60,nil,true) +end +self:_SetPlayerStep(playerData,AIRBOSS.PatternStep.BULLSEYE) +end +end +function AIRBOSS:_Bullseye(playerData) +self:_CheckCorridor(playerData) +local inzone=playerData.unit:IsInZone(self:_GetZoneBullseye(playerData.case)) +local relheading=self:_GetRelativeHeading(playerData.unit,true) +if inzone and math.abs(relheading)<60 then +self:_PlayerHint(playerData) +if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then +self:RadioTransmission(self.LSORadio,self.LSOCall.EXPECTSPOT75,nil,nil,nil,true) +end +self:_SetPlayerStep(playerData,AIRBOSS.PatternStep.GROOVE_XX) +end +end +function AIRBOSS:_BolterPattern(playerData) +local X,Z,rho,phi=self:_GetDistances(playerData.unit) +local Bolter={} +Bolter.name="Bolter Pattern" +Bolter.Xmin=-UTILS.NMToMeters(5) +Bolter.Xmax=UTILS.NMToMeters(3) +Bolter.Zmin=-UTILS.NMToMeters(5) +Bolter.Zmax=UTILS.NMToMeters(1) +Bolter.LimitXmin=100 +Bolter.LimitXmax=nil +Bolter.LimitZmin=nil +Bolter.LimitZmax=nil +if self:_CheckLimits(X,Z,Bolter)then +local nextstep +if playerData.case<3 then +nextstep=AIRBOSS.PatternStep.ABEAM +else +nextstep=AIRBOSS.PatternStep.BULLSEYE +end +self:_SetPlayerStep(playerData,nextstep) +end +end +function AIRBOSS:_BreakEntry(playerData) +local X,Z=self:_GetDistances(playerData.unit) +if self:_CheckAbort(X,Z,self.BreakEntry)then +self:_AbortPattern(playerData,X,Z,self.BreakEntry,true) +return +end +if self:_CheckLimits(X,Z,self.BreakEntry)then +self:_PlayerHint(playerData) +self:_SetPlayerStep(playerData,AIRBOSS.PatternStep.EARLYBREAK) +end +end +function AIRBOSS:_Break(playerData,part) +local X,Z=self:_GetDistances(playerData.unit) +local breakpoint=self.BreakEarly +if part==AIRBOSS.PatternStep.LATEBREAK then +breakpoint=self.BreakLate +end +if self:_CheckAbort(X,Z,breakpoint)then +self:_AbortPattern(playerData,X,Z,breakpoint,true) +return +end +local tooclose=false +if part==AIRBOSS.PatternStep.LATEBREAK then +local close=0.8 +if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then +close=0.5 +end +if X<0 and Z90 and self:_CheckLimits(X,Z,self.Wake)then +self:MessageToPlayer(playerData,"you are already at the wake and have not passed the 90. Turn faster next time!","LSO") +self:RadioTransmission(self.LSORadio,self.LSOCall.DEPARTANDREENTER,nil,nil,nil,true) +playerData.wop=true +self:_AddToDebrief(playerData,"Overshoot at wake - Pattern Waveoff!") +self:_SetPlayerStep(playerData,AIRBOSS.PatternStep.DEBRIEF) +end +end +function AIRBOSS:_Wake(playerData) +local X,Z=self:_GetDistances(playerData.unit) +if self:_CheckAbort(X,Z,self.Wake)then +self:_AbortPattern(playerData,X,Z,self.Wake,true) +return +end +if self:_CheckLimits(X,Z,self.Wake)then +self:_PlayerHint(playerData) +self:_SetPlayerStep(playerData,AIRBOSS.PatternStep.FINAL) +end +end +function AIRBOSS:_GetGrooveData(playerData) +local X,Z=self:_GetDistances(playerData.unit) +local stern=self:_GetSternCoord() +local rho=stern:Get2DDistance(playerData.unit:GetCoordinate()) +local astern=X=RAR and rho<=RIC and not playerData.waveoff then +local waveoff=self:_CheckWaveOff(glideslopeError,lineupError,AoA,playerData) +if waveoff then +self:T3(self.lid..string.format("Waveoff distance rho=%.1f m",rho)) +self:RadioTransmission(self.LSORadio,self.LSOCall.WAVEOFF,nil,nil,nil,true) +playerData.Tlso=timer.getTime() +playerData.waveoff=true +return +end +end +groovedata.Step=playerData.step +if rho>=RAR and rho=RAR and rho<=RIM then +if gd.LUE>0.22 and lineupError<-0.22 then +env.info" Drift Right across centre ==> DR-" +gd.Drift=" DR" +self:T(self.lid..string.format("Got Drift Right across centre step %s, d=%.3f: Max LUE=%.3f, lower LUE=%.3f",gs,d,gd.LUE,lineupError)) +elseif gd.LUE<-0.22 and lineupError>0.22 then +env.info" Drift Left ==> DL-" +gd.Drift=" DL" +self:T(self.lid..string.format("Got Drift Left across centre at step %s, d=%.3f: Min LUE=%.3f, lower LUE=%.3f",gs,d,gd.LUE,lineupError)) +elseif gd.LUE>0.13 and lineupError<-0.14 then +env.info" Little Drift Right across centre ==> (DR-)" +gd.Drift=" (DR)" +self:T(self.lid..string.format("Got Little Drift Right across centre at step %s, d=%.3f: Max LUE=%.3f, lower LUE=%.3f",gs,d,gd.LUE,lineupError)) +elseif gd.LUE<-0.13 and lineupError>0.14 then +env.info" Little Drift Left across centre ==> (DL-)" +gd.Drift=" (DL)" +self:E(self.lid..string.format("Got Little Drift Left across centre at step %s, d=%.3f: Min LUE=%.3f, lower LUE=%.3f",gs,d,gd.LUE,lineupError)) +end +end +if math.abs(lineupError)>math.abs(gd.LUE)then +self:T(self.lid..string.format("Got bigger LUE at step %s, d=%.3f: LUE %.3f>%.3f",gs,d,lineupError,gd.LUE)) +gd.LUE=lineupError +end +if gd.GSE>0.4 and glideslopeError<-0.3 then +gd.FlyThrough="\\" +self:T(self.lid..string.format("Got Fly through DOWN at step %s, d=%.3f: Max GSE=%.3f, lower GSE=%.3f",gs,d,gd.GSE,glideslopeError)) +elseif gd.GSE<-0.3 and glideslopeError>0.4 then +gd.FlyThrough="/" +self:E(self.lid..string.format("Got Fly through UP at step %s, d=%.3f: Min GSE=%.3f, lower GSE=%.3f",gs,d,gd.GSE,glideslopeError)) +end +if math.abs(glideslopeError)>math.abs(gd.GSE)then +self:T(self.lid..string.format("Got bigger GSE at step %s, d=%.3f: GSE |%.3f|>|%.3f|",gs,d,glideslopeError,gd.GSE)) +gd.GSE=glideslopeError +end +local aircraftaoa=self:_GetAircraftAoA(playerData) +local aoaopt=aircraftaoa.OnSpeed +if math.abs(AoA-aoaopt)>math.abs(gd.AoA-aoaopt)then +self:T(self.lid..string.format("Got bigger AoA error at step %s, d=%.3f: AoA %.3f>%.3f.",gs,d,AoA,gd.AoA)) +gd.AoA=AoA +end +end +local deltaT=timer.getTime()-playerData.Tlso +local _advice=true +if playerData.TIG0==nil and playerData.difficulty~=AIRBOSS.Difficulty.EASY then +_advice=false +end +if deltaT>=self.LSOdT and _advice then +self:_LSOadvice(playerData,glideslopeError,lineupError) +end +end +if X>self.carrierparam.totlength+self.carrierparam.sterndist then +if playerData.waveoff then +if playerData.landed then +self:_AddToDebrief(playerData,"You were waved off but landed anyway. Airboss wants to talk to you!") +else +self:_AddToDebrief(playerData,"You were waved off.") +end +elseif playerData.boltered then +self:_AddToDebrief(playerData,"You boltered.") +else +self:T("Player was not waved off but flew past the carrier without landing ==> Own wave off!") +self:_AddToDebrief(playerData,"Own waveoff.") +playerData.owo=true +end +self:_SetPlayerStep(playerData,AIRBOSS.PatternStep.DEBRIEF) +end +end +function AIRBOSS:_CheckWaveOff(glideslopeError,lineupError,AoA,playerData) +local waveoff=false +local glMax=1.8 +local glMin=-1.2 +local luAbs=3.0 +if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then +glMax=4.0 +glMin=-3.0 +luAbs=5.0 +return false +end +if glideslopeError>glMax then +local text=string.format("\n- Waveoff due to glideslope error %.2f > %.1f degrees!",glideslopeError,glMax) +self:T(self.lid..string.format("%s: %s",playerData.name,text)) +self:_AddToDebrief(playerData,text) +waveoff=true +elseif glideslopeErrorluAbs then +local text=string.format("\n- Waveoff due to line up error |%.1f| > %.1f degrees!",lineupError,luAbs) +self:T(self.lid..string.format("%s: %s",playerData.name,text)) +self:_AddToDebrief(playerData,text) +waveoff=true +end +if playerData.difficulty==AIRBOSS.Difficulty.HARD then +local aoaac=self:_GetAircraftAoA(playerData) +if AoAaoaac.SLOW then +local text=string.format("\n- Waveoff due to AoA %.1f > %.1f!",AoA,aoaac.SLOW) +self:T(self.lid..string.format("%s: %s",playerData.name,text)) +self:_AddToDebrief(playerData,text) +waveoff=true +end +end +return waveoff +end +function AIRBOSS:_CheckFoulDeck(playerData) +local check=false +if playerData.step==AIRBOSS.PatternStep.GROOVE_IM or +playerData.step==AIRBOSS.PatternStep.GROOVE_IC then +check=true +end +if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then +if playerData.step==AIRBOSS.PatternStep.GROOVE_AR or +playerData.step==AIRBOSS.PatternStep.GROOVE_AL then +check=true +end +end +if playerData.wofd==true or check==false then +return +end +local runway=self:_GetZoneRunwayBox() +if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then +runway=self:_GetZoneLandingSpot() +end +local R=250 +self:T(self.lid..string.format("Foul deck check: Scanning Carrier Runway Area. Radius=%.1f m.",R)) +local _,_,_,unitscan=self:GetCoordinate():ScanObjects(R,true,false,false) +local fouldeck=false +local foulunit=nil +for _,_unit in pairs(unitscan)do +local unit=_unit +local inzone=unit:IsInZone(runway) +local isaircraft=unit:IsAir() +local isairborn=unit:InAir() +if inzone and isaircraft and not isairborn then +local text=string.format("Unit %s on landing runway ==> Foul deck!",unit:GetName()) +self:T(self.lid..text) +MESSAGE:New(text,10):ToAllIf(self.Debug) +if self.Debug then +runway:FlareZone(FLARECOLOR.Red,30) +end +fouldeck=true +foulunit=unit +end +end +if playerData and fouldeck then +local text=string.format("Foul deck waveoff due to aircraft %s!",foulunit:GetName()) +self:T(self.lid..string.format("%s: %s",playerData.name,text)) +self:_AddToDebrief(playerData,text) +self:RadioTransmission(self.LSORadio,self.LSOCall.FOULDECK,false,1) +self:RadioTransmission(self.LSORadio,self.LSOCall.WAVEOFF,false,1.2,nil,true) +if playerData.showhints then +local text=string.format("overfly landing area and enter bolter pattern.") +self:MessageToPlayer(playerData,text,"LSO",nil,nil,false,3) +end +playerData.wofd=true +playerData.step=AIRBOSS.PatternStep.DEBRIEF +playerData.warning=nil +playerData.valid=false +if foulunit then +local foulflight=self:_GetFlightFromGroupInQueue(foulunit:GetGroup(),self.flights) +if foulflight and not foulflight.ai then +self:MessageToPlayer(foulflight,"move your ass from my runway. NOW!","AIRBOSS") +end +end +end +return fouldeck +end +function AIRBOSS:_GetSternCoord() +local hdg=self.carrier:GetHeading() +local FB=self:GetFinalBearing() +self.sterncoord:UpdateFromCoordinate(self:GetCoordinate()) +if self.carriertype==AIRBOSS.CarrierType.TARAWA then +self.sterncoord:Translate(self.carrierparam.sterndist,hdg,true,true):Translate(8,FB-90,true,true) +elseif self.carriertype==AIRBOSS.CarrierType.STENNIS then +self.sterncoord:Translate(self.carrierparam.sterndist,hdg,true,true):Translate(7,FB+90,true,true) +else +self.sterncoord:Translate(self.carrierparam.sterndist,hdg,true,true):Translate(9.5,FB+90,true,true) +end +self.sterncoord:SetAltitude(self.carrierparam.deckheight) +return self.sterncoord +end +function AIRBOSS:_GetWire(Lcoord,dc) +local FB=self:GetFinalBearing() +local Scoord=self:_GetSternCoord() +local Ldist=Lcoord:Get2DDistance(Scoord) +dc=dc or 65 +local d=Ldist-dc +if self.mpWireCorrection then +d=d-self.mpWireCorrection +end +local w1=self.carrierparam.wire1 +local w2=self.carrierparam.wire2 +local w3=self.carrierparam.wire3 +local w4=self.carrierparam.wire4 +local wire +if d wire=%d (dc=%.1f)",Ldist,Ldist-dc,wire,dc)) +return wire +end +function AIRBOSS:_Trapped(playerData) +if playerData.unit:InAir()==false then +local unit=playerData.unit +local coord=unit:GetCoordinate() +local v=unit:GetVelocityKMH()-self.carrier:GetVelocityKMH() +local stern=self:_GetSternCoord() +local s=stern:Get2DDistance(coord) +local dcorr=100 +if playerData.actype==AIRBOSS.AircraftCarrier.HORNET then +dcorr=100 +elseif playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then +dcorr=100 +elseif playerData.actype==AIRBOSS.AircraftCarrier.A4EC then +dcorr=56 +elseif playerData.actype==AIRBOSS.AircraftCarrier.T45C then +dcorr=56 +end +local wire=self:_GetWire(coord,dcorr) +local text=string.format("Player %s _Trapped: v=%.1f km/h, s-dcorr=%.1f m ==> wire=%d (dcorr=%d)",playerData.name,v,s-dcorr,wire,dcorr) +self:T(self.lid..text) +if v>5 then +if wire>4 and v>10 and not playerData.warning then +self:RadioTransmission(self.LSORadio,self.LSOCall.BOLTER,nil,nil,nil,true) +playerData.warning=true +end +self:ScheduleOnce(0.1,self._Trapped,self,playerData) +return +end +if self.Debug then +coord:SmokeBlue() +coord:MarkToAll(text) +stern:MarkToAll("Stern") +end +playerData.wire=wire +local text=string.format("Trapped %d-wire.",wire) +if wire==3 then +text=text.." Well done!" +elseif wire==2 then +text=text.." Not bad, maybe you even get the 3rd next time." +elseif wire==4 then +text=text.." That was scary. You can do better than this!" +elseif wire==1 then +text=text.." Try harder next time!" +end +self:MessageToPlayer(playerData,text,"LSO","") +local hint=string.format("Trapped %d-wire.",wire) +self:_AddToDebrief(playerData,hint,"Groove: IW") +else +local text=string.format("Player %s boltered in trapped function.",playerData.name) +self:T(self.lid..text) +MESSAGE:New(text,5,"DEBUG"):ToAllIf(self.debug) +playerData.boltered=true +end +playerData.step=AIRBOSS.PatternStep.DEBRIEF +playerData.warning=nil +end +function AIRBOSS:_GetZoneInitial(case) +self.zoneInitial=self.zoneInitial or ZONE_POLYGON_BASE:New("Zone CASE I/II Initial") +local radial=self:GetRadial(2,false,false) +local cv=self:GetCoordinate() +local vec2={} +if case==1 then +local c1=cv:Translate(UTILS.NMToMeters(0.5),radial-90) +local c2=cv:Translate(UTILS.NMToMeters(1.3),radial-90):Translate(UTILS.NMToMeters(3),radial) +local c3=cv:Translate(UTILS.NMToMeters(0.4),radial+90):Translate(UTILS.NMToMeters(3),radial) +local c4=cv:Translate(UTILS.NMToMeters(1.0),radial) +local c5=cv +vec2={c1:GetVec2(),c2:GetVec2(),c3:GetVec2(),c4:GetVec2(),c5:GetVec2()} +else +local c1=cv:Translate(UTILS.NMToMeters(0.5),radial-90) +local c2=c1:Translate(UTILS.NMToMeters(0.5),radial) +local c3=cv:Translate(UTILS.NMToMeters(1.2),radial-90):Translate(UTILS.NMToMeters(3),radial) +local c4=cv:Translate(UTILS.NMToMeters(1.2),radial+90):Translate(UTILS.NMToMeters(3),radial) +local c5=cv:Translate(UTILS.NMToMeters(0.5),radial) +local c6=cv +vec2={c1:GetVec2(),c2:GetVec2(),c3:GetVec2(),c4:GetVec2(),c5:GetVec2(),c6:GetVec2()} +end +self.zoneInitial:UpdateFromVec2(vec2) +return self.zoneInitial +end +function AIRBOSS:_GetZoneLineup() +self.zoneLineup=self.zoneLineup or ZONE_POLYGON_BASE:New("Zone Lineup") +local fbi=self:GetRadial(1,false,false) +local st=self:_GetOptLandingCoordinate() +local c1=st +local c2=st:Translate(UTILS.NMToMeters(0.50),fbi+15) +local c3=st:Translate(UTILS.NMToMeters(0.50),fbi+self.lue._max-0.05) +local c4=st:Translate(UTILS.NMToMeters(0.77),fbi+self.lue._max-0.05) +local c5=c4:Translate(UTILS.NMToMeters(0.25),fbi-90) +local vec2={c1:GetVec2(),c2:GetVec2(),c3:GetVec2(),c4:GetVec2(),c5:GetVec2()} +self.zoneLineup:UpdateFromVec2(vec2) +return self.zoneLineup +end +function AIRBOSS:_GetZoneGroove(l,w,b) +self.zoneGroove=self.zoneGroove or ZONE_POLYGON_BASE:New("Zone Groove") +l=l or 1.50 +w=w or 0.25 +b=b or 0.10 +local fbi=self:GetRadial(1,false,false) +local st=self:_GetSternCoord() +local c1=st:Translate(self.carrierparam.totwidthstarboard,fbi-90) +local c2=st:Translate(UTILS.NMToMeters(0.10),fbi-90):Translate(UTILS.NMToMeters(0.3),fbi) +local c3=st:Translate(UTILS.NMToMeters(0.25),fbi-90):Translate(UTILS.NMToMeters(l),fbi) +local c4=st:Translate(UTILS.NMToMeters(w/2),fbi+90):Translate(UTILS.NMToMeters(l),fbi) +local c5=st:Translate(UTILS.NMToMeters(b),fbi+90):Translate(UTILS.NMToMeters(0.3),fbi) +local c6=st:Translate(self.carrierparam.totwidthport,fbi+90) +local vec2={c1:GetVec2(),c2:GetVec2(),c3:GetVec2(),c4:GetVec2(),c5:GetVec2(),c6:GetVec2()} +self.zoneGroove:UpdateFromVec2(vec2) +return self.zoneGroove +end +function AIRBOSS:_GetZoneBullseye(case) +local radius=UTILS.NMToMeters(1) +local distance=UTILS.NMToMeters(3) +local radial=self:GetRadial(case,false,false) +local coord=self:GetCoordinate():Translate(distance,radial) +local vec2=coord:GetVec2() +local zone=ZONE_RADIUS:New("Zone Bullseye",vec2,radius) +return zone +end +function AIRBOSS:_GetZoneDirtyUp(case) +local radius=UTILS.NMToMeters(1) +local distance=UTILS.NMToMeters(9) +local radial=self:GetRadial(case,false,false) +local coord=self:GetCoordinate():Translate(distance,radial) +local vec2=coord:GetVec2() +local zone=ZONE_RADIUS:New("Zone Dirty Up",vec2,radius) +return zone +end +function AIRBOSS:_GetZoneArcOut(case) +local radius=UTILS.NMToMeters(1.25) +local distance=UTILS.NMToMeters(11.75) +local radial=self:GetRadial(case,false,false) +local coord=self:GetCoordinate():Translate(distance,radial) +local zone=ZONE_RADIUS:New("Zone Arc Out",coord:GetVec2(),radius) +return zone +end +function AIRBOSS:_GetZoneArcIn(case) +local radius=UTILS.NMToMeters(1.25) +local radial=self:GetRadial(case,false,true) +local alpha=math.rad(self.holdingoffset) +local x=14 +local distance=UTILS.NMToMeters(x) +local coord=self:GetCoordinate():Translate(distance,radial) +local zone=ZONE_RADIUS:New("Zone Arc In",coord:GetVec2(),radius) +return zone +end +function AIRBOSS:_GetZonePlatform(case) +local radius=UTILS.NMToMeters(1) +local radial=self:GetRadial(case,false,true) +local alpha=math.rad(self.holdingoffset) +local distance=UTILS.NMToMeters(19) +local coord=self:GetCoordinate():Translate(distance,radial) +local zone=ZONE_RADIUS:New("Zone Platform",coord:GetVec2(),radius) +return zone +end +function AIRBOSS:_GetZoneCorridor(case,l) +l=l or 31 +local radial=self:GetRadial(case,false,false) +local offset=self:GetRadial(case,false,true) +local dx=5 +local w=2 +local w2=w/2 +local d=12 +local cv=self:GetCoordinate() +local c={} +c[1]=cv:Translate(-UTILS.NMToMeters(dx),radial) +if math.abs(self.holdingoffset)>=5 then +c[2]=c[1]:Translate(UTILS.NMToMeters(w2),radial-90) +c[3]=c[2]:Translate(UTILS.NMToMeters(d+dx+w2),radial) +c[4]=cv:Translate(UTILS.NMToMeters(15),offset):Translate(UTILS.NMToMeters(1),offset-90) +c[5]=cv:Translate(UTILS.NMToMeters(l),offset):Translate(UTILS.NMToMeters(1),offset-90) +c[6]=cv:Translate(UTILS.NMToMeters(l),offset):Translate(UTILS.NMToMeters(1),offset+90) +c[7]=cv:Translate(UTILS.NMToMeters(13),offset):Translate(UTILS.NMToMeters(1),offset+90) +c[8]=cv:Translate(UTILS.NMToMeters(11),radial):Translate(UTILS.NMToMeters(1),radial+90) +c[9]=c[1]:Translate(UTILS.NMToMeters(w2),radial+90) +else +c[2]=c[1]:Translate(UTILS.NMToMeters(w2),radial-90) +c[3]=c[2]:Translate(UTILS.NMToMeters(dx+l),radial) +c[4]=c[3]:Translate(UTILS.NMToMeters(w),radial+90) +c[5]=c[1]:Translate(UTILS.NMToMeters(w2),radial+90) +end +local p={} +for _i,_c in ipairs(c)do +if self.Debug then +end +p[_i]=_c:GetVec2() +end +local zone=ZONE_POLYGON_BASE:New("CASE II/III Approach Corridor",p) +return zone +end +function AIRBOSS:_GetZoneCarrierBox() +self.zoneCarrierbox=self.zoneCarrierbox or ZONE_POLYGON_BASE:New("Carrier Box Zone") +local S=self:_GetSternCoord() +local hdg=self:GetHeading(false) +local p={} +p[1]=S:Translate(self.carrierparam.totwidthstarboard,hdg+90) +p[2]=p[1]:Translate(self.carrierparam.totlength,hdg) +p[3]=p[2]:Translate(self.carrierparam.totwidthstarboard+self.carrierparam.totwidthport,hdg-90) +p[4]=p[3]:Translate(self.carrierparam.totlength,hdg-180) +local vec2={} +for _,coord in ipairs(p)do +table.insert(vec2,coord:GetVec2()) +end +self.zoneCarrierbox:UpdateFromVec2(vec2) +return self.zoneCarrierbox +end +function AIRBOSS:_GetZoneRunwayBox() +self.zoneRunwaybox=self.zoneRunwaybox or ZONE_POLYGON_BASE:New("Landing Runway Zone") +local S=self:_GetSternCoord() +local FB=self:GetFinalBearing(false) +local p={} +p[1]=S:Translate(self.carrierparam.rwywidth*0.5,FB+90) +p[2]=p[1]:Translate(self.carrierparam.rwylength,FB) +p[3]=p[2]:Translate(self.carrierparam.rwywidth,FB-90) +p[4]=p[3]:Translate(self.carrierparam.rwylength,FB-180) +local vec2={} +for _,coord in ipairs(p)do +table.insert(vec2,coord:GetVec2()) +end +self.zoneRunwaybox:UpdateFromVec2(vec2) +return self.zoneRunwaybox +end +function AIRBOSS:_GetZoneAbeamLandingSpot() +local S=self:_GetOptLandingCoordinate() +local FB=self:GetFinalBearing(false) +local p={} +p[1]=S:Translate(15,FB):Translate(15,FB+90) +p[2]=S:Translate(-15,FB):Translate(15,FB+90) +p[3]=S:Translate(-15,FB):Translate(15,FB-90) +p[4]=S:Translate(15,FB):Translate(15,FB-90) +local vec2={} +for _,coord in ipairs(p)do +table.insert(vec2,coord:GetVec2()) +end +local zone=ZONE_POLYGON_BASE:New("Abeam Landing Spot Zone",vec2) +return zone +end +function AIRBOSS:_GetZoneLandingSpot() +local S=self:_GetLandingSpotCoordinate() +local FB=self:GetFinalBearing(false) +local p={} +p[1]=S:Translate(10,FB):Translate(10,FB+90) +p[2]=S:Translate(-10,FB):Translate(10,FB+90) +p[3]=S:Translate(-10,FB):Translate(10,FB-90) +p[4]=S:Translate(10,FB):Translate(10,FB-90) +local vec2={} +for _,coord in ipairs(p)do +table.insert(vec2,coord:GetVec2()) +end +local zone=ZONE_POLYGON_BASE:New("Landing Spot Zone",vec2) +return zone +end +function AIRBOSS:_GetZoneHolding(case,stack) +local zoneHolding=nil +if stack<=0 then +self:E(self.lid.."ERROR: Stack <= 0 in _GetZoneHolding!") +self:E({case=case,stack=stack}) +return nil +end +local patternalt,c1,c2=self:_GetMarshalAltitude(stack,case) +if case==1 then +local hdg=self:GetHeading() +local D=UTILS.NMToMeters(2.5) +local Post=self:GetCoordinate():Translate(D,hdg+270) +self.zoneHolding=ZONE_RADIUS:New("CASE I Holding Zone",Post:GetVec2(),self.marshalradius) +if self.carriertype==AIRBOSS.CarrierType.TARAWA then +self.zoneHolding=ZONE_RADIUS:New("CASE I Holding Zone",self.carrier:GetVec2(),UTILS.NMToMeters(5)) +end +else +local radial=self:GetRadial(case,false,true) +local p={} +p[1]=c2:Translate(UTILS.NMToMeters(1),radial-90):GetVec2() +p[2]=c1:Translate(UTILS.NMToMeters(1),radial-90):GetVec2() +p[3]=c1:Translate(UTILS.NMToMeters(7),radial+90):GetVec2() +p[4]=c2:Translate(UTILS.NMToMeters(7),radial+90):GetVec2() +self.zoneHolding=self.zoneHolding or ZONE_POLYGON_BASE:New("CASE II/III Holding Zone") +self.zoneHolding:UpdateFromVec2(p) +end +return self.zoneHolding +end +function AIRBOSS:_GetZoneCommence(case,stack) +local zone +if case==1 then +local hdg=self:GetHeading() +local D=UTILS.NMToMeters(4.75) +local R=UTILS.NMToMeters(1) +local Three=self:GetCoordinate():Translate(D,hdg+275) +if self.carriertype==AIRBOSS.CarrierType.TARAWA then +local Dx=UTILS.NMToMeters(2.25) +local Dz=UTILS.NMToMeters(2.25) +R=UTILS.NMToMeters(1) +Three=self:GetCoordinate():Translate(Dz,hdg-90):Translate(Dx,hdg-180) +end +self.zoneCommence=self.zoneCommence or ZONE_RADIUS:New("CASE I Commence Zone") +self.zoneCommence:UpdateFromVec2(Three:GetVec2(),R) +else +stack=stack or 1 +local l=20+stack +local offset=self:GetRadial(case,false,true) +local cv=self:GetCoordinate() +local c={} +c[1]=cv:Translate(UTILS.NMToMeters(l),offset):Translate(UTILS.NMToMeters(1),offset-90) +c[2]=cv:Translate(UTILS.NMToMeters(l+2.5),offset):Translate(UTILS.NMToMeters(1),offset-90) +c[3]=cv:Translate(UTILS.NMToMeters(l+2.5),offset):Translate(UTILS.NMToMeters(1),offset+90) +c[4]=cv:Translate(UTILS.NMToMeters(l),offset):Translate(UTILS.NMToMeters(1),offset+90) +local p={} +for _i,_c in ipairs(c)do +p[_i]=_c:GetVec2() +end +self.zoneCommence=self.zoneCommence or ZONE_POLYGON_BASE:New("CASE II/III Commence Zone") +self.zoneCommence:UpdateFromVec2(p) +end +return self.zoneCommence +end +function AIRBOSS:_AttitudeMonitor(playerData) +local unit=playerData.unit +local aoa=unit:GetAoA() +local yaw=unit:GetYaw() +local roll=unit:GetRoll() +local pitch=unit:GetPitch() +local dist=playerData.unit:GetCoordinate():Get2DDistance(self:GetCoordinate()) +local dx,dz,rho,phi=self:_GetDistances(unit) +local wind=unit:GetCoordinate():GetWindWithTurbulenceVec3() +local velo=unit:GetVelocityVec3() +local vabs=UTILS.VecNorm(velo) +local rwy=false +local step=playerData.step +if playerData.step==AIRBOSS.PatternStep.FINAL or +playerData.step==AIRBOSS.PatternStep.GROOVE_XX or +playerData.step==AIRBOSS.PatternStep.GROOVE_IM or +playerData.step==AIRBOSS.PatternStep.GROOVE_IC or +playerData.step==AIRBOSS.PatternStep.GROOVE_AR or +playerData.step==AIRBOSS.PatternStep.GROOVE_AL or +playerData.step==AIRBOSS.PatternStep.GROOVE_LC or +playerData.step==AIRBOSS.PatternStep.GROOVE_IW then +step=self:_GS(step,-1) +rwy=true +end +local relhead=self:_GetRelativeHeading(playerData.unit,rwy) +local text=string.format("Pattern step: %s",step) +text=text..string.format("\nAoA=%.1f° = %.1f Units | |V|=%.1f knots",aoa,self:_AoADeg2Units(playerData,aoa),UTILS.MpsToKnots(vabs)) +if self.Debug then +text=text..string.format("\nVx=%.1f Vy=%.1f Vz=%.1f m/s",velo.x,velo.y,velo.z) +text=text..string.format("\nWind Vx=%.1f Vy=%.1f Vz=%.1f m/s",wind.x,wind.y,wind.z) +end +text=text..string.format("\nPitch=%.1f° | Roll=%.1f° | Yaw=%.1f°",pitch,roll,yaw) +text=text..string.format("\nClimb Angle=%.1f° | Rate=%d ft/min",unit:GetClimbAngle(),velo.y*196.85) +local dist=self:_GetOptLandingCoordinate():Get3DDistance(playerData.unit) +local vplayer=playerData.unit:GetVelocityKMH() +local vcarrier=self.carrier:GetVelocityKMH() +local dv=math.abs(vplayer-vcarrier) +local alt=self:_GetAltCarrier(playerData.unit) +text=text..string.format("\nDist=%.1f m Alt=%.1f m delta|V|=%.1f km/h",dist,alt,dv) +if playerData.step==AIRBOSS.PatternStep.FINAL or +playerData.step==AIRBOSS.PatternStep.GROOVE_XX or +playerData.step==AIRBOSS.PatternStep.GROOVE_IM or +playerData.step==AIRBOSS.PatternStep.GROOVE_IC or +playerData.step==AIRBOSS.PatternStep.GROOVE_AR or +playerData.step==AIRBOSS.PatternStep.GROOVE_AL or +playerData.step==AIRBOSS.PatternStep.GROOVE_LC or +playerData.step==AIRBOSS.PatternStep.GROOVE_IW then +local lue=self:_Lineup(playerData.unit,true) +local gle=self:_Glideslope(playerData.unit) +text=text..string.format("\nGamma=%.1f° | Rho=%.1f°",relhead,phi) +text=text..string.format("\nLineUp=%.2f° | GlideSlope=%.2f° | AoA=%.1f Units",lue,gle,self:_AoADeg2Units(playerData,aoa)) +local grade,points,analysis=self:_LSOgrade(playerData) +text=text..string.format("\nTgroove=%.1f sec",self:_GetTimeInGroove(playerData)) +text=text..string.format("\nGrade: %s %.1f PT - %s",grade,points,analysis) +else +text=text..string.format("\nR=%.2f NM | X=%d Z=%d m",UTILS.MetersToNM(rho),dx,dz) +text=text..string.format("\nGamma=%.1f° | Rho=%.1f°",relhead,phi) +end +MESSAGE:New(text,1,nil,true):ToClient(playerData.client) +end +function AIRBOSS:_Glideslope(unit,optangle) +if optangle==nil then +if unit:GetTypeName()==AIRBOSS.AircraftCarrier.AV8B then +optangle=3.0 +else +optangle=3.5 +end +end +local landingcoord=self:_GetOptLandingCoordinate() +local x=unit:GetCoordinate():Get2DDistance(landingcoord) +local h=self:_GetAltCarrier(unit) +if unit:GetTypeName()==AIRBOSS.AircraftCarrier.AV8B then +h=unit:GetAltitude()-(UTILS.FeetToMeters(50)+self.carrierparam.deckheight+2) +end +local glideslope=math.atan(h/x) +local gs=math.deg(glideslope)-optangle +return gs +end +function AIRBOSS:_Glideslope2(unit,optangle) +if optangle==nil then +if unit:GetTypeName()==AIRBOSS.AircraftCarrier.AV8B then +optangle=3.0 +else +optangle=3.5 +end +end +local landingcoord=self:_GetOptLandingCoordinate() +local x=unit:GetCoordinate():Get3DDistance(landingcoord) +local h=self:_GetAltCarrier(unit) +if unit:GetTypeName()==AIRBOSS.AircraftCarrier.AV8B then +h=unit:GetAltitude()-(UTILS.FeetToMeters(50)+self.carrierparam.deckheight+2) +end +local glideslope=math.asin(h/x) +local gs=math.deg(glideslope)-optangle +self:T3(self.lid..string.format("Glide slope error = %.1f, x=%.1f h=%.1f",gs,x,h)) +return gs +end +function AIRBOSS:_Lineup(unit,runway) +local landingcoord=self:_GetOptLandingCoordinate() +local A=landingcoord:GetVec3() +local B=unit:GetVec3() +local C=UTILS.VecSubstract(A,B) +C.y=0.0 +local X=self.carrier:GetOrientationX() +X.y=0.0 +if runway then +X=UTILS.Rotate2D(X,-self.carrierparam.rwyangle) +end +local x=UTILS.VecDot(X,C) +local Z=self.carrier:GetOrientationZ() +Z.y=0.0 +if runway then +Z=UTILS.Rotate2D(Z,-self.carrierparam.rwyangle) +end +local z=UTILS.VecDot(Z,C) +local lineup=math.deg(math.atan2(z,x)) +return lineup +end +function AIRBOSS:_GetAltCarrier(unit) +local h=unit:GetAltitude()-self.carrierparam.deckheight-2 +return h +end +function AIRBOSS:_GetOptLandingCoordinate() +self.landingcoord:UpdateFromCoordinate(self:_GetSternCoord()) +local FB=self:GetFinalBearing(false) +if self.carriertype==AIRBOSS.CarrierType.TARAWA then +self.landingcoord:UpdateFromCoordinate(self:_GetLandingSpotCoordinate()):Translate(35,FB-90,true,true) +self.landingcoord:SetAltitude(UTILS.FeetToMeters(120)) +else +if self.carrierparam.wire3 then +local w3=self.carrierparam.wire3 +self.landingcoord:Translate(w3,FB,true,true) +end +self.landingcoord.y=self.landingcoord.y+2 +end +return self.landingcoord +end +function AIRBOSS:_GetLandingSpotCoordinate() +self.landingspotcoord:UpdateFromCoordinate(self:_GetSternCoord()) +if self.carriertype==AIRBOSS.CarrierType.TARAWA then +local hdg=self:GetHeading() +self.landingspotcoord:Translate(57,hdg,true,true):SetAltitude(self.carrierparam.deckheight) +end +return self.landingspotcoord +end +function AIRBOSS:GetHeading(magnetic) +self:F3({magnetic=magnetic}) +local hdg=self.carrier:GetHeading() +if magnetic then +hdg=hdg-self.magvar +end +if hdg<0 then +hdg=hdg+360 +end +return hdg +end +function AIRBOSS:GetBRC() +return self:GetHeading(true) +end +function AIRBOSS:GetWind(alt,magnetic,coord) +local cv=coord or self:GetCoordinate() +local Wdir,Wspeed=cv:GetWind(alt or 50) +if magnetic then +Wdir=Wdir-self.magvar +if Wdir<0 then +Wdir=Wdir+360 +end +end +return Wdir,Wspeed +end +function AIRBOSS:GetWindOnDeck(alt) +local cv=self:GetCoordinate() +local vc=self.carrier:GetVelocityVec3() +local xc=self.carrier:GetOrientationX() +local zc=self.carrier:GetOrientationZ() +xc=UTILS.Rotate2D(xc,-self.carrierparam.rwyangle) +zc=UTILS.Rotate2D(zc,-self.carrierparam.rwyangle) +local vw=cv:GetWindWithTurbulenceVec3(alt or 50) +local vT=UTILS.VecSubstract(vw,vc) +local vpa=UTILS.VecDot(vT,xc) +local vpp=UTILS.VecDot(vT,zc) +local vabs=UTILS.VecNorm(vT) +return-vpa,vpp,vabs +end +function AIRBOSS:GetHeadingIntoWind(magnetic,coord) +local windfrom,vwind=self:GetWind(nil,nil,coord) +local intowind=windfrom-self.carrierparam.rwyangle +if vwind<0.1 then +intowind=self:GetHeading() +end +if magnetic then +intowind=intowind-self.magvar +end +if intowind<0 then +intowind=intowind+360 +end +return intowind +end +function AIRBOSS:GetBRCintoWind() +return self:GetHeadingIntoWind(true) +end +function AIRBOSS:GetFinalBearing(magnetic) +local fb=self:GetHeading(magnetic) +fb=fb+self.carrierparam.rwyangle +if fb<0 then +fb=fb+360 +end +return fb +end +function AIRBOSS:GetRadial(case,magnetic,offset,inverse) +case=case or self.case +local radial +if case==1 then +radial=self:GetFinalBearing(magnetic)-180 +elseif case==2 then +radial=self:GetHeading(magnetic)-180 +if offset then +radial=radial+self.holdingoffset +end +elseif case==3 then +radial=self:GetFinalBearing(magnetic)-180 +if offset then +radial=radial+self.holdingoffset +end +end +if radial<0 then +radial=radial+360 +end +if inverse then +radial=radial-180 +if radial<0 then +radial=radial+360 +end +end +return radial +end +function AIRBOSS:_GetDeltaHeading(hdg1,hdg2) +local V={} +V.x=math.cos(math.rad(hdg1)) +V.y=0 +V.z=math.sin(math.rad(hdg1)) +local W={} +W.x=math.cos(math.rad(hdg2)) +W.y=0 +W.z=math.sin(math.rad(hdg2)) +local alpha=UTILS.VecAngle(V,W) +return alpha +end +function AIRBOSS:_GetRelativeHeading(unit,runway) +local vC=self.carrier:GetOrientationX() +if runway then +vC=UTILS.Rotate2D(vC,-self.carrierparam.rwyangle) +end +local vP=unit:GetOrientationX() +vC.y=0;vP.y=0 +local rhdg=UTILS.VecAngle(vC,vP) +return rhdg +end +function AIRBOSS:_GetRelativeVelocity(unit) +local vC=self.carrier:GetVelocityVec3() +local vP=unit:GetVelocityVec3() +vC.y=0;vP.y=0 +local v=UTILS.VecSubstract(vP,vC) +return UTILS.VecNorm(v),v +end +function AIRBOSS:_GetDistances(unit) +local a=self.carrier:GetVec3() +local b=unit:GetVec3() +local c={x=b.x-a.x,y=0,z=b.z-a.z} +local x=self.carrier:GetOrientationX() +local dx=UTILS.VecDot(x,c) +local z=self.carrier:GetOrientationZ() +local dz=UTILS.VecDot(z,c) +local rho=math.sqrt(dx*dx+dz*dz) +local phi=math.deg(math.atan2(dz,dx)) +if phi<0 then +phi=phi+360 +end +return dx,dz,rho,phi +end +function AIRBOSS:_CheckLimits(X,Z,check) +local nextXmin=check.LimitXmin==nil or(check.LimitXmin and(check.LimitXmin<0 and X<=check.LimitXmin or check.LimitXmin>=0 and X>=check.LimitXmin)) +local nextXmax=check.LimitXmax==nil or(check.LimitXmax and(check.LimitXmax<0 and X>=check.LimitXmax or check.LimitXmax>=0 and X<=check.LimitXmax)) +local nextZmin=check.LimitZmin==nil or(check.LimitZmin and(check.LimitZmin<0 and Z<=check.LimitZmin or check.LimitZmin>=0 and Z>=check.LimitZmin)) +local nextZmax=check.LimitZmax==nil or(check.LimitZmax and(check.LimitZmax<0 and Z>=check.LimitZmax or check.LimitZmax>=0 and Z<=check.LimitZmax)) +local next=nextXmin and nextXmax and nextZmin and nextZmax +local text=string.format("step=%s: next=%s: X=%d Xmin=%s Xmax=%s | Z=%d Zmin=%s Zmax=%s", +check.name,tostring(next),X,tostring(check.LimitXmin),tostring(check.LimitXmax),Z,tostring(check.LimitZmin),tostring(check.LimitZmax)) +self:T3(self.lid..text) +return next +end +function AIRBOSS:_LSOadvice(playerData,glideslopeError,lineupError) +local advice=0 +if glideslopeError>self.gle.HIGH then +self:RadioTransmission(self.LSORadio,self.LSOCall.HIGH,true,nil,nil,true) +advice=advice+self.LSOCall.HIGH.duration +elseif glideslopeError>self.gle.High then +self:RadioTransmission(self.LSORadio,self.LSOCall.HIGH,false,nil,nil,true) +advice=advice+self.LSOCall.HIGH.duration +elseif glideslopeErrorself.lue.RIGHT then +self:RadioTransmission(self.LSORadio,self.LSOCall.RIGHTFORLINEUP,true,nil,nil,true) +advice=advice+self.LSOCall.RIGHTFORLINEUP.duration +elseif lineupError>self.lue.Right then +self:RadioTransmission(self.LSORadio,self.LSOCall.RIGHTFORLINEUP,false,nil,nil,true) +advice=advice+self.LSOCall.RIGHTFORLINEUP.duration +else +end +local AOA=playerData.unit:GetAoA() +local acaoa=self:_GetAircraftAoA(playerData) +if playerData.actype~=AIRBOSS.AircraftCarrier.AV8B then +if AOA>acaoa.SLOW then +self:RadioTransmission(self.LSORadio,self.LSOCall.SLOW,true,nil,nil,true) +advice=advice+self.LSOCall.SLOW.duration +elseif AOA>acaoa.Slow then +self:RadioTransmission(self.LSORadio,self.LSOCall.SLOW,false,nil,nil,true) +advice=advice+self.LSOCall.SLOW.duration +elseif AOA>acaoa.OnSpeedMax then +elseif AOA=16.4 and t<=16.6 then +grade="_OK_" +end +return grade +end +function AIRBOSS:_LSOgrade(playerData) +local function count(base,pattern) +return select(2,string.gsub(base,pattern,"")) +end +local GXX,nXX=self:_Flightdata2Text(playerData,AIRBOSS.GroovePos.XX) +local GIM,nIM=self:_Flightdata2Text(playerData,AIRBOSS.GroovePos.IM) +local GIC,nIC=self:_Flightdata2Text(playerData,AIRBOSS.GroovePos.IC) +local GAR,nAR=self:_Flightdata2Text(playerData,AIRBOSS.GroovePos.AR) +local G=GXX.." "..GIM.." ".." "..GIC.." "..GAR +local N=nXX+nIM+nIC+nAR +local nL=count(G,'_')/2 +local nS=count(G,'%(') +local nN=N-nS-nL +local Tgroove=playerData.Tgroove +local TgrooveUnicorn=Tgroove and(Tgroove>=15.0 and Tgroove<=18.99)or false +local grade +local points +if N==0 and TgrooveUnicorn then +grade="_OK_" +points=5.0 +G="Unicorn" +else +if nL>0 then +grade="--" +points=2.0 +elseif nN>0 then +grade="(OK)" +points=3.0 +else +grade="OK" +points=4.0 +end +end +G=G:gsub("%)%(","") +G=G:gsub("__","") +local text="LSO grade:\n" +text=text..G.."\n" +text=text.."Grade = "..grade.." points = "..points.."\n" +text=text.."# of total deviations = "..N.."\n" +text=text.."# of large deviations _ = "..nL.."\n" +text=text.."# of normal deviations = "..nN.."\n" +text=text.."# of small deviations ( = "..nS.."\n" +self:T2(self.lid..text) +if playerData.wop then +if playerData.lig then +grade="WO" +points=1.0 +G="LIG" +else +grade="WOP" +points=2.0 +G="n/a" +end +elseif playerData.wofd then +if playerData.landed then +grade="CUT" +points=0.0 +else +grade="WOFD" +points=-1.0 +end +G="n/a" +elseif playerData.owo then +grade="OWO" +points=2.0 +if N==0 then +G="n/a" +end +elseif playerData.waveoff then +if playerData.landed then +grade="CUT" +points=0.0 +else +grade="WO" +points=1.0 +end +elseif playerData.boltered then +grade="-- (BOLTER)" +points=2.5 +end +return grade,points,G +end +function AIRBOSS:_Flightdata2Text(playerData,groovestep) +local function little(text) +return string.format("(%s)",text) +end +local function underline(text) +return string.format("_%s_",text) +end +local fdata=playerData.groove[groovestep] +if fdata==nil then +self:T3(self.lid.."Flight data is nil.") +return"",0 +end +local step=fdata.Step +local AOA=fdata.AoA +local GSE=fdata.GSE +local LUE=fdata.LUE +local ROL=fdata.Roll +local acaoa=self:_GetAircraftAoA(playerData) +local P=nil +if step==AIRBOSS.PatternStep.GROOVE_XX and ROL<=4.0 and playerData.case<3 then +if LUE>self.lue.RIGHT then +P=underline("AA") +elseif +LUE>self.lue.RightMed then +P="AA " +elseif +LUE>self.lue.Right then +P=little("AA") +end +end +local O=nil +if step==AIRBOSS.PatternStep.GROOVE_XX then +if LUEacaoa.SLOW then +S=underline("SLO") +elseif AOA>acaoa.Slow then +S="SLO" +elseif AOA>acaoa.OnSpeedMax then +S=little("SLO") +elseif AOAself.gle.HIGH then +A=underline("H") +elseif GSE>self.gle.High then +A="H" +elseif GSE>self.gle._max then +A=little("H") +elseif GSEself.lue.RIGHT then +D=underline("LUL") +elseif LUE>self.lue.Right then +D="LUL" +elseif LUE>self.lue._max then +D=little("LUL") +elseif playerData.case<3 then +if LUEpos.Xmax then +self:T(string.format("Xmax: X=%d > %d=Xmax",X,pos.Xmax)) +abort=true +elseif pos.Zmin and Zpos.Zmax then +self:T(string.format("Zmax: Z=%d > %d=Zmax",Z,pos.Zmax)) +abort=true +end +return abort +end +function AIRBOSS:_TooFarOutText(X,Z,posData) +local text="you are too " +local xtext=nil +if posData.Xmin and XposData.Xmax then +if posData.Xmax>=0 then +xtext="far ahead of " +else +xtext="close to " +end +end +local ztext=nil +if posData.Zmin and ZposData.Zmax then +if posData.Zmax>=0 then +ztext="far starboard of " +else +ztext="too close to " +end +end +if xtext and ztext then +text=text..xtext.." and "..ztext +elseif xtext then +text=text..xtext +elseif ztext then +text=text..ztext +end +text=text.."the carrier." +if xtext==nil and ztext==nil then +text="you are too far from where you should be!" +end +return text +end +function AIRBOSS:_AbortPattern(playerData,X,Z,posData,patternwo) +local text=self:_TooFarOutText(X,Z,posData) +local dtext=string.format("Abort: X=%d Xmin=%s, Xmax=%s | Z=%d Zmin=%s Zmax=%s",X,tostring(posData.Xmin),tostring(posData.Xmax),Z,tostring(posData.Zmin),tostring(posData.Zmax)) +self:T(self.lid..dtext) +self:MessageToPlayer(playerData,text,"LSO") +if patternwo then +playerData.wop=true +self:_AddToDebrief(playerData,string.format("Pattern wave off: %s",text)) +self:RadioTransmission(self.LSORadio,self.LSOCall.DEPARTANDREENTER,false,3,nil,nil,true) +playerData.step=AIRBOSS.PatternStep.DEBRIEF +playerData.warning=nil +end +end +function AIRBOSS:_PlayerHint(playerData,delay,soundoff) +if not playerData.showhints then +return +end +local alt,aoa,dist,speed=self:_GetAircraftParameters(playerData) +local hintAlt,debriefAlt,callAlt=self:_AltitudeCheck(playerData,alt) +local hintSpeed,debriefSpeed,callSpeed=self:_SpeedCheck(playerData,speed) +local hintAoA,debriefAoA,callAoA=self:_AoACheck(playerData,aoa) +local hintDist,debriefDist,callDist=self:_DistanceCheck(playerData,dist) +local hint="" +if hintAlt and hintAlt~=""then +hint=hint.."\n"..hintAlt +end +if hintSpeed and hintSpeed~=""then +hint=hint.."\n"..hintSpeed +end +if hintAoA and hintAoA~=""then +hint=hint.."\n"..hintAoA +end +if hintDist and hintDist~=""then +hint=hint.."\n"..hintDist +end +local debrief="" +if debriefAlt and debriefAlt~=""then +debrief=debrief.."\n- "..debriefAlt +end +if debriefSpeed and debriefSpeed~=""then +debrief=debrief.."\n- "..debriefSpeed +end +if debriefAoA and debriefAoA~=""then +debrief=debrief.."\n- "..debriefAoA +end +if debriefDist and debriefDist~=""then +debrief=debrief.."\n- "..debriefDist +end +if debrief~=""then +self:_AddToDebrief(playerData,debrief) +end +delay=delay or 0 +if not soundoff then +if callAlt then +self:Sound2Player(playerData,self.LSORadio,callAlt,false,delay) +delay=delay+callAlt.duration+0.5 +end +if callSpeed then +self:Sound2Player(playerData,self.LSORadio,callSpeed,false,delay) +delay=delay+callSpeed.duration+0.5 +end +if callAoA then +self:Sound2Player(playerData,self.LSORadio,callAoA,false,delay) +delay=delay+callAoA.duration+0.5 +end +if callDist then +self:Sound2Player(playerData,self.LSORadio,callDist,false,delay) +delay=delay+callDist.duration+0.5 +end +end +if playerData.step==AIRBOSS.PatternStep.ARCIN then +if playerData.difficulty==AIRBOSS.Difficulty.EASY then +local radial=self:GetRadial(playerData.case,true,false,true) +local turn="right" +if self.holdingoffset<0 then +turn="left" +end +hint=hint..string.format("\nTurn %s and select TACAN %03d°.",turn,radial) +end +end +if playerData.step==AIRBOSS.PatternStep.DIRTYUP then +if playerData.difficulty==AIRBOSS.Difficulty.EASY then +if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then +hint=hint.."\nFAF! Checks completed. Nozzles 50°." +else +hint=hint.."\nDirty up! Hook, gear and flaps down." +end +end +end +if playerData.step==AIRBOSS.PatternStep.BULLSEYE then +if playerData.difficulty==AIRBOSS.Difficulty.EASY then +if playerData.actype==AIRBOSS.AircraftCarrier.HORNET then +hint=hint..string.format("\nIntercept glideslope and follow the needles.") +else +hint=hint..string.format("\nIntercept glideslope.") +end +end +end +if hint~=""then +local text=string.format("%s%s",playerData.step,hint) +self:MessageToPlayer(playerData,hint,"AIRBOSS","") +end +end +function AIRBOSS:_StepHint(playerData,step) +step=step or playerData.step +if playerData.difficulty==AIRBOSS.Difficulty.EASY and playerData.showhints then +local alt,aoa,dist,speed=self:_GetAircraftParameters(playerData,step) +local hint="" +if alt then +hint=hint..string.format("\nAltitude %d ft",UTILS.MetersToFeet(alt)) +end +if aoa then +hint=hint..string.format("\nAoA %.1f",self:_AoADeg2Units(playerData,aoa)) +end +if speed then +hint=hint..string.format("\nSpeed %d knots",UTILS.MpsToKnots(speed)) +end +if dist then +hint=hint..string.format("\nDistance to the boat %.1f NM",UTILS.MetersToNM(dist)) +end +if step==AIRBOSS.PatternStep.LATEBREAK then +if playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then +hint=hint.."\nWing Sweep 20°, Gear DOWN < 280 KIAS." +end +end +if step==AIRBOSS.PatternStep.ABEAM then +if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then +hint=hint.."\nNozzles 50°-60°. Antiskid OFF. Lights OFF." +elseif playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then +hint=hint.."\nSlats/Flaps EXTENDED < 225 KIAS. DLC SELECTED. Auto Throttle IF DESIRED." +else +hint=hint.."\nDirty up! Gear DOWN, flaps DOWN. Check hook down." +end +end +if hint~=""then +local text=string.format("Optimal setup at next step %s:%s",step,hint) +self:MessageToPlayer(playerData,text,"AIRBOSS","",nil,false,1) +end +end +end +function AIRBOSS:_AltitudeCheck(playerData,altopt) +if altopt==nil then +return nil,nil +end +local altitude=playerData.unit:GetAltitude() +local lowscore,badscore=self:_GetGoodBadScore(playerData) +local _error=(altitude-altopt)/altopt*100 +local radiocall=nil +local hint="" +if _error>badscore then +radiocall=self:_NewRadioCall(self.LSOCall.HIGH,"Paddles","") +elseif _error>lowscore then +radiocall=self:_NewRadioCall(self.LSOCall.HIGH,"Paddles","") +elseif _error<-badscore then +radiocall=self:_NewRadioCall(self.LSOCall.LOW,"Paddles","") +elseif _error<-lowscore then +radiocall=self:_NewRadioCall(self.LSOCall.LOW,"Paddles","") +else +hint=string.format("Good altitude. ") +end +if playerData.difficulty==AIRBOSS.Difficulty.EASY then +hint=hint..string.format("Optimal altitude is %d ft.",UTILS.MetersToFeet(altopt)) +elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then +hint="" +elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then +hint="" +end +local debrief=string.format("Altitude %d ft = %d%% deviation from %d ft.",UTILS.MetersToFeet(altitude),_error,UTILS.MetersToFeet(altopt)) +return hint,debrief,radiocall +end +function AIRBOSS:_AoACheck(playerData,optaoa) +if optaoa==nil then +return nil,nil +end +local lowscore,badscore=self:_GetGoodBadScore(playerData) +local aoa=playerData.unit:GetAoA() +local _error=(aoa-optaoa)/optaoa*100 +local aircraftaoa=self:_GetAircraftAoA(playerData) +local radiocall=nil +local hint="" +if aoa>=aircraftaoa.SLOW then +radiocall=self:_NewRadioCall(self.LSOCall.SLOW,"Paddles","") +elseif aoa>=aircraftaoa.Slow then +radiocall=self:_NewRadioCall(self.LSOCall.SLOW,"Paddles","") +elseif aoa>=aircraftaoa.OnSpeedMax then +hint="Your're a little slow. " +elseif aoa>=aircraftaoa.OnSpeedMin then +hint="You're on speed. " +elseif aoa>=aircraftaoa.Fast then +hint="You're a little fast. " +elseif aoa>=aircraftaoa.FAST then +radiocall=self:_NewRadioCall(self.LSOCall.FAST,"Paddles","") +else +radiocall=self:_NewRadioCall(self.LSOCall.FAST,"Paddles","") +end +if playerData.difficulty==AIRBOSS.Difficulty.EASY then +hint=hint..string.format("Optimal AoA is %.1f.",self:_AoADeg2Units(playerData,optaoa)) +elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then +hint="" +elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then +hint="" +end +local debrief=string.format("AoA %.1f = %d%% deviation from %.1f.",self:_AoADeg2Units(playerData,aoa),_error,self:_AoADeg2Units(playerData,optaoa)) +return hint,debrief,radiocall +end +function AIRBOSS:_SpeedCheck(playerData,speedopt) +if speedopt==nil then +return nil,nil +end +local speed=playerData.unit:GetVelocityMPS() +local lowscore,badscore=self:_GetGoodBadScore(playerData) +local _error=(speed-speedopt)/speedopt*100 +local radiocall=nil +local hint="" +if _error>badscore then +radiocall=self:_NewRadioCall(self.LSOCall.FAST,"AIRBOSS","") +elseif _error>lowscore then +radiocall=self:_NewRadioCall(self.LSOCall.FAST,"AIRBOSS","") +elseif _error<-badscore then +radiocall=self:_NewRadioCall(self.LSOCall.SLOW,"AIRBOSS","") +elseif _error<-lowscore then +radiocall=self:_NewRadioCall(self.LSOCall.SLOW,"AIRBOSS","") +else +hint=string.format("Good speed. ") +end +if playerData.difficulty==AIRBOSS.Difficulty.EASY then +hint=hint..string.format("Optimal speed is %d knots.",UTILS.MpsToKnots(speedopt)) +elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then +hint="" +elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then +hint="" +end +local debrief=string.format("Speed %d knots = %d%% deviation from %d knots.",UTILS.MpsToKnots(speed),_error,UTILS.MpsToKnots(speedopt)) +return hint,debrief,radiocall +end +function AIRBOSS:_DistanceCheck(playerData,optdist) +if optdist==nil then +return nil,nil +end +local distance=playerData.unit:GetCoordinate():Get2DDistance(self:GetCoordinate()) +local lowscore,badscore=self:_GetGoodBadScore(playerData) +local _error=(distance-optdist)/optdist*100 +local hint +if _error>badscore then +hint=string.format("You're too far from the boat!") +elseif _error>lowscore then +hint=string.format("You're slightly too far from the boat.") +elseif _error<-badscore then +hint=string.format("You're too close to the boat!") +elseif _error<-lowscore then +hint=string.format("You're slightly too far from the boat.") +else +hint=string.format("Good distance to the boat.") +end +if playerData.difficulty==AIRBOSS.Difficulty.EASY then +hint=hint..string.format(" Optimal distance is %.1f NM.",UTILS.MetersToNM(optdist)) +elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then +hint="" +elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then +hint="" +end +local debrief=string.format("Distance %.1f NM = %d%% deviation from %.1f NM.",UTILS.MetersToNM(distance),_error,UTILS.MetersToNM(optdist)) +return hint,debrief,nil +end +function AIRBOSS:_AddToDebrief(playerData,hint,step) +step=step or playerData.step +table.insert(playerData.debrief,{step=step,hint=hint}) +end +function AIRBOSS:_Debrief(playerData) +self:F(self.lid..string.format("Debriefing of player %s.",playerData.name)) +playerData.debriefschedulerID=nil +playerData.attitudemonitor=false +local grade,points,analysis=self:_LSOgrade(playerData) +if points and points>=0 then +table.insert(playerData.points,points) +end +local Points=0 +if playerData.landed and not playerData.unit:InAir()then +for _,_points in pairs(playerData.points)do +Points=Points+_points +end +Points=Points/#playerData.points +playerData.points={} +else +Points=points +end +local mygrade={} +mygrade.grade=grade +mygrade.points=points +mygrade.details=analysis +mygrade.wire=playerData.wire +mygrade.Tgroove=playerData.Tgroove +if playerData.landed and not playerData.unit:InAir()then +mygrade.finalscore=Points +end +mygrade.case=playerData.case +local windondeck=self:GetWindOnDeck() +mygrade.wind=tostring(UTILS.Round(UTILS.MpsToKnots(windondeck),1)) +mygrade.modex=playerData.onboard +mygrade.airframe=playerData.actype +mygrade.carriertype=self.carriertype +mygrade.carriername=self.alias +mygrade.theatre=self.theatre +mygrade.mitime=UTILS.SecondsToClock(timer.getAbsTime()) +mygrade.midate=UTILS.GetDCSMissionDate() +mygrade.osdate="n/a" +if os then +mygrade.osdate=os.date() +end +if playerData.trapon and self.trapsheet then +self:_SaveTrapSheet(playerData,mygrade) +end +table.insert(self.playerscores[playerData.name],mygrade) +self:LSOGrade(playerData,mygrade) +local text=string.format("%s %.1f PT - %s",grade,Points,analysis) +if Points==-1 then +text=string.format("%s n/a PT - Foul deck",grade,Points,analysis) +end +if not(playerData.wop or playerData.wofd)then +if playerData.wire and playerData.wire<=4 then +text=text..string.format(" %d-wire",playerData.wire) +end +if playerData.Tgroove and playerData.Tgroove<=360 and playerData.case<3 then +text=text..string.format("\nTime in the groove %.1f seconds: %s",playerData.Tgroove,self:_EvalGrooveTime(playerData)) +end +end +playerData.lastdebrief=UTILS.DeepCopy(playerData.debrief) +if playerData.difficulty==AIRBOSS.Difficulty.EASY then +text=text..string.format("\nYour detailed debriefing can be found via the F10 radio menu.") +end +self:MessageToPlayer(playerData,text,"LSO","",30,true) +playerData.step=AIRBOSS.PatternStep.UNDEFINED +if playerData.wop then +if playerData.unit:IsAlive()then +local heading,distance +if playerData.case==1 or playerData.case==2 then +playerData.step=AIRBOSS.PatternStep.INITIAL +local initial=self:GetCoordinate():Translate(UTILS.NMToMeters(3.5),self:GetRadial(2,false,false,false)) +heading=playerData.unit:GetCoordinate():HeadingTo(initial) +distance=playerData.unit:GetCoordinate():Get2DDistance(initial) +elseif playerData.case==3 then +playerData.step=AIRBOSS.PatternStep.BULLSEYE +local zone=self:_GetZoneBullseye(playerData.case) +heading=playerData.unit:GetCoordinate():HeadingTo(zone:GetCoordinate()) +distance=playerData.unit:GetCoordinate():Get2DDistance(zone:GetCoordinate()) +end +local text=string.format("fly heading %03d° for %d NM to re-enter the pattern.",heading,UTILS.MetersToNM(distance)) +self:MessageToPlayer(playerData,text,"LSO",nil,nil,false,5) +else +self:E(self.lid..string.format("ERROR: Player unit not alive!")) +end +elseif playerData.wofd then +if playerData.unit:InAir()then +playerData.step=AIRBOSS.PatternStep.BOLTER +else +self:Sound2Player(playerData,self.LSORadio,self.LSOCall.WELCOMEABOARD) +local text=string.format("deck was fouled but you landed anyway. Airboss wants to talk to you!") +self:MessageToPlayer(playerData,text,"LSO",nil,nil,false,3) +end +elseif playerData.owo then +if playerData.unit:InAir()then +playerData.step=AIRBOSS.PatternStep.BOLTER +else +self:E(self.lid.."ERROR: player landed when OWO was issues. This should not happen. Please report!") +self:Sound2Player(playerData,self.LSORadio,self.LSOCall.WELCOMEABOARD) +end +elseif playerData.waveoff then +if playerData.unit:InAir()then +playerData.step=AIRBOSS.PatternStep.BOLTER +else +self:Sound2Player(playerData,self.LSORadio,self.LSOCall.WELCOMEABOARD) +local text=string.format("you were waved off but landed anyway. Airboss wants to talk to you!") +self:MessageToPlayer(playerData,text,"LSO",nil,nil,false,3) +end +elseif playerData.boltered then +if playerData.unit:InAir()then +playerData.step=AIRBOSS.PatternStep.BOLTER +end +elseif playerData.landed then +if not playerData.unit:InAir()then +self:Sound2Player(playerData,self.LSORadio,self.LSOCall.WELCOMEABOARD) +end +else +self:MessageToPlayer(playerData,"Undefined state after landing! Please report.","ERROR",nil,20) +playerData.step=AIRBOSS.PatternStep.UNDEFINED +end +if playerData.landed and not playerData.unit:InAir()then +self:_RecoveredElement(playerData.unit) +self:_CheckSectionRecovered(playerData) +end +playerData.passes=playerData.passes+1 +self:_StepHint(playerData) +self:_InitPlayer(playerData,playerData.step) +MESSAGE:New(string.format("Player step %s.",playerData.step),5,"DEBUG"):ToAllIf(self.Debug) +if self.autosave and mygrade.finalscore then +self:Save(self.autosavepath,self.autosavefile) +end +end +function AIRBOSS:_CheckCollisionCoord(coordto,coordfrom) +local dx=100 +local d=0 +if coordfrom then +d=0 +else +d=250 +coordfrom=self:GetCoordinate():Translate(d,self:GetHeading()) +end +local dmax=coordfrom:Get2DDistance(coordto) +local direction=coordfrom:HeadingTo(coordto) +local clear=true +while d<=dmax do +local cp=coordfrom:Translate(d,direction) +if not cp:IsSurfaceTypeWater()then +if self.Debug then +local st=cp:GetSurfaceType() +cp:MarkToAll(string.format("Collision check surface type %d",st)) +end +clear=false +break +end +d=d+dx +end +local text="" +if clear then +text=string.format("Path into direction %03d° is clear for the next %.1f NM.",direction,UTILS.MetersToNM(d)) +else +text=string.format("Detected obstacle at distance %.1f NM into direction %03d°.",UTILS.MetersToNM(d),direction) +end +self:T2(self.lid..text) +return not clear,d +end +function AIRBOSS:_CheckFreePathToNextWP(fromcoord) +fromcoord=fromcoord or self:GetCoordinate():Translate(250,self:GetHeading()) +local Nnextwp=math.min(self.currentwp+1,#self.waypoints) +local nextwp=self.waypoints[Nnextwp] +local collision=self:_CheckCollisionCoord(nextwp,fromcoord) +return collision +end +function AIRBOSS:_Pathfinder() +local hdg=self:GetHeading() +local cv=self:GetCoordinate() +local directions={-20,20,-30,30,-40,40,-50,50,-60,60,-70,70,-80,80,-90,90,-100,100} +for _,_direction in pairs(directions)do +local direction=hdg+_direction +local _,dfree=self:_CheckCollisionCoord(cv:Translate(UTILS.NMToMeters(20),direction),cv) +local distance=500 +while distance<=dfree do +local fromcoord=cv:Translate(distance,direction) +local collision=self:_CheckFreePathToNextWP(fromcoord) +self:T2(self.lid..string.format("Pathfinder d=%.1f m, direction=%03d°, collision=%s",distance,direction,tostring(collision))) +if not collision then +self:CarrierDetour(fromcoord) +return +end +distance=distance+500 +end +end +end +function AIRBOSS:CarrierResumeRoute(gotocoord) +AIRBOSS._ResumeRoute(self.carrier:GetGroup(),self,gotocoord) +return self +end +function AIRBOSS:CarrierDetour(coord,speed,uturn,uspeed,tcoord) +local pos0=self:GetCoordinate() +local vel0=self.carrier:GetVelocityKNOTS() +speed=speed or math.max(vel0,5) +local speedkmh=math.max(UTILS.KnotsToKmph(speed),UTILS.KnotsToKmph(2)) +local cspeedkmh=math.max(self.carrier:GetVelocityKMH(),UTILS.KnotsToKmph(10)) +local uspeedkmh=UTILS.KnotsToKmph(uspeed or speed) +local wp={} +table.insert(wp,pos0:WaypointGround(cspeedkmh)) +if tcoord then +table.insert(wp,tcoord:WaypointGround(cspeedkmh)) +end +table.insert(wp,coord:WaypointGround(speedkmh)) +if uturn then +table.insert(wp,pos0:WaypointGround(uspeedkmh)) +end +local group=self.carrier:GetGroup() +local TaskResumeRoute=group:TaskFunction("AIRBOSS._ResumeRoute",self) +group:SetTaskWaypoint(wp[#wp],TaskResumeRoute) +if self.Debug then +if tcoord then +tcoord:MarkToAll(string.format("Detour Turn Help WP. Speed %.1f knots",UTILS.KmphToKnots(cspeedkmh))) +end +coord:MarkToAll(string.format("Detour Waypoint. Speed %.1f knots",UTILS.KmphToKnots(speedkmh))) +if uturn then +pos0:MarkToAll(string.format("Detour U-turn WP. Speed %.1f knots",UTILS.KmphToKnots(uspeedkmh))) +end +end +self.detour=true +self.carrier:Route(wp) +end +function AIRBOSS:CarrierTurnIntoWind(time,vdeck,uturn) +local _,vwind=self:GetWind() +local vtot=math.max(vdeck-vwind,UTILS.KnotsToMps(2)) +local dist=vtot*time +local speedknots=UTILS.MpsToKnots(vtot) +local distNM=UTILS.MetersToNM(dist) +self:I(self.lid..string.format("Carrier steaming into the wind (%.1f kts). Distance=%.1f NM, Speed=%.1f knots, Time=%d sec.",UTILS.MpsToKnots(vwind),distNM,speedknots,time)) +local hiw=self:GetHeadingIntoWind() +local hdg=self:GetHeading() +local deltaH=self:_GetDeltaHeading(hdg,hiw) +local Cv=self:GetCoordinate() +local Ctiw=nil +local Csoo=nil +if deltaH<45 then +Csoo=Cv:Translate(750,hdg):Translate(750,hiw) +local hsw=self:GetHeadingIntoWind(false,Csoo) +Ctiw=Csoo:Translate(dist,hsw) +elseif deltaH<90 then +Csoo=Cv:Translate(900,hdg):Translate(900,hiw) +local hsw=self:GetHeadingIntoWind(false,Csoo) +Ctiw=Csoo:Translate(dist,hsw) +elseif deltaH<135 then +Csoo=Cv:Translate(1100,hdg-90):Translate(1000,hiw) +local hsw=self:GetHeadingIntoWind(false,Csoo) +Ctiw=Csoo:Translate(dist,hsw) +else +Csoo=Cv:Translate(1200,hdg-90):Translate(1000,hiw) +local hsw=self:GetHeadingIntoWind(false,Csoo) +Ctiw=Csoo:Translate(dist,hsw) +end +self.Creturnto=self:GetCoordinate() +local nextwp=self:_GetNextWaypoint() +local vdownwind=UTILS.MpsToKnots(nextwp:GetVelocity()) +if vdownwind<1 then +vdownwind=10 +end +self:CarrierDetour(Ctiw,speedknots,uturn,vdownwind,Csoo) +self.turnintowind=true +return self +end +function AIRBOSS:_GetNextWaypoint() +local Nextwp=nil +if self.currentwp==#self.waypoints then +Nextwp=1 +else +Nextwp=self.currentwp+1 +end +local text=string.format("Current WP=%d/%d, next WP=%d",self.currentwp,#self.waypoints,Nextwp) +self:T2(self.lid..text) +local nextwp=self.waypoints[Nextwp] +return nextwp,Nextwp +end +function AIRBOSS:_InitWaypoints() +local Waypoints=self.carrier:GetGroup():GetTemplateRoutePoints() +self.waypoints={} +for i,point in ipairs(Waypoints)do +local coord=COORDINATE:New(point.x,point.alt,point.y) +coord:SetVelocity(point.speed) +table.insert(self.waypoints,coord) +if self.Debug then +coord:MarkToAll(string.format("Carrier Waypoint %d, Speed=%.1f knots",i,UTILS.MpsToKnots(point.speed))) +end +end +return self +end +function AIRBOSS:_PatrolRoute(n) +local nextWP,N=self:_GetNextWaypoint() +n=n or N +local CarrierGroup=self.carrier:GetGroup() +local Waypoints={} +local wp=self:GetCoordinate():WaypointGround(CarrierGroup:GetVelocityKMH()) +table.insert(Waypoints,wp) +for i=n,#self.waypoints do +local coord=self.waypoints[i] +local wp=coord:WaypointGround(UTILS.MpsToKmph(coord.Velocity)) +local TaskPassingWP=CarrierGroup:TaskFunction("AIRBOSS._PassingWaypoint",self,i,#self.waypoints) +CarrierGroup:SetTaskWaypoint(wp,TaskPassingWP) +table.insert(Waypoints,wp) +end +CarrierGroup:Route(Waypoints) +return self +end +function AIRBOSS:_GetETAatNextWP() +local cwp=self.currentwp +local tnow=timer.getAbsTime() +local p=self:GetCoordinate() +local v=self.carrier:GetVelocityMPS() +local nextWP=self:_GetNextWaypoint() +local s=p:Get2DDistance(nextWP) +local t=s/v +local eta=t+tnow +return eta +end +function AIRBOSS:_CheckCarrierTurning() +local vNew=self.carrier:GetOrientationX() +local vLast=self.Corientlast +vNew.y=0;vLast.y=0 +local deltaLast=math.deg(math.acos(UTILS.VecDot(vNew,vLast)/UTILS.VecNorm(vNew)/UTILS.VecNorm(vLast))) +self.Corientlast=vNew +local turning=math.abs(deltaLast)>=1 +if self.turning and not turning then +local FB=self:GetFinalBearing(true) +self:_MarshalCallNewFinalBearing(FB) +end +if turning and not self.turning then +local hdg +if self.turnintowind then +hdg=self:GetHeadingIntoWind(false) +else +hdg=self:GetCoordinate():HeadingTo(self:_GetNextWaypoint()) +end +hdg=hdg-self.magvar +if hdg<0 then +hdg=360+hdg +end +self:_MarshalCallCarrierTurnTo(hdg) +end +self.turning=turning +end +function AIRBOSS:_CheckPatternUpdate() +local dTPupdate=10*60 +local Dupdate=UTILS.NMToMeters(2.5) +local Hupdate=5 +local dt=timer.getTime()-self.Tpupdate +if dt=Hupdate then +self:T(self.lid..string.format("Carrier heading changed by %d°.",deltaHeading)) +Hchange=true +end +local pos=self:GetCoordinate() +local dist=pos:Get2DDistance(self.Cposition) +local Dchange=false +if dist>=Dupdate then +self:T(self.lid..string.format("Carrier position changed by %.1f NM.",UTILS.MetersToNM(dist))) +Dchange=true +end +if Hchange or Dchange then +for _,_flight in pairs(self.Qmarshal)do +local flight=_flight +if flight.ai then +self:_MarshalAI(flight,flight.flag) +end +end +self.Corientation=vNew +self.Cposition=pos +self.Tpupdate=timer.getTime() +end +end +function AIRBOSS._PassingWaypoint(group,airboss,i,final) +local text=string.format("Group %s passing waypoint %d of %d.",group:GetName(),i,final) +if airboss.Debug and false then +local pos=group:GetCoordinate() +pos:SmokeRed() +local MarkerID=pos:MarkToAll(string.format("Group %s reached waypoint %d",group:GetName(),i)) +end +MESSAGE:New(text,10):ToAllIf(airboss.Debug) +airboss:T(airboss.lid..text) +airboss.currentwp=i +airboss:PassingWaypoint(i) +if i==final and final>1 and airboss.adinfinitum then +airboss:_PatrolRoute() +end +end +function AIRBOSS._ResumeRoute(group,airboss,gotocoord) +local nextwp,Nextwp=airboss:_GetNextWaypoint() +local speedkmh=nextwp.Velocity*3.6 +if speedkmh<1 then +speedkmh=UTILS.KnotsToKmph(10) +end +local waypoints={} +local c0=group:GetCoordinate() +local wp0=c0:WaypointGround(speedkmh) +table.insert(waypoints,wp0) +if gotocoord then +local headingto=c0:HeadingTo(gotocoord) +local hdg1=airboss:GetHeading() +local hdg2=c0:HeadingTo(gotocoord) +local delta=airboss:_GetDeltaHeading(hdg1,hdg2) +if delta>90 then +local turnradius=UTILS.NMToMeters(3) +local gotocoordh=c0:Translate(turnradius,hdg1+45) +local wp=gotocoordh:WaypointGround(speedkmh) +table.insert(waypoints,wp) +gotocoordh=c0:Translate(turnradius,hdg1+90) +wp=gotocoordh:WaypointGround(speedkmh) +table.insert(waypoints,wp) +end +local wp1=gotocoord:WaypointGround(speedkmh) +table.insert(waypoints,wp1) +end +local text=string.format("Carrier is resuming route. Next waypoint %d, Speed=%.1f knots.",Nextwp,UTILS.KmphToKnots(speedkmh)) +MESSAGE:New(text,10):ToAllIf(airboss.Debug) +airboss:I(airboss.lid..text) +for i=Nextwp,#airboss.waypoints do +local coord=airboss.waypoints[i] +local speed=coord.Velocity*3.6 +if speed<1 then +speed=UTILS.KnotsToKmph(10) +end +local wp=coord:WaypointGround(speed) +local TaskPassingWP=group:TaskFunction("AIRBOSS._PassingWaypoint",airboss,i,#airboss.waypoints) +group:SetTaskWaypoint(wp,TaskPassingWP) +table.insert(waypoints,wp) +end +airboss.turnintowind=false +airboss.detour=false +group:Route(waypoints) +end +function AIRBOSS._ReachedHoldingZone(group,airboss,flight) +local text=string.format("Flight %s reached holding zone.",group:GetName()) +MESSAGE:New(text,10):ToAllIf(airboss.Debug) +airboss:T(airboss.lid..text) +if airboss.Debug then +group:GetCoordinate():MarkToAll(text) +end +if flight then +flight.holding=true +flight.time=timer.getAbsTime() +end +end +function AIRBOSS._TaskFunctionMarshalAI(group,airboss,flight) +local text=string.format("Flight %s is send to marshal.",group:GetName()) +MESSAGE:New(text,10):ToAllIf(airboss.Debug) +airboss:T(airboss.lid..text) +local stack=airboss:_GetFreeStack(flight.ai) +if stack then +airboss:_MarshalAI(flight,stack) +else +if not airboss:_InQueue(airboss.Qwaiting,flight.group)then +airboss:_WaitAI(flight) +end +end +if flight.refueling==true then +airboss:I(airboss.lid..string.format("Flight group %s finished refueling task.",flight.groupname)) +end +flight.refueling=false +end +function AIRBOSS:_GetACNickname(actype) +local nickname="unknown" +if actype==AIRBOSS.AircraftCarrier.A4EC then +nickname="Skyhawk" +elseif actype==AIRBOSS.AircraftCarrier.T45C then +nickname="Goshawk" +elseif actype==AIRBOSS.AircraftCarrier.AV8B then +nickname="Harrier" +elseif actype==AIRBOSS.AircraftCarrier.E2D then +nickname="Hawkeye" +elseif actype==AIRBOSS.AircraftCarrier.F14A_AI or actype==AIRBOSS.AircraftCarrier.F14A or actype==AIRBOSS.AircraftCarrier.F14B then +nickname="Tomcat" +elseif actype==AIRBOSS.AircraftCarrier.FA18C or actype==AIRBOSS.AircraftCarrier.HORNET then +nickname="Hornet" +elseif actype==AIRBOSS.AircraftCarrier.S3B or actype==AIRBOSS.AircraftCarrier.S3BTANKER then +nickname="Viking" +end +return nickname +end +function AIRBOSS:_GetOnboardNumberPlayer(group) +return self:_GetOnboardNumbers(group,true) +end +function AIRBOSS:_GetOnboardNumbers(group,playeronly) +local groupname=group:GetName() +local text=string.format("Onboard numbers of group %s:",groupname) +local units=group:GetTemplate().units +local numbers={} +for _,unit in pairs(units)do +local n=tostring(unit.onboard_num) +local name=unit.name +local skill=unit.skill or"Unknown" +text=text..string.format("\n- unit %s: onboard #=%s skill=%s",name,n,tostring(skill)) +if playeronly and skill=="Client"or skill=="Player"then +return n +end +numbers[name]=n +end +self:T2(self.lid..text) +return numbers +end +function AIRBOSS:_GetTowerFrequency() +self.TowerFreq=0 +local striketemplate=self.carrier:GetGroup():GetTemplate() +for _,unit in pairs(striketemplate.units)do +if self.carrier:GetName()==unit.name then +self.TowerFreq=unit.frequency/1000000 +return +end +end +end +function AIRBOSS:_GetGoodBadScore(playerData) +local lowscore +local badscore +if playerData.difficulty==AIRBOSS.Difficulty.EASY then +lowscore=10 +badscore=20 +elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then +lowscore=5 +badscore=10 +elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then +lowscore=2.5 +badscore=5 +end +return lowscore,badscore +end +function AIRBOSS:_IsCarrierAircraft(unit) +local aircrafttype=unit:GetTypeName() +if aircrafttype==AIRBOSS.AircraftCarrier.AV8B then +if self.carriertype==AIRBOSS.CarrierType.TARAWA then +return true +else +return false +end +end +if self.carriertype==AIRBOSS.CarrierType.TARAWA then +if aircrafttype~=AIRBOSS.AircraftCarrier.AV8B then +return false +end +end +for _,actype in pairs(AIRBOSS.AircraftCarrier)do +if actype==aircrafttype then +return true +end +end +return false +end +function AIRBOSS:_IsHumanUnit(unit) +local playerunit=self:_GetPlayerUnitAndName(unit:GetName()) +if playerunit then +return true +else +return false +end +end +function AIRBOSS:_IsHuman(group) +local units=group:GetUnits() +for _,_unit in pairs(units)do +local human=self:_IsHumanUnit(_unit) +if human then +return true +end +end +return false +end +function AIRBOSS:_GetFuelState(unit) +local fuel=unit:GetFuel() +local maxfuel=self:_GetUnitMasses(unit) +local fuelstate=fuel*maxfuel +self:T2(self.lid..string.format("Unit %s fuel state = %.1f kg = %.1f lbs",unit:GetName(),fuelstate,UTILS.kg2lbs(fuelstate))) +return UTILS.kg2lbs(fuelstate) +end +function AIRBOSS:_GetAngels(alt) +if alt then +local angels=UTILS.Round(UTILS.MetersToFeet(alt)/1000,0) +return angels +else +return 0 +end +end +function AIRBOSS:_GetUnitMasses(unit) +local Desc=unit:GetDesc() +local massfuel=Desc.fuelMassMax or 0 +local massempty=Desc.massEmpty or 0 +local massmax=Desc.massMax or 0 +local masscargo=massmax-massfuel-massempty +self:T2(self.lid..string.format("Unit %s mass fuel=%.1f kg, empty=%.1f kg, max=%.1f kg, cargo=%.1f kg",unit:GetName(),massfuel,massempty,massmax,masscargo)) +return massfuel,massempty,massmax,masscargo +end +function AIRBOSS:_GetPlayerDataUnit(unit) +if unit:IsAlive()then +local unitname=unit:GetName() +local playerunit,playername=self:_GetPlayerUnitAndName(unitname) +if playerunit and playername then +return self.players[playername] +end +end +return nil +end +function AIRBOSS:_GetPlayerDataGroup(group) +local units=group:GetUnits() +for _,unit in pairs(units)do +local playerdata=self:_GetPlayerDataUnit(unit) +if playerdata then +return playerdata +end +end +return nil +end +function AIRBOSS:_GetPlayerUnit(_unitName) +for _,_player in pairs(self.players)do +local player=_player +if player.unit and player.unit:GetName()==_unitName then +self:T(self.lid..string.format("Found player=%s unit=%s in players table.",tostring(player.name),tostring(_unitName))) +return player.unit,player.name +end +end +return nil,nil +end +function AIRBOSS:_GetPlayerUnitAndName(_unitName) +self:F2(_unitName) +if _unitName~=nil then +local u,pn=self:_GetPlayerUnit(_unitName) +if u and pn then +return u,pn +end +local DCSunit=Unit.getByName(_unitName) +if DCSunit then +local playername=DCSunit:getPlayerName() +local unit=UNIT:Find(DCSunit) +self:T2({DCSunit=DCSunit,unit=unit,playername=playername}) +if DCSunit and unit and playername then +self:T(self.lid..string.format("Found DCS unit %s with player %s.",tostring(_unitName),tostring(playername))) +return unit,playername +end +end +end +return nil,nil +end +function AIRBOSS:GetCoalition() +return self.carrier:GetCoalition() +end +function AIRBOSS:GetCoordinate() +return self.carrier:GetCoord() +end +function AIRBOSS:GetCoord() +return self.carrier:GetCoord() +end +function AIRBOSS:_GetStaticWeather() +local weather=env.mission.weather +local clouds=weather.clouds +local visibility=weather.visibility.distance +local dust=nil +if weather.enable_dust==true then +dust=weather.dust_density +end +local fog=nil +if weather.enable_fog==true then +fog=weather.fog +end +return clouds,visibility,fog,dust +end +function AIRBOSS._CheckRadioQueueT(param,time) +AIRBOSS._CheckRadioQueue(param.airboss,param.radioqueue,param.name) +return time+0.05 +end +function AIRBOSS:_CheckRadioQueue(radioqueue,name) +if#radioqueue==0 then +if name=="LSO"then +self:T(self.lid..string.format("Stopping LSO radio queue.")) +self.radiotimer:Stop(self.RQLid) +self.RQLid=nil +elseif name=="MARSHAL"then +self:T(self.lid..string.format("Stopping Marshal radio queue.")) +self.radiotimer:Stop(self.RQMid) +self.RQMid=nil +end +return +end +local _time=timer.getAbsTime() +local playing=false +local next=nil +local _remove=nil +for i,_transmission in ipairs(radioqueue)do +local transmission=_transmission +if _time>=transmission.Tplay then +if transmission.isplaying then +if _time>=transmission.Tstarted+transmission.call.duration then +transmission.isplaying=false +_remove=i +if transmission.radio.alias=="LSO"then +self.TQLSO=_time +elseif transmission.radio.alias=="MARSHAL"then +self.TQMarshal=_time +end +else +playing=true +end +else +local Tlast=nil +if transmission.interval then +if transmission.radio.alias=="LSO"then +Tlast=self.TQLSO +elseif transmission.radio.alias=="MARSHAL"then +Tlast=self.TQMarshal +end +end +if transmission.interval==nil then +if next==nil then +next=transmission +end +else +if _time-Tlast>=transmission.interval then +next=transmission +else +end +end +if next or Tlast then +break +end +end +else +end +end +if next~=nil and not playing then +self:Broadcast(next.radio,next.call,next.loud) +next.isplaying=true +next.Tstarted=_time +end +if _remove then +table.remove(radioqueue,_remove) +end +return +end +function AIRBOSS:RadioTransmission(radio,call,loud,delay,interval,click,pilotcall) +self:F2({radio=radio,call=call,loud=loud,delay=delay,interval=interval,click=click}) +if radio==nil or call==nil then +return +end +local transmission={} +transmission.radio=radio +transmission.call=call +transmission.Tplay=timer.getAbsTime()+(delay or 0) +transmission.interval=interval +transmission.isplaying=false +transmission.Tstarted=nil +transmission.loud=loud and call.loud +if self:_IsOnboard(call.modexsender)then +self:_Number2Radio(radio,call.modexsender,delay,0.3,pilotcall) +end +if self:_IsOnboard(call.modexreceiver)then +self:_Number2Radio(radio,call.modexreceiver,delay,0.3,pilotcall) +end +local caller="" +if radio.alias=="LSO"then +table.insert(self.RQLSO,transmission) +caller="LSOCall" +if not self.RQLid then +self:T(self.lid..string.format("Starting LSO radio queue.")) +self.RQLid=self.radiotimer:Schedule(nil,AIRBOSS._CheckRadioQueue,{self,self.RQLSO,"LSO"},0.02,0.05) +end +elseif radio.alias=="MARSHAL"then +table.insert(self.RQMarshal,transmission) +caller="MarshalCall" +if not self.RQMid then +self:T(self.lid..string.format("Starting Marhal radio queue.")) +self.RQMid=self.radiotimer:Schedule(nil,AIRBOSS._CheckRadioQueue,{self,self.RQMarshal,"MARSHAL"},0.02,0.05) +end +end +if click then +self:RadioTransmission(radio,self[caller].CLICK,false,delay) +end +end +function AIRBOSS:_NeedsSubtitle(call) +if call.file==self.MarshalCall.NOISE.file or call.file==self.LSOCall.NOISE.file then +return true +else +return false +end +end +function AIRBOSS:Broadcast(radio,call,loud) +self:F(call) +if not self.usersoundradio then +local sender=self:_GetRadioSender(radio) +local filename=self:_RadioFilename(call,loud,radio.alias) +local subtitle=self:_RadioSubtitle(radio,call,loud) +self:T({filename=filename,subtitle=subtitle}) +if sender then +self:T(self.lid..string.format("Broadcasting from aircraft %s",sender:GetName())) +local commandFrequency={ +id="SetFrequency", +params={ +frequency=radio.frequency*1000000, +modulation=radio.modulation, +}} +local commandTransmit={ +id="TransmitMessage", +params={ +file=filename, +duration=call.subduration or 5, +subtitle=subtitle, +loop=false, +}} +sender:SetCommand(commandFrequency) +sender:SetCommand(commandTransmit) +else +self:T(self.lid..string.format("Broadcasting from carrier via trigger.action.radioTransmission().")) +local vec3=self.carrier:GetPositionVec3() +trigger.action.radioTransmission(filename,vec3,radio.modulation,false,radio.frequency*1000000,100) +for _,_player in pairs(self.players)do +local playerData=_player +if playerData.unit:IsInZone(self.zoneCCA)and playerData.actype~=AIRBOSS.AircraftCarrier.A4EC then +if playerData.subtitles or self:_NeedsSubtitle(call)then +if radio.alias=="MARSHAL"or(radio.alias=="LSO"and self:_InQueue(self.Qpattern,playerData.group))then +self:MessageToPlayer(playerData,subtitle,nil,"",call.subduration or 5) +end +end +end +end +end +end +for _,_player in pairs(self.players)do +local playerData=_player +if self.usersoundradio or playerData.actype==AIRBOSS.AircraftCarrier.A4EC then +if radio.alias=="MARSHAL"or(radio.alias=="LSO"and self:_InQueue(self.Qpattern,playerData.group))then +self:Sound2Player(playerData,radio,call,loud) +end +end +end +end +function AIRBOSS:Sound2Player(playerData,radio,call,loud,delay) +if playerData.unit:IsInZone(self.zoneCCA)and call then +local filename=self:_RadioFilename(call,loud,radio.alias) +local subtitle=self:_RadioSubtitle(radio,call,loud) +USERSOUND:New(filename):ToGroup(playerData.group,delay) +if playerData.subtitles or self:_NeedsSubtitle(call)then +self:MessageToPlayer(playerData,subtitle,nil,"",call.subduration,false,delay) +end +end +end +function AIRBOSS:_RadioSubtitle(radio,call,loud) +if call==nil or call.subtitle==nil or call.subtitle==""then +return"" +end +local sender=call.sender or radio.alias +if call.modexsender then +sender=call.modexsender +end +local receiver=call.modexreceiver or"" +local subtitle=string.format("%s: %s",sender,call.subtitle) +if receiver and receiver~=""then +subtitle=string.format("%s: %s, %s",sender,receiver,call.subtitle) +end +local lastchar=string.sub(subtitle,-1) +if loud then +if lastchar=="."or lastchar=="!"then +subtitle=string.sub(subtitle,1,-1) +end +subtitle=subtitle.."!" +else +if lastchar=="!"then +elseif lastchar=="."then +else +subtitle=subtitle.."." +end +end +return subtitle +end +function AIRBOSS:_RadioFilename(call,loud,channel) +local prefix=call.file or"" +local suffix=call.suffix or"ogg" +local path=self.soundfolder or"l10n/DEFAULT/" +if string.find(call.file,"LSO-")and channel and(channel=="LSO"or channel=="LSOCall")then +path=self.soundfolderLSO or path +end +if string.find(call.file,"MARSHAL-")and channel and(channel=="MARSHAL"or channel=="MarshalCall")then +path=self.soundfolderMSH or path +end +if loud then +prefix=prefix.."_Loud" +end +local filename=string.format("%s%s.%s",path,prefix,suffix) +return filename +end +function AIRBOSS:MessageToPlayer(playerData,message,sender,receiver,duration,clear,delay) +if playerData and message and message~=""then +duration=duration or self.Tmessage +local text +if receiver and receiver==""then +text=string.format("%s",message) +else +receiver=receiver or playerData.onboard +text=string.format("%s, %s",receiver,message) +end +self:T(self.lid..text) +if delay and delay>0 then +self:ScheduleOnce(delay,self.MessageToPlayer,self,playerData,message,sender,receiver,duration,clear) +else +local wait=0 +if receiver==playerData.onboard then +if sender and(sender=="LSO"or sender=="MARSHAL"or sender=="AIRBOSS")then +wait=wait+self:_Number2Sound(playerData,sender,receiver) +end +end +if string.find(text:lower(),"negative")then +local filename=self:_RadioFilename(self.MarshalCall.NEGATIVE,false,"MARSHAL") +USERSOUND:New(filename):ToGroup(playerData.group,wait) +wait=wait+self.MarshalCall.NEGATIVE.duration +end +if string.find(text:lower(),"affirm")then +local filename=self:_RadioFilename(self.MarshalCall.AFFIRMATIVE,false,"MARSHAL") +USERSOUND:New(filename):ToGroup(playerData.group,wait) +wait=wait+self.MarshalCall.AFFIRMATIVE.duration +end +if string.find(text:lower(),"roger")then +local filename=self:_RadioFilename(self.MarshalCall.ROGER,false,"MARSHAL") +USERSOUND:New(filename):ToGroup(playerData.group,wait) +wait=wait+self.MarshalCall.ROGER.duration +end +if wait>0 then +local filename=self:_RadioFilename(self.MarshalCall.CLICK) +USERSOUND:New(filename):ToGroup(playerData.group,wait) +end +if playerData.client then +MESSAGE:New(text,duration,sender,clear):ToClient(playerData.client) +end +end +end +end +function AIRBOSS:MessageToPattern(message,sender,receiver,duration,clear,delay) +local call=self:_NewRadioCall(self.LSOCall.NOISE,sender or"LSO",message,duration,receiver,sender) +self:RadioTransmission(self.LSORadio,call,false,delay,nil,true) +end +function AIRBOSS:MessageToMarshal(message,sender,receiver,duration,clear,delay) +local call=self:_NewRadioCall(self.MarshalCall.NOISE,sender or"MARSHAL",message,duration,receiver,sender) +self:RadioTransmission(self.MarshalRadio,call,false,delay,nil,true) +end +function AIRBOSS:_NewRadioCall(call,sender,subtitle,subduration,modexreceiver,modexsender) +local newcall=UTILS.DeepCopy(call) +newcall.sender=sender +newcall.subtitle=subtitle or call.subtitle +newcall.subduration=subduration or self.Tmessage +if self:_IsOnboard(modexreceiver)then +newcall.modexreceiver=modexreceiver +end +if self:_IsOnboard(modexsender)then +newcall.modexsender=modexsender +end +return newcall +end +function AIRBOSS:_GetRadioSender(radio) +local sender=nil +if self.senderac then +sender=UNIT:FindByName(self.senderac) +end +if radio.alias=="MARSHAL"then +if self.radiorelayMSH then +sender=UNIT:FindByName(self.radiorelayMSH) +end +end +if radio.alias=="LSO"then +if self.radiorelayLSO then +sender=UNIT:FindByName(self.radiorelayLSO) +end +end +if sender and sender:IsAlive()and sender:IsAir()then +return sender +end +return nil +end +function AIRBOSS:_IsOnboard(text) +if text==nil then +return false +end +if text=="99"then +return true +end +for _,_flight in pairs(self.flights)do +local flight=_flight +for _,onboard in pairs(flight.onboardnumbers)do +if text==onboard then +return true +end +end +end +return false +end +function AIRBOSS:_Number2Sound(playerData,sender,number,delay) +delay=delay or 0 +local function _split(str) +local chars={} +for i=1,#str do +local c=str:sub(i,i) +table.insert(chars,c) +end +return chars +end +local Sender +if sender=="LSO"then +Sender="LSOCall" +elseif sender=="MARSHAL"or sender=="AIRBOSS"then +Sender="MarshalCall" +else +self:E(self.lid..string.format("ERROR: Unknown radio sender %s!",tostring(sender))) +return +end +local numbers=_split(number) +local wait=0 +for i=1,#numbers do +local n=numbers[i] +local N=string.format("N%s",n) +local call=self[Sender][N] +local filename=self:_RadioFilename(call,false,Sender) +USERSOUND:New(filename):ToGroup(playerData.group,delay+wait) +wait=wait+call.duration +end +return wait +end +function AIRBOSS:_Number2Radio(radio,number,delay,interval,pilotcall) +local function _split(str) +local chars={} +for i=1,#str do +local c=str:sub(i,i) +table.insert(chars,c) +end +return chars +end +local Sender="" +if radio.alias=="LSO"then +Sender="LSOCall" +elseif radio.alias=="MARSHAL"then +Sender="MarshalCall" +else +self:E(self.lid..string.format("ERROR: Unknown radio alias %s!",tostring(radio.alias))) +end +if pilotcall then +Sender="PilotCall" +end +local numbers=_split(number) +local wait=0 +for i=1,#numbers do +local n=numbers[i] +local N=string.format("N%s",n) +local call=self[Sender][N] +if interval and i==1 then +self:RadioTransmission(radio,call,false,delay,interval) +else +self:RadioTransmission(radio,call,false,delay) +end +wait=wait+call.duration +end +return wait +end +function AIRBOSS:_LSOCallAircraftBall(modex,nickname,fuelstate) +local text=string.format("%s Ball, %.1f.",nickname,fuelstate) +self:I(self.lid..text) +local NICKNAME=nickname:upper() +local FS=UTILS.Split(string.format("%.1f",fuelstate),".") +local call=self:_NewRadioCall(self.PilotCall[NICKNAME],modex,text,self.Tmessage,nil,modex) +self:RadioTransmission(self.LSORadio,call,nil,nil,nil,nil,true) +self:RadioTransmission(self.LSORadio,self.PilotCall.BALL,nil,nil,nil,nil,true) +self:_Number2Radio(self.LSORadio,FS[1],nil,nil,true) +self:RadioTransmission(self.LSORadio,self.PilotCall.POINT,nil,nil,nil,nil,true) +self:_Number2Radio(self.LSORadio,FS[2],nil,nil,true) +self:RadioTransmission(self.LSORadio,self.LSOCall.CLICK) +end +function AIRBOSS:_MarshalCallGasAtTanker(modex) +local text=string.format("Bingo fuel! Going for gas at the recovery tanker.") +self:I(self.lid..text) +local call=self:_NewRadioCall(self.PilotCall.BINGOFUEL,modex,text,self.Tmessage,nil,modex) +self:RadioTransmission(self.MarshalRadio,call,nil,nil,nil,nil,true) +self:RadioTransmission(self.MarshalRadio,self.PilotCall.GASATTANKER,nil,nil,nil,true,true) +end +function AIRBOSS:_MarshalCallGasAtDivert(modex,divertname) +local text=string.format("Bingo fuel! Going for gas at divert field %s.",divertname) +self:I(self.lid..text) +local call=self:_NewRadioCall(self.PilotCall.BINGOFUEL,modex,text,self.Tmessage,nil,modex) +self:RadioTransmission(self.MarshalRadio,call,nil,nil,nil,nil,true) +self:RadioTransmission(self.MarshalRadio,self.PilotCall.GASATDIVERT,nil,nil,nil,true,true) +end +function AIRBOSS:_MarshalCallRecoveryStopped(case) +local text=string.format("Case %d recovery ops are stopped. Deck is closed.",case) +self:I(self.lid..text) +local call=self:_NewRadioCall(self.MarshalCall.CASE,"AIRBOSS",text,self.Tmessage,"99") +self:RadioTransmission(self.MarshalRadio,call) +self:_Number2Radio(self.MarshalRadio,tostring(case)) +self:RadioTransmission(self.MarshalRadio,self.MarshalCall.RECOVERYOPSSTOPPED,nil,nil,0.2) +self:RadioTransmission(self.MarshalRadio,self.MarshalCall.DECKCLOSED,nil,nil,nil,true) +end +function AIRBOSS:_MarshalCallRecoveryPausedUntilFurtherNotice() +local call=self:_NewRadioCall(self.MarshalCall.RECOVERYPAUSEDNOTICE,"AIRBOSS",nil,self.Tmessage,"99") +self:RadioTransmission(self.MarshalRadio,call,nil,nil,nil,true) +end +function AIRBOSS:_MarshalCallRecoveryPausedResumedAt(clock) +local _clock=UTILS.Split(clock,"+") +local CT=UTILS.Split(_clock[1],":") +local text=string.format("aircraft recovery is paused and will be resumed at %s.",clock) +self:I(self.lid..text) +local call=self:_NewRadioCall(self.MarshalCall.RECOVERYPAUSEDRESUMED,"AIRBOSS",text,self.Tmessage,"99") +self:RadioTransmission(self.MarshalRadio,call) +self:_Number2Radio(self.MarshalRadio,CT[1]) +self:_Number2Radio(self.MarshalRadio,CT[2]) +self:RadioTransmission(self.MarshalRadio,self.MarshalCall.HOURS,nil,nil,nil,true) +end +function AIRBOSS:_MarshalCallClearedForRecovery(modex,case) +local text=string.format("you're cleared for Case %d recovery.",case) +self:I(self.lid..text) +local call=self:_NewRadioCall(self.MarshalCall.CLEAREDFORRECOVERY,"MARSHAL",text,self.Tmessage,modex) +local delay=2 +self:RadioTransmission(self.MarshalRadio,call,nil,delay) +self:_Number2Radio(self.MarshalRadio,tostring(case),delay) +self:RadioTransmission(self.MarshalRadio,self.MarshalCall.RECOVERY,nil,delay,nil,true) +end +function AIRBOSS:_MarshalCallResumeRecovery() +local call=self:_NewRadioCall(self.MarshalCall.RESUMERECOVERY,"AIRBOSS",nil,self.Tmessage,"99") +self:RadioTransmission(self.MarshalRadio,call,nil,nil,nil,true) +end +function AIRBOSS:_MarshalCallNewFinalBearing(FB) +local text=string.format("new final bearing %03d°.",FB) +self:I(self.lid..text) +local call=self:_NewRadioCall(self.MarshalCall.NEWFB,"AIRBOSS",text,self.Tmessage,"99") +self:RadioTransmission(self.MarshalRadio,call) +self:_Number2Radio(self.MarshalRadio,string.format("%03d",FB),nil,0.2) +self:RadioTransmission(self.MarshalRadio,self.MarshalCall.DEGREES,nil,nil,nil,true) +end +function AIRBOSS:_MarshalCallCarrierTurnTo(hdg) +local text=string.format("carrier is now starting turn to heading %03d°.",hdg) +self:I(self.lid..text) +local call=self:_NewRadioCall(self.MarshalCall.CARRIERTURNTOHEADING,"AIRBOSS",text,self.Tmessage,"99") +self:RadioTransmission(self.MarshalRadio,call) +self:_Number2Radio(self.MarshalRadio,string.format("%03d",hdg),nil,0.2) +self:RadioTransmission(self.MarshalRadio,self.MarshalCall.DEGREES,nil,nil,nil,true) +end +function AIRBOSS:_MarshalCallStackFull(modex,nwaiting) +local text=string.format("Marshal stack is currently full. Hold outside 10 NM zone and wait for further instructions. ") +if nwaiting==1 then +text=text..string.format("There is one flight ahead of you.") +elseif nwaiting>1 then +text=text..string.format("There are %d flights ahead of you.",nwaiting) +else +text=text..string.format("You are next in line.") +end +self:I(self.lid..text) +local call=self:_NewRadioCall(self.MarshalCall.STACKFULL,"AIRBOSS",text,self.Tmessage,modex) +self:RadioTransmission(self.MarshalRadio,call,nil,nil,nil,true) +end +function AIRBOSS:_MarshalCallRecoveryStart(case) +local radial=self:GetRadial(case,true,true,false) +local text=string.format("Starting aircraft recovery Case %d ops.",case) +if case>1 then +text=text..string.format(" Marshal radial %03d°.",radial) +end +self:T(self.lid..text) +local call=self:_NewRadioCall(self.MarshalCall.STARTINGRECOVERY,"AIRBOSS",text,self.Tmessage,"99") +self:RadioTransmission(self.MarshalRadio,call) +self:_Number2Radio(self.MarshalRadio,tostring(case),nil,0.1) +self:RadioTransmission(self.MarshalRadio,self.MarshalCall.OPS) +if case>1 then +self:RadioTransmission(self.MarshalRadio,self.MarshalCall.MARSHALRADIAL) +self:_Number2Radio(self.MarshalRadio,string.format("%03d",radial),nil,0.2) +self:RadioTransmission(self.MarshalRadio,self.MarshalCall.DEGREES,nil,nil,nil,true) +end +end +function AIRBOSS:_MarshalCallArrived(modex,case,brc,altitude,charlie,qfe) +self:F({modex=modex,case=case,brc=brc,altitude=altitude,charlie=charlie,qfe=qfe}) +local angels=self:_GetAngels(altitude) +local QFE=UTILS.Split(string.format("%.2f",qfe),".") +local clock=UTILS.Split(charlie,"+") +local CT=UTILS.Split(clock[1],":") +local text=string.format("Case %d, expected BRC %03d°, hold at angels %d. Expected Charlie Time %s. Altimeter %.2f. Report see me.",case,brc,angels,charlie,qfe) +self:I(self.lid..text) +local casecall=self:_NewRadioCall(self.MarshalCall.CASE,"MARSHAL",text,self.Tmessage,modex) +self:RadioTransmission(self.MarshalRadio,casecall) +self:_Number2Radio(self.MarshalRadio,tostring(case)) +self:RadioTransmission(self.MarshalRadio,self.MarshalCall.EXPECTED,nil,nil,0.5) +self:RadioTransmission(self.MarshalRadio,self.MarshalCall.BRC) +self:_Number2Radio(self.MarshalRadio,string.format("%03d",brc)) +self:RadioTransmission(self.MarshalRadio,self.MarshalCall.DEGREES) +self:RadioTransmission(self.MarshalRadio,self.MarshalCall.HOLDATANGELS,nil,nil,0.5) +self:_Number2Radio(self.MarshalRadio,tostring(angels)) +self:RadioTransmission(self.MarshalRadio,self.MarshalCall.EXPECTED,nil,nil,0.5) +self:RadioTransmission(self.MarshalRadio,self.MarshalCall.CHARLIETIME) +self:_Number2Radio(self.MarshalRadio,CT[1]) +self:_Number2Radio(self.MarshalRadio,CT[2]) +self:RadioTransmission(self.MarshalRadio,self.MarshalCall.HOURS) +self:RadioTransmission(self.MarshalRadio,self.MarshalCall.ALTIMETER,nil,nil,0.5) +self:_Number2Radio(self.MarshalRadio,QFE[1]) +self:RadioTransmission(self.MarshalRadio,self.MarshalCall.POINT) +self:_Number2Radio(self.MarshalRadio,QFE[2]) +self:RadioTransmission(self.MarshalRadio,self.MarshalCall.REPORTSEEME,nil,nil,0.5,true) +end +function AIRBOSS:_AddF10Commands(_unitName) +self:F(_unitName) +local _unit,playername=self:_GetPlayerUnitAndName(_unitName) +if _unit and playername then +local group=_unit:GetGroup() +local gid=group:GetID() +if group and gid then +if not self.menuadded[gid]then +self.menuadded[gid]=true +local _rootPath=nil +if AIRBOSS.MenuF10Root then +if self.menusingle then +_rootPath=AIRBOSS.MenuF10Root +else +_rootPath=missionCommands.addSubMenuForGroup(gid,self.alias,AIRBOSS.MenuF10Root) +end +else +if AIRBOSS.MenuF10[gid]==nil then +AIRBOSS.MenuF10[gid]=missionCommands.addSubMenuForGroup(gid,"Airboss") +end +if self.menusingle then +_rootPath=AIRBOSS.MenuF10[gid] +else +_rootPath=missionCommands.addSubMenuForGroup(gid,self.alias,AIRBOSS.MenuF10[gid]) +end +end +local _helpPath=missionCommands.addSubMenuForGroup(gid,"Help",_rootPath) +if self.menumarkzones then +local _markPath=missionCommands.addSubMenuForGroup(gid,"Mark Zones",_helpPath) +if self.menusmokezones then +missionCommands.addCommandForGroup(gid,"Smoke Pattern Zones",_markPath,self._MarkCaseZones,self,_unitName,false) +end +missionCommands.addCommandForGroup(gid,"Flare Pattern Zones",_markPath,self._MarkCaseZones,self,_unitName,true) +if self.menusmokezones then +missionCommands.addCommandForGroup(gid,"Smoke Marshal Zone",_markPath,self._MarkMarshalZone,self,_unitName,false) +end +missionCommands.addCommandForGroup(gid,"Flare Marshal Zone",_markPath,self._MarkMarshalZone,self,_unitName,true) +end +local _skillPath=missionCommands.addSubMenuForGroup(gid,"Skill Level",_helpPath) +missionCommands.addCommandForGroup(gid,"Flight Student",_skillPath,self._SetDifficulty,self,_unitName,AIRBOSS.Difficulty.EASY) +missionCommands.addCommandForGroup(gid,"Naval Aviator",_skillPath,self._SetDifficulty,self,_unitName,AIRBOSS.Difficulty.NORMAL) +missionCommands.addCommandForGroup(gid,"TOPGUN Graduate",_skillPath,self._SetDifficulty,self,_unitName,AIRBOSS.Difficulty.HARD) +missionCommands.addCommandForGroup(gid,"Hints On/Off",_skillPath,self._SetHintsOnOff,self,_unitName) +missionCommands.addCommandForGroup(gid,"My Status",_helpPath,self._DisplayPlayerStatus,self,_unitName) +missionCommands.addCommandForGroup(gid,"Attitude Monitor",_helpPath,self._DisplayAttitude,self,_unitName) +missionCommands.addCommandForGroup(gid,"Radio Check LSO",_helpPath,self._LSORadioCheck,self,_unitName) +missionCommands.addCommandForGroup(gid,"Radio Check Marshal",_helpPath,self._MarshalRadioCheck,self,_unitName) +missionCommands.addCommandForGroup(gid,"Subtitles On/Off",_helpPath,self._SubtitlesOnOff,self,_unitName) +missionCommands.addCommandForGroup(gid,"Trapsheet On/Off",_helpPath,self._TrapsheetOnOff,self,_unitName) +local _kneeboardPath=missionCommands.addSubMenuForGroup(gid,"Kneeboard",_rootPath) +local _resultsPath=missionCommands.addSubMenuForGroup(gid,"Results",_kneeboardPath) +missionCommands.addCommandForGroup(gid,"Greenie Board",_resultsPath,self._DisplayScoreBoard,self,_unitName) +missionCommands.addCommandForGroup(gid,"My LSO Grades",_resultsPath,self._DisplayPlayerGrades,self,_unitName) +missionCommands.addCommandForGroup(gid,"Last Debrief",_resultsPath,self._DisplayDebriefing,self,_unitName) +if self.skipperMenu then +local _skipperPath=missionCommands.addSubMenuForGroup(gid,"Skipper",_kneeboardPath) +local _menusetspeed=missionCommands.addSubMenuForGroup(gid,"Set Speed",_skipperPath) +missionCommands.addCommandForGroup(gid,"10 knots",_menusetspeed,self._SkipperRecoverySpeed,self,_unitName,10) +missionCommands.addCommandForGroup(gid,"15 knots",_menusetspeed,self._SkipperRecoverySpeed,self,_unitName,15) +missionCommands.addCommandForGroup(gid,"20 knots",_menusetspeed,self._SkipperRecoverySpeed,self,_unitName,20) +missionCommands.addCommandForGroup(gid,"25 knots",_menusetspeed,self._SkipperRecoverySpeed,self,_unitName,25) +missionCommands.addCommandForGroup(gid,"30 knots",_menusetspeed,self._SkipperRecoverySpeed,self,_unitName,30) +local _menusetrtime=missionCommands.addSubMenuForGroup(gid,"Set Time",_skipperPath) +missionCommands.addCommandForGroup(gid,"15 min",_menusetrtime,self._SkipperRecoveryTime,self,_unitName,15) +missionCommands.addCommandForGroup(gid,"30 min",_menusetrtime,self._SkipperRecoveryTime,self,_unitName,30) +missionCommands.addCommandForGroup(gid,"45 min",_menusetrtime,self._SkipperRecoveryTime,self,_unitName,45) +missionCommands.addCommandForGroup(gid,"60 min",_menusetrtime,self._SkipperRecoveryTime,self,_unitName,60) +missionCommands.addCommandForGroup(gid,"90 min",_menusetrtime,self._SkipperRecoveryTime,self,_unitName,90) +local _menusetrtime=missionCommands.addSubMenuForGroup(gid,"Set Marshal Radial",_skipperPath) +missionCommands.addCommandForGroup(gid,"+30°",_menusetrtime,self._SkipperRecoveryOffset,self,_unitName,30) +missionCommands.addCommandForGroup(gid,"+15°",_menusetrtime,self._SkipperRecoveryOffset,self,_unitName,15) +missionCommands.addCommandForGroup(gid,"0°",_menusetrtime,self._SkipperRecoveryOffset,self,_unitName,0) +missionCommands.addCommandForGroup(gid,"-15°",_menusetrtime,self._SkipperRecoveryOffset,self,_unitName,-15) +missionCommands.addCommandForGroup(gid,"-30°",_menusetrtime,self._SkipperRecoveryOffset,self,_unitName,-30) +missionCommands.addCommandForGroup(gid,"U-turn On/Off",_skipperPath,self._SkipperRecoveryUturn,self,_unitName) +missionCommands.addCommandForGroup(gid,"Start CASE I",_skipperPath,self._SkipperStartRecovery,self,_unitName,1) +missionCommands.addCommandForGroup(gid,"Start CASE II",_skipperPath,self._SkipperStartRecovery,self,_unitName,2) +missionCommands.addCommandForGroup(gid,"Start CASE III",_skipperPath,self._SkipperStartRecovery,self,_unitName,3) +missionCommands.addCommandForGroup(gid,"Stop Recovery",_skipperPath,self._SkipperStopRecovery,self,_unitName) +end +missionCommands.addCommandForGroup(gid,"Carrier Info",_kneeboardPath,self._DisplayCarrierInfo,self,_unitName) +missionCommands.addCommandForGroup(gid,"Weather Report",_kneeboardPath,self._DisplayCarrierWeather,self,_unitName) +missionCommands.addCommandForGroup(gid,"Set Section",_kneeboardPath,self._SetSection,self,_unitName) +missionCommands.addCommandForGroup(gid,"Marshal Queue",_kneeboardPath,self._DisplayQueue,self,_unitName,"Marshal") +missionCommands.addCommandForGroup(gid,"Pattern Queue",_kneeboardPath,self._DisplayQueue,self,_unitName,"Pattern") +missionCommands.addCommandForGroup(gid,"Waiting Queue",_kneeboardPath,self._DisplayQueue,self,_unitName,"Waiting") +missionCommands.addCommandForGroup(gid,"Request Marshal",_rootPath,self._RequestMarshal,self,_unitName) +missionCommands.addCommandForGroup(gid,"Request Commence",_rootPath,self._RequestCommence,self,_unitName) +missionCommands.addCommandForGroup(gid,"Request Refueling",_rootPath,self._RequestRefueling,self,_unitName) +missionCommands.addCommandForGroup(gid,"Spinning",_rootPath,self._RequestSpinning,self,_unitName) +missionCommands.addCommandForGroup(gid,"Emergency Landing",_rootPath,self._RequestEmergency,self,_unitName) +missionCommands.addCommandForGroup(gid,"[Reset My Status]",_rootPath,self._ResetPlayerStatus,self,_unitName) +end +else +self:E(self.lid..string.format("ERROR: Could not find group or group ID in AddF10Menu() function. Unit name: %s.",_unitName)) +end +else +self:E(self.lid..string.format("ERROR: Player unit does not exist in AddF10Menu() function. Unit name: %s.",_unitName)) +end +end +function AIRBOSS:_SkipperStartRecovery(_unitName,case) +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +if _unit and _playername then +local playerData=self.players[_playername] +if playerData then +local text=string.format("affirm, Case %d recovery will start in 5 min for %d min. Wind on deck %d knots. U-turn=%s.",case,self.skipperTime,self.skipperSpeed,tostring(self.skipperUturn)) +if case>1 then +text=text..string.format(" Marshal radial %d°.",self.skipperOffset) +end +if self:IsRecovering()then +text="negative, carrier is already recovering." +self:MessageToPlayer(playerData,text,"AIRBOSS") +return +end +self:MessageToPlayer(playerData,text,"AIRBOSS") +local t0=timer.getAbsTime()+5*60 +local t9=t0+self.skipperTime*60 +local C0=UTILS.SecondsToClock(t0) +local C9=UTILS.SecondsToClock(t9) +self:AddRecoveryWindow(C0,C9,case,self.skipperOffset,true,self.skipperSpeed,self.skipperUturn) +end +end +end +function AIRBOSS:_SkipperStopRecovery(_unitName) +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +if _unit and _playername then +local playerData=self.players[_playername] +if playerData then +local text="roger, stopping recovery right away." +if not self:IsRecovering()then +text="negative, carrier is currently not recovering." +self:MessageToPlayer(playerData,text,"AIRBOSS") +return +end +self:MessageToPlayer(playerData,text,"AIRBOSS") +self:RecoveryStop() +end +end +end +function AIRBOSS:_SkipperRecoveryOffset(_unitName,offset) +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +if _unit and _playername then +local playerData=self.players[_playername] +if playerData then +local text=string.format("roger, relative CASE II/III Marshal radial set to %d°.",offset) +self:MessageToPlayer(playerData,text,"AIRBOSS") +self.skipperOffset=offset +end +end +end +function AIRBOSS:_SkipperRecoveryTime(_unitName,time) +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +if _unit and _playername then +local playerData=self.players[_playername] +if playerData then +local text=string.format("roger, manual recovery time set to %d min.",time) +self:MessageToPlayer(playerData,text,"AIRBOSS") +self.skipperTime=time +end +end +end +function AIRBOSS:_SkipperRecoverySpeed(_unitName,speed) +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +if _unit and _playername then +local playerData=self.players[_playername] +if playerData then +local text=string.format("roger, wind on deck set to %d knots.",speed) +self:MessageToPlayer(playerData,text,"AIRBOSS") +self.skipperSpeed=speed +end +end +end +function AIRBOSS:_SkipperRecoveryUturn(_unitName) +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +if _unit and _playername then +local playerData=self.players[_playername] +if playerData then +self.skipperUturn=not self.skipperUturn +local text=string.format("roger, U-turn is now %s.",tostring(self.skipperUturn)) +self:MessageToPlayer(playerData,text,"AIRBOSS") +end +end +end +function AIRBOSS:_ResetPlayerStatus(_unitName) +self:F(_unitName) +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +if _unit and _playername then +local playerData=self.players[_playername] +if playerData then +local text="roger, status reset executed! You have been removed from all queues." +self:MessageToPlayer(playerData,text,"AIRBOSS") +self:_RemoveFlight(playerData) +if playerData.debriefschedulerID and self.Scheduler then +self.Scheduler:Stop(playerData.debriefschedulerID) +end +self:_InitPlayer(playerData) +end +end +end +function AIRBOSS:_RequestMarshal(_unitName) +self:F(_unitName) +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +if _unit and _playername then +local playerData=self.players[_playername] +if playerData then +local inCCA=playerData.unit:IsInZone(self.zoneCCA) +if inCCA then +if self:_InQueue(self.Qmarshal,playerData.group)then +local text=string.format("negative, you are already in the Marshal queue. New marshal request denied!") +self:MessageToPlayer(playerData,text,"MARSHAL") +elseif self:_InQueue(self.Qpattern,playerData.group)then +local text=string.format("negative, you are already in the Pattern queue. Marshal request denied!") +self:MessageToPlayer(playerData,text,"MARSHAL") +elseif self:_InQueue(self.Qwaiting,playerData.group)then +local text=string.format("negative, you are in the Waiting queue with %d flights ahead of you. Marshal request denied!",#self.Qwaiting) +self:MessageToPlayer(playerData,text,"MARSHAL") +elseif not _unit:InAir()then +local text=string.format("negative, you are not airborne. Marshal request denied!") +self:MessageToPlayer(playerData,text,"MARSHAL") +elseif playerData.name~=playerData.seclead then +local text=string.format("negative, your section lead %s needs to request Marshal.",playerData.seclead) +self:MessageToPlayer(playerData,text,"MARSHAL") +else +local freestack=self:_GetFreeStack(playerData.ai) +if freestack then +self:_MarshalPlayer(playerData,freestack) +else +self:_WaitPlayer(playerData) +end +end +else +local text=string.format("negative, you are not inside CCA. Marshal request denied!") +self:MessageToPlayer(playerData,text,"MARSHAL") +end +end +end +end +function AIRBOSS:_RequestEmergency(_unitName) +self:F(_unitName) +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +if _unit and _playername then +local playerData=self.players[_playername] +if playerData then +local text="" +if not self.emergency then +text="negative, no emergency landings on my carrier. We are currently busy. See how you get along!" +elseif not _unit:InAir()then +local zone=self:_GetZoneCarrierBox() +if playerData.unit:IsInZone(zone)then +text="roger, you are now technically in the bolter pattern. Your next step after takeoff is abeam!" +local lead=self:_GetFlightLead(playerData) +self:_SetPlayerStep(lead,AIRBOSS.PatternStep.BOLTER) +for _,sec in pairs(lead.section)do +local sectionmember=sec +self:_SetPlayerStep(sectionmember,AIRBOSS.PatternStep.BOLTER) +end +self:_RemoveFlightFromQueue(self.Qwaiting,lead) +if self:_InQueue(self.Qmarshal,lead.group)then +self:_RemoveFlightFromMarshalQueue(lead) +else +if not self:_InQueue(self.Qpattern,lead.group)then +self:_AddFlightToPatternQueue(lead) +end +end +else +text=string.format("negative, you are not airborne. Request denied!") +end +else +text="affirmative, you can bypass the pattern and are cleared for final approach!" +local lead=self:_GetFlightLead(playerData) +self:_SetPlayerStep(lead,AIRBOSS.PatternStep.EMERGENCY) +for _,sec in pairs(lead.section)do +local sectionmember=sec +self:_SetPlayerStep(sectionmember,AIRBOSS.PatternStep.EMERGENCY) +self:_RemoveFlightFromQueue(self.Qspinning,sectionmember) +end +self:_RemoveFlightFromQueue(self.Qwaiting,lead) +if self:_InQueue(self.Qmarshal,lead.group)then +self:_RemoveFlightFromMarshalQueue(lead) +else +if not self:_InQueue(self.Qpattern,lead.group)then +self:_AddFlightToPatternQueue(lead) +end +end +end +self:MessageToPlayer(playerData,text,"AIRBOSS") +end +end +end +function AIRBOSS:_RequestSpinning(_unitName) +self:F(_unitName) +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +if _unit and _playername then +local playerData=self.players[_playername] +if playerData then +local text="" +if not self:_InQueue(self.Qpattern,playerData.group)then +text="negative, you have to be in the pattern to spin it!" +elseif playerData.step==AIRBOSS.PatternStep.SPINNING then +text="negative, you are already spinning." +elseif not(playerData.step==AIRBOSS.PatternStep.BREAKENTRY or +playerData.step==AIRBOSS.PatternStep.EARLYBREAK or +playerData.step==AIRBOSS.PatternStep.LATEBREAK)then +text="negative, you have to be in the right step to spin it!" +else +self:_SetPlayerStep(playerData,AIRBOSS.PatternStep.SPINNING) +table.insert(self.Qspinning,playerData) +local call=self:_NewRadioCall(self.LSOCall.SPINIT,"AIRBOSS","Spin it!",self.Tmessage,playerData.onboard) +self:RadioTransmission(self.LSORadio,call,nil,nil,nil,true) +if playerData.difficulty==AIRBOSS.Difficulty.EASY then +local text="Climb to 1200 feet and proceed to the initial again." +self:MessageToPlayer(playerData,text,"INSTRUCTOR","") +end +return +end +self:MessageToPlayer(playerData,text,"AIRBOSS") +end +end +end +function AIRBOSS:_RequestCommence(_unitName) +self:F(_unitName) +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +if _unit and _playername then +local playerData=self.players[_playername] +if playerData then +local text="" +local cleared=false +if _unit:IsInZone(self.zoneCCA)then +local stack=playerData.flag +local _,npattern=self:_GetQueueInfo(self.Qpattern) +if self:_InQueue(self.Qpattern,playerData.group)then +text=string.format("negative, %s, you are already in the Pattern queue.",playerData.name) +elseif not _unit:InAir()then +text=string.format("negative, %s, you are not airborne.",playerData.name) +elseif playerData.seclead~=playerData.name then +text=string.format("negative, %s, your section leader %s has to request commence!",playerData.name,playerData.seclead) +elseif stack>1 then +text=string.format("negative, %s, it's not your turn yet! You are in stack no. %s.",playerData.name,stack) +elseif npattern>=self.Nmaxpattern then +text=string.format("negative ghostrider, pattern is full!\nThere are %d aircraft currently in the pattern.",npattern) +elseif self:IsRecovering()==false and not self.airbossnice then +if self.recoverywindow then +local clock=UTILS.SecondsToClock(self.recoverywindow.START) +text=string.format("negative, carrier is currently not recovery. Next window will open at %s.",clock) +else +text=string.format("negative, carrier is not recovering. No future windows planned.") +end +elseif not self:_InQueue(self.Qmarshal,playerData.group)and not self.airbossnice then +text="negative, you have to request Marshal before you can commence." +else +text=text.."roger." +if not self:IsRecovering()then +text=text.." Carrier is not recovering currently! However, you are cleared anyway as I have a nice day." +end +if not self:_InQueue(self.Qmarshal,playerData.group)then +playerData.case=self.case +if self.TACANon and playerData.difficulty~=AIRBOSS.Difficulty.HARD then +local radial=self:GetRadial(playerData.case,true,true,true) +if playerData.case==1 then +radial=self:GetBRC() +end +text=text..string.format("\nSelect TACAN %03d°, Channel %d%s (%s).\n",radial,self.TACANchannel,self.TACANmode,self.TACANmorse) +end +for _,flight in pairs(playerData.section)do +flight.case=playerData.case +end +self:_AddFlightToPatternQueue(playerData) +end +cleared=true +end +else +text=string.format("negative, %s, you are not inside the CCA!",playerData.name) +end +self:T(self.lid..text) +self:MessageToPlayer(playerData,text,"MARSHAL") +if cleared then +self:_Commencing(playerData,false) +end +end +end +end +function AIRBOSS:_RequestRefueling(_unitName) +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +if _unit and _playername then +local playerData=self.players[_playername] +if playerData then +local text +if self.tanker then +if _unit:IsInZone(self.zoneCCA)then +if self.tanker:IsRunning()or self.tanker:IsRefueling()then +local angels=self:_GetAngels(self.tanker.altitude) +text=string.format("affirmative, proceed to tanker at angels %d.",angels) +if self.tanker.TACANon then +text=text..string.format("\nTanker TACAN channel %d%s (%s).",self.tanker.TACANchannel,self.tanker.TACANmode,self.tanker.TACANmorse) +text=text..string.format("\nRadio frequency %.3f MHz AM.",self.tanker.RadioFreq) +end +if self.tanker:IsRefueling()then +text=text.."\nTanker is currently refueling. You might have to queue up." +end +self:_RemoveFlightFromMarshalQueue(playerData,true) +self:_SetPlayerStep(playerData,AIRBOSS.PatternStep.REFUELING) +for _,sec in pairs(playerData.section)do +local sectext="follow your section leader to the tanker." +self:MessageToPlayer(sec,sectext,"MARSHAL") +self:_SetPlayerStep(sec,AIRBOSS.PatternStep.REFUELING) +end +elseif self.tanker:IsReturning()then +text="negative, tanker is RTB. Request denied!\nWait for the tanker to be back on station if you can." +end +else +text="negative, you are not inside the CCA yet." +end +else +text="negative, no refueling tanker available." +end +self:MessageToPlayer(playerData,text,"MARSHAL") +end +end +end +function AIRBOSS:_RemoveSectionMember(playerData,sectionmember) +for i,_flight in pairs(playerData.section)do +local flight=_flight +if flight.name==sectionmember.name then +table.remove(playerData.section,i) +return true +end +end +return false +end +function AIRBOSS:_SetSection(_unitName) +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +if _unit and _playername then +local playerData=self.players[_playername] +if playerData then +local mycoord=_unit:GetCoordinate() +local dmax=100 +local text +if self.NmaxSection==0 then +text=string.format("negative, setting sections is disabled in this mission. You stay alone.") +elseif self:_InQueue(self.Qmarshal,playerData.group)then +text=string.format("negative, you are already in the Marshal queue. Setting section not possible any more!") +elseif self:_InQueue(self.Qpattern,playerData.group)then +text=string.format("negative, you are already in the Pattern queue. Setting section not possible any more!") +else +if playerData.seclead~=playerData.name then +local lead=self.players[playerData.seclead] +if lead then +local removed=self:_RemoveSectionMember(lead,playerData) +if removed then +self:MessageToPlayer(lead,string.format("Flight %s has been removed from your section.",playerData.name),"AIRBOSS","",5) +self:MessageToPlayer(playerData,string.format("You have been removed from %s's section.",lead.name),"AIRBOSS","",5) +end +end +end +local section={} +for _,_flight in pairs(self.flights)do +local flight=_flight +if flight.ai==false and flight.groupname~=playerData.groupname and#flight.section==0 and flight.seclead==flight.name then +local distance=flight.group:GetCoordinate():Get3DDistance(mycoord) +if distance0 then +_playerResults[playerName]=Paverage/n +end +end +end +local text=string.format("Greenie Board (top ten):") +local i=1 +for _playerName,_points in UTILS.spairs(_playerResults,function(t,a,b)return t[b]=0 then +text=text..string.format("(%.1f)",grade.points) +end +end +i=i+1 +if i>10 then +break +end +end +if i==1 then +text=text.."\nNo results yet." +end +local playerData=self.players[_playername] +if playerData.client then +MESSAGE:New(text,30,nil,true):ToClient(playerData.client) +end +end +end +function AIRBOSS:_DisplayPlayerGrades(_unitName) +self:F(_unitName) +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +if _unit and _playername then +local playerData=self.players[_playername] +if playerData then +local text=string.format("Your last 10 grades, %s:",_playername) +local playerGrades=self.playerscores[_playername]or{} +local p=0 +local n=0 +local m=0 +for i=#playerGrades,1,-1 do +local grade=playerGrades[i] +if grade.points>=0 then +local points=grade.finalscore or grade.points +if m<10 then +text=text..string.format("\n[%d] %s %.1f PT - %s",i,grade.grade,points,grade.details) +if grade.wire and grade.wire<=4 then +text=text..string.format(" %d-wire",grade.wire) +end +if grade.Tgroove and grade.Tgroove<=360 then +text=text..string.format(" Tgroove=%.1f s",grade.Tgroove) +end +end +if grade.finalscore then +p=p+grade.finalscore +n=n+1 +end +m=m+1 +end +end +if n>0 then +text=text..string.format("\nAverage points = %.1f",p/n) +else +text=text..string.format("\nNo data available.") +end +if playerData.client then +MESSAGE:New(text,30,nil,true):ToClient(playerData.client) +end +end +end +end +function AIRBOSS:_DisplayDebriefing(_unitName) +self:F(_unitName) +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +if _unit and _playername then +local playerData=self.players[_playername] +if playerData then +local text=string.format("Debriefing:") +if#playerData.lastdebrief>0 then +text=text..string.format("\n================================\n") +for _,_data in pairs(playerData.lastdebrief)do +local step=_data.step +local comment=_data.hint +text=text..string.format("* %s:",step) +text=text..string.format("%s\n",comment) +end +else +text=text.." Nothing to show yet." +end +self:MessageToPlayer(playerData,text,nil,"",30,true) +end +end +end +function AIRBOSS:_DisplayQueue(_unitname,qname) +local unit,playername=self:_GetPlayerUnitAndName(_unitname) +if unit and playername then +local playerData=self.players[playername] +if playerData then +local queue=nil +if qname=="Marshal"then +queue=self.Qmarshal +elseif qname=="Pattern"then +queue=self.Qpattern +elseif qname=="Waiting"then +queue=self.Qwaiting +end +local Nqueue,nqueue=self:_GetQueueInfo(queue,playerData.case) +local text=string.format("%s Queue:",qname) +if#queue==0 then +text=text.." empty" +else +local N=0 +if qname=="Marshal"then +for i,_flight in pairs(queue)do +local flight=_flight +local charlie=self:_GetCharlieTime(flight) +local Charlie=UTILS.SecondsToClock(charlie) +local stack=flight.flag +local angels=self:_GetAngels(self:_GetMarshalAltitude(stack,flight.case)) +local _,nunit,nsec=self:_GetFlightUnits(flight,true) +local nick=self:_GetACNickname(flight.actype) +N=N+nunit +text=text..string.format("\n[Stack %d] %s (%s*%d+%d): Case %d, Angels %d, Charlie %s",stack,flight.onboard,nick,nunit,nsec,flight.case,angels,tostring(Charlie)) +end +elseif qname=="Pattern"or qname=="Waiting"then +for i,_flight in pairs(queue)do +local flight=_flight +local _,nunit,nsec=self:_GetFlightUnits(flight,true) +local nick=self:_GetACNickname(flight.actype) +local ptime=UTILS.SecondsToClock(timer.getAbsTime()-flight.time) +N=N+nunit +text=text..string.format("\n[%d] %s (%s*%d+%d): Case %d, T=%s",i,flight.onboard,nick,nunit,nsec,flight.case,ptime) +end +end +text=text..string.format("\nTotal AC: %d (airborne %d)",N,nqueue) +end +self:MessageToPlayer(playerData,text,nil,"",nil,true) +end +end +end +function AIRBOSS:_DisplayCarrierInfo(_unitname) +self:F2(_unitname) +local unit,playername=self:_GetPlayerUnitAndName(_unitname) +if unit and playername then +local playerData=self.players[playername] +if playerData then +local coord=self:GetCoordinate() +local carrierheading=self.carrier:GetHeading() +local carrierspeed=UTILS.MpsToKnots(self.carrier:GetVelocityMPS()) +local tacan="unknown" +local icls="unknown" +if self.TACANon and self.TACANchannel~=nil then +tacan=string.format("%d%s (%s)",self.TACANchannel,self.TACANmode,self.TACANmorse) +end +if self.ICLSon and self.ICLSchannel~=nil then +icls=string.format("%d (%s)",self.ICLSchannel,self.ICLSmorse) +end +local wind=UTILS.MpsToKnots(select(1,self:GetWindOnDeck())) +local Nmarshal,nmarshal=self:_GetQueueInfo(self.Qmarshal,playerData.case) +local Npattern,npattern=self:_GetQueueInfo(self.Qpattern) +local Nspinning,nspinning=self:_GetQueueInfo(self.Qspinning) +local Nwaiting,nwaiting=self:_GetQueueInfo(self.Qwaiting) +local Ntotal,ntotal=self:_GetQueueInfo(self.flights) +local Tabs=timer.getAbsTime() +local recoverytext="Recovery time windows (max 5):" +if#self.recoverytimes==0 then +recoverytext=recoverytext.." none." +else +local rw=0 +for _,_recovery in pairs(self.recoverytimes)do +local recovery=_recovery +if Tabs=5 then +break +end +end +end +end +local tankertext=nil +if self.tanker then +tankertext=string.format("Recovery tanker frequency %.3f MHz\n",self.tanker.RadioFreq) +if self.tanker.TACANon then +tankertext=tankertext..string.format("Recovery tanker TACAN %d%s (%s)",self.tanker.TACANchannel,self.tanker.TACANmode,self.tanker.TACANmorse) +else +tankertext=tankertext.."Recovery tanker TACAN n/a" +end +end +local state=self:GetState() +if state=="Idle"then +state="Deck closed" +end +if self.turning then +state=state.." (turning currently)" +end +local text=string.format("%s info:\n",self.alias) +text=text..string.format("================================\n") +text=text..string.format("Carrier state: %s\n",state) +if self.case==1 then +text=text..string.format("Case %d recovery ops\n",self.case) +else +local radial=self:GetRadial(self.case,true,true,false) +text=text..string.format("Case %d recovery ops\nMarshal radial %03d°\n",self.case,radial) +end +text=text..string.format("BRC %03d° - FB %03d°\n",self:GetBRC(),self:GetFinalBearing(true)) +text=text..string.format("Speed %.1f kts - Wind on deck %.1f kts\n",carrierspeed,wind) +text=text..string.format("Tower frequency %.3f MHz\n",self.TowerFreq) +text=text..string.format("Marshal radio %.3f MHz\n",self.MarshalFreq) +text=text..string.format("LSO radio %.3f MHz\n",self.LSOFreq) +text=text..string.format("TACAN Channel %s\n",tacan) +text=text..string.format("ICLS Channel %s\n",icls) +if tankertext then +text=text..tankertext.."\n" +end +text=text..string.format("# A/C total %d (%d)\n",Ntotal,ntotal) +text=text..string.format("# A/C marshal %d (%d)\n",Nmarshal,nmarshal) +text=text..string.format("# A/C pattern %d (%d) - spinning %d (%d)\n",Npattern,npattern,Nspinning,nspinning) +text=text..string.format("# A/C waiting %d (%d)\n",Nwaiting,nwaiting) +text=text..string.format(recoverytext) +self:T2(self.lid..text) +self:MessageToPlayer(playerData,text,nil,"",30,true) +else +self:E(self.lid..string.format("ERROR: Could not get player data for player %s.",playername)) +end +end +end +function AIRBOSS:_DisplayCarrierWeather(_unitname) +self:F2(_unitname) +local unit,playername=self:_GetPlayerUnitAndName(_unitname) +if unit and playername then +local text="" +local coord=self:GetCoordinate() +local T=coord:GetTemperature() +local P=coord:GetPressure() +local Wd,Ws=self:GetWind(nil,true) +local Bn,Bd=UTILS.BeaufortScale(Ws) +local WodPA,WodPP=self:GetWindOnDeck() +local WodPA=UTILS.MpsToKnots(WodPA) +local WodPP=UTILS.MpsToKnots(WodPP) +local WD=string.format('%03d°',Wd) +local Ts=string.format("%d°C",T) +local tT=string.format("%d°C",T) +local tW=string.format("%.1f knots",UTILS.MpsToKnots(Ws)) +local tP=string.format("%.2f inHg",UTILS.hPa2inHg(P)) +text=text..string.format("Weather Report at Carrier %s:\n",self.alias) +text=text..string.format("================================\n") +text=text..string.format("Temperature %s\n",tT) +text=text..string.format("Wind from %s at %s (%s)\n",WD,tW,Bd) +text=text..string.format("Wind on deck || %.1f kts, == %.1f kts\n",WodPA,WodPP) +text=text..string.format("QFE %.1f hPa = %s",P,tP) +if self.staticweather then +local clouds,visibility,fog,dust=self:_GetStaticWeather() +text=text..string.format("\nVisibility %.1f NM",UTILS.MetersToNM(visibility)) +text=text..string.format("\nCloud base %d ft",UTILS.MetersToFeet(clouds.base)) +text=text..string.format("\nCloud thickness %d ft",UTILS.MetersToFeet(clouds.thickness)) +text=text..string.format("\nCloud density %d",clouds.density) +text=text..string.format("\nPrecipitation %d",clouds.iprecptns) +if fog then +text=text..string.format("\nFog thickness %d ft",UTILS.MetersToFeet(fog.thickness)) +text=text..string.format("\nFog visibility %d ft",UTILS.MetersToFeet(fog.visibility)) +else +text=text..string.format("\nNo fog") +end +if dust then +text=text..string.format("\nDust density %d",dust) +else +text=text..string.format("\nNo dust") +end +end +self:T2(self.lid..text) +self:MessageToPlayer(self.players[playername],text,nil,"",30,true) +else +self:E(self.lid..string.format("ERROR! Could not find player unit in CarrierWeather! Unit name = %s",_unitname)) +end +end +function AIRBOSS:_SetDifficulty(_unitname,difficulty) +self:T2({difficulty=difficulty,unitname=_unitname}) +local unit,playername=self:_GetPlayerUnitAndName(_unitname) +if unit and playername then +local playerData=self.players[playername] +if playerData then +playerData.difficulty=difficulty +local text=string.format("roger, your skill level is now: %s.",difficulty) +self:MessageToPlayer(playerData,text,nil,playerData.name,5) +else +self:E(self.lid..string.format("ERROR: Could not get player data for player %s.",playername)) +end +if playerData.difficulty==AIRBOSS.Difficulty.HARD then +playerData.showhints=false +else +playerData.showhints=true +end +end +end +function AIRBOSS:_SetHintsOnOff(_unitname) +self:F2(_unitname) +local unit,playername=self:_GetPlayerUnitAndName(_unitname) +if unit and playername then +local playerData=self.players[playername] +if playerData then +playerData.showhints=not playerData.showhints +local text="" +if playerData.showhints==true then +text=string.format("roger, hints are now ON.") +else +text=string.format("affirm, hints are now OFF.") +end +self:MessageToPlayer(playerData,text,nil,playerData.name,5) +end +end +end +function AIRBOSS:_DisplayAttitude(_unitname) +self:F2(_unitname) +local unit,playername=self:_GetPlayerUnitAndName(_unitname) +if unit and playername then +local playerData=self.players[playername] +if playerData then +playerData.attitudemonitor=not playerData.attitudemonitor +end +end +end +function AIRBOSS:_SubtitlesOnOff(_unitname) +self:F2(_unitname) +local unit,playername=self:_GetPlayerUnitAndName(_unitname) +if unit and playername then +local playerData=self.players[playername] +if playerData then +playerData.subtitles=not playerData.subtitles +local text="" +if playerData.subtitles==true then +text=string.format("roger, subtitiles are now ON.") +elseif playerData.subtitles==false then +text=string.format("affirm, subtitiles are now OFF.") +end +self:MessageToPlayer(playerData,text,nil,playerData.name,5) +end +end +end +function AIRBOSS:_TrapsheetOnOff(_unitname) +self:F2(_unitname) +local unit,playername=self:_GetPlayerUnitAndName(_unitname) +if unit and playername then +local playerData=self.players[playername] +if playerData then +local text="" +if self.trapsheet then +playerData.trapon=not playerData.trapon +if playerData.trapon==true then +text=string.format("roger, your trapsheets are now SAVED.") +else +text=string.format("affirm, your trapsheets are NOT SAVED.") +end +else +text="negative, trap sheet data recorder is broken on this carrier." +end +self:MessageToPlayer(playerData,text,nil,playerData.name,5) +end +end +end +function AIRBOSS:_DisplayPlayerStatus(_unitName) +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +if _unit and _playername then +local playerData=self.players[_playername] +if playerData then +local steptext=playerData.step +if playerData.step==AIRBOSS.PatternStep.HOLDING then +if playerData.holding==nil then +steptext="Transit to Marshal" +elseif playerData.holding==false then +steptext="Marshal (outside zone)" +elseif playerData.holding==true then +steptext="Marshal Stack Holding" +end +end +local stack=playerData.flag +local stacktext=nil +if stack>0 then +local stackalt=self:_GetMarshalAltitude(stack) +local angels=self:_GetAngels(stackalt) +stacktext=string.format("Marshal Stack %d, Angels %d\n",stack,angels) +if playerData.step==AIRBOSS.PatternStep.HOLDING and playerData.case>1 then +local radial=self:GetRadial(playerData.case,true,true,true) +stacktext=stacktext..string.format("Select TACAN %03d°, %d DME\n",radial,angels+15) +end +end +local fuel=playerData.unit:GetFuel()*100 +local fuelstate=self:_GetFuelState(playerData.unit) +local _,nunitsGround=self:_GetFlightUnits(playerData,true) +local _,nunitsAirborne=self:_GetFlightUnits(playerData,false) +local text=string.format("Status of player %s (%s)\n",playerData.name,playerData.callsign) +text=text..string.format("================================\n") +text=text..string.format("Step: %s\n",steptext) +if stacktext then +text=text..stacktext +end +text=text..string.format("Recovery Case: %d\n",playerData.case) +text=text..string.format("Skill Level: %s\n",playerData.difficulty) +text=text..string.format("Modex: %s (%s)\n",playerData.onboard,self:_GetACNickname(playerData.actype)) +text=text..string.format("Fuel State: %.1f lbs/1000 (%.1f %%)\n",fuelstate/1000,fuel) +text=text..string.format("# units: %d (%d airborne)\n",nunitsGround,nunitsAirborne) +text=text..string.format("Section Lead: %s (%d/%d)",tostring(playerData.seclead),#playerData.section+1,self.NmaxSection+1) +for _,_sec in pairs(playerData.section)do +local sec=_sec +text=text..string.format("\n- %s",sec.name) +end +if playerData.step==AIRBOSS.PatternStep.INITIAL then +local zoneinitial=self:GetCoordinate():Translate(UTILS.NMToMeters(3.5),self:GetRadial(2,false,false,false)) +local flyhdg=playerData.unit:GetCoordinate():HeadingTo(zoneinitial) +local flydist=UTILS.MetersToNM(playerData.unit:GetCoordinate():Get2DDistance(zoneinitial)) +local brc=self:GetBRC() +text=text..string.format("\nTo Initial: Fly heading %03d° for %.1f NM and turn to BRC %03d°",flyhdg,flydist,brc) +elseif playerData.step==AIRBOSS.PatternStep.PLATFORM then +local zoneplatform=self:_GetZonePlatform(playerData.case):GetCoordinate() +local flyhdg=playerData.unit:GetCoordinate():HeadingTo(zoneplatform) +local flydist=UTILS.MetersToNM(playerData.unit:GetCoordinate():Get2DDistance(zoneplatform)) +local hdg=self:GetRadial(playerData.case,true,true,true) +text=text..string.format("\nTo Platform: Fly heading %03d° for %.1f NM and turn to %03d°",flyhdg,flydist,hdg) +end +self:MessageToPlayer(playerData,text,nil,"",30,true) +else +self:E(self.lid..string.format("ERROR: playerData=nil. Unit name=%s, player name=%s",_unitName,_playername)) +end +else +self:E(self.lid..string.format("ERROR: could not find player for unit %s",_unitName)) +end +end +function AIRBOSS:_MarkMarshalZone(_unitName,flare) +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +if _unit and _playername then +local playerData=self.players[_playername] +if playerData then +local stack=playerData.flag +local case=playerData.case +local text="" +if stack>0 then +local zoneHolding=self:_GetZoneHolding(case,stack) +local zoneThree=self:_GetZoneCommence(case,stack) +local patternalt=self:_GetMarshalAltitude(stack,case) +patternalt=5 +text="roger, marking" +if flare then +text=text..string.format("\n* Marshal zone stack %d with WHITE flares.",stack) +zoneHolding:FlareZone(FLARECOLOR.White,45,nil,patternalt) +text=text.."\n* Commence zone with RED flares." +zoneThree:FlareZone(FLARECOLOR.Red,45,nil,patternalt) +else +text=text..string.format("\n* Marshal zone stack %d with WHITE smoke.",stack) +zoneHolding:SmokeZone(SMOKECOLOR.White,45,patternalt) +text=text.."\n* Commence zone with RED smoke." +zoneThree:SmokeZone(SMOKECOLOR.Red,45,patternalt) +end +else +text="negative, you are currently not in a Marshal stack. No zones will be marked!" +end +self:MessageToPlayer(playerData,text,"MARSHAL",playerData.name) +end +end +end +function AIRBOSS:_MarkCaseZones(_unitName,flare) +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +if _unit and _playername then +local playerData=self.players[_playername] +if playerData then +local case=playerData.case +local text=string.format("affirm, marking CASE %d zones",case) +if flare then +if case==1 or case==2 then +text=text.."\n* initial with GREEN flares" +self:_GetZoneInitial(case):FlareZone(FLARECOLOR.Green,45) +end +if case==2 or case==3 then +text=text.."\n* approach corridor with GREEN flares" +self:_GetZoneCorridor(case):FlareZone(FLARECOLOR.Green,45) +end +if case==2 or case==3 then +text=text.."\n* platform with RED flares" +self:_GetZonePlatform(case):FlareZone(FLARECOLOR.Red,45) +end +if case==3 then +text=text.."\n* dirty up with YELLOW flares" +self:_GetZoneDirtyUp(case):FlareZone(FLARECOLOR.Yellow,45) +end +if case==2 or case==3 then +if math.abs(self.holdingoffset)>0 then +self:_GetZoneArcIn(case):FlareZone(FLARECOLOR.White,45) +text=text.."\n* arc turn in with WHITE flares" +self:_GetZoneArcOut(case):FlareZone(FLARECOLOR.White,45) +text=text.."\n* arc trun out with WHITE flares" +end +end +if case==3 then +text=text.."\n* bullseye with GREEN flares" +self:_GetZoneBullseye(case):FlareZone(FLARECOLOR.Green,45) +end +if self.carriertype==AIRBOSS.CarrierType.TARAWA then +text=text.."\n* abeam landing stop with RED flares" +local ALSPT=self:_GetZoneAbeamLandingSpot() +ALSPT:FlareZone(FLARECOLOR.Red,5,nil,UTILS.FeetToMeters(110)) +text=text.."\n* primary landing spot with GREEN flares" +local LSPT=self:_GetZoneLandingSpot() +LSPT:FlareZone(FLARECOLOR.Green,5,nil,self.carrierparam.deckheight) +end +else +if case==1 or case==2 then +text=text.."\n* initial with GREEN smoke" +self:_GetZoneInitial(case):SmokeZone(SMOKECOLOR.Green,45) +end +if case==2 or case==3 then +text=text.."\n* approach corridor with GREEN smoke" +self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green,45) +end +if case==2 or case==3 then +text=text.."\n* platform with RED smoke" +self:_GetZonePlatform(case):SmokeZone(SMOKECOLOR.Red,45) +end +if case==2 or case==3 then +if math.abs(self.holdingoffset)>0 then +self:_GetZoneArcIn(case):SmokeZone(SMOKECOLOR.Blue,45) +text=text.."\n* arc turn in with BLUE smoke" +self:_GetZoneArcOut(case):SmokeZone(SMOKECOLOR.Blue,45) +text=text.."\n* arc trun out with BLUE smoke" +end +end +if case==3 then +text=text.."\n* dirty up with ORANGE smoke" +self:_GetZoneDirtyUp(case):SmokeZone(SMOKECOLOR.Orange,45) +end +if case==3 then +text=text.."\n* bullseye with GREEN smoke" +self:_GetZoneBullseye(case):SmokeZone(SMOKECOLOR.Green,45) +end +end +self:MessageToPlayer(playerData,text,"MARSHAL",playerData.name) +end +end +end +function AIRBOSS:_LSORadioCheck(_unitName) +self:F(_unitName) +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +if _unit and _playername then +local playerData=self.players[_playername] +if playerData then +self:RadioTransmission(self.LSORadio,self.LSOCall.RADIOCHECK,nil,nil,nil,true) +end +end +end +function AIRBOSS:_MarshalRadioCheck(_unitName) +self:F(_unitName) +local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) +if _unit and _playername then +local playerData=self.players[_playername] +if playerData then +self:RadioTransmission(self.MarshalRadio,self.MarshalCall.RADIOCHECK,nil,nil,nil,true) +end +end +end +function AIRBOSS:_SaveTrapSheet(playerData,grade) +if playerData.trapsheet==nil or#playerData.trapsheet==0 or not io then +return +end +local function _savefile(filename,data) +local f=io.open(filename,"wb") +if f then +f:write(data) +f:close() +else +self:E(self.lid..string.format("ERROR: could not save trap sheet to file %s.\nFile may contain invalid characters.",tostring(filename))) +end +end +local path=self.trappath +if lfs then +path=path or lfs.writedir() +end +local filename=nil +for i=1,9999 do +if self.trapprefix then +filename=string.format("%s_%s-%04d.csv",self.trapprefix,playerData.actype,i) +else +local name=UTILS.ReplaceIllegalCharacters(playerData.name,"_") +filename=string.format("AIRBOSS-%s_Trapsheet-%s_%s-%04d.csv",self.alias,name,playerData.actype,i) +end +if path~=nil then +filename=path.."\\"..filename +end +local _exists=UTILS.FileExists(filename) +if not _exists then +break +end +end +local text=string.format("Saving player %s trapsheet to file %s",playerData.name,filename) +self:I(self.lid..text) +local data="#Time,Rho,X,Z,Alt,AoA,GSE,LUE,Vtot,Vy,Gamma,Pitch,Roll,Yaw,Step,Grade,Points,Details\n" +local g0=playerData.trapsheet[1] +local T0=g0.Time +for i=1,#playerData.trapsheet do +local groove=playerData.trapsheet[i] +local t=groove.Time-T0 +local a=UTILS.MetersToNM(groove.Rho or 0) +local b=-groove.X or 0 +local c=groove.Z or 0 +local d=UTILS.MetersToFeet(groove.Alt or 0) +local e=groove.AoA or 0 +local f=groove.GSE or 0 +local g=-groove.LUE or 0 +local h=UTILS.MpsToKnots(groove.Vel or 0) +local i=(groove.Vy or 0)*196.85 +local j=groove.Gamma or 0 +local k=groove.Pitch or 0 +local l=groove.Roll or 0 +local m=groove.Yaw or 0 +local n=self:_GS(groove.Step,-1)or"n/a" +local o=groove.Grade or"n/a" +local p=groove.GradePoints or 0 +local q=groove.GradeDetail or"n/a" +data=data..string.format("%.2f,%.3f,%.1f,%.1f,%.1f,%.2f,%.2f,%.2f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%s,%s,%.1f,%s\n",t,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q) +end +_savefile(filename,data) +end +function AIRBOSS:onbeforeSave(From,Event,To,path,filename) +if not io then +self:E(self.lid.."ERROR: io not desanitized. Can't save player grades.") +return false +end +if path==nil and not lfs then +self:E(self.lid.."WARNING: lfs not desanitized. Results will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.") +end +return true +end +function AIRBOSS:onafterSave(From,Event,To,path,filename) +local function _savefile(filename,data) +local f=assert(io.open(filename,"wb")) +f:write(data) +f:close() +end +if lfs then +path=path or lfs.writedir() +end +filename=filename or string.format("AIRBOSS-%s_LSOgrades.csv",self.alias) +if path~=nil then +filename=path.."\\"..filename +end +local scores="Name,Pass,Points Final,Points Pass,Grade,Details,Wire,Tgroove,Case,Wind,Modex,Airframe,Carrier Type,Carrier Name,Theatre,Mission Time,Mission Date,OS Date\n" +local n=0 +for playername,grades in pairs(self.playerscores)do +for i,_grade in pairs(grades)do +local grade=_grade +local wire="n/a" +if grade.wire and grade.wire<=4 then +wire=tostring(grade.wire) +end +local Tgroove="n/a" +if grade.Tgroove and grade.Tgroove<=360 and grade.case<3 then +Tgroove=tostring(UTILS.Round(grade.Tgroove,1)) +end +local finalscore="n/a" +if grade.finalscore then +finalscore=tostring(UTILS.Round(grade.finalscore,1)) +end +scores=scores..string.format("%s,%d,%s,%.1f,%s,%s,%s,%s,%d,%s,%s,%s,%s,%s,%s,%s,%s,%s\n", +playername,i,finalscore,grade.points,grade.grade,grade.details,wire,Tgroove,grade.case, +grade.wind,grade.modex,grade.airframe,grade.carriertype,grade.carriername,grade.theatre,grade.mitime,grade.midate,grade.osdate) +n=n+1 +end +end +local text=string.format("Saving %d player LSO grades to file %s",n,filename) +self:I(self.lid..text) +_savefile(filename,scores) +end +function AIRBOSS:onbeforeLoad(From,Event,To,path,filename) +local function _fileexists(name) +local f=io.open(name,"r") +if f~=nil then +io.close(f) +return true +else +return false +end +end +if not io then +self:E(self.lid.."WARNING: io not desanitized. Can't load player grades.") +return false +end +if path==nil and not lfs then +self:E(self.lid.."WARNING: lfs not desanitized. Results will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.") +end +if lfs then +path=path or lfs.writedir() +end +filename=filename or string.format("AIRBOSS-%s_LSOgrades.csv",self.alias) +if path~=nil then +filename=path.."\\"..filename +end +local exists=_fileexists(filename) +if exists then +return true +else +self:E(self.lid..string.format("WARNING: Player LSO grades file %s does not exist.",filename)) +return false +end +end +function AIRBOSS:onafterLoad(From,Event,To,path,filename) +local function _loadfile(filename) +local f=assert(io.open(filename,"rb")) +local data=f:read("*all") +f:close() +return data +end +if lfs then +path=path or lfs.writedir() +end +filename=filename or string.format("AIRBOSS-%s_LSOgrades.csv",self.alias) +if path~=nil then +filename=path.."\\"..filename +end +local text=string.format("Loading player LSO grades from file %s",filename) +MESSAGE:New(text,10):ToAllIf(self.Debug) +self:I(self.lid..text) +local data=_loadfile(filename) +local playergrades=UTILS.Split(data,"\n") +table.remove(playergrades,1) +self.playerscores={} +local n=0 +for _,gradeline in pairs(playergrades)do +local gradedata=UTILS.Split(gradeline,",") +self:T2(gradedata) +local grade={} +local playername=gradedata[1] +if gradedata[3]~=nil and gradedata[3]~="n/a"then +grade.finalscore=tonumber(gradedata[3]) +end +grade.points=tonumber(gradedata[4]) +grade.grade=tostring(gradedata[5]) +grade.details=tostring(gradedata[6]) +if gradedata[7]~=nil and gradedata[7]~="n/a"then +grade.wire=tonumber(gradedata[7]) +end +if gradedata[8]~=nil and gradedata[8]~="n/a"then +grade.Tgroove=tonumber(gradedata[8]) +end +grade.case=tonumber(gradedata[9]) +grade.wind=gradedata[10]or"n/a" +grade.modex=gradedata[11]or"n/a" +grade.airframe=gradedata[12]or"n/a" +grade.carriertype=gradedata[13]or"n/a" +grade.carriername=gradedata[14]or"n/a" +grade.theatre=gradedata[15]or"n/a" +grade.mitime=gradedata[16]or"n/a" +grade.midate=gradedata[17]or"n/a" +grade.osdate=gradedata[18]or"n/a" +self.playerscores[playername]=self.playerscores[playername]or{} +table.insert(self.playerscores[playername],grade) +n=n+1 +self:T2({playername,self.playerscores[playername]}) +end +local text=string.format("Loaded %d player LSO grades from file %s",n,filename) +self:I(self.lid..text) +end +RECOVERYTANKER={ +ClassName="RECOVERYTANKER", +Debug=false, +lid=nil, +carrier=nil, +carriertype=nil, +tankergroupname=nil, +tanker=nil, +airbase=nil, +beacon=nil, +TACANchannel=nil, +TACANmode=nil, +TACANmorse=nil, +TACANon=nil, +RadioFreq=nil, +RadioModu=nil, +altitude=nil, +speed=nil, +distStern=nil, +distBow=nil, +dTupdate=nil, +Dupdate=nil, +Hupdate=nil, +Tupdate=nil, +takeoff=nil, +lowfuel=nil, +respawn=nil, +respawninair=nil, +uncontrolledac=nil, +orientation=nil, +orientlast=nil, +position=nil, +alias=nil, +uid=0, +awacs=nil, +callsignname=nil, +callsignnumber=nil, +modex=nil, +eplrs=nil, +recovery=nil, +terminaltype=nil, +} +_RECOVERYTANKERID=0 +RECOVERYTANKER.version="1.0.9" +function RECOVERYTANKER:New(carrierunit,tankergroupname) +local self=BASE:Inherit(self,FSM:New()) +if type(carrierunit)=="string"then +self.carrier=UNIT:FindByName(carrierunit) +else +self.carrier=carrierunit +end +self.carriertype=self.carrier:GetTypeName() +self.tankergroupname=tankergroupname +_RECOVERYTANKERID=_RECOVERYTANKERID+1 +self.uid=_RECOVERYTANKERID +self.carrier:SetState(self.carrier,string.format("RECOVERYTANKER_%d",self.uid),self) +self.alias=string.format("%s_%s_%02d",self.carrier:GetName(),self.tankergroupname,_RECOVERYTANKERID) +self.lid=string.format("RECOVERYTANKER %s | ",self.alias) +self:SetAltitude() +self:SetSpeed() +self:SetRacetrackDistances() +self:SetHomeBase(AIRBASE:FindByName(self.carrier:GetName())) +self:SetTakeoffHot() +self:SetLowFuelThreshold() +self:SetRespawnOnOff() +self:SetTACAN() +self:SetRadio() +self:SetPatternUpdateDistance() +self:SetPatternUpdateHeading() +self:SetPatternUpdateInterval() +self:SetAWACS(false) +self:SetRecoveryAirboss(false) +self.terminaltype=AIRBASE.TerminalType.OpenMedOrBig +if false then +BASE:TraceOnOff(true) +BASE:TraceClass(self.ClassName) +BASE:TraceLevel(1) +end +self:SetStartState("Stopped") +self:AddTransition("Stopped","Start","Running") +self:AddTransition("*","RefuelStart","Refueling") +self:AddTransition("*","RefuelStop","Running") +self:AddTransition("*","Run","Running") +self:AddTransition("Running","RTB","Returning") +self:AddTransition("Returning","Returned","Returned") +self:AddTransition("*","Status","*") +self:AddTransition("Running","PatternUpdate","*") +self:AddTransition("*","Stop","Stopped") +return self +end +function RECOVERYTANKER:SetSpeed(speed) +self.speed=UTILS.KnotsToMps(speed or 274) +return self +end +function RECOVERYTANKER:SetAltitude(altitude) +self.altitude=UTILS.FeetToMeters(altitude or 6000) +return self +end +function RECOVERYTANKER:SetRacetrackDistances(distbow,diststern) +self.distBow=UTILS.NMToMeters(distbow or 10) +self.distStern=-UTILS.NMToMeters(diststern or 4) +return self +end +function RECOVERYTANKER:SetPatternUpdateInterval(interval) +self.dTupdate=(interval or 10)*60 +return self +end +function RECOVERYTANKER:SetPatternUpdateDistance(distancechange) +self.Dupdate=UTILS.NMToMeters(distancechange or 5) +return self +end +function RECOVERYTANKER:SetPatternUpdateHeading(headingchange) +self.Hupdate=headingchange or 5 +return self +end +function RECOVERYTANKER:SetLowFuelThreshold(fuelthreshold) +self.lowfuel=fuelthreshold or 10 +return self +end +function RECOVERYTANKER:SetHomeBase(airbase,terminaltype) +if type(airbase)=="string"then +self.airbase=AIRBASE:FindByName(airbase) +else +self.airbase=airbase +end +if not self.airbase then +self:E(self.lid.."ERROR: Airbase is nil!") +end +if terminaltype then +self.terminaltype=terminaltype +end +return self +end +function RECOVERYTANKER:SetRecoveryAirboss(switch) +if switch==true or switch==nil then +self.recovery=true +else +self.recovery=false +end +return self +end +function RECOVERYTANKER:SetAWACS(switch,eplrs) +if switch==nil or switch==true then +self.awacs=true +else +self.awacs=false +end +if eplrs==nil or eplrs==true then +self.eplrs=true +else +self.eplrs=false +end +return self +end +function RECOVERYTANKER:SetCallsign(callsignname,callsignnumber) +self.callsignname=callsignname +self.callsignnumber=callsignnumber +return self +end +function RECOVERYTANKER:SetModex(modex) +self.modex=modex +return self +end +function RECOVERYTANKER:SetTakeoff(takeofftype) +self.takeoff=takeofftype +return self +end +function RECOVERYTANKER:SetTakeoffHot() +self:SetTakeoff(SPAWN.Takeoff.Hot) +return self +end +function RECOVERYTANKER:SetTakeoffCold() +self:SetTakeoff(SPAWN.Takeoff.Cold) +return self +end +function RECOVERYTANKER:SetTakeoffAir() +self:SetTakeoff(SPAWN.Takeoff.Air) +return self +end +function RECOVERYTANKER:SetRespawnOn() +self.respawn=true +return self +end +function RECOVERYTANKER:SetRespawnOff() +self.respawn=false +return self +end +function RECOVERYTANKER:SetRespawnOnOff(switch) +if switch==nil or switch==true then +self.respawn=true +else +self.respawn=false +end +return self +end +function RECOVERYTANKER:SetRespawnInAir() +self.respawninair=true +return self +end +function RECOVERYTANKER:SetUseUncontrolledAircraft() +self.uncontrolledac=true +return self +end +function RECOVERYTANKER:SetTACANoff() +self.TACANon=false +return self +end +function RECOVERYTANKER:SetTACAN(channel,morse) +self.TACANchannel=channel or 1 +self.TACANmode="Y" +self.TACANmorse=morse or"TKR" +self.TACANon=true +return self +end +function RECOVERYTANKER:SetRadio(frequency,modulation) +self.RadioFreq=frequency or 251 +self.RadioModu=modulation or"AM" +return self +end +function RECOVERYTANKER:SetDebugModeON() +self.Debug=true +return self +end +function RECOVERYTANKER:SetDebugModeOFF() +self.Debug=false +return self +end +function RECOVERYTANKER:IsReturning() +return self:is("Returning") +end +function RECOVERYTANKER:IsReturned() +return self:is("Returned") +end +function RECOVERYTANKER:IsRunning() +return self:is("Running") +end +function RECOVERYTANKER:IsRefueling() +return self:is("Refueling") +end +function RECOVERYTANKER:IsStopped() +return self:is("Stopped") +end +function RECOVERYTANKER:GetAlias() +return self.alias +end +function RECOVERYTANKER:GetUnitName() +local unit=self.tanker:GetUnit(1) +if unit then +return unit:GetName() +end +return nil +end +function RECOVERYTANKER:onafterStart(From,Event,To) +self:I(string.format("Starting Recovery Tanker v%s for carrier unit %s of type %s for tanker group %s.",RECOVERYTANKER.version,self.carrier:GetName(),self.carriertype,self.tankergroupname)) +self:HandleEvent(EVENTS.EngineShutdown) +self:HandleEvent(EVENTS.Land) +self:HandleEvent(EVENTS.Refueling,self._RefuelingStart) +self:HandleEvent(EVENTS.RefuelingStop,self._RefuelingStop) +self:HandleEvent(EVENTS.Crash,self._OnEventCrashOrDead) +self:HandleEvent(EVENTS.Dead,self._OnEventCrashOrDead) +local Spawn=SPAWN:NewWithAlias(self.tankergroupname,self.alias) +Spawn:InitRadioCommsOnOff(true) +Spawn:InitRadioFrequency(self.RadioFreq) +Spawn:InitRadioModulation(self.RadioModu) +Spawn:InitModex(self.modex) +if self.takeoff==SPAWN.Takeoff.Air then +local hdg=self.carrier:GetHeading() +local dist=-self.distStern+UTILS.NMToMeters(4) +local Carrier=self.carrier:GetCoordinate():Translate(dist,hdg+190):SetAltitude(self.altitude) +Spawn:InitHeading(hdg+10) +self.tanker=Spawn:SpawnFromCoordinate(Carrier) +else +if self.uncontrolledac then +self.tanker=GROUP:FindByName(self.tankergroupname) +if self.tanker:IsAlive()then +self.tanker:StartUncontrolled() +else +self:E(string.format("ERROR: No uncontrolled (alive) tanker group with name %s could be found!",self.tankergroupname)) +return +end +else +self.tanker=Spawn:SpawnAtAirbase(self.airbase,self.takeoff,nil,self.terminaltype) +end +end +self:ScheduleOnce(1,self._InitRoute,self,-self.distStern+UTILS.NMToMeters(3)) +if self.TACANon then +self:_ActivateTACAN(2) +end +if self.callsignname then +self.tanker:CommandSetCallsign(self.callsignname,self.callsignnumber,2) +end +if self.eplrs then +self.tanker:CommandEPLRS(true,3) +end +self.orientation=self.carrier:GetOrientationX() +self.orientlast=self.carrier:GetOrientationX() +self.position=self.carrier:GetCoordinate() +self:__Status(10) +end +function RECOVERYTANKER:onafterStatus(From,Event,To) +local time=timer.getTime() +if self.tanker and self.tanker:IsAlive()then +local fuel=self.tanker:GetFuel()*100 +local life=self.tanker:GetUnit(1):GetLife() +local life0=self.tanker:GetUnit(1):GetLife0() +local lifeR=self.tanker:GetUnit(1):GetLifeRelative() +local text=string.format("Recovery tanker %s: state=%s fuel=%.1f, life=%.1f/%.1f=%d",self.tanker:GetName(),self:GetState(),fuel,life,life0,lifeR*100) +self:T(self.lid..text) +MESSAGE:New(text,10):ToAllIf(self.Debug) +if self:IsRunning()then +if fuel100 then +return +end +local text=string.format("Recovery tanker %s started refueling unit %s",self.tanker:GetName(),receiver:GetName()) +MESSAGE:New(text,10,"DEBUG"):ToAllIf(self.Debug) +self:T(self.lid..text) +self:RefuelStart(receiver) +end +end +function RECOVERYTANKER:_RefuelingStop(EventData) +if EventData and EventData.IniUnit and EventData.IniUnit:IsAlive()then +local receiver=EventData.IniUnit +local dist=receiver:GetCoordinate():Get2DDistance(self.tanker:GetCoordinate()) +if dist>100 then +return +end +local text=string.format("Recovery tanker %s stopped refueling unit %s",self.tanker:GetName(),receiver:GetName()) +MESSAGE:New(text,10,"DEBUG"):ToAllIf(self.Debug) +self:T(self.lid..text) +self:RefuelStop(receiver) +end +end +function RECOVERYTANKER:_OnEventCrashOrDead(EventData) +self:F2({eventdata=EventData}) +if EventData and EventData.IniUnit then +local unit=EventData.IniUnit +local unitname=tostring(EventData.IniUnitName) +if EventData.IniGroupName==self.tanker:GetName()then +self:E(self.lid..string.format("Recovery tanker %s crashed!",unitname)) +self:Stop() +if self.respawn then +self:__Start(5) +end +end +end +end +function RECOVERYTANKER:_InitPatternTaskFunction() +local carriername=self.carrier:GetName() +local DCSScript={} +DCSScript[#DCSScript+1]=string.format('local mycarrier = UNIT:FindByName(\"%s\") ',carriername) +DCSScript[#DCSScript+1]=string.format('local mytanker = mycarrier:GetState(mycarrier, \"RECOVERYTANKER_%d\") ',self.uid) +DCSScript[#DCSScript+1]=string.format('mytanker:PatternUpdate()') +local DCSTask=CONTROLLABLE.TaskWrappedAction(self,CONTROLLABLE.CommandDoScript(self,table.concat(DCSScript))) +return DCSTask +end +function RECOVERYTANKER:_InitRoute(dist,delay) +dist=dist or UTILS.NMToMeters(8) +delay=delay or 1 +self:T(self.lid..string.format("Initializing route of recovery tanker %s.",self.tanker:GetName())) +local Carrier=self.carrier:GetCoordinate() +local hdg=self.carrier:GetHeading() +local p=Carrier:Translate(dist,hdg+190):SetAltitude(self.altitude) +local speed=self.tanker:GetSpeedMax()*0.8 +if self.Debug then +p:MarkToAll(string.format("Enter Pattern WP: alt=%d ft, speed=%d kts",UTILS.MetersToFeet(self.altitude),speed*0.539957)) +end +local task=self:_InitPatternTaskFunction() +local wp={} +if self.takeoff==SPAWN.Takeoff.Air then +wp[#wp+1]=self.tanker:GetCoordinate():SetAltitude(self.altitude):WaypointAirTurningPoint(nil,speed,{},"Spawn Position") +else +wp[#wp+1]=Carrier:WaypointAirTakeOffParking() +end +wp[#wp+1]=p:WaypointAirTurningPoint(nil,speed,{task},"Enter Pattern") +self.tanker:Route(wp,delay) +self:__Run(1) +self.Tupdate=nil +end +function RECOVERYTANKER:_CheckPatternUpdate(dt) +local pos=self.carrier:GetCoordinate() +local vNew=self.carrier:GetOrientationX() +local vOld=self.orientation +local vLast=self.orientlast +vNew.y=0;vOld.y=0;vLast.y=0 +local deltaHeading=math.deg(math.acos(UTILS.VecDot(vNew,vOld)/UTILS.VecNorm(vNew)/UTILS.VecNorm(vOld))) +local deltaLast=math.deg(math.acos(UTILS.VecDot(vNew,vLast)/UTILS.VecNorm(vNew)/UTILS.VecNorm(vLast))) +self.orientlast=vNew +local turning=deltaLast>=1 +if turning then +self:T2(self.lid..string.format("Carrier is turning. Delta Heading = %.1f",deltaLast)) +end +local Hchange=false +if math.abs(deltaHeading)>=self.Hupdate then +self:T(self.lid..string.format("Carrier heading changed by %d degrees. Turning=%s.",deltaHeading,tostring(turning))) +Hchange=true +end +local dist=pos:Get2DDistance(self.position) +local Dchange=false +if dist>self.Dupdate then +self:T(self.lid..string.format("Carrier position changed by %.1f NM. Turning=%s.",UTILS.MetersToNM(dist),tostring(turning))) +Dchange=true +end +local update=false +if self:IsRunning()and dt>self.dTupdate and not turning then +if Hchange or Dchange then +local text=string.format("Updating tanker %s pattern due to carrier position=%s or heading=%s change.",self.tanker:GetName(),tostring(Dchange),tostring(Hchange)) +MESSAGE:New(text,10,"DEBUG"):ToAllIf(self.Debug) +self:T(self.lid..text) +self.orientation=vNew +self.position=pos +update=true +end +end +return update +end +function RECOVERYTANKER:_ActivateTACAN(delay) +if delay and delay>0 then +self:ScheduleOnce(delay,RECOVERYTANKER._ActivateTACAN,self) +else +local unit=self.tanker:GetUnit(1) +if unit and unit:IsAlive()then +local text=string.format("Activating TACAN beacon: channel=%d mode=%s, morse=%s.",self.TACANchannel,self.TACANmode,self.TACANmorse) +MESSAGE:New(text,10,"DEBUG"):ToAllIf(self.Debug) +self:T(self.lid..text) +self.beacon=BEACON:New(unit) +self.beacon:ActivateTACAN(self.TACANchannel,self.TACANmode,self.TACANmorse,true) +else +self:E(self.lid.."ERROR: Recovery tanker is not alive!") +end +end +end +function RECOVERYTANKER:_Pattern() +local hdg=self.carrier:GetHeading() +local alt=self.altitude +local Carrier=self.carrier:GetCoordinate() +local width=UTILS.NMToMeters(8) +local p={} +p[1]=self.tanker:GetCoordinate() +p[2]=Carrier:SetAltitude(alt) +p[3]=p[2]:Translate(self.distBow,hdg) +p[4]=p[3]:Translate(width/math.sqrt(2),hdg-45) +p[5]=p[3]:Translate(width,hdg-90) +p[6]=p[5]:Translate(self.distStern-self.distBow,hdg) +p[7]=p[2]:Translate(self.distStern,hdg) +local wp={} +for i=1,#p do +local coord=p[i] +coord:MarkToAll(string.format("Waypoint %d",i)) +table.insert(wp,coord:WaypointAirTurningPoint(nil,UTILS.MpsToKmph(self.speed))) +end +return wp +end +RESCUEHELO={ +ClassName="RESCUEHELO", +Debug=false, +lid=nil, +carrier=nil, +carriertype=nil, +helogroupname=nil, +helo=nil, +airbase=nil, +takeoff=nil, +followset=nil, +formation=nil, +lowfuel=nil, +altitude=nil, +offsetX=nil, +offsetZ=nil, +rescuezone=nil, +respawn=nil, +respawninair=nil, +uncontrolledac=nil, +rescueon=nil, +rescueduration=nil, +rescuespeed=nil, +rescuestopboat=nil, +HeloFuel0=nil, +rtb=nil, +carrierstop=nil, +alias=nil, +uid=0, +modex=nil, +dtFollow=nil, +} +_RESCUEHELOID=0 +RESCUEHELO.version="1.1.0" +function RESCUEHELO:New(carrierunit,helogroupname) +local self=BASE:Inherit(self,FSM:New()) +if type(carrierunit)=="string"then +self.carrier=UNIT:FindByName(carrierunit) +else +self.carrier=carrierunit +end +self.carriertype=self.carrier:GetTypeName() +self.helogroupname=helogroupname +_RESCUEHELOID=_RESCUEHELOID+1 +self.uid=_RESCUEHELOID +self.carrier:SetState(self.carrier,string.format("RESCUEHELO_%d",self.uid),self) +self.alias=string.format("%s_%s_%02d",self.carrier:GetName(),self.helogroupname,_RESCUEHELOID) +self.lid=string.format("RESCUEHELO %s | ",self.alias) +self:SetHomeBase(AIRBASE:FindByName(self.carrier:GetName())) +self:SetTakeoffHot() +self:SetLowFuelThreshold() +self:SetAltitude() +self:SetOffsetX() +self:SetOffsetZ() +self:SetRespawnOn() +self:SetRescueOn() +self:SetRescueZone() +self:SetRescueHoverSpeed() +self:SetRescueDuration() +self:SetFollowTimeInterval() +self:SetRescueStopBoatOff() +self.rtb=false +self.carrierstop=false +if false then +self.Debug=true +BASE:TraceOnOff(true) +BASE:TraceClass(self.ClassName) +BASE:TraceLevel(1) +end +self:SetStartState("Stopped") +self:AddTransition("Stopped","Start","Running") +self:AddTransition("Running","Rescue","Rescuing") +self:AddTransition("Running","RTB","Returning") +self:AddTransition("Rescuing","RTB","Returning") +self:AddTransition("Returning","Returned","Returned") +self:AddTransition("Running","Run","Running") +self:AddTransition("Returned","Run","Running") +self:AddTransition("*","Status","*") +self:AddTransition("*","Stop","Stopped") +return self +end +function RESCUEHELO:SetLowFuelThreshold(threshold) +self.lowfuel=threshold or 5 +return self +end +function RESCUEHELO:SetHomeBase(airbase) +if type(airbase)=="string"then +self.airbase=AIRBASE:FindByName(airbase) +else +self.airbase=airbase +end +if not self.airbase then +self:E(self.lid.."ERROR: Airbase is nil!") +end +return self +end +function RESCUEHELO:SetRescueZone(radius) +radius=UTILS.NMToMeters(radius or 15) +self.rescuezone=ZONE_UNIT:New("Rescue Zone",self.carrier,radius) +return self +end +function RESCUEHELO:SetRescueHoverSpeed(speed) +self.rescuespeed=UTILS.KnotsToMps(speed or 5) +return self +end +function RESCUEHELO:SetRescueDuration(duration) +self.rescueduration=(duration or 5)*60 +return self +end +function RESCUEHELO:SetRescueOn() +self.rescueon=true +return self +end +function RESCUEHELO:SetRescueOff() +self.rescueon=false +return self +end +function RESCUEHELO:SetRescueStopBoatOn() +self.rescuestopboat=true +return self +end +function RESCUEHELO:SetRescueStopBoatOff() +self.rescuestopboat=false +return self +end +function RESCUEHELO:SetTakeoff(takeofftype) +self.takeoff=takeofftype or SPAWN.Takeoff.Hot +return self +end +function RESCUEHELO:SetTakeoffHot() +self:SetTakeoff(SPAWN.Takeoff.Hot) +return self +end +function RESCUEHELO:SetTakeoffCold() +self:SetTakeoff(SPAWN.Takeoff.Cold) +return self +end +function RESCUEHELO:SetTakeoffAir() +self:SetTakeoff(SPAWN.Takeoff.Air) +return self +end +function RESCUEHELO:SetAltitude(alt) +self.altitude=alt or 70 +return self +end +function RESCUEHELO:SetOffsetX(distance) +self.offsetX=distance or 200 +return self +end +function RESCUEHELO:SetOffsetZ(distance) +self.offsetZ=distance or 240 +return self +end +function RESCUEHELO:SetRespawnOn() +self.respawn=true +return self +end +function RESCUEHELO:SetRespawnOff() +self.respawn=false +return self +end +function RESCUEHELO:SetRespawnOnOff(switch) +if switch==nil or switch==true then +self.respawn=true +else +self.respawn=false +end +return self +end +function RESCUEHELO:SetRespawnInAir() +self.respawninair=true +return self +end +function RESCUEHELO:SetModex(modex) +self.modex=modex +return self +end +function RESCUEHELO:SetFollowTimeInterval(dt) +self.dtFollow=dt or 1.0 +return self +end +function RESCUEHELO:SetUseUncontrolledAircraft() +self.uncontrolledac=true +return self +end +function RESCUEHELO:SetDebugModeON() +self.Debug=true +return self +end +function RESCUEHELO:SetDebugModeOFF() +self.Debug=false +return self +end +function RESCUEHELO:IsReturning() +return self:is("Returning") +end +function RESCUEHELO:IsRunning() +return self:is("Running") +end +function RESCUEHELO:IsRescuing() +return self:is("Rescuing") +end +function RESCUEHELO:IsStopped() +return self:is("Stopped") +end +function RESCUEHELO:GetAlias() +return self.alias +end +function RESCUEHELO:GetUnitName() +local unit=self.helo:GetUnit(1) +if unit then +return unit:GetName() +end +return nil +end +function RESCUEHELO:OnEventLand(EventData) +local group=EventData.IniGroup +if group and group:IsAlive()then +local groupname=group:GetName() +if groupname==self.helo:GetName()then +local airbase=nil +local airbasename="unknown" +if EventData.Place then +airbase=EventData.Place +airbasename=airbase:GetName() +end +local text=string.format("Rescue helo group %s landed at airbase %s.",groupname,airbasename) +MESSAGE:New(text,10,"DEBUG"):ToAllIf(self.Debug) +self:T(self.lid..text) +if self:IsRescuing()then +self:T(self.lid..string.format("Rescue helo %s returned from rescue operation.",groupname)) +end +if self.takeoff==SPAWN.Takeoff.Air or self.respawninair then +if not self:IsRescuing()then +self:E(self.lid..string.format("WARNING: Rescue helo %s landed. This should not happen for Takeoff=Air or respawninair=true and no rescue operation in progress.",groupname)) +end +end +self:__Returned(3,airbase) +end +end +end +function RESCUEHELO:_OnEventCrashOrEject(EventData) +self:F2({eventdata=EventData}) +if EventData and EventData.IniUnit then +local unit=EventData.IniUnit +local unitname=tostring(EventData.IniUnitName) +if EventData.IniGroupName~=self.helo:GetName()then +local text=string.format("Unit %s crashed or ejected.",unitname) +MESSAGE:New(text,10,"DEBUG"):ToAllIf(self.Debug) +self:I(self.lid..text) +local coord=unit:GetCoordinate() +if coord and self.rescuezone:IsCoordinateInZone(coord)then +if self.Debug then +coord:MarkToCoalition(self.lid..string.format("Crash site of unit %s.",unitname),self.helo:GetCoalition()) +end +local rightcoalition=EventData.IniGroup:GetCoalition()==self.helo:GetCoalition() +if self:IsRunning()and self.rescueon and rightcoalition then +self:Rescue(coord) +end +end +else +self:E(self.lid..string.format("Rescue helo %s crashed!",unitname)) +self:Stop() +if self.respawn then +self:__Start(5) +end +end +end +end +function RESCUEHELO:onafterStart(From,Event,To) +local text=string.format("Starting Rescue Helo Formation v%s for carrier unit %s of type %s.",RESCUEHELO.version,self.carrier:GetName(),self.carriertype) +self:I(self.lid..text) +self:HandleEvent(EVENTS.Land) +self:HandleEvent(EVENTS.Crash,self._OnEventCrashOrEject) +self:HandleEvent(EVENTS.Ejection,self._OnEventCrashOrEject) +local delay=120 +local Spawn=SPAWN:NewWithAlias(self.helogroupname,self.alias) +Spawn:InitModex(self.modex) +if self.takeoff==SPAWN.Takeoff.Air then +local hdg=self.carrier:GetHeading() +local dist=UTILS.NMToMeters(0.2) +local Carrier=self.carrier:GetCoordinate():Translate(dist,hdg):SetAltitude(math.max(100,self.altitude)) +Spawn:InitHeading(hdg) +self.helo=Spawn:SpawnFromCoordinate(Carrier) +delay=1 +else +if self.uncontrolledac then +self.helo=GROUP:FindByName(self.helogroupname) +if self.helo and self.helo:IsAlive()then +self.helo:StartUncontrolled() +delay=60 +else +self:E(string.format("ERROR: No uncontrolled (alive) rescue helo group with name %s could be found!",self.helogroupname)) +return +end +else +self.helo=Spawn:SpawnAtAirbase(self.airbase,self.takeoff,nil,AIRBASE.TerminalType.HelicopterUsable) +if self.takeoff==SPAWN.Takeoff.Runway then +delay=5 +elseif self.takeoff==SPAWN.Takeoff.Hot then +delay=30 +elseif self.takeoff==SPAWN.Takeoff.Cold then +delay=60 +end +end +end +self.followset=SET_GROUP:New() +self.followset:AddGroup(self.helo) +self.HeloFuel0=self.helo:GetFuel() +self.formation=AI_FORMATION:New(self.carrier,self.followset,"Helo Formation with Carrier","Follow Carrier at given parameters.") +self.formation:FormationCenterWing(-self.offsetX,50,math.abs(self.altitude),50,self.offsetZ,50) +self.formation:SetFollowTimeInterval(self.dtFollow) +self.formation:SetFlightModeFormation(self.helo) +self.formation:__Start(delay) +self:__Status(1) +end +function RESCUEHELO:onafterStatus(From,Event,To) +local time=timer.getTime() +if self.helo and self.helo:IsAlive()then +local fuel=self.helo:GetFuel()*100 +local fuelrel=fuel/self.HeloFuel0 +local life=self.helo:GetUnit(1):GetLife() +local life0=self.helo:GetUnit(1):GetLife0() +local lifeR=self.helo:GetUnit(1):GetLifeRelative() +local text=string.format("Rescue Helo %s: state=%s fuel=%.1f, rel.fuel=%.1f, life=%.1f/%.1f=%d",self.helo:GetName(),self:GetState(),fuel,fuelrel,life,life0,lifeR*100) +MESSAGE:New(text,10,"DEBUG"):ToAllIf(self.Debug) +self:T(self.lid..text) +if self:IsRunning()then +if fuelUTILS.FeetToMeters(1500)then +dust=nil +end +local visibilitymin=visibility +if fog then +if fog.visibility10 then +reportedviz=10 +end +VISIBILITY=string.format("%d",reportedviz) +else +local reportedviz=UTILS.Round(UTILS.MetersToSM(visibilitymin)) +if reportedviz>10 then +reportedviz=10 +end +VISIBILITY=string.format("%d",reportedviz) +end +local cloudbase=clouds.base +local cloudceil=clouds.base+clouds.thickness +local clouddens=clouds.density +local cloudspreset=clouds.preset or"Nothing" +local precepitation=0 +if cloudspreset:find("Preset10")then +clouddens=4 +elseif cloudspreset:find("Preset11")then +clouddens=4 +elseif cloudspreset:find("Preset12")then +clouddens=4 +elseif cloudspreset:find("Preset13")then +clouddens=7 +elseif cloudspreset:find("Preset14")then +clouddens=7 +elseif cloudspreset:find("Preset15")then +clouddens=7 +elseif cloudspreset:find("Preset16")then +clouddens=7 +elseif cloudspreset:find("Preset17")then +clouddens=7 +elseif cloudspreset:find("Preset18")then +clouddens=7 +elseif cloudspreset:find("Preset19")then +clouddens=7 +elseif cloudspreset:find("Preset20")then +clouddens=7 +elseif cloudspreset:find("Preset21")then +clouddens=9 +elseif cloudspreset:find("Preset22")then +clouddens=9 +elseif cloudspreset:find("Preset23")then +clouddens=9 +elseif cloudspreset:find("Preset24")then +clouddens=9 +elseif cloudspreset:find("Preset25")then +clouddens=9 +elseif cloudspreset:find("Preset26")then +clouddens=9 +elseif cloudspreset:find("Preset27")then +clouddens=9 +elseif cloudspreset:find("Preset1")then +clouddens=1 +elseif cloudspreset:find("Preset2")then +clouddens=1 +elseif cloudspreset:find("Preset3")then +clouddens=4 +elseif cloudspreset:find("Preset4")then +clouddens=4 +elseif cloudspreset:find("Preset5")then +clouddens=4 +elseif cloudspreset:find("Preset6")then +clouddens=4 +elseif cloudspreset:find("Preset7")then +clouddens=4 +elseif cloudspreset:find("Preset8")then +clouddens=4 +elseif cloudspreset:find("Preset9")then +clouddens=4 +elseif cloudspreset:find("RainyPreset")then +clouddens=9 +if temperature>5 then +precepitation=1 +else +precepitation=3 +end +end +local CLOUDBASE=string.format("%d",UTILS.MetersToFeet(cloudbase)) +local CLOUDCEIL=string.format("%d",UTILS.MetersToFeet(cloudceil)) +if self.metric then +CLOUDBASE=string.format("%d",cloudbase) +CLOUDCEIL=string.format("%d",cloudceil) +end +local CLOUDBASE1000,CLOUDBASE0100=self:_GetThousandsAndHundreds(UTILS.MetersToFeet(cloudbase)) +local CLOUDCEIL1000,CLOUDCEIL0100=self:_GetThousandsAndHundreds(UTILS.MetersToFeet(cloudceil)) +if self.metric then +CLOUDBASE1000,CLOUDBASE0100=self:_GetThousandsAndHundreds(cloudbase) +CLOUDCEIL1000,CLOUDCEIL0100=self:_GetThousandsAndHundreds(cloudceil) +end +local CloudCover={} +CloudCover=ATIS.Sound.CloudsNotAvailable +local CLOUDSsub="Cloud coverage information not available" +if static then +if clouddens>=9 then +CloudCover=ATIS.Sound.CloudsOvercast +CLOUDSsub="Overcast" +elseif clouddens>=7 then +CloudCover=ATIS.Sound.CloudsBroken +CLOUDSsub="Broken clouds" +elseif clouddens>=4 then +CloudCover=ATIS.Sound.CloudsScattered +CLOUDSsub="Scattered clouds" +elseif clouddens>=1 then +CloudCover=ATIS.Sound.CloudsFew +CLOUDSsub="Few clouds" +else +CLOUDBASE=nil +CLOUDCEIL=nil +CloudCover=ATIS.Sound.CloudsNo +CLOUDSsub="No clouds" +end +end +local subtitle="" +subtitle=string.format("%s",self.airbasename) +if self.airbasename:find("AFB")==nil and self.airbasename:find("Airport")==nil and self.airbasename:find("Airstrip")==nil and self.airbasename:find("airfield")==nil and self.airbasename:find("AB")==nil then +subtitle=subtitle.." Airport" +end +self.radioqueue:NewTransmission(string.format("%s/%s.ogg",self.theatre,self.airbasename),3.0,self.soundpath,nil,nil,subtitle,self.subduration) +local alltext=subtitle +subtitle=string.format("Information %s",NATO) +local _INFORMATION=subtitle +self:Transmission(ATIS.Sound.Information,0.5,subtitle) +self.radioqueue:NewTransmission(string.format("NATO Alphabet/%s.ogg",NATO),0.75,self.soundpath) +alltext=alltext..";\n"..subtitle +subtitle=string.format("%s Zulu",ZULU) +self.radioqueue:Number2Transmission(ZULU,nil,0.5) +self:Transmission(ATIS.Sound.Zulu,0.2,subtitle) +alltext=alltext..";\n"..subtitle +if not self.zulutimeonly then +subtitle=string.format("Sunrise at %s local time",SUNRISE) +self:Transmission(ATIS.Sound.SunriseAt,0.5,subtitle) +self.radioqueue:Number2Transmission(SUNRISE,nil,0.2) +self:Transmission(ATIS.Sound.TimeLocal,0.2) +alltext=alltext..";\n"..subtitle +subtitle=string.format("Sunset at %s local time",SUNSET) +self:Transmission(ATIS.Sound.SunsetAt,0.5,subtitle) +self.radioqueue:Number2Transmission(SUNSET,nil,0.5) +self:Transmission(ATIS.Sound.TimeLocal,0.2) +alltext=alltext..";\n"..subtitle +end +if self.metric then +subtitle=string.format("Wind from %s at %s m/s",WINDFROM,WINDSPEED) +else +subtitle=string.format("Wind from %s at %s knots",WINDFROM,WINDSPEED) +end +if turbulence>0 then +subtitle=subtitle..", gusting" +end +local _WIND=subtitle +self:Transmission(ATIS.Sound.WindFrom,1.0,subtitle) +self.radioqueue:Number2Transmission(WINDFROM) +self:Transmission(ATIS.Sound.At,0.2) +self.radioqueue:Number2Transmission(WINDSPEED) +if self.metric then +self:Transmission(ATIS.Sound.MetersPerSecond,0.2) +else +self:Transmission(ATIS.Sound.Knots,0.2) +end +if turbulence>0 then +self:Transmission(ATIS.Sound.Gusting,0.2) +end +alltext=alltext..";\n"..subtitle +if self.metric then +subtitle=string.format("Visibility %s km",VISIBILITY) +else +subtitle=string.format("Visibility %s SM",VISIBILITY) +end +self:Transmission(ATIS.Sound.Visibilty,1.0,subtitle) +self.radioqueue:Number2Transmission(VISIBILITY) +if self.metric then +self:Transmission(ATIS.Sound.Kilometers,0.2) +else +self:Transmission(ATIS.Sound.StatuteMiles,0.2) +end +alltext=alltext..";\n"..subtitle +local wp=false +local wpsub="" +if precepitation==1 then +wp=true +wpsub=wpsub.." rain" +elseif precepitation==2 then +if wp then +wpsub=wpsub.."," +end +wpsub=wpsub.." thunderstorm" +wp=true +elseif precepitation==3 then +wpsub=wpsub.." snow" +wp=true +elseif precepitation==4 then +wpsub=wpsub.." snowstorm" +wp=true +end +if fog then +if wp then +wpsub=wpsub.."," +end +wpsub=wpsub.." fog" +wp=true +end +if dust then +if wp then +wpsub=wpsub.."," +end +wpsub=wpsub.." dust" +wp=true +end +if wp then +subtitle=string.format("Weather phenomena:%s",wpsub) +self:Transmission(ATIS.Sound.WeatherPhenomena,1.0,subtitle) +if precepitation==1 then +self:Transmission(ATIS.Sound.Rain,0.5) +elseif precepitation==2 then +self:Transmission(ATIS.Sound.ThunderStorm,0.5) +elseif precepitation==3 then +self:Transmission(ATIS.Sound.Snow,0.5) +elseif precepitation==4 then +self:Transmission(ATIS.Sound.SnowStorm,0.5) +end +if fog then +self:Transmission(ATIS.Sound.Fog,0.5) +end +if dust then +self:Transmission(ATIS.Sound.Dust,0.5) +end +alltext=alltext..";\n"..subtitle +end +self:Transmission(CloudCover,1.0,CLOUDSsub) +if CLOUDBASE and static then +if self.metric then +subtitle=string.format("Cloudbase %s, ceiling %s meters",CLOUDBASE,CLOUDCEIL) +else +subtitle=string.format("Cloudbase %s, ceiling %s ft",CLOUDBASE,CLOUDCEIL) +end +self:Transmission(ATIS.Sound.CloudBase,1.0,subtitle) +if tonumber(CLOUDBASE1000)>0 then +self.radioqueue:Number2Transmission(CLOUDBASE1000) +self:Transmission(ATIS.Sound.Thousand,0.1) +end +if tonumber(CLOUDBASE0100)>0 then +self.radioqueue:Number2Transmission(CLOUDBASE0100) +self:Transmission(ATIS.Sound.Hundred,0.1) +end +self:Transmission(ATIS.Sound.CloudCeiling,0.5) +if tonumber(CLOUDCEIL1000)>0 then +self.radioqueue:Number2Transmission(CLOUDCEIL1000) +self:Transmission(ATIS.Sound.Thousand,0.1) +end +if tonumber(CLOUDCEIL0100)>0 then +self.radioqueue:Number2Transmission(CLOUDCEIL0100) +self:Transmission(ATIS.Sound.Hundred,0.1) +end +if self.metric then +self:Transmission(ATIS.Sound.Meters,0.1) +else +self:Transmission(ATIS.Sound.Feet,0.1) +end +end +alltext=alltext..";\n"..subtitle +if self.TDegF then +if temperature<0 then +subtitle=string.format("Temperature -%s °F",TEMPERATURE) +else +subtitle=string.format("Temperature %s °F",TEMPERATURE) +end +else +if temperature<0 then +subtitle=string.format("Temperature -%s °C",TEMPERATURE) +else +subtitle=string.format("Temperature %s °C",TEMPERATURE) +end +end +local _TEMPERATURE=subtitle +self:Transmission(ATIS.Sound.Temperature,1.0,subtitle) +if temperature<0 then +self:Transmission(ATIS.Sound.Minus,0.2) +end +self.radioqueue:Number2Transmission(TEMPERATURE) +if self.TDegF then +self:Transmission(ATIS.Sound.DegreesFahrenheit,0.2) +else +self:Transmission(ATIS.Sound.DegreesCelsius,0.2) +end +alltext=alltext..";\n"..subtitle +if self.TDegF then +if dewpoint<0 then +subtitle=string.format("Dew point -%s °F",DEWPOINT) +else +subtitle=string.format("Dew point %s °F",DEWPOINT) +end +else +if dewpoint<0 then +subtitle=string.format("Dew point -%s °C",DEWPOINT) +else +subtitle=string.format("Dew point %s °C",DEWPOINT) +end +end +local _DEWPOINT=subtitle +self:Transmission(ATIS.Sound.DewPoint,1.0,subtitle) +if dewpoint<0 then +self:Transmission(ATIS.Sound.Minus,0.2) +end +self.radioqueue:Number2Transmission(DEWPOINT) +if self.TDegF then +self:Transmission(ATIS.Sound.DegreesFahrenheit,0.2) +else +self:Transmission(ATIS.Sound.DegreesCelsius,0.2) +end +alltext=alltext..";\n"..subtitle +if self.PmmHg then +if self.qnhonly then +subtitle=string.format("Altimeter %s.%s mmHg",QNH[1],QNH[2]) +else +subtitle=string.format("Altimeter QNH %s.%s, QFE %s.%s mmHg",QNH[1],QNH[2],QFE[1],QFE[2]) +end +else +if self.metric then +if self.qnhonly then +subtitle=string.format("Altimeter %s.%s hPa",QNH[1],QNH[2]) +else +subtitle=string.format("Altimeter QNH %s.%s, QFE %s.%s hPa",QNH[1],QNH[2],QFE[1],QFE[2]) +end +else +if self.qnhonly then +subtitle=string.format("Altimeter %s.%s inHg",QNH[1],QNH[2]) +else +subtitle=string.format("Altimeter QNH %s.%s, QFE %s.%s inHg",QNH[1],QNH[2],QFE[1],QFE[2]) +end +end +end +local _ALTIMETER=subtitle +self:Transmission(ATIS.Sound.Altimeter,1.0,subtitle) +if not self.qnhonly then +self:Transmission(ATIS.Sound.QNH,0.5) +end +self.radioqueue:Number2Transmission(QNH[1]) +if ATIS.ICAOPhraseology[UTILS.GetDCSMap()]then +self:Transmission(ATIS.Sound.Decimal,0.2) +end +self.radioqueue:Number2Transmission(QNH[2]) +if not self.qnhonly then +self:Transmission(ATIS.Sound.QFE,0.75) +self.radioqueue:Number2Transmission(QFE[1]) +if ATIS.ICAOPhraseology[UTILS.GetDCSMap()]then +self:Transmission(ATIS.Sound.Decimal,0.2) +end +self.radioqueue:Number2Transmission(QFE[2]) +end +if self.PmmHg then +self:Transmission(ATIS.Sound.MillimetersOfMercury,0.1) +else +if self.metric then +self:Transmission(ATIS.Sound.HectoPascal,0.1) +else +self:Transmission(ATIS.Sound.InchesOfMercury,0.1) +end +end +alltext=alltext..";\n"..subtitle +local subtitle=string.format("Active runway %s",runway) +if rwyLeft==true then +subtitle=subtitle.." Left" +elseif rwyLeft==false then +subtitle=subtitle.." Right" +end +local _RUNACT=subtitle +self:Transmission(ATIS.Sound.ActiveRunway,1.0,subtitle) +self.radioqueue:Number2Transmission(runway) +if rwyLeft==true then +self:Transmission(ATIS.Sound.Left,0.2) +elseif rwyLeft==false then +self:Transmission(ATIS.Sound.Right,0.2) +end +alltext=alltext..";\n"..subtitle +if self.rwylength then +local runact=self.airbase:GetActiveRunway(self.runwaym2t) +local length=runact.length +if not self.metric then +length=UTILS.MetersToFeet(length) +end +local L1000,L0100=self:_GetThousandsAndHundreds(length) +local subtitle=string.format("Runway length %d",length) +if self.metric then +subtitle=subtitle.." meters" +else +subtitle=subtitle.." feet" +end +self:Transmission(ATIS.Sound.RunwayLength,1.0,subtitle) +if tonumber(L1000)>0 then +self.radioqueue:Number2Transmission(L1000) +self:Transmission(ATIS.Sound.Thousand,0.1) +end +if tonumber(L0100)>0 then +self.radioqueue:Number2Transmission(L0100) +self:Transmission(ATIS.Sound.Hundred,0.1) +end +if self.metric then +self:Transmission(ATIS.Sound.Meters,0.1) +else +self:Transmission(ATIS.Sound.Feet,0.1) +end +alltext=alltext..";\n"..subtitle +end +if self.elevation then +local elevation=self.airbase:GetHeight() +if not self.metric then +elevation=UTILS.MetersToFeet(elevation) +end +local L1000,L0100=self:_GetThousandsAndHundreds(elevation) +local subtitle=string.format("Elevation %d",elevation) +if self.metric then +subtitle=subtitle.." meters" +else +subtitle=subtitle.." feet" +end +self:Transmission(ATIS.Sound.Elevation,1.0,subtitle) +if tonumber(L1000)>0 then +self.radioqueue:Number2Transmission(L1000) +self:Transmission(ATIS.Sound.Thousand,0.1) +end +if tonumber(L0100)>0 then +self.radioqueue:Number2Transmission(L0100) +self:Transmission(ATIS.Sound.Hundred,0.1) +end +if self.metric then +self:Transmission(ATIS.Sound.Meters,0.1) +else +self:Transmission(ATIS.Sound.Feet,0.1) +end +alltext=alltext..";\n"..subtitle +end +if self.towerfrequency then +local freqs="" +for i,freq in pairs(self.towerfrequency)do +freqs=freqs..string.format("%.3f MHz",freq) +if i<#self.towerfrequency then +freqs=freqs..", " +end +end +subtitle=string.format("Tower frequency %s",freqs) +self:Transmission(ATIS.Sound.TowerFrequency,1.0,subtitle) +for _,freq in pairs(self.towerfrequency)do +local f=string.format("%.3f",freq) +f=UTILS.Split(f,".") +self.radioqueue:Number2Transmission(f[1],nil,0.5) +if tonumber(f[2])>0 then +self:Transmission(ATIS.Sound.Decimal,0.2) +self.radioqueue:Number2Transmission(f[2]) +end +self:Transmission(ATIS.Sound.MegaHertz,0.2) +end +alltext=alltext..";\n"..subtitle +end +local ils=self:GetNavPoint(self.ils,runway,rwyLeft) +if ils then +subtitle=string.format("ILS frequency %.2f MHz",ils.frequency) +self:Transmission(ATIS.Sound.ILSFrequency,1.0,subtitle) +local f=string.format("%.2f",ils.frequency) +f=UTILS.Split(f,".") +self.radioqueue:Number2Transmission(f[1],nil,0.5) +if tonumber(f[2])>0 then +self:Transmission(ATIS.Sound.Decimal,0.2) +self.radioqueue:Number2Transmission(f[2]) +end +self:Transmission(ATIS.Sound.MegaHertz,0.2) +alltext=alltext..";\n"..subtitle +end +local ndb=self:GetNavPoint(self.ndbouter,runway,rwyLeft) +if ndb then +subtitle=string.format("Outer NDB frequency %.2f MHz",ndb.frequency) +self:Transmission(ATIS.Sound.OuterNDBFrequency,1.0,subtitle) +local f=string.format("%.2f",ndb.frequency) +f=UTILS.Split(f,".") +self.radioqueue:Number2Transmission(f[1],nil,0.5) +if tonumber(f[2])>0 then +self:Transmission(ATIS.Sound.Decimal,0.2) +self.radioqueue:Number2Transmission(f[2]) +end +self:Transmission(ATIS.Sound.MegaHertz,0.2) +alltext=alltext..";\n"..subtitle +end +local ndb=self:GetNavPoint(self.ndbinner,runway,rwyLeft) +if ndb then +subtitle=string.format("Inner NDB frequency %.2f MHz",ndb.frequency) +self:Transmission(ATIS.Sound.InnerNDBFrequency,1.0,subtitle) +local f=string.format("%.2f",ndb.frequency) +f=UTILS.Split(f,".") +self.radioqueue:Number2Transmission(f[1],nil,0.5) +if tonumber(f[2])>0 then +self:Transmission(ATIS.Sound.Decimal,0.2) +self.radioqueue:Number2Transmission(f[2]) +end +self:Transmission(ATIS.Sound.MegaHertz,0.2) +alltext=alltext..";\n"..subtitle +end +if self.vor then +subtitle=string.format("VOR frequency %.2f MHz",self.vor) +self:Transmission(ATIS.Sound.VORFrequency,1.0,subtitle) +local f=string.format("%.2f",self.vor) +f=UTILS.Split(f,".") +self.radioqueue:Number2Transmission(f[1],nil,0.5) +if tonumber(f[2])>0 then +self:Transmission(ATIS.Sound.Decimal,0.2) +self.radioqueue:Number2Transmission(f[2]) +end +self:Transmission(ATIS.Sound.MegaHertz,0.2) +alltext=alltext..";\n"..subtitle +end +if self.tacan then +subtitle=string.format("TACAN channel %dX",self.tacan) +self:Transmission(ATIS.Sound.TACANChannel,1.0,subtitle) +self.radioqueue:Number2Transmission(tostring(self.tacan),nil,0.2) +self.radioqueue:NewTransmission("NATO Alphabet/Xray.ogg",0.75,self.soundpath,nil,0.2) +alltext=alltext..";\n"..subtitle +end +if self.rsbn then +subtitle=string.format("RSBN channel %d",self.rsbn) +self:Transmission(ATIS.Sound.RSBNChannel,1.0,subtitle) +self.radioqueue:Number2Transmission(tostring(self.rsbn),nil,0.2) +alltext=alltext..";\n"..subtitle +end +local ndb=self:GetNavPoint(self.prmg,runway,rwyLeft) +if ndb then +subtitle=string.format("PRMG channel %d",ndb.frequency) +self:Transmission(ATIS.Sound.PRMGChannel,1.0,subtitle) +self.radioqueue:Number2Transmission(tostring(ndb.frequency),nil,0.5) +alltext=alltext..";\n"..subtitle +end +subtitle=string.format("Advise on initial contact, you have information %s",NATO) +self:Transmission(ATIS.Sound.AdviceOnInitial,0.5,subtitle) +self.radioqueue:NewTransmission(string.format("NATO Alphabet/%s.ogg",NATO),0.75,self.soundpath) +alltext=alltext..";\n"..subtitle +self:Report(alltext) +if self.usemarker then +self:UpdateMarker(_INFORMATION,_RUNACT,_WIND,_ALTIMETER,_TEMPERATURE) +end +end +function ATIS:onafterReport(From,Event,To,Text) +self:T(self.lid..string.format("Report:\n%s",Text)) +end +function ATIS:UpdateMarker(information,runact,wind,altimeter,temperature) +if self.markerid then +self.airbase:GetCoordinate():RemoveMark(self.markerid) +end +local text=string.format("ATIS on %.3f %s, %s:\n",self.frequency,UTILS.GetModulationName(self.modulation),tostring(information)) +text=text..string.format("%s\n",tostring(runact)) +text=text..string.format("%s\n",tostring(wind)) +text=text..string.format("%s\n",tostring(altimeter)) +text=text..string.format("%s",tostring(temperature)) +self.markerid=self.airbase:GetCoordinate():MarkToAll(text,true) +return self.markerid +end +function ATIS:GetActiveRunway() +local coord=self.airbase:GetCoordinate() +local height=coord:GetLandHeight() +local windFrom,windSpeed=coord:GetWind(height+10) +local runact=self.airbase:GetActiveRunway(self.runwaym2t) +local runway=self:GetMagneticRunway(windFrom)or runact.idx +local rwyLeft=nil +if self.activerunway then +local runwayno=self:GetRunwayWithoutLR(self.activerunway) +if runwayno~=""then +runway=runwayno +end +rwyLeft=self:GetRunwayLR(self.activerunway) +end +return runway,rwyLeft +end +function ATIS:GetMagneticRunway(windfrom) +local diffmin=nil +local runway=nil +for _,heading in pairs(self.runwaymag)do +local hdg=self:GetRunwayWithoutLR(heading) +local diff=UTILS.HdgDiff(windfrom,tonumber(hdg)*10) +if diffmin==nil or diffself.Tstop or false then +return false +end +local startme=self:EvalConditionsAll(self.conditionStart) +if not startme then +return false +end +return true +end +function AUFTRAG:IsReadyToCancel() +local Tnow=timer.getAbsTime() +if self.Tstop and Tnow>self.Tstop then +return true +end +local failure=self:EvalConditionsAny(self.conditionFailure) +if failure then +self.failurecondition=true +return true +end +local success=self:EvalConditionsAny(self.conditionSuccess) +if success then +self.successcondition=true +return true +end +return false +end +function AUFTRAG:EvalConditionsAll(Conditions) +for _,_condition in pairs(Conditions or{})do +local condition=_condition +local istrue=condition.func(unpack(condition.arg)) +if not istrue then +return false +end +end +return true +end +function AUFTRAG:EvalConditionsAny(Conditions) +for _,_condition in pairs(Conditions or{})do +local condition=_condition +local istrue=condition.func(unpack(condition.arg)) +if istrue then +return true +end +end +return false +end +function AUFTRAG:onafterStatus(From,Event,To) +local Tnow=timer.getAbsTime() +local Ntargets=self:CountMissionTargets() +local Ntargets0=self:GetTargetInitialNumber() +local Ngroups=self:CountOpsGroups() +if self:IsNotOver()then +if self:CheckGroupsDone()then +self:Done() +elseif(self.Tstop and Tnow>self.Tstop+10)or(Ntargets0>0 and Ntargets==0)then +self:Cancel() +end +end +local fsmstate=self:GetState() +if fsmstate~=self.status then +self:E(self.lid..string.format("ERROR: FSM state %s != %s mission status!",fsmstate,self.status)) +end +if self.verbose>=1 then +local Cstart=UTILS.SecondsToClock(self.Tstart,true) +local Cstop=self.Tstop and UTILS.SecondsToClock(self.Tstop,true)or"INF" +local targetname=self:GetTargetName()or"unknown" +local airwing=self.airwing and self.airwing.alias or"N/A" +local commander=self.wingcommander and tostring(self.wingcommander.coalition)or"N/A" +self:I(self.lid..string.format("Status %s: Target=%s, T=%s-%s, assets=%d, groups=%d, targets=%d, wing=%s, commander=%s",self.status,targetname,Cstart,Cstop,#self.assets,Ngroups,Ntargets,airwing,commander)) +end +if self.verbose>=2 then +local text="Group data:" +for groupname,_groupdata in pairs(self.groupdata)do +local groupdata=_groupdata +text=text..string.format("\n- %s: status mission=%s opsgroup=%s",groupname,groupdata.status,groupdata.opsgroup and groupdata.opsgroup:GetState()or"N/A") +end +self:I(self.lid..text) +end +local ready2evaluate=self.Tover and Tnow-self.Tover>=self.dTevaluate or false +if self:IsOver()and ready2evaluate then +self:Evaluate() +else +self:__Status(-30) +end +if self.markerOn then +self:UpdateMarker() +end +end +function AUFTRAG:Evaluate() +local failed=false +local targetdamage=self:GetTargetDamage() +local owndamage=self.Ncasualties/self.Nelements*100 +local Ntargets=self:CountMissionTargets() +local Ntargets0=self:GetTargetInitialNumber() +local Life=self:GetTargetLife() +local Life0=self:GetTargetInitialLife() +if Ntargets0>0 then +if self.type==AUFTRAG.Type.TROOPTRANSPORT or self.type==AUFTRAG.Type.ESCORT then +if Ntargets0 then +failed=true +end +end +else +if self.Nelements==self.Ncasualties then +failed=true +end +end +local successCondition=self:EvalConditionsAny(self.conditionSuccess) +local failureCondition=self:EvalConditionsAny(self.conditionFailure) +if failureCondition then +failed=true +elseif successCondition then +failed=false +end +local text=string.format("Evaluating mission:\n") +text=text..string.format("Own casualties = %d/%d\n",self.Ncasualties,self.Nelements) +text=text..string.format("Own losses = %.1f %%\n",owndamage) +text=text..string.format("Killed units = %d\n",self.Nkills) +text=text..string.format("--------------------------\n") +text=text..string.format("Targets left = %d/%d\n",Ntargets,Ntargets0) +text=text..string.format("Targets life = %.1f/%.1f\n",Life,Life0) +text=text..string.format("Enemy losses = %.1f %%\n",targetdamage) +text=text..string.format("--------------------------\n") +text=text..string.format("Success Cond = %s\n",tostring(successCondition)) +text=text..string.format("Failure Cond = %s\n",tostring(failureCondition)) +text=text..string.format("--------------------------\n") +text=text..string.format("Final Success = %s\n",tostring(not failed)) +text=text..string.format("=========================") +self:I(self.lid..text) +if failed then +self:Failed() +else +self:Success() +end +return self +end +function AUFTRAG:GetOpsGroups() +local opsgroups={} +for _,_groupdata in pairs(self.groupdata or{})do +local groupdata=_groupdata +table.insert(opsgroups,groupdata.opsgroup) +end +return opsgroups +end +function AUFTRAG:GetAssetDataByName(AssetName) +return self.groupdata[tostring(AssetName)] +end +function AUFTRAG:GetGroupData(opsgroup) +if opsgroup and self.groupdata then +return self.groupdata[opsgroup.groupname] +end +return nil +end +function AUFTRAG:SetGroupStatus(opsgroup,status) +self:T(self.lid..string.format("Setting flight %s to status %s",opsgroup and opsgroup.groupname or"nil",tostring(status))) +if self:GetGroupStatus(opsgroup)==AUFTRAG.GroupStatus.CANCELLED and status==AUFTRAG.GroupStatus.DONE then +else +local groupdata=self:GetGroupData(opsgroup) +if groupdata then +groupdata.status=status +else +self:E(self.lid.."WARNING: Could not SET flight data for flight group. Setting status to DONE") +end +end +self:T2(self.lid..string.format("Setting flight %s status to %s. IsNotOver=%s CheckGroupsDone=%s",opsgroup.groupname,self:GetGroupStatus(opsgroup),tostring(self:IsNotOver()),tostring(self:CheckGroupsDone()))) +if self:IsNotOver()and self:CheckGroupsDone()then +self:T3(self.lid.."All flights done ==> mission DONE!") +self:Done() +else +self:T3(self.lid.."Mission NOT DONE yet!") +end +end +function AUFTRAG:GetGroupStatus(opsgroup) +self:T3(self.lid..string.format("Trying to get Flight status for flight group %s",opsgroup and opsgroup.groupname or"nil")) +local groupdata=self:GetGroupData(opsgroup) +if groupdata then +return groupdata.status +else +self:E(self.lid..string.format("WARNING: Could not GET groupdata for opsgroup %s. Returning status DONE.",opsgroup and opsgroup.groupname or"nil")) +return AUFTRAG.GroupStatus.DONE +end +end +function AUFTRAG:SetGroupWaypointCoordinate(opsgroup,coordinate) +local groupdata=self:GetGroupData(opsgroup) +if groupdata then +groupdata.waypointcoordinate=coordinate +end +end +function AUFTRAG:GetGroupWaypointCoordinate(opsgroup) +local groupdata=self:GetGroupData(opsgroup) +if groupdata then +return groupdata.waypointcoordinate +end +end +function AUFTRAG:SetGroupWaypointTask(opsgroup,task) +self:T2(self.lid..string.format("Setting waypoint task %s",task and task.description or"WTF")) +local groupdata=self:GetGroupData(opsgroup) +if groupdata then +groupdata.waypointtask=task +end +end +function AUFTRAG:GetGroupWaypointTask(opsgroup) +local groupdata=self:GetGroupData(opsgroup) +if groupdata then +return groupdata.waypointtask +end +end +function AUFTRAG:SetGroupWaypointIndex(opsgroup,waypointindex) +self:T2(self.lid..string.format("Setting waypoint index %d",waypointindex)) +local groupdata=self:GetGroupData(opsgroup) +if groupdata then +groupdata.waypointindex=waypointindex +end +end +function AUFTRAG:GetGroupWaypointIndex(opsgroup) +local groupdata=self:GetGroupData(opsgroup) +if groupdata then +return groupdata.waypointindex +end +end +function AUFTRAG:CheckGroupsDone() +if self:IsPlanned()or self:IsQueued()or self:IsRequested()then +return false +end +if self:IsStarted()and self:CountOpsGroups()==0 then +return true +end +for groupname,data in pairs(self.groupdata)do +local groupdata=data +if groupdata then +if groupdata.status==AUFTRAG.GroupStatus.DONE or groupdata.status==AUFTRAG.GroupStatus.CANCELLED then +else +return false +end +end +end +return true +end +function AUFTRAG:OnEventUnitLost(EventData) +if EventData and EventData.IniGroup and EventData.IniUnit then +local unit=EventData.IniUnit +local group=EventData.IniGroup +local unitname=EventData.IniUnitName +for _,_groupdata in pairs(self.groupdata)do +local groupdata=_groupdata +if groupdata and groupdata.opsgroup and groupdata.opsgroup.groupname==EventData.IniGroupName then +self:I(self.lid..string.format("UNIT LOST event for opsgroup %s unit %s",groupdata.opsgroup.groupname,EventData.IniUnitName)) +end +end +end +end +function AUFTRAG:onafterPlanned(From,Event,To) +self.status=AUFTRAG.Status.PLANNED +self:T(self.lid..string.format("New mission status=%s",self.status)) +end +function AUFTRAG:onafterQueued(From,Event,To,Airwing) +self.status=AUFTRAG.Status.QUEUED +self.airwing=Airwing +self:T(self.lid..string.format("New mission status=%s at airwing %s",self.status,tostring(Airwing.alias))) +end +function AUFTRAG:onafterRequested(From,Event,To) +self.status=AUFTRAG.Status.REQUESTED +self:T(self.lid..string.format("New mission status=%s",self.status)) +end +function AUFTRAG:onafterAssign(From,Event,To) +self.status=AUFTRAG.Status.ASSIGNED +self:T(self.lid..string.format("New mission status=%s",self.status)) +end +function AUFTRAG:onafterScheduled(From,Event,To) +self.status=AUFTRAG.Status.SCHEDULED +self:T(self.lid..string.format("New mission status=%s",self.status)) +end +function AUFTRAG:onafterStarted(From,Event,To) +self.status=AUFTRAG.Status.STARTED +self:T(self.lid..string.format("New mission status=%s",self.status)) +end +function AUFTRAG:onafterExecuting(From,Event,To) +self.status=AUFTRAG.Status.EXECUTING +self:T(self.lid..string.format("New mission status=%s",self.status)) +end +function AUFTRAG:onafterDone(From,Event,To) +self.status=AUFTRAG.Status.DONE +self:T(self.lid..string.format("New mission status=%s",self.status)) +self.Tover=timer.getAbsTime() +end +function AUFTRAG:onafterElementDestroyed(From,Event,To,OpsGroup,Element) +self.Ncasualties=self.Ncasualties+1 +end +function AUFTRAG:onafterGroupDead(From,Event,To,OpsGroup) +local asset=self:GetAssetByName(OpsGroup.groupname) +if asset then +self:AssetDead(asset) +end +end +function AUFTRAG:onafterAssetDead(From,Event,To,Asset) +local N=self:CountOpsGroups() +self:I(self.lid..string.format("Asset %s dead! Number of ops groups remaining %d",tostring(Asset.spawngroupname),N)) +if N==0 then +if self:IsNotOver()then +self:Cancel() +else +end +end +self:DelAsset(Asset) +end +function AUFTRAG:onafterCancel(From,Event,To) +self:I(self.lid..string.format("CANCELLING mission in status %s. Will wait for groups to report mission DONE before evaluation",self.status)) +self.Tover=timer.getAbsTime() +self.Nrepeat=self.repeated +self.NrepeatFailure=self.repeatedFailure +self.NrepeatSuccess=self.repeatedSuccess +self.dTevaluate=0 +if self.wingcommander then +self:T(self.lid..string.format("Wingcommander will cancel the mission. Will wait for mission DONE before evaluation!")) +self.wingcommander:CancelMission(self) +elseif self.airwing then +self:T(self.lid..string.format("Airwing %s will cancel the mission. Will wait for mission DONE before evaluation!",self.airwing.alias)) +self.airwing:MissionCancel(self) +else +self:T(self.lid..string.format("No airwing or wingcommander. Attached flights will cancel the mission on their own. Will wait for mission DONE before evaluation!")) +for _,_groupdata in pairs(self.groupdata)do +local groupdata=_groupdata +groupdata.opsgroup:MissionCancel(self) +end +end +if self.status==AUFTRAG.Status.PLANNED then +self:T(self.lid..string.format("Cancelled mission was in planned stage. Call it done!")) +self:Done() +end +end +function AUFTRAG:onafterSuccess(From,Event,To) +self.status=AUFTRAG.Status.SUCCESS +self:T(self.lid..string.format("New mission status=%s",self.status)) +local repeatme=self.repeatedSuccess Repeat mission!",self.repeated+1,N)) +self:Repeat() +else +self:I(self.lid..string.format("Mission SUCCESS! Number of max repeats %d reached ==> Stopping mission!",self.repeated+1)) +self:Stop() +end +end +function AUFTRAG:onafterFailed(From,Event,To) +self.status=AUFTRAG.Status.FAILED +self:T(self.lid..string.format("New mission status=%s",self.status)) +local repeatme=self.repeatedFailure Repeat mission!",self.repeated+1,N)) +self:Repeat() +else +self:I(self.lid..string.format("Mission FAILED! Number of max repeats %d reached ==> Stopping mission!",self.repeated+1)) +self:Stop() +end +end +function AUFTRAG:onafterRepeat(From,Event,To) +self.status=AUFTRAG.Status.PLANNED +self:T(self.lid..string.format("New mission status=%s (on Repeat)",self.status)) +self.repeated=self.repeated+1 +if self.chief then +elseif self.wingcommander then +if self.airwing then +self.airwing:RemoveMission(self) +end +elseif self.airwing then +self:Queued(self.airwing) +else +self:E(self.lid.."ERROR: Mission can only be repeated by a CHIEF, WINGCOMMANDER or AIRWING! Stopping AUFTRAG") +self:Stop() +end +self.assets={} +for _,_groupdata in pairs(self.groupdata)do +local groupdata=_groupdata +local opsgroup=groupdata.opsgroup +if opsgroup then +self:DelOpsGroup(opsgroup) +end +end +self.groupdata={} +self.Ncasualties=0 +self.Nelements=0 +self:__Status(-30) +end +function AUFTRAG:onafterStop(From,Event,To) +self:I(self.lid..string.format("STOPPED mission in status=%s. Removing missions from queues. Stopping CallScheduler!",self.status)) +if self.wingcommander then +self.wingcommander:RemoveMission(self) +end +if self.airwing then +self.airwing:RemoveMission(self) +end +for _,_groupdata in pairs(self.groupdata)do +local groupdata=_groupdata +groupdata.opsgroup:RemoveMission(self) +end +self.assets={} +self.groupdata={} +self.CallScheduler:Clear() +end +function AUFTRAG:_TargetFromObject(Object) +if not self.engageTarget then +if Object:IsInstanceOf("TARGET")then +self.engageTarget=Object +else +self.engageTarget=TARGET:New(Object) +end +else +end +return self +end +function AUFTRAG:CountMissionTargets() +if self.engageTarget then +return self.engageTarget:CountTargets() +else +return 0 +end +end +function AUFTRAG:GetTargetInitialNumber() +local target=self:GetTargetData() +if target then +return target.N0 +else +return 0 +end +end +function AUFTRAG:GetTargetInitialLife() +local target=self:GetTargetData() +if target then +return target.life0 +else +return 0 +end +end +function AUFTRAG:GetTargetDamage() +local target=self:GetTargetData() +if target then +return target:GetDamage() +else +return 0 +end +end +function AUFTRAG:GetTargetLife() +local target=self:GetTargetData() +if target then +return target:GetLife() +else +return 0 +end +end +function AUFTRAG:GetTargetData() +return self.engageTarget +end +function AUFTRAG:GetObjective() +return self:GetTargetData():GetObject() +end +function AUFTRAG:GetTargetType() +return self:GetTargetData().Type +end +function AUFTRAG:GetTargetVec2() +local coord=self:GetTargetCoordinate() +if coord then +return coord:GetVec2() +end +return nil +end +function AUFTRAG:GetTargetCoordinate() +if self.transportPickup then +return self.transportPickup +elseif self.engageTarget then +return self.engageTarget:GetCoordinate() +else +self:E(self.lid.."ERROR: Cannot get target coordinate!") +end +return nil +end +function AUFTRAG:GetTargetName() +if self.engageTarget then +return self.engageTarget:GetName() +end +return"N/A" +end +function AUFTRAG:GetTargetDistance(FromCoord) +local TargetCoord=self:GetTargetCoordinate() +if TargetCoord and FromCoord then +return TargetCoord:Get2DDistance(FromCoord) +else +self:E(self.lid.."ERROR: TargetCoord or FromCoord does not exist in AUFTRAG:GetTargetDistance() function! Returning 0") +end +return 0 +end +function AUFTRAG:AddAsset(Asset) +self.assets=self.assets or{} +table.insert(self.assets,Asset) +return self +end +function AUFTRAG:DelAsset(Asset) +for i,_asset in pairs(self.assets or{})do +local asset=_asset +if asset.uid==Asset.uid then +self:T(self.lid..string.format("Removing asset \"%s\" from mission",tostring(asset.spawngroupname))) +table.remove(self.assets,i) +return self +end +end +return self +end +function AUFTRAG:GetAssetByName(Name) +for i,_asset in pairs(self.assets or{})do +local asset=_asset +if asset.spawngroupname==Name then +return asset +end +end +return nil +end +function AUFTRAG:CountOpsGroups() +local N=0 +for _,_groupdata in pairs(self.groupdata)do +local groupdata=_groupdata +if groupdata and groupdata.opsgroup and groupdata.opsgroup:IsAlive()and not groupdata.opsgroup:IsDead()then +N=N+1 +end +end +return N +end +function AUFTRAG:GetMissionTypesText(MissionTypes) +local text="" +for _,missiontype in pairs(MissionTypes)do +text=text..string.format("%s, ",missiontype) +end +return text +end +function AUFTRAG:SetMissionWaypointCoord(Coordinate) +self.missionWaypointCoord=Coordinate +end +function AUFTRAG:GetMissionWaypointCoord(group) +if self.missionWaypointCoord then +local coord=self.missionWaypointCoord +if self.missionAltitude then +coord.y=self.missionAltitude +end +return coord +end +local waypointcoord=group:GetCoordinate():GetIntermediateCoordinate(self:GetTargetCoordinate(),self.missionFraction) +local alt=waypointcoord.y +waypointcoord=ZONE_RADIUS:New("Temp",waypointcoord:GetVec2(),1000):GetRandomCoordinate():SetAltitude(alt,false) +if self.missionAltitude then +waypointcoord:SetAltitude(self.missionAltitude,true) +end +return waypointcoord +end +function AUFTRAG:_SetLogID() +self.lid=string.format("Auftrag #%d %s | ",self.auftragsnummer,tostring(self.type)) +return self +end +function AUFTRAG:UpdateMarker() +local text=string.format("%s %s: %s",self.name,self.type:upper(),self.status:upper()) +text=text..string.format("\n%s",self:GetTargetName()) +text=text..string.format("\nTargets %d/%d, Life Points=%d/%d",self:CountMissionTargets(),self:GetTargetInitialNumber(),self:GetTargetLife(),self:GetTargetInitialLife()) +text=text..string.format("\nFlights %d/%d",self:CountOpsGroups(),self.nassets) +if not self.marker then +local targetcoord=self:GetTargetCoordinate() +if self.markerCoaliton and self.markerCoaliton>=0 then +self.marker=MARKER:New(targetcoord,text):ReadOnly():ToCoalition(self.markerCoaliton) +else +self.marker=MARKER:New(targetcoord,text):ReadOnly():ToAll() +end +else +if self.marker:GetText()~=text then +self.marker:UpdateText(text) +end +end +return self +end +function AUFTRAG:GetDCSMissionTask(TaskControllable) +local DCStasks={} +if self.type==AUFTRAG.Type.ANTISHIP then +self:_GetDCSAttackTask(self.engageTarget,DCStasks) +elseif self.type==AUFTRAG.Type.AWACS then +local DCStask=CONTROLLABLE.EnRouteTaskAWACS(nil) +table.insert(self.enrouteTasks,DCStask) +elseif self.type==AUFTRAG.Type.BAI then +self:_GetDCSAttackTask(self.engageTarget,DCStasks) +elseif self.type==AUFTRAG.Type.BOMBING then +local DCStask=CONTROLLABLE.TaskBombing(nil,self:GetTargetVec2(),self.engageAsGroup,self.engageWeaponExpend,self.engageQuantity,self.engageDirection,self.engageAltitude,self.engageWeaponType,Divebomb) +table.insert(DCStasks,DCStask) +elseif self.type==AUFTRAG.Type.BOMBRUNWAY then +local DCStask=CONTROLLABLE.TaskBombingRunway(nil,self.engageTarget:GetObject(),self.engageWeaponType,self.engageWeaponExpend,self.engageQuantity,self.engageDirection,self.engageAsGroup) +table.insert(DCStasks,DCStask) +elseif self.type==AUFTRAG.Type.BOMBCARPET then +local DCStask=CONTROLLABLE.TaskCarpetBombing(nil,self:GetTargetVec2(),self.engageAsGroup,self.engageWeaponExpend,self.engageQuantity,self.engageDirection,self.engageAltitude,self.engageWeaponType,self.engageCarpetLength) +table.insert(DCStasks,DCStask) +elseif self.type==AUFTRAG.Type.CAP then +local DCStask=CONTROLLABLE.EnRouteTaskEngageTargetsInZone(nil,self.engageZone:GetVec2(),self.engageZone:GetRadius(),self.engageTargetTypes,Priority) +table.insert(self.enrouteTasks,DCStask) +elseif self.type==AUFTRAG.Type.CAS then +local DCStask=CONTROLLABLE.EnRouteTaskEngageTargetsInZone(nil,self.engageZone:GetVec2(),self.engageZone:GetRadius(),self.engageTargetTypes,Priority) +table.insert(self.enrouteTasks,DCStask) +elseif self.type==AUFTRAG.Type.ESCORT then +local DCStask=CONTROLLABLE.TaskEscort(nil,self.engageTarget:GetObject(),self.escortVec3,LastWaypointIndex,self.engageMaxDistance,self.engageTargetTypes) +table.insert(DCStasks,DCStask) +elseif self.type==AUFTRAG.Type.FACA then +local DCStask=CONTROLLABLE.TaskFAC_AttackGroup(nil,self.engageTarget:GetObject(),self.engageWeaponType,self.facDesignation,self.facDatalink,self.facFreq,self.facModu,CallsignName,CallsignNumber) +table.insert(DCStasks,DCStask) +elseif self.type==AUFTRAG.Type.FERRY then +elseif self.type==AUFTRAG.Type.INTERCEPT then +self:_GetDCSAttackTask(self.engageTarget,DCStasks) +elseif self.type==AUFTRAG.Type.ORBIT then +elseif self.type==AUFTRAG.Type.GCICAP then +elseif self.type==AUFTRAG.Type.RECON then +elseif self.type==AUFTRAG.Type.SEAD then +self:_GetDCSAttackTask(self.engageTarget,DCStasks) +elseif self.type==AUFTRAG.Type.STRIKE then +local DCStask=CONTROLLABLE.TaskAttackMapObject(nil,self:GetTargetVec2(),self.engageAsGroup,self.engageWeaponExpend,self.engageQuantity,self.engageDirection,self.engageAltitude,self.engageWeaponType) +table.insert(DCStasks,DCStask) +elseif self.type==AUFTRAG.Type.TANKER then +local DCStask=CONTROLLABLE.EnRouteTaskTanker(nil) +table.insert(self.enrouteTasks,DCStask) +elseif self.type==AUFTRAG.Type.TROOPTRANSPORT then +local TaskEmbark=CONTROLLABLE.TaskEmbarking(TaskControllable,self.transportPickup,self.transportGroupSet,self.transportWaitForCargo) +local TaskDisEmbark=CONTROLLABLE.TaskDisembarking(TaskControllable,self.transportDropoff,self.transportGroupSet) +table.insert(DCStasks,TaskEmbark) +table.insert(DCStasks,TaskDisEmbark) +elseif self.type==AUFTRAG.Type.RESCUEHELO then +local DCStask={} +DCStask.id="Formation" +local param={} +param.unitname=self:GetTargetName() +param.offsetX=200 +param.offsetZ=240 +param.altitude=70 +param.dtFollow=1.0 +DCStask.params=param +table.insert(DCStasks,DCStask) +elseif self.type==AUFTRAG.Type.ARTY then +local DCStask=CONTROLLABLE.TaskFireAtPoint(nil,self:GetTargetVec2(),self.artyRadius,self.artyShots,self.engageWeaponType) +table.insert(DCStasks,DCStask) +elseif self.type==AUFTRAG.Type.PATROLZONE then +local DCStask={} +DCStask.id="PatrolZone" +local param={} +param.zone=self:GetObjective() +param.altitude=self.missionAltitude +param.speed=self.missionSpeed +DCStask.params=param +table.insert(DCStasks,DCStask) +else +self:E(self.lid..string.format("ERROR: Unknown mission task!")) +return nil +end +if self.type==AUFTRAG.Type.ORBIT or +self.type==AUFTRAG.Type.CAP or +self.type==AUFTRAG.Type.CAS or +self.type==AUFTRAG.Type.GCICAP or +self.type==AUFTRAG.Type.AWACS or +self.type==AUFTRAG.Type.TANKER then +local Coordinate=self:GetTargetCoordinate() +local DCStask=CONTROLLABLE.TaskOrbit(nil,Coordinate,self.orbitAltitude,self.orbitSpeed,self.orbitRaceTrack) +table.insert(DCStasks,DCStask) +end +self:T3({missiontask=DCStasks}) +if#DCStasks==1 then +return DCStasks[1] +else +return CONTROLLABLE.TaskCombo(nil,DCStasks) +end +end +function AUFTRAG:_GetDCSAttackTask(Target,DCStasks) +DCStasks=DCStasks or{} +for _,_target in pairs(Target.targets)do +local target=_target +if target.Type==TARGET.ObjectType.GROUP then +local DCStask=CONTROLLABLE.TaskAttackGroup(nil,target.Object,self.engageWeaponType,self.engageWeaponExpend,self.engageQuantity,self.engageDirection,self.engageAltitude,self.engageAsGroup) +table.insert(DCStasks,DCStask) +elseif target.Type==TARGET.ObjectType.UNIT or target.Type==TARGET.ObjectType.STATIC then +local DCStask=CONTROLLABLE.TaskAttackUnit(nil,target.Object,self.engageAsGroup,self.WeaponExpend,self.engageQuantity,self.engageDirection,self.engageAltitude,self.engageWeaponType) +table.insert(DCStasks,DCStask) +end +end +return DCStasks +end +TARGET={ +ClassName="TARGET", +verbose=0, +lid=nil, +targets={}, +targetcounter=0, +life=0, +life0=0, +N0=0, +Ntargets0=0, +Ndestroyed=0, +Ndead=0, +elements={}, +casualties={}, +threatlevel0=0 +} +TARGET.ObjectType={ +GROUP="Group", +UNIT="Unit", +STATIC="Static", +SCENERY="Scenery", +COORDINATE="Coordinate", +AIRBASE="Airbase", +ZONE="Zone", +} +TARGET.Category={ +AIRCRAFT="Aircraft", +GROUND="Ground", +NAVAL="Naval", +AIRBASE="Airbase", +COORDINATE="Coordinate", +ZONE="Zone", +} +TARGET.ObjectStatus={ +ALIVE="Alive", +DEAD="Dead", +} +_TARGETID=0 +TARGET.version="0.3.1" +function TARGET:New(TargetObject) +local self=BASE:Inherit(self,FSM:New()) +_TARGETID=_TARGETID+1 +self:AddObject(TargetObject) +local Target=self.targets[1] +if not Target then +self:E("ERROR: No valid TARGET!") +return nil +end +self.name=self:GetTargetName(Target) +self.category=self:GetTargetCategory(Target) +self.lid=string.format("TARGET #%03d [%s] | ",_TARGETID,tostring(self.category)) +self:SetStartState("Stopped") +self:AddTransition("Stopped","Start","Alive") +self:AddTransition("*","Status","*") +self:AddTransition("*","Stop","Stopped") +self:AddTransition("*","ObjectDamaged","*") +self:AddTransition("*","ObjectDestroyed","*") +self:AddTransition("*","ObjectDead","*") +self:AddTransition("*","Damaged","*") +self:AddTransition("*","Destroyed","Dead") +self:AddTransition("*","Dead","Dead") +self:__Start(-1) +return self +end +function TARGET:AddObject(Object) +if Object:IsInstanceOf("SET_GROUP")or Object:IsInstanceOf("SET_UNIT")then +local set=Object +for _,object in pairs(set.Set)do +self:AddObject(object) +end +else +self:_AddObject(Object) +end +end +function TARGET:IsAlive() +return self:Is("Alive") +end +function TARGET:IsDead() +return self:Is("Dead") +end +function TARGET:onafterStart(From,Event,To) +local text=string.format("Starting Target") +self:T(self.lid..text) +self:HandleEvent(EVENTS.Dead,self.OnEventUnitDeadOrLost) +self:HandleEvent(EVENTS.UnitLost,self.OnEventUnitDeadOrLost) +self:HandleEvent(EVENTS.RemoveUnit,self.OnEventUnitDeadOrLost) +self:__Status(-1) +end +function TARGET:onafterStatus(From,Event,To) +local fsmstate=self:GetState() +local damaged=false +for i,_target in pairs(self.targets)do +local target=_target +local life=target.Life +target.Life=self:GetTargetLife(target) +if target.Life=1 then +local text=string.format("%s: Targets=%d/%d Life=%.1f/%.1f Damage=%.1f",fsmstate,self:CountTargets(),self.N0,self:GetLife(),self:GetLife0(),self:GetDamage()) +if damaged then +text=text.." Damaged!" +end +self:I(self.lid..text) +end +if self.verbose>=2 then +local text="Target:" +for i,_target in pairs(self.targets)do +local target=_target +local damage=(1-target.Life/target.Life0)*100 +text=text..string.format("\n[%d] %s %s %s: Life=%.1f/%.1f, Damage=%.1f",i,target.Type,target.Name,target.Status,target.Life,target.Life0,damage) +end +self:I(self.lid..text) +end +if self:IsAlive()then +self:__Status(-30) +end +end +function TARGET:onafterObjectDamaged(From,Event,To,Target) +self:T(self.lid..string.format("Object %s damaged",Target.Name)) +end +function TARGET:onafterObjectDestroyed(From,Event,To,Target) +self:T(self.lid..string.format("Object %s destroyed",Target.Name)) +self.Ndestroyed=self.Ndestroyed+1 +self:ObjectDead(Target) +end +function TARGET:onafterObjectDead(From,Event,To,Target) +self:T(self.lid..string.format("Object %s dead",Target.Name)) +Target.Status=TARGET.ObjectStatus.DEAD +self.Ndead=self.Ndead+1 +local dead=true +for _,_target in pairs(self.targets)do +local target=_target +if target.Status==TARGET.ObjectStatus.ALIVE then +dead=false +end +end +if dead then +if self.Ndestroyed==self.Ntargets0 then +self:Destroyed() +else +self:Dead() +end +end +end +function TARGET:onafterDamaged(From,Event,To) +self:T(self.lid..string.format("TARGET damaged")) +end +function TARGET:onafterDestroyed(From,Event,To) +self:T(self.lid..string.format("TARGET destroyed")) +self:Dead() +end +function TARGET:onafterDead(From,Event,To) +self:T(self.lid..string.format("TARGET dead")) +end +function TARGET:OnEventUnitDeadOrLost(EventData) +local Name=EventData and EventData.IniUnitName or nil +if self:IsElement(Name)and not self:IsCasualty(Name)then +self:T3(self.lid..string.format("EVENT ID=%d: Unit %s dead or lost!",EventData.id,tostring(Name))) +table.insert(self.casualties,Name) +local target=self:GetTargetByName(EventData.IniGroupName) +if not target then +target=self:GetTargetByName(EventData.IniUnitName) +end +if target then +if EventData.id==EVENTS.RemoveUnit then +target.Ndead=target.Ndead+1 +else +target.Ndestroyed=target.Ndestroyed+1 +target.Ndead=target.Ndead+1 +end +if target.Ndead==target.N0 then +if target.Ndestroyed>=target.N0 then +self:T2(self.lid..string.format("EVENT ID=%d: target %s dead/lost ==> destroyed",EventData.id,tostring(target.Name))) +self:ObjectDestroyed(target) +else +self:T2(self.lid..string.format("EVENT ID=%d: target %s removed ==> dead",EventData.id,tostring(target.Name))) +self:ObjectDead(target) +end +end +end +end +end +function TARGET:_AddObject(Object) +local target={} +target.N0=0 +target.Ndead=0 +target.Ndestroyed=0 +if Object:IsInstanceOf("GROUP")then +local group=Object +target.Type=TARGET.ObjectType.GROUP +target.Name=group:GetName() +target.Coordinate=group:GetCoordinate() +local units=group:GetUnits() +target.Life=0;target.Life0=0 +for _,_unit in pairs(units or{})do +local unit=_unit +local life=unit:GetLife() +target.Life=target.Life+life +target.Life0=target.Life0+math.max(unit:GetLife0(),life) +self.threatlevel0=self.threatlevel0+unit:GetThreatLevel() +table.insert(self.elements,unit:GetName()) +target.N0=target.N0+1 +end +elseif Object:IsInstanceOf("UNIT")then +local unit=Object +target.Type=TARGET.ObjectType.UNIT +target.Name=unit:GetName() +target.Coordinate=unit:GetCoordinate() +if unit then +target.Life=unit:GetLife() +target.Life0=math.max(unit:GetLife0(),target.Life) +self.threatlevel0=self.threatlevel0+unit:GetThreatLevel() +table.insert(self.elements,unit:GetName()) +target.N0=target.N0+1 +end +elseif Object:IsInstanceOf("STATIC")then +local static=Object +target.Type=TARGET.ObjectType.STATIC +target.Name=static:GetName() +target.Coordinate=static:GetCoordinate() +if static and static:IsAlive()then +target.Life0=1 +target.Life=1 +target.N0=target.N0+1 +table.insert(self.elements,target.Name) +end +elseif Object:IsInstanceOf("SCENERY")then +local scenery=Object +target.Type=TARGET.ObjectType.SCENERY +target.Name=scenery:GetName() +target.Coordinate=scenery:GetCoordinate() +target.Life0=1 +target.Life=1 +target.N0=target.N0+1 +table.insert(self.elements,target.Name) +elseif Object:IsInstanceOf("AIRBASE")then +local airbase=Object +target.Type=TARGET.ObjectType.AIRBASE +target.Name=airbase:GetName() +target.Coordinate=airbase:GetCoordinate() +target.Life0=1 +target.Life=1 +target.N0=target.N0+1 +table.insert(self.elements,target.Name) +elseif Object:IsInstanceOf("COORDINATE")then +local coord=UTILS.DeepCopy(Object) +target.Type=TARGET.ObjectType.COORDINATE +target.Name=coord:ToStringMGRS() +target.Coordinate=coord +target.Life0=1 +target.Life=1 +elseif Object:IsInstanceOf("ZONE_BASE")then +local zone=Object +Object=zone +target.Type=TARGET.ObjectType.ZONE +target.Name=zone:GetName() +target.Coordinate=zone:GetCoordinate() +target.Life0=1 +target.Life=1 +else +self:E(self.lid.."ERROR: Unknown object type!") +return nil +end +self.life=self.life+target.Life +self.life0=self.life0+target.Life0 +self.N0=self.N0+target.N0 +self.Ntargets0=self.Ntargets0+1 +self.targetcounter=self.targetcounter+1 +target.ID=self.targetcounter +target.Status=TARGET.ObjectStatus.ALIVE +target.Object=Object +table.insert(self.targets,target) +end +function TARGET:GetLife0() +return self.life0 +end +function TARGET:GetDamage() +local life=self:GetLife()/self:GetLife0() +local damage=1-life +return damage*100 +end +function TARGET:GetTargetLife(Target) +if Target.Type==TARGET.ObjectType.GROUP then +if Target.Object and Target.Object:IsAlive()then +local units=Target.Object:GetUnits() +local life=0 +for _,_unit in pairs(units or{})do +local unit=_unit +life=life+unit:GetLife() +end +return life +else +return 0 +end +elseif Target.Type==TARGET.ObjectType.UNIT then +local unit=Target.Object +if unit and unit:IsAlive()then +local life=unit:GetLife() +return life +else +return 0 +end +elseif Target.Type==TARGET.ObjectType.STATIC then +if Target.Object and Target.Object:IsAlive()then +return 1 +else +return 0 +end +elseif Target.Type==TARGET.ObjectType.SCENERY then +if Target.Status==TARGET.ObjectStatus.ALIVE then +return 1 +else +return 0 +end +elseif Target.Type==TARGET.ObjectType.AIRBASE then +if Target.Status==TARGET.ObjectStatus.ALIVE then +return 1 +else +return 0 +end +elseif Target.Type==TARGET.ObjectType.COORDINATE then +return 1 +elseif Target.Type==TARGET.ObjectType.ZONE then +return 1 +else +self:E("ERROR: unknown target object type in GetTargetLife!") +end +end +function TARGET:GetLife() +local N=0 +for _,_target in pairs(self.targets)do +local Target=_target +N=N+self:GetTargetLife(Target) +end +return N +end +function TARGET:GetTargetVec3(Target) +if Target.Type==TARGET.ObjectType.GROUP then +local object=Target.Object +if object and object:IsAlive()then +local vec3=object:GetVec3() +return vec3 +else +return nil +end +elseif Target.Type==TARGET.ObjectType.UNIT then +local object=Target.Object +if object and object:IsAlive()then +local vec3=object:GetVec3() +return vec3 +else +return nil +end +elseif Target.Type==TARGET.ObjectType.STATIC then +local object=Target.Object +if object and object:IsAlive()then +local vec3=object:GetVec3() +return vec3 +else +return nil +end +elseif Target.Type==TARGET.ObjectType.SCENERY then +local object=Target.Object +if object then +local vec3=object:GetVec3() +return vec3 +else +return nil +end +elseif Target.Type==TARGET.ObjectType.AIRBASE then +local object=Target.Object +local vec3=object:GetVec3() +return vec3 +elseif Target.Type==TARGET.ObjectType.COORDINATE then +local object=Target.Object +local vec3={x=object.x,y=object.y,z=object.z} +return vec3 +elseif Target.Type==TARGET.ObjectType.ZONE then +local object=Target.Object +local vec3=object:GetVec3() +return vec3 +end +self:E(self.lid.."ERROR: Unknown TARGET type! Cannot get Vec3") +end +function TARGET:GetTargetCoordinate(Target) +if Target.Type==TARGET.ObjectType.COORDINATE then +return Target.Object +else +local vec3=self:GetTargetVec3(Target) +if vec3 then +Target.Coordinate.x=vec3.x +Target.Coordinate.y=vec3.y +Target.Coordinate.z=vec3.z +end +return Target.Coordinate +end +return nil +end +function TARGET:GetTargetName(Target) +if Target.Type==TARGET.ObjectType.GROUP then +if Target.Object and Target.Object:IsAlive()then +return Target.Object:GetName() +end +elseif Target.Type==TARGET.ObjectType.UNIT then +if Target.Object and Target.Object:IsAlive()then +return Target.Object:GetName() +end +elseif Target.Type==TARGET.ObjectType.STATIC then +if Target.Object and Target.Object:IsAlive()then +return Target.Object:GetName() +end +elseif Target.Type==TARGET.ObjectType.AIRBASE then +if Target.Status==TARGET.ObjectStatus.ALIVE then +return Target.Object:GetName() +end +elseif Target.Type==TARGET.ObjectType.COORDINATE then +local coord=Target.Object +return coord:ToStringMGRS() +end +return"Unknown" +end +function TARGET:GetName() +return self.name +end +function TARGET:GetCoordinate() +for _,_target in pairs(self.targets)do +local Target=_target +local coordinate=self:GetTargetCoordinate(Target) +if coordinate then +return coordinate +end +end +self:E(self.lid..string.format("ERROR: Cannot get coordinate of target %s",self.name)) +return nil +end +function TARGET:GetTargetCategory(Target) +local category=nil +if Target.Type==TARGET.ObjectType.GROUP then +if Target.Object and Target.Object:IsAlive()~=nil then +local group=Target.Object +local cat=group:GetCategory() +if cat==Group.Category.AIRPLANE or cat==Group.Category.HELICOPTER then +category=TARGET.Category.AIRCRAFT +elseif cat==Group.Category.GROUND or cat==Group.Category.TRAIN then +category=TARGET.Category.GROUND +elseif cat==Group.Category.SHIP then +category=TARGET.Category.NAVAL +end +end +elseif Target.Type==TARGET.ObjectType.UNIT then +if Target.Object and Target.Object:IsAlive()~=nil then +local unit=Target.Object +local group=unit:GetGroup() +local cat=group:GetCategory() +if cat==Group.Category.AIRPLANE or cat==Group.Category.HELICOPTER then +category=TARGET.Category.AIRCRAFT +elseif cat==Group.Category.GROUND or cat==Group.Category.TRAIN then +category=TARGET.Category.GROUND +elseif cat==Group.Category.SHIP then +category=TARGET.Category.NAVAL +end +end +elseif Target.Type==TARGET.ObjectType.STATIC then +return TARGET.Category.GROUND +elseif Target.Type==TARGET.ObjectType.SCENERY then +return TARGET.Category.GROUND +elseif Target.Type==TARGET.ObjectType.AIRBASE then +return TARGET.Category.AIRBASE +elseif Target.Type==TARGET.ObjectType.COORDINATE then +return TARGET.Category.COORDINATE +elseif Target.Type==TARGET.ObjectType.ZONE then +return TARGET.Category.ZONE +else +self:E("ERROR: unknown target category!") +end +return category +end +function TARGET:GetTargetByName(ObjectName) +for _,_target in pairs(self.targets)do +local target=_target +if ObjectName==target.Name then +return target +end +end +return nil +end +function TARGET:GetObjective() +for _,_target in pairs(self.targets)do +local target=_target +if target.Status==TARGET.ObjectStatus.ALIVE then +return target +end +end +return nil +end +function TARGET:GetObject() +local target=self:GetObjective() +if target then +return target.Object +end +return nil +end +function TARGET:CountObjectives(Target) +local N=0 +if Target.Type==TARGET.ObjectType.GROUP then +local target=Target.Object +local units=target:GetUnits() +for _,_unit in pairs(units or{})do +local unit=_unit +if unit and unit:IsAlive()~=nil and unit:GetLife()>1 then +N=N+1 +end +end +elseif Target.Type==TARGET.ObjectType.UNIT then +local target=Target.Object +if target and target:IsAlive()~=nil and target:GetLife()>1 then +N=N+1 +end +elseif Target.Type==TARGET.ObjectType.STATIC then +local target=Target.Object +if target and target:IsAlive()then +N=N+1 +end +elseif Target.Type==TARGET.ObjectType.SCENERY then +if Target.Status==TARGET.ObjectStatus.ALIVE then +N=N+1 +end +elseif Target.Type==TARGET.ObjectType.AIRBASE then +if Target.Status==TARGET.ObjectStatus.ALIVE then +N=N+1 +end +elseif Target.Type==TARGET.ObjectType.COORDINATE then +elseif Target.Type==TARGET.ObjectType.ZONE then +else +self:E(self.lid.."ERROR: Unknown target type! Cannot count targets") +end +return N +end +function TARGET:CountTargets() +local N=0 +for _,_target in pairs(self.targets)do +local Target=_target +N=N+self:CountObjectives(Target) +end +return N +end +function TARGET:IsElement(Name) +if Name==nil then +return false +end +for _,name in pairs(self.elements)do +if name==Name then +return true +end +end +return false +end +function TARGET:IsCasualty(Name) +if Name==nil then +return false +end +for _,name in pairs(self.casualties)do +if name==Name then +return true +end +end +return false +end +OPSGROUP={ +ClassName="OPSGROUP", +Debug=false, +verbose=0, +lid=nil, +groupname=nil, +group=nil, +template=nil, +isLateActivated=nil, +waypoints=nil, +waypoints0=nil, +currentwp=1, +elements={}, +taskqueue={}, +taskcounter=nil, +taskcurrent=nil, +taskenroute=nil, +taskpaused={}, +missionqueue={}, +currentmission=nil, +detectedunits={}, +detectedgroups={}, +attribute=nil, +checkzones=nil, +inzones=nil, +groupinitialized=nil, +respawning=nil, +wpcounter=1, +radio={}, +option={}, +optionDefault={}, +tacan={}, +icls={}, +callsign={}, +Ndestroyed=0, +Nkills=0, +weaponData={}, +} +OPSGROUP.ElementStatus={ +INUTERO="inutero", +SPAWNED="spawned", +PARKING="parking", +ENGINEON="engineon", +TAXIING="taxiing", +TAKEOFF="takeoff", +AIRBORNE="airborne", +LANDING="landing", +LANDED="landed", +ARRIVED="arrived", +DEAD="dead", +} +OPSGROUP.TaskStatus={ +SCHEDULED="scheduled", +EXECUTING="executing", +PAUSED="paused", +DONE="done", +} +OPSGROUP.TaskType={ +SCHEDULED="scheduled", +WAYPOINT="waypoint", +} +OPSGROUP.version="0.7.1" +function OPSGROUP:New(Group) +local self=BASE:Inherit(self,FSM:New()) +if type(Group)=="string"then +self.groupname=Group +self.group=GROUP:FindByName(self.groupname) +else +self.group=Group +self.groupname=Group:GetName() +end +self.lid=string.format("OPSGROUP %s | ",tostring(self.groupname)) +if self.group then +if not self:IsExist()then +self:E(self.lid.."ERROR: GROUP does not exist! Returning nil") +return nil +end +end +self.detectedunits=SET_UNIT:New() +self.detectedgroups=SET_GROUP:New() +self.inzones=SET_ZONE:New() +self.spot={} +self.spot.On=false +self.spot.timer=TIMER:New(self._UpdateLaser,self) +self.spot.Coordinate=COORDINATE:New(0,0,0) +self:SetLaser(1688,true,false,0.5) +self.taskcurrent=0 +self.taskcounter=0 +self:SetStartState("InUtero") +self:AddTransition("InUtero","Spawned","Spawned") +self:AddTransition("*","Dead","Dead") +self:AddTransition("*","Stop","Stopped") +self:AddTransition("*","Status","*") +self:AddTransition("*","Destroyed","*") +self:AddTransition("*","Damaged","*") +self:AddTransition("*","UpdateRoute","*") +self:AddTransition("*","Respawn","*") +self:AddTransition("*","PassingWaypoint","*") +self:AddTransition("*","DetectedUnit","*") +self:AddTransition("*","DetectedUnitNew","*") +self:AddTransition("*","DetectedUnitKnown","*") +self:AddTransition("*","DetectedUnitLost","*") +self:AddTransition("*","DetectedGroup","*") +self:AddTransition("*","DetectedGroupNew","*") +self:AddTransition("*","DetectedGroupKnown","*") +self:AddTransition("*","DetectedGroupLost","*") +self:AddTransition("*","PassingWaypoint","*") +self:AddTransition("*","GotoWaypoint","*") +self:AddTransition("*","OutOfAmmo","*") +self:AddTransition("*","OutOfGuns","*") +self:AddTransition("*","OutOfRockets","*") +self:AddTransition("*","OutOfBombs","*") +self:AddTransition("*","OutOfMissiles","*") +self:AddTransition("*","EnterZone","*") +self:AddTransition("*","LeaveZone","*") +self:AddTransition("*","LaserOn","*") +self:AddTransition("*","LaserOff","*") +self:AddTransition("*","LaserCode","*") +self:AddTransition("*","LaserPause","*") +self:AddTransition("*","LaserResume","*") +self:AddTransition("*","LaserLostLOS","*") +self:AddTransition("*","LaserGotLOS","*") +self:AddTransition("*","TaskExecute","*") +self:AddTransition("*","TaskPause","*") +self:AddTransition("*","TaskCancel","*") +self:AddTransition("*","TaskDone","*") +self:AddTransition("*","MissionStart","*") +self:AddTransition("*","MissionExecute","*") +self:AddTransition("*","MissionCancel","*") +self:AddTransition("*","PauseMission","*") +self:AddTransition("*","UnpauseMission","*") +self:AddTransition("*","MissionDone","*") +self:AddTransition("*","ElementSpawned","*") +self:AddTransition("*","ElementDestroyed","*") +self:AddTransition("*","ElementDead","*") +self:AddTransition("*","ElementDamaged","*") +return self +end +function OPSGROUP:GetCoalition() +return self.group:GetCoalition() +end +function OPSGROUP:GetLifePoints() +if self.group then +return self.group:GetLife(),self.group:GetLife0() +end +end +function OPSGROUP:SetVerbosity(VerbosityLevel) +self.verbose=VerbosityLevel or 0 +return self +end +function OPSGROUP:SetDefaultSpeed(Speed) +if Speed then +self.speedCruise=UTILS.KnotsToKmph(Speed) +end +return self +end +function OPSGROUP:GetSpeedCruise() +return UTILS.KmphToKnots(self.speedCruise or self.speedMax*0.7) +end +function OPSGROUP:SetDetection(Switch) +self.detectionOn=Switch +return self +end +function OPSGROUP:SetLaser(Code,CheckLOS,IROff,UpdateTime) +self.spot.Code=Code or 1688 +if CheckLOS~=nil then +self.spot.CheckLOS=CheckLOS +else +self.spot.CheckLOS=true +end +self.spot.IRon=not IROff +self.spot.dt=UpdateTime or 0.5 +return self +end +function OPSGROUP:GetLaserCode() +return self.spot.Code +end +function OPSGROUP:GetLaserCoordinate() +return self.spot.Coordinate +end +function OPSGROUP:GetLaserTarget() +return self.spot.TargetUnit +end +function OPSGROUP:SetCheckZones(CheckZonesSet) +self.checkzones=CheckZonesSet +return self +end +function OPSGROUP:AddCheckZone(CheckZone) +if not self.checkzones then +self.checkzones=SET_ZONE:New() +end +self.checkzones:AddZone(CheckZone) +return self +end +function OPSGROUP:AddWeaponRange(RangeMin,RangeMax,BitType) +RangeMin=UTILS.NMToMeters(RangeMin or 0) +RangeMax=UTILS.NMToMeters(RangeMax or 10) +local weapon={} +weapon.BitType=BitType or ENUMS.WeaponFlag.Auto +weapon.RangeMax=RangeMax +weapon.RangeMin=RangeMin +self.weaponData=self.weaponData or{} +self.weaponData[weapon.BitType]=weapon +return self +end +function OPSGROUP:GetWeaponData(BitType) +BitType=BitType or ENUMS.WeaponFlag.Auto +if self.weaponData[BitType]then +return self.weaponData[BitType] +else +return self.weaponData[ENUMS.WeaponFlag.Auto] +end +end +function OPSGROUP:GetDetectedUnits() +return self.detectedunits or{} +end +function OPSGROUP:GetDetectedGroups() +return self.detectedgroups or{} +end +function OPSGROUP:GetAmmo0() +return self.ammo +end +function OPSGROUP:GetThreat(ThreatLevelMin,ThreatLevelMax) +ThreatLevelMin=ThreatLevelMin or 1 +ThreatLevelMax=ThreatLevelMax or 10 +local threat=nil +local level=0 +for _,_unit in pairs(self.detectedunits:GetSet())do +local unit=_unit +local threatlevel=unit:GetThreatLevel() +if threatlevel>=ThreatLevelMin and threatlevel<=ThreatLevelMax then +if threatlevellevelmax then +threat=unit +levelmax=threatlevel +end +end +return threat,levelmax +end +function OPSGROUP:HasLoS(Coordinate,Element,OffsetElement,OffsetCoordinate) +local Vec3=Coordinate:GetVec3() +if OffsetCoordinate then +Vec3=UTILS.VecAdd(Vec3,OffsetCoordinate) +end +local function checklos(element) +local vec3=element.unit:GetVec3() +if OffsetElement then +vec3=UTILS.VecAdd(vec3,OffsetElement) +end +local _los=land.isVisible(vec3,Vec3) +return _los +end +if Element then +local los=checklos(Element) +return los +else +for _,element in pairs(self.elements)do +local los=checklos(element) +if los then +return true +end +end +return false +end +return nil +end +function OPSGROUP:GetGroup() +return self.group +end +function OPSGROUP:GetName() +return self.groupname +end +function OPSGROUP:GetDCSGroup() +local DCSGroup=Group.getByName(self.groupname) +return DCSGroup +end +function OPSGROUP:GetUnit(UnitNumber) +local DCSUnit=self:GetDCSUnit(UnitNumber) +if DCSUnit then +local unit=UNIT:Find(DCSUnit) +return unit +end +return nil +end +function OPSGROUP:GetDCSUnit(UnitNumber) +local DCSGroup=self:GetDCSGroup() +if DCSGroup then +local unit=DCSGroup:getUnit(UnitNumber or 1) +return unit +end +return nil +end +function OPSGROUP:GetDCSUnits() +local DCSGroup=self:GetDCSGroup() +if DCSGroup then +local units=DCSGroup:getUnits() +return units +end +return nil +end +function OPSGROUP:Despawn(Delay,NoEventRemoveUnit) +if Delay and Delay>0 then +self:ScheduleOnce(Delay,OPSGROUP.Despawn,self,0,NoEventRemoveUnit) +else +local DCSGroup=self:GetDCSGroup() +if DCSGroup then +DCSGroup:destroy() +if not NoEventRemoveUnit then +local units=self:GetDCSUnits() +local EventTime=timer.getTime() +for i=1,#units do +self:CreateEventRemoveUnit(EventTime,units[i]) +end +end +end +end +return self +end +function OPSGROUP:Destroy(Delay) +if Delay and Delay>0 then +self:ScheduleOnce(Delay,OPSGROUP.Destroy,self) +else +local DCSGroup=self:GetDCSGroup() +if DCSGroup then +self:T(self.lid.."Destroying group") +DCSGroup:destroy() +local units=self:GetDCSUnits() +local EventTime=timer.getTime() +for i=1,#units do +if self.isAircraft then +self:CreateEventUnitLost(EventTime,units[i]) +else +self:CreateEventDead(EventTime,units[i]) +end +end +end +end +return self +end +function OPSGROUP:DespawnElement(Element,Delay,NoEventRemoveUnit) +if Delay and Delay>0 then +self:ScheduleOnce(Delay,OPSGROUP.DespawnElement,self,Element,0,NoEventRemoveUnit) +else +if Element then +local DCSunit=Unit.getByName(Element.name) +if DCSunit then +DCSunit:destroy() +if not NoEventRemoveUnit then +self:CreateEventRemoveUnit(timer.getTime(),DCSunit) +end +end +end +end +return self +end +function OPSGROUP:GetVec2() +local vec3=self:GetVec3() +if vec3 then +local vec2={x=vec3.x,y=vec3.z} +return vec2 +end +return nil +end +function OPSGROUP:GetVec3() +if self:IsExist()then +local unit=self:GetDCSUnit() +if unit then +local vec3=unit:getPoint() +return vec3 +end +end +return nil +end +function OPSGROUP:GetCoordinate(NewObject) +local vec3=self:GetVec3() +if vec3 then +self.coordinate=self.coordinate or COORDINATE:New(0,0,0) +self.coordinate.x=vec3.x +self.coordinate.y=vec3.y +self.coordinate.z=vec3.z +if NewObject then +local coord=COORDINATE:NewFromCoordinate(self.coordinate) +return coord +else +return self.coordinate +end +else +self:E(self.lid.."WARNING: Group is not alive. Cannot get coordinate!") +end +return nil +end +function OPSGROUP:GetVelocity() +if self:IsExist()then +local unit=self:GetDCSUnit(1) +if unit then +local velvec3=unit:getVelocity() +local vel=UTILS.VecNorm(velvec3) +return vel +end +else +self:E(self.lid.."WARNING: Group does not exist. Cannot get velocity!") +end +return nil +end +function OPSGROUP:GetHeading() +if self:IsExist()then +local unit=self:GetDCSUnit() +if unit then +local pos=unit:getPosition() +local heading=math.atan2(pos.x.z,pos.x.x) +if heading<0 then +heading=heading+2*math.pi +end +heading=math.deg(heading) +return heading +end +else +self:E(self.lid.."WARNING: Group does not exist. Cannot get heading!") +end +return nil +end +function OPSGROUP:GetOrientation() +if self:IsExist()then +local unit=self:GetDCSUnit() +if unit then +local pos=unit:getPosition() +return pos.x,pos.y,pos.z +end +else +self:E(self.lid.."WARNING: Group does not exist. Cannot get orientation!") +end +return nil +end +function OPSGROUP:GetOrientationX() +local X,Y,Z=self:GetOrientation() +return X +end +function OPSGROUP:CheckTaskDescriptionUnique(description) +for _,_task in pairs(self.taskqueue)do +local task=_task +if task.description==description then +return false +end +end +return true +end +function OPSGROUP:Activate(delay) +if delay and delay>0 then +self:T2(self.lid..string.format("Activating late activated group in %d seconds",delay)) +self:ScheduleOnce(delay,OPSGROUP.Activate,self) +else +if self:IsAlive()==false then +self:T(self.lid.."Activating late activated group") +self.group:Activate() +self.isLateActivated=false +elseif self:IsAlive()==true then +self:E(self.lid.."WARNING: Activating group that is already activated") +else +self:E(self.lid.."ERROR: Activating group that is does not exist!") +end +end +return self +end +function OPSGROUP:SelfDestruction(Delay,ExplosionPower) +if Delay and Delay>0 then +self:ScheduleOnce(Delay,OPSGROUP.SelfDestruction,self,0,ExplosionPower) +else +for i,_element in pairs(self.elements)do +local element=_element +local unit=element.unit +if unit and unit:IsAlive()then +unit:Explode(ExplosionPower) +end +end +end +end +function OPSGROUP:IsExist() +local DCSGroup=self:GetDCSGroup() +if DCSGroup then +local exists=DCSGroup:isExist() +return exists +end +return nil +end +function OPSGROUP:IsActive() +end +function OPSGROUP:IsAlive() +if self.group then +local alive=self.group:IsAlive() +return alive +end +return nil +end +function OPSGROUP:IsLateActivated() +return self.isLateActivated +end +function OPSGROUP:IsInUtero() +return self:Is("InUtero") +end +function OPSGROUP:IsSpawned() +return self:Is("Spawned") +end +function OPSGROUP:IsDead() +return self:Is("Dead") +end +function OPSGROUP:IsStopped() +return self:Is("Stopped") +end +function OPSGROUP:IsUncontrolled() +return self.isUncontrolled +end +function OPSGROUP:HasPassedFinalWaypoint() +return self.passedfinalwp +end +function OPSGROUP:IsRearming() +local rearming=self:Is("Rearming")or self:Is("Rearm") +return rearming +end +function OPSGROUP:IsLasing() +return self.spot.On +end +function OPSGROUP:IsRetreating() +return self:is("Retreating") +end +function OPSGROUP:IsEngaging() +return self:is("Engaging") +end +function OPSGROUP:GetWaypoints() +return self.waypoints +end +function OPSGROUP:MarkWaypoints(Duration) +for i,_waypoint in pairs(self.waypoints or{})do +local waypoint=_waypoint +local text=string.format("Waypoint ID=%d of %s",waypoint.uid,self.groupname) +text=text..string.format("\nSpeed=%.1f kts, Alt=%d ft (%s)",UTILS.MpsToKnots(waypoint.speed),UTILS.MetersToFeet(waypoint.alt),"BARO") +if waypoint.marker then +if waypoint.marker.text~=text then +waypoint.marker.text=text +end +else +waypoint.marker=MARKER:New(waypoint.coordinate,text):ToCoalition(self:GetCoalition()) +end +end +if Duration then +self:RemoveWaypointMarkers(Duration) +end +return self +end +function OPSGROUP:RemoveWaypointMarkers(Delay) +if Delay and Delay>0 then +self:ScheduleOnce(Delay,OPSGROUP.RemoveWaypointMarkers,self) +else +for i,_waypoint in pairs(self.waypoints or{})do +local waypoint=_waypoint +if waypoint.marker then +waypoint.marker:Remove() +end +end +end +return self +end +function OPSGROUP:GetWaypointByID(uid) +for _,_waypoint in pairs(self.waypoints or{})do +local waypoint=_waypoint +if waypoint.uid==uid then +return waypoint +end +end +return nil +end +function OPSGROUP:GetWaypointByIndex(index) +for i,_waypoint in pairs(self.waypoints)do +local waypoint=_waypoint +if i==index then +return waypoint +end +end +return nil +end +function OPSGROUP:GetWaypointUIDFromIndex(index) +for i,_waypoint in pairs(self.waypoints)do +local waypoint=_waypoint +if i==index then +return waypoint.uid +end +end +return nil +end +function OPSGROUP:GetWaypointIndex(uid) +if uid then +for i,_waypoint in pairs(self.waypoints or{})do +local waypoint=_waypoint +if waypoint.uid==uid then +return i +end +end +end +return nil +end +function OPSGROUP:GetWaypointIndexNext(cyclic,i) +if cyclic==nil then +cyclic=self.adinfinitum +end +local N=#self.waypoints +i=i or self.currentwp +local n=math.min(i+1,N) +if cyclic and i==N then +n=1 +end +return n +end +function OPSGROUP:GetWaypointIndexCurrent() +return self.currentwp or 1 +end +function OPSGROUP:GetWaypointIndexAfterID(uid) +local index=self:GetWaypointIndex(uid) +if index then +return index+1 +else +return#self.waypoints+1 +end +end +function OPSGROUP:GetWaypoint(indx) +return self.waypoints[indx] +end +function OPSGROUP:GetWaypointFinal() +return self.waypoints[#self.waypoints] +end +function OPSGROUP:GetWaypointNext(cyclic) +local n=self:GetWaypointIndexNext(cyclic) +return self.waypoints[n] +end +function OPSGROUP:GetWaypointCurrent() +return self.waypoints[self.currentwp] +end +function OPSGROUP:GetNextWaypointCoordinate(cyclic) +local waypoint=self:GetWaypointNext(cyclic) +return waypoint.coordinate +end +function OPSGROUP:GetWaypointCoordinate(index) +local waypoint=self:GetWaypoint(index) +if waypoint then +return waypoint.coordinate +end +return nil +end +function OPSGROUP:GetWaypointSpeed(indx) +local waypoint=self:GetWaypoint(indx) +if waypoint then +return UTILS.MpsToKnots(waypoint.speed) +end +return nil +end +function OPSGROUP:GetWaypointUID(waypoint) +return waypoint.uid +end +function OPSGROUP:GetWaypointID(indx) +local waypoint=self:GetWaypoint(indx) +if waypoint then +return waypoint.uid +end +return nil +end +function OPSGROUP:GetSpeedToWaypoint(indx) +local speed=self:GetWaypointSpeed(indx) +if speed<=0.1 then +speed=self:GetSpeedCruise() +end +return speed +end +function OPSGROUP:GetDistanceToWaypoint(indx) +local dist=0 +if#self.waypoints>0 then +indx=indx or self:GetWaypointIndexNext() +local wp=self:GetWaypoint(indx) +if wp then +local coord=self:GetCoordinate() +dist=coord:Get2DDistance(wp.coordinate) +end +end +return dist +end +function OPSGROUP:GetTimeToWaypoint(indx) +local s=self:GetDistanceToWaypoint(indx) +local v=self:GetVelocity() +local t=s/v +if t==math.inf then +return 365*24*60*60 +elseif t==math.nan then +return 0 +else +return t +end +end +function OPSGROUP:GetExpectedSpeed() +if self:IsHolding()then +return 0 +else +return self.speedWp or 0 +end +end +function OPSGROUP:RemoveWaypointByID(uid) +local index=self:GetWaypointIndex(uid) +if index then +self:RemoveWaypoint(index) +end +return self +end +function OPSGROUP:RemoveWaypoint(wpindex) +if self.waypoints then +local N=#self.waypoints +local wp=self:GetWaypoint(wpindex) +if wp and wp.marker then +wp.marker:Remove() +end +table.remove(self.waypoints,wpindex) +local n=#self.waypoints +self:T(self.lid..string.format("Removing waypoint index %d, current wp index %d. N %d-->%d",wpindex,self.currentwp,N,n)) +if wpindex>self.currentwp then +if self.currentwp>=n then +self.passedfinalwp=true +end +self:_CheckGroupDone(1) +else +if self.currentwp==1 then +if self.adinfinitum then +self.currentwp=#self.waypoints +else +self.currentwp=1 +end +else +self.currentwp=self.currentwp-1 +end +end +end +return self +end +function OPSGROUP:SetTask(DCSTask) +if self:IsAlive()then +if self.taskcurrent>0 then +end +if self.taskenroute and#self.taskenroute>0 then +if tostring(DCSTask.id)=="ComboTask"then +for _,task in pairs(self.taskenroute)do +table.insert(DCSTask.params.tasks,1,task) +end +else +local tasks=UTILS.DeepCopy(self.taskenroute) +table.insert(tasks,DCSTask) +DCSTask=self.group.TaskCombo(self,tasks) +end +end +self.group:SetTask(DCSTask) +local text=string.format("SETTING Task %s",tostring(DCSTask.id)) +if tostring(DCSTask.id)=="ComboTask"then +for i,task in pairs(DCSTask.params.tasks)do +text=text..string.format("\n[%d] %s",i,tostring(task.id)) +end +end +self:T(self.lid..text) +end +return self +end +function OPSGROUP:PushTask(DCSTask) +if self:IsAlive()then +self.group:PushTask(DCSTask) +local text=string.format("PUSHING Task %s",tostring(DCSTask.id)) +if tostring(DCSTask.id)=="ComboTask"then +for i,task in pairs(DCSTask.params.tasks)do +text=text..string.format("\n[%d] %s",i,tostring(task.id)) +end +end +self:T(self.lid..text) +end +return self +end +function OPSGROUP:ClearTasks() +if self:IsAlive()then +self.group:ClearTasks() +self:I(self.lid..string.format("CLEARING Tasks")) +end +return self +end +function OPSGROUP:AddTask(task,clock,description,prio,duration) +local newtask=self:NewTaskScheduled(task,clock,description,prio,duration) +table.insert(self.taskqueue,newtask) +self:T(self.lid..string.format("Adding SCHEDULED task %s starting at %s",newtask.description,UTILS.SecondsToClock(newtask.time,true))) +self:T3({newtask=newtask}) +return newtask +end +function OPSGROUP:NewTaskScheduled(task,clock,description,prio,duration) +self.taskcounter=self.taskcounter+1 +local time=timer.getAbsTime()+5 +if clock then +if type(clock)=="string"then +time=UTILS.ClockToSeconds(clock) +elseif type(clock)=="number"then +time=timer.getAbsTime()+clock +end +end +local newtask={} +newtask.status=OPSGROUP.TaskStatus.SCHEDULED +newtask.dcstask=task +newtask.description=description or task.id +newtask.prio=prio or 50 +newtask.time=time +newtask.id=self.taskcounter +newtask.duration=duration +newtask.waypoint=-1 +newtask.type=OPSGROUP.TaskType.SCHEDULED +newtask.stopflag=USERFLAG:New(string.format("%s StopTaskFlag %d",self.groupname,newtask.id)) +newtask.stopflag:Set(0) +return newtask +end +function OPSGROUP:AddTaskWaypoint(task,Waypoint,description,prio,duration) +Waypoint=Waypoint or self:GetWaypointNext() +if Waypoint then +self.taskcounter=self.taskcounter+1 +local newtask={} +newtask.description=description or string.format("Task #%d",self.taskcounter) +newtask.status=OPSGROUP.TaskStatus.SCHEDULED +newtask.dcstask=task +newtask.prio=prio or 50 +newtask.id=self.taskcounter +newtask.duration=duration +newtask.time=0 +newtask.waypoint=Waypoint.uid +newtask.type=OPSGROUP.TaskType.WAYPOINT +newtask.stopflag=USERFLAG:New(string.format("%s StopTaskFlag %d",self.groupname,newtask.id)) +newtask.stopflag:Set(0) +table.insert(self.taskqueue,newtask) +self:T(self.lid..string.format("Adding WAYPOINT task %s at WP ID=%d",newtask.description,newtask.waypoint)) +self:T3({newtask=newtask}) +self:__UpdateRoute(-1) +return newtask +end +return nil +end +function OPSGROUP:AddTaskEnroute(task) +if not self.taskenroute then +self.taskenroute={} +end +local gotit=false +for _,Task in pairs(self.taskenroute)do +if Task.id==task.id then +gotit=true +break +end +end +if not gotit then +table.insert(self.taskenroute,task) +end +end +function OPSGROUP:GetTasksWaypoint(id) +local tasks={} +self:_SortTaskQueue() +for _,_task in pairs(self.taskqueue)do +local task=_task +if task.type==OPSGROUP.TaskType.WAYPOINT and task.status==OPSGROUP.TaskStatus.SCHEDULED and task.waypoint==id then +table.insert(tasks,task) +end +end +return tasks +end +function OPSGROUP:CountTasksWaypoint(id) +local n=0 +for _,_task in pairs(self.taskqueue)do +local task=_task +if task.type==OPSGROUP.TaskType.WAYPOINT and task.status==OPSGROUP.TaskStatus.SCHEDULED and task.waypoint==id then +n=n+1 +end +end +return n +end +function OPSGROUP:_SortTaskQueue() +local function _sort(a,b) +local taskA=a +local taskB=b +return(taskA.prio=task.time then +return task +end +end +return nil +end +function OPSGROUP:GetTaskCurrent() +local task=self:GetTaskByID(self.taskcurrent,OPSGROUP.TaskStatus.EXECUTING) +return task +end +function OPSGROUP:GetTaskByID(id,status) +for _,_task in pairs(self.taskqueue)do +local task=_task +if task.id==id then +if status==nil or status==task.status then +return task +end +end +end +return nil +end +function OPSGROUP:onafterTaskExecute(From,Event,To,Task) +local text=string.format("Task %s ID=%d execute",tostring(Task.description),Task.id) +self:T(self.lid..text) +if self.taskcurrent>0 then +self:TaskCancel() +end +self.taskcurrent=Task.id +Task.timestamp=timer.getAbsTime() +Task.status=OPSGROUP.TaskStatus.EXECUTING +if Task.dcstask.id=="Formation"then +local followSet=SET_GROUP:New():AddGroup(self.group) +local param=Task.dcstask.params +local followUnit=UNIT:FindByName(param.unitname) +Task.formation=AI_FORMATION:New(followUnit,followSet,"Formation","Follow X at given parameters.") +Task.formation:FormationCenterWing(-param.offsetX,50,math.abs(param.altitude),50,param.offsetZ,50) +Task.formation:SetFollowTimeInterval(param.dtFollow) +Task.formation:SetFlightModeFormation(self.group) +Task.formation:Start() +elseif Task.dcstask.id=="PatrolZone"then +local zone=Task.dcstask.params.zone +local Coordinate=zone:GetRandomCoordinate() +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 +if self.isFlightgroup then +FLIGHTGROUP.AddWaypoint(self,Coordinate,Speed,AfterWaypointWithID,Altitude) +elseif self.isNavygroup then +ARMYGROUP.AddWaypoint(self,Coordinate,Speed,AfterWaypointWithID,Formation) +elseif self.isArmygroup then +NAVYGROUP.AddWaypoint(self,Coordinate,Speed,AfterWaypointWithID,Altitude) +end +else +if Task.type==OPSGROUP.TaskType.SCHEDULED then +local DCStasks={} +if Task.dcstask.id=='ComboTask'then +for TaskID,Task in ipairs(Task.dcstask.params.tasks)do +table.insert(DCStasks,Task) +end +else +table.insert(DCStasks,Task.dcstask) +end +local TaskCombo=self.group:TaskCombo(DCStasks) +local TaskCondition=self.group:TaskCondition(nil,Task.stopflag:GetName(),1,nil,Task.duration) +local TaskControlled=self.group:TaskControlled(TaskCombo,TaskCondition) +local TaskDone=self.group:TaskFunction("OPSGROUP._TaskDone",self,Task) +local TaskFinal=self.group:TaskCombo({TaskControlled,TaskDone}) +self:SetTask(TaskFinal) +end +end +local Mission=self:GetMissionByTaskID(self.taskcurrent) +if Mission then +self:MissionExecute(Mission) +end +end +function OPSGROUP:onafterTaskCancel(From,Event,To,Task) +local currenttask=self:GetTaskCurrent() +Task=Task or currenttask +if Task then +if currenttask and Task.id==currenttask.id then +local stopflag=Task.stopflag:Get() +local text=string.format("Current task %s ID=%d cancelled (flag %s=%d)",Task.description,Task.id,Task.stopflag:GetName(),stopflag) +self:T(self.lid..text) +Task.stopflag:Set(1) +local done=false +if Task.dcstask.id=="Formation"then +Task.formation:Stop() +done=true +elseif Task.dcstask.id=="PatrolZone"then +done=true +elseif stopflag==1 or(not self:IsAlive())or self:IsDead()or self:IsStopped()then +done=true +end +if done then +self:TaskDone(Task) +end +else +self:T(self.lid..string.format("TaskCancel: Setting task %s ID=%d to DONE",Task.description,Task.id)) +self:TaskDone(Task) +end +else +local text=string.format("WARNING: No (current) task to cancel!") +self:E(self.lid..text) +end +end +function OPSGROUP:onbeforeTaskDone(From,Event,To,Task) +local allowed=true +if Task.status==OPSGROUP.TaskStatus.PAUSED then +allowed=false +end +return allowed +end +function OPSGROUP:onafterTaskDone(From,Event,To,Task) +local text=string.format("Task done: %s ID=%d",Task.description,Task.id) +self:T(self.lid..text) +if Task.id==self.taskcurrent then +self.taskcurrent=0 +end +Task.status=OPSGROUP.TaskStatus.DONE +if Task.backupROE then +self:SwitchROE(Task.backupROE) +end +local Mission=self:GetMissionByTaskID(Task.id) +if Mission and Mission:IsNotOver()then +local status=Mission:GetGroupStatus(self) +if status~=AUFTRAG.GroupStatus.PAUSED then +self:T(self.lid.."Task Done ==> Mission Done!") +self:MissionDone(Mission) +else +end +else +if Task.description=="Engage_Target"then +self:Disengage() +end +self:T(self.lid.."Task Done but NO mission found ==> _CheckGroupDone in 1 sec") +self:_CheckGroupDone(1) +end +end +function OPSGROUP:AddMission(Mission) +Mission:AddOpsGroup(self) +Mission:SetGroupStatus(self,AUFTRAG.GroupStatus.SCHEDULED) +Mission:Scheduled() +Mission.Nelements=Mission.Nelements+#self.elements +table.insert(self.missionqueue,Mission) +local text=string.format("Added %s mission %s starting at %s, stopping at %s", +tostring(Mission.type),tostring(Mission.name),UTILS.SecondsToClock(Mission.Tstart,true),Mission.Tstop and UTILS.SecondsToClock(Mission.Tstop,true)or"INF") +self:T(self.lid..text) +return self +end +function OPSGROUP:RemoveMission(Mission) +for i,_mission in pairs(self.missionqueue)do +local mission=_mission +if mission.auftragsnummer==Mission.auftragsnummer then +local Task=Mission:GetGroupWaypointTask(self) +if Task then +self:RemoveTask(Task) +end +table.remove(self.missionqueue,i) +return self +end +end +return self +end +function OPSGROUP:CountRemainingMissison() +local N=0 +for _,_mission in pairs(self.missionqueue)do +local mission=_mission +if mission and mission:IsNotOver()then +local status=mission:GetGroupStatus(self) +if status~=AUFTRAG.GroupStatus.DONE and status~=AUFTRAG.GroupStatus.CANCELLED then +N=N+1 +end +end +end +return N +end +function OPSGROUP:_GetNextMission() +local Nmissions=#self.missionqueue +if Nmissions==0 then +return nil +end +local function _sort(a,b) +local taskA=a +local taskB=b +return(taskA.prio0 then +self:ScheduleOnce(delay,OPSGROUP.RouteToMission,self,mission) +else +if self:IsDead()then +return +end +local uid=self:GetWaypointCurrent().uid +local waypointcoord=mission:GetMissionWaypointCoord(self.group) +for _,task in pairs(mission.enrouteTasks)do +self:AddTaskEnroute(task) +end +local SpeedToMission=UTILS.KmphToKnots(self.speedCruise) +if mission.type==AUFTRAG.Type.TROOPTRANSPORT then +mission.DCStask=mission:GetDCSMissionTask(self.group) +for _,_group in pairs(mission.transportGroupSet.Set)do +local group=_group +if group and group:IsAlive()then +local DCSTask=group:TaskEmbarkToTransport(mission.transportPickup,500) +group:SetTask(DCSTask,5) +end +end +elseif mission.type==AUFTRAG.Type.ARTY then +local weapondata=self:GetWeaponData(mission.engageWeaponType) +if weapondata then +local targetcoord=mission:GetTargetCoordinate() +local heading=self:GetCoordinate():HeadingTo(targetcoord) +local dist=self:GetCoordinate():Get2DDistance(targetcoord) +if dist>weapondata.RangeMax then +local d=(dist-weapondata.RangeMax)*1.1 +waypointcoord=self:GetCoordinate():Translate(d,heading) +self:T(self.lid..string.format("Out of max range = %.1f km for weapon %d",weapondata.RangeMax/1000,mission.engageWeaponType)) +elseif dist0 then +for i,_task in pairs(tasks)do +local task=_task +text=text..string.format("\n[%d] %s",i,task.description) +end +else +text=text.." None" +end +self:T(self.lid..text) +local taskswp={} +for _,task in pairs(tasks)do +local Task=task +table.insert(taskswp,self.group:TaskFunction("OPSGROUP._TaskExecute",self,Task)) +local TaskCondition=self.group:TaskCondition(nil,Task.stopflag:GetName(),1,nil,Task.duration) +table.insert(taskswp,self.group:TaskControlled(Task.dcstask,TaskCondition)) +table.insert(taskswp,self.group:TaskFunction("OPSGROUP._TaskDone",self,Task)) +end +if#taskswp>0 then +self:SetTask(self.group:TaskCombo(taskswp)) +end +return#taskswp +end +function OPSGROUP:onafterGotoWaypoint(From,Event,To,UID) +local n=self:GetWaypointIndex(UID) +if n then +if false then +local tasks=self:GetTasksWaypoint(n) +for _,_task in pairs(tasks)do +local task=_task +task.status=OPSGROUP.TaskStatus.SCHEDULED +end +end +local Speed=self:GetSpeedToWaypoint(n) +self:__UpdateRoute(-1,n,Speed) +end +end +function OPSGROUP:onafterDetectedUnit(From,Event,To,Unit) +local unitname=Unit and Unit:GetName()or"unknown" +self:T2(self.lid..string.format("Detected unit %s",unitname)) +if self.detectedunits:FindUnit(unitname)then +self:DetectedUnitKnown(Unit) +else +self:DetectedUnitNew(Unit) +end +end +function OPSGROUP:onafterDetectedUnitNew(From,Event,To,Unit) +self:T(self.lid..string.format("Detected New unit %s",Unit:GetName())) +self.detectedunits:AddUnit(Unit) +end +function OPSGROUP:onafterDetectedGroup(From,Event,To,Group) +local groupname=Group and Group:GetName()or"unknown" +self:T(self.lid..string.format("Detected group %s",groupname)) +if self.detectedgroups:FindGroup(groupname)then +self:DetectedGroupKnown(Group) +else +self:DetectedGroupNew(Group) +end +end +function OPSGROUP:onafterDetectedGroupNew(From,Event,To,Group) +self:T(self.lid..string.format("Detected New group %s",Group:GetName())) +self.detectedgroups:AddGroup(Group) +end +function OPSGROUP:onafterEnterZone(From,Event,To,Zone) +local zonename=Zone and Zone:GetName()or"unknown" +self:T2(self.lid..string.format("Entered Zone %s",zonename)) +self.inzones:Add(Zone:GetName(),Zone) +end +function OPSGROUP:onafterLeaveZone(From,Event,To,Zone) +local zonename=Zone and Zone:GetName()or"unknown" +self:T2(self.lid..string.format("Left Zone %s",zonename)) +self.inzones:Remove(zonename,true) +end +function OPSGROUP:onbeforeLaserOn(From,Event,To,Target) +if self.spot.On then +return false +end +if Target then +self:SetLaserTarget(Target) +else +self:E(self.lid.."ERROR: No target provided for LASER!") +return false +end +local element=self:GetElementAlive() +if element then +self.spot.element=element +local offsetY=0 +if self.isGround or self.isNaval then +offsetY=element.height +end +self.spot.offset={x=0,y=offsetY,z=0} +if self.spot.CheckLOS then +local los=self:HasLoS(self.spot.Coordinate,self.spot.element,self.spot.offset) +if los then +self:LaserGotLOS() +else +self:I(self.lid.."LASER got no LOS currently. Trying to switch the laser on again in 10 sec") +self:__LaserOn(-10,Target) +return false +end +end +else +self:E(self.lid.."ERROR: No element alive for lasing") +return false +end +return true +end +function OPSGROUP:onafterLaserOn(From,Event,To,Target) +if not self.spot.timer:IsRunning()then +self.spot.timer:Start(nil,self.spot.dt) +end +local DCSunit=self.spot.element.unit:GetDCSObject() +self.spot.Laser=Spot.createLaser(DCSunit,self.spot.offset,self.spot.vec3,self.spot.Code or 1688) +if self.spot.IRon then +self.spot.IR=Spot.createInfraRed(DCSunit,self.spot.offset,self.spot.vec3) +end +self.spot.On=true +self.spot.Paused=false +self:T(self.lid.."Switching LASER on") +end +function OPSGROUP:onbeforeLaserOff(From,Event,To) +return self.spot.On or self.spot.Paused +end +function OPSGROUP:onafterLaserOff(From,Event,To) +self:T(self.lid.."Switching LASER off") +if self.spot.On then +self.spot.Laser:destroy() +self.spot.IR:destroy() +self.spot.Laser=nil +self.spot.IR=nil +end +self.spot.timer:Stop() +self.spot.TargetUnit=nil +self.spot.On=false +self.spot.Paused=false +end +function OPSGROUP:onafterLaserPause(From,Event,To) +self:T(self.lid.."Switching LASER off temporarily") +self.spot.Laser:destroy() +self.spot.IR:destroy() +self.spot.Laser=nil +self.spot.IR=nil +self.spot.On=false +self.spot.Paused=true +end +function OPSGROUP:onbeforeLaserResume(From,Event,To) +return self.spot.Paused +end +function OPSGROUP:onafterLaserResume(From,Event,To) +self:T(self.lid.."Resuming LASER") +self.spot.Paused=false +local target=nil +if self.spot.TargetType==0 then +target=self.spot.Coordinate +elseif self.spot.TargetType==1 or self.spot.TargetType==2 then +target=self.spot.TargetUnit +elseif self.spot.TargetType==3 then +target=self.spot.TargetGroup +end +if target then +self:T(self.lid.."Switching LASER on again") +self:LaserOn(target) +end +end +function OPSGROUP:onafterLaserCode(From,Event,To,Code) +self.spot.Code=Code or 1688 +self:T2(self.lid..string.format("Setting LASER Code to %d",self.spot.Code)) +if self.spot.On then +self:T(self.lid..string.format("New LASER Code is %d",self.spot.Code)) +self.spot.Laser:setCode(self.spot.Code) +end +end +function OPSGROUP:onafterLaserLostLOS(From,Event,To) +self.spot.LOS=false +self.spot.lostLOS=true +if self.spot.On then +self:LaserPause() +end +end +function OPSGROUP:onafterLaserGotLOS(From,Event,To) +self.spot.LOS=true +if self.spot.lostLOS then +self.spot.lostLOS=false +if self.spot.Paused then +self:LaserResume() +end +end +end +function OPSGROUP:SetLaserTarget(Target) +if Target then +if Target:IsInstanceOf("SCENERY")then +self.spot.TargetType=0 +self.spot.offsetTarget={x=0,y=1,z=0} +elseif Target:IsInstanceOf("POSITIONABLE")then +local target=Target +if target:IsAlive()then +if target:IsInstanceOf("GROUP")then +self.spot.TargetGroup=target +self.spot.TargetUnit=target:GetHighestThreat() +self.spot.TargetType=3 +else +self.spot.TargetUnit=target +if target:IsInstanceOf("STATIC")then +self.spot.TargetType=1 +elseif target:IsInstanceOf("UNIT")then +self.spot.TargetType=2 +end +end +local size,x,y,z=self.spot.TargetUnit:GetObjectSize() +if y then +self.spot.offsetTarget={x=0,y=y*0.75,z=0} +else +self.spot.offsetTarget={x=0,2,z=0} +end +else +self:E("WARNING: LASER target is not alive!") +return +end +elseif Target:IsInstanceOf("COORDINATE")then +self.spot.TargetType=0 +self.spot.offsetTarget={x=0,y=0,z=0} +else +self:E(self.lid.."ERROR: LASER target should be a POSITIONABLE (GROUP, UNIT or STATIC) or a COORDINATE object!") +return +end +self.spot.vec3=UTILS.VecAdd(Target:GetVec3(),self.spot.offsetTarget) +self.spot.Coordinate:UpdateFromVec3(self.spot.vec3) +end +end +function OPSGROUP:_UpdateLaser() +if self.spot.TargetUnit then +if self.spot.TargetUnit:IsAlive()then +local vec3=self.spot.TargetUnit:GetVec3() +vec3=UTILS.VecAdd(vec3,self.spot.offsetTarget) +local dist=UTILS.VecDist3D(vec3,self.spot.vec3) +self.spot.vec3=vec3 +self.spot.Coordinate:UpdateFromVec3(vec3) +if dist>1 then +if self.spot.On then +self.spot.Laser:setPoint(vec3) +if self.spot.IRon then +self.spot.IR:setPoint(vec3) +end +end +end +else +if self.spot.TargetGroup and self.spot.TargetGroup:IsAlive()then +local unit=self.spot.TargetGroup:GetHighestThreat() +if unit then +self:T(self.lid..string.format("Switching to target unit %s in the group",unit:GetName())) +self.spot.TargetUnit=unit +return +else +self:T(self.lid.."Target is not alive any more ==> switching LASER off") +self:LaserOff() +return +end +else +self:T(self.lid.."Target is not alive any more ==> switching LASER off") +self:LaserOff() +return +end +end +end +if self.spot.CheckLOS then +local los=self:HasLoS(self.spot.Coordinate,self.spot.element,self.spot.offset) +if los then +if self.spot.lostLOS then +self:LaserGotLOS() +end +else +if not self.spot.lostLOS then +self:LaserLostLOS() +end +end +end +end +function OPSGROUP:onafterElementDestroyed(From,Event,To,Element) +self:T(self.lid..string.format("Element destroyed %s",Element.name)) +for _,_mission in pairs(self.missionqueue)do +local mission=_mission +mission:ElementDestroyed(self,Element) +end +self.Ndestroyed=self.Ndestroyed+1 +self:_UpdateStatus(Element,OPSGROUP.ElementStatus.DEAD) +end +function OPSGROUP:onafterElementDead(From,Event,To,Element) +self:T(self.lid..string.format("Element dead %s at t=%.3f",Element.name,timer.getTime())) +self:_UpdateStatus(Element,OPSGROUP.ElementStatus.DEAD) +if self.spot.On and self.spot.element.name==Element.name then +self:LaserOff() +if self:GetNelements()>0 then +local target=nil +if self.spot.TargetType==0 then +target=self.spot.Coordinate +elseif self.spot.TargetType==1 or self.spot.TargetType==2 then +if self.spot.TargetUnit and self.spot.TargetUnit:IsAlive()then +target=self.spot.TargetUnit +end +elseif self.spot.TargetType==3 then +if self.spot.TargetGroup and self.spot.TargetGroup:IsAlive()then +target=self.spot.TargetGroup +end +end +if target then +self:__LaserOn(-1,target) +end +end +end +end +function OPSGROUP:onbeforeDead(From,Event,To) +if self.Ndestroyed==#self.elements then +self:Destroyed() +end +end +function OPSGROUP:onafterDead(From,Event,To) +self:T(self.lid..string.format("Group dead at t=%.3f",timer.getTime())) +self.waypoints=nil +self.groupinitialized=false +for _,_mission in pairs(self.missionqueue)do +local mission=_mission +self:T(self.lid.."Cancelling mission because group is dead! Mission name "..tostring(mission:GetName())) +self:MissionCancel(mission) +mission:GroupDead(self) +end +self:__Stop(-5) +end +function OPSGROUP:onafterStop(From,Event,To) +self.timerCheckZone:Stop() +self.timerQueueUpdate:Stop() +self.CallScheduler:Clear() +if self:IsAlive()and not(self:IsDead()or self:IsStopped())then +local life,life0=self:GetLifePoints() +local state=self:GetState() +local text=string.format("WARNING: Group is still alive! Current state=%s. Life points=%d/%d. Use OPSGROUP:Destroy() or OPSGROUP:Despawn() for a clean stop",state,life,life0) +self:E(self.lid..text) +end +self:I(self.lid.."STOPPED! Unhandled events, cleared scheduler and removed from database.") +end +function OPSGROUP:_CheckInZones() +if self.checkzones and self:IsAlive()then +local Ncheck=self.checkzones:Count() +local Ninside=self.inzones:Count() +self:T(self.lid..string.format("Check if group is in %d zones. Currently it is in %d zones.",self.checkzones:Count(),self.inzones:Count())) +local leftzones={} +for inzonename,inzone in pairs(self.inzones:GetSet())do +local isstillinzone=self.group:IsInZone(inzone) +if not isstillinzone then +table.insert(leftzones,inzone) +end +end +for _,leftzone in pairs(leftzones)do +self:LeaveZone(leftzone) +end +local enterzones={} +for checkzonename,_checkzone in pairs(self.checkzones:GetSet())do +local checkzone=_checkzone +local isincheckzone=self.group:IsInZone(checkzone) +if isincheckzone and not self.inzones:_Find(checkzonename)then +table.insert(enterzones,checkzone) +end +end +for _,enterzone in pairs(enterzones)do +self:EnterZone(enterzone) +end +end +end +function OPSGROUP:_CheckDetectedUnits() +if self.group and not self:IsDead()then +local detectedtargets=self.group:GetDetectedTargets() +local detected={} +local groups={} +for DetectionObjectID,Detection in pairs(detectedtargets or{})do +local DetectedObject=Detection.object +if DetectedObject and DetectedObject:isExist()and DetectedObject.id_<50000000 then +local unit=UNIT:Find(DetectedObject) +if unit and unit:IsAlive()then +local unitname=unit:GetName() +table.insert(detected,unit) +self:DetectedUnit(unit) +local group=unit:GetGroup() +if group then +groups[group:GetName()]=group +end +end +end +end +for groupname,group in pairs(groups)do +self:DetectedGroup(group) +end +local lost={} +for _,_unit in pairs(self.detectedunits:GetSet())do +local unit=_unit +local gotit=false +for _,_du in pairs(detected)do +local du=_du +if unit:GetName()==du:GetName()then +gotit=true +end +end +if not gotit then +table.insert(lost,unit:GetName()) +self:DetectedUnitLost(unit) +end +end +self.detectedunits:RemoveUnitsByName(lost) +local lost={} +for _,_group in pairs(self.detectedgroups:GetSet())do +local group=_group +local gotit=false +for _,_du in pairs(groups)do +local du=_du +if group:GetName()==du:GetName()then +gotit=true +end +end +if not gotit then +table.insert(lost,group:GetName()) +self:DetectedGroupLost(group) +end +end +self.detectedgroups:RemoveGroupsByName(lost) +end +end +function OPSGROUP:_CheckGroupDone(delay) +if self:IsAlive()and self.isAI then +if delay and delay>0 then +self:ScheduleOnce(delay,self._CheckGroupDone,self) +else +if self:IsEngaging()then +self:UpdateRoute() +return +end +local waypoint=self:GetWaypoint(self.currentwp) +if waypoint then +local ntasks=self:CountTasksWaypoint(waypoint.uid) +if ntasks>0 then +self:T(self.lid..string.format("Still got %d tasks for the current waypoint UID=%d ==> RETURN (no action)",ntasks,waypoint.uid)) +return +end +end +if self.adinfinitum then +if#self.waypoints>0 then +local i=self:GetWaypointIndexNext(true) +local speed=self:GetSpeedToWaypoint(i) +self:UpdateRoute(i,speed) +self:T(self.lid..string.format("Adinfinitum=TRUE ==> Goto WP index=%d at speed=%d knots",i,speed)) +else +self:E(self.lid..string.format("WARNING: No waypoints left! Commanding a Full Stop")) +self:__FullStop(-1) +end +else +if self.passedfinalwp then +self:__FullStop(-1) +self:T(self.lid..string.format("Passed final WP, adinfinitum=FALSE ==> Full Stop")) +else +if#self.waypoints>0 then +self:T(self.lid..string.format("NOT Passed final WP, #WP>0 ==> Update Route")) +self:UpdateRoute() +else +self:E(self.lid..string.format("WARNING: No waypoints left! Commanding a Full Stop")) +self:__FullStop(-1) +end +end +end +end +end +end +function OPSGROUP:_CheckStuck() +if self:IsHolding()or self:Is("Rearming")then +return +end +local Tnow=timer.getTime() +local ExpectedSpeed=self:GetExpectedSpeed() +local speed=self:GetVelocity() +if speed<0.5 then +if ExpectedSpeed>0 and not self.stuckTimestamp then +self:T2(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected",speed,ExpectedSpeed)) +self.stuckTimestamp=Tnow +self.stuckVec3=self:GetVec3() +end +else +self.stuckTimestamp=nil +end +if self.stuckTimestamp then +local holdtime=Tnow-self.stuckTimestamp +if holdtime>=10*60 then +self:E(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected for %d sec",speed,ExpectedSpeed,holdtime)) +end +end +end +function OPSGROUP:_CheckDamage() +self.life=0 +local damaged=false +for _,_element in pairs(self.elements)do +local element=_element +local life=element.unit:GetLife() +self.life=self.life+life +if life0 then +local ammo=self:GetAmmoTot() +if self:IsRearming()then +if ammo.Total==self.ammo.Total then +self:Rearmed() +end +end +if self.outofAmmo and ammo.Total>0 then +self.outofAmmo=false +end +if ammo.Total==0 and not self.outofAmmo then +self.outofAmmo=true +self:OutOfAmmo() +end +if self.outofGuns and ammo.Guns>0 then +self.outoffGuns=false +end +if ammo.Guns==0 and self.ammo.Guns>0 and not self.outofGuns then +self.outofGuns=true +self:OutOfGuns() +end +if self.outofRockets and ammo.Rockets>0 then +self.outoffRockets=false +end +if ammo.Rockets==0 and self.ammo.Rockets>0 and not self.outofRockets then +self.outofRockets=true +self:OutOfRockets() +end +if self.outofBombs and ammo.Bombs>0 then +self.outoffBombs=false +end +if ammo.Bombs==0 and self.ammo.Bombs>0 and not self.outofBombs then +self.outofBombs=true +self:OutOfBombs() +end +if self.outofMissiles and ammo.Missiles>0 then +self.outoffMissiles=false +end +if ammo.Missiles==0 and self.ammo.Missiles>0 and not self.outofMissiles then +self.outofMissiles=true +self:OutOfMissiles() +end +if self:IsEngaging()and ammo.Total==0 then +self:Disengage() +end +end +end +function OPSGROUP:_PrintTaskAndMissionStatus() +if self.verbose>=3 and#self.taskqueue>0 then +local text=string.format("Tasks #%d",#self.taskqueue) +for i,_task in pairs(self.taskqueue)do +local task=_task +local name=task.description +local taskid=task.dcstask.id or"unknown" +local status=task.status +local clock=UTILS.SecondsToClock(task.time,true) +local eta=task.time-timer.getAbsTime() +local started=task.timestamp and UTILS.SecondsToClock(task.timestamp,true)or"N/A" +local duration=-1 +if task.duration then +duration=task.duration +if task.timestamp then +duration=task.duration-(timer.getAbsTime()-task.timestamp) +else +duration=task.duration +end +end +if task.type==OPSGROUP.TaskType.SCHEDULED then +text=text..string.format("\n[%d] %s (%s): status=%s, scheduled=%s (%d sec), started=%s, duration=%d",i,taskid,name,status,clock,eta,started,duration) +elseif task.type==OPSGROUP.TaskType.WAYPOINT then +text=text..string.format("\n[%d] %s (%s): status=%s, waypoint=%d, started=%s, duration=%d, stopflag=%d",i,taskid,name,status,task.waypoint,started,duration,task.stopflag:Get()) +end +end +self:I(self.lid..text) +end +if self.verbose>=2 then +local Mission=self:GetMissionByID(self.currentmission) +local text=string.format("Missions %d, Current: %s",self:CountRemainingMissison(),Mission and Mission.name or"none") +for i,_mission in pairs(self.missionqueue)do +local mission=_mission +local Cstart=UTILS.SecondsToClock(mission.Tstart,true) +local Cstop=mission.Tstop and UTILS.SecondsToClock(mission.Tstop,true)or"INF" +text=text..string.format("\n[%d] %s (%s) status=%s (%s), Time=%s-%s, prio=%d wp=%s targets=%d", +i,tostring(mission.name),mission.type,mission:GetGroupStatus(self),tostring(mission.status),Cstart,Cstop,mission.prio,tostring(mission:GetGroupWaypointIndex(self)),mission:CountMissionTargets()) +end +self:I(self.lid..text) +end +end +function OPSGROUP:_CreateWaypoint(waypoint) +waypoint.uid=self.wpcounter +waypoint.npassed=0 +waypoint.coordinate=COORDINATE:New(waypoint.x,waypoint.alt,waypoint.y) +waypoint.name=string.format("Waypoint UID=%d",waypoint.uid) +waypoint.patrol=false +waypoint.detour=false +waypoint.astar=false +self.wpcounter=self.wpcounter+1 +return waypoint +end +function OPSGROUP:_AddWaypoint(waypoint,wpnumber) +wpnumber=wpnumber or#self.waypoints+1 +table.insert(self.waypoints,wpnumber,waypoint) +self:T(self.lid..string.format("Adding waypoint at index=%d id=%d",wpnumber,waypoint.uid)) +self.passedfinalwp=false +if self:IsHolding()then +self:Cruise() +end +end +function OPSGROUP:InitWaypoints() +self.waypoints0=self.group:GetTemplateRoutePoints() +self.waypoints={} +for index,wp in pairs(self.waypoints0)do +local coordinate=COORDINATE:New(wp.x,wp.alt,wp.y) +wp.speed=wp.speed or 0 +local speedknots=UTILS.MpsToKnots(wp.speed) +if index==1 then +self.speedWp=wp.speed +end +self:AddWaypoint(coordinate,speedknots,index-1,nil,false) +end +self:T(self.lid..string.format("Initializing %d waypoints",#self.waypoints)) +if#self.waypoints>0 then +if#self.waypoints==1 then +self.passedfinalwp=true +end +end +return self +end +function OPSGROUP:Route(waypoints,delay) +if delay and delay>0 then +self:ScheduleOnce(delay,OPSGROUP.Route,self,waypoints) +else +if self:IsAlive()then +local Tasks={} +local TaskRoute=self.group:TaskRoute(waypoints) +table.insert(Tasks,TaskRoute) +local TaskCombo=self.group:TaskCombo(Tasks) +if#Tasks>1 then +self:SetTask(TaskCombo) +else +self:SetTask(TaskRoute) +end +else +self:E(self.lid.."ERROR: Group is not alive! Cannot route group.") +end +end +return self +end +function OPSGROUP:_UpdateWaypointTasks(n) +local waypoints=self.waypoints or{} +local nwaypoints=#waypoints +for i,_wp in pairs(waypoints)do +local wp=_wp +if i>=n or nwaypoints==1 then +self:T2(self.lid..string.format("Updating waypoint task for waypoint %d/%d ID=%d. Last waypoint passed %d",i,nwaypoints,wp.uid,self.currentwp)) +local taskswp={} +local TaskPassingWaypoint=self.group:TaskFunction("OPSGROUP._PassingWaypoint",self,wp.uid) +table.insert(taskswp,TaskPassingWaypoint) +wp.task=self.group:TaskCombo(taskswp) +end +end +end +function OPSGROUP._PassingWaypoint(group,opsgroup,uid) +local waypoint=opsgroup:GetWaypointByID(uid) +if waypoint then +local currentwp=opsgroup.currentwp +opsgroup.currentwp=opsgroup:GetWaypointIndex(uid) +local wpnext=opsgroup:GetWaypointNext() +if wpnext then +if opsgroup.isGround then +opsgroup.formation=wpnext.action +end +opsgroup.speed=wpnext.speed +end +local text=string.format("Group passing waypoint uid=%d",uid) +opsgroup:T(opsgroup.lid..text) +if waypoint.astar then +opsgroup:RemoveWaypointByID(uid) +opsgroup:Cruise() +elseif waypoint.detour then +opsgroup:RemoveWaypointByID(uid) +if opsgroup:IsRearming()then +opsgroup:Rearming() +elseif opsgroup:IsRetreating()then +opsgroup:Retreated() +elseif opsgroup:IsEngaging()then +else +opsgroup:DetourReached() +if waypoint.detour==0 then +opsgroup:FullStop() +elseif waypoint.detour==1 then +opsgroup:Cruise() +else +opsgroup:E("ERROR: waypoint.detour should be 0 or 1") +end +end +else +if opsgroup.ispathfinding then +opsgroup.ispathfinding=false +end +waypoint.npassed=waypoint.npassed+1 +opsgroup:PassingWaypoint(waypoint) +end +end +end +function OPSGROUP._TaskExecute(group,opsgroup,task) +local text=string.format("_TaskExecute %s",task.description) +opsgroup:T3(opsgroup.lid..text) +if opsgroup then +opsgroup:TaskExecute(task) +end +end +function OPSGROUP._TaskDone(group,opsgroup,task) +local text=string.format("_TaskDone %s",task.description) +opsgroup:T3(opsgroup.lid..text) +if opsgroup then +opsgroup:TaskDone(task) +end +end +function OPSGROUP:SetDefaultROE(roe) +self.optionDefault.ROE=roe or ENUMS.ROE.ReturnFire +return self +end +function OPSGROUP:SwitchROE(roe) +if self:IsAlive()or self:IsInUtero()then +self.option.ROE=roe or self.optionDefault.ROE +if self:IsInUtero()then +self:T2(self.lid..string.format("Setting current ROE=%d when GROUP is SPAWNED",self.option.ROE)) +else +self.group:OptionROE(self.option.ROE) +self:T(self.lid..string.format("Setting current ROE=%d (%s)",self.option.ROE,self:_GetROEName(self.option.ROE))) +end +else +self:E(self.lid.."WARNING: Cannot switch ROE! Group is not alive") +end +return self +end +function OPSGROUP:_GetROEName(roe) +local name="unknown" +if roe==0 then +name="Weapon Free" +elseif roe==1 then +name="Open Fire/Weapon Free" +elseif roe==2 then +name="Open Fire" +elseif roe==3 then +name="Return Fire" +elseif roe==4 then +name="Weapon Hold" +end +return name +end +function OPSGROUP:GetROE() +return self.option.ROE or self.optionDefault.ROE +end +function OPSGROUP:SetDefaultROT(rot) +self.optionDefault.ROT=rot or ENUMS.ROT.PassiveDefense +return self +end +function OPSGROUP:SwitchROT(rot) +if self:IsAlive()or self:IsInUtero()then +self.option.ROT=rot or self.optionDefault.ROT +if self:IsInUtero()then +self:T2(self.lid..string.format("Setting current ROT=%d when GROUP is SPAWNED",self.option.ROT)) +else +self.group:OptionROT(self.option.ROT) +self:T(self.lid..string.format("Setting current ROT=%d (0=NoReaction, 1=Passive, 2=Evade, 3=ByPass, 4=AllowAbort)",self.option.ROT)) +end +else +self:E(self.lid.."WARNING: Cannot switch ROT! Group is not alive") +end +return self +end +function OPSGROUP:GetROT() +return self.option.ROT or self.optionDefault.ROT +end +function OPSGROUP:SetDefaultAlarmstate(alarmstate) +self.optionDefault.Alarm=alarmstate or 0 +return self +end +function OPSGROUP:SwitchAlarmstate(alarmstate) +if self:IsAlive()or self:IsInUtero()then +if self.isArmygroup or self.isNavygroup then +self.option.Alarm=alarmstate or self.optionDefault.Alarm +if self:IsInUtero()then +self:T2(self.lid..string.format("Setting current Alarm State=%d when GROUP is SPAWNED",self.option.Alarm)) +else +if self.option.Alarm==0 then +self.group:OptionAlarmStateAuto() +elseif self.option.Alarm==1 then +self.group:OptionAlarmStateGreen() +elseif self.option.Alarm==2 then +self.group:OptionAlarmStateRed() +else +self:E("ERROR: Unknown Alarm State! Setting to AUTO") +self.group:OptionAlarmStateAuto() +self.option.Alarm=0 +end +self:T(self.lid..string.format("Setting current Alarm State=%d (0=Auto, 1=Green, 2=Red)",self.option.Alarm)) +end +end +else +self:E(self.lid.."WARNING: Cannot switch Alarm State! Group is not alive.") +end +return self +end +function OPSGROUP:GetAlarmstate() +return self.option.Alarm or self.optionDefault.Alarm +end +function OPSGROUP:SetDefaultTACAN(Channel,Morse,UnitName,Band,OffSwitch) +self.tacanDefault={} +self.tacanDefault.Channel=Channel or 74 +self.tacanDefault.Morse=Morse or"XXX" +self.tacanDefault.BeaconName=UnitName +if self.isAircraft then +Band=Band or"Y" +else +Band=Band or"X" +end +self.tacanDefault.Band=Band +if OffSwitch then +self.tacanDefault.On=false +else +self.tacanDefault.On=true +end +return self +end +function OPSGROUP:_SwitchTACAN(Tacan) +if Tacan then +self:SwitchTACAN(Tacan.Channel,Tacan.Morse,Tacan.BeaconName,Tacan.Band) +else +if self.tacanDefault.On then +self:SwitchTACAN() +else +self:TurnOffTACAN() +end +end +end +function OPSGROUP:SwitchTACAN(Channel,Morse,UnitName,Band) +if self:IsInUtero()then +self:T(self.lid..string.format("Switching TACAN to DEFAULT when group is spawned")) +self:SetDefaultTACAN(Channel,Morse,UnitName,Band) +elseif self:IsAlive()then +Channel=Channel or self.tacanDefault.Channel +Morse=Morse or self.tacanDefault.Morse +Band=Band or self.tacanDefault.Band +UnitName=UnitName or self.tacanDefault.BeaconName +local unit=self:GetUnit(1) +if UnitName then +if type(UnitName)=="number"then +unit=self.group:GetUnit(UnitName) +else +unit=UNIT:FindByName(UnitName) +end +end +if not unit then +self:T(self.lid.."WARNING: Could not get TACAN unit. Trying first unit in the group") +unit=self:GetUnit(1) +end +if unit and unit:IsAlive()then +local UnitID=unit:GetID() +local Type=BEACON.Type.TACAN +local System=BEACON.System.TACAN +if self.isAircraft then +System=BEACON.System.TACAN_TANKER_Y +end +local Frequency=UTILS.TACANToFrequency(Channel,Band) +unit:CommandActivateBeacon(Type,System,Frequency,UnitID,Channel,Band,true,Morse,true) +self.tacan.Channel=Channel +self.tacan.Morse=Morse +self.tacan.Band=Band +self.tacan.BeaconName=unit:GetName() +self.tacan.BeaconUnit=unit +self.tacan.On=true +self:T(self.lid..string.format("Switching TACAN to Channel %d%s Morse %s on unit %s",self.tacan.Channel,self.tacan.Band,tostring(self.tacan.Morse),self.tacan.BeaconName)) +else +self:E(self.lid.."ERROR: Cound not set TACAN! Unit is not alive") +end +else +self:E(self.lid.."ERROR: Cound not set TACAN! Group is not alive and not in utero any more") +end +return self +end +function OPSGROUP:TurnOffTACAN() +if self.tacan.BeaconUnit and self.tacan.BeaconUnit:IsAlive()then +self.tacan.BeaconUnit:CommandDeactivateBeacon() +end +self:T(self.lid..string.format("Switching TACAN OFF")) +self.tacan.On=false +end +function OPSGROUP:GetTACAN() +return self.tacan.Channel,self.tacan.Morse,self.tacan.Band,self.tacan.On,self.tacan.BeaconName +end +function OPSGROUP:SetDefaultICLS(Channel,Morse,UnitName,OffSwitch) +self.iclsDefault={} +self.iclsDefault.Channel=Channel or 1 +self.iclsDefault.Morse=Morse or"XXX" +self.iclsDefault.BeaconName=UnitName +if OffSwitch then +self.iclsDefault.On=false +else +self.iclsDefault.On=true +end +return self +end +function OPSGROUP:_SwitchICLS(Icls) +if Icls then +self:SwitchICLS(Icls.Channel,Icls.Morse,Icls.BeaconName) +else +if self.iclsDefault.On then +self:SwitchICLS() +else +self:TurnOffICLS() +end +end +end +function OPSGROUP:SwitchICLS(Channel,Morse,UnitName) +if self:IsInUtero()then +self:SetDefaultICLS(Channel,Morse,UnitName) +self:T2(self.lid..string.format("Switching ICLS to Channel %d Morse %s on unit %s when GROUP is SPAWNED",self.iclsDefault.Channel,tostring(self.iclsDefault.Morse),tostring(self.iclsDefault.BeaconName))) +elseif self:IsAlive()then +Channel=Channel or self.iclsDefault.Channel +Morse=Morse or self.iclsDefault.Morse +local unit=self:GetUnit(1) +if UnitName then +if type(UnitName)=="number"then +unit=self:GetUnit(UnitName) +else +unit=UNIT:FindByName(UnitName) +end +end +if not unit then +self:T(self.lid.."WARNING: Could not get ICLS unit. Trying first unit in the group") +unit=self:GetUnit(1) +end +if unit and unit:IsAlive()then +local UnitID=unit:GetID() +unit:CommandActivateICLS(Channel,UnitID,Morse) +self.icls.Channel=Channel +self.icls.Morse=Morse +self.icls.Band=nil +self.icls.BeaconName=unit:GetName() +self.icls.BeaconUnit=unit +self.icls.On=true +self:T(self.lid..string.format("Switching ICLS to Channel %d Morse %s on unit %s",self.icls.Channel,tostring(self.icls.Morse),self.icls.BeaconName)) +else +self:E(self.lid.."ERROR: Cound not set ICLS! Unit is not alive.") +end +end +return self +end +function OPSGROUP:TurnOffICLS() +if self.icls.BeaconUnit and self.icls.BeaconUnit:IsAlive()then +self.icls.BeaconUnit:CommandDeactivateICLS() +end +self:T(self.lid..string.format("Switching ICLS OFF")) +self.icls.On=false +end +function OPSGROUP:SetDefaultRadio(Frequency,Modulation,OffSwitch) +self.radioDefault={} +self.radioDefault.Freq=Frequency or 251 +self.radioDefault.Modu=Modulation or radio.modulation.AM +if OffSwitch then +self.radioDefault.On=false +else +self.radioDefault.On=true +end +return self +end +function OPSGROUP:GetRadio() +return self.radio.Freq,self.radio.Modu,self.radio.On +end +function OPSGROUP:SwitchRadio(Frequency,Modulation) +if self:IsInUtero()then +self:SetDefaultRadio(Frequency,Modulation) +self:T2(self.lid..string.format("Switching radio to frequency %.3f MHz %s when GROUP is SPAWNED",self.radioDefault.Freq,UTILS.GetModulationName(self.radioDefault.Modu))) +elseif self:IsAlive()then +Frequency=Frequency or self.radioDefault.Freq +Modulation=Modulation or self.radioDefault.Modu +if self.isAircraft and not self.radio.On then +self.group:SetOption(AI.Option.Air.id.SILENCE,false) +end +self.group:CommandSetFrequency(Frequency,Modulation) +self.radio.Freq=Frequency +self.radio.Modu=Modulation +self.radio.On=true +self:T(self.lid..string.format("Switching radio to frequency %.3f MHz %s",self.radio.Freq,UTILS.GetModulationName(self.radio.Modu))) +else +self:E(self.lid.."ERROR: Cound not set Radio! Group is not alive or not in utero any more") +end +return self +end +function OPSGROUP:TurnOffRadio() +if self:IsAlive()then +if self.isAircraft then +self.group:SetOption(AI.Option.Air.id.SILENCE,true) +self.radio.On=false +self:T(self.lid..string.format("Switching radio OFF")) +else +self:E(self.lid.."ERROR: Radio can only be turned off for aircraft!") +end +end +return self +end +function OPSGROUP:SetDefaultFormation(Formation) +self.optionDefault.Formation=Formation +return self +end +function OPSGROUP:SwitchFormation(Formation) +if self:IsAlive()then +Formation=Formation or self.optionDefault.Formation +if self.isAircraft then +self.group:SetOption(AI.Option.Air.id.FORMATION,Formation) +elseif self.isGround then +else +self:E(self.lid.."ERROR: Formation can only be set for aircraft or ground units!") +return self +end +self.option.Formation=Formation +self:T(self.lid..string.format("Switching formation to %d",self.option.Formation)) +end +return self +end +function OPSGROUP:SetDefaultCallsign(CallsignName,CallsignNumber) +self.callsignDefault={} +self.callsignDefault.NumberSquad=CallsignName +self.callsignDefault.NumberGroup=CallsignNumber or 1 +return self +end +function OPSGROUP:SwitchCallsign(CallsignName,CallsignNumber) +if self:IsInUtero()then +self:SetDefaultCallsign(CallsignName,CallsignNumber) +elseif self:IsAlive()then +CallsignName=CallsignName or self.callsignDefault.NumberSquad +CallsignNumber=CallsignNumber or self.callsignDefault.NumberGroup +self.callsign.NumberSquad=CallsignName +self.callsign.NumberGroup=CallsignNumber +self:T(self.lid..string.format("Switching callsign to %d-%d",self.callsign.NumberSquad,self.callsign.NumberGroup)) +self.group:CommandSetCallsign(self.callsign.NumberSquad,self.callsign.NumberGroup) +else +end +return self +end +function OPSGROUP:_UpdatePosition() +if self:IsAlive()then +self.positionLast=self.position or self:GetVec3() +self.headingLast=self.heading or self:GetHeading() +self.orientXLast=self.orientX or self:GetOrientationX() +self.velocityLast=self.velocity or self.group:GetVelocityMPS() +self.position=self:GetVec3() +self.heading=self:GetHeading() +self.orientX=self:GetOrientationX() +self.velocity=self:GetVelocity() +local Tnow=timer.getTime() +self.dTpositionUpdate=self.TpositionUpdate and Tnow-self.TpositionUpdate or 0 +self.TpositionUpdate=Tnow +if not self.traveldist then +self.traveldist=0 +end +self.travelds=UTILS.VecNorm(UTILS.VecSubstract(self.position,self.positionLast)) +self.traveldist=self.traveldist+self.travelds +end +return self +end +function OPSGROUP:_AllSameStatus(status) +for _,_element in pairs(self.elements)do +local element=_element +if element.status==OPSGROUP.ElementStatus.DEAD then +elseif element.status~=status then +return false +end +end +return true +end +function OPSGROUP:_AllSimilarStatus(status) +if status==OPSGROUP.ElementStatus.DEAD then +for _,_element in pairs(self.elements)do +local element=_element +if element.status~=OPSGROUP.ElementStatus.DEAD then +return false +end +end +return true +end +for _,_element in pairs(self.elements)do +local element=_element +self:T2(self.lid..string.format("Status=%s, element %s status=%s",status,element.name,element.status)) +if element.status~=OPSGROUP.ElementStatus.DEAD then +if status==OPSGROUP.ElementStatus.SPAWNED then +if element.status~=status and +element.status==OPSGROUP.ElementStatus.INUTERO then +return false +end +elseif status==OPSGROUP.ElementStatus.PARKING then +if element.status~=status or +(element.status==OPSGROUP.ElementStatus.INUTERO or +element.status==OPSGROUP.ElementStatus.SPAWNED)then +return false +end +elseif status==OPSGROUP.ElementStatus.ENGINEON then +if element.status~=status and +(element.status==OPSGROUP.ElementStatus.INUTERO or +element.status==OPSGROUP.ElementStatus.SPAWNED or +element.status==OPSGROUP.ElementStatus.PARKING)then +return false +end +elseif status==OPSGROUP.ElementStatus.TAXIING then +if element.status~=status and +(element.status==OPSGROUP.ElementStatus.INUTERO or +element.status==OPSGROUP.ElementStatus.SPAWNED or +element.status==OPSGROUP.ElementStatus.PARKING or +element.status==OPSGROUP.ElementStatus.ENGINEON)then +return false +end +elseif status==OPSGROUP.ElementStatus.TAKEOFF then +if element.status~=status and +(element.status==OPSGROUP.ElementStatus.INUTERO or +element.status==OPSGROUP.ElementStatus.SPAWNED or +element.status==OPSGROUP.ElementStatus.PARKING or +element.status==OPSGROUP.ElementStatus.ENGINEON or +element.status==OPSGROUP.ElementStatus.TAXIING)then +return false +end +elseif status==OPSGROUP.ElementStatus.AIRBORNE then +if element.status~=status and +(element.status==OPSGROUP.ElementStatus.INUTERO or +element.status==OPSGROUP.ElementStatus.SPAWNED or +element.status==OPSGROUP.ElementStatus.PARKING or +element.status==OPSGROUP.ElementStatus.ENGINEON or +element.status==OPSGROUP.ElementStatus.TAXIING or +element.status==OPSGROUP.ElementStatus.TAKEOFF)then +return false +end +elseif status==OPSGROUP.ElementStatus.LANDED then +if element.status~=status and +(element.status==OPSGROUP.ElementStatus.AIRBORNE or +element.status==OPSGROUP.ElementStatus.LANDING)then +return false +end +elseif status==OPSGROUP.ElementStatus.ARRIVED then +if element.status~=status and +(element.status==OPSGROUP.ElementStatus.AIRBORNE or +element.status==OPSGROUP.ElementStatus.LANDING or +element.status==OPSGROUP.ElementStatus.LANDED)then +return false +end +end +else +end +end +self:T2(self.lid..string.format("All %d elements have similar status %s ==> returning TRUE",#self.elements,status)) +return true +end +function OPSGROUP:_UpdateStatus(element,newstatus,airbase) +local oldstatus=element.status +element.status=newstatus +self:T3(self.lid..string.format("UpdateStatus element=%s: %s --> %s",element.name,oldstatus,newstatus)) +for _,_element in pairs(self.elements)do +local Element=_element +self:T3(self.lid..string.format("Element %s: %s",Element.name,Element.status)) +end +if newstatus==OPSGROUP.ElementStatus.SPAWNED then +if self:_AllSimilarStatus(newstatus)then +self:__Spawned(-0.5) +end +elseif newstatus==OPSGROUP.ElementStatus.PARKING then +if self:_AllSimilarStatus(newstatus)then +self:__Parking(-0.5) +end +elseif newstatus==OPSGROUP.ElementStatus.ENGINEON then +elseif newstatus==OPSGROUP.ElementStatus.TAXIING then +if self:_AllSimilarStatus(newstatus)then +self:__Taxiing(-0.5) +end +elseif newstatus==OPSGROUP.ElementStatus.TAKEOFF then +if self:_AllSimilarStatus(newstatus)then +self:__Takeoff(-0.5,airbase) +end +elseif newstatus==OPSGROUP.ElementStatus.AIRBORNE then +if self:_AllSimilarStatus(newstatus)then +self:__Airborne(-0.5) +end +elseif newstatus==OPSGROUP.ElementStatus.LANDED then +if self:_AllSimilarStatus(newstatus)then +if self:IsLandingAt()then +self:LandedAt() +else +self:Landed(airbase) +end +end +elseif newstatus==OPSGROUP.ElementStatus.ARRIVED then +if self:_AllSimilarStatus(newstatus)then +if self:IsLanded()then +self:Arrived() +elseif self:IsAirborne()then +self:Landed() +self:Arrived() +end +end +elseif newstatus==OPSGROUP.ElementStatus.DEAD then +if self:_AllSimilarStatus(newstatus)then +self:__Dead(-1) +end +end +end +function OPSGROUP:_SetElementStatusAll(status) +for _,_element in pairs(self.elements)do +local element=_element +if element.status~=OPSGROUP.ElementStatus.DEAD then +element.status=status +end +end +end +function OPSGROUP:GetElementByName(unitname) +for _,_element in pairs(self.elements)do +local element=_element +if element.name==unitname then +return element +end +end +return nil +end +function OPSGROUP:GetElementAlive() +for _,_element in pairs(self.elements)do +local element=_element +if element.status~=OPSGROUP.ElementStatus.DEAD then +if element.unit and element.unit:IsAlive()then +return element +end +end +end +return nil +end +function OPSGROUP:GetNelements(status) +local n=0 +for _,_element in pairs(self.elements)do +local element=_element +if element.status~=OPSGROUP.ElementStatus.DEAD then +if element.unit and element.unit:IsAlive()then +if status==nil or element.status==status then +n=n+1 +end +end +end +end +return n +end +function OPSGROUP:GetAmmoElement(element) +return self:GetAmmoUnit(element.unit) +end +function OPSGROUP:GetAmmoTot() +local units=self.group:GetUnits() +local Ammo={} +Ammo.Total=0 +Ammo.Guns=0 +Ammo.Rockets=0 +Ammo.Bombs=0 +Ammo.Torpedos=0 +Ammo.Missiles=0 +Ammo.MissilesAA=0 +Ammo.MissilesAG=0 +Ammo.MissilesAS=0 +Ammo.MissilesCR=0 +Ammo.MissilesSA=0 +for _,_unit in pairs(units)do +local unit=_unit +if unit and unit:IsAlive()~=nil then +local ammo=self:GetAmmoUnit(unit) +Ammo.Total=Ammo.Total+ammo.Total +Ammo.Guns=Ammo.Guns+ammo.Guns +Ammo.Rockets=Ammo.Rockets+ammo.Rockets +Ammo.Bombs=Ammo.Bombs+ammo.Bombs +Ammo.Torpedos=Ammo.Torpedos+ammo.Torpedos +Ammo.Missiles=Ammo.Missiles+ammo.Missiles +Ammo.MissilesAA=Ammo.MissilesAA+ammo.MissilesAA +Ammo.MissilesAG=Ammo.MissilesAG+ammo.MissilesAG +Ammo.MissilesAS=Ammo.MissilesAS+ammo.MissilesAS +Ammo.MissilesCR=Ammo.MissilesCR+ammo.MissilesCR +Ammo.MissilesSA=Ammo.MissilesSA+ammo.MissilesSA +end +end +return Ammo +end +function OPSGROUP:GetAmmoUnit(unit,display) +if display==nil then +display=false +end +local nammo=0 +local nshells=0 +local nrockets=0 +local nmissiles=0 +local nmissilesAA=0 +local nmissilesAG=0 +local nmissilesAS=0 +local nmissilesSA=0 +local nmissilesBM=0 +local nmissilesCR=0 +local ntorps=0 +local nbombs=0 +local text=string.format("OPSGROUP group %s - unit %s:\n",self.groupname,unit:GetName()) +local ammotable=unit:GetAmmo() +if ammotable then +local weapons=#ammotable +for w=1,weapons do +local Nammo=ammotable[w]["count"] +local Tammo=ammotable[w]["desc"]["typeName"] +local _weaponString=UTILS.Split(Tammo,"%.") +local _weaponName=_weaponString[#_weaponString] +local Category=ammotable[w].desc.category +local MissileCategory=nil +if Category==Weapon.Category.MISSILE then +MissileCategory=ammotable[w].desc.missileCategory +end +if Category==Weapon.Category.SHELL then +nshells=nshells+Nammo +text=text..string.format("- %d shells of type %s\n",Nammo,_weaponName) +elseif Category==Weapon.Category.ROCKET then +nrockets=nrockets+Nammo +text=text..string.format("- %d rockets of type %s\n",Nammo,_weaponName) +elseif Category==Weapon.Category.BOMB then +nbombs=nbombs+Nammo +text=text..string.format("- %d bombs of type %s\n",Nammo,_weaponName) +elseif Category==Weapon.Category.MISSILE then +if MissileCategory==Weapon.MissileCategory.AAM then +nmissiles=nmissiles+Nammo +nmissilesAA=nmissilesAA+Nammo +elseif MissileCategory==Weapon.MissileCategory.SAM then +nmissiles=nmissiles+Nammo +nmissilesSA=nmissilesSA+Nammo +elseif MissileCategory==Weapon.MissileCategory.ANTI_SHIP then +nmissiles=nmissiles+Nammo +nmissilesAS=nmissilesAS+Nammo +elseif MissileCategory==Weapon.MissileCategory.BM then +nmissiles=nmissiles+Nammo +nmissilesAG=nmissilesAG+Nammo +elseif MissileCategory==Weapon.MissileCategory.CRUISE then +nmissiles=nmissiles+Nammo +nmissilesCR=nmissilesCR+Nammo +elseif MissileCategory==Weapon.MissileCategory.OTHER then +nmissiles=nmissiles+Nammo +nmissilesAG=nmissilesAG+Nammo +end +text=text..string.format("- %d %s missiles of type %s\n",Nammo,self:_MissileCategoryName(MissileCategory),_weaponName) +elseif Category==Weapon.Category.TORPEDO then +ntorps=ntorps+Nammo +text=text..string.format("- %d torpedos of type %s\n",Nammo,_weaponName) +else +text=text..string.format("- %d unknown ammo of type %s (category=%d, missile category=%s)\n",Nammo,Tammo,Category,tostring(MissileCategory)) +end +end +end +if display then +self:I(self.lid..text) +else +self:T3(self.lid..text) +end +nammo=nshells+nrockets+nmissiles+nbombs+ntorps +local ammo={} +ammo.Total=nammo +ammo.Guns=nshells +ammo.Rockets=nrockets +ammo.Bombs=nbombs +ammo.Torpedos=ntorps +ammo.Missiles=nmissiles +ammo.MissilesAA=nmissilesAA +ammo.MissilesAG=nmissilesAG +ammo.MissilesAS=nmissilesAS +ammo.MissilesCR=nmissilesCR +ammo.MissilesBM=nmissilesBM +ammo.MissilesSA=nmissilesSA +return ammo +end +function OPSGROUP:_MissileCategoryName(categorynumber) +local cat="unknown" +if categorynumber==Weapon.MissileCategory.AAM then +cat="air-to-air" +elseif categorynumber==Weapon.MissileCategory.SAM then +cat="surface-to-air" +elseif categorynumber==Weapon.MissileCategory.BM then +cat="ballistic" +elseif categorynumber==Weapon.MissileCategory.ANTI_SHIP then +cat="anti-ship" +elseif categorynumber==Weapon.MissileCategory.CRUISE then +cat="cruise" +elseif categorynumber==Weapon.MissileCategory.OTHER then +cat="other" +end +return cat +end +function OPSGROUP:_CoordinateFromObject(Object) +if Object:IsInstanceOf("COORDINATE")then +return Object +else +if Object:IsInstanceOf("POSITIONABLE")or Object:IsInstanceOf("ZONE_BASE")then +self:T(self.lid.."WARNING: Coordinate is not a COORDINATE but a POSITIONABLE or ZONE. Trying to get coordinate") +return Object:GetCoordinate() +else +self:E(self.lid.."ERROR: Coordinate is neither a COORDINATE nor any POSITIONABLE or ZONE!") +end +end +return nil +end +FLIGHTGROUP={ +ClassName="FLIGHTGROUP", +homebase=nil, +destbase=nil, +homezone=nil, +destzone=nil, +actype=nil, +speedMax=nil, +rangemax=nil, +ceiling=nil, +fuellow=false, +fuellowthresh=nil, +fuellowrtb=nil, +fuelcritical=nil, +fuelcriticalthresh=nil, +fuelcriticalrtb=false, +outofAAMrtb=true, +outofAGMrtb=true, +squadron=nil, +flightcontrol=nil, +flaghold=nil, +Tholding=nil, +Tparking=nil, +menu=nil, +ishelo=nil, +RTBRecallCount=0, +} +FLIGHTGROUP.Attribute={ +TRANSPORTPLANE="TransportPlane", +AWACS="AWACS", +FIGHTER="Fighter", +BOMBER="Bomber", +TANKER="Tanker", +TRANSPORTHELO="TransportHelo", +ATTACKHELO="AttackHelo", +UAV="UAV", +OTHER="Other", +} +FLIGHTGROUP.version="0.6.1" +function FLIGHTGROUP:New(group) +local fg=_DATABASE:GetFlightGroup(group) +if fg then +fg:I(fg.lid..string.format("WARNING: Flight group already exists in data base!")) +return fg +end +local self=BASE:Inherit(self,OPSGROUP:New(group)) +self.lid=string.format("FLIGHTGROUP %s | ",self.groupname) +self:SetFuelLowThreshold() +self:SetFuelLowRTB() +self:SetFuelCriticalThreshold() +self:SetFuelCriticalRTB() +self:SetDefaultROE() +self:SetDefaultROT() +self:SetDetection() +self.isFlightgroup=true +self.flaghold=USERFLAG:New(string.format("%s_FlagHold",self.groupname)) +self.flaghold:Set(0) +self:AddTransition("*","RTB","Inbound") +self:AddTransition("*","RTZ","Inbound") +self:AddTransition("Inbound","Holding","Holding") +self:AddTransition("*","Refuel","Going4Fuel") +self:AddTransition("Going4Fuel","Refueled","Airborne") +self:AddTransition("*","LandAt","LandingAt") +self:AddTransition("LandingAt","LandedAt","LandedAt") +self:AddTransition("*","Wait","*") +self:AddTransition("*","FuelLow","*") +self:AddTransition("*","FuelCritical","*") +self:AddTransition("*","OutOfMissilesAA","*") +self:AddTransition("*","OutOfMissilesAG","*") +self:AddTransition("*","OutOfMissilesAS","*") +self:AddTransition("Airborne","EngageTarget","Engaging") +self:AddTransition("Engaging","Disengage","Airborne") +self:AddTransition("*","ElementParking","*") +self:AddTransition("*","ElementEngineOn","*") +self:AddTransition("*","ElementTaxiing","*") +self:AddTransition("*","ElementTakeoff","*") +self:AddTransition("*","ElementAirborne","*") +self:AddTransition("*","ElementLanded","*") +self:AddTransition("*","ElementArrived","*") +self:AddTransition("*","ElementOutOfAmmo","*") +self:AddTransition("*","Parking","Parking") +self:AddTransition("*","Taxiing","Taxiing") +self:AddTransition("*","Takeoff","Airborne") +self:AddTransition("*","Airborne","Airborne") +self:AddTransition("*","Landing","Landing") +self:AddTransition("*","Landed","Landed") +self:AddTransition("*","Arrived","Arrived") +if false then +BASE:TraceOnOff(true) +BASE:TraceClass(self.ClassName) +BASE:TraceLevel(1) +end +_DATABASE:AddFlightGroup(self) +self:HandleEvent(EVENTS.Birth,self.OnEventBirth) +self:HandleEvent(EVENTS.EngineStartup,self.OnEventEngineStartup) +self:HandleEvent(EVENTS.Takeoff,self.OnEventTakeOff) +self:HandleEvent(EVENTS.Land,self.OnEventLanding) +self:HandleEvent(EVENTS.EngineShutdown,self.OnEventEngineShutdown) +self:HandleEvent(EVENTS.PilotDead,self.OnEventPilotDead) +self:HandleEvent(EVENTS.Ejection,self.OnEventEjection) +self:HandleEvent(EVENTS.Crash,self.OnEventCrash) +self:HandleEvent(EVENTS.RemoveUnit,self.OnEventRemoveUnit) +self:HandleEvent(EVENTS.UnitLost,self.OnEventUnitLost) +self:HandleEvent(EVENTS.Kill,self.OnEventKill) +self:InitWaypoints() +self:_InitGroup() +self:__Status(-1) +self.timerQueueUpdate=TIMER:New(self._QueueUpdate,self):Start(2,5) +self.timerCheckZone=TIMER:New(self._CheckInZones,self):Start(3,10) +return self +end +function FLIGHTGROUP:AddTaskEnrouteEngageTargetsInZone(ZoneRadius,TargetTypes,Priority) +local Task=self.group:EnRouteTaskEngageTargetsInZone(ZoneRadius:GetVec2(),ZoneRadius:GetRadius(),TargetTypes,Priority) +self:AddTaskEnroute(Task) +end +function FLIGHTGROUP:SetAirwing(airwing) +self:T(self.lid..string.format("Add flight to AIRWING %s",airwing.alias)) +self.airwing=airwing +return self +end +function FLIGHTGROUP:GetAirWing() +return self.airwing +end +function FLIGHTGROUP:SetFlightControl(flightcontrol) +if self.flightcontrol then +if self.flightcontrol.airbasename==flightcontrol.airbasename then +return +else +self.flightcontrol:_RemoveFlight(self) +end +end +self:I(self.lid..string.format("Setting FLIGHTCONTROL to airbase %s",flightcontrol.airbasename)) +self.flightcontrol=flightcontrol +table.insert(flightcontrol.flights,self) +if self.isAI==false then +self:_UpdateMenu(0.5) +end +return self +end +function FLIGHTGROUP:GetFlightControl() +return self.flightcontrol +end +function FLIGHTGROUP:SetHomebase(HomeAirbase) +self.homebase=HomeAirbase +return self +end +function FLIGHTGROUP:SetDestinationbase(DestinationAirbase) +self.destbase=DestinationAirbase +return self +end +function FLIGHTGROUP:SetAirboss(airboss) +self.airboss=airboss +return self +end +function FLIGHTGROUP:SetFuelLowThreshold(threshold) +self.fuellowthresh=threshold or 25 +return self +end +function FLIGHTGROUP:SetFuelLowRTB(switch) +if switch==false then +self.fuellowrtb=false +else +self.fuellowrtb=true +end +return self +end +function FLIGHTGROUP:SetOutOfAAMRTB(switch) +if switch==false then +self.outofAAMrtb=false +else +self.outofAAMrtb=true +end +return self +end +function FLIGHTGROUP:SetOutOfAGMRTB(switch) +if switch==false then +self.outofAGMrtb=false +else +self.outofAGMrtb=true +end +return self +end +function FLIGHTGROUP:SetFuelLowRefuel(switch) +if switch==false then +self.fuellowrefuel=false +else +self.fuellowrefuel=true +end +return self +end +function FLIGHTGROUP:SetFuelCriticalThreshold(threshold) +self.fuelcriticalthresh=threshold or 10 +return self +end +function FLIGHTGROUP:SetFuelCriticalRTB(switch) +if switch==false then +self.fuelcriticalrtb=false +else +self.fuelcriticalrtb=true +end +return self +end +function FLIGHTGROUP:SetEngageDetectedOn(RangeMax,TargetTypes,EngageZoneSet,NoEngageZoneSet) +if TargetTypes then +if type(TargetTypes)~="table"then +TargetTypes={TargetTypes} +end +else +TargetTypes={"All"} +end +if EngageZoneSet and EngageZoneSet:IsInstanceOf("ZONE_BASE")then +local zoneset=SET_ZONE:New():AddZone(EngageZoneSet) +EngageZoneSet=zoneset +end +if NoEngageZoneSet and NoEngageZoneSet:IsInstanceOf("ZONE_BASE")then +local zoneset=SET_ZONE:New():AddZone(NoEngageZoneSet) +NoEngageZoneSet=zoneset +end +self.engagedetectedOn=true +self.engagedetectedRmax=UTILS.NMToMeters(RangeMax or 25) +self.engagedetectedTypes=TargetTypes +self.engagedetectedEngageZones=EngageZoneSet +self.engagedetectedNoEngageZones=NoEngageZoneSet +self:SetDetection(true) +return self +end +function FLIGHTGROUP:SetEngageDetectedOff() +self.engagedetectedOn=false +return self +end +function FLIGHTGROUP:SetDespawnAfterLanding() +self.despawnAfterLanding=true +return self +end +function FLIGHTGROUP:IsParking() +return self:Is("Parking") +end +function FLIGHTGROUP:IsTaxiing() +return self:Is("Taxiing") +end +function FLIGHTGROUP:IsAirborne() +return self:Is("Airborne") +end +function FLIGHTGROUP:IsWaiting() +return self:Is("Waiting") +end +function FLIGHTGROUP:IsLanding() +return self:Is("Landing") +end +function FLIGHTGROUP:IsLanded() +return self:Is("Landed") +end +function FLIGHTGROUP:IsArrived() +return self:Is("Arrived") +end +function FLIGHTGROUP:IsInbound() +return self:Is("Inbound") +end +function FLIGHTGROUP:IsHolding() +return self:Is("Holding") +end +function FLIGHTGROUP:IsGoing4Fuel() +return self:Is("Going4Fuel") +end +function FLIGHTGROUP:IsLandingAt() +return self:Is("LandingAt") +end +function FLIGHTGROUP:IsLandedAt() +return self:Is("LandedAt") +end +function FLIGHTGROUP:IsFuelLow() +return self.fuellow +end +function FLIGHTGROUP:IsFuelCritical() +return self.fuelcritical +end +function FLIGHTGROUP:CanAirToGround(ExcludeGuns) +local ammo=self:GetAmmoTot() +if ExcludeGuns then +return ammo.MissilesAG+ammo.Rockets+ammo.Bombs>0 +else +return ammo.MissilesAG+ammo.Rockets+ammo.Bombs+ammo.Guns>0 +end +end +function FLIGHTGROUP:CanAirToAir(ExcludeGuns) +local ammo=self:GetAmmoTot() +if ExcludeGuns then +return ammo.MissilesAA>0 +else +return ammo.MissilesAA+ammo.Guns>0 +end +end +function FLIGHTGROUP:StartUncontrolled(delay) +if delay and delay>0 then +self:T2(self.lid..string.format("Starting uncontrolled group in %d seconds",delay)) +self:ScheduleOnce(delay,FLIGHTGROUP.StartUncontrolled,self) +else +if self:IsAlive()then +self:T(self.lid.."Starting uncontrolled group") +self.group:StartUncontrolled(delay) +self.isUncontrolled=true +else +self:E(self.lid.."ERROR: Could not start uncontrolled group as it is NOT alive!") +end +end +return self +end +function FLIGHTGROUP:ClearToLand(Delay) +if Delay and Delay>0 then +self:ScheduleOnce(Delay,FLIGHTGROUP.ClearToLand,self) +else +if self:IsHolding()then +self:T(self.lid..string.format("Clear to land ==> setting holding flag to 1 (true)")) +self.flaghold:Set(1) +end +end +return self +end +function FLIGHTGROUP:GetFuelMin() +local fuelmin=math.huge +for i,_element in pairs(self.elements)do +local element=_element +local unit=element.unit +local life=unit:GetLife() +if unit and unit:IsAlive()and life>1 then +local fuel=unit:GetFuel() +if fuel false")) +return false +elseif self:IsStopped()then +self:T(self.lid..string.format("Onbefore Status STOPPED ==> false")) +return false +end +return true +end +function FLIGHTGROUP:onafterStatus(From,Event,To) +local fsmstate=self:GetState() +self:_UpdatePosition() +if self.detectionOn then +self:_CheckDetectedUnits() +end +if self:IsParking()then +for _,_element in pairs(self.elements)do +local element=_element +if element.parking then +local dist=element.unit:GetCoordinate():Get2DDistance(element.parking.Coordinate) +if dist>10 then +if element.status==OPSGROUP.ElementStatus.ENGINEON then +self:ElementTaxiing(element) +end +end +else +end +end +end +if self.verbose>=1 then +local nTaskTot,nTaskSched,nTaskWP=self:CountRemainingTasks() +local nMissions=self:CountRemainingMissison() +local text=string.format("Status %s [%d/%d]: Tasks=%d (%d,%d) Curr=%d, Missions=%s, Waypoint=%d/%d, Detected=%d, Home=%s, Destination=%s", +fsmstate,#self.elements,#self.elements,nTaskTot,nTaskSched,nTaskWP,self.taskcurrent,nMissions,self.currentwp or 0,self.waypoints and#self.waypoints or 0, +self.detectedunits:Count(),self.homebase and self.homebase:GetName()or"unknown",self.destbase and self.destbase:GetName()or"unknown") +self:I(self.lid..text) +end +if self.verbose>=2 then +local text="Elements:" +for i,_element in pairs(self.elements)do +local element=_element +local name=element.name +local status=element.status +local unit=element.unit +local fuel=unit:GetFuel()or 0 +local life=unit:GetLifeRelative()or 0 +local parking=element.parking and tostring(element.parking.TerminalID)or"X" +local ammo=self:GetAmmoElement(element) +text=text..string.format("\n[%d] %s: status=%s, fuel=%.1f, life=%.1f, guns=%d, rockets=%d, bombs=%d, missiles=%d (AA=%d, AG=%d, AS=%s), parking=%s", +i,name,status,fuel*100,life*100,ammo.Guns,ammo.Rockets,ammo.Bombs,ammo.Missiles,ammo.MissilesAA,ammo.MissilesAG,ammo.MissilesAS,parking) +end +if#self.elements==0 then +text=text.." none!" +end +self:I(self.lid..text) +end +if self.verbose>=4 and self:IsAlive()then +local ds=self.travelds +local dt=self.dTpositionUpdate +local v=ds/dt +local TmaxFuel=math.huge +for _,_element in pairs(self.elements)do +local element=_element +local fuel=element.unit:GetFuel()or 0 +local dFrel=element.fuelrel-fuel +local dFreldt=dFrel/dt +local Tfuel=fuel/dFreldt +if Tfuel Tfuel=%.1f min",element.name,fuel*100,dFrel*100,dFreldt*100*60,Tfuel/60)) +element.fuelrel=fuel +end +self:I(self.lid..string.format("Travelled ds=%.1f km dt=%.1f s ==> v=%.1f knots. Fuel left for %.1f min",self.traveldist/1000,dt,UTILS.MpsToKnots(v),TmaxFuel/60)) +end +self:_PrintTaskAndMissionStatus() +if self:IsAlive()and self.group:IsAirborne(true)then +local fuelmin=self:GetFuelMin() +if fuelmin>=self.fuellowthresh then +self.fuellow=false +end +if fuelmin>=self.fuelcriticalthresh then +self.fuelcritical=false +end +if fuelmin spawned",element.name,self.homebase and self.homebase:GetName()or"unknown")) +self:__ElementSpawned(0.0,element) +end +end +end +function FLIGHTGROUP:OnEventEngineStartup(EventData) +if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then +local unit=EventData.IniUnit +local group=EventData.IniGroup +local unitname=EventData.IniUnitName +local element=self:GetElementByName(unitname) +if element then +if self:IsAirborne()or self:IsInbound()or self:IsHolding()then +else +self:T3(self.lid..string.format("EVENT: Element %s started engines ==> taxiing (if AI)",element.name)) +if self.isAI then +self:ElementEngineOn(element) +else +if element.ai then +self:ElementEngineOn(element) +end +end +end +end +end +end +function FLIGHTGROUP:OnEventTakeOff(EventData) +if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then +local unit=EventData.IniUnit +local group=EventData.IniGroup +local unitname=EventData.IniUnitName +local element=self:GetElementByName(unitname) +if element then +self:T3(self.lid..string.format("EVENT: Element %s took off ==> airborne",element.name)) +self:ElementTakeoff(element,EventData.Place) +end +end +end +function FLIGHTGROUP:OnEventLanding(EventData) +if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then +local unit=EventData.IniUnit +local group=EventData.IniGroup +local unitname=EventData.IniUnitName +local element=self:GetElementByName(unitname) +local airbase=EventData.Place +local airbasename="unknown" +if airbase then +airbasename=tostring(airbase:GetName()) +end +if element then +self:T3(self.lid..string.format("EVENT: Element %s landed at %s ==> landed",element.name,airbasename)) +self:ElementLanded(element,airbase) +end +end +end +function FLIGHTGROUP:OnEventEngineShutdown(EventData) +if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then +local unit=EventData.IniUnit +local group=EventData.IniGroup +local unitname=EventData.IniUnitName +local element=self:GetElementByName(unitname) +if element then +if element.unit and element.unit:IsAlive()then +local airbase=self:GetClosestAirbase() +local parking=self:GetParkingSpot(element,10,airbase) +if airbase and parking then +self:ElementArrived(element,airbase,parking) +self:T3(self.lid..string.format("EVENT: Element %s shut down engines ==> arrived",element.name)) +else +self:T3(self.lid..string.format("EVENT: Element %s shut down engines but is not parking. Is it dead?",element.name)) +end +else +end +end +end +end +function FLIGHTGROUP:OnEventCrash(EventData) +if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then +local unit=EventData.IniUnit +local group=EventData.IniGroup +local unitname=EventData.IniUnitName +local element=self:GetElementByName(unitname) +if element and element.status~=OPSGROUP.ElementStatus.DEAD then +self:T(self.lid..string.format("EVENT: Element %s crashed ==> destroyed",element.name)) +self:ElementDestroyed(element) +end +end +end +function FLIGHTGROUP:OnEventUnitLost(EventData) +if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then +self:T2(self.lid..string.format("EVENT: Unit %s lost at t=%.3f",EventData.IniUnitName,timer.getTime())) +local unit=EventData.IniUnit +local group=EventData.IniGroup +local unitname=EventData.IniUnitName +local element=self:GetElementByName(unitname) +if element and element.status~=OPSGROUP.ElementStatus.DEAD then +self:T(self.lid..string.format("EVENT: Element %s unit lost ==> destroyed t=%.3f",element.name,timer.getTime())) +self:ElementDestroyed(element) +end +end +end +function FLIGHTGROUP:OnEventKill(EventData) +if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then +local targetname=tostring(EventData.TgtUnitName) +self:T2(self.lid..string.format("EVENT: Unit %s killed object %s!",tostring(EventData.IniUnitName),targetname)) +local target=UNIT:FindByName(targetname) +if not target then +target=STATIC:FindByName(targetname,false) +end +if target then +self:T(self.lid..string.format("EVENT: Unit %s killed unit/static %s!",tostring(EventData.IniUnitName),targetname)) +self.Nkills=self.Nkills+1 +local mission=self:GetMissionCurrent() +if mission then +mission.Nkills=mission.Nkills+1 +end +end +end +end +function FLIGHTGROUP:OnEventRemoveUnit(EventData) +if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then +local unit=EventData.IniUnit +local group=EventData.IniGroup +local unitname=EventData.IniUnitName +local element=self:GetElementByName(unitname) +if element then +self:T3(self.lid..string.format("EVENT: Element %s removed ==> dead",element.name)) +self:ElementDead(element) +end +end +end +function FLIGHTGROUP:onafterElementSpawned(From,Event,To,Element) +self:T(self.lid..string.format("Element spawned %s",Element.name)) +self:_UpdateStatus(Element,OPSGROUP.ElementStatus.SPAWNED) +if Element.unit:InAir(true)then +self:__ElementAirborne(0.11,Element) +else +local spot=self:GetParkingSpot(Element,10) +if spot then +self:__ElementParking(0.11,Element,spot) +else +self:T(self.lid..string.format("Element spawned not in air but not on any parking spot.")) +self:__ElementParking(0.11,Element) +end +end +end +function FLIGHTGROUP:onafterElementParking(From,Event,To,Element,Spot) +self:T(self.lid..string.format("Element parking %s at spot %s",Element.name,Element.parking and tostring(Element.parking.TerminalID)or"N/A")) +self:_UpdateStatus(Element,OPSGROUP.ElementStatus.PARKING) +if Spot then +self:_SetElementParkingAt(Element,Spot) +end +if self:IsTakeoffCold()then +elseif self:IsTakeoffHot()then +self:__ElementEngineOn(0.5,Element) +elseif self:IsTakeoffRunway()then +self:__ElementEngineOn(0.5,Element) +end +end +function FLIGHTGROUP:onafterElementEngineOn(From,Event,To,Element) +self:T(self.lid..string.format("Element %s started engines",Element.name)) +self:_UpdateStatus(Element,OPSGROUP.ElementStatus.ENGINEON) +end +function FLIGHTGROUP:onafterElementTaxiing(From,Event,To,Element) +local TerminalID=Element.parking and tostring(Element.parking.TerminalID)or"N/A" +self:T(self.lid..string.format("Element taxiing %s. Parking spot %s is now free",Element.name,TerminalID)) +self:_SetElementParkingFree(Element) +self:_UpdateStatus(Element,OPSGROUP.ElementStatus.TAXIING) +end +function FLIGHTGROUP:onafterElementTakeoff(From,Event,To,Element,airbase) +self:T(self.lid..string.format("Element takeoff %s at %s airbase.",Element.name,airbase and airbase:GetName()or"unknown")) +if Element.parking then +self:_SetElementParkingFree(Element) +end +self:_UpdateStatus(Element,OPSGROUP.ElementStatus.TAKEOFF,airbase) +self:__ElementAirborne(2,Element) +end +function FLIGHTGROUP:onafterElementAirborne(From,Event,To,Element) +self:T2(self.lid..string.format("Element airborne %s",Element.name)) +self:_UpdateStatus(Element,OPSGROUP.ElementStatus.AIRBORNE) +end +function FLIGHTGROUP:onafterElementLanded(From,Event,To,Element,airbase) +self:T2(self.lid..string.format("Element landed %s at %s airbase",Element.name,airbase and airbase:GetName()or"unknown")) +if self.despawnAfterLanding then +self:DespawnElement(Element) +else +if self.ishelo then +local Spot=self:GetParkingSpot(Element,10,airbase) +self:_SetElementParkingAt(Element,Spot) +end +self:_UpdateStatus(Element,OPSGROUP.ElementStatus.LANDED,airbase) +end +end +function FLIGHTGROUP:onafterElementArrived(From,Event,To,Element,airbase,Parking) +self:T(self.lid..string.format("Element arrived %s at %s airbase using parking spot %d",Element.name,airbase and airbase:GetName()or"unknown",Parking and Parking.TerminalID or-99)) +self:_SetElementParkingAt(Element,Parking) +self:_UpdateStatus(Element,OPSGROUP.ElementStatus.ARRIVED) +end +function FLIGHTGROUP:onafterElementDestroyed(From,Event,To,Element) +self:GetParent(self).onafterElementDestroyed(self,From,Event,To,Element) +end +function FLIGHTGROUP:onafterElementDead(From,Event,To,Element) +self:GetParent(self).onafterElementDead(self,From,Event,To,Element) +if self.flightcontrol and Element.parking then +self.flightcontrol:SetParkingFree(Element.parking) +end +Element.parking=nil +end +function FLIGHTGROUP:onafterSpawned(From,Event,To) +self:T(self.lid..string.format("Flight spawned")) +self:_UpdatePosition() +if self.isAI then +self:SwitchROE(self.option.ROE) +self:SwitchROT(self.option.ROT) +self:SwitchFormation(self.option.Formation) +self:_SwitchTACAN() +if self.radioDefault then +self:SwitchRadio() +else +self:SetDefaultRadio(self.radio.Freq,self.radio.Modu,self.radio.On) +end +if self.callsignDefault then +self:SwitchCallsign(self.callsignDefault.NumberSquad,self.callsignDefault.NumberGroup) +else +self:SetDefaultCallsign(self.callsign.NumberSquad,self.callsign.NumberGroup) +end +self:GetGroup():SetOption(AI.Option.Air.id.PROHIBIT_JETT,true) +self:GetGroup():SetOption(AI.Option.Air.id.PROHIBIT_AB,true) +self:GetGroup():SetOption(AI.Option.Air.id.RTB_ON_BINGO,false) +self:__UpdateRoute(-0.5) +else +self:_UpdateMenu() +end +end +function FLIGHTGROUP:onafterParking(From,Event,To) +self:T(self.lid..string.format("Flight is parking")) +local airbase=self:GetClosestAirbase() +local airbasename=airbase:GetName()or"unknown" +self.Tparking=timer.getAbsTime() +local flightcontrol=_DATABASE:GetFlightControl(airbasename) +if flightcontrol then +self:SetFlightControl(flightcontrol) +if self.flightcontrol then +self.flightcontrol:SetFlightStatus(self,FLIGHTCONTROL.FlightStatus.PARKING) +if not self.isAI then +self:_UpdateMenu(0.5) +end +end +end +end +function FLIGHTGROUP:onafterTaxiing(From,Event,To) +self:T(self.lid..string.format("Flight is taxiing")) +self.Tparking=nil +local airbase=self:GetClosestAirbase() +if self.flightcontrol and airbase and self.flightcontrol.airbasename==airbase:GetName()then +if self.isAI then +self.flightcontrol:SetFlightStatus(self,FLIGHTCONTROL.FlightStatus.TAKEOFF) +else +self.flightcontrol:SetFlightStatus(self,FLIGHTCONTROL.FlightStatus.TAXIOUT) +self:_UpdateMenu() +end +end +end +function FLIGHTGROUP:onafterTakeoff(From,Event,To,airbase) +self:T(self.lid..string.format("Flight takeoff from %s",airbase and airbase:GetName()or"unknown airbase")) +if self.flightcontrol and airbase and self.flightcontrol.airbasename==airbase:GetName()then +self.flightcontrol:_RemoveFlight(self) +self.flightcontrol=nil +end +end +function FLIGHTGROUP:onafterAirborne(From,Event,To) +self:T(self.lid..string.format("Flight airborne")) +if self.isAI then +self:_CheckGroupDone(1) +else +self:_UpdateMenu() +end +end +function FLIGHTGROUP:onafterLanding(From,Event,To) +self:T(self.lid..string.format("Flight is landing")) +self:_SetElementStatusAll(OPSGROUP.ElementStatus.LANDING) +end +function FLIGHTGROUP:onafterLanded(From,Event,To,airbase) +self:T(self.lid..string.format("Flight landed at %s",airbase and airbase:GetName()or"unknown place")) +if self.flightcontrol and airbase and self.flightcontrol.airbasename==airbase:GetName()then +self.flightcontrol:SetFlightStatus(self,FLIGHTCONTROL.FlightStatus.TAXIINB) +end +end +function FLIGHTGROUP:onafterLandedAt(From,Event,To) +self:T(self.lid..string.format("Flight landed at")) +end +function FLIGHTGROUP:onafterArrived(From,Event,To) +self:T(self.lid..string.format("Flight arrived")) +if self.flightcontrol then +self.flightcontrol:SetFlightStatus(self,FLIGHTCONTROL.FlightStatus.ARRIVED) +end +if not self.airwing then +self:Despawn(5*60) +end +end +function FLIGHTGROUP:onafterDead(From,Event,To) +if self.flightcontrol then +self.flightcontrol:_RemoveFlight(self) +self.flightcontrol=nil +end +if self.Ndestroyed==#self.elements then +if self.squadron then +self.squadron:DelGroup(self.groupname) +end +else +if self.airwing then +self.airwing:AddAsset(self.group,1) +end +end +self:GetParent(self).onafterDead(self,From,Event,To) +end +function FLIGHTGROUP:onbeforeUpdateRoute(From,Event,To,n) +local allowed=true +local trepeat=nil +if self:IsAlive()then +self:T3(self.lid.."Update route possible. Group is ALIVE") +elseif self:IsDead()then +self:E(self.lid.."Update route denied. Group is DEAD!") +allowed=false +else +self:T(self.lid.."Update route denied ==> checking back in 5 sec") +trepeat=-5 +allowed=false +end +if n and n<1 then +self:E(self.lid.."Update route denied because waypoint n<1!") +allowed=false +end +if not self.currentwp then +self:E(self.lid.."Update route denied because self.currentwp=nil!") +allowed=false +end +local N=n or self.currentwp+1 +if not N or N<1 then +self:E(self.lid.."Update route denied because N=nil or N<1") +trepeat=-5 +allowed=false +end +if self.taskcurrent>0 then +local task=self:GetTaskByID(self.taskcurrent) +if task then +if task.dcstask.id=="PatrolZone"then +else +local taskname=task and task.description or"No description" +self:E(self.lid..string.format("WARNING: Update route denied because taskcurrent=%d>0! Task description = %s",self.taskcurrent,tostring(taskname))) +allowed=false +end +else +self:T(self.lid..string.format("WARNING: before update route taskcurrent=%d>0 but no task?!",self.taskcurrent)) +allowed=false +end +end +if not self.isAI then +allowed=false +end +self:T2(self.lid..string.format("Onbefore Updateroute allowed=%s state=%s repeat in %s",tostring(allowed),self:GetState(),tostring(trepeat))) +if trepeat then +self:__UpdateRoute(trepeat,n) +end +return allowed +end +function FLIGHTGROUP:onafterUpdateRoute(From,Event,To,n) +n=n or self.currentwp+1 +self:_UpdateWaypointTasks(n) +local wp={} +local speed=self.group and self.group:GetVelocityKMH()or 100 +local current=self.group:GetCoordinate():WaypointAir(COORDINATE.WaypointAltType.BARO,COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.TurningPoint,speed,true,nil,{},"Current") +table.insert(wp,current) +local Nwp=self.waypoints and#self.waypoints or 0 +for i=n,Nwp do +table.insert(wp,self.waypoints[i]) +end +local hb=self.homebase and self.homebase:GetName()or"unknown" +local db=self.destbase and self.destbase:GetName()or"unknown" +self:T(self.lid..string.format("Updating route for WP #%d-%d homebase=%s destination=%s",n,#wp,hb,db)) +if#wp>1 then +self:Route(wp) +else +if self:IsAirborne()then +self:T(self.lid.."No waypoints left ==> CheckGroupDone") +self:_CheckGroupDone() +end +end +end +function FLIGHTGROUP:onafterRespawn(From,Event,To,Template) +self:T(self.lid.."Respawning group!") +local template=UTILS.DeepCopy(Template or self.template) +if self.group and self.group:InAir()then +template.lateActivation=false +self.respawning=true +self.group=self.group:Respawn(template) +end +end +function FLIGHTGROUP:onafterOutOfMissilesAA(From,Event,To) +self:I(self.lid.."Group is out of AA Missiles!") +if self.outofAAMrtb then +local airbase=self.destbase or self.homebase +self:__RTB(-5,airbase) +end +end +function FLIGHTGROUP:onafterOutOfMissilesAG(From,Event,To) +self:I(self.lid.."Group is out of AG Missiles!") +if self.outofAGMrtb then +local airbase=self.destbase or self.homebase +self:__RTB(-5,airbase) +end +end +function FLIGHTGROUP:_CheckGroupDone(delay) +if self:IsAlive()and self.isAI then +if delay and delay>0 then +self:ScheduleOnce(delay,FLIGHTGROUP._CheckGroupDone,self) +else +if self.missionpaused then +self:UnpauseMission() +return +end +if self:IsEngaging()then +return +end +local nTasks=self:CountRemainingTasks() +local nMissions=self:CountRemainingMissison() +if self.passedfinalwp then +if self.currentmission==nil and self.taskcurrent==0 then +if nTasks==0 and nMissions==0 then +local destbase=self.destbase or self.homebase +local destzone=self.destzone or self.homezone +if destbase then +self:T(self.lid.."Passed Final WP and No current and/or future missions/task ==> RTB!") +self:__RTB(-3,destbase) +elseif destzone then +self:T(self.lid.."Passed Final WP and No current and/or future missions/task ==> RTZ!") +self:__RTZ(-3,destzone) +else +self:T(self.lid.."Passed Final WP and NO Tasks/Missions left. No DestBase or DestZone ==> Wait!") +self:__Wait(-1) +end +else +self:T(self.lid..string.format("Passed Final WP but Tasks=%d or Missions=%d left in the queue. Wait!",nTasks,nMissions)) +self:__Wait(-1) +end +else +self:T(self.lid..string.format("Passed Final WP but still have current Task (#%s) or Mission (#%s) left to do",tostring(self.taskcurrent),tostring(self.currentmission))) +end +else +self:T(self.lid..string.format("Flight (status=%s) did NOT pass the final waypoint yet ==> update route",self:GetState())) +self:__UpdateRoute(-1) +end +end +end +end +function FLIGHTGROUP:onbeforeRTB(From,Event,To,airbase,SpeedTo,SpeedHold) +if self:IsAlive()then +local allowed=true +local Tsuspend=nil +if airbase==nil then +self:E(self.lid.."ERROR: Airbase is nil in RTB() call!") +allowed=false +end +if airbase and airbase:GetCoalition()~=self.group:GetCoalition()and airbase:GetCoalition()>0 then +self:E(self.lid..string.format("ERROR: Wrong airbase coalition %d in RTB() call! We allow only same as group %d or neutral airbases 0.",airbase:GetCoalition(),self.group:GetCoalition())) +allowed=false +end +if not self.group:IsAirborne(true)then +self:I(self.lid..string.format("WARNING: Group is not AIRBORNE ==> RTB event is suspended for 20 sec.")) +allowed=false +Tsuspend=-20 +local groupspeed=self.group:GetVelocityMPS() +if groupspeed<=1 then self.RTBRecallCount=self.RTBRecallCount+1 end +if self.RTBRecallCount>6 then +self:Despawn(5) +end +end +if not(self:IsFuelLow()or self:IsFuelCritical())then +local Ntot,Nsched,Nwp=self:CountRemainingTasks() +if self.taskcurrent>0 then +self:I(self.lid..string.format("WARNING: Got current task ==> RTB event is suspended for 10 sec.")) +Tsuspend=-10 +allowed=false +end +if Nsched>0 then +self:I(self.lid..string.format("WARNING: Still got %d SCHEDULED tasks in the queue ==> RTB event is suspended for 10 sec.",Nsched)) +Tsuspend=-10 +allowed=false +end +if Nwp>0 then +self:I(self.lid..string.format("WARNING: Still got %d WAYPOINT tasks in the queue ==> RTB event is suspended for 10 sec.",Nwp)) +Tsuspend=-10 +allowed=false +end +end +if Tsuspend and not allowed then +self:__RTB(Tsuspend,airbase,SpeedTo,SpeedHold) +end +return allowed +else +self:E(self.lid.."WARNING: Group is not alive! RTB call not allowed.") +return false +end +end +function FLIGHTGROUP:onafterRTB(From,Event,To,airbase,SpeedTo,SpeedHold,SpeedLand) +self:T(self.lid..string.format("RTB: event=%s: %s --> %s to %s",Event,From,To,airbase:GetName())) +self.destbase=airbase +self.Tholding=nil +for _,_mission in pairs(self.missionqueue)do +local mission=_mission +local mystatus=mission:GetGroupStatus(self) +if not(mystatus==AUFTRAG.GroupStatus.DONE or mystatus==AUFTRAG.GroupStatus.CANCELLED)then +local text=string.format("Canceling mission %s in state=%s",mission.name,mission.status) +self:T(self.lid..text) +self:MissionCancel(mission) +end +end +SpeedTo=SpeedTo or UTILS.KmphToKnots(self.speedCruise) +SpeedHold=SpeedHold or(self.ishelo and 80 or 250) +SpeedLand=SpeedLand or(self.ishelo and 40 or 170) +local text=string.format("Flight group set to hold at airbase %s. SpeedTo=%d, SpeedHold=%d, SpeedLand=%d",airbase:GetName(),SpeedTo,SpeedHold,SpeedLand) +self:T(self.lid..text) +local althold=self.ishelo and 1000+math.random(10)*100 or math.random(4,10)*1000 +local c0=self.group:GetCoordinate() +local p0=airbase:GetZone():GetRandomCoordinate():SetAltitude(UTILS.FeetToMeters(althold)) +local p1=nil +local wpap=nil +local fc=_DATABASE:GetFlightControl(airbase:GetName()) +if fc then +local HoldingPoint=fc:_GetHoldingpoint(self) +p0=HoldingPoint.pos0 +p1=HoldingPoint.pos1 +if self.Debug then +p0:MarkToAll("Holding point P0") +p1:MarkToAll("Holding point P1") +end +self:SetFlightControl(fc) +self.flightcontrol:SetFlightStatus(self,FLIGHTCONTROL.FlightStatus.INBOUND) +end +local x1=self.ishelo and UTILS.NMToMeters(5.0)or UTILS.NMToMeters(10) +local x2=self.ishelo and UTILS.NMToMeters(2.5)or UTILS.NMToMeters(5) +local alpha=math.rad(3) +local h1=x1*math.tan(alpha) +local h2=x2*math.tan(alpha) +local runway=airbase:GetActiveRunway() +self.flaghold:Set(0) +local holdtime=5*60 +if fc or self.airboss then +holdtime=nil +end +local TaskArrived=self.group:TaskFunction("FLIGHTGROUP._ReachedHolding",self) +local TaskOrbit=self.group:TaskOrbit(p0,nil,UTILS.KnotsToMps(SpeedHold),p1) +local TaskLand=self.group:TaskCondition(nil,self.flaghold.UserFlagName,1,nil,holdtime) +local TaskHold=self.group:TaskControlled(TaskOrbit,TaskLand) +local TaskKlar=self.group:TaskFunction("FLIGHTGROUP._ClearedToLand",self) +local wp={} +wp[#wp+1]=c0:WaypointAir(nil,COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.TurningPoint,UTILS.KnotsToKmph(SpeedTo),true,nil,{},"Current Pos") +wp[#wp+1]=p0:WaypointAir(nil,COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.TurningPoint,UTILS.KnotsToKmph(SpeedTo),true,nil,{TaskArrived,TaskHold,TaskKlar},"Holding Point") +if airbase:GetAirbaseCategory()==Airbase.Category.AIRDROME then +local papp=airbase:GetCoordinate():Translate(x1,runway.heading-180):SetAltitude(h1) +wp[#wp+1]=papp:WaypointAirTurningPoint(nil,UTILS.KnotsToKmph(SpeedLand),{},"Final Approach") +local pland=airbase:GetCoordinate():Translate(x2,runway.heading-180):SetAltitude(h2) +wp[#wp+1]=pland:WaypointAirLanding(UTILS.KnotsToKmph(SpeedLand),airbase,{},"Landing") +elseif airbase:GetAirbaseCategory()==Airbase.Category.SHIP then +local pland=airbase:GetCoordinate() +wp[#wp+1]=pland:WaypointAirLanding(UTILS.KnotsToKmph(SpeedLand),airbase,{},"Landing") +end +if self.isAI then +local routeto=false +if fc or world.event.S_EVENT_KILL then +routeto=true +end +if routeto then +self:Route(wp,1) +else +local Template=self.group:GetTemplate() +Template.route.points=wp +self:Respawn(Template) +end +end +end +function FLIGHTGROUP:onbeforeWait(From,Event,To,Coord,Altitude,Speed) +local allowed=true +local Tsuspend=nil +local Ntot,Nsched,Nwp=self:CountRemainingTasks() +if self.taskcurrent>0 then +self:I(self.lid..string.format("WARNING: Got current task ==> WAIT event is suspended for 10 sec.")) +Tsuspend=-10 +allowed=false +end +if Nsched>0 then +self:I(self.lid..string.format("WARNING: Still got %d SCHEDULED tasks in the queue ==> WAIT event is suspended for 10 sec.",Nsched)) +Tsuspend=-10 +allowed=false +end +if Nwp>0 then +self:I(self.lid..string.format("WARNING: Still got %d WAYPOINT tasks in the queue ==> WAIT event is suspended for 10 sec.",Nwp)) +Tsuspend=-10 +allowed=false +end +if Tsuspend and not allowed then +self:__Wait(Tsuspend,Coord,Altitude,Speed) +end +return allowed +end +function FLIGHTGROUP:onafterWait(From,Event,To,Coord,Altitude,Speed) +Coord=Coord or self.group:GetCoordinate() +Altitude=Altitude or(self.ishelo and 1000 or 10000) +Speed=Speed or(self.ishelo and 80 or 250) +local text=string.format("Flight group set to wait/orbit at altitude %d m and speed %.1f km/h",Altitude,Speed) +self:T(self.lid..text) +local TaskOrbit=self.group:TaskOrbit(Coord,UTILS.FeetToMeters(Altitude),UTILS.KnotsToMps(Speed)) +self:SetTask(TaskOrbit) +end +function FLIGHTGROUP:onafterRefuel(From,Event,To,Coordinate) +local text=string.format("Flight group set to refuel at the nearest tanker") +self:I(self.lid..text) +self:PauseMission() +local TaskRefuel=self.group:TaskRefueling() +local TaskFunction=self.group:TaskFunction("FLIGHTGROUP._FinishedRefuelling",self) +local DCSTasks={TaskRefuel,TaskFunction} +local Speed=self.speedCruise +local coordinate=self.group:GetCoordinate() +Coordinate=Coordinate or coordinate:Translate(UTILS.NMToMeters(5),self.group:GetHeading(),true) +local wp0=coordinate:WaypointAir("BARO",COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.TurningPoint,Speed,true) +local wp9=Coordinate:WaypointAir("BARO",COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.TurningPoint,Speed,true,nil,DCSTasks,"Refuel") +self:Route({wp0,wp9},1) +end +function FLIGHTGROUP:onafterRefueled(From,Event,To) +local text=string.format("Flight group finished refuelling") +self:I(self.lid..text) +self:_CheckGroupDone(1) +end +function FLIGHTGROUP:onafterHolding(From,Event,To) +self.flaghold:Set(0) +self.Tholding=timer.getAbsTime() +local text=string.format("Flight group %s is HOLDING now",self.groupname) +self:T(self.lid..text) +if self.flightcontrol then +self.flightcontrol:SetFlightStatus(self,FLIGHTCONTROL.FlightStatus.HOLDING) +if not self.isAI then +self:_UpdateMenu() +end +elseif self.airboss then +if self.ishelo then +local carrierpos=self.airboss:GetCoordinate() +local carrierheading=self.airboss:GetHeading() +local Distance=UTILS.NMToMeters(5) +local Angle=carrierheading+90 +local altitude=math.random(12,25)*100 +local oc=carrierpos:Translate(Distance,Angle):SetAltitude(altitude,true) +local TaskOrbit=self.group:TaskOrbit(oc,nil,UTILS.KnotsToMps(50)) +local TaskLand=self.group:TaskCondition(nil,self.flaghold.UserFlagName,1) +local TaskHold=self.group:TaskControlled(TaskOrbit,TaskLand) +local TaskKlar=self.group:TaskFunction("FLIGHTGROUP._ClearedToLand",self) +local DCSTask=self.group:TaskCombo({TaskOrbit,TaskHold,TaskKlar}) +self:SetTask(DCSTask) +end +end +end +function FLIGHTGROUP:onafterEngageTarget(From,Event,To,Target) +local DCStask=nil +if Target:IsInstanceOf("UNIT")or Target:IsInstanceOf("STATIC")then +DCStask=self:GetGroup():TaskAttackUnit(Target,true) +elseif Target:IsInstanceOf("GROUP")then +DCStask=self:GetGroup():TaskAttackGroup(Target,nil,nil,nil,nil,nil,nil,true) +elseif Target:IsInstanceOf("SET_UNIT")then +local DCSTasks={} +for _,_unit in pairs(Target:GetSet())do +local unit=_unit +local task=self:GetGroup():TaskAttackUnit(unit,true) +table.insert(DCSTasks) +end +DCStask=self:GetGroup():TaskCombo(DCSTasks) +elseif Target:IsInstanceOf("SET_GROUP")then +local DCSTasks={} +for _,_unit in pairs(Target:GetSet())do +local unit=_unit +local task=self:GetGroup():TaskAttackGroup(Target,nil,nil,nil,nil,nil,nil,true) +table.insert(DCSTasks) +end +DCStask=self:GetGroup():TaskCombo(DCSTasks) +else +self:E("ERROR: unknown Target in EngageTarget! Needs to be a UNIT, STATIC, GROUP, SET_UNIT or SET_GROUP") +return +end +local Task=self:NewTaskScheduled(DCStask,1,"Engage_Target",0) +Task.backupROE=self:GetROE() +self:SwitchROE(ENUMS.ROE.OpenFire) +local mission=self:GetMissionCurrent() +if mission then +self:PauseMission() +end +self:TaskExecute(Task) +end +function FLIGHTGROUP:onafterDisengage(From,Event,To) +self:T(self.lid.."Disengage target") +end +function FLIGHTGROUP:onbeforeLandAt(From,Event,To,Coordinate,Duration) +return self.ishelo +end +function FLIGHTGROUP:onafterLandAt(From,Event,To,Coordinate,Duration) +Duration=Duration or 600 +Coordinate=Coordinate or self:GetCoordinate() +local DCStask=self.group:TaskLandAtVec2(Coordinate:GetVec2(),Duration) +local Task=self:NewTaskScheduled(DCStask,1,"Task_Land_At",0) +self:TaskExecute(Task) +end +function FLIGHTGROUP:onafterFuelLow(From,Event,To) +local text=string.format("Low fuel for flight group %s",self.groupname) +self:I(self.lid..text) +self.fuellow=true +local airbase=self.destbase or self.homebase +if self.airwing then +local tanker=self.airwing:GetTankerForFlight(self) +if tanker then +self:I(self.lid..string.format("Send to refuel at tanker %s",tanker.flightgroup:GetName())) +local coordinate=self:GetCoordinate():GetIntermediateCoordinate(tanker.flightgroup:GetCoordinate(),0.75) +self:Refuel(coordinate) +else +if airbase and self.fuellowrtb then +self:RTB(airbase) +end +end +else +if self.fuellowrefuel and self.refueltype then +local tanker=self:FindNearestTanker(50) +if tanker then +self:I(self.lid..string.format("Send to refuel at tanker %s",tanker:GetName())) +local coordinate=self:GetCoordinate():GetIntermediateCoordinate(tanker:GetCoordinate(),0.75) +self:Refuel(coordinate) +return +end +end +if airbase and self.fuellowrtb then +self:RTB(airbase) +end +end +end +function FLIGHTGROUP:onafterFuelCritical(From,Event,To) +local text=string.format("Critical fuel for flight group %s",self.groupname) +self:I(self.lid..text) +self.fuelcritical=true +local airbase=self.destbase or self.homebase +if airbase and self.fuelcriticalrtb and not self:IsGoing4Fuel()then +self:RTB(airbase) +end +end +function FLIGHTGROUP:onafterStop(From,Event,To) +if self:IsAlive()then +if self.flightcontrol then +for _,_element in pairs(self.elements)do +local element=_element +self:_SetElementParkingFree(element) +end +end +end +self:UnHandleEvent(EVENTS.Birth) +self:UnHandleEvent(EVENTS.EngineStartup) +self:UnHandleEvent(EVENTS.Takeoff) +self:UnHandleEvent(EVENTS.Land) +self:UnHandleEvent(EVENTS.EngineShutdown) +self:UnHandleEvent(EVENTS.PilotDead) +self:UnHandleEvent(EVENTS.Ejection) +self:UnHandleEvent(EVENTS.Crash) +self:UnHandleEvent(EVENTS.RemoveUnit) +self:GetParent(self).onafterStop(self,From,Event,To) +_DATABASE.FLIGHTGROUPS[self.groupname]=nil +end +function FLIGHTGROUP._ReachedHolding(group,flightgroup) +flightgroup:T2(flightgroup.lid..string.format("Group reached holding point")) +flightgroup:__Holding(-1) +end +function FLIGHTGROUP._ClearedToLand(group,flightgroup) +flightgroup:T2(flightgroup.lid..string.format("Group was cleared to land")) +flightgroup:__Landing(-1) +end +function FLIGHTGROUP._FinishedRefuelling(group,flightgroup) +flightgroup:T2(flightgroup.lid..string.format("Group finished refueling")) +flightgroup:__Refueled(-1) +end +function FLIGHTGROUP:_InitGroup() +if self.groupinitialized then +self:E(self.lid.."WARNING: Group was already initialized!") +return +end +local group=self.group +self.template=group:GetTemplate() +self.isAircraft=true +self.isNaval=false +self.isGround=false +self.ishelo=group:IsHelicopter() +self.isUncontrolled=self.template.uncontrolled +self.isLateActivated=self.template.lateActivation +self.speedMax=group:GetSpeedMax() +local speedCruiseLimit=self.ishelo and UTILS.KnotsToKmph(80)or UTILS.KnotsToKmph(350) +self.speedCruise=math.min(self.speedMax*0.7,speedCruiseLimit) +self.ammo=self:GetAmmoTot() +self.radio.Freq=tonumber(self.template.frequency) +self.radio.Modu=tonumber(self.template.modulation) +self.radio.On=self.template.communication +local callsign=self.template.units[1].callsign +if type(callsign)=="number"then +local cs=tostring(callsign) +callsign={} +callsign[1]=cs:sub(1,1) +callsign[2]=cs:sub(2,2) +callsign[3]=cs:sub(3,3) +end +self.callsign.NumberSquad=callsign[1] +self.callsign.NumberGroup=callsign[2] +self.callsign.NumberElement=callsign[3] +self.callsign.NameSquad=UTILS.GetCallsignName(self.callsign.NumberSquad) +if self.ishelo then +self.optionDefault.Formation=ENUMS.Formation.RotaryWing.EchelonLeft.D300 +else +self.optionDefault.Formation=ENUMS.Formation.FixedWing.EchelonLeft.Group +end +self:SetDefaultTACAN(nil,nil,nil,nil,true) +self.tacan=UTILS.DeepCopy(self.tacanDefault) +self.isAI=not self:_IsHuman(group) +if not self.isAI then +self.menu=self.menu or{} +self.menu.atc=self.menu.atc or{} +self.menu.atc.root=self.menu.atc.root or MENU_GROUP:New(self.group,"ATC") +end +for _,unit in pairs(self.group:GetUnits())do +local element=self:AddElementByName(unit:GetName()) +end +local unit=self.group:GetUnit(1) +if unit then +self.rangemax=unit:GetRange() +self.descriptors=unit:GetDesc() +self.actype=unit:GetTypeName() +self.ceiling=self.descriptors.Hmax +self.tankertype=select(2,unit:IsTanker()) +self.refueltype=select(2,unit:IsRefuelable()) +if self.verbose>=1 then +local text=string.format("Initialized Flight Group %s:\n",self.groupname) +text=text..string.format("Unit type = %s\n",self.actype) +text=text..string.format("Speed max = %.1f Knots\n",UTILS.KmphToKnots(self.speedMax)) +text=text..string.format("Range max = %.1f km\n",self.rangemax/1000) +text=text..string.format("Ceiling = %.1f feet\n",UTILS.MetersToFeet(self.ceiling)) +text=text..string.format("Tanker type = %s\n",tostring(self.tankertype)) +text=text..string.format("Refuel type = %s\n",tostring(self.refueltype)) +text=text..string.format("AI = %s\n",tostring(self.isAI)) +text=text..string.format("Helicopter = %s\n",tostring(self.group:IsHelicopter())) +text=text..string.format("Elements = %d\n",#self.elements) +text=text..string.format("Waypoints = %d\n",#self.waypoints) +text=text..string.format("Radio = %.1f MHz %s %s\n",self.radio.Freq,UTILS.GetModulationName(self.radio.Modu),tostring(self.radio.On)) +text=text..string.format("Ammo = %d (G=%d/R=%d/B=%d/M=%d)\n",self.ammo.Total,self.ammo.Guns,self.ammo.Rockets,self.ammo.Bombs,self.ammo.Missiles) +text=text..string.format("FSM state = %s\n",self:GetState()) +text=text..string.format("Is alive = %s\n",tostring(self.group:IsAlive())) +text=text..string.format("LateActivate = %s\n",tostring(self:IsLateActivated())) +text=text..string.format("Uncontrolled = %s\n",tostring(self:IsUncontrolled())) +text=text..string.format("Start Air = %s\n",tostring(self:IsTakeoffAir())) +text=text..string.format("Start Cold = %s\n",tostring(self:IsTakeoffCold())) +text=text..string.format("Start Hot = %s\n",tostring(self:IsTakeoffHot())) +text=text..string.format("Start Rwy = %s\n",tostring(self:IsTakeoffRunway())) +self:I(self.lid..text) +end +self.groupinitialized=true +end +return self +end +function FLIGHTGROUP:AddElementByName(unitname) +local unit=UNIT:FindByName(unitname) +if unit then +local element={} +element.name=unitname +element.unit=unit +element.status=OPSGROUP.ElementStatus.INUTERO +element.group=unit:GetGroup() +local unittemplate=element.unit:GetTemplate() +element.modex=unittemplate.onboard_num +element.skill=unittemplate.skill +element.payload=unittemplate.payload +element.pylons=unittemplate.payload and unittemplate.payload.pylons or nil +element.fuelmass0=unittemplate.payload and unittemplate.payload.fuel or 0 +element.fuelmass=element.fuelmass0 +element.fuelrel=element.unit:GetFuel() +element.category=element.unit:GetUnitCategory() +element.categoryname=element.unit:GetCategoryName() +element.callsign=element.unit:GetCallsign() +element.size=element.unit:GetObjectSize() +if element.skill=="Client"or element.skill=="Player"then +element.ai=false +element.client=CLIENT:FindByName(unitname) +else +element.ai=true +end +local text=string.format("Adding element %s: status=%s, skill=%s, modex=%s, fuelmass=%.1f (%d), category=%d, categoryname=%s, callsign=%s, ai=%s", +element.name,element.status,element.skill,element.modex,element.fuelmass,element.fuelrel*100,element.category,element.categoryname,element.callsign,tostring(element.ai)) +self:T(self.lid..text) +table.insert(self.elements,element) +if unit:IsAlive()then +self:ElementSpawned(element) +end +return element +end +return nil +end +function FLIGHTGROUP:GetHomebaseFromWaypoints() +local wp=self:GetWaypoint(1) +if wp then +if wp and wp.action and wp.action==COORDINATE.WaypointAction.FromParkingArea +or wp.action==COORDINATE.WaypointAction.FromParkingAreaHot +or wp.action==COORDINATE.WaypointAction.FromRunway then +local airbaseID=nil +if wp.airdromeId then +airbaseID=wp.airdromeId +else +airbaseID=-wp.helipadId +end +local airbase=AIRBASE:FindByID(airbaseID) +return airbase +end +end +return nil +end +function FLIGHTGROUP:FindNearestAirbase(Radius) +local coord=self:GetCoordinate() +local dmin=math.huge +local airbase=nil +for _,_airbase in pairs(AIRBASE.GetAllAirbases())do +local ab=_airbase +local coalitionAB=ab:GetCoalition() +if coalitionAB==self:GetCoalition()or coalitionAB==coalition.side.NEUTRAL then +if airbase then +local d=ab:GetCoordinate():Get2DDistance(coord) +if d %s Destination",#self.waypoints,self.homebase and self.homebase:GetName()or"unknown",self.destbase and self.destbase:GetName()or"uknown")) +if#self.waypoints>0 then +if#self.waypoints==1 then +self.passedfinalwp=true +end +end +return self +end +function FLIGHTGROUP:AddWaypoint(Coordinate,Speed,AfterWaypointWithID,Altitude,Updateroute) +local wpnumber=self:GetWaypointIndexAfterID(AfterWaypointWithID) +if wpnumber>self.currentwp then +self.passedfinalwp=false +end +Speed=Speed or 350 +local wp=Coordinate:WaypointAir(COORDINATE.WaypointAltType.BARO,COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.TurningPoint,UTILS.KnotsToKmph(Speed),true,nil,{}) +local waypoint=self:_CreateWaypoint(wp) +if Altitude then +waypoint.alt=UTILS.FeetToMeters(Altitude) +end +self:_AddWaypoint(waypoint,wpnumber) +self:T(self.lid..string.format("Adding AIR waypoint #%d, speed=%.1f knots. Last waypoint passed was #%s. Total waypoints #%d",wpnumber,Speed,self.currentwp,#self.waypoints)) +if Updateroute==nil or Updateroute==true then +self:__UpdateRoute(-1) +end +return waypoint +end +function FLIGHTGROUP:_IsElement(unitname) +for _,_element in pairs(self.elements)do +local element=_element +if element.name==unitname then +return true +end +end +return false +end +function FLIGHTGROUP:_SetElementParkingAt(Element,Spot) +Element.parking=Spot +if Spot then +self:T(self.lid..string.format("Element %s is parking on spot %d",Element.name,Spot.TerminalID)) +if self.flightcontrol then +self.flightcontrol:SetParkingOccupied(Element.parking,Element.name) +end +end +end +function FLIGHTGROUP:_SetElementParkingFree(Element) +if Element.parking then +if self.flightcontrol then +self.flightcontrol:SetParkingFree(Element.parking) +end +Element.parking=nil +end +end +function FLIGHTGROUP:_GetOnboardNumber(unitname) +local group=UNIT:FindByName(unitname):GetGroup() +local units=group:GetTemplate().units +local numbers={} +for _,unit in pairs(units)do +if unitname==unit.name then +return tostring(unit.onboard_num) +end +end +return nil +end +function FLIGHTGROUP:_IsHumanUnit(unit) +local playerunit=self:_GetPlayerUnitAndName(unit:GetName()) +if playerunit then +return true +else +return false +end +end +function FLIGHTGROUP:_IsHuman(group) +local units=group:GetUnits() +for _,_unit in pairs(units)do +local human=self:_IsHumanUnit(_unit) +if human then +return true +end +end +return false +end +function FLIGHTGROUP:_GetPlayerUnitAndName(_unitName) +self:F2(_unitName) +if _unitName~=nil then +local DCSunit=Unit.getByName(_unitName) +if DCSunit then +local playername=DCSunit:getPlayerName() +local unit=UNIT:Find(DCSunit) +if DCSunit and unit and playername then +return unit,playername +end +end +end +return nil,nil +end +function FLIGHTGROUP:GetParkingSpot(element,maxdist,airbase) +local coord=element.unit:GetCoordinate() +airbase=airbase or self:GetClosestAirbase() +local parking=airbase:GetParkingSpotsTable() +local spot=nil +local dist=nil +local distmin=math.huge +for _,_parking in pairs(parking)do +local parking=_parking +dist=coord:Get2DDistance(parking.Coordinate) +if distsafedist) +return safe +end +local function _clients() +local clients=_DATABASE.CLIENTS +local coords={} +for clientname,client in pairs(clients)do +local template=_DATABASE:GetGroupTemplateFromUnitName(clientname) +local units=template.units +for i,unit in pairs(units)do +local coord=COORDINATE:New(unit.x,unit.alt,unit.y) +coords[unit.name]=coord +end +end +return coords +end +local airbasecategory=airbase:GetAirbaseCategory() +local parkingdata=airbase:GetParkingSpotsTable() +local obstacles={} +for _,_parkingspot in pairs(parkingdata)do +local parkingspot=_parkingspot +local _,_,_,_units,_statics,_sceneries=parkingspot.Coordinate:ScanObjects(scanradius,scanunits,scanstatics,scanscenery) +for _,_unit in pairs(_units)do +local unit=_unit +local _coord=unit:GetCoordinate() +local _size=self:_GetObjectSize(unit:GetDCSObject()) +local _name=unit:GetName() +table.insert(obstacles,{coord=_coord,size=_size,name=_name,type="unit"}) +end +local clientcoords=_clients() +for clientname,_coord in pairs(clientcoords)do +table.insert(obstacles,{coord=_coord,size=15,name=clientname,type="client"}) +end +for _,static in pairs(_statics)do +local _vec3=static:getPoint() +local _coord=COORDINATE:NewFromVec3(_vec3) +local _name=static:getName() +local _size=self:_GetObjectSize(static) +table.insert(obstacles,{coord=_coord,size=_size,name=_name,type="static"}) +end +for _,scenery in pairs(_sceneries)do +local _vec3=scenery:getPoint() +local _coord=COORDINATE:NewFromVec3(_vec3) +local _name=scenery:getTypeName() +local _size=self:_GetObjectSize(scenery) +table.insert(obstacles,{coord=_coord,size=_size,name=_name,type="scenery"}) +end +end +local parking={} +local terminaltype=self:_GetTerminal(self.attribute,airbase:GetAirbaseCategory()) +for i,_element in pairs(self.elements)do +local element=_element +local gotit=false +for _,_parkingspot in pairs(parkingdata)do +local parkingspot=_parkingspot +if AIRBASE._CheckTerminalType(parkingspot.TerminalType,terminaltype)then +local free=true +local problem=nil +if verysafe and parkingspot.TOAC then +free=false +self:T2(self.lid..string.format("Parking spot %d is occupied by other aircraft taking off (TOAC).",parkingspot.TerminalID)) +end +for _,obstacle in pairs(obstacles)do +local dist=parkingspot.Coordinate:Get2DDistance(obstacle.coord) +local safe=_overlap(element.size,obstacle.size,dist) +if not safe then +free=false +problem=obstacle +problem.dist=dist +break +end +end +if self.flightcontrol and self.flightcontrol.airbasename==airbase:GetName()then +local problem=self.flightcontrol:IsParkingReserved(parkingspot)or self.flightcontrol:IsParkingOccupied(parkingspot) +if problem then +free=false +end +end +if free then +table.insert(parking,parkingspot) +self:T2(self.lid..string.format("Parking spot %d is free for element %s!",parkingspot.TerminalID,element.name)) +table.insert(obstacles,{coord=parkingspot.Coordinate,size=element.size,name=element.name,type="element"}) +gotit=true +break +else +self:T2(self.lid..string.format("Parking spot %d is occupied or not big enough!",parkingspot.TerminalID)) +end +end +end +if not gotit then +self:E(self.lid..string.format("WARNING: No free parking spot for element %s",element.name)) +return nil +end +end +return parking +end +function FLIGHTGROUP:_GetObjectSize(DCSobject) +local DCSdesc=DCSobject:getDesc() +if DCSdesc.box then +local x=DCSdesc.box.max.x+math.abs(DCSdesc.box.min.x) +local y=DCSdesc.box.max.y+math.abs(DCSdesc.box.min.y) +local z=DCSdesc.box.max.z+math.abs(DCSdesc.box.min.z) +return math.max(x,z),x,y,z +end +return 0,0,0,0 +end +function FLIGHTGROUP:_GetAttribute() +local attribute=FLIGHTGROUP.Attribute.OTHER +local group=self.group +if group then +local transportplane=group:HasAttribute("Transports")and group:HasAttribute("Planes") +local awacs=group:HasAttribute("AWACS") +local fighter=group:HasAttribute("Fighters")or group:HasAttribute("Interceptors")or group:HasAttribute("Multirole fighters")or(group:HasAttribute("Bombers")and not group:HasAttribute("Strategic bombers")) +local bomber=group:HasAttribute("Strategic bombers") +local tanker=group:HasAttribute("Tankers") +local uav=group:HasAttribute("UAVs") +local transporthelo=group:HasAttribute("Transport helicopters") +local attackhelicopter=group:HasAttribute("Attack helicopters") +if transportplane then +attribute=FLIGHTGROUP.Attribute.AIR_TRANSPORTPLANE +elseif awacs then +attribute=FLIGHTGROUP.Attribute.AIR_AWACS +elseif fighter then +attribute=FLIGHTGROUP.Attribute.AIR_FIGHTER +elseif bomber then +attribute=FLIGHTGROUP.Attribute.AIR_BOMBER +elseif tanker then +attribute=FLIGHTGROUP.Attribute.AIR_TANKER +elseif transporthelo then +attribute=FLIGHTGROUP.Attribute.AIR_TRANSPORTHELO +elseif attackhelicopter then +attribute=FLIGHTGROUP.Attribute.AIR_ATTACKHELO +elseif uav then +attribute=FLIGHTGROUP.Attribute.AIR_UAV +end +end +return attribute +end +function FLIGHTGROUP:_GetTerminal(_attribute,_category) +local _terminal=AIRBASE.TerminalType.OpenBig +if _attribute==FLIGHTGROUP.Attribute.AIR_FIGHTER then +_terminal=AIRBASE.TerminalType.FighterAircraft +elseif _attribute==FLIGHTGROUP.Attribute.AIR_BOMBER or _attribute==FLIGHTGROUP.Attribute.AIR_TRANSPORTPLANE or _attribute==FLIGHTGROUP.Attribute.AIR_TANKER or _attribute==FLIGHTGROUP.Attribute.AIR_AWACS then +_terminal=AIRBASE.TerminalType.OpenBig +elseif _attribute==FLIGHTGROUP.Attribute.AIR_TRANSPORTHELO or _attribute==FLIGHTGROUP.Attribute.AIR_ATTACKHELO then +_terminal=AIRBASE.TerminalType.HelicopterUsable +else +end +if _category==Airbase.Category.SHIP then +if not(_attribute==FLIGHTGROUP.Attribute.AIR_TRANSPORTHELO or _attribute==FLIGHTGROUP.Attribute.AIR_ATTACKHELO)then +_terminal=AIRBASE.TerminalType.OpenMedOrBig +end +end +return _terminal +end +function FLIGHTGROUP:_UpdateMenu(delay) +if delay and delay>0 then +self:I(self.lid..string.format("FF updating menu in %.1f sec",delay)) +self:ScheduleOnce(delay,FLIGHTGROUP._UpdateMenu,self) +else +self:I(self.lid.."FF updating menu NOW") +local position=self.group:GetCoordinate() +local fc={} +for airbasename,_flightcontrol in pairs(_DATABASE.FLIGHTCONTROLS)do +local airbase=AIRBASE:FindByName(airbasename) +local coord=airbase:GetCoordinate() +local dist=coord:Get2DDistance(position) +local fcitem={airbasename=airbasename,dist=dist} +table.insert(fc,fcitem) +end +local function _sort(a,b) +return a.distTstop then +self:E(string.format("ERROR:Into wind stop time %s lies before start time %s. Input rejected!",UTILS.SecondsToClock(Tstart),UTILS.SecondsToClock(Tstop))) +return self +end +if Tstop<=Tnow then +self:E(string.format("WARNING: Into wind stop time %s already over. Tnow=%s! Input rejected.",UTILS.SecondsToClock(Tstop),UTILS.SecondsToClock(Tnow))) +return self +end +self.intowindcounter=self.intowindcounter+1 +local recovery={} +recovery.Tstart=Tstart +recovery.Tstop=Tstop +recovery.Open=false +recovery.Over=false +recovery.Speed=speed or 20 +recovery.Uturn=uturn and uturn or false +recovery.Offset=offset or 0 +recovery.Id=self.intowindcounter +return recovery +end +function NAVYGROUP:AddTurnIntoWind(starttime,stoptime,speed,uturn,offset) +local recovery=self:_CreateTurnIntoWind(starttime,stoptime,speed,uturn,offset) +table.insert(self.Qintowind,recovery) +return recovery +end +function NAVYGROUP:RemoveTurnIntoWind(IntoWindData) +if self.intowind and self.intowind.Id==IntoWindData.Id then +self:TurnIntoWindStop() +return +end +for i,_tiw in pairs(self.Qintowind)do +local tiw=_tiw +if tiw.Id==IntoWindData.Id then +table.remove(self.Qintowind,i) +break +end +end +return self +end +function NAVYGROUP:IsHolding() +return self:Is("Holding") +end +function NAVYGROUP:IsCruising() +return self:Is("Cruising") +end +function NAVYGROUP:IsOnDetour() +return self:Is("OnDetour") +end +function NAVYGROUP:IsDiving() +return self:Is("Diving") +end +function NAVYGROUP:IsTurning() +return self.turning +end +function NAVYGROUP:IsSteamingIntoWind() +if self.intowind then +return true +else +return false +end +end +function NAVYGROUP:onbeforeStatus(From,Event,To) +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 +function NAVYGROUP:onafterStatus(From,Event,To) +local fsmstate=self:GetState() +if self:IsAlive()then +if self.detectionOn then +self:_CheckDetectedUnits() +end +self:_UpdatePosition() +self:_CheckTurning() +local freepath=UTILS.NMToMeters(10) +if not self:IsTurning()then +freepath=self:_CheckFreePath(freepath,100) +if freepath<5000 then +if not self.collisionwarning then +self:CollisionWarning(freepath) +end +if self.pathfindingOn and not self.ispathfinding then +self.ispathfinding=self:_FindPathToNextWaypoint() +end +end +end +self:_CheckTurnsIntoWind() +self:_CheckStuck() +if self.verbose>=1 then +local nTaskTot,nTaskSched,nTaskWP=self:CountRemainingTasks() +local nMissions=self:CountRemainingMissison() +local intowind=self:IsSteamingIntoWind()and UTILS.SecondsToClock(self.intowind.Tstop-timer.getAbsTime(),true)or"N/A" +local turning=tostring(self:IsTurning()) +local alt=self.position.y +local speed=UTILS.MpsToKnots(self.velocity) +local speedExpected=UTILS.MpsToKnots(self:GetExpectedSpeed()) +local wpidxCurr=self.currentwp +local wpuidCurr=self:GetWaypointUIDFromIndex(wpidxCurr)or 0 +local wpidxNext=self:GetWaypointIndexNext()or 0 +local wpuidNext=self:GetWaypointUIDFromIndex(wpidxNext)or 0 +local wpDist=UTILS.MetersToNM(self:GetDistanceToWaypoint()or 0) +local wpETA=UTILS.SecondsToClock(self:GetTimeToWaypoint()or 0,true) +local roe=self:GetROE()or 0 +local als=self:GetAlarmstate()or 0 +local text=string.format("%s [ROE=%d,AS=%d, T/M=%d/%d]: Wp=%d[%d]-->%d[%d] (of %d) Dist=%.1f NM ETA=%s - Speed=%.1f (%.1f) kts, Depth=%.1f m, Hdg=%03d, Turn=%s Collision=%d IntoWind=%s", +fsmstate,roe,als,nTaskTot,nMissions,wpidxCurr,wpuidCurr,wpidxNext,wpuidNext,#self.waypoints or 0,wpDist,wpETA,speed,speedExpected,alt,self.heading,turning,freepath,intowind) +self:I(self.lid..text) +if false then +local text="Waypoints:" +for i,wp in pairs(self.waypoints)do +local waypoint=wp +text=text..string.format("\n%d. UID=%d",i,waypoint.uid) +if i==self.currentwp then +text=text.." current!" +end +end +env.info(text) +end +end +else +local text=string.format("State %s: Alive=%s",fsmstate,tostring(self:IsAlive())) +self:T(self.lid..text) +end +if self.verbose>=2 then +local text=string.format(self.lid.."Turn into wind time windows:") +if#self.Qintowind==0 then +text=text.." none!" +end +for i,_recovery in pairs(self.Qintowind)do +local recovery=_recovery +local Cstart=UTILS.SecondsToClock(recovery.Tstart) +local Cstop=UTILS.SecondsToClock(recovery.Tstop) +text=text..string.format("\n[%d] ID=%d Start=%s Stop=%s Open=%s Over=%s",i,recovery.Id,Cstart,Cstop,tostring(recovery.Open),tostring(recovery.Over)) +end +self:I(self.lid..text) +end +self:_PrintTaskAndMissionStatus() +self:__Status(-30) +end +function NAVYGROUP:onafterElementSpawned(From,Event,To,Element) +self:T(self.lid..string.format("Element spawned %s",Element.name)) +self:_UpdateStatus(Element,OPSGROUP.ElementStatus.SPAWNED) +end +function NAVYGROUP:onafterSpawned(From,Event,To) +self:T(self.lid..string.format("Group spawned!")) +self:_UpdatePosition() +if self.isAI then +self:SwitchROE(self.option.ROE) +self:SwitchAlarmstate(self.option.Alarm) +self:_SwitchTACAN() +self:_SwitchICLS() +if self.radioDefault then +self:SwitchRadio() +else +self:SetDefaultRadio(self.radio.Freq,self.radio.Modu,false) +end +end +if#self.waypoints>1 then +self:Cruise() +else +self:FullStop() +end +end +function NAVYGROUP:onafterUpdateRoute(From,Event,To,n,Speed,Depth) +n=n or self:GetWaypointIndexNext() +self:_UpdateWaypointTasks(n) +local waypoints={} +local wp=UTILS.DeepCopy(self.waypoints[n]) +if Speed then +wp.speed=UTILS.KnotsToMps(Speed) +else +if self.adinfinitum and wp.speed<0.1 then +wp.speed=UTILS.KmphToMps(self.speedCruise) +end +end +if Depth then +wp.alt=-Depth +elseif self.depth then +wp.alt=-self.depth +else +end +self.speedWp=wp.speed +table.insert(waypoints,wp) +local current=self:GetCoordinate():WaypointNaval(UTILS.MpsToKmph(self.speedWp),wp.alt) +table.insert(waypoints,1,current) +if not self.passedfinalwp then +self:T(self.lid..string.format("Updateing route: WP %d-->%d (%d/%d), Speed=%.1f knots, Depth=%d m",self.currentwp,n,#waypoints,#self.waypoints,UTILS.MpsToKnots(self.speedWp),wp.alt)) +self:Route(waypoints) +else +self:E(self.lid..string.format("WARNING: Passed final WP ==> Full Stop!")) +self:FullStop() +end +end +function NAVYGROUP:onafterDetour(From,Event,To,Coordinate,Speed,Depth,ResumeRoute) +Depth=Depth or 0 +Speed=Speed or self:GetSpeedCruise() +local uid=self:GetWaypointCurrent().uid +local wp=self:AddWaypoint(Coordinate,Speed,uid,Depth,true) +if ResumeRoute then +wp.detour=1 +else +wp.detour=0 +end +end +function NAVYGROUP:onafterDetourReached(From,Event,To) +self:T(self.lid.."Group reached detour coordinate.") +end +function NAVYGROUP:onafterTurnIntoWind(From,Event,To,IntoWind) +IntoWind.Heading=self:GetHeadingIntoWind(IntoWind.Offset) +IntoWind.Open=true +IntoWind.Coordinate=self:GetCoordinate() +self.intowind=IntoWind +local _,vwind=self:GetWind() +vwind=UTILS.MpsToKnots(vwind) +local speed=math.max(IntoWind.Speed-vwind,2) +self:T(self.lid..string.format("Steaming into wind: Heading=%03d Speed=%.1f Vwind=%.1f Vtot=%.1f knots, Tstart=%d Tstop=%d",IntoWind.Heading,speed,vwind,speed+vwind,IntoWind.Tstart,IntoWind.Tstop)) +local distance=UTILS.NMToMeters(1000) +local coord=self:GetCoordinate() +local Coord=coord:Translate(distance,IntoWind.Heading) +local uid=self:GetWaypointCurrent().uid +local wptiw=self:AddWaypoint(Coord,speed,uid) +wptiw.intowind=true +IntoWind.waypoint=wptiw +if IntoWind.Uturn and self.Debug then +IntoWind.Coordinate:MarkToAll("Return coord") +end +end +function NAVYGROUP:onbeforeTurnIntoWindStop(From,Event,To) +if self.intowind then +return true +else +return false +end +end +function NAVYGROUP:onafterTurnIntoWindStop(From,Event,To) +self:TurnIntoWindOver(self.intowind) +end +function NAVYGROUP:onafterTurnIntoWindOver(From,Event,To,IntoWindData) +if IntoWindData and self.intowind and IntoWindData.Id==self.intowind.Id then +self:T2(self.lid.."Turn Into Wind Over!") +self.intowind.Over=true +self.intowind.Open=false +self:RemoveWaypointByID(self.intowind.waypoint.uid) +if self.intowind.Uturn then +self:T(self.lid.."FF Turn Into Wind Over ==> Uturn!") +self:Detour(self.intowind.Coordinate,self:GetSpeedCruise(),0,true) +else +local indx=self:GetWaypointIndexNext() +local speed=self:GetWaypointSpeed(indx) +self:T(self.lid..string.format("FF Turn Into Wind Over ==> Next WP Index=%d at %.1f knots via update route!",indx,speed)) +self:__UpdateRoute(-1,indx,speed) +end +self.intowind=nil +self:RemoveTurnIntoWind(IntoWindData) +end +end +function NAVYGROUP:onafterFullStop(From,Event,To) +self:T(self.lid.."Full stop ==> holding") +local pos=self:GetCoordinate() +local wp=pos:WaypointNaval(0) +self:Route({wp}) +end +function NAVYGROUP:onafterCruise(From,Event,To,Speed) +self.depth=nil +self:__UpdateRoute(-1,nil,Speed) +end +function NAVYGROUP:onafterDive(From,Event,To,Depth,Speed) +Depth=Depth or 50 +self:T(self.lid..string.format("Diving to %d meters",Depth)) +self.depth=Depth +self:__UpdateRoute(-1,nil,Speed) +end +function NAVYGROUP:onafterSurface(From,Event,To,Speed) +self.depth=0 +self:__UpdateRoute(-1,nil,Speed) +end +function NAVYGROUP:onafterTurningStarted(From,Event,To) +self.turning=true +end +function NAVYGROUP:onafterTurningStopped(From,Event,To) +self.turning=false +self.collisionwarning=false +if self:IsSteamingIntoWind()then +self:TurnedIntoWind() +end +end +function NAVYGROUP:onafterCollisionWarning(From,Event,To,Distance) +self:T(self.lid..string.format("Iceberg ahead in %d meters!",Distance or-1)) +self.collisionwarning=true +end +function NAVYGROUP:onafterStop(From,Event,To) +self:UnHandleEvent(EVENTS.Birth) +self:UnHandleEvent(EVENTS.Dead) +self:UnHandleEvent(EVENTS.RemoveUnit) +self:GetParent(self).onafterStop(self,From,Event,To) +end +function NAVYGROUP:OnEventBirth(EventData) +if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then +local unit=EventData.IniUnit +local group=EventData.IniGroup +local unitname=EventData.IniUnitName +if self.respawning then +local function reset() +self.respawning=nil +end +self:ScheduleOnce(1,reset) +else +local element=self:GetElementByName(unitname) +self:T3(self.lid..string.format("EVENT: Element %s born ==> spawned",element.name)) +self:ElementSpawned(element) +end +end +end +function NAVYGROUP:OnEventDead(EventData) +if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then +self:T(self.lid..string.format("EVENT: Unit %s dead!",EventData.IniUnitName)) +local unit=EventData.IniUnit +local group=EventData.IniGroup +local unitname=EventData.IniUnitName +local element=self:GetElementByName(unitname) +if element then +self:T(self.lid..string.format("EVENT: Element %s dead ==> destroyed",element.name)) +self:ElementDestroyed(element) +end +end +end +function NAVYGROUP:OnEventRemoveUnit(EventData) +if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then +local unit=EventData.IniUnit +local group=EventData.IniGroup +local unitname=EventData.IniUnitName +local element=self:GetElementByName(unitname) +if element then +self:T(self.lid..string.format("EVENT: Element %s removed ==> dead",element.name)) +self:ElementDead(element) +end +end +end +function NAVYGROUP:AddWaypoint(Coordinate,Speed,AfterWaypointWithID,Depth,Updateroute) +if not Coordinate:IsInstanceOf("COORDINATE")then +if Coordinate:IsInstanceOf("POSITIONABLE")or Coordinate:IsInstanceOf("ZONE_BASE")then +self:T(self.lid.."WARNING: Coordinate is not a COORDINATE but a POSITIONABLE or ZONE. Trying to get coordinate") +Coordinate=Coordinate:GetCoordinate() +else +self:E(self.lid.."ERROR: Coordinate is neither a COORDINATE nor any POSITIONABLE or ZONE!") +return nil +end +end +local wpnumber=self:GetWaypointIndexAfterID(AfterWaypointWithID) +if wpnumber>self.currentwp then +self.passedfinalwp=false +end +Speed=Speed or self:GetSpeedCruise() +local wp=Coordinate:WaypointNaval(UTILS.KnotsToKmph(Speed),Depth) +local waypoint=self:_CreateWaypoint(wp) +self:_AddWaypoint(waypoint,wpnumber) +self:T(self.lid..string.format("Adding NAVAL waypoint index=%d uid=%d, speed=%.1f knots. Last waypoint passed was #%d. Total waypoints #%d",wpnumber,waypoint.uid,Speed,self.currentwp,#self.waypoints)) +if Updateroute==nil or Updateroute==true then +self:_CheckGroupDone(1) +end +return waypoint +end +function NAVYGROUP:_InitGroup() +if self.groupinitialized then +self:E(self.lid.."WARNING: Group was already initialized!") +return +end +self.template=self.group:GetTemplate() +self.isAircraft=false +self.isNaval=true +self.isGround=false +self.isAI=true +self.isLateActivated=self.template.lateActivation +self.isUncontrolled=false +self.speedMax=self.group:GetSpeedMax() +self.speedCruise=self.speedMax*0.7 +self.ammo=self:GetAmmoTot() +self.radio.On=true +self.radio.Freq=tonumber(self.template.units[1].frequency)/1000000 +self.radio.Modu=tonumber(self.template.units[1].modulation) +self.optionDefault.Formation="Off Road" +self.option.Formation=self.optionDefault.Formation +self:SetDefaultTACAN(nil,nil,nil,nil,true) +self.tacan=UTILS.DeepCopy(self.tacanDefault) +self:SetDefaultICLS(nil,nil,nil,true) +self.icls=UTILS.DeepCopy(self.iclsDefault) +local units=self.group:GetUnits() +for _,_unit in pairs(units)do +local unit=_unit +local unittemplate=unit:GetTemplate() +local element={} +element.name=unit:GetName() +element.unit=unit +element.status=OPSGROUP.ElementStatus.INUTERO +element.typename=unit:GetTypeName() +element.skill=unittemplate.skill or"Unknown" +element.ai=true +element.category=element.unit:GetUnitCategory() +element.categoryname=element.unit:GetCategoryName() +element.size,element.length,element.height,element.width=unit:GetObjectSize() +element.ammo0=self:GetAmmoUnit(unit,false) +if self.verbose>=2 then +local text=string.format("Adding element %s: status=%s, skill=%s, category=%s (%d), size: %.1f (L=%.1f H=%.1f W=%.1f)", +element.name,element.status,element.skill,element.categoryname,element.category,element.size,element.length,element.height,element.width) +self:I(self.lid..text) +end +table.insert(self.elements,element) +self.descriptors=self.descriptors or unit:GetDesc() +self.actype=self.actype or unit:GetTypeName() +if unit:IsAlive()then +self:ElementSpawned(element) +end +end +if self.verbose>=1 then +local text=string.format("Initialized Navy Group %s:\n",self.groupname) +text=text..string.format("Unit type = %s\n",self.actype) +text=text..string.format("Speed max = %.1f Knots\n",UTILS.KmphToKnots(self.speedMax)) +text=text..string.format("Speed cruise = %.1f Knots\n",UTILS.KmphToKnots(self.speedCruise)) +text=text..string.format("Elements = %d\n",#self.elements) +text=text..string.format("Waypoints = %d\n",#self.waypoints) +text=text..string.format("Radio = %.1f MHz %s %s\n",self.radio.Freq,UTILS.GetModulationName(self.radio.Modu),tostring(self.radio.On)) +text=text..string.format("Ammo = %d (G=%d/R=%d/M=%d/T=%d)\n",self.ammo.Total,self.ammo.Guns,self.ammo.Rockets,self.ammo.Missiles,self.ammo.Torpedos) +text=text..string.format("FSM state = %s\n",self:GetState()) +text=text..string.format("Is alive = %s\n",tostring(self:IsAlive())) +text=text..string.format("LateActivate = %s\n",tostring(self:IsLateActivated())) +self:I(self.lid..text) +end +self.groupinitialized=true +return self +end +function NAVYGROUP:_CheckFreePath(DistanceMax,dx) +local distance=DistanceMax or 5000 +local dx=dx or 100 +if self:IsTurning()then +return distance +end +local offsetY=0.1 +if UTILS.GetDCSMap()==DCSMAP.Caucasus then +offsetY=5.01 +end +local vec3=self:GetVec3() +vec3.y=offsetY +local heading=self:GetHeading() +local function LoS(dist) +local checkvec3=UTILS.VecTranslate(vec3,dist,heading) +local los=land.isVisible(vec3,checkvec3) +return los +end +if LoS(DistanceMax)then +return DistanceMax +end +local function check() +local xmin=0 +local xmax=DistanceMax +local Nmax=100 +local eps=100 +local N=1 +while N<=Nmax do +local d=xmax-xmin +local x=xmin+d/2 +local los=LoS(x) +self:T2(self.lid..string.format("N=%d: xmin=%.1f xmax=%.1f x=%.1f d=%.3f los=%s",N,xmin,xmax,x,d,tostring(los))) +if los and d<=eps then +return x +end +if los then +xmin=x +else +xmax=x +end +N=N+1 +end +return 0 +end +return check() +end +function NAVYGROUP:_CheckTurning() +local unit=self.group:GetUnit(1) +if unit and unit:IsAlive()then +local vNew=self.orientX +local vLast=self.orientXLast +vNew.y=0;vLast.y=0 +local deltaLast=math.deg(math.acos(UTILS.VecDot(vNew,vLast)/UTILS.VecNorm(vNew)/UTILS.VecNorm(vLast))) +local turning=math.abs(deltaLast)>=2 +if self.turning and not turning then +self:TurningStopped() +elseif turning and not self.turning then +self:TurningStarted() +end +self.turning=turning +end +end +function NAVYGROUP:_CheckTurnsIntoWind() +local time=timer.getAbsTime() +if self.intowind then +if time>=self.intowind.Tstop then +self:TurnIntoWindOver(self.intowind) +end +else +local IntoWind=self:GetTurnIntoWindNext() +if IntoWind then +self:TurnIntoWind(IntoWind) +end +end +end +function NAVYGROUP:GetTurnIntoWindNext() +if#self.Qintowind>0 then +local time=timer.getAbsTime() +table.sort(self.Qintowind,function(a,b)return a.Tstart=recovery.Tstart and time0 +else +return false +end +end +return findpath() +end +ARMYGROUP={ +ClassName="ARMYGROUP", +formationPerma=nil, +engage={}, +} +ARMYGROUP.version="0.4.0" +function ARMYGROUP:New(Group) +local self=BASE:Inherit(self,OPSGROUP:New(Group)) +self.lid=string.format("ARMYGROUP %s | ",self.groupname) +self.isArmygroup=true +self:SetDefaultROE() +self:SetDefaultAlarmstate() +self:SetDetection() +self:SetPatrolAdInfinitum(false) +self:SetRetreatZones() +self:AddTransition("*","FullStop","Holding") +self:AddTransition("*","Cruise","Cruising") +self:AddTransition("*","Detour","OnDetour") +self:AddTransition("OnDetour","DetourReached","Cruising") +self:AddTransition("*","Retreat","Retreating") +self:AddTransition("Retreating","Retreated","Retreated") +self:AddTransition("Cruising","EngageTarget","Engaging") +self:AddTransition("Holding","EngageTarget","Engaging") +self:AddTransition("OnDetour","EngageTarget","Engaging") +self:AddTransition("Engaging","Disengage","Cruising") +self:AddTransition("*","Rearm","Rearm") +self:AddTransition("Rearm","Rearming","Rearming") +self:AddTransition("Rearming","Rearmed","Cruising") +self:InitWaypoints() +self:_InitGroup() +self:HandleEvent(EVENTS.Birth,self.OnEventBirth) +self:HandleEvent(EVENTS.Dead,self.OnEventDead) +self:HandleEvent(EVENTS.RemoveUnit,self.OnEventRemoveUnit) +self:__Status(-1) +self.timerQueueUpdate=TIMER:New(self._QueueUpdate,self):Start(2,5) +self.timerCheckZone=TIMER:New(self._CheckInZones,self):Start(2,30) +return self +end +function ARMYGROUP:SetPatrolAdInfinitum(switch) +if switch==false then +self.adinfinitum=false +else +self.adinfinitum=true +end +return self +end +function ARMYGROUP:GetClosestRoad() +return self:GetCoordinate():GetClosestPointToRoad() +end +function ARMYGROUP:GetClosestRoadDist() +local road=self:GetClosestRoad() +if road then +local dist=road:Get2DDistance(self:GetCoordinate()) +return dist +end +return math.huge +end +function ARMYGROUP:AddTaskFireAtPoint(Coordinate,Clock,Radius,Nshots,WeaponType,Prio) +Coordinate=self:_CoordinateFromObject(Coordinate) +local DCStask=CONTROLLABLE.TaskFireAtPoint(nil,Coordinate:GetVec2(),Radius,Nshots,WeaponType) +local task=self:AddTask(DCStask,Clock,nil,Prio) +return task +end +function ARMYGROUP:AddTaskWaypointFireAtPoint(Coordinate,Waypoint,Radius,Nshots,WeaponType,Prio) +Coordinate=self:_CoordinateFromObject(Coordinate) +Waypoint=Waypoint or self:GetWaypointNext() +local DCStask=CONTROLLABLE.TaskFireAtPoint(nil,Coordinate:GetVec2(),Radius,Nshots,WeaponType) +local task=self:AddTaskWaypoint(DCStask,Waypoint,nil,Prio) +return task +end +function ARMYGROUP:AddTaskAttackGroup(TargetGroup,WeaponExpend,WeaponType,Clock,Prio) +local DCStask=CONTROLLABLE.TaskAttackGroup(nil,TargetGroup,WeaponType,WeaponExpend,AttackQty,Direction,Altitude,AttackQtyLimit,GroupAttack) +local task=self:AddTask(DCStask,Clock,nil,Prio) +return task +end +function ARMYGROUP:SetRetreatZones(RetreatZoneSet) +self.retreatZones=RetreatZoneSet or SET_ZONE:New() +return self +end +function ARMYGROUP:AddRetreatZone(RetreatZone) +self.retreatZones:AddZone(RetreatZone) +return self +end +function ARMYGROUP:IsHolding() +return self:Is("Holding") +end +function ARMYGROUP:IsCruising() +return self:Is("Cruising") +end +function ARMYGROUP:IsOnDetour() +return self:Is("OnDetour") +end +function ARMYGROUP:IsCombatReady() +local combatready=true +if self:IsRearming()or self:IsRetreating()or self.outofAmmo or self:IsEngaging()or self:is("Retreated")or self:IsDead()or self:IsStopped()or self:IsInUtero()then +combatready=false +end +return combatready +end +function ARMYGROUP:onbeforeStatus(From,Event,To) +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 +function ARMYGROUP:onafterStatus(From,Event,To) +local fsmstate=self:GetState() +if self:IsAlive()then +if self.detectionOn then +self:_CheckDetectedUnits() +end +self:_CheckAmmoStatus() +self:_UpdatePosition() +self:_CheckStuck() +self:_CheckDamage() +if self:IsEngaging()then +self:_UpdateEngageTarget() +end +if self.verbose>=1 then +local nTaskTot,nTaskSched,nTaskWP=self:CountRemainingTasks() +local nMissions=self:CountRemainingMissison() +local roe=self:GetROE() +local alarm=self:GetAlarmstate() +local speed=UTILS.MpsToKnots(self.velocity) +local speedEx=UTILS.MpsToKnots(self:GetExpectedSpeed()) +local formation=self.option.Formation or"unknown" +local ammo=self:GetAmmoTot() +local text=string.format("%s [ROE-AS=%d-%d T/M=%d/%d]: Wp=%d/%d-->%d (final %s), Life=%.1f, Speed=%.1f (%d), Heading=%03d, Ammo=%d", +fsmstate,roe,alarm,nTaskTot,nMissions,self.currentwp,#self.waypoints,self:GetWaypointIndexNext(),tostring(self.passedfinalwp),self.life or 0,speed,speedEx,self.heading,ammo.Total) +self:I(self.lid..text) +end +else +local text=string.format("State %s: Alive=%s",fsmstate,tostring(self:IsAlive())) +self:T2(self.lid..text) +end +self:_PrintTaskAndMissionStatus() +self:__Status(-30) +end +function ARMYGROUP:onafterElementSpawned(From,Event,To,Element) +self:T(self.lid..string.format("Element spawned %s",Element.name)) +self:_UpdateStatus(Element,OPSGROUP.ElementStatus.SPAWNED) +end +function ARMYGROUP:onafterSpawned(From,Event,To) +self:T(self.lid..string.format("Group spawned!")) +self:_UpdatePosition() +if self.isAI then +self:SwitchROE(self.option.ROE) +self:SwitchAlarmstate(self.option.Alarm) +self:_SwitchTACAN() +if self.radioDefault then +self:SwitchRadio(self.radioDefault.Freq,self.radioDefault.Modu) +else +self:SetDefaultRadio(self.radio.Freq,self.radio.Modu,true) +end +if not self.option.Formation then +self.option.Formation=self.optionDefault.Formation +end +end +if#self.waypoints>1 then +self:Cruise(nil,self.option.Formation or self.optionDefault.Formation) +else +self:FullStop() +end +end +function ARMYGROUP:onafterUpdateRoute(From,Event,To,n,Speed,Formation) +local text=string.format("Update route n=%s, Speed=%s, Formation=%s",tostring(n),tostring(Speed),tostring(Formation)) +self:T(self.lid..text) +n=n or self:GetWaypointIndexNext(self.adinfinitum) +self:_UpdateWaypointTasks(n) +local waypoints={} +local wp=UTILS.DeepCopy(self.waypoints[n]) +local onroad=wp.action==ENUMS.Formation.Vehicle.OnRoad +if Speed then +wp.speed=UTILS.KnotsToMps(Speed) +else +if self.adinfinitum and wp.speed<0.1 then +wp.speed=UTILS.KmphToMps(self.speedCruise) +end +end +if self.formationPerma then +wp.action=self.formationPerma +elseif Formation then +wp.action=Formation +end +self.option.Formation=wp.action +self.speedWp=wp.speed +if onroad then +wp.action=ENUMS.Formation.Vehicle.OffRoad +local wproad=wp.roadcoord:WaypointGround(wp.speed,ENUMS.Formation.Vehicle.OnRoad) +table.insert(waypoints,wproad) +end +table.insert(waypoints,wp) +local formation=ENUMS.Formation.Vehicle.OffRoad +if wp.action~=ENUMS.Formation.Vehicle.OnRoad then +formation=wp.action +end +local current=self:GetCoordinate():WaypointGround(UTILS.MpsToKmph(self.speedWp),formation) +table.insert(waypoints,1,current) +if onroad then +local current=self:GetClosestRoad():WaypointGround(UTILS.MpsToKmph(self.speedWp),ENUMS.Formation.Vehicle.OnRoad) +table.insert(waypoints,2,current) +end +if false then +for i,_wp in pairs(waypoints)do +local wp=_wp +local text=string.format("WP #%d UID=%d type=%s: Speed=%d m/s, alt=%d m, Action=%s",i,wp.uid and wp.uid or 0,wp.type,wp.speed,wp.alt,wp.action) +self:T(text) +end +end +if self:IsEngaging()or not self.passedfinalwp then +self:T(self.lid..string.format("Updateing route: WP %d-->%d (%d/%d), Speed=%.1f knots, Formation=%s", +self.currentwp,n,#waypoints,#self.waypoints,UTILS.MpsToKnots(self.speedWp),tostring(self.option.Formation))) +self:Route(waypoints) +else +self:E(self.lid..string.format("WARNING: Passed final WP ==> Full Stop!")) +self:FullStop() +end +end +function ARMYGROUP:onafterGotoWaypoint(From,Event,To,UID,Speed,Formation) +local n=self:GetWaypointIndex(UID) +if n then +if false then +local tasks=self:GetTasksWaypoint(n) +for _,_task in pairs(tasks)do +local task=_task +task.status=OPSGROUP.TaskStatus.SCHEDULED +end +end +Speed=Speed or self:GetSpeedToWaypoint(n) +self:UpdateRoute(n,Speed,Formation) +end +end +function ARMYGROUP:onafterDetour(From,Event,To,Coordinate,Speed,Formation,ResumeRoute) +for _,_wp in pairs(self.waypoints)do +local wp=_wp +if wp.detour then +self:RemoveWaypointByID(wp.uid) +end +end +Speed=Speed or self:GetSpeedCruise() +local uid=self:GetWaypointCurrent().uid +local wp=self:AddWaypoint(Coordinate,Speed,uid,Formation,true) +if ResumeRoute then +wp.detour=1 +else +wp.detour=0 +end +end +function ARMYGROUP:onafterRearm(From,Event,To,Coordinate,Formation) +local uid=self:GetWaypointCurrent().uid +local wp=self:AddWaypoint(Coordinate,nil,uid,Formation,true) +wp.detour=0 +end +function ARMYGROUP:onafterRearming(From,Event,To) +local pos=self:GetCoordinate() +local wp=pos:WaypointGround(0) +self:Route({wp}) +end +function ARMYGROUP:onbeforeRetreat(From,Event,To,Zone,Formation) +if not Zone then +local a=self:GetVec2() +local distmin=math.huge +local zonemin=nil +for _,_zone in pairs(self.retreatZones:GetSet())do +local zone=_zone +local b=zone:GetVec2() +local dist=UTILS.VecDist2D(a,b) +if dist100 then +self.engage.Coordinate:UpdateFromVec3(vec3) +local uid=self:GetWaypointCurrent().uid +self:RemoveWaypointByID(self.engage.Waypoint.uid) +self.engage.Waypoint=self:AddWaypoint(self.engage.Coordinate,nil,uid,Formation,true) +self.engage.Waypoint.detour=0 +end +else +self:Disengage() +end +end +function ARMYGROUP:onafterDisengage(From,Event,To) +self:_CheckGroupDone(1) +end +function ARMYGROUP:onafterRearmed(From,Event,To) +self:_CheckGroupDone(1) +end +function ARMYGROUP:onafterDetourReached(From,Event,To) +self:I(self.lid.."Group reached detour coordinate.") +end +function ARMYGROUP:onafterFullStop(From,Event,To) +local pos=self:GetCoordinate() +local wp=pos:WaypointGround(0) +self:Route({wp}) +end +function ARMYGROUP:onafterCruise(From,Event,To,Speed,Formation) +self:__UpdateRoute(-1,nil,Speed,Formation) +end +function ARMYGROUP:onafterStop(From,Event,To) +self:UnHandleEvent(EVENTS.Birth) +self:UnHandleEvent(EVENTS.Dead) +self:UnHandleEvent(EVENTS.RemoveUnit) +self:GetParent(self).onafterStop(self,From,Event,To) +end +function ARMYGROUP:OnEventBirth(EventData) +if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then +local unit=EventData.IniUnit +local group=EventData.IniGroup +local unitname=EventData.IniUnitName +if self.respawning then +local function reset() +self.respawning=nil +end +self:ScheduleOnce(1,reset) +else +local element=self:GetElementByName(unitname) +self:T3(self.lid..string.format("EVENT: Element %s born ==> spawned",element.name)) +self:ElementSpawned(element) +end +end +end +function ARMYGROUP:OnEventDead(EventData) +if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then +self:T(self.lid..string.format("EVENT: Unit %s dead!",EventData.IniUnitName)) +local unit=EventData.IniUnit +local group=EventData.IniGroup +local unitname=EventData.IniUnitName +local element=self:GetElementByName(unitname) +if element then +self:T(self.lid..string.format("EVENT: Element %s dead ==> destroyed",element.name)) +self:ElementDestroyed(element) +end +end +end +function ARMYGROUP:OnEventRemoveUnit(EventData) +if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then +local unit=EventData.IniUnit +local group=EventData.IniGroup +local unitname=EventData.IniUnitName +local element=self:GetElementByName(unitname) +if element then +self:T(self.lid..string.format("EVENT: Element %s removed ==> dead",element.name)) +self:ElementDead(element) +end +end +end +function ARMYGROUP:OnEventHit(EventData) +if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then +local unit=EventData.IniUnit +local group=EventData.IniGroup +local unitname=EventData.IniUnitName +end +end +function ARMYGROUP:AddWaypoint(Coordinate,Speed,AfterWaypointWithID,Formation,Updateroute) +local coordinate=self:_CoordinateFromObject(Coordinate) +local wpnumber=self:GetWaypointIndexAfterID(AfterWaypointWithID) +if wpnumber>self.currentwp then +self.passedfinalwp=false +end +Speed=Speed or self:GetSpeedCruise() +local wp=coordinate:WaypointGround(UTILS.KnotsToKmph(Speed),Formation) +local waypoint=self:_CreateWaypoint(wp) +self:_AddWaypoint(waypoint,wpnumber) +waypoint.roadcoord=coordinate:GetClosestPointToRoad(false) +if waypoint.roadcoord then +waypoint.roaddist=coordinate:Get2DDistance(waypoint.roadcoord) +else +waypoint.roaddist=1000*1000 +end +self:T(self.lid..string.format("Adding waypoint UID=%d (index=%d), Speed=%.1f knots, Dist2Road=%d m, Action=%s",waypoint.uid,wpnumber,Speed,waypoint.roaddist,waypoint.action)) +if Updateroute==nil or Updateroute==true then +self:_CheckGroupDone(1) +end +return waypoint +end +function ARMYGROUP:_InitGroup() +if self.groupinitialized then +self:E(self.lid.."WARNING: Group was already initialized!") +return +end +self.template=self.group:GetTemplate() +self.isAircraft=false +self.isNaval=false +self.isGround=true +self.isAI=true +self.isLateActivated=self.template.lateActivation +self.isUncontrolled=false +self.speedMax=self.group:GetSpeedMax() +self.speedCruise=self.speedMax*0.7 +self.ammo=self:GetAmmoTot() +self.radio.On=false +self.radio.Freq=133 +self.radio.Modu=radio.modulation.AM +self:SetDefaultRadio(self.radio.Freq,self.radio.Modu,self.radio.On) +self.optionDefault.Formation=self:GetWaypoint(1).action +self:SetDefaultTACAN(nil,nil,nil,nil,true) +self.tacan=UTILS.DeepCopy(self.tacanDefault) +local units=self.group:GetUnits() +for _,_unit in pairs(units)do +local unit=_unit +local unittemplate=unit:GetTemplate() +local element={} +element.name=unit:GetName() +element.unit=unit +element.status=OPSGROUP.ElementStatus.INUTERO +element.typename=unit:GetTypeName() +element.skill=unittemplate.skill or"Unknown" +element.ai=true +element.category=element.unit:GetUnitCategory() +element.categoryname=element.unit:GetCategoryName() +element.size,element.length,element.height,element.width=unit:GetObjectSize() +element.ammo0=self:GetAmmoUnit(unit,false) +element.life0=unit:GetLife0() +element.life=element.life0 +if self.verbose>=2 then +local text=string.format("Adding element %s: status=%s, skill=%s, life=%.3f category=%s (%d), size: %.1f (L=%.1f H=%.1f W=%.1f)", +element.name,element.status,element.skill,element.life,element.categoryname,element.category,element.size,element.length,element.height,element.width) +self:I(self.lid..text) +end +table.insert(self.elements,element) +self.descriptors=self.descriptors or unit:GetDesc() +self.actype=self.actype or unit:GetTypeName() +if unit:IsAlive()then +self:ElementSpawned(element) +end +end +if self.verbose>=1 then +local text=string.format("Initialized Army Group %s:\n",self.groupname) +text=text..string.format("Unit type = %s\n",self.actype) +text=text..string.format("Speed max = %.1f Knots\n",UTILS.KmphToKnots(self.speedMax)) +text=text..string.format("Speed cruise = %.1f Knots\n",UTILS.KmphToKnots(self.speedCruise)) +text=text..string.format("Elements = %d\n",#self.elements) +text=text..string.format("Waypoints = %d\n",#self.waypoints) +text=text..string.format("Radio = %.1f MHz %s %s\n",self.radio.Freq,UTILS.GetModulationName(self.radio.Modu),tostring(self.radio.On)) +text=text..string.format("Ammo = %d (G=%d/R=%d/M=%d)\n",self.ammo.Total,self.ammo.Guns,self.ammo.Rockets,self.ammo.Missiles) +text=text..string.format("FSM state = %s\n",self:GetState()) +text=text..string.format("Is alive = %s\n",tostring(self:IsAlive())) +text=text..string.format("LateActivate = %s\n",tostring(self:IsLateActivated())) +self:I(self.lid..text) +end +self.groupinitialized=true +return self +end +function ARMYGROUP:SwitchFormation(Formation,Permanently,NoRouteUpdate) +if self:IsAlive()or self:IsInUtero()then +Formation=Formation or self.optionDefault.Formation +if Permanently then +self.formationPerma=Formation +else +self.formationPerma=nil +end +self.option.Formation=Formation +if self:IsInUtero()then +self:T(self.lid..string.format("Will switch formation to %s (permanently=%s) when group is spawned",self.option.Formation,tostring(Permanently))) +else +if NoRouteUpdate then +else +self:__UpdateRoute(-1,nil,nil,Formation) +end +self:T(self.lid..string.format("Switching formation to %s (permanently=%s)",self.option.Formation,tostring(Permanently))) +end +end +return self +end +SQUADRON={ +ClassName="SQUADRON", +verbose=0, +lid=nil, +name=nil, +templatename=nil, +aircrafttype=nil, +assets={}, +missiontypes={}, +repairtime=0, +maintenancetime=0, +livery=nil, +skill=nil, +modex=nil, +modexcounter=0, +callsignName=nil, +callsigncounter=11, +airwing=nil, +Ngroups=nil, +engageRange=nil, +tankerSystem=nil, +refuelSystem=nil, +tacanChannel={}, +} +SQUADRON.version="0.5.0" +function SQUADRON:New(TemplateGroupName,Ngroups,SquadronName) +local self=BASE:Inherit(self,FSM:New()) +self.templatename=TemplateGroupName +self.name=tostring(SquadronName or TemplateGroupName) +self.lid=string.format("SQUADRON %s | ",self.name) +self.templategroup=GROUP:FindByName(self.templatename) +if not self.templategroup then +self:E(self.lid..string.format("ERROR: Template group %s does not exist!",tostring(self.templatename))) +return nil +end +self.Ngroups=Ngroups or 3 +self:SetMissionRange() +self:SetSkill(AI.Skill.GOOD) +self:AddMissionCapability(AUFTRAG.Type.ORBIT) +self.attribute=self.templategroup:GetAttribute() +self.aircrafttype=self.templategroup:GetTypeName() +self.refuelSystem=select(2,self.templategroup:GetUnit(1):IsRefuelable()) +self.tankerSystem=select(2,self.templategroup:GetUnit(1):IsTanker()) +self:SetStartState("Stopped") +self:AddTransition("Stopped","Start","OnDuty") +self:AddTransition("*","Status","*") +self:AddTransition("OnDuty","Pause","Paused") +self:AddTransition("Paused","Unpause","OnDuty") +self:AddTransition("*","Stop","Stopped") +if false then +BASE:TraceOnOff(true) +BASE:TraceClass(self.ClassName) +BASE:TraceLevel(1) +end +return self +end +function SQUADRON:SetLivery(LiveryName) +self.livery=LiveryName +return self +end +function SQUADRON:SetSkill(Skill) +self.skill=Skill +return self +end +function SQUADRON:SetVerbosity(VerbosityLevel) +self.verbose=VerbosityLevel or 0 +return self +end +function SQUADRON:SetTurnoverTime(MaintenanceTime,RepairTime) +self.maintenancetime=MaintenanceTime and MaintenanceTime*60 or 0 +self.repairtime=RepairTime and RepairTime*60 or 0 +return self +end +function SQUADRON:SetRadio(Frequency,Modulation) +self.radioFreq=Frequency or 251 +self.radioModu=Modulation or radio.modulation.AM +return self +end +function SQUADRON: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 +function SQUADRON:AddMissionCapability(MissionTypes,Performance) +if MissionTypes and type(MissionTypes)~="table"then +MissionTypes={MissionTypes} +end +self.missiontypes=self.missiontypes or{} +for _,missiontype in pairs(MissionTypes)do +if self:CheckMissionCapability(missiontype,self.missiontypes)then +self:E(self.lid.."WARNING: Mission capability already present! No need to add it twice.") +else +local capability={} +capability.MissionType=missiontype +capability.Performance=Performance or 50 +table.insert(self.missiontypes,capability) +end +end +self:T2(self.missiontypes) +return self +end +function SQUADRON:GetMissionTypes() +local missiontypes={} +for _,Capability in pairs(self.missiontypes)do +local capability=Capability +table.insert(missiontypes,capability.MissionType) +end +return missiontypes +end +function SQUADRON:GetMissionCapabilities() +return self.missiontypes +end +function SQUADRON:GetMissionPeformance(MissionType) +for _,Capability in pairs(self.missiontypes)do +local capability=Capability +if capability.MissionType==MissionType then +return capability.Performance +end +end +return-1 +end +function SQUADRON:SetMissionRange(Range) +self.engageRange=UTILS.NMToMeters(Range or 100) +return self +end +function SQUADRON:SetCallsign(Callsign,Index) +self.callsignName=Callsign +self.callsignIndex=Index +return self +end +function SQUADRON:SetModex(Modex,Prefix,Suffix) +self.modex=Modex +self.modexPrefix=Prefix +self.modexSuffix=Suffix +return self +end +function SQUADRON:SetFuelLowThreshold(LowFuel) +self.fuellow=LowFuel or 25 +return self +end +function SQUADRON:SetFuelLowRefuel(switch) +if switch==false then +self.fuellowRefuel=false +else +self.fuellowRefuel=true +end +return self +end +function SQUADRON:SetAirwing(Airwing) +self.airwing=Airwing +return self +end +function SQUADRON:AddAsset(Asset) +self:T(self.lid..string.format("Adding asset %s of type %s",Asset.spawngroupname,Asset.unittype)) +Asset.squadname=self.name +table.insert(self.assets,Asset) +return self +end +function SQUADRON:DelAsset(Asset) +for i,_asset in pairs(self.assets)do +local asset=_asset +if Asset.uid==asset.uid then +self:T2(self.lid..string.format("Removing asset %s",asset.spawngroupname)) +table.remove(self.assets,i) +break +end +end +return self +end +function SQUADRON:DelGroup(GroupName) +for i,_asset in pairs(self.assets)do +local asset=_asset +if GroupName==asset.spawngroupname then +self:T2(self.lid..string.format("Removing asset %s",asset.spawngroupname)) +table.remove(self.assets,i) +break +end +end +return self +end +function SQUADRON:GetName() +return self.name +end +function SQUADRON:GetRadio() +return self.radioFreq,self.radioModu +end +function SQUADRON:GetCallsign(Asset) +if self.callsignName then +Asset.callsign={} +for i=1,Asset.nunits do +local callsign={} +callsign[1]=self.callsignName +callsign[2]=math.floor(self.callsigncounter/10) +callsign[3]=self.callsigncounter%10 +if callsign[3]==0 then +callsign[3]=1 +self.callsigncounter=self.callsigncounter+2 +else +self.callsigncounter=self.callsigncounter+1 +end +Asset.callsign[i]=callsign +self:T3({callsign=callsign}) +end +end +end +function SQUADRON:GetModex(Asset) +if self.modex then +Asset.modex={} +for i=1,Asset.nunits do +Asset.modex[i]=string.format("%03d",self.modex+self.modexcounter) +self.modexcounter=self.modexcounter+1 +self:T3({modex=Asset.modex[i]}) +end +end +end +function SQUADRON:AddTacanChannel(ChannelMin,ChannelMax) +ChannelMax=ChannelMax or ChannelMin +if ChannelMin>126 then +self:E(self.lid.."ERROR: TACAN Channel must be <= 126! Will not add to available channels") +return self +end +if ChannelMax>126 then +self:E(self.lid.."WARNING: TACAN Channel must be <= 126! Adjusting ChannelMax to 126") +ChannelMax=126 +end +for i=ChannelMin,ChannelMax do +self.tacanChannel[i]=true +end +return self +end +function SQUADRON:FetchTacan() +for channel,free in pairs(self.tacanChannel)do +if free then +self:T(self.lid..string.format("Checking out Tacan channel %d",channel)) +self.tacanChannel[channel]=false +return channel +end +end +return nil +end +function SQUADRON:ReturnTacan(channel) +self:T(self.lid..string.format("Returning Tacan channel %d",channel)) +self.tacanChannel[channel]=true +end +function SQUADRON:IsOnDuty() +return self:Is("OnDuty") +end +function SQUADRON:IsStopped() +return self:Is("Stopped") +end +function SQUADRON:IsPaused() +return self:Is("Paused") +end +function SQUADRON:onafterStart(From,Event,To) +local text=string.format("Starting SQUADRON",self.name) +self:T(self.lid..text) +self:__Status(-1) +end +function SQUADRON:onafterStatus(From,Event,To) +if self.verbose>=1 then +local fsmstate=self:GetState() +local callsign=self.callsignName and UTILS.GetCallsignName(self.callsignName)or"N/A" +local modex=self.modex and self.modex or-1 +local skill=self.skill and tostring(self.skill)or"N/A" +local NassetsTot=#self.assets +local NassetsInS=self:CountAssetsInStock() +local NassetsQP=0;local NassetsP=0;local NassetsQ=0 +if self.airwing then +NassetsQP,NassetsP,NassetsQ=self.airwing:CountAssetsOnMission(nil,self) +end +local text=string.format("%s [Type=%s, Call=%s, Modex=%d, Skill=%s]: Assets Total=%d, Stock=%d, Mission=%d [Active=%d, Queue=%d]", +fsmstate,self.aircrafttype,callsign,modex,skill,NassetsTot,NassetsInS,NassetsQP,NassetsP,NassetsQ) +self:I(self.lid..text) +self:_CheckAssetStatus() +end +if not self:IsStopped()then +self:__Status(-60) +end +end +function SQUADRON:_CheckAssetStatus() +if self.verbose>=2 and#self.assets>0 then +local text="" +for j,_asset in pairs(self.assets)do +local asset=_asset +text=text..string.format("\n[%d] %s (%s*%d): ",j,asset.spawngroupname,asset.unittype,asset.nunits) +if asset.spawned then +local mission=self.airwing and self.airwing:GetAssetCurrentMission(asset)or false +if mission then +local distance=asset.flightgroup and UTILS.MetersToNM(mission:GetTargetDistance(asset.flightgroup.group:GetCoordinate()))or 0 +text=text..string.format("Mission %s - %s: Status=%s, Dist=%.1f NM",mission.name,mission.type,mission.status,distance) +else +text=text.."Mission None" +end +text=text..", Flight: " +if asset.flightgroup and asset.flightgroup:IsAlive()then +local status=asset.flightgroup:GetState() +local fuelmin=asset.flightgroup:GetFuelMin() +local fuellow=asset.flightgroup:IsFuelLow() +local fuelcri=asset.flightgroup:IsFuelCritical() +text=text..string.format("%s Fuel=%d",status,fuelmin) +if fuelcri then +text=text.." (Critical!)" +elseif fuellow then +text=text.." (Low)" +end +local lifept,lifept0=asset.flightgroup:GetLifePoints() +text=text..string.format(", Life=%d/%d",lifept,lifept0) +local ammo=asset.flightgroup:GetAmmoTot() +text=text..string.format(", Ammo=%d [G=%d, R=%d, B=%d, M=%d]",ammo.Total,ammo.Guns,ammo.Rockets,ammo.Bombs,ammo.Missiles) +else +text=text.."N/A" +end +local payload=asset.payload and table.concat(self.airwing:GetPayloadMissionTypes(asset.payload),", ")or"None" +text=text..", Payload={"..payload.."}" +else +text=text..string.format("In Stock") +if self:IsRepaired(asset)then +text=text..", Combat Ready" +else +text=text..string.format(", Repaired in %d sec",self:GetRepairTime(asset)) +if asset.damage then +text=text..string.format(" (Damage=%.1f)",asset.damage) +end +end +if asset.Treturned then +local T=timer.getAbsTime()-asset.Treturned +text=text..string.format(", Returned for %d sec",T) +end +end +end +self:I(self.lid..text) +end +end +function SQUADRON:onafterStop(From,Event,To) +self:I(self.lid.."STOPPING Squadron!") +for i=#self.assets,1,-1 do +local asset=self.assets[i] +self:DelAsset(asset) +end +self.CallScheduler:Clear() +end +function SQUADRON:CanMission(Mission) +local cando=true +if not self:IsOnDuty()then +self:T(self.lid..string.format("Squad in not OnDuty but in state %s. Cannot do mission %s with target %s",self:GetState(),Mission.name,Mission:GetTargetName())) +return false +end +if not self:CheckMissionType(Mission.type,self:GetMissionTypes())then +self:T(self.lid..string.format("INFO: Squad cannot do mission type %s (%s, %s)",Mission.type,Mission.name,Mission:GetTargetName())) +return false +end +if Mission.type==AUFTRAG.Type.TANKER then +if Mission.refuelSystem and Mission.refuelSystem==self.tankerSystem then +else +self:T(self.lid..string.format("INFO: Wrong refueling system requested=%s != %s=available",tostring(Mission.refuelSystem),tostring(self.tankerSystem))) +return false +end +end +local TargetDistance=Mission:GetTargetDistance(self.airwing:GetCoordinate()) +local engagerange=Mission.engageRange and math.max(self.engageRange,Mission.engageRange)or self.engageRange +if TargetDistance>engagerange then +self:I(self.lid..string.format("INFO: Squad is not in range. Target dist=%d > %d NM max mission Range",UTILS.MetersToNM(TargetDistance),UTILS.MetersToNM(engagerange))) +return false +end +return true +end +function SQUADRON:CountAssetsInStock() +local N=0 +for _,_asset in pairs(self.assets)do +local asset=_asset +if asset.spawned then +else +N=N+1 +end +end +return N +end +function SQUADRON:RecruitAssets(Mission,Npayloads) +Npayloads=Npayloads or self.airwing:CountPayloadsInStock(Mission.type,self.aircrafttype,Mission.payloads) +local assets={} +for _,_asset in pairs(self.assets)do +local asset=_asset +if self.airwing:IsAssetOnMission(asset)then +if self.airwing:IsAssetOnMission(asset,AUFTRAG.Type.GCICAP)and Mission.type==AUFTRAG.Type.INTERCEPT then +self:I(self.lid.."Adding asset on GCICAP mission for an INTERCEPT mission") +table.insert(assets,asset) +end +else +if asset.spawned then +local flightgroup=asset.flightgroup +if self:CheckMissionCapability(Mission.type,asset.payload.capabilities)and flightgroup and flightgroup:IsAlive()then +local combatready=true +if Mission.type==AUFTRAG.Type.INTERCEPT then +combatready=flightgroup:CanAirToAir() +else +local excludeguns=Mission.type==AUFTRAG.Type.BOMBING or Mission.type==AUFTRAG.Type.BOMBRUNWAY or Mission.type==AUFTRAG.Type.BOMBCARPET or Mission.type==AUFTRAG.Type.SEAD or Mission.type==AUFTRAG.Type.ANTISHIP +combatready=flightgroup:CanAirToGround(excludeguns) +end +if flightgroup:IsFuelLow()then +combatready=false +end +if flightgroup:IsHolding()or flightgroup:IsLanding()or flightgroup:IsLanded()or flightgroup:IsArrived()or flightgroup:IsDead()or flightgroup:IsStopped()then +combatready=false +end +if combatready then +self:I(self.lid.."Adding SPAWNED asset to ANOTHER mission as it is COMBATREADY") +table.insert(assets,asset) +end +end +else +if Npayloads>0 and self:IsRepaired(asset)and(not asset.requested)then +table.insert(assets,asset) +Npayloads=Npayloads-1 +end +end +end +end +return assets +end +function SQUADRON:GetRepairTime(Asset) +if Asset.Treturned then +local t=self.maintenancetime +t=t+Asset.damage*self.repairtime +local dt=timer.getAbsTime()-Asset.Treturned +local T=t-dt +return T +else +return 0 +end +end +function SQUADRON:IsRepaired(Asset) +if Asset.Treturned then +local Tnow=timer.getAbsTime() +local Trepaired=Asset.Treturned+self.maintenancetime +if Tnow>=Trepaired then +return true +else +return false +end +else +return true +end +end +function SQUADRON:CheckMissionType(MissionType,PossibleTypes) +if type(PossibleTypes)=="string"then +PossibleTypes={PossibleTypes} +end +for _,canmission in pairs(PossibleTypes)do +if canmission==MissionType then +return true +end +end +return false +end +function SQUADRON:CheckMissionCapability(MissionType,Capabilities) +for _,cap in pairs(Capabilities)do +local capability=cap +if capability.MissionType==MissionType then +return true +end +end +return false +end +AIRWING={ +ClassName="AIRWING", +verbose=0, +lid=nil, +menu=nil, +squadrons={}, +missionqueue={}, +payloads={}, +payloadcounter=0, +pointsCAP={}, +pointsTANKER={}, +pointsAWACS={}, +wingcommander=nil, +markpoints=false, +} +AIRWING.version="0.5.1" +function AIRWING:New(warehousename,airwingname) +local self=BASE:Inherit(self,WAREHOUSE:New(warehousename,airwingname)) +if not self then +BASE:E(string.format("ERROR: Could not find warehouse %s!",warehousename)) +return nil +end +self.lid=string.format("AIRWING %s | ",self.alias) +self:AddTransition("*","MissionRequest","*") +self:AddTransition("*","MissionCancel","*") +self:AddTransition("*","SquadAssetReturned","*") +self:AddTransition("*","FlightOnMission","*") +self.nflightsCAP=0 +self.nflightsAWACS=0 +self.nflightsTANKERboom=0 +self.nflightsTANKERprobe=0 +self.nflightsRecoveryTanker=0 +self.nflightsRescueHelo=0 +self.markpoints=false +return self +end +function AIRWING:AddSquadron(Squadron) +table.insert(self.squadrons,Squadron) +self:AddAssetToSquadron(Squadron,Squadron.Ngroups) +if Squadron.attribute==GROUP.Attribute.AIR_AWACS then +self:NewPayload(Squadron.templategroup,-1,AUFTRAG.Type.AWACS) +elseif Squadron.attribute==GROUP.Attribute.AIR_TANKER then +self:NewPayload(Squadron.templategroup,-1,AUFTRAG.Type.TANKER) +end +Squadron:SetAirwing(self) +if Squadron:IsStopped()then +Squadron:Start() +end +return self +end +function AIRWING:NewPayload(Unit,Npayloads,MissionTypes,Performance) +Performance=Performance or 50 +if type(Unit)=="string"then +local name=Unit +Unit=UNIT:FindByName(name) +if not Unit then +Unit=GROUP:FindByName(name) +end +end +if Unit then +if Unit:IsInstanceOf("GROUP")then +Unit=Unit:GetUnit(1) +end +if MissionTypes and type(MissionTypes)~="table"then +MissionTypes={MissionTypes} +end +local payload={} +payload.uid=self.payloadcounter +payload.unitname=Unit:GetName() +payload.aircrafttype=Unit:GetTypeName() +payload.pylons=Unit:GetTemplatePayload() +payload.unlimited=Npayloads<0 +if payload.unlimited then +payload.navail=1 +else +payload.navail=Npayloads or 99 +end +payload.capabilities={} +for _,missiontype in pairs(MissionTypes)do +local capability={} +capability.MissionType=missiontype +capability.Performance=Performance +table.insert(payload.capabilities,capability) +end +if not self:CheckMissionType(AUFTRAG.Type.ORBIT,MissionTypes)then +local capability={} +capability.MissionType=AUFTRAG.Type.ORBIT +capability.Performance=50 +table.insert(payload.capabilities,capability) +end +self:T(self.lid..string.format("Adding new payload from unit %s for aircraft type %s: ID=%d, N=%d (unlimited=%s), performance=%d, missions: %s", +payload.unitname,payload.aircrafttype,payload.uid,payload.navail,tostring(payload.unlimited),Performance,table.concat(MissionTypes,", "))) +table.insert(self.payloads,payload) +self.payloadcounter=self.payloadcounter+1 +return payload +end +self:E(self.lid.."ERROR: No UNIT found to create PAYLOAD!") +return nil +end +function AIRWING:AddPayloadCapability(Payload,MissionTypes,Performance) +if MissionTypes and type(MissionTypes)~="table"then +MissionTypes={MissionTypes} +end +Payload.capabilities=Payload.capabilities or{} +for _,missiontype in pairs(MissionTypes)do +local capability={} +capability.MissionType=missiontype +capability.Performance=Performance +table.insert(Payload.capabilities,capability) +end +return self +end +function AIRWING:FetchPayloadFromStock(UnitType,MissionType,Payloads) +if not self.payloads or#self.payloads==0 then +self:T(self.lid.."WARNING: No payloads in stock!") +return nil +end +if self.verbose>=4 then +self:I(self.lid..string.format("Looking for payload for unit type=%s and mission type=%s",UnitType,MissionType)) +for i,_payload in pairs(self.payloads)do +local payload=_payload +local performance=self:GetPayloadPeformance(payload,MissionType) +self:I(self.lid..string.format("[%d] Payload type=%s navail=%d unlimited=%s",i,payload.aircrafttype,payload.navail,tostring(payload.unlimited))) +end +end +local function sortpayloads(a,b) +local pA=a +local pB=b +if a and b then +local performanceA=self:GetPayloadPeformance(a,MissionType) +local performanceB=self:GetPayloadPeformance(b,MissionType) +return(performanceA>performanceB)or(performanceA==performanceB and a.unlimited==true)or(performanceA==performanceB and a.unlimited==true and b.unlimited==true and a.navail>b.navail) +elseif not a then +self:I(self.lid..string.format("FF ERROR in sortpayloads: a is nil")) +return false +elseif not b then +self:I(self.lid..string.format("FF ERROR in sortpayloads: b is nil")) +return true +else +self:I(self.lid..string.format("FF ERROR in sortpayloads: a and b are nil")) +return false +end +end +local function _checkPayloads(payload) +if Payloads then +for _,Payload in pairs(Payloads)do +if Payload.uid==payload.uid then +return true +end +end +else +return nil +end +return false +end +local payloads={} +for _,_payload in pairs(self.payloads)do +local payload=_payload +local specialpayload=_checkPayloads(payload) +local compatible=self:CheckMissionCapability(MissionType,payload.capabilities) +local goforit=specialpayload or(specialpayload==nil and compatible) +if payload.aircrafttype==UnitType and payload.navail>0 and goforit then +table.insert(payloads,payload) +end +end +if self.verbose>=4 then +self:I(self.lid..string.format("Sorted payloads for mission type X and aircraft type=Y:")) +for _,_payload in ipairs(self.payloads)do +local payload=_payload +if payload.aircrafttype==UnitType and self:CheckMissionCapability(MissionType,payload.capabilities)then +local performace=self:GetPayloadPeformance(payload,MissionType) +self:I(self.lid..string.format("FF %s payload for %s: avail=%d performace=%d",MissionType,payload.aircrafttype,payload.navail,performace)) +end +end +end +if#payloads==0 then +self:T(self.lid.."Warning could not find a payload for airframe X mission type Y!") +return nil +elseif#payloads==1 then +local payload=payloads[1] +if not payload.unlimited then +payload.navail=payload.navail-1 +end +return payload +else +table.sort(payloads,sortpayloads) +local payload=payloads[1] +if not payload.unlimited then +payload.navail=payload.navail-1 +end +return payload +end +end +function AIRWING:ReturnPayloadFromAsset(asset) +local payload=asset.payload +if payload then +if not payload.unlimited then +payload.navail=payload.navail+1 +end +asset.payload=nil +else +self:E(self.lid.."ERROR: asset had no payload attached!") +end +end +function AIRWING:AddAssetToSquadron(Squadron,Nassets) +if Squadron then +local Group=GROUP:FindByName(Squadron.templatename) +if Group then +local text=string.format("Adding asset %s to squadron %s",Group:GetName(),Squadron.name) +self:T(self.lid..text) +self:AddAsset(Group,Nassets,nil,nil,nil,nil,Squadron.skill,Squadron.livery,Squadron.name) +else +self:E(self.lid.."ERROR: Group does not exist!") +end +else +self:E(self.lid.."ERROR: Squadron does not exit!") +end +return self +end +function AIRWING:GetSquadron(SquadronName) +for _,_squadron in pairs(self.squadrons)do +local squadron=_squadron +if squadron.name==SquadronName then +return squadron +end +end +return nil +end +function AIRWING:SetVerbosity(VerbosityLevel) +self.verbose=VerbosityLevel or 0 +return self +end +function AIRWING:GetSquadronOfAsset(Asset) +return self:GetSquadron(Asset.squadname) +end +function AIRWING:RemoveAssetFromSquadron(Asset) +local squad=self:GetSquadronOfAsset(Asset) +if squad then +squad:DelAsset(Asset) +end +end +function AIRWING:AddMission(Mission) +Mission:Queued(self) +table.insert(self.missionqueue,Mission) +local text=string.format("Added mission %s (type=%s). Starting at %s. Stopping at %s", +tostring(Mission.name),tostring(Mission.type),UTILS.SecondsToClock(Mission.Tstart,true),Mission.Tstop and UTILS.SecondsToClock(Mission.Tstop,true)or"INF") +self:T(self.lid..text) +return self +end +function AIRWING:RemoveMission(Mission) +for i,_mission in pairs(self.missionqueue)do +local mission=_mission +if mission.auftragsnummer==Mission.auftragsnummer then +table.remove(self.missionqueue,i) +break +end +end +return self +end +function AIRWING:SetNumberCAP(n) +self.nflightsCAP=n or 1 +return self +end +function AIRWING:SetNumberTankerBoom(Nboom) +self.nflightsTANKERboom=Nboom or 1 +return self +end +function AIRWING:ShowPatrolPointMarkers(onoff) +if onoff then +self.markpoints=true +else +self.markpoints=false +end +return self +end +function AIRWING:SetNumberTankerProbe(Nprobe) +self.nflightsTANKERprobe=Nprobe or 1 +return self +end +function AIRWING:SetNumberAWACS(n) +self.nflightsAWACS=n or 1 +return self +end +function AIRWING:SetNumberRescuehelo(n) +self.nflightsRescueHelo=n or 1 +return self +end +function AIRWING:_PatrolPointMarkerText(point) +local text=string.format("%s Occupied=%d, \nheading=%03d, leg=%d NM, alt=%d ft, speed=%d kts", +point.type,point.noccupied,point.heading,point.leg,point.altitude,point.speed) +return text +end +function AIRWING:UpdatePatrolPointMarker(point) +if self.markpoints then +local text=string.format("%s Occupied=%d\nheading=%03d, leg=%d NM, alt=%d ft, speed=%d kts", +point.type,point.noccupied,point.heading,point.leg,point.altitude,point.speed) +point.marker:UpdateText(text,1) +end +end +function AIRWING:NewPatrolPoint(Type,Coordinate,Altitude,Speed,Heading,LegLength) +local patrolpoint={} +patrolpoint.type=Type or"Unknown" +patrolpoint.coord=Coordinate or self:GetCoordinate():Translate(UTILS.NMToMeters(math.random(10,15)),math.random(360)) +patrolpoint.heading=Heading or math.random(360) +patrolpoint.leg=LegLength or 15 +patrolpoint.altitude=Altitude or math.random(10,20)*1000 +patrolpoint.speed=Speed or 350 +patrolpoint.noccupied=0 +if self.markpoints then +patrolpoint.marker=MARKER:New(Coordinate,"New Patrol Point"):ToAll() +AIRWING.UpdatePatrolPointMarker(patrolpoint) +end +return patrolpoint +end +function AIRWING:AddPatrolPointCAP(Coordinate,Altitude,Speed,Heading,LegLength) +local patrolpoint=self:NewPatrolPoint("CAP",Coordinate,Altitude,Speed,Heading,LegLength) +table.insert(self.pointsCAP,patrolpoint) +return self +end +function AIRWING:AddPatrolPointTANKER(Coordinate,Altitude,Speed,Heading,LegLength) +local patrolpoint=self:NewPatrolPoint("Tanker",Coordinate,Altitude,Speed,Heading,LegLength) +table.insert(self.pointsTANKER,patrolpoint) +return self +end +function AIRWING:AddPatrolPointAWACS(Coordinate,Altitude,Speed,Heading,LegLength) +local patrolpoint=self:NewPatrolPoint("AWACS",Coordinate,Altitude,Speed,Heading,LegLength) +table.insert(self.pointsAWACS,patrolpoint) +return self +end +function AIRWING:onafterStart(From,Event,To) +self:GetParent(self).onafterStart(self,From,Event,To) +self:I(self.lid..string.format("Starting AIRWING v%s",AIRWING.version)) +end +function AIRWING:onafterStatus(From,Event,To) +self:GetParent(self).onafterStatus(self,From,Event,To) +local fsmstate=self:GetState() +self:CheckCAP() +self:CheckTANKER() +self:CheckAWACS() +self:CheckRescuhelo() +if self.verbose>=1 then +local Nmissions=self:CountMissionsInQueue() +local Npayloads=self:CountPayloadsInStock(AUFTRAG.Type) +local Npq,Np,Nq=self:CountAssetsOnMission() +local assets=string.format("%d (OnMission: Total=%d, Active=%d, Queued=%d)",self:CountAssets(),Npq,Np,Nq) +local text=string.format("%s: Missions=%d, Payloads=%d (%d), Squads=%d, Assets=%s",fsmstate,Nmissions,Npayloads,#self.payloads,#self.squadrons,assets) +self:I(self.lid..text) +end +if self.verbose>=2 then +local text=string.format("Missions Total=%d:",#self.missionqueue) +for i,_mission in pairs(self.missionqueue)do +local mission=_mission +local prio=string.format("%d/%s",mission.prio,tostring(mission.importance));if mission.urgent then prio=prio.." (!)"end +local assets=string.format("%d/%d",mission:CountOpsGroups(),mission.nassets) +local target=string.format("%d/%d Damage=%.1f",mission:CountMissionTargets(),mission:GetTargetInitialNumber(),mission:GetTargetDamage()) +text=text..string.format("\n[%d] %s %s: Status=%s, Prio=%s, Assets=%s, Targets=%s",i,mission.name,mission.type,mission.status,prio,assets,target) +end +self:I(self.lid..text) +end +if self.verbose>=3 then +local text="Squadrons:" +for i,_squadron in pairs(self.squadrons)do +local squadron=_squadron +local callsign=squadron.callsignName and UTILS.GetCallsignName(squadron.callsignName)or"N/A" +local modex=squadron.modex and squadron.modex or-1 +local skill=squadron.skill and tostring(squadron.skill)or"N/A" +text=text..string.format("\n* %s %s: %s*%d/%d, Callsign=%s, Modex=%d, Skill=%s",squadron.name,squadron:GetState(),squadron.aircrafttype,squadron:CountAssetsInStock(),#squadron.assets,callsign,modex,skill) +end +self:I(self.lid..text) +end +self:_CheckMissions() +local mission=self:_GetNextMission() +if mission then +self:MissionRequest(mission) +end +end +function AIRWING:_GetPatrolData(PatrolPoints) +local function sort(a,b) +return a.noccupied0 then +table.sort(PatrolPoints,sort) +return PatrolPoints[1] +else +return self:NewPatrolPoint() +end +end +function AIRWING:CheckCAP() +local Ncap=self:CountMissionsInQueue({AUFTRAG.Type.GCICAP,AUFTRAG.Type.INTERCEPT}) +for i=1,self.nflightsCAP-Ncap do +local patrol=self:_GetPatrolData(self.pointsCAP) +local altitude=patrol.altitude+1000*patrol.noccupied +local missionCAP=AUFTRAG:NewGCICAP(patrol.coord,altitude,patrol.speed,patrol.heading,patrol.leg) +missionCAP.patroldata=patrol +patrol.noccupied=patrol.noccupied+1 +if self.markpoints then AIRWING.UpdatePatrolPointMarker(patrol)end +self:AddMission(missionCAP) +end +return self +end +function AIRWING:CheckTANKER() +local Nboom=0 +local Nprob=0 +for _,_mission in pairs(self.missionqueue)do +local mission=_mission +if mission:IsNotOver()and mission.type==AUFTRAG.Type.TANKER then +if mission.refuelSystem==0 then +Nboom=Nboom+1 +elseif mission.refuelSystem==1 then +Nprob=Nprob+1 +end +end +end +for i=1,self.nflightsTANKERboom-Nboom do +local patrol=self:_GetPatrolData(self.pointsTANKER) +local altitude=patrol.altitude+1000*patrol.noccupied +local mission=AUFTRAG:NewTANKER(patrol.coord,altitude,patrol.speed,patrol.heading,patrol.leg,1) +mission.patroldata=patrol +patrol.noccupied=patrol.noccupied+1 +if self.markpoints then AIRWING.UpdatePatrolPointMarker(patrol)end +self:AddMission(mission) +end +for i=1,self.nflightsTANKERprobe-Nprob do +local patrol=self:_GetPatrolData(self.pointsTANKER) +local altitude=patrol.altitude+1000*patrol.noccupied +local mission=AUFTRAG:NewTANKER(patrol.coord,altitude,patrol.speed,patrol.heading,patrol.leg,0) +mission.patroldata=patrol +patrol.noccupied=patrol.noccupied+1 +if self.markpoints then AIRWING.UpdatePatrolPointMarker(patrol)end +self:AddMission(mission) +end +return self +end +function AIRWING:CheckAWACS() +local N=self:CountMissionsInQueue({AUFTRAG.Type.AWACS}) +for i=1,self.nflightsAWACS-N do +local patrol=self:_GetPatrolData(self.pointsAWACS) +local altitude=patrol.altitude+1000*patrol.noccupied +local mission=AUFTRAG:NewAWACS(patrol.coord,altitude,patrol.speed,patrol.heading,patrol.leg) +mission.patroldata=patrol +patrol.noccupied=patrol.noccupied+1 +if self.markpoints then AIRWING.UpdatePatrolPointMarker(patrol)end +self:AddMission(mission) +end +return self +end +function AIRWING:CheckRescuhelo() +local N=self:CountMissionsInQueue({AUFTRAG.Type.RESCUEHELO}) +local name=self.airbase:GetName() +local carrier=UNIT:FindByName(name) +for i=1,self.nflightsRescueHelo-N do +local mission=AUFTRAG:NewRESCUEHELO(carrier) +self:AddMission(mission) +end +return self +end +function AIRWING:GetTankerForFlight(flightgroup) +local tankers=self:GetAssetsOnMission(AUFTRAG.Type.TANKER) +if#tankers>0 then +local tankeropt={} +for _,_tanker in pairs(tankers)do +local tanker=_tanker +if flightgroup.refueltype and flightgroup.refueltype==tanker.flightgroup.tankertype then +local tankercoord=tanker.flightgroup.group:GetCoordinate() +local assetcoord=flightgroup.group:GetCoordinate() +local dist=assetcoord:Get2DDistance(tankercoord) +if dist>5 then +table.insert(tankeropt,{tanker=tanker,dist=dist}) +end +end +end +table.sort(tankeropt,function(a,b)return a.dist0 then +return tankeropt[1].tanker +else +return nil +end +end +return nil +end +function AIRWING:_CheckMissions() +for _,_mission in pairs(self.missionqueue)do +local mission=_mission +if mission:IsNotOver()and mission:IsReadyToCancel()then +mission:Cancel() +end +end +end +function AIRWING:_GetNextMission() +local Nmissions=#self.missionqueue +if Nmissions==0 then +return nil +end +local function _sort(a,b) +local taskA=a +local taskB=b +return(taskA.prio0 then +self:E(self.lid..string.format("ERROR: mission %s of type %s has already assets attached!",mission.name,mission.type)) +end +mission.assets={} +for i=1,mission.nassets do +local asset=assets[i] +if not asset.payload then +self:E(self.lid.."ERROR: No payload for asset! This should not happen!") +end +mission:AddAsset(asset) +end +for i=mission.nassets+1,#assets do +local asset=assets[i] +for _,uid in pairs(gotpayload)do +if uid==asset.uid then +self:ReturnPayloadFromAsset(asset) +break +end +end +end +return mission +end +end +end +return nil +end +function AIRWING:CalculateAssetMissionScore(asset,Mission,includePayload) +local score=0 +if asset.skill==AI.Skill.AVERAGE then +score=score+0 +elseif asset.skill==AI.Skill.GOOD then +score=score+10 +elseif asset.skill==AI.Skill.HIGH then +score=score+20 +elseif asset.skill==AI.Skill.EXCELLENT then +score=score+30 +end +local squad=self:GetSquadronOfAsset(asset) +local missionperformance=squad:GetMissionPeformance(Mission.type) +score=score+missionperformance +if includePayload and asset.payload then +score=score+self:GetPayloadPeformance(asset.payload,Mission.type) +end +if Mission.type==AUFTRAG.Type.INTERCEPT then +if asset.spawned then +self:T(self.lid.."Adding 25 to asset because it is spawned") +score=score+25 +end +end +return score +end +function AIRWING:_OptimizeAssetSelection(assets,Mission,includePayload) +local TargetVec2=Mission:GetTargetVec2() +local dStock=UTILS.VecDist2D(TargetVec2,self:GetVec2()) +local distmin=math.huge +local distmax=0 +for _,_asset in pairs(assets)do +local asset=_asset +if asset.spawned then +local group=GROUP:FindByName(asset.spawngroupname) +asset.dist=UTILS.VecDist2D(group:GetVec2(),TargetVec2) +else +asset.dist=dStock +end +if asset.distdistmax then +distmax=asset.dist +end +end +for _,_asset in pairs(assets)do +local asset=_asset +asset.score=self:CalculateAssetMissionScore(asset,Mission,includePayload) +end +local function optimize(a,b) +local assetA=a +local assetB=b +return(assetA.score>assetB.score)or(assetA.score==assetB.score and assetA.dist0 then +for i,_asset in pairs(Assetlist)do +local asset=_asset +asset.requested=true +if Mission.missionTask then +asset.missionTask=Mission.missionTask +end +end +self:AddRequest(self,WAREHOUSE.Descriptor.ASSETLIST,Assetlist,#Assetlist,nil,nil,Mission.prio,tostring(Mission.auftragsnummer)) +Mission.requestID=self.queueid +end +end +function AIRWING:onafterMissionCancel(From,Event,To,Mission) +self:I(self.lid..string.format("Cancel mission %s",Mission.name)) +local Ngroups=Mission:CountOpsGroups() +if Mission:IsPlanned()or Mission:IsQueued()or Mission:IsRequested()or Ngroups==0 then +Mission:Done() +else +for _,_asset in pairs(Mission.assets)do +local asset=_asset +local flightgroup=asset.flightgroup +if flightgroup then +flightgroup:MissionCancel(Mission) +end +asset.requested=nil +end +end +if Mission.requestID then +self:_DeleteQueueItemByID(Mission.requestID,self.queue) +end +end +function AIRWING:onafterNewAsset(From,Event,To,asset,assignment) +self:GetParent(self).onafterNewAsset(self,From,Event,To,asset,assignment) +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) +local squad=self:GetSquadron(asset.assignment) +if squad then +if asset.assignment==assignment then +local nunits=#asset.template.units +local text=string.format("Adding asset to squadron %s: assignment=%s, type=%s, attribute=%s, nunits=%d %s",squad.name,assignment,asset.unittype,asset.attribute,nunits,tostring(squad.ngrouping)) +self:T(self.lid..text) +if squad.ngrouping then +local template=asset.template +local N=math.max(#template.units,squad.ngrouping) +for i=1,N do +local unit=template.units[i] +if i>nunits then +table.insert(template.units,UTILS.DeepCopy(template.units[1])) +end +if squad.ngroupingnunits then +unit=nil +end +end +asset.nunits=squad.ngrouping +end +squad:GetCallsign(asset) +squad:GetModex(asset) +asset.spawngroupname=string.format("%s_AID-%d",squad.name,asset.uid) +squad:AddAsset(asset) +else +self:SquadAssetReturned(squad,asset) +end +end +end +function AIRWING:onafterSquadAssetReturned(From,Event,To,Squadron,Asset) +self:T(self.lid..string.format("Asset %s from squadron %s returned! asset.assignment=\"%s\"",Asset.spawngroupname,Squadron.name,tostring(Asset.assignment))) +if Asset.flightgroup and not Asset.flightgroup:IsStopped()then +Asset.flightgroup:Stop() +end +self:ReturnPayloadFromAsset(Asset) +if Asset.tacan then +Squadron:ReturnTacan(Asset.tacan) +end +Asset.Treturned=timer.getAbsTime() +end +function AIRWING:onafterAssetSpawned(From,Event,To,group,asset,request) +self:GetParent(self).onafterAssetSpawned(self,From,Event,To,group,asset,request) +local flightgroup=self:_CreateFlightGroup(asset) +asset.flightgroup=flightgroup +asset.requested=nil +asset.Treturned=nil +local squadron=self:GetSquadronOfAsset(asset) +local Tacan=squadron:FetchTacan() +if Tacan then +asset.tacan=Tacan +end +local radioFreq,radioModu=squadron:GetRadio() +if radioFreq then +flightgroup:SwitchRadio(radioFreq,radioModu) +end +if squadron.fuellow then +flightgroup:SetFuelLowThreshold(squadron.fuellow) +end +if squadron.fuellowRefuel then +flightgroup:SetFuelLowRefuel(squadron.fuellowRefuel) +end +local mission=self:GetMissionByID(request.assignment) +if mission then +if Tacan then +mission:SetTACAN(Tacan,Morse,UnitName,Band) +end +asset.flightgroup:AddMission(mission) +self:FlightOnMission(flightgroup,mission) +else +if Tacan then +flightgroup:SwitchTACAN(Tacan,Morse,UnitName,Band) +end +end +if self.wingcommander and self.wingcommander.chief then +self.wingcommander.chief.detectionset:AddGroup(asset.flightgroup.group) +end +end +function AIRWING:onafterAssetDead(From,Event,To,asset,request) +self:GetParent(self).onafterAssetDead(self,From,Event,To,asset,request) +if self.wingcommander and self.wingcommander.chief then +self.wingcommander.chief.detectionset:RemoveGroupsByName({asset.spawngroupname}) +end +end +function AIRWING:onafterDestroyed(From,Event,To) +self:I(self.lid.."Airwing warehouse destroyed!") +for _,_mission in pairs(self.missionqueue)do +local mission=_mission +mission:Cancel() +end +for _,_squadron in pairs(self.squadrons)do +local squadron=_squadron +squadron:Stop() +end +self:GetParent(self).onafterDestroyed(self,From,Event,To) +end +function AIRWING:onafterRequest(From,Event,To,Request) +local assets=Request.cargoassets +local Mission=self:GetMissionByID(Request.assignment) +if Mission and assets then +for _,_asset in pairs(assets)do +local asset=_asset +end +end +self:GetParent(self).onafterRequest(self,From,Event,To,Request) +end +function AIRWING:onafterSelfRequest(From,Event,To,groupset,request) +self:GetParent(self).onafterSelfRequest(self,From,Event,To,groupset,request) +local mission=self:GetMissionByID(request.assignment) +for _,_asset in pairs(request.assets)do +local asset=_asset +end +for _,_group in pairs(groupset:GetSet())do +local group=_group +end +end +function AIRWING:_CreateFlightGroup(asset) +local flightgroup=FLIGHTGROUP:New(asset.spawngroupname) +flightgroup:SetAirwing(self) +flightgroup.squadron=self:GetSquadronOfAsset(asset) +flightgroup.homebase=self.airbase +return flightgroup +end +function AIRWING:IsAssetOnMission(asset,MissionTypes) +if MissionTypes then +if type(MissionTypes)~="table"then +MissionTypes={MissionTypes} +end +else +MissionTypes=AUFTRAG.Type +end +if asset.flightgroup and asset.flightgroup:IsAlive()then +for _,_mission in pairs(asset.flightgroup.missionqueue or{})do +local mission=_mission +if mission:IsNotOver()then +local status=mission:GetGroupStatus(asset.flightgroup) +if(status==AUFTRAG.GroupStatus.STARTED or status==AUFTRAG.GroupStatus.EXECUTING)and self:CheckMissionType(mission.type,MissionTypes)then +return true +end +end +end +end +return false +end +function AIRWING:GetAssetCurrentMission(asset) +if asset.flightgroup then +return asset.flightgroup:GetMissionCurrent() +end +return nil +end +function AIRWING:CountPayloadsInStock(MissionTypes,UnitTypes,Payloads) +if MissionTypes then +if type(MissionTypes)=="string"then +MissionTypes={MissionTypes} +end +end +if UnitTypes then +if type(UnitTypes)=="string"then +UnitTypes={UnitTypes} +end +end +local function _checkUnitTypes(payload) +if UnitTypes then +for _,unittype in pairs(UnitTypes)do +if unittype==payload.aircrafttype then +return true +end +end +else +return true +end +return false +end +local function _checkPayloads(payload) +if Payloads then +for _,Payload in pairs(Payloads)do +if Payload.uid==payload.uid then +return true +end +end +else +return nil +end +return false +end +local n=0 +for _,_payload in pairs(self.payloads)do +local payload=_payload +for _,MissionType in pairs(MissionTypes)do +local specialpayload=_checkPayloads(payload) +local compatible=self:CheckMissionCapability(MissionType,payload.capabilities) +local goforit=specialpayload or(specialpayload==nil and compatible) +if goforit and _checkUnitTypes(payload)then +if payload.unlimited then +return 999 +else +n=n+payload.navail +end +end +end +end +return n +end +function AIRWING:CountMissionsInQueue(MissionTypes) +MissionTypes=MissionTypes or AUFTRAG.Type +local N=0 +for _,_mission in pairs(self.missionqueue)do +local mission=_mission +if mission:IsNotOver()and self:CheckMissionType(mission.type,MissionTypes)then +N=N+1 +end +end +return N +end +function AIRWING:CountAssets() +local N=0 +for _,_squad in pairs(self.squadrons)do +local squad=_squad +N=N+#squad.assets +end +return N +end +function AIRWING:CountAssetsOnMission(MissionTypes,Squadron) +local Nq=0 +local Np=0 +for _,_mission in pairs(self.missionqueue)do +local mission=_mission +if self:CheckMissionType(mission.type,MissionTypes or AUFTRAG.Type)then +for _,_asset in pairs(mission.assets or{})do +local asset=_asset +if Squadron==nil or Squadron.name==asset.squadname then +local request,isqueued=self:GetRequestByID(mission.requestID) +if isqueued then +Nq=Nq+1 +else +Np=Np+1 +end +end +end +end +end +return Np+Nq,Np,Nq +end +function AIRWING:GetAssetsOnMission(MissionTypes) +local assets={} +local Np=0 +for _,_mission in pairs(self.missionqueue)do +local mission=_mission +if self:CheckMissionType(mission.type,MissionTypes)then +for _,_asset in pairs(mission.assets or{})do +local asset=_asset +table.insert(assets,asset) +end +end +end +return assets +end +function AIRWING:GetAircraftTypes(onlyactive,squadrons) +local unittypes={} +for _,_squadron in pairs(squadrons or self.squadrons)do +local squadron=_squadron +if(not onlyactive)or squadron:IsOnDuty()then +local gotit=false +for _,unittype in pairs(unittypes)do +if squadron.aircrafttype==unittype then +gotit=true +break +end +end +if not gotit then +table.insert(unittypes,squadron.aircrafttype) +end +end +end +return unittypes +end +function AIRWING:CanMission(Mission) +local Can=true +local Assets={} +local squadrons=Mission.squadrons or self.squadrons +local unittypes=self:GetAircraftTypes(true,squadrons) +local Npayloads=self:CountPayloadsInStock(Mission.type,unittypes,Mission.payloads) +if Npayloads#Assets then +self:T(self.lid..string.format("INFO: Not enough assets available! Got %d but need at least %d",#Assets,Mission.nassets)) +Can=false +end +return Can,Assets +end +function AIRWING:RecruitAssets(Mission) +end +function AIRWING:CheckMissionType(MissionType,PossibleTypes) +if type(PossibleTypes)=="string"then +PossibleTypes={PossibleTypes} +end +for _,canmission in pairs(PossibleTypes)do +if canmission==MissionType then +return true +end +end +return false +end +function AIRWING:CheckMissionCapability(MissionType,Capabilities) +for _,cap in pairs(Capabilities)do +local capability=cap +if capability.MissionType==MissionType then +return true +end +end +return false +end +function AIRWING:GetPayloadPeformance(Payload,MissionType) +if Payload then +for _,Capability in pairs(Payload.capabilities)do +local capability=Capability +if capability.MissionType==MissionType then +return capability.Performance +end +end +else +self:E(self.lid.."ERROR: Payload is nil!") +end +return-1 +end +function AIRWING:GetPayloadMissionTypes(Payload) +local missiontypes={} +for _,Capability in pairs(Payload.capabilities)do +local capability=Capability +table.insert(missiontypes,capability.MissionType) +end +return missiontypes +end +function AIRWING:GetMissionByID(mid) +for _,_mission in pairs(self.missionqueue)do +local mission=_mission +if mission.auftragsnummer==tonumber(mid)then +return mission +end +end +return nil +end +function AIRWING:GetMissionFromRequestID(RequestID) +for _,_mission in pairs(self.missionqueue)do +local mission=_mission +if mission.requestID and mission.requestID==RequestID then +return mission +end +end +return nil +end +function AIRWING:GetMissionFromRequest(Request) +return self:GetMissionFromRequestID(Request.uid) +end +INTEL={ +ClassName="INTEL", +verbose=0, +lid=nil, +alias=nil, +filterCategory={}, +detectionset=nil, +Contacts={}, +ContactsLost={}, +ContactsUnknown={}, +Clusters={}, +clustercounter=1, +clusterradius=15, +} +INTEL.version="0.2.1" +function INTEL:New(DetectionSet,Coalition,Alias) +local self=BASE:Inherit(self,FSM:New()) +self.detectionset=DetectionSet or SET_GROUP:New() +if Coalition and type(Coalition)=="string"then +if Coalition=="blue"then +Coalition=coalition.side.BLUE +elseif Coalition=="red"then +Coalition=coalition.side.RED +elseif Coalition=="neutral"then +Coalition=coalition.side.NEUTRAL +else +self:E("ERROR: Unknown coalition in INTEL!") +end +end +self.coalition=Coalition or DetectionSet:CountAlive()>0 and DetectionSet:GetFirst():GetCoalition()or nil +if self.coalition then +local coalitionname=UTILS.GetCoalitionName(self.coalition):lower() +self.detectionset:FilterCoalitions(coalitionname) +end +self.detectionset:FilterOnce() +if Alias then +self.alias=tostring(Alias) +else +self.alias="SPECTRE" +if self.coalition then +if self.coalition==coalition.side.RED then +self.alias="KGB" +elseif self.coalition==coalition.side.BLUE then +self.alias="CIA" +end +end +end +self.lid=string.format("INTEL %s (%s) | ",self.alias,self.coalition and UTILS.GetCoalitionName(self.coalition)or"unknown") +self:SetStartState("Stopped") +self:AddTransition("Stopped","Start","Running") +self:AddTransition("*","Status","*") +self:AddTransition("*","Detect","*") +self:AddTransition("*","NewContact","*") +self:AddTransition("*","LostContact","*") +self:AddTransition("*","NewCluster","*") +self:AddTransition("*","LostCluster","*") +self:SetForgetTime() +self:SetAcceptZones() +self:SetRejectZones() +return self +end +function INTEL:SetAcceptZones(AcceptZoneSet) +self.acceptzoneset=AcceptZoneSet or SET_ZONE:New() +return self +end +function INTEL:AddAcceptZone(AcceptZone) +self.acceptzoneset:AddZone(AcceptZone) +return self +end +function INTEL:RemoveAcceptZone(AcceptZone) +self.acceptzoneset:Remove(AcceptZone:GetName(),true) +return self +end +function INTEL:SetRejectZones(RejectZoneSet) +self.rejectzoneset=RejectZoneSet or SET_ZONE:New() +return self +end +function INTEL:AddRejectZone(RejectZone) +self.rejectzoneset:AddZone(RejectZone) +return self +end +function INTEL:RemoveRejectZone(RejectZone) +self.rejectzoneset:Remove(RejectZone:GetName(),true) +return self +end +function INTEL:SetForgetTime(TimeInterval) +self.dTforget=TimeInterval or 120 +return self +end +function INTEL:SetFilterCategory(Categories) +if type(Categories)~="table"then +Categories={Categories} +end +self.filterCategory=Categories +local text="Filter categories: " +for _,category in pairs(self.filterCategory)do +text=text..string.format("%d,",category) +end +self:T(self.lid..text) +return self +end +function INTEL:FilterCategoryGroup(GroupCategories) +if type(GroupCategories)~="table"then +GroupCategories={GroupCategories} +end +self.filterCategoryGroup=GroupCategories +local text="Filter group categories: " +for _,category in pairs(self.filterCategoryGroup)do +text=text..string.format("%d,",category) +end +self:T(self.lid..text) +return self +end +function INTEL:SetClusterAnalysis(Switch,Markers) +self.clusteranalysis=Switch +self.clustermarkers=Markers +return self +end +function INTEL:SetVerbosity(Verbosity) +self.verbose=Verbosity or 2 +return self +end +function INTEL:AddMissionToContact(Contact,Mission) +if Mission and Contact then +Contact.mission=Mission +end +return self +end +function INTEL:AddMissionToCluster(Cluster,Mission) +if Mission and Cluster then +Cluster.mission=Mission +end +return self +end +function INTEL:SetClusterRadius(radius) +local radius=radius or 15 +self.clusterradius=radius +return self +end +function INTEL:onafterStart(From,Event,To) +local text=string.format("Starting INTEL v%s",self.version) +self:I(self.lid..text) +self:__Status(-math.random(10)) +end +function INTEL:onafterStatus(From,Event,To) +local fsmstate=self:GetState() +self.ContactsLost={} +self.ContactsUnknown={} +self:UpdateIntel() +local Ncontacts=#self.Contacts +local Nclusters=#self.Clusters +if self.verbose>=1 then +local text=string.format("Status %s [Agents=%s]: Contacts=%d, Clusters=%d, New=%d, Lost=%d",fsmstate,self.detectionset:CountAlive(),Ncontacts,Nclusters,#self.ContactsUnknown,#self.ContactsLost) +self:I(self.lid..text) +end +if self.verbose>=2 and Ncontacts>0 then +local text="Detected Contacts:" +for _,_contact in pairs(self.Contacts)do +local contact=_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) +if contact.mission then +local mission=contact.mission +text=text..string.format(" mission name=%s type=%s target=%s",mission.name,mission.type,mission:GetTargetName()or"unkown") +end +end +self:I(self.lid..text) +end +self:__Status(-60) +end +function INTEL:UpdateIntel() +local DetectedUnits={} +local RecceDetecting={} +for _,_group in pairs(self.detectionset.Set or{})do +local group=_group +if group and group:IsAlive()then +for _,_recce in pairs(group:GetUnits())do +local recce=_recce +self:GetDetectedUnits(recce,DetectedUnits,RecceDetecting) +end +end +end +local remove={} +for unitname,_unit in pairs(DetectedUnits)do +local unit=_unit +if self.acceptzoneset:Count()>0 then +local inzone=false +for _,_zone in pairs(self.acceptzoneset.Set)do +local zone=_zone +if unit:IsInZone(zone)then +inzone=true +break +end +end +if not inzone then +table.insert(remove,unitname) +end +end +if self.rejectzoneset:Count()>0 then +local inzone=false +for _,_zone in pairs(self.rejectzoneset.Set)do +local zone=_zone +if unit:IsInZone(zone)then +inzone=true +break +end +end +if inzone then +table.insert(remove,unitname) +end +end +if#self.filterCategory>0 then +local unitcategory=unit:GetUnitCategory() +local keepit=false +for _,filtercategory in pairs(self.filterCategory)do +if unitcategory==filtercategory then +keepit=true +break +end +end +if not keepit then +self:T(self.lid..string.format("Removing unit %s category=%d",unitname,unit:GetCategory())) +table.insert(remove,unitname) +end +end +end +for _,unitname in pairs(remove)do +DetectedUnits[unitname]=nil +end +local DetectedGroups={} +local RecceGroups={} +for unitname,_unit in pairs(DetectedUnits)do +local unit=_unit +local group=unit:GetGroup() +if group then +local groupname=group:GetName() +DetectedGroups[groupname]=group +RecceGroups[groupname]=RecceDetecting[unitname] +end +end +self:CreateDetectedItems(DetectedGroups,RecceGroups) +if self.clusteranalysis then +self:PaintPicture() +end +end +function INTEL:CreateDetectedItems(DetectedGroups,RecceDetecting) +self:F({RecceDetecting=RecceDetecting}) +local Tnow=timer.getAbsTime() +for groupname,_group in pairs(DetectedGroups)do +local group=_group +local detecteditem=self:GetContactByName(groupname) +if detecteditem then +detecteditem.Tdetected=Tnow +detecteditem.position=group:GetCoordinate() +detecteditem.velocity=group:GetVelocityVec3() +detecteditem.speed=group:GetVelocityMPS() +else +local item={} +item.groupname=groupname +item.group=group +item.Tdetected=Tnow +item.typename=group:GetTypeName() +item.attribute=group:GetAttribute() +item.category=group:GetCategory() +item.categoryname=group:GetCategoryName() +item.threatlevel=group:GetThreatLevel() +item.position=group:GetCoordinate() +item.velocity=group:GetVelocityVec3() +item.speed=group:GetVelocityMPS() +item.recce=RecceDetecting[groupname] +self:T(string.format("%s group detect by %s/%s",groupname,RecceDetecting[groupname]or"unknonw",item.recce or"unknown")) +self:AddContact(item) +self:NewContact(item) +end +end +for i=#self.Contacts,1,-1 do +local item=self.Contacts[i] +if self:_CheckContactLost(item)then +self:LostContact(item) +self:RemoveContact(item) +end +end +end +function INTEL:GetDetectedUnits(Unit,DetectedUnits,RecceDetecting,DetectVisual,DetectOptical,DetectRadar,DetectIRST,DetectRWR,DetectDLINK) +local detectedtargets=Unit:GetDetectedTargets(DetectVisual,DetectOptical,DetectRadar,DetectIRST,DetectRWR,DetectDLINK) +local reccename=Unit:GetName() +for DetectionObjectID,Detection in pairs(detectedtargets or{})do +local DetectedObject=Detection.object +if DetectedObject and DetectedObject:isExist()and DetectedObject.id_<50000000 then +local unit=UNIT:Find(DetectedObject) +if unit and unit:IsAlive()then +local unitname=unit:GetName() +DetectedUnits[unitname]=unit +RecceDetecting[unitname]=reccename +self:T(string.format("Unit %s detect by %s",unitname,reccename)) +end +end +end +end +function INTEL:onafterNewContact(From,Event,To,Contact) +self:F(self.lid..string.format("NEW contact %s",Contact.groupname)) +table.insert(self.ContactsUnknown,Contact) +end +function INTEL:onafterLostContact(From,Event,To,Contact) +self:F(self.lid..string.format("LOST contact %s",Contact.groupname)) +table.insert(self.ContactsLost,Contact) +end +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)) +end +function INTEL:onafterLostCluster(From,Event,To,Cluster,Mission) +local text=self.lid..string.format("LOST cluster %d",Cluster.index) +if Mission then +local mission=Mission +text=text..string.format(" mission name=%s type=%s target=%s",mission.name,mission.type,mission:GetTargetName()or"unkown") +end +self:T(text) +end +function INTEL:GetContactByName(groupname) +for i,_contact in pairs(self.Contacts)do +local contact=_contact +if contact.groupname==groupname then +return contact +end +end +return nil +end +function INTEL:AddContact(Contact) +table.insert(self.Contacts,Contact) +end +function INTEL:RemoveContact(Contact) +for i,_contact in pairs(self.Contacts)do +local contact=_contact +if contact.groupname==Contact.groupname then +table.remove(self.Contacts,i) +end +end +end +function INTEL:_CheckContactLost(Contact) +if Contact.group==nil or not Contact.group:IsAlive()then +return true +end +local dT=timer.getAbsTime()-Contact.Tdetected +local dTforget=self.dTforget +if Contact.category==Group.Category.GROUND then +dTforget=60*60*2 +elseif Contact.category==Group.Category.AIRPLANE then +dTforget=60*10 +elseif Contact.category==Group.Category.HELICOPTER then +dTforget=60*20 +elseif Contact.category==Group.Category.SHIP then +dTforget=60*60 +elseif Contact.category==Group.Category.TRAIN then +dTforget=60*60 +end +if dT>dTforget then +return true +else +return false +end +end +function INTEL:PaintPicture() +for _,_contact in pairs(self.ContactsLost)do +local contact=_contact +local cluster=self:GetClusterOfContact(contact) +if cluster then +self:RemoveContactFromCluster(contact,cluster) +end +end +local ClusterSet={} +for _i,_cluster in pairs(self.Clusters)do +if(_cluster.size>0)and(self:ClusterCountUnits(_cluster)>0)then +table.insert(ClusterSet,_cluster) +else +local mission=_cluster.mission or nil +local marker=_cluster.marker +if marker then +marker:Remove() +end +self:LostCluster(_cluster,mission) +end +end +self.Clusters=ClusterSet +self:_UpdateClusterPositions() +for _,_contact in pairs(self.Contacts)do +local contact=_contact +self:T(string.format("Paint Picture: checking for %s",contact.groupname)) +local isincluster=self:CheckContactInClusters(contact) +local currentcluster=self:GetClusterOfContact(contact) +if currentcluster then +local isconnected=self:IsContactConnectedToCluster(contact,currentcluster) +if(not isconnected)and(currentcluster.size>1)then +local cluster=self:IsContactPartOfAnyClusters(contact) +if cluster then +self:AddContactToCluster(contact,cluster) +else +local newcluster=self:CreateCluster(contact.position) +self:AddContactToCluster(contact,newcluster) +self:NewCluster(contact,newcluster) +end +end +else +local cluster=self:IsContactPartOfAnyClusters(contact) +if cluster then +self:AddContactToCluster(contact,cluster) +else +local newcluster=self:CreateCluster(contact.position) +self:AddContactToCluster(contact,newcluster) +self:NewCluster(contact,newcluster) +end +end +end +if self.clustermarkers then +for _,_cluster in pairs(self.Clusters)do +local cluster=_cluster +local coordinate=self:GetClusterCoordinate(cluster) +self:UpdateClusterMarker(cluster) +end +end +end +function INTEL:CreateCluster(coordinate) +local cluster={} +cluster.index=self.clustercounter +cluster.coordinate=coordinate +cluster.threatlevelSum=0 +cluster.threatlevelMax=0 +cluster.size=0 +cluster.Contacts={} +table.insert(self.Clusters,cluster) +self.clustercounter=self.clustercounter+1 +return cluster +end +function INTEL:AddContactToCluster(contact,cluster) +if contact and cluster then +table.insert(cluster.Contacts,contact) +cluster.threatlevelSum=cluster.threatlevelSum+contact.threatlevel +cluster.size=cluster.size+1 +end +end +function INTEL:RemoveContactFromCluster(contact,cluster) +if contact and cluster then +for i,_contact in pairs(cluster.Contacts)do +local Contact=_contact +if Contact.groupname==contact.groupname then +cluster.threatlevelSum=cluster.threatlevelSum-contact.threatlevel +cluster.size=cluster.size-1 +table.remove(cluster.Contacts,i) +return +end +end +end +end +function INTEL:CalcClusterThreatlevelSum(cluster) +local threatlevel=0 +for _,_contact in pairs(cluster.Contacts)do +local contact=_contact +threatlevel=threatlevel+contact.threatlevel +end +cluster.threatlevelSum=threatlevel +return threatlevel +end +function INTEL:CalcClusterThreatlevelAverage(cluster) +local threatlevel=self:CalcClusterThreatlevelSum(cluster) +threatlevel=threatlevel/cluster.size +cluster.threatlevelAve=threatlevel +return threatlevel +end +function INTEL:CalcClusterThreatlevelMax(cluster) +local threatlevel=0 +for _,_contact in pairs(cluster.Contacts)do +local contact=_contact +if contact.threatlevel>threatlevel then +threatlevel=contact.threatlevel +end +end +cluster.threatlevelMax=threatlevel +return threatlevel +end +function INTEL:CheckContactInClusters(contact) +for _,_cluster in pairs(self.Clusters)do +local cluster=_cluster +for _,_contact in pairs(cluster.Contacts)do +local Contact=_contact +if Contact.groupname==contact.groupname then +return true +end +end +end +return false +end +function INTEL:IsContactConnectedToCluster(contact,cluster) +for _,_contact in pairs(cluster.Contacts)do +local Contact=_contact +if Contact.groupname~=contact.groupname then +local dist=Contact.position:DistanceFromPointVec2(contact.position) +local radius=self.clusterradius or 15 +if dist1000 then +return true +else +return false +end +end +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)) +end +end +function INTEL:ClusterCountUnits(Cluster) +local unitcount=0 +for _,_group in pairs(Cluster.Contacts)do +unitcount=unitcount+_group.group:CountAliveUnits() +end +return unitcount +end +function INTEL:UpdateClusterMarker(cluster) +local unitcount=self:ClusterCountUnits(cluster) +local text=string.format("Cluster #%d. Size %d, Units %d, TLsum=%d",cluster.index,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 +else +local refresh=false +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 +refresh=true +end +if refresh then +cluster.marker:Refresh() +end +end +return self +end +AI_BALANCER={ +ClassName="AI_BALANCER", +PatrolZones={}, +AIGroups={}, +Earliest=5, +Latest=60, +} +function AI_BALANCER:New(SetClient,SpawnAI) +local self=BASE:Inherit(self,FSM_SET:New(SET_GROUP:New())) +self:SetStartState("None") +self:AddTransition("*","Monitor","Monitoring") +self:AddTransition("*","Spawn","Spawning") +self:AddTransition("Spawning","Spawned","Spawned") +self:AddTransition("*","Destroy","Destroying") +self:AddTransition("*","Return","Returning") +self.SetClient=SetClient +self.SetClient:FilterOnce() +self.SpawnAI=SpawnAI +self.SpawnQueue={} +self.ToNearestAirbase=false +self.ToHomeAirbase=false +self:__Monitor(1) +return self +end +function AI_BALANCER:InitSpawnInterval(Earliest,Latest) +self.Earliest=Earliest +self.Latest=Latest +return self +end +function AI_BALANCER:ReturnToNearestAirbases(ReturnThresholdRange,ReturnAirbaseSet) +self.ToNearestAirbase=true +self.ReturnThresholdRange=ReturnThresholdRange +self.ReturnAirbaseSet=ReturnAirbaseSet +end +function AI_BALANCER:ReturnToHomeAirbase(ReturnThresholdRange) +self.ToHomeAirbase=true +self.ReturnThresholdRange=ReturnThresholdRange +end +function AI_BALANCER:onenterSpawning(SetGroup,From,Event,To,ClientName) +local AIGroup=self.SpawnAI:Spawn() +if AIGroup then +AIGroup:T({"Spawning new AIGroup",ClientName=ClientName}) +SetGroup:Remove(ClientName) +SetGroup:Add(ClientName,AIGroup) +self.SpawnQueue[ClientName]=nil +self:Spawned(AIGroup) +end +end +function AI_BALANCER:onenterDestroying(SetGroup,From,Event,To,ClientName,AIGroup) +AIGroup:Destroy() +SetGroup:Flush(self) +SetGroup:Remove(ClientName) +SetGroup:Flush(self) +end +function AI_BALANCER:onenterReturning(SetGroup,From,Event,To,AIGroup) +local AIGroupTemplate=AIGroup:GetTemplate() +if self.ToHomeAirbase==true then +local WayPointCount=#AIGroupTemplate.route.points +local SwitchWayPointCommand=AIGroup:CommandSwitchWayPoint(1,WayPointCount,1) +AIGroup:SetCommand(SwitchWayPointCommand) +AIGroup:MessageToRed("Returning to home base ...",30) +else +local PointVec2=POINT_VEC2:New(AIGroup:GetVec2().x,AIGroup:GetVec2().y) +local ClosestAirbase=self.ReturnAirbaseSet:FindNearestAirbaseFromPointVec2(PointVec2) +self:T(ClosestAirbase.AirbaseName) +AIGroup:RouteRTB(ClosestAirbase) +end +end +function AI_BALANCER:onenterMonitoring(SetGroup) +self:T2({self.SetClient:Count()}) +self.SetClient:ForEachClient( +function(Client) +self:T3(Client.ClientName) +local AIGroup=self.Set:Get(Client.UnitName) +if AIGroup then self:T({AIGroup=AIGroup:GetName(),IsAlive=AIGroup:IsAlive()})end +if Client:IsAlive()==true then +if AIGroup and AIGroup:IsAlive()==true then +if self.ToNearestAirbase==false and self.ToHomeAirbase==false then +self:Destroy(Client.UnitName,AIGroup) +else +local PlayerInRange={Value=false} +local RangeZone=ZONE_RADIUS:New('RangeZone',AIGroup:GetVec2(),self.ReturnThresholdRange) +self:T2(RangeZone) +_DATABASE:ForEachPlayerUnit( +function(RangeTestUnit,RangeZone,AIGroup,PlayerInRange) +self:T2({PlayerInRange,RangeTestUnit.UnitName,RangeZone.ZoneName}) +if RangeTestUnit:IsInZone(RangeZone)==true then +self:T2("in zone") +if RangeTestUnit:GetCoalition()~=AIGroup:GetCoalition()then +self:T2("in range") +PlayerInRange.Value=true +end +end +end, +function(RangeZone,AIGroup,PlayerInRange) +if PlayerInRange.Value==false then +self:Return(AIGroup) +end +end +,RangeZone,AIGroup,PlayerInRange +) +end +self.Set:Remove(Client.UnitName) +end +else +if not AIGroup or not AIGroup:IsAlive()==true then +self:T("Client "..Client.UnitName.." not alive.") +self:T({Queue=self.SpawnQueue[Client.UnitName]}) +if not self.SpawnQueue[Client.UnitName]then +self:__Spawn(math.random(self.Earliest,self.Latest),Client.UnitName) +self.SpawnQueue[Client.UnitName]=true +self:T("New AI Spawned for Client "..Client.UnitName) +end +end +end +return true +end +) +self:__Monitor(10) +end +AI_AIR={ +ClassName="AI_AIR", +} +AI_AIR.TaskDelay=0.5 +function AI_AIR:New(AIGroup) +local self=BASE:Inherit(self,FSM_CONTROLLABLE:New()) +self:SetControllable(AIGroup) +self:SetStartState("Stopped") +self:AddTransition("*","Queue","Queued") +self:AddTransition("*","Start","Started") +self:AddTransition("*","Stop","Stopped") +self:AddTransition("*","Status","*") +self:AddTransition("*","RTB","*") +self:AddTransition("Patrolling","Refuel","Refuelling") +self:AddTransition("*","Takeoff","Airborne") +self:AddTransition("*","Return","Returning") +self:AddTransition("*","Hold","Holding") +self:AddTransition("*","Home","Home") +self:AddTransition("*","LostControl","LostControl") +self:AddTransition("*","Fuel","Fuel") +self:AddTransition("*","Damaged","Damaged") +self:AddTransition("*","Eject","*") +self:AddTransition("*","Crash","Crashed") +self:AddTransition("*","PilotDead","*") +self.IdleCount=0 +return self +end +function GROUP:OnEventTakeoff(EventData,Fsm) +Fsm:Takeoff() +self:UnHandleEvent(EVENTS.Takeoff) +end +function AI_AIR:SetDispatcher(Dispatcher) +self.Dispatcher=Dispatcher +end +function AI_AIR:GetDispatcher() +return self.Dispatcher +end +function AI_AIR:SetTargetDistance(Coordinate) +local CurrentCoord=self.Controllable:GetCoordinate() +self.TargetDistance=CurrentCoord:Get2DDistance(Coordinate) +self.ClosestTargetDistance=(not self.ClosestTargetDistance or self.ClosestTargetDistance>self.TargetDistance)and self.TargetDistance or self.ClosestTargetDistance +end +function AI_AIR:ClearTargetDistance() +self.TargetDistance=nil +self.ClosestTargetDistance=nil +end +function AI_AIR:SetSpeed(PatrolMinSpeed,PatrolMaxSpeed) +self:F2({PatrolMinSpeed,PatrolMaxSpeed}) +self.PatrolMinSpeed=PatrolMinSpeed +self.PatrolMaxSpeed=PatrolMaxSpeed +end +function AI_AIR:SetRTBSpeed(RTBMinSpeed,RTBMaxSpeed) +self:F({RTBMinSpeed,RTBMaxSpeed}) +self.RTBMinSpeed=RTBMinSpeed +self.RTBMaxSpeed=RTBMaxSpeed +end +function AI_AIR:SetAltitude(PatrolFloorAltitude,PatrolCeilingAltitude) +self:F2({PatrolFloorAltitude,PatrolCeilingAltitude}) +self.PatrolFloorAltitude=PatrolFloorAltitude +self.PatrolCeilingAltitude=PatrolCeilingAltitude +end +function AI_AIR:SetHomeAirbase(HomeAirbase) +self:F2({HomeAirbase}) +self.HomeAirbase=HomeAirbase +end +function AI_AIR:SetTanker(TankerName) +self:F2({TankerName}) +self.TankerName=TankerName +end +function AI_AIR:SetDisengageRadius(DisengageRadius) +self:F2({DisengageRadius}) +self.DisengageRadius=DisengageRadius +end +function AI_AIR:SetStatusOff() +self:F2() +self.CheckStatus=false +end +function AI_AIR:SetFuelThreshold(FuelThresholdPercentage,OutOfFuelOrbitTime) +self.FuelThresholdPercentage=FuelThresholdPercentage +self.OutOfFuelOrbitTime=OutOfFuelOrbitTime +self.Controllable:OptionRTBBingoFuel(false) +return self +end +function AI_AIR:SetDamageThreshold(PatrolDamageThreshold) +self.PatrolManageDamage=true +self.PatrolDamageThreshold=PatrolDamageThreshold +return self +end +function AI_AIR:onafterStart(Controllable,From,Event,To) +self:__Status(10) +self:HandleEvent(EVENTS.PilotDead,self.OnPilotDead) +self:HandleEvent(EVENTS.Crash,self.OnCrash) +self:HandleEvent(EVENTS.Ejection,self.OnEjection) +Controllable:OptionROEHoldFire() +Controllable:OptionROTVertical() +end +function AI_AIR:onafterReturn(Controllable,From,Event,To) +self:__RTB(self.TaskDelay) +end +function AI_AIR:onbeforeStatus() +return self.CheckStatus +end +function AI_AIR:onafterStatus() +if self.Controllable and self.Controllable:IsAlive()then +local RTB=false +local DistanceFromHomeBase=self.HomeAirbase:GetCoordinate():Get2DDistance(self.Controllable:GetCoordinate()) +if not self:Is("Holding")and not self:Is("Returning")then +local DistanceFromHomeBase=self.HomeAirbase:GetCoordinate():Get2DDistance(self.Controllable:GetCoordinate()) +if DistanceFromHomeBase>self.DisengageRadius then +self:I(self.Controllable:GetName().." is too far from home base, RTB!") +self:Hold(300) +RTB=false +end +end +if not self:Is("Fuel")and not self:Is("Home")and not self:is("Refuelling")then +local Fuel=self.Controllable:GetFuelMin() +if Fuel=10 then +if Damage~=InitialLife then +self:Damaged() +else +self:I(self.Controllable:GetName().." control lost! ") +self:LostControl() +end +else +self.IdleCount=self.IdleCount+1 +end +end +else +self.IdleCount=0 +end +if RTB==true then +self:__RTB(self.TaskDelay) +end +if not self:Is("Home")then +self:__Status(10) +end +end +end +function AI_AIR.RTBRoute(AIGroup,Fsm) +AIGroup:F({"AI_AIR.RTBRoute:",AIGroup:GetName()}) +if AIGroup:IsAlive()then +Fsm:RTB() +end +end +function AI_AIR.RTBHold(AIGroup,Fsm) +AIGroup:F({"AI_AIR.RTBHold:",AIGroup:GetName()}) +if AIGroup:IsAlive()then +Fsm:__RTB(Fsm.TaskDelay) +Fsm:Return() +local Task=AIGroup:TaskOrbitCircle(4000,400) +AIGroup:SetTask(Task) +end +end +function AI_AIR:onafterRTB(AIGroup,From,Event,To) +self:F({AIGroup,From,Event,To}) +if AIGroup and AIGroup:IsAlive()then +self:I("Group "..AIGroup:GetName().." ... RTB! ( "..self:GetState().." )") +self:ClearTargetDistance() +local EngageRoute={} +local FromCoord=AIGroup:GetCoordinate() +local ToTargetCoord=self.HomeAirbase:GetCoordinate() +local ToTargetVec3=ToTargetCoord:GetVec3() +ToTargetVec3.y=ToTargetCoord:GetLandHeight()+1000 +local ToTargetCoord2=COORDINATE:NewFromVec3(ToTargetVec3) +if not self.RTBMinSpeed or not self.RTBMaxSpeed then +local RTBSpeedMax=AIGroup:GetSpeedMax() +self:SetRTBSpeed(RTBSpeedMax*0.5,RTBSpeedMax*0.6) +end +local RTBSpeed=math.random(self.RTBMinSpeed,self.RTBMaxSpeed) +local Distance=FromCoord:Get2DDistance(ToTargetCoord2) +local ToAirbaseCoord=ToTargetCoord2 +if Distance<5000 then +self:I("RTB and near the airbase!") +self:Home() +return +end +if not AIGroup:InAir()==true then +self:I("Not anymore in the air, considered Home.") +self:Home() +return +end +local FromRTBRoutePoint=FromCoord:WaypointAir( +self.PatrolAltType, +POINT_VEC3.RoutePointType.TurningPoint, +POINT_VEC3.RoutePointAction.TurningPoint, +RTBSpeed, +true +) +local ToRTBRoutePoint=ToAirbaseCoord:WaypointAir( +self.PatrolAltType, +POINT_VEC3.RoutePointType.TurningPoint, +POINT_VEC3.RoutePointAction.TurningPoint, +RTBSpeed, +true +) +EngageRoute[#EngageRoute+1]=FromRTBRoutePoint +EngageRoute[#EngageRoute+1]=ToRTBRoutePoint +local Tasks={} +Tasks[#Tasks+1]=AIGroup:TaskFunction("AI_AIR.RTBRoute",self) +EngageRoute[#EngageRoute].task=AIGroup:TaskCombo(Tasks) +AIGroup:OptionROEHoldFire() +AIGroup:OptionROTEvadeFire() +AIGroup:Route(EngageRoute,self.TaskDelay) +end +end +function AI_AIR:onafterHome(AIGroup,From,Event,To) +self:F({AIGroup,From,Event,To}) +self:I("Group "..self.Controllable:GetName().." ... Home! ( "..self:GetState().." )") +if AIGroup and AIGroup:IsAlive()then +end +end +function AI_AIR:onafterHold(AIGroup,From,Event,To,HoldTime) +self:F({AIGroup,From,Event,To}) +self:I("Group "..self.Controllable:GetName().." ... Holding! ( "..self:GetState().." )") +if AIGroup and AIGroup:IsAlive()then +local OrbitTask=AIGroup:TaskOrbitCircle(math.random(self.PatrolFloorAltitude,self.PatrolCeilingAltitude),self.PatrolMinSpeed) +local TimedOrbitTask=AIGroup:TaskControlled(OrbitTask,AIGroup:TaskCondition(nil,nil,nil,nil,HoldTime,nil)) +local RTBTask=AIGroup:TaskFunction("AI_AIR.RTBHold",self) +local OrbitHoldTask=AIGroup:TaskOrbitCircle(4000,self.PatrolMinSpeed) +AIGroup:SetTask(AIGroup:TaskCombo({TimedOrbitTask,RTBTask,OrbitHoldTask}),1) +end +end +function AI_AIR.Resume(AIGroup,Fsm) +AIGroup:I({"AI_AIR.Resume:",AIGroup:GetName()}) +if AIGroup:IsAlive()then +Fsm:__RTB(Fsm.TaskDelay) +end +end +function AI_AIR:onafterRefuel(AIGroup,From,Event,To) +self:F({AIGroup,From,Event,To}) +if AIGroup and AIGroup:IsAlive()then +local Tanker=GROUP:FindByName(self.TankerName) +if Tanker and Tanker:IsAlive()and Tanker:IsAirPlane()then +self:I("Group "..self.Controllable:GetName().." ... Refuelling! State="..self:GetState()..", Refuelling tanker "..self.TankerName) +local RefuelRoute={} +local FromRefuelCoord=AIGroup:GetCoordinate() +local ToRefuelCoord=Tanker:GetCoordinate() +local ToRefuelSpeed=math.random(self.PatrolMinSpeed,self.PatrolMaxSpeed) +local FromRefuelRoutePoint=FromRefuelCoord:WaypointAir(self.PatrolAltType,POINT_VEC3.RoutePointType.TurningPoint,POINT_VEC3.RoutePointAction.TurningPoint,ToRefuelSpeed,true) +local ToRefuelRoutePoint=Tanker:GetCoordinate():WaypointAir(self.PatrolAltType,POINT_VEC3.RoutePointType.TurningPoint,POINT_VEC3.RoutePointAction.TurningPoint,ToRefuelSpeed,true) +self:F({ToRefuelSpeed=ToRefuelSpeed}) +RefuelRoute[#RefuelRoute+1]=FromRefuelRoutePoint +RefuelRoute[#RefuelRoute+1]=ToRefuelRoutePoint +AIGroup:OptionROEHoldFire() +AIGroup:OptionROTEvadeFire() +local classname=self:GetClassName() +if classname=="AI_A2A_CAP"then +classname="AI_AIR_PATROL" +end +env.info("FF refueling classname="..classname) +local Tasks={} +Tasks[#Tasks+1]=AIGroup:TaskRefueling() +Tasks[#Tasks+1]=AIGroup:TaskFunction(classname..".Resume",self) +RefuelRoute[#RefuelRoute].task=AIGroup:TaskCombo(Tasks) +AIGroup:Route(RefuelRoute,self.TaskDelay) +else +self:RTB() +end +end +end +function AI_AIR:onafterDead() +self:SetStatusOff() +end +function AI_AIR:OnCrash(EventData) +if self.Controllable:IsAlive()and EventData.IniDCSGroupName==self.Controllable:GetName()then +if#self.Controllable:GetUnits()==1 then +self:__Crash(self.TaskDelay,EventData) +end +end +end +function AI_AIR:OnEjection(EventData) +if self.Controllable:IsAlive()and EventData.IniDCSGroupName==self.Controllable:GetName()then +self:__Eject(self.TaskDelay,EventData) +end +end +function AI_AIR:OnPilotDead(EventData) +if self.Controllable:IsAlive()and EventData.IniDCSGroupName==self.Controllable:GetName()then +self:__PilotDead(self.TaskDelay,EventData) +end +end +AI_AIR_PATROL={ +ClassName="AI_AIR_PATROL", +} +function AI_AIR_PATROL:New(AI_Air,AIGroup,PatrolZone,PatrolFloorAltitude,PatrolCeilingAltitude,PatrolMinSpeed,PatrolMaxSpeed,PatrolAltType) +local self=BASE:Inherit(self,AI_Air) +local SpeedMax=AIGroup:GetSpeedMax() +self.PatrolZone=PatrolZone +self.PatrolFloorAltitude=PatrolFloorAltitude or 1000 +self.PatrolCeilingAltitude=PatrolCeilingAltitude or 1500 +self.PatrolMinSpeed=PatrolMinSpeed or SpeedMax*0.5 +self.PatrolMaxSpeed=PatrolMaxSpeed or SpeedMax*0.75 +self.PatrolAltType=PatrolAltType or"RADIO" +self:AddTransition({"Started","Airborne","Refuelling"},"Patrol","Patrolling") +self:AddTransition("Patrolling","PatrolRoute","Patrolling") +self:AddTransition("*","Reset","Patrolling") +return self +end +function AI_AIR_PATROL:SetEngageRange(EngageRange) +self:F2() +if EngageRange then +self.EngageRange=EngageRange +else +self.EngageRange=nil +end +end +function AI_AIR_PATROL:SetRaceTrackPattern(LegMin,LegMax,HeadingMin,HeadingMax,DurationMin,DurationMax,CapCoordinates) +self.racetrack=true +self.racetracklegmin=LegMin or 10000 +self.racetracklegmax=LegMax or 15000 +self.racetrackheadingmin=HeadingMin or 0 +self.racetrackheadingmax=HeadingMax or 180 +self.racetrackdurationmin=DurationMin +self.racetrackdurationmax=DurationMax +if self.racetrackdurationmax and not self.racetrackdurationmin then +self.racetrackdurationmin=self.racetrackdurationmax +end +self.racetrackcapcoordinates=CapCoordinates +end +function AI_AIR_PATROL:onafterPatrol(AIPatrol,From,Event,To) +self:F2() +self:ClearTargetDistance() +self:__PatrolRoute(self.TaskDelay) +AIPatrol:OnReSpawn( +function(PatrolGroup) +self:__Reset(self.TaskDelay) +self:__PatrolRoute(self.TaskDelay) +end +) +end +function AI_AIR_PATROL.___PatrolRoute(AIPatrol,Fsm) +AIPatrol:F({"AI_AIR_PATROL.___PatrolRoute:",AIPatrol:GetName()}) +if AIPatrol and AIPatrol:IsAlive()then +Fsm:PatrolRoute() +end +end +function AI_AIR_PATROL:onafterPatrolRoute(AIPatrol,From,Event,To) +self:F2() +if From=="RTB"then +return +end +if AIPatrol and AIPatrol:IsAlive()then +local PatrolRoute={} +local CurrentCoord=AIPatrol:GetCoordinate() +local altitude=math.random(self.PatrolFloorAltitude,self.PatrolCeilingAltitude) +local ToTargetCoord=self.PatrolZone:GetRandomPointVec2() +ToTargetCoord:SetAlt(altitude) +self:SetTargetDistance(ToTargetCoord) +local ToTargetSpeed=math.random(self.PatrolMinSpeed,self.PatrolMaxSpeed) +local speedkmh=ToTargetSpeed +local FromWP=CurrentCoord:WaypointAir(self.PatrolAltType or"RADIO",POINT_VEC3.RoutePointType.TurningPoint,POINT_VEC3.RoutePointAction.TurningPoint,ToTargetSpeed,true) +PatrolRoute[#PatrolRoute+1]=FromWP +if self.racetrack then +local heading=math.random(self.racetrackheadingmin,self.racetrackheadingmax) +local leg=math.random(self.racetracklegmin,self.racetracklegmax) +local duration=self.racetrackdurationmin +if self.racetrackdurationmax then +duration=math.random(self.racetrackdurationmin,self.racetrackdurationmax) +end +local c0=self.PatrolZone:GetRandomCoordinate() +if self.racetrackcapcoordinates and#self.racetrackcapcoordinates>0 then +c0=self.racetrackcapcoordinates[math.random(#self.racetrackcapcoordinates)] +end +local c1=c0:SetAltitude(altitude) +local c2=c1:Translate(leg,heading):SetAltitude(altitude) +self:SetTargetDistance(c0) +self:T(string.format("Patrol zone race track: v=%.1f knots, h=%.1f ft, heading=%03d, leg=%d m, t=%s sec",UTILS.KmphToKnots(speedkmh),UTILS.MetersToFeet(altitude),heading,leg,tostring(duration))) +local taskOrbit=AIPatrol:TaskOrbit(c1,altitude,UTILS.KmphToMps(speedkmh),c2) +local taskPatrol=AIPatrol:TaskFunction("AI_AIR_PATROL.___PatrolRoute",self) +local taskCond=AIPatrol:TaskCondition(nil,nil,nil,nil,duration,nil) +local taskCont=AIPatrol:TaskControlled(taskOrbit,taskCond) +PatrolRoute[2]=c1:WaypointAirTurningPoint(self.PatrolAltType,speedkmh,{taskCont,taskPatrol},"CAP Orbit") +else +local ToWP=ToTargetCoord:WaypointAir(self.PatrolAltType,POINT_VEC3.RoutePointType.TurningPoint,POINT_VEC3.RoutePointAction.TurningPoint,ToTargetSpeed,true) +PatrolRoute[#PatrolRoute+1]=ToWP +local Tasks={} +Tasks[#Tasks+1]=AIPatrol:TaskFunction("AI_AIR_PATROL.___PatrolRoute",self) +PatrolRoute[#PatrolRoute].task=AIPatrol:TaskCombo(Tasks) +end +AIPatrol:OptionROEReturnFire() +AIPatrol:OptionROTEvadeFire() +AIPatrol:Route(PatrolRoute,self.TaskDelay) +end +end +function AI_AIR_PATROL.Resume(AIPatrol,Fsm) +AIPatrol:F({"AI_AIR_PATROL.Resume:",AIPatrol:GetName()}) +if AIPatrol and AIPatrol:IsAlive()then +Fsm:__Reset(Fsm.TaskDelay) +Fsm:__PatrolRoute(Fsm.TaskDelay) +end +end +AI_AIR_ENGAGE={ +ClassName="AI_AIR_ENGAGE", +} +function AI_AIR_ENGAGE:New(AI_Air,AIGroup,EngageMinSpeed,EngageMaxSpeed,EngageFloorAltitude,EngageCeilingAltitude,EngageAltType) +local self=BASE:Inherit(self,AI_Air) +self.Accomplished=false +self.Engaging=false +local SpeedMax=AIGroup:GetSpeedMax() +self.EngageMinSpeed=EngageMinSpeed or SpeedMax*0.5 +self.EngageMaxSpeed=EngageMaxSpeed or SpeedMax*0.75 +self.EngageFloorAltitude=EngageFloorAltitude or 1000 +self.EngageCeilingAltitude=EngageCeilingAltitude or 1500 +self.EngageAltType=EngageAltType or"RADIO" +self:AddTransition({"Started","Engaging","Returning","Airborne","Patrolling"},"EngageRoute","Engaging") +self:AddTransition({"Started","Engaging","Returning","Airborne","Patrolling"},"Engage","Engaging") +self:AddTransition("Engaging","Fired","Engaging") +self:AddTransition("*","Destroy","*") +self:AddTransition("Engaging","Abort","Patrolling") +self:AddTransition("Engaging","Accomplish","Patrolling") +self:AddTransition({"Patrolling","Engaging"},"Refuel","Refuelling") +return self +end +function AI_AIR_ENGAGE:onafterStart(AIGroup,From,Event,To) +self:GetParent(self,AI_AIR_ENGAGE).onafterStart(self,AIGroup,From,Event,To) +AIGroup:HandleEvent(EVENTS.Takeoff,nil,self) +end +function AI_AIR_ENGAGE:onafterEngage(AIGroup,From,Event,To) +self:HandleEvent(EVENTS.Dead) +end +function AI_AIR_ENGAGE:onbeforeEngage(AIGroup,From,Event,To) +if self.Accomplished==true then +return false +end +return true +end +function AI_AIR_ENGAGE:onafterAbort(AIGroup,From,Event,To) +AIGroup:ClearTasks() +self:Return() +end +function AI_AIR_ENGAGE:onafterAccomplish(AIGroup,From,Event,To) +self.Accomplished=true +end +function AI_AIR_ENGAGE:onafterDestroy(AIGroup,From,Event,To,EventData) +if EventData.IniUnit then +self.AttackUnits[EventData.IniUnit]=nil +end +end +function AI_AIR_ENGAGE:OnEventDead(EventData) +self:F({"EventDead",EventData}) +if EventData.IniDCSUnit then +if self.AttackUnits and self.AttackUnits[EventData.IniUnit]then +self:__Destroy(self.TaskDelay,EventData) +end +end +end +function AI_AIR_ENGAGE.___EngageRoute(AIGroup,Fsm,AttackSetUnit) +Fsm:I(string.format("AI_AIR_ENGAGE.___EngageRoute: %s",tostring(AIGroup:GetName()))) +if AIGroup and AIGroup:IsAlive()then +Fsm:__EngageRoute(Fsm.TaskDelay or 0.1,AttackSetUnit) +end +end +function AI_AIR_ENGAGE:onafterEngageRoute(DefenderGroup,From,Event,To,AttackSetUnit) +self:I({DefenderGroup,From,Event,To,AttackSetUnit}) +local DefenderGroupName=DefenderGroup:GetName() +self.AttackSetUnit=AttackSetUnit +local AttackCount=AttackSetUnit:CountAlive() +if AttackCount>0 then +if DefenderGroup:IsAlive()then +local EngageAltitude=math.random(self.EngageFloorAltitude,self.EngageCeilingAltitude) +local EngageSpeed=math.random(self.EngageMinSpeed,self.EngageMaxSpeed) +local DefenderCoord=DefenderGroup:GetPointVec3() +DefenderCoord:SetY(EngageAltitude) +local TargetCoord=AttackSetUnit:GetFirst():GetPointVec3() +TargetCoord:SetY(EngageAltitude) +local TargetDistance=DefenderCoord:Get2DDistance(TargetCoord) +local EngageDistance=(DefenderGroup:IsHelicopter()and 5000)or(DefenderGroup:IsAirPlane()and 10000) +if TargetDistance<=EngageDistance*9 then +self:I(string.format("AI_AIR_ENGAGE onafterEngageRoute ==> __Engage - target distance = %.1f km",TargetDistance/1000)) +self:__Engage(0.1,AttackSetUnit) +else +self:I(string.format("FF AI_AIR_ENGAGE onafterEngageRoute ==> Routing - target distance = %.1f km",TargetDistance/1000)) +local EngageRoute={} +local AttackTasks={} +local FromWP=DefenderCoord:WaypointAir(self.PatrolAltType or"RADIO",POINT_VEC3.RoutePointType.TurningPoint,POINT_VEC3.RoutePointAction.TurningPoint,EngageSpeed,true) +EngageRoute[#EngageRoute+1]=FromWP +self:SetTargetDistance(TargetCoord) +local FromEngageAngle=DefenderCoord:GetAngleDegrees(DefenderCoord:GetDirectionVec3(TargetCoord)) +local ToCoord=DefenderCoord:Translate(EngageDistance,FromEngageAngle,true) +local ToWP=ToCoord:WaypointAir(self.PatrolAltType or"RADIO",POINT_VEC3.RoutePointType.TurningPoint,POINT_VEC3.RoutePointAction.TurningPoint,EngageSpeed,true) +EngageRoute[#EngageRoute+1]=ToWP +AttackTasks[#AttackTasks+1]=DefenderGroup:TaskFunction("AI_AIR_ENGAGE.___EngageRoute",self,AttackSetUnit) +EngageRoute[#EngageRoute].task=DefenderGroup:TaskCombo(AttackTasks) +DefenderGroup:OptionROEReturnFire() +DefenderGroup:OptionROTEvadeFire() +DefenderGroup:Route(EngageRoute,self.TaskDelay or 0.1) +end +end +else +self:I(DefenderGroupName..": No targets found -> Going RTB") +self:Return() +end +end +function AI_AIR_ENGAGE.___Engage(AIGroup,Fsm,AttackSetUnit) +Fsm:I(string.format("AI_AIR_ENGAGE.___Engage: %s",tostring(AIGroup:GetName()))) +if AIGroup and AIGroup:IsAlive()then +local delay=Fsm.TaskDelay or 0.1 +Fsm:__Engage(delay,AttackSetUnit) +end +end +function AI_AIR_ENGAGE:onafterEngage(DefenderGroup,From,Event,To,AttackSetUnit) +self:F({DefenderGroup,From,Event,To,AttackSetUnit}) +local DefenderGroupName=DefenderGroup:GetName() +self.AttackSetUnit=AttackSetUnit +local AttackCount=AttackSetUnit:CountAlive() +self:T({AttackCount=AttackCount}) +if AttackCount>0 then +if DefenderGroup and DefenderGroup:IsAlive()then +local EngageAltitude=math.random(self.EngageFloorAltitude or 500,self.EngageCeilingAltitude or 1000) +local EngageSpeed=math.random(self.EngageMinSpeed,self.EngageMaxSpeed) +local DefenderCoord=DefenderGroup:GetPointVec3() +DefenderCoord:SetY(EngageAltitude) +local TargetCoord=AttackSetUnit:GetFirst():GetPointVec3() +TargetCoord:SetY(EngageAltitude) +local TargetDistance=DefenderCoord:Get2DDistance(TargetCoord) +local EngageDistance=(DefenderGroup:IsHelicopter()and 5000)or(DefenderGroup:IsAirPlane()and 10000) +local EngageRoute={} +local AttackTasks={} +local FromWP=DefenderCoord:WaypointAir(self.EngageAltType or"RADIO",POINT_VEC3.RoutePointType.TurningPoint,POINT_VEC3.RoutePointAction.TurningPoint,EngageSpeed,true) +EngageRoute[#EngageRoute+1]=FromWP +self:SetTargetDistance(TargetCoord) +local FromEngageAngle=DefenderCoord:GetAngleDegrees(DefenderCoord:GetDirectionVec3(TargetCoord)) +local ToCoord=DefenderCoord:Translate(EngageDistance,FromEngageAngle,true) +local ToWP=ToCoord:WaypointAir(self.EngageAltType or"RADIO",POINT_VEC3.RoutePointType.TurningPoint,POINT_VEC3.RoutePointAction.TurningPoint,EngageSpeed,true) +EngageRoute[#EngageRoute+1]=ToWP +if TargetDistance<=EngageDistance*9 then +local AttackUnitTasks=self:CreateAttackUnitTasks(AttackSetUnit,DefenderGroup,EngageAltitude) +if#AttackUnitTasks==0 then +self:I(DefenderGroupName..": No valid targets found -> Going RTB") +self:Return() +return +else +local text=string.format("%s: Engaging targets at distance %.2f NM",DefenderGroupName,UTILS.MetersToNM(TargetDistance)) +self:I(text) +DefenderGroup:OptionROEOpenFire() +DefenderGroup:OptionROTEvadeFire() +DefenderGroup:OptionKeepWeaponsOnThreat() +AttackTasks[#AttackTasks+1]=DefenderGroup:TaskCombo(AttackUnitTasks) +end +end +AttackTasks[#AttackTasks+1]=DefenderGroup:TaskFunction("AI_AIR_ENGAGE.___Engage",self,AttackSetUnit) +EngageRoute[#EngageRoute].task=DefenderGroup:TaskCombo(AttackTasks) +DefenderGroup:Route(EngageRoute,self.TaskDelay or 0.1) +end +else +self:I(DefenderGroupName..": No targets found -> returning.") +self:Return() +return +end +end +function AI_AIR_ENGAGE.Resume(AIEngage,Fsm) +AIEngage:F({"Resume:",AIEngage:GetName()}) +if AIEngage and AIEngage:IsAlive()then +Fsm:__Reset(Fsm.TaskDelay or 0.1) +Fsm:__EngageRoute(Fsm.TaskDelay or 0.2,Fsm.AttackSetUnit) +end +end +AI_A2A_PATROL={ +ClassName="AI_A2A_PATROL", +} +function AI_A2A_PATROL:New(AIPatrol,PatrolZone,PatrolFloorAltitude,PatrolCeilingAltitude,PatrolMinSpeed,PatrolMaxSpeed,PatrolAltType) +local AI_Air=AI_AIR:New(AIPatrol) +local AI_Air_Patrol=AI_AIR_PATROL:New(AI_Air,AIPatrol,PatrolZone,PatrolFloorAltitude,PatrolCeilingAltitude,PatrolMinSpeed,PatrolMaxSpeed,PatrolAltType) +local self=BASE:Inherit(self,AI_Air_Patrol) +self:SetFuelThreshold(.2,60) +self:SetDamageThreshold(0.4) +self:SetDisengageRadius(70000) +self.PatrolZone=PatrolZone +self.PatrolFloorAltitude=PatrolFloorAltitude +self.PatrolCeilingAltitude=PatrolCeilingAltitude +self.PatrolMinSpeed=PatrolMinSpeed +self.PatrolMaxSpeed=PatrolMaxSpeed +self.PatrolAltType=PatrolAltType or"BARO" +self:AddTransition({"Started","Airborne","Refuelling"},"Patrol","Patrolling") +self:AddTransition("Patrolling","Route","Patrolling") +self:AddTransition("*","Reset","Patrolling") +return self +end +function AI_A2A_PATROL:SetSpeed(PatrolMinSpeed,PatrolMaxSpeed) +self:F2({PatrolMinSpeed,PatrolMaxSpeed}) +self.PatrolMinSpeed=PatrolMinSpeed +self.PatrolMaxSpeed=PatrolMaxSpeed +end +function AI_A2A_PATROL:SetAltitude(PatrolFloorAltitude,PatrolCeilingAltitude) +self:F2({PatrolFloorAltitude,PatrolCeilingAltitude}) +self.PatrolFloorAltitude=PatrolFloorAltitude +self.PatrolCeilingAltitude=PatrolCeilingAltitude +end +function AI_A2A_PATROL:onafterPatrol(AIPatrol,From,Event,To) +self:F2() +self:ClearTargetDistance() +self:__Route(1) +AIPatrol:OnReSpawn( +function(PatrolGroup) +self:__Reset(1) +self:__Route(5) +end +) +end +function AI_A2A_PATROL.PatrolRoute(AIPatrol,Fsm) +AIPatrol:F({"AI_A2A_PATROL.PatrolRoute:",AIPatrol:GetName()}) +if AIPatrol and AIPatrol:IsAlive()then +Fsm:Route() +end +end +function AI_A2A_PATROL:onafterRoute(AIPatrol,From,Event,To) +self:F2() +if From=="RTB"then +return +end +if AIPatrol and AIPatrol:IsAlive()then +local PatrolRoute={} +local CurrentCoord=AIPatrol:GetCoordinate() +local altitude=math.random(self.PatrolFloorAltitude,self.PatrolCeilingAltitude) +local speedkmh=math.random(self.PatrolMinSpeed,self.PatrolMaxSpeed) +PatrolRoute[1]=CurrentCoord:WaypointAirTurningPoint(nil,speedkmh,{},"Current") +if self.racetrack then +local heading=math.random(self.racetrackheadingmin,self.racetrackheadingmax) +local leg=math.random(self.racetracklegmin,self.racetracklegmax) +local duration=self.racetrackdurationmin +if self.racetrackdurationmax then +duration=math.random(self.racetrackdurationmin,self.racetrackdurationmax) +end +local c0=self.PatrolZone:GetRandomCoordinate() +if self.racetrackcapcoordinates and#self.racetrackcapcoordinates>0 then +c0=self.racetrackcapcoordinates[math.random(#self.racetrackcapcoordinates)] +end +local c1=c0:SetAltitude(altitude) +local c2=c1:Translate(leg,heading):SetAltitude(altitude) +self:SetTargetDistance(c0) +self:T(string.format("Patrol zone race track: v=%.1f knots, h=%.1f ft, heading=%03d, leg=%d m, t=%s sec",UTILS.KmphToKnots(speedkmh),UTILS.MetersToFeet(altitude),heading,leg,tostring(duration))) +local taskOrbit=AIPatrol:TaskOrbit(c1,altitude,UTILS.KmphToMps(speedkmh),c2) +local taskPatrol=AIPatrol:TaskFunction("AI_A2A_PATROL.PatrolRoute",self) +local taskCond=AIPatrol:TaskCondition(nil,nil,nil,nil,duration,nil) +local taskCont=AIPatrol:TaskControlled(taskOrbit,taskCond) +PatrolRoute[2]=c1:WaypointAirTurningPoint(self.PatrolAltType,speedkmh,{taskCont,taskPatrol},"CAP Orbit") +else +local ToTargetCoord=self.PatrolZone:GetRandomCoordinate() +ToTargetCoord:SetAltitude(altitude) +self:SetTargetDistance(ToTargetCoord) +local taskReRoute=AIPatrol:TaskFunction("AI_A2A_PATROL.PatrolRoute",self) +PatrolRoute[2]=ToTargetCoord:WaypointAirTurningPoint(self.PatrolAltType,speedkmh,{taskReRoute},"Patrol Point") +end +AIPatrol:OptionROEReturnFire() +AIPatrol:OptionROTEvadeFire() +AIPatrol:Route(PatrolRoute,0.5) +end +end +AI_A2A_CAP={ +ClassName="AI_A2A_CAP", +} +function AI_A2A_CAP:New2(AICap,EngageMinSpeed,EngageMaxSpeed,EngageFloorAltitude,EngageCeilingAltitude,EngageAltType,PatrolZone,PatrolMinSpeed,PatrolMaxSpeed,PatrolFloorAltitude,PatrolCeilingAltitude,PatrolAltType) +local AI_Air=AI_AIR:New(AICap) +local AI_Air_Patrol=AI_AIR_PATROL:New(AI_Air,AICap,PatrolZone,PatrolFloorAltitude,PatrolCeilingAltitude,PatrolMinSpeed,PatrolMaxSpeed,PatrolAltType) +local AI_Air_Engage=AI_AIR_ENGAGE:New(AI_Air_Patrol,AICap,EngageMinSpeed,EngageMaxSpeed,EngageFloorAltitude,EngageCeilingAltitude,EngageAltType) +local self=BASE:Inherit(self,AI_Air_Engage) +self:SetFuelThreshold(.2,60) +self:SetDamageThreshold(0.4) +self:SetDisengageRadius(70000) +return self +end +function AI_A2A_CAP:New(AICap,PatrolZone,PatrolFloorAltitude,PatrolCeilingAltitude,PatrolMinSpeed,PatrolMaxSpeed,EngageMinSpeed,EngageMaxSpeed,PatrolAltType) +return self:New2(AICap,EngageMinSpeed,EngageMaxSpeed,PatrolFloorAltitude,PatrolCeilingAltitude,PatrolAltType,PatrolZone,PatrolMinSpeed,PatrolMaxSpeed,PatrolFloorAltitude,PatrolCeilingAltitude,PatrolAltType) +end +function AI_A2A_CAP:onafterStart(AICap,From,Event,To) +self:GetParent(self,AI_A2A_CAP).onafterStart(self,AICap,From,Event,To) +AICap:HandleEvent(EVENTS.Takeoff,nil,self) +end +function AI_A2A_CAP:SetEngageZone(EngageZone) +self:F2() +if EngageZone then +self.EngageZone=EngageZone +else +self.EngageZone=nil +end +end +function AI_A2A_CAP:SetEngageRange(EngageRange) +self:F2() +if EngageRange then +self.EngageRange=EngageRange +else +self.EngageRange=nil +end +end +function AI_A2A_CAP:CreateAttackUnitTasks(AttackSetUnit,DefenderGroup,EngageAltitude) +local AttackUnitTasks={} +for AttackUnitID,AttackUnit in pairs(self.AttackSetUnit:GetSet())do +local AttackUnit=AttackUnit +if AttackUnit and AttackUnit:IsAlive()and AttackUnit:IsAir()then +self:T({"Attacking Task:",AttackUnit:GetName(),AttackUnit:IsAlive(),AttackUnit:IsAir()}) +AttackUnitTasks[#AttackUnitTasks+1]=DefenderGroup:TaskAttackUnit(AttackUnit) +end +end +return AttackUnitTasks +end +AI_A2A_GCI={ +ClassName="AI_A2A_GCI", +} +function AI_A2A_GCI:New2(AIIntercept,EngageMinSpeed,EngageMaxSpeed,EngageFloorAltitude,EngageCeilingAltitude,EngageAltType) +local AI_Air=AI_AIR:New(AIIntercept) +local AI_Air_Engage=AI_AIR_ENGAGE:New(AI_Air,AIIntercept,EngageMinSpeed,EngageMaxSpeed,EngageFloorAltitude,EngageCeilingAltitude,EngageAltType) +local self=BASE:Inherit(self,AI_Air_Engage) +self:SetFuelThreshold(.2,60) +self:SetDamageThreshold(0.4) +self:SetDisengageRadius(70000) +return self +end +function AI_A2A_GCI:New(AIIntercept,EngageMinSpeed,EngageMaxSpeed,EngageFloorAltitude,EngageCeilingAltitude,EngageAltType) +return self:New2(AIIntercept,EngageMinSpeed,EngageMaxSpeed,EngageFloorAltitude,EngageCeilingAltitude,EngageAltType) +end +function AI_A2A_GCI:onafterStart(AIIntercept,From,Event,To) +self:GetParent(self,AI_A2A_GCI).onafterStart(self,AIIntercept,From,Event,To) +end +function AI_A2A_GCI:CreateAttackUnitTasks(AttackSetUnit,DefenderGroup,EngageAltitude) +local AttackUnitTasks={} +for AttackUnitID,AttackUnit in pairs(self.AttackSetUnit:GetSet())do +local AttackUnit=AttackUnit +self:T({"Attacking Unit:",AttackUnit:GetName(),AttackUnit:IsAlive(),AttackUnit:IsAir()}) +if AttackUnit:IsAlive()and AttackUnit:IsAir()then +AttackUnitTasks[#AttackUnitTasks+1]=DefenderGroup:TaskAttackUnit(AttackUnit) +end +end +return AttackUnitTasks +end +do +AI_A2A_DISPATCHER={ +ClassName="AI_A2A_DISPATCHER", +Detection=nil, +} +AI_A2A_DISPATCHER.Takeoff=GROUP.Takeoff +AI_A2A_DISPATCHER.Landing={ +NearAirbase=1, +AtRunway=2, +AtEngineShutdown=3, +} +function AI_A2A_DISPATCHER:New(Detection) +local self=BASE:Inherit(self,DETECTION_MANAGER:New(nil,Detection)) +self.Detection=Detection +self.DefenderSquadrons={} +self.DefenderSpawns={} +self.DefenderTasks={} +self.DefenderDefault={} +self.SetSendPlayerMessages=false +self.Detection:FilterCategories({Unit.Category.AIRPLANE,Unit.Category.HELICOPTER}) +self.Detection:SetRefreshTimeInterval(30) +self:SetEngageRadius() +self:SetGciRadius() +self:SetIntercept(300) +self:SetDisengageRadius(300000) +self:SetDefaultTakeoff(AI_A2A_DISPATCHER.Takeoff.Air) +self:SetDefaultTakeoffInAirAltitude(500) +self:SetDefaultLanding(AI_A2A_DISPATCHER.Landing.NearAirbase) +self:SetDefaultOverhead(1) +self:SetDefaultGrouping(1) +self:SetDefaultFuelThreshold(0.15,0) +self:SetDefaultDamageThreshold(0.4) +self:SetDefaultCapTimeInterval(180,600) +self:SetDefaultCapLimit(1) +self:AddTransition("Started","Assign","Started") +self:AddTransition("*","CAP","*") +self:AddTransition("*","GCI","*") +self:AddTransition("*","ENGAGE","*") +self:HandleEvent(EVENTS.Crash,self.OnEventCrashOrDead) +self:HandleEvent(EVENTS.Dead,self.OnEventCrashOrDead) +self:HandleEvent(EVENTS.Land) +self:HandleEvent(EVENTS.EngineShutdown) +self:HandleEvent(EVENTS.BaseCaptured) +self:SetTacticalDisplay(false) +self.DefenderCAPIndex=0 +self:__Start(5) +return self +end +function AI_A2A_DISPATCHER:onafterStart(From,Event,To) +self:GetParent(self,AI_A2A_DISPATCHER).onafterStart(self,From,Event,To) +for SquadronName,_DefenderSquadron in pairs(self.DefenderSquadrons)do +local DefenderSquadron=_DefenderSquadron +DefenderSquadron.Resources={} +if DefenderSquadron.ResourceCount then +for Resource=1,DefenderSquadron.ResourceCount do +self:ParkDefender(DefenderSquadron) +end +end +end +end +function AI_A2A_DISPATCHER:ParkDefender(DefenderSquadron) +local TemplateID=math.random(1,#DefenderSquadron.Spawn) +local Spawn=DefenderSquadron.Spawn[TemplateID] +Spawn:InitGrouping(1) +local SpawnGroup +if self:IsSquadronVisible(DefenderSquadron.Name)then +local Grouping=DefenderSquadron.Grouping or self.DefenderDefault.Grouping +Grouping=1 +Spawn:InitGrouping(Grouping) +SpawnGroup=Spawn:SpawnAtAirbase(DefenderSquadron.Airbase,SPAWN.Takeoff.Cold) +local GroupName=SpawnGroup:GetName() +DefenderSquadron.Resources=DefenderSquadron.Resources or{} +DefenderSquadron.Resources[TemplateID]=DefenderSquadron.Resources[TemplateID]or{} +DefenderSquadron.Resources[TemplateID][GroupName]={} +DefenderSquadron.Resources[TemplateID][GroupName]=SpawnGroup +self.uncontrolled=self.uncontrolled or{} +self.uncontrolled[DefenderSquadron.Name]=self.uncontrolled[DefenderSquadron.Name]or{} +table.insert(self.uncontrolled[DefenderSquadron.Name],{group=SpawnGroup,name=GroupName,grouping=Grouping}) +end +end +function AI_A2A_DISPATCHER:OnEventBaseCaptured(EventData) +local AirbaseName=EventData.PlaceName +self:I("Captured "..AirbaseName) +for SquadronName,Squadron in pairs(self.DefenderSquadrons)do +if Squadron.AirbaseName==AirbaseName then +Squadron.ResourceCount=-999 +Squadron.Captured=true +self:I("Squadron "..SquadronName.." captured.") +end +end +end +function AI_A2A_DISPATCHER:OnEventCrashOrDead(EventData) +self.Detection:ForgetDetectedUnit(EventData.IniUnitName) +end +function AI_A2A_DISPATCHER:OnEventLand(EventData) +self:F("Landed") +local DefenderUnit=EventData.IniUnit +local Defender=EventData.IniGroup +local Squadron=self:GetSquadronFromDefender(Defender) +if Squadron then +self:F({SquadronName=Squadron.Name}) +local LandingMethod=self:GetSquadronLanding(Squadron.Name) +if LandingMethod==AI_A2A_DISPATCHER.Landing.AtRunway then +local DefenderSize=Defender:GetSize() +if DefenderSize==1 then +self:RemoveDefenderFromSquadron(Squadron,Defender) +end +DefenderUnit:Destroy() +self:ParkDefender(Squadron) +return +end +if DefenderUnit:GetLife()~=DefenderUnit:GetLife0()then +DefenderUnit:Destroy() +return +end +end +end +function AI_A2A_DISPATCHER:OnEventEngineShutdown(EventData) +local DefenderUnit=EventData.IniUnit +local Defender=EventData.IniGroup +local Squadron=self:GetSquadronFromDefender(Defender) +if Squadron then +self:F({SquadronName=Squadron.Name}) +local LandingMethod=self:GetSquadronLanding(Squadron.Name) +if LandingMethod==AI_A2A_DISPATCHER.Landing.AtEngineShutdown and +not DefenderUnit:InAir()then +local DefenderSize=Defender:GetSize() +if DefenderSize==1 then +self:RemoveDefenderFromSquadron(Squadron,Defender) +end +DefenderUnit:Destroy() +self:ParkDefender(Squadron) +end +end +end +function AI_A2A_DISPATCHER:SetEngageRadius(EngageRadius) +self.Detection:SetFriendliesRange(EngageRadius or 100000) +return self +end +function AI_A2A_DISPATCHER:SetDisengageRadius(DisengageRadius) +self.DisengageRadius=DisengageRadius or 300000 +return self +end +function AI_A2A_DISPATCHER:SetGciRadius(GciRadius) +self.GciRadius=GciRadius or 200000 +return self +end +function AI_A2A_DISPATCHER:SetBorderZone(BorderZone) +self.Detection:SetAcceptZones(BorderZone) +return self +end +function AI_A2A_DISPATCHER:SetTacticalDisplay(TacticalDisplay) +self.TacticalDisplay=TacticalDisplay +return self +end +function AI_A2A_DISPATCHER:SetDefaultDamageThreshold(DamageThreshold) +self.DefenderDefault.DamageThreshold=DamageThreshold +return self +end +function AI_A2A_DISPATCHER:SetDefaultCapTimeInterval(CapMinSeconds,CapMaxSeconds) +self.DefenderDefault.CapMinSeconds=CapMinSeconds +self.DefenderDefault.CapMaxSeconds=CapMaxSeconds +return self +end +function AI_A2A_DISPATCHER:SetDefaultCapLimit(CapLimit) +self.DefenderDefault.CapLimit=CapLimit +return self +end +function AI_A2A_DISPATCHER:SetIntercept(InterceptDelay) +self.DefenderDefault.InterceptDelay=InterceptDelay +local Detection=self.Detection +Detection:SetIntercept(true,InterceptDelay) +return self +end +function AI_A2A_DISPATCHER:GetAIFriendliesNearBy(DetectedItem) +local FriendliesNearBy=self.Detection:GetFriendliesDistance(DetectedItem) +return FriendliesNearBy +end +function AI_A2A_DISPATCHER:GetDefenderTasks() +return self.DefenderTasks or{} +end +function AI_A2A_DISPATCHER:GetDefenderTask(Defender) +return self.DefenderTasks[Defender] +end +function AI_A2A_DISPATCHER:GetDefenderTaskFsm(Defender) +return self:GetDefenderTask(Defender).Fsm +end +function AI_A2A_DISPATCHER:GetDefenderTaskTarget(Defender) +return self:GetDefenderTask(Defender).Target +end +function AI_A2A_DISPATCHER:GetDefenderTaskSquadronName(Defender) +return self:GetDefenderTask(Defender).SquadronName +end +function AI_A2A_DISPATCHER:ClearDefenderTask(Defender) +if Defender and Defender:IsAlive()and self.DefenderTasks[Defender]then +local Target=self.DefenderTasks[Defender].Target +local Message="Clearing ("..self.DefenderTasks[Defender].Type..") " +Message=Message..Defender:GetName() +if Target then +Message=Message..(Target and(" from "..Target.Index.." ["..Target.Set:Count().."]"))or"" +end +self:F({Target=Message}) +end +self.DefenderTasks[Defender]=nil +return self +end +function AI_A2A_DISPATCHER:ClearDefenderTaskTarget(Defender) +local DefenderTask=self:GetDefenderTask(Defender) +if Defender and Defender:IsAlive()and DefenderTask then +local Target=DefenderTask.Target +local Message="Clearing ("..DefenderTask.Type..") " +Message=Message..Defender:GetName() +if Target then +Message=Message..(Target and(" from "..Target.Index.." ["..Target.Set:Count().."]"))or"" +end +self:F({Target=Message}) +end +if Defender and DefenderTask and DefenderTask.Target then +DefenderTask.Target=nil +end +return self +end +function AI_A2A_DISPATCHER:SetDefenderTask(SquadronName,Defender,Type,Fsm,Target) +self:F({SquadronName=SquadronName,Defender=Defender:GetName(),Type=Type,Target=Target}) +self.DefenderTasks[Defender]=self.DefenderTasks[Defender]or{} +self.DefenderTasks[Defender].Type=Type +self.DefenderTasks[Defender].Fsm=Fsm +self.DefenderTasks[Defender].SquadronName=SquadronName +if Target then +self:SetDefenderTaskTarget(Defender,Target) +end +return self +end +function AI_A2A_DISPATCHER:SetDefenderTaskTarget(Defender,AttackerDetection) +local Message="("..self.DefenderTasks[Defender].Type..") " +Message=Message..Defender:GetName() +Message=Message..(AttackerDetection and(" target "..AttackerDetection.Index.." ["..AttackerDetection.Set:Count().."]"))or"" +self:F({AttackerDetection=Message}) +if AttackerDetection then +self.DefenderTasks[Defender].Target=AttackerDetection +end +return self +end +function AI_A2A_DISPATCHER:SetSquadron(SquadronName,AirbaseName,TemplatePrefixes,ResourceCount) +self.DefenderSquadrons[SquadronName]=self.DefenderSquadrons[SquadronName]or{} +local DefenderSquadron=self.DefenderSquadrons[SquadronName] +DefenderSquadron.Name=SquadronName +DefenderSquadron.Airbase=AIRBASE:FindByName(AirbaseName) +DefenderSquadron.AirbaseName=DefenderSquadron.Airbase:GetName() +if not DefenderSquadron.Airbase then +error("Cannot find airbase with name:"..AirbaseName) +end +DefenderSquadron.Spawn={} +if type(TemplatePrefixes)=="string"then +local SpawnTemplate=TemplatePrefixes +self.DefenderSpawns[SpawnTemplate]=self.DefenderSpawns[SpawnTemplate]or SPAWN:New(SpawnTemplate) +DefenderSquadron.Spawn[1]=self.DefenderSpawns[SpawnTemplate] +else +for TemplateID,SpawnTemplate in pairs(TemplatePrefixes)do +self.DefenderSpawns[SpawnTemplate]=self.DefenderSpawns[SpawnTemplate]or SPAWN:New(SpawnTemplate) +DefenderSquadron.Spawn[#DefenderSquadron.Spawn+1]=self.DefenderSpawns[SpawnTemplate] +end +end +DefenderSquadron.ResourceCount=ResourceCount +DefenderSquadron.TemplatePrefixes=TemplatePrefixes +DefenderSquadron.Captured=false +self:SetSquadronLanguage(SquadronName,"EN") +self:F({Squadron={SquadronName,AirbaseName,TemplatePrefixes,ResourceCount}}) +return self +end +function AI_A2A_DISPATCHER:GetSquadron(SquadronName) +local DefenderSquadron=self.DefenderSquadrons[SquadronName] +if not DefenderSquadron then +error("Unknown Squadron:"..SquadronName) +end +return DefenderSquadron +end +function AI_A2A_DISPATCHER:SetSquadronVisible(SquadronName) +self.DefenderSquadrons[SquadronName]=self.DefenderSquadrons[SquadronName]or{} +local DefenderSquadron=self:GetSquadron(SquadronName) +DefenderSquadron.Uncontrolled=true +DefenderSquadron.Grouping=1 +local nfreeparking=DefenderSquadron.Airbase:GetFreeParkingSpotsNumber(AIRBASE.TerminalType.FighterAircraft,true) +DefenderSquadron.ResourceCount=DefenderSquadron.ResourceCount or nfreeparking +DefenderSquadron.ResourceCount=math.min(DefenderSquadron.ResourceCount,nfreeparking) +for SpawnTemplate,_DefenderSpawn in pairs(self.DefenderSpawns)do +local DefenderSpawn=_DefenderSpawn +DefenderSpawn:InitUnControlled(true) +end +end +function AI_A2A_DISPATCHER:IsSquadronVisible(SquadronName) +self.DefenderSquadrons[SquadronName]=self.DefenderSquadrons[SquadronName]or{} +local DefenderSquadron=self:GetSquadron(SquadronName) +if DefenderSquadron then +return DefenderSquadron.Uncontrolled==true +end +return nil +end +function AI_A2A_DISPATCHER:SetSquadronCap2(SquadronName,EngageMinSpeed,EngageMaxSpeed,EngageFloorAltitude,EngageCeilingAltitude,EngageAltType,Zone,PatrolMinSpeed,PatrolMaxSpeed,PatrolFloorAltitude,PatrolCeilingAltitude,PatrolAltType) +self.DefenderSquadrons[SquadronName]=self.DefenderSquadrons[SquadronName]or{} +self.DefenderSquadrons[SquadronName].Cap=self.DefenderSquadrons[SquadronName].Cap or{} +local DefenderSquadron=self:GetSquadron(SquadronName) +local Cap=self.DefenderSquadrons[SquadronName].Cap +Cap.Name=SquadronName +Cap.EngageMinSpeed=EngageMinSpeed +Cap.EngageMaxSpeed=EngageMaxSpeed +Cap.EngageFloorAltitude=EngageFloorAltitude +Cap.EngageCeilingAltitude=EngageCeilingAltitude +Cap.Zone=Zone +Cap.PatrolMinSpeed=PatrolMinSpeed +Cap.PatrolMaxSpeed=PatrolMaxSpeed +Cap.PatrolFloorAltitude=PatrolFloorAltitude +Cap.PatrolCeilingAltitude=PatrolCeilingAltitude +Cap.PatrolAltType=PatrolAltType +Cap.EngageAltType=EngageAltType +self:SetSquadronCapInterval(SquadronName,self.DefenderDefault.CapLimit,self.DefenderDefault.CapMinSeconds,self.DefenderDefault.CapMaxSeconds,1) +self:I({CAP={SquadronName,EngageMinSpeed,EngageMaxSpeed,EngageFloorAltitude,EngageCeilingAltitude,Zone,PatrolMinSpeed,PatrolMaxSpeed,PatrolFloorAltitude,PatrolCeilingAltitude,PatrolAltType,EngageAltType}}) +local RecceSet=self.Detection:GetDetectionSet() +RecceSet:FilterPrefixes(DefenderSquadron.TemplatePrefixes) +RecceSet:FilterStart() +self.Detection:SetFriendlyPrefixes(DefenderSquadron.TemplatePrefixes) +return self +end +function AI_A2A_DISPATCHER:SetSquadronCap(SquadronName,Zone,PatrolFloorAltitude,PatrolCeilingAltitude,PatrolMinSpeed,PatrolMaxSpeed,EngageMinSpeed,EngageMaxSpeed,AltType) +return self:SetSquadronCap2(SquadronName,EngageMinSpeed,EngageMaxSpeed,PatrolFloorAltitude,PatrolCeilingAltitude,AltType,Zone,PatrolMinSpeed,PatrolMaxSpeed,PatrolFloorAltitude,PatrolCeilingAltitude,AltType) +end +function AI_A2A_DISPATCHER:SetSquadronCapInterval(SquadronName,CapLimit,LowInterval,HighInterval,Probability) +self.DefenderSquadrons[SquadronName]=self.DefenderSquadrons[SquadronName]or{} +self.DefenderSquadrons[SquadronName].Cap=self.DefenderSquadrons[SquadronName].Cap or{} +local DefenderSquadron=self:GetSquadron(SquadronName) +local Cap=self.DefenderSquadrons[SquadronName].Cap +if Cap then +Cap.LowInterval=LowInterval or 180 +Cap.HighInterval=HighInterval or 600 +Cap.Probability=Probability or 1 +Cap.CapLimit=CapLimit or 1 +Cap.Scheduler=Cap.Scheduler or SCHEDULER:New(self) +local Scheduler=Cap.Scheduler +local ScheduleID=Cap.ScheduleID +local Variance=(Cap.HighInterval-Cap.LowInterval)/2 +local Repeat=Cap.LowInterval+Variance +local Randomization=Variance/Repeat +local Start=math.random(1,Cap.HighInterval) +if ScheduleID then +Scheduler:Stop(ScheduleID) +end +Cap.ScheduleID=Scheduler:Schedule(self,self.SchedulerCAP,{SquadronName},Start,Repeat,Randomization) +else +error("This squadron does not exist:"..SquadronName) +end +end +function AI_A2A_DISPATCHER:GetCAPDelay(SquadronName) +self.DefenderSquadrons[SquadronName]=self.DefenderSquadrons[SquadronName]or{} +self.DefenderSquadrons[SquadronName].Cap=self.DefenderSquadrons[SquadronName].Cap or{} +local DefenderSquadron=self:GetSquadron(SquadronName) +local Cap=self.DefenderSquadrons[SquadronName].Cap +if Cap then +return math.random(Cap.LowInterval,Cap.HighInterval) +else +error("This squadron does not exist:"..SquadronName) +end +end +function AI_A2A_DISPATCHER:CanCAP(SquadronName) +self:F({SquadronName=SquadronName}) +self.DefenderSquadrons[SquadronName]=self.DefenderSquadrons[SquadronName]or{} +self.DefenderSquadrons[SquadronName].Cap=self.DefenderSquadrons[SquadronName].Cap or{} +local DefenderSquadron=self:GetSquadron(SquadronName) +if DefenderSquadron.Captured==false then +if(not DefenderSquadron.ResourceCount)or(DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount>0)then +local Cap=DefenderSquadron.Cap +if Cap then +local CapCount=self:CountCapAirborne(SquadronName) +self:F({CapCount=CapCount}) +if CapCount0)then +local Gci=DefenderSquadron.Gci +if Gci then +return DefenderSquadron +end +end +end +return nil +end +function AI_A2A_DISPATCHER:SetSquadronGci2(SquadronName,EngageMinSpeed,EngageMaxSpeed,EngageFloorAltitude,EngageCeilingAltitude,EngageAltType) +self.DefenderSquadrons[SquadronName]=self.DefenderSquadrons[SquadronName]or{} +self.DefenderSquadrons[SquadronName].Gci=self.DefenderSquadrons[SquadronName].Gci or{} +local Intercept=self.DefenderSquadrons[SquadronName].Gci +Intercept.Name=SquadronName +Intercept.EngageMinSpeed=EngageMinSpeed +Intercept.EngageMaxSpeed=EngageMaxSpeed +Intercept.EngageFloorAltitude=EngageFloorAltitude +Intercept.EngageCeilingAltitude=EngageCeilingAltitude +Intercept.EngageAltType=EngageAltType +self:I({GCI={SquadronName,EngageMinSpeed,EngageMaxSpeed,EngageFloorAltitude,EngageCeilingAltitude,EngageAltType}}) +end +function AI_A2A_DISPATCHER:SetSquadronGci(SquadronName,EngageMinSpeed,EngageMaxSpeed) +self.DefenderSquadrons[SquadronName]=self.DefenderSquadrons[SquadronName]or{} +self.DefenderSquadrons[SquadronName].Gci=self.DefenderSquadrons[SquadronName].Gci or{} +local Intercept=self.DefenderSquadrons[SquadronName].Gci +Intercept.Name=SquadronName +Intercept.EngageMinSpeed=EngageMinSpeed +Intercept.EngageMaxSpeed=EngageMaxSpeed +self:F({GCI={SquadronName,EngageMinSpeed,EngageMaxSpeed}}) +end +function AI_A2A_DISPATCHER:SetDefaultOverhead(Overhead) +self.DefenderDefault.Overhead=Overhead +return self +end +function AI_A2A_DISPATCHER:SetSquadronOverhead(SquadronName,Overhead) +local DefenderSquadron=self:GetSquadron(SquadronName) +DefenderSquadron.Overhead=Overhead +return self +end +function AI_A2A_DISPATCHER:SetDefaultGrouping(Grouping) +self.DefenderDefault.Grouping=Grouping +return self +end +function AI_A2A_DISPATCHER:SetSquadronGrouping(SquadronName,Grouping) +local DefenderSquadron=self:GetSquadron(SquadronName) +DefenderSquadron.Grouping=Grouping +return self +end +function AI_A2A_DISPATCHER:SetDefaultTakeoff(Takeoff) +self.DefenderDefault.Takeoff=Takeoff +return self +end +function AI_A2A_DISPATCHER:SetSquadronTakeoff(SquadronName,Takeoff) +local DefenderSquadron=self:GetSquadron(SquadronName) +DefenderSquadron.Takeoff=Takeoff +return self +end +function AI_A2A_DISPATCHER:GetDefaultTakeoff() +return self.DefenderDefault.Takeoff +end +function AI_A2A_DISPATCHER:GetSquadronTakeoff(SquadronName) +local DefenderSquadron=self:GetSquadron(SquadronName) +return DefenderSquadron.Takeoff or self.DefenderDefault.Takeoff +end +function AI_A2A_DISPATCHER:SetDefaultTakeoffInAir() +self:SetDefaultTakeoff(AI_A2A_DISPATCHER.Takeoff.Air) +return self +end +function AI_A2A_DISPATCHER:SetSendMessages(onoff) +self.SetSendPlayerMessages=onoff +end +function AI_A2A_DISPATCHER:SetSquadronTakeoffInAir(SquadronName,TakeoffAltitude) +self:SetSquadronTakeoff(SquadronName,AI_A2A_DISPATCHER.Takeoff.Air) +if TakeoffAltitude then +self:SetSquadronTakeoffInAirAltitude(SquadronName,TakeoffAltitude) +end +return self +end +function AI_A2A_DISPATCHER:SetDefaultTakeoffFromRunway() +self:SetDefaultTakeoff(AI_A2A_DISPATCHER.Takeoff.Runway) +return self +end +function AI_A2A_DISPATCHER:SetSquadronTakeoffFromRunway(SquadronName) +self:SetSquadronTakeoff(SquadronName,AI_A2A_DISPATCHER.Takeoff.Runway) +return self +end +function AI_A2A_DISPATCHER:SetDefaultTakeoffFromParkingHot() +self:SetDefaultTakeoff(AI_A2A_DISPATCHER.Takeoff.Hot) +return self +end +function AI_A2A_DISPATCHER:SetSquadronTakeoffFromParkingHot(SquadronName) +self:SetSquadronTakeoff(SquadronName,AI_A2A_DISPATCHER.Takeoff.Hot) +return self +end +function AI_A2A_DISPATCHER:SetDefaultTakeoffFromParkingCold() +self:SetDefaultTakeoff(AI_A2A_DISPATCHER.Takeoff.Cold) +return self +end +function AI_A2A_DISPATCHER:SetSquadronTakeoffFromParkingCold(SquadronName) +self:SetSquadronTakeoff(SquadronName,AI_A2A_DISPATCHER.Takeoff.Cold) +return self +end +function AI_A2A_DISPATCHER:SetDefaultTakeoffInAirAltitude(TakeoffAltitude) +self.DefenderDefault.TakeoffAltitude=TakeoffAltitude +return self +end +function AI_A2A_DISPATCHER:SetSquadronTakeoffInAirAltitude(SquadronName,TakeoffAltitude) +local DefenderSquadron=self:GetSquadron(SquadronName) +DefenderSquadron.TakeoffAltitude=TakeoffAltitude +return self +end +function AI_A2A_DISPATCHER:SetDefaultLanding(Landing) +self.DefenderDefault.Landing=Landing +return self +end +function AI_A2A_DISPATCHER:SetSquadronLanding(SquadronName,Landing) +local DefenderSquadron=self:GetSquadron(SquadronName) +DefenderSquadron.Landing=Landing +return self +end +function AI_A2A_DISPATCHER:GetDefaultLanding() +return self.DefenderDefault.Landing +end +function AI_A2A_DISPATCHER:GetSquadronLanding(SquadronName) +local DefenderSquadron=self:GetSquadron(SquadronName) +return DefenderSquadron.Landing or self.DefenderDefault.Landing +end +function AI_A2A_DISPATCHER:SetDefaultLandingNearAirbase() +self:SetDefaultLanding(AI_A2A_DISPATCHER.Landing.NearAirbase) +return self +end +function AI_A2A_DISPATCHER:SetSquadronLandingNearAirbase(SquadronName) +self:SetSquadronLanding(SquadronName,AI_A2A_DISPATCHER.Landing.NearAirbase) +return self +end +function AI_A2A_DISPATCHER:SetDefaultLandingAtRunway() +self:SetDefaultLanding(AI_A2A_DISPATCHER.Landing.AtRunway) +return self +end +function AI_A2A_DISPATCHER:SetSquadronLandingAtRunway(SquadronName) +self:SetSquadronLanding(SquadronName,AI_A2A_DISPATCHER.Landing.AtRunway) +return self +end +function AI_A2A_DISPATCHER:SetDefaultLandingAtEngineShutdown() +self:SetDefaultLanding(AI_A2A_DISPATCHER.Landing.AtEngineShutdown) +return self +end +function AI_A2A_DISPATCHER:SetSquadronLandingAtEngineShutdown(SquadronName) +self:SetSquadronLanding(SquadronName,AI_A2A_DISPATCHER.Landing.AtEngineShutdown) +return self +end +function AI_A2A_DISPATCHER:SetDefaultFuelThreshold(FuelThreshold) +self.DefenderDefault.FuelThreshold=FuelThreshold +return self +end +function AI_A2A_DISPATCHER:SetSquadronFuelThreshold(SquadronName,FuelThreshold) +local DefenderSquadron=self:GetSquadron(SquadronName) +DefenderSquadron.FuelThreshold=FuelThreshold +return self +end +function AI_A2A_DISPATCHER:SetDefaultTanker(TankerName) +self.DefenderDefault.TankerName=TankerName +return self +end +function AI_A2A_DISPATCHER:SetSquadronTanker(SquadronName,TankerName) +local DefenderSquadron=self:GetSquadron(SquadronName) +DefenderSquadron.TankerName=TankerName +return self +end +function AI_A2A_DISPATCHER:SetSquadronLanguage(SquadronName,Language) +local DefenderSquadron=self:GetSquadron(SquadronName) +DefenderSquadron.Language=Language +if DefenderSquadron.RadioQueue then +DefenderSquadron.RadioQueue:SetLanguage(Language) +end +return self +end +function AI_A2A_DISPATCHER:SetSquadronRadioFrequency(SquadronName,RadioFrequency,RadioModulation,RadioPower) +local DefenderSquadron=self:GetSquadron(SquadronName) +DefenderSquadron.RadioFrequency=RadioFrequency +DefenderSquadron.RadioModulation=RadioModulation or radio.modulation.AM +DefenderSquadron.RadioPower=RadioPower or 100 +if DefenderSquadron.RadioQueue then +DefenderSquadron.RadioQueue:Stop() +end +DefenderSquadron.RadioQueue=nil +DefenderSquadron.RadioQueue=RADIOSPEECH:New(DefenderSquadron.RadioFrequency,DefenderSquadron.RadioModulation) +DefenderSquadron.RadioQueue.power=DefenderSquadron.RadioPower +DefenderSquadron.RadioQueue:Start(0.5) +DefenderSquadron.RadioQueue:SetLanguage(DefenderSquadron.Language) +end +function AI_A2A_DISPATCHER:AddDefenderToSquadron(Squadron,Defender,Size) +self.Defenders=self.Defenders or{} +local DefenderName=Defender:GetName() +self.Defenders[DefenderName]=Squadron +if Squadron.ResourceCount then +Squadron.ResourceCount=Squadron.ResourceCount-Size +end +self:F({DefenderName=DefenderName,SquadronResourceCount=Squadron.ResourceCount}) +end +function AI_A2A_DISPATCHER:RemoveDefenderFromSquadron(Squadron,Defender) +self.Defenders=self.Defenders or{} +local DefenderName=Defender:GetName() +if Squadron.ResourceCount then +Squadron.ResourceCount=Squadron.ResourceCount+Defender:GetSize() +end +self.Defenders[DefenderName]=nil +self:F({DefenderName=DefenderName,SquadronResourceCount=Squadron.ResourceCount}) +end +function AI_A2A_DISPATCHER:GetSquadronFromDefender(Defender) +self.Defenders=self.Defenders or{} +if Defender~=nil then +local DefenderName=Defender:GetName() +self:F({DefenderName=DefenderName}) +return self.Defenders[DefenderName] +else +return nil +end +end +function AI_A2A_DISPATCHER:EvaluateSWEEP(DetectedItem) +self:F({DetectedItem.ItemID}) +local DetectedSet=DetectedItem.Set +local DetectedZone=DetectedItem.Zone +if DetectedItem.IsDetected==false then +local TargetSetUnit=SET_UNIT:New() +TargetSetUnit:SetDatabase(DetectedSet) +TargetSetUnit:FilterOnce() +return TargetSetUnit +end +return nil +end +function AI_A2A_DISPATCHER:CountCapAirborne(SquadronName) +local CapCount=0 +local DefenderSquadron=self.DefenderSquadrons[SquadronName] +if DefenderSquadron then +for AIGroup,DefenderTask in pairs(self:GetDefenderTasks())do +if DefenderTask.SquadronName==SquadronName then +if DefenderTask.Type=="CAP"then +if AIGroup and AIGroup:IsAlive()then +if DefenderTask.Fsm:Is("Patrolling")or DefenderTask.Fsm:Is("Engaging")or DefenderTask.Fsm:Is("Refuelling")or DefenderTask.Fsm:Is("Started")then +CapCount=CapCount+1 +end +end +end +end +end +end +return CapCount +end +function AI_A2A_DISPATCHER:CountDefendersEngaged(AttackerDetection) +local DefenderCount=0 +local DetectedSet=AttackerDetection.Set +local DefenderTasks=self:GetDefenderTasks() +for DefenderGroup,DefenderTask in pairs(DefenderTasks)do +local Defender=DefenderGroup +local DefenderTaskTarget=DefenderTask.Target +local DefenderSquadronName=DefenderTask.SquadronName +if DefenderTaskTarget and DefenderTaskTarget.Index==AttackerDetection.Index then +local Squadron=self:GetSquadron(DefenderSquadronName) +local SquadronOverhead=Squadron.Overhead or self.DefenderDefault.Overhead +local DefenderSize=Defender:GetInitialSize() +if DefenderSize then +DefenderCount=DefenderCount+DefenderSize/SquadronOverhead +self:F("Defender Group Name: "..Defender:GetName()..", Size: "..DefenderSize) +else +DefenderCount=0 +end +end +end +self:F({DefenderCount=DefenderCount}) +return DefenderCount +end +function AI_A2A_DISPATCHER:CountDefendersToBeEngaged(AttackerDetection,DefenderCount) +local Friendlies=nil +local AttackerSet=AttackerDetection.Set +local AttackerCount=AttackerSet:Count() +local DefenderFriendlies=self:GetAIFriendliesNearBy(AttackerDetection) +for FriendlyDistance,AIFriendly in UTILS.spairs(DefenderFriendlies or{})do +if AttackerCount>DefenderCount then +local Friendly=AIFriendly:GetGroup() +if Friendly and Friendly:IsAlive()then +local DefenderTask=self:GetDefenderTask(Friendly) +if DefenderTask then +if DefenderTask.Type=="CAP"or DefenderTask.Type=="GCI"then +if DefenderTask.Target==nil then +if DefenderTask.Fsm:Is("Returning")or DefenderTask.Fsm:Is("Patrolling")then +Friendlies=Friendlies or{} +Friendlies[Friendly]=Friendly +DefenderCount=DefenderCount+Friendly:GetSize() +self:F({Friendly=Friendly:GetName(),FriendlyDistance=FriendlyDistance}) +end +end +end +end +end +else +break +end +end +return Friendlies +end +function AI_A2A_DISPATCHER:ResourceActivate(DefenderSquadron,DefendersNeeded) +local SquadronName=DefenderSquadron.Name +DefendersNeeded=DefendersNeeded or 4 +local DefenderGrouping=DefenderSquadron.Grouping or self.DefenderDefault.Grouping +DefenderGrouping=(DefenderGrouping0 then +local id=math.random(n) +local Defender=self.uncontrolled[SquadronName][id].group +Defender:StartUncontrolled() +DefenderGrouping=self.uncontrolled[SquadronName][id].grouping +self:AddDefenderToSquadron(DefenderSquadron,Defender,DefenderGrouping) +table.remove(self.uncontrolled[SquadronName],id) +return Defender,DefenderGrouping +else +return nil,0 +end +local TemplateID=math.random(1,#DefenderSquadron.Spawn) +else +local Spawn=DefenderSquadron.Spawn[math.random(1,#DefenderSquadron.Spawn)] +if DefenderGrouping then +Spawn:InitGrouping(DefenderGrouping) +else +Spawn:InitGrouping() +end +local TakeoffMethod=self:GetSquadronTakeoff(SquadronName) +local Defender=Spawn:SpawnAtAirbase(DefenderSquadron.Airbase,TakeoffMethod,DefenderSquadron.TakeoffAltitude or self.DefenderDefault.TakeoffAltitude) +self:AddDefenderToSquadron(DefenderSquadron,Defender,DefenderGrouping) +return Defender,DefenderGrouping +end +return nil,nil +end +function AI_A2A_DISPATCHER:onafterCAP(From,Event,To,SquadronName) +self:F({SquadronName=SquadronName}) +self.DefenderSquadrons[SquadronName]=self.DefenderSquadrons[SquadronName]or{} +self.DefenderSquadrons[SquadronName].Cap=self.DefenderSquadrons[SquadronName].Cap or{} +local DefenderSquadron=self:CanCAP(SquadronName) +if DefenderSquadron then +local Cap=DefenderSquadron.Cap +if Cap then +local DefenderCAP,DefenderGrouping=self:ResourceActivate(DefenderSquadron) +if DefenderCAP then +local AI_A2A_Fsm=AI_A2A_CAP:New2(DefenderCAP,Cap.EngageMinSpeed,Cap.EngageMaxSpeed,Cap.EngageFloorAltitude,Cap.EngageCeilingAltitude,Cap.EngageAltType,Cap.Zone,Cap.PatrolMinSpeed,Cap.PatrolMaxSpeed,Cap.PatrolFloorAltitude,Cap.PatrolCeilingAltitude,Cap.PatrolAltType) +AI_A2A_Fsm:SetDispatcher(self) +AI_A2A_Fsm:SetHomeAirbase(DefenderSquadron.Airbase) +AI_A2A_Fsm:SetFuelThreshold(DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold,60) +AI_A2A_Fsm:SetDamageThreshold(self.DefenderDefault.DamageThreshold) +AI_A2A_Fsm:SetDisengageRadius(self.DisengageRadius) +AI_A2A_Fsm:SetTanker(DefenderSquadron.TankerName or self.DefenderDefault.TankerName) +if DefenderSquadron.Racetrack or self.DefenderDefault.Racetrack then +AI_A2A_Fsm:SetRaceTrackPattern(DefenderSquadron.RacetrackLengthMin or self.DefenderDefault.RacetrackLengthMin, +DefenderSquadron.RacetrackLengthMax or self.DefenderDefault.RacetrackLengthMax, +DefenderSquadron.RacetrackHeadingMin or self.DefenderDefault.RacetrackHeadingMin, +DefenderSquadron.RacetrackHeadingMax or self.DefenderDefault.RacetrackHeadingMax, +DefenderSquadron.RacetrackDurationMin or self.DefenderDefault.RacetrackDurationMin, +DefenderSquadron.RacetrackDurationMax or self.DefenderDefault.RacetrackDurationMax, +DefenderSquadron.RacetrackCoordinates or self.DefenderDefault.RacetrackCoordinates) +end +AI_A2A_Fsm:Start() +self:SetDefenderTask(SquadronName,DefenderCAP,"CAP",AI_A2A_Fsm) +function AI_A2A_Fsm:onafterTakeoff(DefenderGroup,From,Event,To) +if DefenderGroup and DefenderGroup:IsAlive()then +self:F({"CAP Takeoff",DefenderGroup:GetName()}) +local DefenderName=DefenderGroup:GetCallsign() +local Dispatcher=AI_A2A_Fsm:GetDispatcher() +local Squadron=Dispatcher:GetSquadronFromDefender(DefenderGroup) +if Squadron then +if self.SetSendPlayerMessages then +Dispatcher:MessageToPlayers(Squadron,DefenderName.." Wheels up.",DefenderGroup) +end +AI_A2A_Fsm:__Patrol(2) +end +end +end +function AI_A2A_Fsm:onafterPatrolRoute(DefenderGroup,From,Event,To) +if DefenderGroup and DefenderGroup:IsAlive()then +self:F({"CAP PatrolRoute",DefenderGroup:GetName()}) +self:GetParent(self).onafterPatrolRoute(self,DefenderGroup,From,Event,To) +local DefenderName=DefenderGroup:GetCallsign() +local Dispatcher=self:GetDispatcher() +local Squadron=Dispatcher:GetSquadronFromDefender(DefenderGroup) +if Squadron and self.SetSendPlayerMessages then +Dispatcher:MessageToPlayers(Squadron,DefenderName..", patrolling.",DefenderGroup) +end +Dispatcher:ClearDefenderTaskTarget(DefenderGroup) +end +end +function AI_A2A_Fsm:onafterRTB(DefenderGroup,From,Event,To) +if DefenderGroup and DefenderGroup:IsAlive()then +self:F({"CAP RTB",DefenderGroup:GetName()}) +self:GetParent(self).onafterRTB(self,DefenderGroup,From,Event,To) +local DefenderName=DefenderGroup:GetCallsign() +local Dispatcher=self:GetDispatcher() +local Squadron=Dispatcher:GetSquadronFromDefender(DefenderGroup) +if Squadron and self.SetSendPlayerMessages then +Dispatcher:MessageToPlayers(Squadron,DefenderName.." returning to base.",DefenderGroup) +end +Dispatcher:ClearDefenderTaskTarget(DefenderGroup) +end +end +function AI_A2A_Fsm:onafterHome(Defender,From,Event,To,Action) +if Defender and Defender:IsAlive()then +self:F({"CAP Home",Defender:GetName()}) +self:GetParent(self).onafterHome(self,Defender,From,Event,To) +local Dispatcher=self:GetDispatcher() +local Squadron=Dispatcher:GetSquadronFromDefender(Defender) +if Action and Action=="Destroy"then +Dispatcher:RemoveDefenderFromSquadron(Squadron,Defender) +Defender:Destroy() +end +if Dispatcher:GetSquadronLanding(Squadron.Name)==AI_A2A_DISPATCHER.Landing.NearAirbase then +Dispatcher:RemoveDefenderFromSquadron(Squadron,Defender) +Defender:Destroy() +Dispatcher:ParkDefender(Squadron) +end +end +end +end +end +end +end +function AI_A2A_DISPATCHER:onafterENGAGE(From,Event,To,AttackerDetection,Defenders) +self:F("ENGAGING Detection ID="..tostring(AttackerDetection.ID)) +if Defenders then +for DefenderID,Defender in pairs(Defenders)do +local Fsm=self:GetDefenderTaskFsm(Defender) +Fsm:EngageRoute(AttackerDetection.Set) +self:SetDefenderTaskTarget(Defender,AttackerDetection) +end +end +end +function AI_A2A_DISPATCHER:onafterGCI(From,Event,To,AttackerDetection,DefendersMissing,DefenderFriendlies) +self:F("GCI Detection ID="..tostring(AttackerDetection.ID)) +self:F({From,Event,To,AttackerDetection.Index,DefendersMissing,DefenderFriendlies}) +local AttackerSet=AttackerDetection.Set +local AttackerUnit=AttackerSet:GetFirst() +if AttackerUnit and AttackerUnit:IsAlive()then +local AttackerCount=AttackerSet:Count() +local DefenderCount=0 +for DefenderID,DefenderGroup in pairs(DefenderFriendlies or{})do +local Fsm=self:GetDefenderTaskFsm(DefenderGroup) +Fsm:__EngageRoute(0.1,AttackerSet) +self:SetDefenderTaskTarget(DefenderGroup,AttackerDetection) +DefenderCount=DefenderCount+DefenderGroup:GetSize() +end +self:F({DefenderCount=DefenderCount,DefendersMissing=DefendersMissing}) +DefenderCount=DefendersMissing +local ClosestDistance=0 +local ClosestDefenderSquadronName=nil +local BreakLoop=false +while(DefenderCount>0 and not BreakLoop)do +self:F({DefenderSquadrons=self.DefenderSquadrons}) +for SquadronName,DefenderSquadron in pairs(self.DefenderSquadrons or{})do +self:F({GCI=DefenderSquadron.Gci}) +for InterceptID,Intercept in pairs(DefenderSquadron.Gci or{})do +self:F({DefenderSquadron}) +local SpawnCoord=DefenderSquadron.Airbase:GetCoordinate() +local AttackerCoord=AttackerUnit:GetCoordinate() +local InterceptCoord=AttackerDetection.InterceptCoord +self:F({InterceptCoord=InterceptCoord}) +if InterceptCoord then +local InterceptDistance=SpawnCoord:Get2DDistance(InterceptCoord) +local AirbaseDistance=SpawnCoord:Get2DDistance(AttackerCoord) +self:F({InterceptDistance=InterceptDistance,AirbaseDistance=AirbaseDistance,InterceptCoord=InterceptCoord}) +if ClosestDistance==0 or InterceptDistanceDefenderSquadron.ResourceCount then +DefendersNeeded=DefenderSquadron.ResourceCount +BreakLoop=true +end +while(DefendersNeeded>0)do +local DefenderGCI,DefenderGrouping=self:ResourceActivate(DefenderSquadron,DefendersNeeded) +DefendersNeeded=DefendersNeeded-DefenderGrouping +if DefenderGCI then +DefenderCount=DefenderCount-DefenderGrouping/DefenderOverhead +local Fsm=AI_A2A_GCI:New2(DefenderGCI,Gci.EngageMinSpeed,Gci.EngageMaxSpeed,Gci.EngageFloorAltitude,Gci.EngageCeilingAltitude,Gci.EngageAltType) +Fsm:SetDispatcher(self) +Fsm:SetHomeAirbase(DefenderSquadron.Airbase) +Fsm:SetFuelThreshold(DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold,60) +Fsm:SetDamageThreshold(self.DefenderDefault.DamageThreshold) +Fsm:SetDisengageRadius(self.DisengageRadius) +Fsm:Start() +self:SetDefenderTask(ClosestDefenderSquadronName,DefenderGCI,"GCI",Fsm,AttackerDetection) +function Fsm:onafterTakeoff(DefenderGroup,From,Event,To) +self:F({"GCI Birth",DefenderGroup:GetName()}) +local DefenderName=DefenderGroup:GetCallsign() +local Dispatcher=Fsm:GetDispatcher() +local Squadron=Dispatcher:GetSquadronFromDefender(DefenderGroup) +local DefenderTarget=Dispatcher:GetDefenderTaskTarget(DefenderGroup) +if DefenderTarget then +if Squadron.Language=="EN"and self.SetSendPlayerMessages then +Dispatcher:MessageToPlayers(Squadron,DefenderName.." wheels up.",DefenderGroup) +elseif Squadron.Language=="RU"and self.SetSendPlayerMessages then +Dispatcher:MessageToPlayers(Squadron,DefenderName.." колеÑ�а вверх.",DefenderGroup) +end +Fsm:EngageRoute(DefenderTarget.Set) +end +end +function Fsm:onafterEngageRoute(DefenderGroup,From,Event,To,AttackSetUnit) +self:F({"GCI Route",DefenderGroup:GetName()}) +local DefenderName=DefenderGroup:GetCallsign() +local Dispatcher=Fsm:GetDispatcher() +local Squadron=Dispatcher:GetSquadronFromDefender(DefenderGroup) +if Squadron and AttackSetUnit:Count()>0 then +local FirstUnit=AttackSetUnit:GetFirst() +local Coordinate=FirstUnit:GetCoordinate() +if Squadron.Language=="EN"and self.SetSendPlayerMessages then +Dispatcher:MessageToPlayers(Squadron,DefenderName..", intercepting bogeys at "..Coordinate:ToStringA2A(DefenderGroup,nil,Squadron.Language),DefenderGroup) +elseif Squadron.Language=="RU"and self.SetSendPlayerMessages then +Dispatcher:MessageToPlayers(Squadron,DefenderName..", перехват Ñ�амолетов в "..Coordinate:ToStringA2A(DefenderGroup,nil,Squadron.Language),DefenderGroup) +elseif Squadron.Language=="DE"and self.SetSendPlayerMessages then +Dispatcher:MessageToPlayers(Squadron,DefenderName..", Eindringlinge abfangen bei"..Coordinate:ToStringA2A(DefenderGroup,nil,Squadron.Language),DefenderGroup) +end +end +self:GetParent(Fsm).onafterEngageRoute(self,DefenderGroup,From,Event,To,AttackSetUnit) +end +function Fsm:onafterEngage(DefenderGroup,From,Event,To,AttackSetUnit) +self:F({"GCI Engage",DefenderGroup:GetName()}) +local DefenderName=DefenderGroup:GetCallsign() +local Dispatcher=Fsm:GetDispatcher() +local Squadron=Dispatcher:GetSquadronFromDefender(DefenderGroup) +if Squadron and AttackSetUnit:Count()>0 then +local FirstUnit=AttackSetUnit:GetFirst() +local Coordinate=FirstUnit:GetCoordinate() +if Squadron.Language=="EN"and self.SetSendPlayerMessages then +Dispatcher:MessageToPlayers(Squadron,DefenderName..", engaging bogeys at "..Coordinate:ToStringA2A(DefenderGroup,nil,Squadron.Language),DefenderGroup) +elseif Squadron.Language=="RU"and self.SetSendPlayerMessages then +Dispatcher:MessageToPlayers(Squadron,DefenderName..", захватывающие Ñ�амолеты в "..Coordinate:ToStringA2A(DefenderGroup,nil,Squadron.Language),DefenderGroup) +end +end +self:GetParent(Fsm).onafterEngage(self,DefenderGroup,From,Event,To,AttackSetUnit) +end +function Fsm:onafterRTB(DefenderGroup,From,Event,To) +self:F({"GCI RTB",DefenderGroup:GetName()}) +self:GetParent(self).onafterRTB(self,DefenderGroup,From,Event,To) +local DefenderName=DefenderGroup:GetCallsign() +local Dispatcher=self:GetDispatcher() +local Squadron=Dispatcher:GetSquadronFromDefender(DefenderGroup) +if Squadron then +if Squadron.Language=="EN"and self.SetSendPlayerMessages then +Dispatcher:MessageToPlayers(Squadron,DefenderName.." returning to base.",DefenderGroup) +elseif Squadron.Language=="RU"and self.SetSendPlayerMessages then +Dispatcher:MessageToPlayers(Squadron,DefenderName..", возвращаÑ�Ñ�ÑŒ на базу.",DefenderGroup) +end +end +Dispatcher:ClearDefenderTaskTarget(DefenderGroup) +end +function Fsm:onafterLostControl(Defender,From,Event,To) +self:F({"GCI LostControl",Defender:GetName()}) +self:GetParent(self).onafterHome(self,Defender,From,Event,To) +local Dispatcher=Fsm:GetDispatcher() +local Squadron=Dispatcher:GetSquadronFromDefender(Defender) +if Defender:IsAboveRunway()then +Dispatcher:RemoveDefenderFromSquadron(Squadron,Defender) +Defender:Destroy() +end +end +function Fsm:onafterHome(DefenderGroup,From,Event,To,Action) +self:F({"GCI Home",DefenderGroup:GetName()}) +self:GetParent(self).onafterHome(self,DefenderGroup,From,Event,To) +local DefenderName=DefenderGroup:GetCallsign() +local Dispatcher=self:GetDispatcher() +local Squadron=Dispatcher:GetSquadronFromDefender(DefenderGroup) +if Squadron.Language=="EN"and self.SetSendPlayerMessages then +Dispatcher:MessageToPlayers(Squadron,DefenderName.." landing at base.",DefenderGroup) +elseif Squadron.Language=="RU"and self.SetSendPlayerMessages then +Dispatcher:MessageToPlayers(Squadron,DefenderName..", захватывающие Ñ�амолеты в поÑ�адка на базу.",DefenderGroup) +end +if Action and Action=="Destroy"then +Dispatcher:RemoveDefenderFromSquadron(Squadron,DefenderGroup) +DefenderGroup:Destroy() +end +if Dispatcher:GetSquadronLanding(Squadron.Name)==AI_A2A_DISPATCHER.Landing.NearAirbase then +Dispatcher:RemoveDefenderFromSquadron(Squadron,DefenderGroup) +DefenderGroup:Destroy() +Dispatcher:ParkDefender(Squadron) +end +end +end +end +end +else +BreakLoop=true +break +end +else +break +end +end +end +end +function AI_A2A_DISPATCHER:EvaluateENGAGE(DetectedItem) +self:F({DetectedItem.ItemID}) +local DefenderCount=self:CountDefendersEngaged(DetectedItem) +local DefenderGroups=self:CountDefendersToBeEngaged(DetectedItem,DefenderCount) +self:F({DefenderCount=DefenderCount}) +if DefenderGroups and DetectedItem.IsDetected==true then +return DefenderGroups +end +return nil +end +function AI_A2A_DISPATCHER:EvaluateGCI(DetectedItem) +self:F({DetectedItem.ItemID}) +local AttackerSet=DetectedItem.Set +local AttackerCount=AttackerSet:Count() +local DefenderCount=self:CountDefendersEngaged(DetectedItem) +local DefendersMissing=AttackerCount-DefenderCount +self:F({AttackerCount=AttackerCount,DefenderCount=DefenderCount,DefendersMissing=DefendersMissing}) +local Friendlies=self:CountDefendersToBeEngaged(DetectedItem,DefenderCount) +if DetectedItem.IsDetected==true then +return DefendersMissing,Friendlies +end +return nil,nil +end +function AI_A2A_DISPATCHER:Order(DetectedItem) +local detection=self.Detection +local ShortestDistance=999999999 +local AttackCoordinate=detection:GetDetectedItemCoordinate(DetectedItem) +if AttackCoordinate then +for DefenderSquadronName,DefenderSquadron in pairs(self.DefenderSquadrons)do +self:T({DefenderSquadron=DefenderSquadron.Name}) +local Airbase=DefenderSquadron.Airbase +local AirbaseCoordinate=Airbase:GetCoordinate() +local EvaluateDistance=AttackCoordinate:Get2DDistance(AirbaseCoordinate) +if EvaluateDistance<=ShortestDistance then +ShortestDistance=EvaluateDistance +end +end +end +return ShortestDistance +end +function AI_A2A_DISPATCHER:ShowTacticalDisplay(Detection) +local AreaMsg={} +local TaskMsg={} +local ChangeMsg={} +local TaskReport=REPORT:New() +local Report=REPORT:New("Tactical Overview:") +local DefenderGroupCount=0 +for DetectedItemID,DetectedItem in UTILS.spairs(Detection:GetDetectedItems(),function(t,a,b)return self:Order(t[a])0 then +self:F({DefendersMissing=DefendersMissing}) +self:GCI(DetectedItem,DefendersMissing,Friendlies) +end +end +end +if self.TacticalDisplay then +self:ShowTacticalDisplay(Detection) +end +return true +end +end +do +function AI_A2A_DISPATCHER:GetPlayerFriendliesNearBy(DetectedItem) +local DetectedSet=DetectedItem.Set +local PlayersNearBy=self.Detection:GetPlayersNearBy(DetectedItem) +local PlayerTypes={} +local PlayersCount=0 +if PlayersNearBy then +local DetectedTreatLevel=DetectedSet:CalculateThreatLevelA2G() +for PlayerUnitName,PlayerUnitData in pairs(PlayersNearBy)do +local PlayerUnit=PlayerUnitData +local PlayerName=PlayerUnit:GetPlayerName() +if PlayerUnit:IsAirPlane()and PlayerName~=nil then +local FriendlyUnitThreatLevel=PlayerUnit:GetThreatLevel() +PlayersCount=PlayersCount+1 +local PlayerType=PlayerUnit:GetTypeName() +PlayerTypes[PlayerName]=PlayerType +if DetectedTreatLevel0 then +for PlayerName,PlayerType in pairs(PlayerTypes)do +PlayerTypesReport:Add(string.format('"%s" in %s',PlayerName,PlayerType)) +end +else +PlayerTypesReport:Add("-") +end +return PlayersCount,PlayerTypesReport +end +function AI_A2A_DISPATCHER:GetFriendliesNearBy(DetectedItem) +local DetectedSet=DetectedItem.Set +local FriendlyUnitsNearBy=self.Detection:GetFriendliesNearBy(DetectedItem) +local FriendlyTypes={} +local FriendliesCount=0 +if FriendlyUnitsNearBy then +local DetectedTreatLevel=DetectedSet:CalculateThreatLevelA2G() +for FriendlyUnitName,FriendlyUnitData in pairs(FriendlyUnitsNearBy)do +local FriendlyUnit=FriendlyUnitData +if FriendlyUnit:IsAirPlane()then +local FriendlyUnitThreatLevel=FriendlyUnit:GetThreatLevel() +FriendliesCount=FriendliesCount+1 +local FriendlyType=FriendlyUnit:GetTypeName() +FriendlyTypes[FriendlyType]=FriendlyTypes[FriendlyType]and(FriendlyTypes[FriendlyType]+1)or 1 +if DetectedTreatLevel0 then +for FriendlyType,FriendlyTypeCount in pairs(FriendlyTypes)do +FriendlyTypesReport:Add(string.format("%d of %s",FriendlyTypeCount,FriendlyType)) +end +else +FriendlyTypesReport:Add("-") +end +return FriendliesCount,FriendlyTypesReport +end +function AI_A2A_DISPATCHER:SchedulerCAP(SquadronName) +self:CAP(SquadronName) +end +end +do +AI_A2A_GCICAP={ +ClassName="AI_A2A_GCICAP", +Detection=nil, +} +function AI_A2A_GCICAP:New(EWRPrefixes,TemplatePrefixes,CapPrefixes,CapLimit,GroupingRadius,EngageRadius,GciRadius,ResourceCount) +local EWRSetGroup=SET_GROUP:New() +EWRSetGroup:FilterPrefixes(EWRPrefixes) +EWRSetGroup:FilterStart() +local Detection=DETECTION_AREAS:New(EWRSetGroup,GroupingRadius or 30000) +local self=BASE:Inherit(self,AI_A2A_DISPATCHER:New(Detection)) +self:SetEngageRadius(EngageRadius) +self:SetGciRadius(GciRadius) +local EWRFirst=EWRSetGroup:GetFirst() +local EWRCoalition=EWRFirst:GetCoalition() +local AirbaseNames={} +for AirbaseID,AirbaseData in pairs(_DATABASE.AIRBASES)do +local Airbase=AirbaseData +local AirbaseName=Airbase:GetName() +if Airbase:GetCoalition()==EWRCoalition then +table.insert(AirbaseNames,AirbaseName) +end +end +self.Templates=SET_GROUP +:New() +:FilterPrefixes(TemplatePrefixes) +:FilterOnce() +self:I({Airbases=AirbaseNames}) +self:I("Defining Templates for Airbases ...") +for AirbaseID,AirbaseName in pairs(AirbaseNames)do +local Airbase=_DATABASE:FindAirbase(AirbaseName) +local AirbaseName=Airbase:GetName() +local AirbaseCoord=Airbase:GetCoordinate() +local AirbaseZone=ZONE_RADIUS:New("Airbase",AirbaseCoord:GetVec2(),3000) +local Templates=nil +self:I({Airbase=AirbaseName}) +for TemplateID,Template in pairs(self.Templates:GetSet())do +local Template=Template +local TemplateCoord=Template:GetCoordinate() +if AirbaseZone:IsVec2InZone(TemplateCoord:GetVec2())then +Templates=Templates or{} +table.insert(Templates,Template:GetName()) +self:I({Template=Template:GetName()}) +end +end +if Templates then +self:SetSquadron(AirbaseName,AirbaseName,Templates,ResourceCount) +end +end +self.CAPTemplates=SET_GROUP:New() +self.CAPTemplates:FilterPrefixes(CapPrefixes) +self.CAPTemplates:FilterOnce() +self:I("Setting up CAP ...") +for CAPID,CAPTemplate in pairs(self.CAPTemplates:GetSet())do +local CAPZone=ZONE_POLYGON:New(CAPTemplate:GetName(),CAPTemplate) +local AirbaseDistance=99999999 +local AirbaseClosest=nil +self:I({CAPZoneGroup=CAPID}) +for AirbaseID,AirbaseName in pairs(AirbaseNames)do +local Airbase=_DATABASE:FindAirbase(AirbaseName) +local AirbaseName=Airbase:GetName() +local AirbaseCoord=Airbase:GetCoordinate() +local Squadron=self.DefenderSquadrons[AirbaseName] +if Squadron then +local Distance=AirbaseCoord:Get2DDistance(CAPZone:GetCoordinate()) +self:I({AirbaseDistance=Distance}) +if Distance0)then +local Patrol=DefenderSquadron[DefenseTaskType] +if Patrol and Patrol.Patrol==true then +local PatrolCount=self:CountPatrolAirborne(SquadronName,DefenseTaskType) +self:F({PatrolCount=PatrolCount,PatrolLimit=Patrol.PatrolLimit,PatrolProbability=Patrol.Probability}) +if PatrolCount0)then +if DefenderSquadron[DefenseTaskType]and(DefenderSquadron[DefenseTaskType].Defend==true)then +return DefenderSquadron,DefenderSquadron[DefenseTaskType] +end +end +end +return nil +end +function AI_A2G_DISPATCHER:SetSquadronEngageLimit(SquadronName,EngageLimit,DefenseTaskType) +local DefenderSquadron=self:GetSquadron(SquadronName) +local Defense=DefenderSquadron[DefenseTaskType] +if Defense then +Defense.EngageLimit=EngageLimit or 1 +else +error("This squadron does not exist:"..SquadronName) +end +end +function AI_A2G_DISPATCHER:SetSquadronSead2(SquadronName,EngageMinSpeed,EngageMaxSpeed,EngageFloorAltitude,EngageCeilingAltitude,EngageAltType) +local DefenderSquadron=self:GetSquadron(SquadronName) +DefenderSquadron.SEAD=DefenderSquadron.SEAD or{} +local Sead=DefenderSquadron.SEAD +Sead.Name=SquadronName +Sead.EngageMinSpeed=EngageMinSpeed +Sead.EngageMaxSpeed=EngageMaxSpeed +Sead.EngageFloorAltitude=EngageFloorAltitude or 500 +Sead.EngageCeilingAltitude=EngageCeilingAltitude or 1000 +Sead.EngageAltType=EngageAltType +Sead.Defend=true +self:I({SEAD={SquadronName,EngageMinSpeed,EngageMaxSpeed,EngageFloorAltitude,EngageCeilingAltitude,EngageAltType}}) +return self +end +function AI_A2G_DISPATCHER:SetSquadronSead(SquadronName,EngageMinSpeed,EngageMaxSpeed,EngageFloorAltitude,EngageCeilingAltitude) +return self:SetSquadronSead2(SquadronName,EngageMinSpeed,EngageMaxSpeed,EngageFloorAltitude,EngageCeilingAltitude,"RADIO") +end +function AI_A2G_DISPATCHER:SetSquadronSeadEngageLimit(SquadronName,EngageLimit) +self:SetSquadronEngageLimit(SquadronName,EngageLimit,"SEAD") +end +function AI_A2G_DISPATCHER:SetSquadronSeadPatrol2(SquadronName,Zone,PatrolMinSpeed,PatrolMaxSpeed,PatrolFloorAltitude,PatrolCeilingAltitude,PatrolAltType,EngageMinSpeed,EngageMaxSpeed,EngageFloorAltitude,EngageCeilingAltitude,EngageAltType) +local DefenderSquadron=self:GetSquadron(SquadronName) +DefenderSquadron.SEAD=DefenderSquadron.SEAD or{} +local SeadPatrol=DefenderSquadron.SEAD +SeadPatrol.Name=SquadronName +SeadPatrol.Zone=Zone +SeadPatrol.PatrolFloorAltitude=PatrolFloorAltitude +SeadPatrol.PatrolCeilingAltitude=PatrolCeilingAltitude +SeadPatrol.EngageFloorAltitude=EngageFloorAltitude +SeadPatrol.EngageCeilingAltitude=EngageCeilingAltitude +SeadPatrol.PatrolMinSpeed=PatrolMinSpeed +SeadPatrol.PatrolMaxSpeed=PatrolMaxSpeed +SeadPatrol.EngageMinSpeed=EngageMinSpeed +SeadPatrol.EngageMaxSpeed=EngageMaxSpeed +SeadPatrol.PatrolAltType=PatrolAltType +SeadPatrol.EngageAltType=EngageAltType +SeadPatrol.Patrol=true +self:SetSquadronPatrolInterval(SquadronName,self.DefenderDefault.PatrolLimit,self.DefenderDefault.PatrolMinSeconds,self.DefenderDefault.PatrolMaxSeconds,1,"SEAD") +self:I({SEAD={Zone:GetName(),PatrolMinSpeed,PatrolMaxSpeed,PatrolFloorAltitude,PatrolCeilingAltitude,PatrolAltType,EngageMinSpeed,EngageMaxSpeed,EngageFloorAltitude,EngageCeilingAltitude,EngageAltType}}) +end +function AI_A2G_DISPATCHER:SetSquadronSeadPatrol(SquadronName,Zone,FloorAltitude,CeilingAltitude,PatrolMinSpeed,PatrolMaxSpeed,EngageMinSpeed,EngageMaxSpeed,AltType) +self:SetSquadronSeadPatrol2(SquadronName,Zone,PatrolMinSpeed,PatrolMaxSpeed,FloorAltitude,CeilingAltitude,AltType,EngageMinSpeed,EngageMaxSpeed,FloorAltitude,CeilingAltitude,AltType) +end +function AI_A2G_DISPATCHER:SetSquadronCas2(SquadronName,EngageMinSpeed,EngageMaxSpeed,EngageFloorAltitude,EngageCeilingAltitude,EngageAltType) +local DefenderSquadron=self:GetSquadron(SquadronName) +DefenderSquadron.CAS=DefenderSquadron.CAS or{} +local Cas=DefenderSquadron.CAS +Cas.Name=SquadronName +Cas.EngageMinSpeed=EngageMinSpeed +Cas.EngageMaxSpeed=EngageMaxSpeed +Cas.EngageFloorAltitude=EngageFloorAltitude or 500 +Cas.EngageCeilingAltitude=EngageCeilingAltitude or 1000 +Cas.EngageAltType=EngageAltType +Cas.Defend=true +self:I({CAS={SquadronName,EngageMinSpeed,EngageMaxSpeed,EngageFloorAltitude,EngageCeilingAltitude,EngageAltType}}) +return self +end +function AI_A2G_DISPATCHER:SetSquadronCas(SquadronName,EngageMinSpeed,EngageMaxSpeed,EngageFloorAltitude,EngageCeilingAltitude) +return self:SetSquadronCas2(SquadronName,EngageMinSpeed,EngageMaxSpeed,EngageFloorAltitude,EngageCeilingAltitude,"RADIO") +end +function AI_A2G_DISPATCHER:SetSquadronCasEngageLimit(SquadronName,EngageLimit) +self:SetSquadronEngageLimit(SquadronName,EngageLimit,"CAS") +end +function AI_A2G_DISPATCHER:SetSquadronCasPatrol2(SquadronName,Zone,PatrolMinSpeed,PatrolMaxSpeed,PatrolFloorAltitude,PatrolCeilingAltitude,PatrolAltType,EngageMinSpeed,EngageMaxSpeed,EngageFloorAltitude,EngageCeilingAltitude,EngageAltType) +local DefenderSquadron=self:GetSquadron(SquadronName) +DefenderSquadron.CAS=DefenderSquadron.CAS or{} +local CasPatrol=DefenderSquadron.CAS +CasPatrol.Name=SquadronName +CasPatrol.Zone=Zone +CasPatrol.PatrolFloorAltitude=PatrolFloorAltitude +CasPatrol.PatrolCeilingAltitude=PatrolCeilingAltitude +CasPatrol.EngageFloorAltitude=EngageFloorAltitude +CasPatrol.EngageCeilingAltitude=EngageCeilingAltitude +CasPatrol.PatrolMinSpeed=PatrolMinSpeed +CasPatrol.PatrolMaxSpeed=PatrolMaxSpeed +CasPatrol.EngageMinSpeed=EngageMinSpeed +CasPatrol.EngageMaxSpeed=EngageMaxSpeed +CasPatrol.PatrolAltType=PatrolAltType +CasPatrol.EngageAltType=EngageAltType +CasPatrol.Patrol=true +self:SetSquadronPatrolInterval(SquadronName,self.DefenderDefault.PatrolLimit,self.DefenderDefault.PatrolMinSeconds,self.DefenderDefault.PatrolMaxSeconds,1,"CAS") +self:I({CAS={Zone:GetName(),PatrolMinSpeed,PatrolMaxSpeed,PatrolFloorAltitude,PatrolCeilingAltitude,PatrolAltType,EngageMinSpeed,EngageMaxSpeed,EngageFloorAltitude,EngageCeilingAltitude,EngageAltType}}) +end +function AI_A2G_DISPATCHER:SetSquadronCasPatrol(SquadronName,Zone,FloorAltitude,CeilingAltitude,PatrolMinSpeed,PatrolMaxSpeed,EngageMinSpeed,EngageMaxSpeed,AltType) +self:SetSquadronCasPatrol2(SquadronName,Zone,PatrolMinSpeed,PatrolMaxSpeed,FloorAltitude,CeilingAltitude,AltType,EngageMinSpeed,EngageMaxSpeed,FloorAltitude,CeilingAltitude,AltType) +end +function AI_A2G_DISPATCHER:SetSquadronBai2(SquadronName,EngageMinSpeed,EngageMaxSpeed,EngageFloorAltitude,EngageCeilingAltitude,EngageAltType) +local DefenderSquadron=self:GetSquadron(SquadronName) +DefenderSquadron.BAI=DefenderSquadron.BAI or{} +local Bai=DefenderSquadron.BAI +Bai.Name=SquadronName +Bai.EngageMinSpeed=EngageMinSpeed +Bai.EngageMaxSpeed=EngageMaxSpeed +Bai.EngageFloorAltitude=EngageFloorAltitude or 500 +Bai.EngageCeilingAltitude=EngageCeilingAltitude or 1000 +Bai.EngageAltType=EngageAltType +Bai.Defend=true +self:I({BAI={SquadronName,EngageMinSpeed,EngageMaxSpeed,EngageFloorAltitude,EngageCeilingAltitude,EngageAltType}}) +return self +end +function AI_A2G_DISPATCHER:SetSquadronBai(SquadronName,EngageMinSpeed,EngageMaxSpeed,EngageFloorAltitude,EngageCeilingAltitude) +return self:SetSquadronBai2(SquadronName,EngageMinSpeed,EngageMaxSpeed,EngageFloorAltitude,EngageCeilingAltitude,"RADIO") +end +function AI_A2G_DISPATCHER:SetSquadronBaiEngageLimit(SquadronName,EngageLimit) +self:SetSquadronEngageLimit(SquadronName,EngageLimit,"BAI") +end +function AI_A2G_DISPATCHER:SetSquadronBaiPatrol2(SquadronName,Zone,PatrolMinSpeed,PatrolMaxSpeed,PatrolFloorAltitude,PatrolCeilingAltitude,PatrolAltType,EngageMinSpeed,EngageMaxSpeed,EngageFloorAltitude,EngageCeilingAltitude,EngageAltType) +local DefenderSquadron=self:GetSquadron(SquadronName) +DefenderSquadron.BAI=DefenderSquadron.BAI or{} +local BaiPatrol=DefenderSquadron.BAI +BaiPatrol.Name=SquadronName +BaiPatrol.Zone=Zone +BaiPatrol.PatrolFloorAltitude=PatrolFloorAltitude +BaiPatrol.PatrolCeilingAltitude=PatrolCeilingAltitude +BaiPatrol.EngageFloorAltitude=EngageFloorAltitude +BaiPatrol.EngageCeilingAltitude=EngageCeilingAltitude +BaiPatrol.PatrolMinSpeed=PatrolMinSpeed +BaiPatrol.PatrolMaxSpeed=PatrolMaxSpeed +BaiPatrol.EngageMinSpeed=EngageMinSpeed +BaiPatrol.EngageMaxSpeed=EngageMaxSpeed +BaiPatrol.PatrolAltType=PatrolAltType +BaiPatrol.EngageAltType=EngageAltType +BaiPatrol.Patrol=true +self:SetSquadronPatrolInterval(SquadronName,self.DefenderDefault.PatrolLimit,self.DefenderDefault.PatrolMinSeconds,self.DefenderDefault.PatrolMaxSeconds,1,"BAI") +self:I({BAI={Zone:GetName(),PatrolMinSpeed,PatrolMaxSpeed,PatrolFloorAltitude,PatrolCeilingAltitude,PatrolAltType,EngageMinSpeed,EngageMaxSpeed,EngageFloorAltitude,EngageCeilingAltitude,EngageAltType}}) +end +function AI_A2G_DISPATCHER:SetSquadronBaiPatrol(SquadronName,Zone,FloorAltitude,CeilingAltitude,PatrolMinSpeed,PatrolMaxSpeed,EngageMinSpeed,EngageMaxSpeed,AltType) +self:SetSquadronBaiPatrol2(SquadronName,Zone,PatrolMinSpeed,PatrolMaxSpeed,FloorAltitude,CeilingAltitude,AltType,EngageMinSpeed,EngageMaxSpeed,FloorAltitude,CeilingAltitude,AltType) +end +function AI_A2G_DISPATCHER:SetDefaultOverhead(Overhead) +self.DefenderDefault.Overhead=Overhead +return self +end +function AI_A2G_DISPATCHER:SetSquadronOverhead(SquadronName,Overhead) +local DefenderSquadron=self:GetSquadron(SquadronName) +DefenderSquadron.Overhead=Overhead +return self +end +function AI_A2G_DISPATCHER:GetSquadronOverhead(SquadronName) +local DefenderSquadron=self:GetSquadron(SquadronName) +return DefenderSquadron.Overhead or self.DefenderDefault.Overhead +end +function AI_A2G_DISPATCHER:SetDefaultGrouping(Grouping) +self.DefenderDefault.Grouping=Grouping +return self +end +function AI_A2G_DISPATCHER:SetSquadronGrouping(SquadronName,Grouping) +local DefenderSquadron=self:GetSquadron(SquadronName) +DefenderSquadron.Grouping=Grouping +return self +end +function AI_A2G_DISPATCHER:SetSquadronEngageProbability(SquadronName,EngageProbability) +local DefenderSquadron=self:GetSquadron(SquadronName) +DefenderSquadron.EngageProbability=EngageProbability +return self +end +function AI_A2G_DISPATCHER:SetDefaultTakeoff(Takeoff) +self.DefenderDefault.Takeoff=Takeoff +return self +end +function AI_A2G_DISPATCHER:SetSquadronTakeoff(SquadronName,Takeoff) +local DefenderSquadron=self:GetSquadron(SquadronName) +DefenderSquadron.Takeoff=Takeoff +return self +end +function AI_A2G_DISPATCHER:GetDefaultTakeoff() +return self.DefenderDefault.Takeoff +end +function AI_A2G_DISPATCHER:GetSquadronTakeoff(SquadronName) +local DefenderSquadron=self:GetSquadron(SquadronName) +return DefenderSquadron.Takeoff or self.DefenderDefault.Takeoff +end +function AI_A2G_DISPATCHER:SetDefaultTakeoffInAir() +self:SetDefaultTakeoff(AI_A2G_DISPATCHER.Takeoff.Air) +return self +end +function AI_A2G_DISPATCHER:SetSquadronTakeoffInAir(SquadronName,TakeoffAltitude) +self:SetSquadronTakeoff(SquadronName,AI_A2G_DISPATCHER.Takeoff.Air) +if TakeoffAltitude then +self:SetSquadronTakeoffInAirAltitude(SquadronName,TakeoffAltitude) +end +return self +end +function AI_A2G_DISPATCHER:SetDefaultTakeoffFromRunway() +self:SetDefaultTakeoff(AI_A2G_DISPATCHER.Takeoff.Runway) +return self +end +function AI_A2G_DISPATCHER:SetSquadronTakeoffFromRunway(SquadronName) +self:SetSquadronTakeoff(SquadronName,AI_A2G_DISPATCHER.Takeoff.Runway) +return self +end +function AI_A2G_DISPATCHER:SetDefaultTakeoffFromParkingHot() +self:SetDefaultTakeoff(AI_A2G_DISPATCHER.Takeoff.Hot) +return self +end +function AI_A2G_DISPATCHER:SetSquadronTakeoffFromParkingHot(SquadronName) +self:SetSquadronTakeoff(SquadronName,AI_A2G_DISPATCHER.Takeoff.Hot) +return self +end +function AI_A2G_DISPATCHER:SetDefaultTakeoffFromParkingCold() +self:SetDefaultTakeoff(AI_A2G_DISPATCHER.Takeoff.Cold) +return self +end +function AI_A2G_DISPATCHER:SetSquadronTakeoffFromParkingCold(SquadronName) +self:SetSquadronTakeoff(SquadronName,AI_A2G_DISPATCHER.Takeoff.Cold) +return self +end +function AI_A2G_DISPATCHER:SetDefaultTakeoffInAirAltitude(TakeoffAltitude) +self.DefenderDefault.TakeoffAltitude=TakeoffAltitude +return self +end +function AI_A2G_DISPATCHER:SetSquadronTakeoffInAirAltitude(SquadronName,TakeoffAltitude) +local DefenderSquadron=self:GetSquadron(SquadronName) +DefenderSquadron.TakeoffAltitude=TakeoffAltitude +return self +end +function AI_A2G_DISPATCHER:SetDefaultLanding(Landing) +self.DefenderDefault.Landing=Landing +return self +end +function AI_A2G_DISPATCHER:SetSquadronLanding(SquadronName,Landing) +local DefenderSquadron=self:GetSquadron(SquadronName) +DefenderSquadron.Landing=Landing +return self +end +function AI_A2G_DISPATCHER:GetDefaultLanding() +return self.DefenderDefault.Landing +end +function AI_A2G_DISPATCHER:GetSquadronLanding(SquadronName) +local DefenderSquadron=self:GetSquadron(SquadronName) +return DefenderSquadron.Landing or self.DefenderDefault.Landing +end +function AI_A2G_DISPATCHER:SetDefaultLandingNearAirbase() +self:SetDefaultLanding(AI_A2G_DISPATCHER.Landing.NearAirbase) +return self +end +function AI_A2G_DISPATCHER:SetSquadronLandingNearAirbase(SquadronName) +self:SetSquadronLanding(SquadronName,AI_A2G_DISPATCHER.Landing.NearAirbase) +return self +end +function AI_A2G_DISPATCHER:SetDefaultLandingAtRunway() +self:SetDefaultLanding(AI_A2G_DISPATCHER.Landing.AtRunway) +return self +end +function AI_A2G_DISPATCHER:SetSquadronLandingAtRunway(SquadronName) +self:SetSquadronLanding(SquadronName,AI_A2G_DISPATCHER.Landing.AtRunway) +return self +end +function AI_A2G_DISPATCHER:SetDefaultLandingAtEngineShutdown() +self:SetDefaultLanding(AI_A2G_DISPATCHER.Landing.AtEngineShutdown) +return self +end +function AI_A2G_DISPATCHER:SetSquadronLandingAtEngineShutdown(SquadronName) +self:SetSquadronLanding(SquadronName,AI_A2G_DISPATCHER.Landing.AtEngineShutdown) +return self +end +function AI_A2G_DISPATCHER:SetDefaultFuelThreshold(FuelThreshold) +self.DefenderDefault.FuelThreshold=FuelThreshold +return self +end +function AI_A2G_DISPATCHER:SetSquadronFuelThreshold(SquadronName,FuelThreshold) +local DefenderSquadron=self:GetSquadron(SquadronName) +DefenderSquadron.FuelThreshold=FuelThreshold +return self +end +function AI_A2G_DISPATCHER:SetDefaultTanker(TankerName) +self.DefenderDefault.TankerName=TankerName +return self +end +function AI_A2G_DISPATCHER:SetSquadronTanker(SquadronName,TankerName) +local DefenderSquadron=self:GetSquadron(SquadronName) +DefenderSquadron.TankerName=TankerName +return self +end +function AI_A2G_DISPATCHER:SetSquadronRadioFrequency(SquadronName,RadioFrequency,RadioModulation,RadioPower) +local DefenderSquadron=self:GetSquadron(SquadronName) +DefenderSquadron.RadioFrequency=RadioFrequency +DefenderSquadron.RadioModulation=RadioModulation or radio.modulation.AM +DefenderSquadron.RadioPower=RadioPower or 100 +if DefenderSquadron.RadioQueue then +DefenderSquadron.RadioQueue:Stop() +end +DefenderSquadron.RadioQueue=nil +DefenderSquadron.RadioQueue=RADIOSPEECH:New(DefenderSquadron.RadioFrequency,DefenderSquadron.RadioModulation) +DefenderSquadron.RadioQueue.power=DefenderSquadron.RadioPower +DefenderSquadron.RadioQueue:Start(0.5) +DefenderSquadron.RadioQueue:SetLanguage(DefenderSquadron.Language) +end +function AI_A2G_DISPATCHER:AddDefenderToSquadron(Squadron,Defender,Size) +self.Defenders=self.Defenders or{} +local DefenderName=Defender:GetName() +self.Defenders[DefenderName]=Squadron +if Squadron.ResourceCount then +Squadron.ResourceCount=Squadron.ResourceCount-Size +end +self:F({DefenderName=DefenderName,SquadronResourceCount=Squadron.ResourceCount}) +end +function AI_A2G_DISPATCHER:RemoveDefenderFromSquadron(Squadron,Defender) +self.Defenders=self.Defenders or{} +local DefenderName=Defender:GetName() +if Squadron.ResourceCount then +Squadron.ResourceCount=Squadron.ResourceCount+Defender:GetSize() +end +self.Defenders[DefenderName]=nil +self:F({DefenderName=DefenderName,SquadronResourceCount=Squadron.ResourceCount}) +end +function AI_A2G_DISPATCHER:GetSquadronFromDefender(Defender) +self.Defenders=self.Defenders or{} +local DefenderName=Defender:GetName() +self:F({DefenderName=DefenderName}) +return self.Defenders[DefenderName] +end +function AI_A2G_DISPATCHER:CountPatrolAirborne(SquadronName,DefenseTaskType) +local PatrolCount=0 +local DefenderSquadron=self.DefenderSquadrons[SquadronName] +if DefenderSquadron then +for AIGroup,DefenderTask in pairs(self:GetDefenderTasks())do +if DefenderTask.SquadronName==SquadronName then +if DefenderTask.Type==DefenseTaskType then +if AIGroup:IsAlive()then +if DefenderTask.Fsm:Is("Patrolling")or DefenderTask.Fsm:Is("Engaging")or DefenderTask.Fsm:Is("Refuelling") +or DefenderTask.Fsm:Is("Started")then +PatrolCount=PatrolCount+1 +end +end +end +end +end +end +return PatrolCount +end +function AI_A2G_DISPATCHER:CountDefendersEngaged(AttackerDetection,AttackerCount) +local DefendersEngaged=0 +local DefendersTotal=0 +local AttackerSet=AttackerDetection.Set +local DefendersMissing=AttackerCount +local DefenderTasks=self:GetDefenderTasks() +for DefenderGroup,DefenderTask in pairs(DefenderTasks)do +local Defender=DefenderGroup +local DefenderTaskTarget=DefenderTask.Target +local DefenderSquadronName=DefenderTask.SquadronName +local DefenderSize=DefenderTask.Size +if DefenderTask.Target then +self:F("Defender Group Name: "..Defender:GetName()..", Size: "..DefenderSize) +DefendersTotal=DefendersTotal+DefenderSize +if DefenderTaskTarget and DefenderTaskTarget.Index==AttackerDetection.Index then +local SquadronOverhead=self:GetSquadronOverhead(DefenderSquadronName) +self:F({SquadronOverhead=SquadronOverhead}) +if DefenderSize then +DefendersEngaged=DefendersEngaged+DefenderSize +DefendersMissing=DefendersMissing-DefenderSize/SquadronOverhead +self:F("Defender Group Name: "..Defender:GetName()..", Size: "..DefenderSize) +else +DefendersEngaged=0 +end +end +end +end +for QueueID,QueueItem in pairs(self.DefenseQueue)do +local QueueItem=QueueItem +if QueueItem.AttackerDetection and QueueItem.AttackerDetection.ItemID==AttackerDetection.ItemID then +DefendersMissing=DefendersMissing-QueueItem.DefendersNeeded/QueueItem.DefenderSquadron.Overhead +self:F({QueueItemName=QueueItem.Defense,QueueItem_ItemID=QueueItem.AttackerDetection.ItemID,DetectedItem=AttackerDetection.ItemID,DefendersMissing=DefendersMissing}) +end +end +self:F({DefenderCount=DefendersEngaged}) +return DefendersTotal,DefendersEngaged,DefendersMissing +end +function AI_A2G_DISPATCHER:CountDefenders(AttackerDetection,DefenderCount,DefenderTaskType) +local Friendlies=nil +local AttackerSet=AttackerDetection.Set +local AttackerCount=AttackerSet:Count() +local DefenderFriendlies=self:GetDefenderFriendliesNearBy(AttackerDetection) +for FriendlyDistance,DefenderFriendlyUnit in UTILS.spairs(DefenderFriendlies or{})do +if AttackerCount>DefenderCount then +local FriendlyGroup=DefenderFriendlyUnit:GetGroup() +if FriendlyGroup and FriendlyGroup:IsAlive()then +local DefenderTask=self:GetDefenderTask(FriendlyGroup) +if DefenderTask then +if DefenderTaskType==DefenderTask.Type then +if DefenderTask.Target==nil then +if DefenderTask.Fsm:Is("Returning") +or DefenderTask.Fsm:Is("Patrolling")then +Friendlies=Friendlies or{} +Friendlies[FriendlyGroup]=FriendlyGroup +DefenderCount=DefenderCount+FriendlyGroup:GetSize() +self:F({Friendly=FriendlyGroup:GetName(),FriendlyDistance=FriendlyDistance}) +end +end +end +end +end +else +break +end +end +return Friendlies +end +function AI_A2G_DISPATCHER:ResourceActivate(DefenderSquadron,DefendersNeeded) +local SquadronName=DefenderSquadron.Name +DefendersNeeded=DefendersNeeded or 4 +local DefenderGrouping=DefenderSquadron.Grouping or self.DefenderDefault.Grouping +DefenderGrouping=(DefenderGroupingDefenderGrouping then +break +end +end +if DefenderPatrolTemplate then +local TakeoffMethod=self:GetSquadronTakeoff(SquadronName) +local SpawnGroup=GROUP:Register(DefenderName) +DefenderPatrolTemplate.lateActivation=nil +DefenderPatrolTemplate.uncontrolled=nil +local Takeoff=self:GetSquadronTakeoff(SquadronName) +DefenderPatrolTemplate.route.points[1].type=GROUPTEMPLATE.Takeoff[Takeoff][1] +DefenderPatrolTemplate.route.points[1].action=GROUPTEMPLATE.Takeoff[Takeoff][2] +local Defender=_DATABASE:Spawn(DefenderPatrolTemplate) +self:AddDefenderToSquadron(DefenderSquadron,Defender,DefenderGrouping) +Defender:Activate() +return Defender,DefenderGrouping +end +else +local Spawn=DefenderSquadron.Spawn[math.random(1,#DefenderSquadron.Spawn)] +if DefenderGrouping then +Spawn:InitGrouping(DefenderGrouping) +else +Spawn:InitGrouping() +end +local TakeoffMethod=self:GetSquadronTakeoff(SquadronName) +local Defender=Spawn:SpawnAtAirbase(DefenderSquadron.Airbase,TakeoffMethod,DefenderSquadron.TakeoffAltitude or self.DefenderDefault.TakeoffAltitude) +self:AddDefenderToSquadron(DefenderSquadron,Defender,DefenderGrouping) +return Defender,DefenderGrouping +end +return nil,nil +end +function AI_A2G_DISPATCHER:onafterPatrol(From,Event,To,SquadronName,DefenseTaskType) +local DefenderSquadron,Patrol=self:CanPatrol(SquadronName,DefenseTaskType) +if DefenderSquadron then +local DefendersNeeded +local DefendersGrouping=(DefenderSquadron.Grouping or self.DefenderDefault.Grouping) +if DefenderSquadron.ResourceCount==nil then +DefendersNeeded=DefendersGrouping +else +if DefenderSquadron.ResourceCount>=DefendersGrouping then +DefendersNeeded=DefendersGrouping +else +DefendersNeeded=DefenderSquadron.ResourceCount +end +end +if Patrol then +self:ResourceQueue(true,DefenderSquadron,DefendersNeeded,Patrol,DefenseTaskType,nil,SquadronName) +end +end +end +function AI_A2G_DISPATCHER:ResourceQueue(Patrol,DefenderSquadron,DefendersNeeded,Defense,DefenseTaskType,AttackerDetection,SquadronName) +self:F({DefenderSquadron,DefendersNeeded,Defense,DefenseTaskType,AttackerDetection,SquadronName}) +local DefenseQueueItem={} +DefenseQueueItem.Patrol=Patrol +DefenseQueueItem.DefenderSquadron=DefenderSquadron +DefenseQueueItem.DefendersNeeded=DefendersNeeded +DefenseQueueItem.Defense=Defense +DefenseQueueItem.DefenseTaskType=DefenseTaskType +DefenseQueueItem.AttackerDetection=AttackerDetection +DefenseQueueItem.SquadronName=SquadronName +table.insert(self.DefenseQueue,DefenseQueueItem) +self:F({QueueItems=#self.DefenseQueue}) +end +function AI_A2G_DISPATCHER:ResourceTakeoff() +for DefenseQueueID,DefenseQueueItem in pairs(self.DefenseQueue)do +self:F({DefenseQueueID}) +end +for SquadronName,Squadron in pairs(self.DefenderSquadrons)do +if#self.DefenseQueue>0 then +self:F({SquadronName,Squadron.Name,Squadron.TakeoffTime,Squadron.TakeoffInterval,timer.getTime()}) +local DefenseQueueItem=self.DefenseQueue[1] +self:F({DefenderSquadron=DefenseQueueItem.DefenderSquadron}) +if DefenseQueueItem.SquadronName==SquadronName then +if Squadron.TakeoffTime+Squadron.TakeoffInterval0 then +local FirstUnit=AttackSetUnit:GetFirst() +local Coordinate=FirstUnit:GetCoordinate() +if self.SetSendPlayerMessages then +Dispatcher:MessageToPlayers(Squadron,DefenderName..", moving on to ground target at "..Coordinate:ToStringA2G(DefenderGroup),DefenderGroup) +end +end +end +function AI_A2G_Fsm:OnAfterEngage(DefenderGroup,From,Event,To,AttackSetUnit) +self:F({"Engage Route",DefenderGroup:GetName()}) +local DefenderName=DefenderGroup:GetCallsign() +local Dispatcher=AI_A2G_Fsm:GetDispatcher() +local Squadron=Dispatcher:GetSquadronFromDefender(DefenderGroup) +local FirstUnit=AttackSetUnit:GetFirst() +if FirstUnit then +local Coordinate=FirstUnit:GetCoordinate() +if self.SetSendPlayerMessages then +Dispatcher:MessageToPlayers(Squadron,DefenderName..", engaging ground target at "..Coordinate:ToStringA2G(DefenderGroup),DefenderGroup) +end +end +end +function AI_A2G_Fsm:onafterRTB(DefenderGroup,From,Event,To) +self:F({"RTB",DefenderGroup:GetName()}) +self:GetParent(self).onafterRTB(self,DefenderGroup,From,Event,To) +local DefenderName=DefenderGroup:GetCallsign() +local Dispatcher=self:GetDispatcher() +local Squadron=Dispatcher:GetSquadronFromDefender(DefenderGroup) +if self.SetSendPlayerMessages then +Dispatcher:MessageToPlayers(Squadron,DefenderName..", returning to base.",DefenderGroup) +end +Dispatcher:ClearDefenderTaskTarget(DefenderGroup) +end +function AI_A2G_Fsm:onafterLostControl(DefenderGroup,From,Event,To) +self:F({"LostControl",DefenderGroup:GetName()}) +self:GetParent(self).onafterHome(self,DefenderGroup,From,Event,To) +local DefenderName=DefenderGroup:GetCallsign() +local Dispatcher=AI_A2G_Fsm:GetDispatcher() +local Squadron=Dispatcher:GetSquadronFromDefender(DefenderGroup) +if self.SetSendPlayerMessages then +Dispatcher:MessageToPlayers(Squadron,DefenderName..", lost control.") +end +if DefenderGroup:IsAboveRunway()then +Dispatcher:RemoveDefenderFromSquadron(Squadron,DefenderGroup) +DefenderGroup:Destroy() +end +end +function AI_A2G_Fsm:onafterHome(DefenderGroup,From,Event,To,Action) +self:F({"Home",DefenderGroup:GetName()}) +self:GetParent(self).onafterHome(self,DefenderGroup,From,Event,To) +local DefenderName=DefenderGroup:GetCallsign() +local Dispatcher=self:GetDispatcher() +local Squadron=Dispatcher:GetSquadronFromDefender(DefenderGroup) +if self.SetSendPlayerMessages then +Dispatcher:MessageToPlayers(Squadron,DefenderName..", landing at base.",DefenderGroup) +end +if Action and Action=="Destroy"then +Dispatcher:RemoveDefenderFromSquadron(Squadron,DefenderGroup) +DefenderGroup:Destroy() +end +if Dispatcher:GetSquadronLanding(Squadron.Name)==AI_A2G_DISPATCHER.Landing.NearAirbase then +Dispatcher:RemoveDefenderFromSquadron(Squadron,DefenderGroup) +DefenderGroup:Destroy() +Dispatcher:ResourcePark(Squadron,DefenderGroup) +end +end +end +end +function AI_A2G_DISPATCHER:ResourceEngage(DefenderSquadron,DefendersNeeded,Defense,DefenseTaskType,AttackerDetection,SquadronName) +self:F({DefenderSquadron=DefenderSquadron}) +self:F({DefendersNeeded=DefendersNeeded}) +self:F({Defense=Defense}) +self:F({DefenseTaskType=DefenseTaskType}) +self:F({AttackerDetection=AttackerDetection}) +self:F({SquadronName=SquadronName}) +local DefenderGroup,DefenderGrouping=self:ResourceActivate(DefenderSquadron,DefendersNeeded) +if DefenderGroup then +local AI_A2G_ENGAGE={SEAD=AI_A2G_SEAD,BAI=AI_A2G_BAI,CAS=AI_A2G_CAS} +local AI_A2G_Fsm=AI_A2G_ENGAGE[DefenseTaskType]:New(DefenderGroup,Defense.EngageMinSpeed,Defense.EngageMaxSpeed,Defense.EngageFloorAltitude,Defense.EngageCeilingAltitude,Defense.EngageAltType) +AI_A2G_Fsm:SetDispatcher(self) +AI_A2G_Fsm:SetHomeAirbase(DefenderSquadron.Airbase) +AI_A2G_Fsm:SetFuelThreshold(DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold,60) +AI_A2G_Fsm:SetDamageThreshold(self.DefenderDefault.DamageThreshold) +AI_A2G_Fsm:SetDisengageRadius(self.DisengageRadius) +AI_A2G_Fsm:Start() +self:SetDefenderTask(SquadronName,DefenderGroup,DefenseTaskType,AI_A2G_Fsm,AttackerDetection,DefenderGrouping) +function AI_A2G_Fsm:onafterTakeoff(DefenderGroup,From,Event,To) +self:F({"Defender Birth",DefenderGroup:GetName()}) +local DefenderName=DefenderGroup:GetCallsign() +local Dispatcher=AI_A2G_Fsm:GetDispatcher() +local Squadron=Dispatcher:GetSquadronFromDefender(DefenderGroup) +local DefenderTarget=Dispatcher:GetDefenderTaskTarget(DefenderGroup) +self:F({DefenderTarget=DefenderTarget}) +if DefenderTarget then +if self.SetSendPlayerMessages then +Dispatcher:MessageToPlayers(Squadron,DefenderName..", wheels up.",DefenderGroup) +end +AI_A2G_Fsm:EngageRoute(DefenderTarget.Set) +end +end +function AI_A2G_Fsm:onafterEngageRoute(DefenderGroup,From,Event,To,AttackSetUnit) +self:F({"Engage Route",DefenderGroup:GetName()}) +local DefenderName=DefenderGroup:GetCallsign() +local Dispatcher=AI_A2G_Fsm:GetDispatcher() +local Squadron=Dispatcher:GetSquadronFromDefender(DefenderGroup) +if Squadron then +local FirstUnit=AttackSetUnit:GetFirst() +local Coordinate=FirstUnit:GetCoordinate() +if self.SetSendPlayerMessages then +Dispatcher:MessageToPlayers(Squadron,DefenderName..", on route to ground target at "..Coordinate:ToStringA2G(DefenderGroup),DefenderGroup) +end +end +self:GetParent(self).onafterEngageRoute(self,DefenderGroup,From,Event,To,AttackSetUnit) +end +function AI_A2G_Fsm:OnAfterEngage(DefenderGroup,From,Event,To,AttackSetUnit) +self:F({"Engage Route",DefenderGroup:GetName()}) +local DefenderName=DefenderGroup:GetCallsign() +local Dispatcher=AI_A2G_Fsm:GetDispatcher() +local Squadron=Dispatcher:GetSquadronFromDefender(DefenderGroup) +local FirstUnit=AttackSetUnit:GetFirst() +if FirstUnit then +local Coordinate=FirstUnit:GetCoordinate() +if self.SetSendPlayerMessages then +Dispatcher:MessageToPlayers(Squadron,DefenderName..", engaging ground target at "..Coordinate:ToStringA2G(DefenderGroup),DefenderGroup) +end +end +end +function AI_A2G_Fsm:onafterRTB(DefenderGroup,From,Event,To) +self:F({"Defender RTB",DefenderGroup:GetName()}) +local DefenderName=DefenderGroup:GetCallsign() +local Dispatcher=self:GetDispatcher() +local Squadron=Dispatcher:GetSquadronFromDefender(DefenderGroup) +if self.SetSendPlayerMessages then +Dispatcher:MessageToPlayers(Squadron,DefenderName..", returning to base.",DefenderGroup) +end +self:GetParent(self).onafterRTB(self,DefenderGroup,From,Event,To) +Dispatcher:ClearDefenderTaskTarget(DefenderGroup) +end +function AI_A2G_Fsm:onafterLostControl(DefenderGroup,From,Event,To) +self:F({"Defender LostControl",DefenderGroup:GetName()}) +self:GetParent(self).onafterHome(self,DefenderGroup,From,Event,To) +local DefenderName=DefenderGroup:GetCallsign() +local Dispatcher=AI_A2G_Fsm:GetDispatcher() +local Squadron=Dispatcher:GetSquadronFromDefender(DefenderGroup) +if self.SetSendPlayerMessages then +Dispatcher:MessageToPlayers(Squadron,"Squadron "..Squadron.Name..", "..DefenderName.." lost control.") +end +if DefenderGroup:IsAboveRunway()then +Dispatcher:RemoveDefenderFromSquadron(Squadron,DefenderGroup) +DefenderGroup:Destroy() +end +end +function AI_A2G_Fsm:onafterHome(DefenderGroup,From,Event,To,Action) +self:F({"Defender Home",DefenderGroup:GetName()}) +self:GetParent(self).onafterHome(self,DefenderGroup,From,Event,To) +local DefenderName=DefenderGroup:GetCallsign() +local Dispatcher=self:GetDispatcher() +local Squadron=Dispatcher:GetSquadronFromDefender(DefenderGroup) +if self.SetSendPlayerMessages then +Dispatcher:MessageToPlayers(Squadron,DefenderName..", landing at base.",DefenderGroup) +end +if Action and Action=="Destroy"then +Dispatcher:RemoveDefenderFromSquadron(Squadron,DefenderGroup) +DefenderGroup:Destroy() +end +if Dispatcher:GetSquadronLanding(Squadron.Name)==AI_A2G_DISPATCHER.Landing.NearAirbase then +Dispatcher:RemoveDefenderFromSquadron(Squadron,DefenderGroup) +DefenderGroup:Destroy() +Dispatcher:ResourcePark(Squadron,DefenderGroup) +end +end +end +end +function AI_A2G_DISPATCHER:onafterEngage(From,Event,To,AttackerDetection,Defenders) +if Defenders then +for DefenderID,Defender in pairs(Defenders or{})do +local Fsm=self:GetDefenderTaskFsm(Defender) +Fsm:Engage(AttackerDetection.Set) +self:SetDefenderTaskTarget(Defender,AttackerDetection) +end +end +end +function AI_A2G_DISPATCHER:HasDefenseLine(DefenseCoordinate,DetectedItem) +local AttackCoordinate=self.Detection:GetDetectedItemCoordinate(DetectedItem) +local EvaluateDistance=AttackCoordinate:Get2DDistance(DefenseCoordinate) +local c1=DefenseCoordinate +local c2=AttackCoordinate +local a=c1.z-c2.z +local b=c2.x-c1.x +local c=c1.x*c2.z-c2.x*c1.z +local ok=true +for AttackItemID,CheckAttackItem in pairs(self.Detection:GetDetectedItems())do +if AttackItemID~=DetectedItem.ID then +local CheckAttackCoordinate=self.Detection:GetDetectedItemCoordinate(CheckAttackItem) +local x=CheckAttackCoordinate.x +local y=CheckAttackCoordinate.z +local r=5000 +local IntersectDistance=(math.abs(a*x+b*y+c))/math.sqrt(a*a+b*b) +self:F({IntersectDistance=IntersectDistance,x=x,y=y}) +local IntersectAttackDistance=CheckAttackCoordinate:Get2DDistance(DefenseCoordinate) +self:F({IntersectAttackDistance=IntersectAttackDistance,EvaluateDistance=EvaluateDistance}) +if IntersectDistance0 and not BreakLoop)do +self:F({DefenderSquadrons=self.DefenderSquadrons}) +for SquadronName,DefenderSquadron in UTILS.rpairs(self.DefenderSquadrons or{})do +if DefenderSquadron[DefenseTaskType]then +local AirbaseCoordinate=DefenderSquadron.Airbase:GetCoordinate() +local AttackerCoord=AttackerUnit:GetCoordinate() +local InterceptCoord=DetectedItem.InterceptCoord +self:F({InterceptCoord=InterceptCoord}) +if InterceptCoord then +local InterceptDistance=AirbaseCoordinate:Get2DDistance(InterceptCoord) +local AirbaseDistance=AirbaseCoordinate:Get2DDistance(AttackerCoord) +self:F({InterceptDistance=InterceptDistance,AirbaseDistance=AirbaseDistance,InterceptCoord=InterceptCoord}) +if AirbaseDistance<=self.DefenseRadius then +local HasDefenseLine=self:HasDefenseLine(AirbaseCoordinate,DetectedItem) +if HasDefenseLine==true then +local EngageProbability=(DefenderSquadron.EngageProbability or 1) +local Probability=math.random() +if Probability=DefendersLimit then +DefendersNeeded=0 +BreakLoop=true +else +if DefendersTotal+DefendersNeeded>DefendersLimit then +DefendersNeeded=DefendersLimit-DefendersTotal +end +end +end +if DefenderSquadron.ResourceCount and DefendersNeeded>DefenderSquadron.ResourceCount then +DefendersNeeded=DefenderSquadron.ResourceCount +BreakLoop=true +end +while(DefendersNeeded>0)do +self:ResourceQueue(false,DefenderSquadron,DefendersNeeded,Defense,DefenseTaskType,DetectedItem,EngageSquadronName) +DefendersNeeded=DefendersNeeded-DefenderGrouping +DefenderCount=DefenderCount-DefenderGrouping/DefenderOverhead +end +else +BreakLoop=true +break +end +else +break +end +end +end +end +function AI_A2G_DISPATCHER:Evaluate_SEAD(DetectedItem) +self:F({DetectedItem.ItemID}) +local AttackerSet=DetectedItem.Set +local AttackerCount=AttackerSet:HasSEAD() +if(AttackerCount>0)then +local DefendersTotal,DefendersEngaged,DefendersMissing=self:CountDefendersEngaged(DetectedItem,AttackerCount) +self:F({AttackerCount=AttackerCount,DefendersTotal=DefendersTotal,DefendersEngaged=DefendersEngaged,DefendersMissing=DefendersMissing}) +local DefenderGroups=self:CountDefenders(DetectedItem,DefendersEngaged,"SEAD") +if DetectedItem.IsDetected==true then +return DefendersTotal,DefendersEngaged,DefendersMissing,DefenderGroups +end +end +return 0,0,0 +end +function AI_A2G_DISPATCHER:Evaluate_CAS(DetectedItem) +self:F({DetectedItem.ItemID}) +local AttackerSet=DetectedItem.Set +local AttackerCount=AttackerSet:Count() +local AttackerRadarCount=AttackerSet:HasSEAD() +local IsFriendliesNearBy=self.Detection:IsFriendliesNearBy(DetectedItem,Unit.Category.GROUND_UNIT) +local IsCas=(AttackerRadarCount==0)and(IsFriendliesNearBy==true) +if IsCas==true then +local DefendersTotal,DefendersEngaged,DefendersMissing=self:CountDefendersEngaged(DetectedItem,AttackerCount) +self:F({AttackerCount=AttackerCount,DefendersTotal=DefendersTotal,DefendersEngaged=DefendersEngaged,DefendersMissing=DefendersMissing}) +local DefenderGroups=self:CountDefenders(DetectedItem,DefendersEngaged,"CAS") +if DetectedItem.IsDetected==true then +return DefendersTotal,DefendersEngaged,DefendersMissing,DefenderGroups +end +end +return 0,0,0 +end +function AI_A2G_DISPATCHER:Evaluate_BAI(DetectedItem) +self:F({DetectedItem.ItemID}) +local AttackerSet=DetectedItem.Set +local AttackerCount=AttackerSet:Count() +local AttackerRadarCount=AttackerSet:HasSEAD() +local IsFriendliesNearBy=self.Detection:IsFriendliesNearBy(DetectedItem,Unit.Category.GROUND_UNIT) +local IsBai=(AttackerRadarCount==0)and(IsFriendliesNearBy==false) +if IsBai==true then +local DefendersTotal,DefendersEngaged,DefendersMissing=self:CountDefendersEngaged(DetectedItem,AttackerCount) +self:F({AttackerCount=AttackerCount,DefendersTotal=DefendersTotal,DefendersEngaged=DefendersEngaged,DefendersMissing=DefendersMissing}) +local DefenderGroups=self:CountDefenders(DetectedItem,DefendersEngaged,"BAI") +if DetectedItem.IsDetected==true then +return DefendersTotal,DefendersEngaged,DefendersMissing,DefenderGroups +end +end +return 0,0,0 +end +function AI_A2G_DISPATCHER:Keys(DetectedItem) +self:F({DetectedItem=DetectedItem}) +local AttackCoordinate=self.Detection:GetDetectedItemCoordinate(DetectedItem) +local ShortestDistance=999999999 +for DefenseCoordinateName,DefenseCoordinate in pairs(self.DefenseCoordinates)do +local DefenseCoordinate=DefenseCoordinate +local EvaluateDistance=AttackCoordinate:Get2DDistance(DefenseCoordinate) +if EvaluateDistance<=ShortestDistance then +ShortestDistance=EvaluateDistance +end +end +return ShortestDistance +end +function AI_A2G_DISPATCHER:Order(DetectedItem) +local AttackCoordinate=self.Detection:GetDetectedItemCoordinate(DetectedItem) +local ShortestDistance=999999999 +for DefenseCoordinateName,DefenseCoordinate in pairs(self.DefenseCoordinates)do +local DefenseCoordinate=DefenseCoordinate +local EvaluateDistance=AttackCoordinate:Get2DDistance(DefenseCoordinate) +if EvaluateDistance<=ShortestDistance then +ShortestDistance=EvaluateDistance +end +end +return ShortestDistance +end +function AI_A2G_DISPATCHER:ShowTacticalDisplay(Detection) +local AreaMsg={} +local TaskMsg={} +local ChangeMsg={} +local TaskReport=REPORT:New() +local DefenseTotal=0 +local Report=REPORT:New("\nTactical Overview") +local DefenderGroupCount=0 +local DefendersTotal=0 +for DetectedItemID,DetectedItem in UTILS.spairs(Detection:GetDetectedItems(),function(t,a,b)return self:Order(t[a])0 then +self:F({DefendersTotal=DefendersTotal,DefendersEngaged=DefendersEngaged,DefendersMissing=DefendersMissing}) +self:Defend(DetectedItem,DefendersTotal,DefendersEngaged,DefendersMissing,Friendlies,"SEAD") +end +end +do +local DefendersTotal,DefendersEngaged,DefendersMissing,Friendlies=self:Evaluate_CAS(DetectedItem) +if DefendersMissing>0 then +self:F({DefendersTotal=DefendersTotal,DefendersEngaged=DefendersEngaged,DefendersMissing=DefendersMissing}) +self:Defend(DetectedItem,DefendersTotal,DefendersEngaged,DefendersMissing,Friendlies,"CAS") +end +end +do +local DefendersTotal,DefendersEngaged,DefendersMissing,Friendlies=self:Evaluate_BAI(DetectedItem) +if DefendersMissing>0 then +self:F({DefendersTotal=DefendersTotal,DefendersEngaged=DefendersEngaged,DefendersMissing=DefendersMissing}) +self:Defend(DetectedItem,DefendersTotal,DefendersEngaged,DefendersMissing,Friendlies,"BAI") +end +end +end +for Defender,DefenderTask in pairs(self:GetDefenderTasks())do +local Defender=Defender +if DefenderTask.Target and DefenderTask.Target.Index==DetectedItem.Index then +DefenseTotal=DefenseTotal+1 +end +end +for DefenseQueueID,DefenseQueueItem in pairs(self.DefenseQueue)do +local DefenseQueueItem=DefenseQueueItem +if DefenseQueueItem.AttackerDetection and DefenseQueueItem.AttackerDetection.Index and DefenseQueueItem.AttackerDetection.Index==DetectedItem.Index then +DefenseTotal=DefenseTotal+1 +end +end +if self.TacticalDisplay then +local ThreatLevel=DetectedItem.Set:CalculateThreatLevelA2G() +Report:Add(string.format(" - %1s%s ( %4s ): ( #%d - %4s ) %s",(DetectedItem.IsDetected==true)and"!"or" ",DetectedItem.ItemID,DetectedItem.Index,DetectedItem.Set:Count(),DetectedItem.Type or" --- ",string.rep("■",ThreatLevel))) +for Defender,DefenderTask in pairs(self:GetDefenderTasks())do +local Defender=Defender +if DefenderTask.Target and DefenderTask.Target.Index==DetectedItem.Index then +if Defender:IsAlive()then +DefenderGroupCount=DefenderGroupCount+1 +local Fuel=Defender:GetFuelMin()*100 +local Damage=Defender:GetLife()/Defender:GetLife0()*100 +Report:Add(string.format(" - %s ( %s - %s ): ( #%d ) F: %3d, D:%3d - %s", +Defender:GetName(), +DefenderTask.Type, +DefenderTask.Fsm:GetState(), +Defender:GetSize(), +Fuel, +Damage, +Defender:HasTask()==true and"Executing"or"Idle")) +end +end +end +end +end +end +if self.TacticalDisplay then +Report:Add("\n - No Targets:") +local TaskCount=0 +for Defender,DefenderTask in pairs(self:GetDefenderTasks())do +TaskCount=TaskCount+1 +local Defender=Defender +if not DefenderTask.Target then +if Defender:IsAlive()then +local DefenderHasTask=Defender:HasTask() +local Fuel=Defender:GetFuelMin()*100 +local Damage=Defender:GetLife()/Defender:GetLife0()*100 +DefenderGroupCount=DefenderGroupCount+1 +Report:Add(string.format(" - %s ( %s - %s ): ( #%d ) F: %3d, D:%3d - %s", +Defender:GetName(), +DefenderTask.Type, +DefenderTask.Fsm:GetState(), +Defender:GetSize(), +Fuel, +Damage, +Defender:HasTask()==true and"Executing"or"Idle")) +end +end +end +Report:Add(string.format("\n - %d Tasks - %d Defender Groups",TaskCount,DefenderGroupCount)) +Report:Add(string.format("\n - %d Queued Aircraft Launches",#self.DefenseQueue)) +for DefenseQueueID,DefenseQueueItem in pairs(self.DefenseQueue)do +local DefenseQueueItem=DefenseQueueItem +Report:Add(string.format(" - %s - %s",DefenseQueueItem.SquadronName,DefenseQueueItem.DefenderSquadron.TakeoffTime,DefenseQueueItem.DefenderSquadron.TakeoffInterval)) +end +Report:Add(string.format("\n - Squadron Resources: ",#self.DefenseQueue)) +for DefenderSquadronName,DefenderSquadron in pairs(self.DefenderSquadrons)do +Report:Add(string.format(" - %s - %s",DefenderSquadronName,DefenderSquadron.ResourceCount and tostring(DefenderSquadron.ResourceCount)or"n/a")) +end +self:F(Report:Text("\n")) +trigger.action.outText(Report:Text("\n"),25) +end +return true +end +end +do +function AI_A2G_DISPATCHER:GetPlayerFriendliesNearBy(DetectedItem) +local DetectedSet=DetectedItem.Set +local PlayersNearBy=self.Detection:GetPlayersNearBy(DetectedItem) +local PlayerTypes={} +local PlayersCount=0 +if PlayersNearBy then +local DetectedTreatLevel=DetectedSet:CalculateThreatLevelA2G() +for PlayerUnitName,PlayerUnitData in pairs(PlayersNearBy)do +local PlayerUnit=PlayerUnitData +local PlayerName=PlayerUnit:GetPlayerName() +if PlayerUnit:IsAirPlane()and PlayerName~=nil then +local FriendlyUnitThreatLevel=PlayerUnit:GetThreatLevel() +PlayersCount=PlayersCount+1 +local PlayerType=PlayerUnit:GetTypeName() +PlayerTypes[PlayerName]=PlayerType +if DetectedTreatLevel0 then +for PlayerName,PlayerType in pairs(PlayerTypes)do +PlayerTypesReport:Add(string.format('"%s" in %s',PlayerName,PlayerType)) +end +else +PlayerTypesReport:Add("-") +end +return PlayersCount,PlayerTypesReport +end +function AI_A2G_DISPATCHER:GetFriendliesNearBy(DetectedItem) +local DetectedSet=DetectedItem.Set +local FriendlyUnitsNearBy=self.Detection:GetFriendliesNearBy(DetectedItem) +local FriendlyTypes={} +local FriendliesCount=0 +if FriendlyUnitsNearBy then +local DetectedTreatLevel=DetectedSet:CalculateThreatLevelA2G() +for FriendlyUnitName,FriendlyUnitData in pairs(FriendlyUnitsNearBy)do +local FriendlyUnit=FriendlyUnitData +if FriendlyUnit:IsAirPlane()then +local FriendlyUnitThreatLevel=FriendlyUnit:GetThreatLevel() +FriendliesCount=FriendliesCount+1 +local FriendlyType=FriendlyUnit:GetTypeName() +FriendlyTypes[FriendlyType]=FriendlyTypes[FriendlyType]and(FriendlyTypes[FriendlyType]+1)or 1 +if DetectedTreatLevel0 then +for FriendlyType,FriendlyTypeCount in pairs(FriendlyTypes)do +FriendlyTypesReport:Add(string.format("%d of %s",FriendlyTypeCount,FriendlyType)) +end +else +FriendlyTypesReport:Add("-") +end +return FriendliesCount,FriendlyTypesReport +end +function AI_A2G_DISPATCHER:SchedulerPatrol(SquadronName) +local PatrolTaskTypes={"SEAD","CAS","BAI"} +local PatrolTaskType=PatrolTaskTypes[math.random(1,3)] +self:Patrol(SquadronName,PatrolTaskType) +end +function AI_A2G_DISPATCHER:SetSendMessages(onoff) +self.SetSendPlayerMessages=onoff +end +end +AI_PATROL_ZONE={ +ClassName="AI_PATROL_ZONE", +} +function AI_PATROL_ZONE:New(PatrolZone,PatrolFloorAltitude,PatrolCeilingAltitude,PatrolMinSpeed,PatrolMaxSpeed,PatrolAltType) +local self=BASE:Inherit(self,FSM_CONTROLLABLE:New()) +self.PatrolZone=PatrolZone +self.PatrolFloorAltitude=PatrolFloorAltitude +self.PatrolCeilingAltitude=PatrolCeilingAltitude +self.PatrolMinSpeed=PatrolMinSpeed +self.PatrolMaxSpeed=PatrolMaxSpeed +self.PatrolAltType=PatrolAltType or"BARO" +self:SetRefreshTimeInterval(30) +self.CheckStatus=true +self:ManageFuel(.2,60) +self:ManageDamage(1) +self.DetectedUnits={} +self:SetStartState("None") +self:AddTransition("*","Stop","Stopped") +self:AddTransition("None","Start","Patrolling") +self:AddTransition("Patrolling","Route","Patrolling") +self:AddTransition("*","Status","*") +self:AddTransition("*","Detect","*") +self:AddTransition("*","Detected","*") +self:AddTransition("*","RTB","Returning") +self:AddTransition("*","Reset","Patrolling") +self:AddTransition("*","Eject","*") +self:AddTransition("*","Crash","Crashed") +self:AddTransition("*","PilotDead","*") +return self +end +function AI_PATROL_ZONE:SetSpeed(PatrolMinSpeed,PatrolMaxSpeed) +self:F2({PatrolMinSpeed,PatrolMaxSpeed}) +self.PatrolMinSpeed=PatrolMinSpeed +self.PatrolMaxSpeed=PatrolMaxSpeed +end +function AI_PATROL_ZONE:SetAltitude(PatrolFloorAltitude,PatrolCeilingAltitude) +self:F2({PatrolFloorAltitude,PatrolCeilingAltitude}) +self.PatrolFloorAltitude=PatrolFloorAltitude +self.PatrolCeilingAltitude=PatrolCeilingAltitude +end +function AI_PATROL_ZONE:SetDetectionOn() +self:F2() +self.DetectOn=true +end +function AI_PATROL_ZONE:SetDetectionOff() +self:F2() +self.DetectOn=false +end +function AI_PATROL_ZONE:SetStatusOff() +self:F2() +self.CheckStatus=false +end +function AI_PATROL_ZONE:SetDetectionActivated() +self:F2() +self:ClearDetectedUnits() +self.DetectActivated=true +self:__Detect(-self.DetectInterval) +end +function AI_PATROL_ZONE:SetDetectionDeactivated() +self:F2() +self:ClearDetectedUnits() +self.DetectActivated=false +end +function AI_PATROL_ZONE:SetRefreshTimeInterval(Seconds) +self:F2() +if Seconds then +self.DetectInterval=Seconds +else +self.DetectInterval=30 +end +end +function AI_PATROL_ZONE:SetDetectionZone(DetectionZone) +self:F2() +if DetectionZone then +self.DetectZone=DetectionZone +else +self.DetectZone=nil +end +end +function AI_PATROL_ZONE:GetDetectedUnits() +self:F2() +return self.DetectedUnits +end +function AI_PATROL_ZONE:ClearDetectedUnits() +self:F2() +self.DetectedUnits={} +end +function AI_PATROL_ZONE:ManageFuel(PatrolFuelThresholdPercentage,PatrolOutOfFuelOrbitTime) +self.PatrolFuelThresholdPercentage=PatrolFuelThresholdPercentage +self.PatrolOutOfFuelOrbitTime=PatrolOutOfFuelOrbitTime +return self +end +function AI_PATROL_ZONE:ManageDamage(PatrolDamageThreshold) +self.PatrolManageDamage=true +self.PatrolDamageThreshold=PatrolDamageThreshold +return self +end +function AI_PATROL_ZONE:onafterStart(Controllable,From,Event,To) +self:F2() +self:__Route(1) +self:__Status(60) +self:SetDetectionActivated() +self:HandleEvent(EVENTS.PilotDead,self.OnPilotDead) +self:HandleEvent(EVENTS.Crash,self.OnCrash) +self:HandleEvent(EVENTS.Ejection,self.OnEjection) +Controllable:OptionROEHoldFire() +Controllable:OptionROTVertical() +self.Controllable:OnReSpawn( +function(PatrolGroup) +self:T("ReSpawn") +self:__Reset(1) +self:__Route(5) +end +) +self:SetDetectionOn() +end +function AI_PATROL_ZONE:onbeforeDetect(Controllable,From,Event,To) +return self.DetectOn and self.DetectActivated +end +function AI_PATROL_ZONE:onafterDetect(Controllable,From,Event,To) +local Detected=false +local DetectedTargets=Controllable:GetDetectedTargets() +for TargetID,Target in pairs(DetectedTargets or{})do +local TargetObject=Target.object +if TargetObject and TargetObject:isExist()and TargetObject.id_<50000000 then +local TargetUnit=UNIT:Find(TargetObject) +if TargetUnit and TargetUnit:IsAlive()then +local TargetUnitName=TargetUnit:GetName() +if self.DetectionZone then +if TargetUnit:IsInZone(self.DetectionZone)then +self:T({"Detected ",TargetUnit}) +if self.DetectedUnits[TargetUnit]==nil then +self.DetectedUnits[TargetUnit]=true +end +Detected=true +end +else +if self.DetectedUnits[TargetUnit]==nil then +self.DetectedUnits[TargetUnit]=true +end +Detected=true +end +end +end +end +self:__Detect(-self.DetectInterval) +if Detected==true then +self:__Detected(1.5) +end +end +function AI_PATROL_ZONE:_NewPatrolRoute(AIControllable) +local PatrolZone=AIControllable:GetState(AIControllable,"PatrolZone") +PatrolZone:__Route(1) +end +function AI_PATROL_ZONE:onafterRoute(Controllable,From,Event,To) +self:F2() +if From=="RTB"then +return +end +if self.Controllable:IsAlive()then +local PatrolRoute={} +if self.Controllable:InAir()==false then +self:T("Not in the air, finding route path within PatrolZone") +local CurrentVec2=self.Controllable:GetVec2() +local CurrentAltitude=self.Controllable:GetUnit(1):GetAltitude() +local CurrentPointVec3=POINT_VEC3:New(CurrentVec2.x,CurrentAltitude,CurrentVec2.y) +local ToPatrolZoneSpeed=self.PatrolMaxSpeed +local CurrentRoutePoint=CurrentPointVec3:WaypointAir( +self.PatrolAltType, +POINT_VEC3.RoutePointType.TakeOffParking, +POINT_VEC3.RoutePointAction.FromParkingArea, +ToPatrolZoneSpeed, +true +) +PatrolRoute[#PatrolRoute+1]=CurrentRoutePoint +else +self:T("In the air, finding route path within PatrolZone") +local CurrentVec2=self.Controllable:GetVec2() +local CurrentAltitude=self.Controllable:GetUnit(1):GetAltitude() +local CurrentPointVec3=POINT_VEC3:New(CurrentVec2.x,CurrentAltitude,CurrentVec2.y) +local ToPatrolZoneSpeed=self.PatrolMaxSpeed +local CurrentRoutePoint=CurrentPointVec3:WaypointAir( +self.PatrolAltType, +POINT_VEC3.RoutePointType.TurningPoint, +POINT_VEC3.RoutePointAction.TurningPoint, +ToPatrolZoneSpeed, +true +) +PatrolRoute[#PatrolRoute+1]=CurrentRoutePoint +end +local ToTargetVec2=self.PatrolZone:GetRandomVec2() +self:T2(ToTargetVec2) +local ToTargetAltitude=math.random(self.PatrolFloorAltitude,self.PatrolCeilingAltitude) +local ToTargetSpeed=math.random(self.PatrolMinSpeed,self.PatrolMaxSpeed) +self:T2({self.PatrolMinSpeed,self.PatrolMaxSpeed,ToTargetSpeed}) +local ToTargetPointVec3=POINT_VEC3:New(ToTargetVec2.x,ToTargetAltitude,ToTargetVec2.y) +local ToTargetRoutePoint=ToTargetPointVec3:WaypointAir( +self.PatrolAltType, +POINT_VEC3.RoutePointType.TurningPoint, +POINT_VEC3.RoutePointAction.TurningPoint, +ToTargetSpeed, +true +) +PatrolRoute[#PatrolRoute+1]=ToTargetRoutePoint +self.Controllable:WayPointInitialize(PatrolRoute) +self.Controllable:SetState(self.Controllable,"PatrolZone",self) +self.Controllable:WayPointFunction(#PatrolRoute,1,"AI_PATROL_ZONE:_NewPatrolRoute") +self.Controllable:WayPointExecute(1,2) +end +end +function AI_PATROL_ZONE:onbeforeStatus() +return self.CheckStatus +end +function AI_PATROL_ZONE:onafterStatus() +self:F2() +if self.Controllable and self.Controllable:IsAlive()then +local RTB=false +local Fuel=self.Controllable:GetFuelMin() +if Fuel Engaging') +self:__Engage(1) +end +end +end +function AI_CAP_ZONE:onafterAbort(Controllable,From,Event,To) +Controllable:ClearTasks() +self:__Route(1) +end +function AI_CAP_ZONE:onafterEngage(Controllable,From,Event,To) +if Controllable and Controllable:IsAlive()then +local EngageRoute={} +local CurrentVec2=self.Controllable:GetVec2() +local CurrentAltitude=self.Controllable:GetUnit(1):GetAltitude() +local CurrentPointVec3=POINT_VEC3:New(CurrentVec2.x,CurrentAltitude,CurrentVec2.y) +local ToEngageZoneSpeed=self.PatrolMaxSpeed +local CurrentRoutePoint=CurrentPointVec3:WaypointAir( +self.PatrolAltType, +POINT_VEC3.RoutePointType.TurningPoint, +POINT_VEC3.RoutePointAction.TurningPoint, +ToEngageZoneSpeed, +true +) +EngageRoute[#EngageRoute+1]=CurrentRoutePoint +local ToTargetVec2=self.PatrolZone:GetRandomVec2() +self:T2(ToTargetVec2) +local ToTargetAltitude=math.random(self.EngageFloorAltitude,self.EngageCeilingAltitude) +local ToTargetSpeed=math.random(self.PatrolMinSpeed,self.PatrolMaxSpeed) +self:T2({self.PatrolMinSpeed,self.PatrolMaxSpeed,ToTargetSpeed}) +local ToTargetPointVec3=POINT_VEC3:New(ToTargetVec2.x,ToTargetAltitude,ToTargetVec2.y) +local ToPatrolRoutePoint=ToTargetPointVec3:WaypointAir( +self.PatrolAltType, +POINT_VEC3.RoutePointType.TurningPoint, +POINT_VEC3.RoutePointAction.TurningPoint, +ToTargetSpeed, +true +) +EngageRoute[#EngageRoute+1]=ToPatrolRoutePoint +Controllable:OptionROEOpenFire() +Controllable:OptionROTEvadeFire() +local AttackTasks={} +for DetectedUnit,Detected in pairs(self.DetectedUnits)do +local DetectedUnit=DetectedUnit +self:T({DetectedUnit,DetectedUnit:IsAlive(),DetectedUnit:IsAir()}) +if DetectedUnit:IsAlive()and DetectedUnit:IsAir()then +if self.EngageZone then +if DetectedUnit:IsInZone(self.EngageZone)then +self:F({"Within Zone and Engaging ",DetectedUnit}) +AttackTasks[#AttackTasks+1]=Controllable:TaskAttackUnit(DetectedUnit) +end +else +if self.EngageRange then +if DetectedUnit:GetPointVec3():Get2DDistance(Controllable:GetPointVec3())<=self.EngageRange then +self:F({"Within Range and Engaging",DetectedUnit}) +AttackTasks[#AttackTasks+1]=Controllable:TaskAttackUnit(DetectedUnit) +end +else +AttackTasks[#AttackTasks+1]=Controllable:TaskAttackUnit(DetectedUnit) +end +end +else +self.DetectedUnits[DetectedUnit]=nil +end +end +if#AttackTasks==0 then +self:F("No targets found -> Going back to Patrolling") +self:__Abort(1) +self:__Route(1) +self:SetDetectionActivated() +else +AttackTasks[#AttackTasks+1]=Controllable:TaskFunction("AI_CAP_ZONE.EngageRoute",self) +EngageRoute[1].task=Controllable:TaskCombo(AttackTasks) +self:SetDetectionDeactivated() +end +Controllable:Route(EngageRoute,0.5) +end +end +function AI_CAP_ZONE:onafterAccomplish(Controllable,From,Event,To) +self.Accomplished=true +self:SetDetectionOff() +end +function AI_CAP_ZONE:onafterDestroy(Controllable,From,Event,To,EventData) +if EventData.IniUnit then +self.DetectedUnits[EventData.IniUnit]=nil +end +end +function AI_CAP_ZONE:OnEventDead(EventData) +self:F({"EventDead",EventData}) +if EventData.IniDCSUnit then +if self.DetectedUnits and self.DetectedUnits[EventData.IniUnit]then +self:__Destroy(1,EventData) +end +end +end +AI_CAS_ZONE={ +ClassName="AI_CAS_ZONE", +} +function AI_CAS_ZONE:New(PatrolZone,PatrolFloorAltitude,PatrolCeilingAltitude,PatrolMinSpeed,PatrolMaxSpeed,EngageZone,PatrolAltType) +local self=BASE:Inherit(self,AI_PATROL_ZONE:New(PatrolZone,PatrolFloorAltitude,PatrolCeilingAltitude,PatrolMinSpeed,PatrolMaxSpeed,PatrolAltType)) +self.EngageZone=EngageZone +self.Accomplished=false +self:SetDetectionZone(self.EngageZone) +self:AddTransition({"Patrolling","Engaging"},"Engage","Engaging") +self:AddTransition("Engaging","Target","Engaging") +self:AddTransition("Engaging","Fired","Engaging") +self:AddTransition("*","Destroy","*") +self:AddTransition("Engaging","Abort","Patrolling") +self:AddTransition("Engaging","Accomplish","Patrolling") +return self +end +function AI_CAS_ZONE:SetEngageZone(EngageZone) +self:F2() +if EngageZone then +self.EngageZone=EngageZone +else +self.EngageZone=nil +end +end +function AI_CAS_ZONE:onafterStart(Controllable,From,Event,To) +self:GetParent(self).onafterStart(self,Controllable,From,Event,To) +self:HandleEvent(EVENTS.Dead) +self:SetDetectionDeactivated() +end +function AI_CAS_ZONE.EngageRoute(EngageGroup,Fsm) +EngageGroup:F({"AI_CAS_ZONE.EngageRoute:",EngageGroup:GetName()}) +if EngageGroup:IsAlive()then +Fsm:__Engage(1,Fsm.EngageSpeed,Fsm.EngageAltitude,Fsm.EngageWeaponExpend,Fsm.EngageAttackQty,Fsm.EngageDirection) +end +end +function AI_CAS_ZONE:onbeforeEngage(Controllable,From,Event,To) +if self.Accomplished==true then +return false +end +end +function AI_CAS_ZONE:onafterTarget(Controllable,From,Event,To) +if Controllable:IsAlive()then +local AttackTasks={} +for DetectedUnit,Detected in pairs(self.DetectedUnits)do +local DetectedUnit=DetectedUnit +if DetectedUnit:IsAlive()then +if DetectedUnit:IsInZone(self.EngageZone)then +if Detected==true then +self:F({"Target: ",DetectedUnit}) +self.DetectedUnits[DetectedUnit]=false +local AttackTask=Controllable:TaskAttackUnit(DetectedUnit,false,self.EngageWeaponExpend,self.EngageAttackQty,self.EngageDirection,self.EngageAltitude,nil) +self.Controllable:PushTask(AttackTask,1) +end +end +else +self.DetectedUnits[DetectedUnit]=nil +end +end +self:__Target(-10) +end +end +function AI_CAS_ZONE:onafterAbort(Controllable,From,Event,To) +Controllable:ClearTasks() +self:__Route(1) +end +function AI_CAS_ZONE:onafterEngage(Controllable,From,Event,To, +EngageSpeed, +EngageAltitude, +EngageWeaponExpend, +EngageAttackQty, +EngageDirection) +self:F("onafterEngage") +self.EngageSpeed=EngageSpeed or 400 +self.EngageAltitude=EngageAltitude or 2000 +self.EngageWeaponExpend=EngageWeaponExpend +self.EngageAttackQty=EngageAttackQty +self.EngageDirection=EngageDirection +if Controllable:IsAlive()then +Controllable:OptionROEOpenFire() +Controllable:OptionROTVertical() +local EngageRoute={} +local CurrentVec2=self.Controllable:GetVec2() +local CurrentAltitude=self.Controllable:GetUnit(1):GetAltitude() +local CurrentPointVec3=POINT_VEC3:New(CurrentVec2.x,CurrentAltitude,CurrentVec2.y) +local ToEngageZoneSpeed=self.PatrolMaxSpeed +local CurrentRoutePoint=CurrentPointVec3:WaypointAir( +self.PatrolAltType, +POINT_VEC3.RoutePointType.TurningPoint, +POINT_VEC3.RoutePointAction.TurningPoint, +self.EngageSpeed, +true +) +EngageRoute[#EngageRoute+1]=CurrentRoutePoint +local AttackTasks={} +for DetectedUnit,Detected in pairs(self.DetectedUnits)do +local DetectedUnit=DetectedUnit +self:T(DetectedUnit) +if DetectedUnit:IsAlive()then +if DetectedUnit:IsInZone(self.EngageZone)then +self:F({"Engaging ",DetectedUnit}) +AttackTasks[#AttackTasks+1]=Controllable:TaskAttackUnit(DetectedUnit, +true, +EngageWeaponExpend, +EngageAttackQty, +EngageDirection +) +end +else +self.DetectedUnits[DetectedUnit]=nil +end +end +AttackTasks[#AttackTasks+1]=Controllable:TaskFunction("AI_CAS_ZONE.EngageRoute",self) +EngageRoute[#EngageRoute].task=Controllable:TaskCombo(AttackTasks) +local ToTargetVec2=self.EngageZone:GetRandomVec2() +self:T2(ToTargetVec2) +local ToTargetPointVec3=POINT_VEC3:New(ToTargetVec2.x,self.EngageAltitude,ToTargetVec2.y) +local ToTargetRoutePoint=ToTargetPointVec3:WaypointAir( +self.PatrolAltType, +POINT_VEC3.RoutePointType.TurningPoint, +POINT_VEC3.RoutePointAction.TurningPoint, +self.EngageSpeed, +true +) +EngageRoute[#EngageRoute+1]=ToTargetRoutePoint +Controllable:Route(EngageRoute,0.5) +self:SetRefreshTimeInterval(2) +self:SetDetectionActivated() +self:__Target(-2) +end +end +function AI_CAS_ZONE:onafterAccomplish(Controllable,From,Event,To) +self.Accomplished=true +self:SetDetectionDeactivated() +end +function AI_CAS_ZONE:onafterDestroy(Controllable,From,Event,To,EventData) +if EventData.IniUnit then +self.DetectedUnits[EventData.IniUnit]=nil +end +end +function AI_CAS_ZONE:OnEventDead(EventData) +self:F({"EventDead",EventData}) +if EventData.IniDCSUnit then +if self.DetectedUnits and self.DetectedUnits[EventData.IniUnit]then +self:__Destroy(1,EventData) +end +end +end +AI_BAI_ZONE={ +ClassName="AI_BAI_ZONE", +} +function AI_BAI_ZONE:New(PatrolZone,PatrolFloorAltitude,PatrolCeilingAltitude,PatrolMinSpeed,PatrolMaxSpeed,EngageZone,PatrolAltType) +local self=BASE:Inherit(self,AI_PATROL_ZONE:New(PatrolZone,PatrolFloorAltitude,PatrolCeilingAltitude,PatrolMinSpeed,PatrolMaxSpeed,PatrolAltType)) +self.EngageZone=EngageZone +self.Accomplished=false +self:SetDetectionZone(self.EngageZone) +self:SearchOn() +self:AddTransition({"Patrolling","Engaging"},"Engage","Engaging") +self:AddTransition("Engaging","Target","Engaging") +self:AddTransition("Engaging","Fired","Engaging") +self:AddTransition("*","Destroy","*") +self:AddTransition("Engaging","Abort","Patrolling") +self:AddTransition("Engaging","Accomplish","Patrolling") +return self +end +function AI_BAI_ZONE:SetEngageZone(EngageZone) +self:F2() +if EngageZone then +self.EngageZone=EngageZone +else +self.EngageZone=nil +end +end +function AI_BAI_ZONE:SearchOnOff(Search) +self.Search=Search +return self +end +function AI_BAI_ZONE:SearchOff() +self:SearchOnOff(false) +return self +end +function AI_BAI_ZONE:SearchOn() +self:SearchOnOff(true) +return self +end +function AI_BAI_ZONE:onafterStart(Controllable,From,Event,To) +self:GetParent(self).onafterStart(self,Controllable,From,Event,To) +self:HandleEvent(EVENTS.Dead) +self:SetDetectionDeactivated() +end +function _NewEngageRoute(AIControllable) +AIControllable:T("NewEngageRoute") +local EngageZone=AIControllable:GetState(AIControllable,"EngageZone") +EngageZone:__Engage(1,EngageZone.EngageSpeed,EngageZone.EngageAltitude,EngageZone.EngageWeaponExpend,EngageZone.EngageAttackQty,EngageZone.EngageDirection) +end +function AI_BAI_ZONE:onbeforeEngage(Controllable,From,Event,To) +if self.Accomplished==true then +return false +end +end +function AI_BAI_ZONE:onafterTarget(Controllable,From,Event,To) +self:F({"onafterTarget",self.Search,Controllable:IsAlive()}) +if Controllable:IsAlive()then +local AttackTasks={} +if self.Search==true then +for DetectedUnit,Detected in pairs(self.DetectedUnits)do +local DetectedUnit=DetectedUnit +if DetectedUnit:IsAlive()then +if DetectedUnit:IsInZone(self.EngageZone)then +if Detected==true then +self:F({"Target: ",DetectedUnit}) +self.DetectedUnits[DetectedUnit]=false +local AttackTask=Controllable:TaskAttackUnit(DetectedUnit,false,self.EngageWeaponExpend,self.EngageAttackQty,self.EngageDirection,self.EngageAltitude,nil) +self.Controllable:PushTask(AttackTask,1) +end +end +else +self.DetectedUnits[DetectedUnit]=nil +end +end +else +self:F("Attack zone") +local AttackTask=Controllable:TaskAttackMapObject( +self.EngageZone:GetPointVec2():GetVec2(), +true, +self.EngageWeaponExpend, +self.EngageAttackQty, +self.EngageDirection, +self.EngageAltitude +) +self.Controllable:PushTask(AttackTask,1) +end +self:__Target(-10) +end +end +function AI_BAI_ZONE:onafterAbort(Controllable,From,Event,To) +Controllable:ClearTasks() +self:__Route(1) +end +function AI_BAI_ZONE:onafterEngage(Controllable,From,Event,To, +EngageSpeed, +EngageAltitude, +EngageWeaponExpend, +EngageAttackQty, +EngageDirection) +self:F("onafterEngage") +self.EngageSpeed=EngageSpeed or 400 +self.EngageAltitude=EngageAltitude or 2000 +self.EngageWeaponExpend=EngageWeaponExpend +self.EngageAttackQty=EngageAttackQty +self.EngageDirection=EngageDirection +if Controllable:IsAlive()then +local EngageRoute={} +local CurrentVec2=self.Controllable:GetVec2() +local CurrentAltitude=self.Controllable:GetUnit(1):GetAltitude() +local CurrentPointVec3=POINT_VEC3:New(CurrentVec2.x,CurrentAltitude,CurrentVec2.y) +local ToEngageZoneSpeed=self.PatrolMaxSpeed +local CurrentRoutePoint=CurrentPointVec3:WaypointAir( +self.PatrolAltType, +POINT_VEC3.RoutePointType.TurningPoint, +POINT_VEC3.RoutePointAction.TurningPoint, +self.EngageSpeed, +true +) +EngageRoute[#EngageRoute+1]=CurrentRoutePoint +local AttackTasks={} +if self.Search==true then +for DetectedUnitID,DetectedUnitData in pairs(self.DetectedUnits)do +local DetectedUnit=DetectedUnitData +self:T(DetectedUnit) +if DetectedUnit:IsAlive()then +if DetectedUnit:IsInZone(self.EngageZone)then +self:F({"Engaging ",DetectedUnit}) +AttackTasks[#AttackTasks+1]=Controllable:TaskBombing( +DetectedUnit:GetPointVec2():GetVec2(), +true, +EngageWeaponExpend, +EngageAttackQty, +EngageDirection, +EngageAltitude +) +end +else +self.DetectedUnits[DetectedUnit]=nil +end +end +else +self:F("Attack zone") +AttackTasks[#AttackTasks+1]=Controllable:TaskAttackMapObject( +self.EngageZone:GetPointVec2():GetVec2(), +true, +EngageWeaponExpend, +EngageAttackQty, +EngageDirection, +EngageAltitude +) +end +EngageRoute[#EngageRoute].task=Controllable:TaskCombo(AttackTasks) +local ToTargetVec2=self.EngageZone:GetRandomVec2() +self:T2(ToTargetVec2) +local ToTargetPointVec3=POINT_VEC3:New(ToTargetVec2.x,self.EngageAltitude,ToTargetVec2.y) +local ToTargetRoutePoint=ToTargetPointVec3:WaypointAir( +self.PatrolAltType, +POINT_VEC3.RoutePointType.TurningPoint, +POINT_VEC3.RoutePointAction.TurningPoint, +self.EngageSpeed, +true +) +EngageRoute[#EngageRoute+1]=ToTargetRoutePoint +Controllable:OptionROEOpenFire() +Controllable:OptionROTVertical() +Controllable:WayPointInitialize(EngageRoute) +Controllable:SetState(Controllable,"EngageZone",self) +Controllable:WayPointFunction(#EngageRoute,1,"_NewEngageRoute") +Controllable:WayPointExecute(1) +self:SetRefreshTimeInterval(2) +self:SetDetectionActivated() +self:__Target(-2) +end +end +function AI_BAI_ZONE:onafterAccomplish(Controllable,From,Event,To) +self.Accomplished=true +self:SetDetectionDeactivated() +end +function AI_BAI_ZONE:onafterDestroy(Controllable,From,Event,To,EventData) +if EventData.IniUnit then +self.DetectedUnits[EventData.IniUnit]=nil +end +end +function AI_BAI_ZONE:OnEventDead(EventData) +self:F({"EventDead",EventData}) +if EventData.IniDCSUnit then +if self.DetectedUnits and self.DetectedUnits[EventData.IniUnit]then +self:__Destroy(1,EventData) +end +end +end +AI_FORMATION={ +ClassName="AI_FORMATION", +FollowName=nil, +FollowUnit=nil, +FollowGroupSet=nil, +FollowMode=1, +MODE={ +FOLLOW=1, +MISSION=2, +}, +FollowScheduler=nil, +OptionROE=AI.Option.Air.val.ROE.OPEN_FIRE, +OptionReactionOnThreat=AI.Option.Air.val.REACTION_ON_THREAT.ALLOW_ABORT_MISSION, +dtFollow=0.5, +} +AI_FORMATION.__Enum={} +AI_FORMATION.__Enum.Formation={ +None=0, +Mission=1, +Line=2, +Trail=3, +Stack=4, +LeftLine=5, +RightLine=6, +LeftWing=7, +RightWing=8, +Vic=9, +Box=10, +} +AI_FORMATION.__Enum.Mode={ +Mission="M", +Formation="F", +Attack="A", +Reconnaissance="R", +} +AI_FORMATION.__Enum.ReportType={ +Airborne="*", +Airborne="A", +GroundRadar="R", +Ground="G", +} +function AI_FORMATION:New(FollowUnit,FollowGroupSet,FollowName,FollowBriefing) +local self=BASE:Inherit(self,FSM_SET:New(FollowGroupSet)) +self:F({FollowUnit,FollowGroupSet,FollowName}) +self.FollowUnit=FollowUnit +self.FollowGroupSet=FollowGroupSet +self.FollowGroupSet:ForEachGroup( +function(FollowGroup) +FollowGroup:SetState(self,"Mode",self.__Enum.Mode.Formation) +end +) +self:SetFlightModeFormation() +self:SetFlightRandomization(2) +self:SetStartState("None") +self:AddTransition("*","Stop","Stopped") +self:AddTransition({"None","Stopped"},"Start","Following") +self:AddTransition("*","FormationLine","*") +self:AddTransition("*","FormationTrail","*") +self:AddTransition("*","FormationStack","*") +self:AddTransition("*","FormationLeftLine","*") +self:AddTransition("*","FormationRightLine","*") +self:AddTransition("*","FormationLeftWing","*") +self:AddTransition("*","FormationRightWing","*") +self:AddTransition("*","FormationCenterWing","*") +self:AddTransition("*","FormationVic","*") +self:AddTransition("*","FormationBox","*") +self:AddTransition("*","Follow","Following") +self:FormationLeftLine(500,0,250,250) +self.FollowName=FollowName +self.FollowBriefing=FollowBriefing +self.CT1=0 +self.GT1=0 +self.FollowMode=AI_FORMATION.MODE.MISSION +return self +end +function AI_FORMATION:SetFollowTimeInterval(dt) +self.dtFollow=dt or 0.5 +return self +end +function AI_FORMATION:TestSmokeDirectionVector(SmokeDirection) +self.SmokeDirectionVector=(SmokeDirection==true)and true or false +return self +end +function AI_FORMATION:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,YSpace,ZStart,ZSpace,Formation) +self:F({FollowGroupSet,From,Event,To,XStart,XSpace,YStart,YSpace,ZStart,ZSpace,Formation}) +XStart=XStart or self.XStart +XSpace=XSpace or self.XSpace +YStart=YStart or self.YStart +YSpace=YSpace or self.YSpace +ZStart=ZStart or self.ZStart +ZSpace=ZSpace or self.ZSpace +FollowGroupSet:Flush(self) +local FollowSet=FollowGroupSet:GetSet() +local i=1 +for FollowID,FollowGroup in pairs(FollowSet)do +local PointVec3=POINT_VEC3:New() +PointVec3:SetX(XStart+i*XSpace) +PointVec3:SetY(YStart+i*YSpace) +PointVec3:SetZ(ZStart+i*ZSpace) +local Vec3=PointVec3:GetVec3() +FollowGroup:SetState(self,"FormationVec3",Vec3) +i=i+1 +FollowGroup:SetState(FollowGroup,"Formation",Formation) +end +return self +end +function AI_FORMATION:onafterFormationTrail(FollowGroupSet,From,Event,To,XStart,XSpace,YStart) +self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,0,0,self.__Enum.Formation.Trail) +return self +end +function AI_FORMATION:onafterFormationStack(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,YSpace) +self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,YSpace,0,0,self.__Enum.Formation.Stack) +return self +end +function AI_FORMATION:onafterFormationLeftLine(FollowGroupSet,From,Event,To,XStart,YStart,ZStart,ZSpace) +self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,0,YStart,0,-ZStart,-ZSpace,self.__Enum.Formation.LeftLine) +return self +end +function AI_FORMATION:onafterFormationRightLine(FollowGroupSet,From,Event,To,XStart,YStart,ZStart,ZSpace) +self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,0,YStart,0,ZStart,ZSpace,self.__Enum.Formation.RightLine) +return self +end +function AI_FORMATION:onafterFormationLeftWing(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,ZStart,ZSpace) +self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,-ZStart,-ZSpace,self.__Enum.Formation.LeftWing) +return self +end +function AI_FORMATION:onafterFormationRightWing(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,ZStart,ZSpace) +self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,ZStart,ZSpace,self.__Enum.Formation.RightWing) +return self +end +function AI_FORMATION:onafterFormationCenterWing(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,YSpace,ZStart,ZSpace) +local FollowSet=FollowGroupSet:GetSet() +local i=0 +for FollowID,FollowGroup in pairs(FollowSet)do +local PointVec3=POINT_VEC3:New() +local Side=(i%2==0)and 1 or-1 +local Row=i/2+1 +PointVec3:SetX(XStart+Row*XSpace) +PointVec3:SetY(YStart) +PointVec3:SetZ(Side*(ZStart+i*ZSpace)) +local Vec3=PointVec3:GetVec3() +FollowGroup:SetState(self,"FormationVec3",Vec3) +i=i+1 +FollowGroup:SetState(FollowGroup,"Formation",self.__Enum.Formation.Vic) +end +return self +end +function AI_FORMATION:onafterFormationVic(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,YSpace,ZStart,ZSpace) +self:onafterFormationCenterWing(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,YSpace,ZStart,ZSpace) +return self +end +function AI_FORMATION:onafterFormationBox(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,YSpace,ZStart,ZSpace,ZLevels) +local FollowSet=FollowGroupSet:GetSet() +local i=0 +for FollowID,FollowGroup in pairs(FollowSet)do +local PointVec3=POINT_VEC3:New() +local ZIndex=i%ZLevels +local XIndex=math.floor(i/ZLevels) +local YIndex=math.floor(i/ZLevels) +PointVec3:SetX(XStart+XIndex*XSpace) +PointVec3:SetY(YStart+YIndex*YSpace) +PointVec3:SetZ(-ZStart-(ZSpace*ZLevels/2)+ZSpace*ZIndex) +local Vec3=PointVec3:GetVec3() +FollowGroup:SetState(self,"FormationVec3",Vec3) +i=i+1 +FollowGroup:SetState(FollowGroup,"Formation",self.__Enum.Formation.Box) +end +return self +end +function AI_FORMATION:SetFlightRandomization(FlightRandomization) +self.FlightRandomization=FlightRandomization +return self +end +function AI_FORMATION:GetFlightMode(FollowGroup) +if FollowGroup then +FollowGroup:SetState(FollowGroup,"PreviousMode",FollowGroup:GetState(FollowGroup,"Mode")) +FollowGroup:SetState(FollowGroup,"Mode",self.__Enum.Mode.Mission) +end +return FollowGroup:GetState(FollowGroup,"Mode") +end +function AI_FORMATION:SetFlightModeMission(FollowGroup) +if FollowGroup then +FollowGroup:SetState(FollowGroup,"PreviousMode",FollowGroup:GetState(FollowGroup,"Mode")) +FollowGroup:SetState(FollowGroup,"Mode",self.__Enum.Mode.Mission) +else +self.FollowGroupSet:ForSomeGroupAlive( +function(FollowGroup) +FollowGroup:SetState(FollowGroup,"PreviousMode",FollowGroup:GetState(FollowGroup,"Mode")) +FollowGroup:SetState(FollowGroup,"Mode",self.__Enum.Mode.Mission) +end +) +end +return self +end +function AI_FORMATION:SetFlightModeAttack(FollowGroup) +if FollowGroup then +FollowGroup:SetState(FollowGroup,"PreviousMode",FollowGroup:GetState(FollowGroup,"Mode")) +FollowGroup:SetState(FollowGroup,"Mode",self.__Enum.Mode.Attack) +else +self.FollowGroupSet:ForSomeGroupAlive( +function(FollowGroup) +FollowGroup:SetState(FollowGroup,"PreviousMode",FollowGroup:GetState(FollowGroup,"Mode")) +FollowGroup:SetState(FollowGroup,"Mode",self.__Enum.Mode.Attack) +end +) +end +return self +end +function AI_FORMATION:SetFlightModeFormation(FollowGroup) +if FollowGroup then +FollowGroup:SetState(FollowGroup,"PreviousMode",FollowGroup:GetState(FollowGroup,"Mode")) +FollowGroup:SetState(FollowGroup,"Mode",self.__Enum.Mode.Formation) +else +self.FollowGroupSet:ForSomeGroupAlive( +function(FollowGroup) +FollowGroup:SetState(FollowGroup,"PreviousMode",FollowGroup:GetState(FollowGroup,"Mode")) +FollowGroup:SetState(FollowGroup,"Mode",self.__Enum.Mode.Formation) +end +) +end +return self +end +function AI_FORMATION:onafterStop(FollowGroupSet,From,Event,To) +self:E("Stopping formation.") +end +function AI_FORMATION:onbeforeFollow(FollowGroupSet,From,Event,To) +if From=="Stopped"then +return false +end +return true +end +function AI_FORMATION:onenterFollowing(FollowGroupSet) +if self.FollowUnit:IsAlive()then +local ClientUnit=self.FollowUnit +local CT1,CT2,CV1,CV2 +CT1=ClientUnit:GetState(self,"CT1") +local CuVec3=ClientUnit:GetVec3() +if CT1==nil or CT1==0 then +ClientUnit:SetState(self,"CV1",CuVec3) +ClientUnit:SetState(self,"CT1",timer.getTime()) +else +CT1=ClientUnit:GetState(self,"CT1") +CT2=timer.getTime() +CV1=ClientUnit:GetState(self,"CV1") +CV2=CuVec3 +ClientUnit:SetState(self,"CT1",CT2) +ClientUnit:SetState(self,"CV1",CV2) +end +for _,_group in pairs(FollowGroupSet:GetSet())do +local group=_group +if group and group:IsAlive()then +self:FollowMe(group,ClientUnit,CT1,CV1,CT2,CV2) +end +end +self:__Follow(-self.dtFollow) +end +end +function AI_FORMATION:FollowMe(FollowGroup,ClientUnit,CT1,CV1,CT2,CV2) +if FollowGroup:GetState(FollowGroup,"Mode")==self.__Enum.Mode.Formation and not self:Is("Stopped")then +self:T({Mode=FollowGroup:GetState(FollowGroup,"Mode")}) +FollowGroup:OptionROTEvadeFire() +FollowGroup:OptionROEReturnFire() +local GroupUnit=FollowGroup:GetUnit(1) +local GuVec3=GroupUnit:GetVec3() +local FollowFormation=FollowGroup:GetState(self,"FormationVec3") +if FollowFormation then +local FollowDistance=FollowFormation.x +local GT1=GroupUnit:GetState(self,"GT1") +if CT1==nil or CT1==0 or GT1==nil or GT1==0 then +GroupUnit:SetState(self,"GV1",GuVec3) +GroupUnit:SetState(self,"GT1",timer.getTime()) +else +local CD=((CV2.x-CV1.x)^2+(CV2.y-CV1.y)^2+(CV2.z-CV1.z)^2)^0.5 +local CT=CT2-CT1 +local CS=(3600/CT)*(CD/1000)/3.6 +local CDv={x=CV2.x-CV1.x,y=CV2.y-CV1.y,z=CV2.z-CV1.z} +local Ca=math.atan2(CDv.x,CDv.z) +local GT1=GroupUnit:GetState(self,"GT1") +local GT2=timer.getTime() +local GV1=GroupUnit:GetState(self,"GV1") +local GV2=GuVec3 +GV2.x=GV2.x+math.random(-self.FlightRandomization/2,self.FlightRandomization/2) +GV2.y=GV2.y+math.random(-self.FlightRandomization/2,self.FlightRandomization/2) +GV2.z=GV2.z+math.random(-self.FlightRandomization/2,self.FlightRandomization/2) +GroupUnit:SetState(self,"GT1",GT2) +GroupUnit:SetState(self,"GV1",GV2) +local GD=((GV2.x-GV1.x)^2+(GV2.y-GV1.y)^2+(GV2.z-GV1.z)^2)^0.5 +local GT=GT2-GT1 +local GDv={x=GV2.x-CV1.x,y=GV2.y-CV1.y,z=GV2.z-CV1.z} +local Alpha_T=math.atan2(GDv.x,GDv.z)-math.atan2(CDv.x,CDv.z) +local Alpha_R=(Alpha_T<0)and Alpha_T+2*math.pi or Alpha_T +local Position=math.cos(Alpha_R) +local GD=((GDv.x)^2+(GDv.z)^2)^0.5 +local Distance=GD*Position+-CS*0.5 +local GV={x=GV2.x-CV2.x,y=GV2.y-CV2.y,z=GV2.z-CV2.z} +local GH2={x=GV2.x,y=CV2.y+FollowFormation.y,z=GV2.z} +local alpha=math.atan2(GV.x,GV.z) +local GVx=FollowFormation.z*math.cos(Ca)+FollowFormation.x*math.sin(Ca) +local GVz=FollowFormation.x*math.cos(Ca)-FollowFormation.z*math.sin(Ca) +local Inclination=(Distance+FollowFormation.x)/10 +if Inclination<-30 then +Inclination=-30 +end +local CVI={ +x=CV2.x+CS*10*math.sin(Ca), +y=GH2.y+Inclination, +y=GH2.y, +z=CV2.z+CS*10*math.cos(Ca), +} +local DV={x=CV2.x-CVI.x,y=CV2.y-CVI.y,z=CV2.z-CVI.z} +local DVu={x=DV.x/FollowDistance,y=DV.y,z=DV.z/FollowDistance} +local GDV={x=CVI.x,y=CVI.y,z=CVI.z} +local ADDx=FollowFormation.x*math.cos(alpha)-FollowFormation.z*math.sin(alpha) +local ADDz=FollowFormation.z*math.cos(alpha)+FollowFormation.x*math.sin(alpha) +local GDV_Formation={ +x=GDV.x-GVx, +y=GDV.y, +z=GDV.z-GVz +} +if self.SmokeDirectionVector==true then +trigger.action.smoke(GDV,trigger.smokeColor.Green) +trigger.action.smoke(GDV_Formation,trigger.smokeColor.White) +end +local Time=120 +local Speed=-(Distance+FollowFormation.x)/Time +if Distance>-10000 then +Speed=-(Distance+FollowFormation.x)/60 +end +if Distance>-2500 then +Speed=-(Distance+FollowFormation.x)/20 +end +local GS=Speed+CS +FollowGroup:RouteToVec3(GDV_Formation,GS) +end +end +end +end +AI_ESCORT={ +ClassName="AI_ESCORT", +EscortName=nil, +EscortUnit=nil, +EscortGroup=nil, +EscortMode=1, +Targets={}, +FollowScheduler=nil, +ReportTargets=true, +OptionROE=AI.Option.Air.val.ROE.OPEN_FIRE, +OptionReactionOnThreat=AI.Option.Air.val.REACTION_ON_THREAT.ALLOW_ABORT_MISSION, +SmokeDirectionVector=false, +TaskPoints={} +} +AI_ESCORT.Detection=nil +function AI_ESCORT:New(EscortUnit,EscortGroupSet,EscortName,EscortBriefing) +local self=BASE:Inherit(self,AI_FORMATION:New(EscortUnit,EscortGroupSet,EscortName,EscortBriefing)) +self:F({EscortUnit,EscortGroupSet}) +self.PlayerUnit=self.FollowUnit +self.PlayerGroup=self.FollowUnit:GetGroup() +self.EscortName=EscortName +self.EscortGroupSet=EscortGroupSet +self.EscortGroupSet:SetSomeIteratorLimit(8) +self.EscortBriefing=EscortBriefing +self.Menu={} +self.FollowDistance=100 +self.CT1=0 +self.GT1=0 +EscortGroupSet:ForEachGroup( +function(EscortGroup) +if not self.PlayerUnit._EscortGroups then +self.PlayerUnit._EscortGroups={} +end +if not self.PlayerUnit._EscortGroups[EscortGroup:GetName()]then +self.PlayerUnit._EscortGroups[EscortGroup:GetName()]={} +self.PlayerUnit._EscortGroups[EscortGroup:GetName()].EscortGroup=EscortGroup +self.PlayerUnit._EscortGroups[EscortGroup:GetName()].EscortName=self.EscortName +self.PlayerUnit._EscortGroups[EscortGroup:GetName()].Detection=self.Detection +end +end +) +self:SetFlightReportType(self.__Enum.ReportType.All) +return self +end +function AI_ESCORT:_InitFlightMenus() +self:SetFlightMenuJoinUp() +self:SetFlightMenuFormation("Trail") +self:SetFlightMenuFormation("Stack") +self:SetFlightMenuFormation("LeftLine") +self:SetFlightMenuFormation("RightLine") +self:SetFlightMenuFormation("LeftWing") +self:SetFlightMenuFormation("RightWing") +self:SetFlightMenuFormation("Vic") +self:SetFlightMenuFormation("Box") +self:SetFlightMenuHoldAtEscortPosition() +self:SetFlightMenuHoldAtLeaderPosition() +self:SetFlightMenuFlare() +self:SetFlightMenuSmoke() +self:SetFlightMenuROE() +self:SetFlightMenuROT() +self:SetFlightMenuTargets() +self:SetFlightMenuReportType() +end +function AI_ESCORT:_InitEscortMenus(EscortGroup) +EscortGroup.EscortMenu=MENU_GROUP:New(self.PlayerGroup,EscortGroup:GetCallsign(),self.MainMenu) +self:SetEscortMenuJoinUp(EscortGroup) +self:SetEscortMenuResumeMission(EscortGroup) +self:SetEscortMenuHoldAtEscortPosition(EscortGroup) +self:SetEscortMenuHoldAtLeaderPosition(EscortGroup) +self:SetEscortMenuFlare(EscortGroup) +self:SetEscortMenuSmoke(EscortGroup) +self:SetEscortMenuROE(EscortGroup) +self:SetEscortMenuROT(EscortGroup) +self:SetEscortMenuTargets(EscortGroup) +end +function AI_ESCORT:_InitEscortRoute(EscortGroup) +EscortGroup.MissionRoute=EscortGroup:GetTaskRoute() +end +function AI_ESCORT:onafterStart(EscortGroupSet) +self:F() +EscortGroupSet:ForEachGroup( +function(EscortGroup) +EscortGroup:WayPointInitialize() +EscortGroup:OptionROTVertical() +EscortGroup:OptionROEOpenFire() +end +) +local LeaderEscort=EscortGroupSet:GetFirst() +if LeaderEscort then +local Report=REPORT:New("Escort reporting:") +Report:Add("Joining Up "..EscortGroupSet:GetUnitTypeNames():Text(", ").." from "..LeaderEscort:GetCoordinate():ToString(self.PlayerUnit)) +LeaderEscort:MessageTypeToGroup(Report:Text(),MESSAGE.Type.Information,self.PlayerUnit) +end +self.Detection=DETECTION_AREAS:New(EscortGroupSet,5000) +self.Detection:InitDetectVisual(true) +self.Detection:InitDetectIRST(true) +self.Detection:InitDetectOptical(true) +self.Detection:InitDetectRadar(true) +self.Detection:InitDetectRWR(true) +self.Detection:SetAcceptRange(100000) +self.Detection:__Start(30) +self.MainMenu=MENU_GROUP:New(self.PlayerGroup,self.EscortName) +self.FlightMenu=MENU_GROUP:New(self.PlayerGroup,"Flight",self.MainMenu) +self:_InitFlightMenus() +self.EscortGroupSet:ForSomeGroupAlive( +function(EscortGroup) +self:_InitEscortMenus(EscortGroup) +self:_InitEscortRoute(EscortGroup) +self:SetFlightModeFormation(EscortGroup) +function EscortGroup:OnEventDeadOrCrash(EventData) +self:F({"EventDead",EventData}) +self.EscortMenu:Remove() +end +EscortGroup:HandleEvent(EVENTS.Dead,EscortGroup.OnEventDeadOrCrash) +EscortGroup:HandleEvent(EVENTS.Crash,EscortGroup.OnEventDeadOrCrash) +end +) +end +function AI_ESCORT:onafterStop(EscortGroupSet) +self:F() +EscortGroupSet:ForEachGroup( +function(EscortGroup) +EscortGroup:WayPointInitialize() +EscortGroup:OptionROTVertical() +EscortGroup:OptionROEOpenFire() +end +) +self.Detection:Stop() +self.MainMenu:Remove() +end +function AI_ESCORT:SetDetection(Detection) +self.Detection=Detection +self.EscortGroup.Detection=self.Detection +self.PlayerUnit._EscortGroups[self.EscortGroup:GetName()].Detection=self.EscortGroup.Detection +Detection:__Start(1) +end +function AI_ESCORT:TestSmokeDirectionVector(SmokeDirection) +self.SmokeDirectionVector=(SmokeDirection==true)and true or false +end +function AI_ESCORT:MenusHelicopters(XStart,XSpace,YStart,YSpace,ZStart,ZSpace,ZLevels) +self:F() +self.XStart=XStart or 50 +self.XSpace=XSpace or 50 +self.YStart=YStart or 50 +self.YSpace=YSpace or 50 +self.ZStart=ZStart or 50 +self.ZSpace=ZSpace or 50 +self.ZLevels=ZLevels or 10 +self:MenuJoinUp() +self:MenuFormationTrail(self.XStart,self.XSpace,self.YStart) +self:MenuFormationStack(self.XStart,self.XSpace,self.YStart,self.YSpace) +self:MenuFormationLeftLine(self.XStart,self.YStart,self.ZStart,self.ZSpace) +self:MenuFormationRightLine(self.XStart,self.YStart,self.ZStart,self.ZSpace) +self:MenuFormationLeftWing(self.XStart,self.XSpace,self.YStart,self.ZStart,self.ZSpace) +self:MenuFormationRightWing(self.XStart,self.XSpace,self.YStart,self.ZStart,self.ZSpace) +self:MenuFormationVic(self.XStart,self.XSpace,self.YStart,self.YSpace,self.ZStart,self.ZSpace) +self:MenuFormationBox(self.XStart,self.XSpace,self.YStart,self.YSpace,self.ZStart,self.ZSpace,self.ZLevels) +self:MenuHoldAtEscortPosition(30) +self:MenuHoldAtEscortPosition(100) +self:MenuHoldAtEscortPosition(500) +self:MenuHoldAtLeaderPosition(30,500) +self:MenuFlare() +self:MenuSmoke() +self:MenuTargets(60) +self:MenuAssistedAttack() +self:MenuROE() +self:MenuROT() +return self +end +function AI_ESCORT:MenusAirplanes(XStart,XSpace,YStart,YSpace,ZStart,ZSpace,ZLevels) +self:F() +self.XStart=XStart or 50 +self.XSpace=XSpace or 50 +self.YStart=YStart or 50 +self.YSpace=YSpace or 50 +self.ZStart=ZStart or 50 +self.ZSpace=ZSpace or 50 +self.ZLevels=ZLevels or 10 +self:MenuJoinUp() +self:MenuFormationTrail(self.XStart,self.XSpace,self.YStart) +self:MenuFormationStack(self.XStart,self.XSpace,self.YStart,self.YSpace) +self:MenuFormationLeftLine(self.XStart,self.YStart,self.ZStart,self.ZSpace) +self:MenuFormationRightLine(self.XStart,self.YStart,self.ZStart,self.ZSpace) +self:MenuFormationLeftWing(self.XStart,self.XSpace,self.YStart,self.ZStart,self.ZSpace) +self:MenuFormationRightWing(self.XStart,self.XSpace,self.YStart,self.ZStart,self.ZSpace) +self:MenuFormationVic(self.XStart,self.XSpace,self.YStart,self.YSpace,self.ZStart,self.ZSpace) +self:MenuFormationBox(self.XStart,self.XSpace,self.YStart,self.YSpace,self.ZStart,self.ZSpace,self.ZLevels) +self:MenuHoldAtEscortPosition(1000,500) +self:MenuHoldAtLeaderPosition(1000,500) +self:MenuFlare() +self:MenuSmoke() +self:MenuTargets(60) +self:MenuAssistedAttack() +self:MenuROE() +self:MenuROT() +return self +end +function AI_ESCORT:SetFlightMenuFormation(Formation) +local FormationID="Formation"..Formation +local MenuFormation=self.Menu[FormationID] +if MenuFormation then +local Arguments=MenuFormation.Arguments +local FlightMenuFormation=MENU_GROUP:New(self.PlayerGroup,"Formation",self.MainMenu) +local MenuFlightFormationID=MENU_GROUP_COMMAND:New(self.PlayerGroup,Formation,FlightMenuFormation, +function(self,Formation,...) +self.EscortGroupSet:ForSomeGroupAlive( +function(EscortGroup,self,Formation,Arguments) +if EscortGroup:IsAir()then +self:E({FormationID=FormationID}) +self[FormationID](self,unpack(Arguments)) +end +end,self,Formation,Arguments +) +end,self,Formation,Arguments +) +end +return self +end +function AI_ESCORT:MenuFormation(Formation,...) +local FormationID="Formation"..Formation +self.Menu[FormationID]=self.Menu[FormationID]or{} +self.Menu[FormationID].Arguments=arg +end +function AI_ESCORT:MenuFormationTrail(XStart,XSpace,YStart) +self:MenuFormation("Trail",XStart,XSpace,YStart) +return self +end +function AI_ESCORT:MenuFormationStack(XStart,XSpace,YStart,YSpace) +self:MenuFormation("Stack",XStart,XSpace,YStart,YSpace) +return self +end +function AI_ESCORT:MenuFormationLeftLine(XStart,YStart,ZStart,ZSpace) +self:MenuFormation("LeftLine",XStart,YStart,ZStart,ZSpace) +return self +end +function AI_ESCORT:MenuFormationRightLine(XStart,YStart,ZStart,ZSpace) +self:MenuFormation("RightLine",XStart,YStart,ZStart,ZSpace) +return self +end +function AI_ESCORT:MenuFormationLeftWing(XStart,XSpace,YStart,ZStart,ZSpace) +self:MenuFormation("LeftWing",XStart,XSpace,YStart,ZStart,ZSpace) +return self +end +function AI_ESCORT:MenuFormationRightWing(XStart,XSpace,YStart,ZStart,ZSpace) +self:MenuFormation("RightWing",XStart,XSpace,YStart,ZStart,ZSpace) +return self +end +function AI_ESCORT:MenuFormationCenterWing(XStart,XSpace,YStart,YSpace,ZStart,ZSpace) +self:MenuFormation("CenterWing",XStart,XSpace,YStart,YSpace,ZStart,ZSpace) +return self +end +function AI_ESCORT:MenuFormationVic(XStart,XSpace,YStart,YSpace,ZStart,ZSpace) +self:MenuFormation("Vic",XStart,XSpace,YStart,YSpace,ZStart,ZSpace) +return self +end +function AI_ESCORT:MenuFormationBox(XStart,XSpace,YStart,YSpace,ZStart,ZSpace,ZLevels) +self:MenuFormation("Box",XStart,XSpace,YStart,YSpace,ZStart,ZSpace,ZLevels) +return self +end +function AI_ESCORT:SetFlightMenuJoinUp() +if self.Menu.JoinUp==true then +local FlightMenuReportNavigation=MENU_GROUP:New(self.PlayerGroup,"Navigation",self.FlightMenu) +local FlightMenuJoinUp=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Join Up",FlightMenuReportNavigation,AI_ESCORT._FlightJoinUp,self) +end +end +function AI_ESCORT:SetEscortMenuJoinUp(EscortGroup) +if self.Menu.JoinUp==true then +if EscortGroup:IsAir()then +local EscortGroupName=EscortGroup:GetName() +local EscortMenuReportNavigation=MENU_GROUP:New(self.PlayerGroup,"Navigation",EscortGroup.EscortMenu) +local EscortMenuJoinUp=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Join Up",EscortMenuReportNavigation,AI_ESCORT._JoinUp,self,EscortGroup) +end +end +end +function AI_ESCORT:MenuJoinUp() +self.Menu.JoinUp=true +return self +end +function AI_ESCORT:SetFlightMenuHoldAtEscortPosition() +for _,MenuHoldAtEscortPosition in pairs(self.Menu.HoldAtEscortPosition)do +local FlightMenuReportNavigation=MENU_GROUP:New(self.PlayerGroup,"Navigation",self.FlightMenu) +local FlightMenuHoldPosition=MENU_GROUP_COMMAND +:New( +self.PlayerGroup, +MenuHoldAtEscortPosition.MenuText, +FlightMenuReportNavigation, +AI_ESCORT._FlightHoldPosition, +self, +nil, +MenuHoldAtEscortPosition.Height, +MenuHoldAtEscortPosition.Speed +) +end +return self +end +function AI_ESCORT:SetEscortMenuHoldAtEscortPosition(EscortGroup) +for _,HoldAtEscortPosition in pairs(self.Menu.HoldAtEscortPosition)do +if EscortGroup:IsAir()then +local EscortGroupName=EscortGroup:GetName() +local EscortMenuReportNavigation=MENU_GROUP:New(self.PlayerGroup,"Navigation",EscortGroup.EscortMenu) +local EscortMenuHoldPosition=MENU_GROUP_COMMAND +:New( +self.PlayerGroup, +HoldAtEscortPosition.MenuText, +EscortMenuReportNavigation, +AI_ESCORT._HoldPosition, +self, +EscortGroup, +EscortGroup, +HoldAtEscortPosition.Height, +HoldAtEscortPosition.Speed +) +end +end +return self +end +function AI_ESCORT:MenuHoldAtEscortPosition(Height,Speed,MenuTextFormat) +self:F({Height,Speed,MenuTextFormat}) +if not Height then +Height=30 +end +if not Speed then +Speed=0 +end +local MenuText="" +if not MenuTextFormat then +if Speed==0 then +MenuText=string.format("Hold at %d meter",Height) +else +MenuText=string.format("Hold at %d meter at %d",Height,Speed) +end +else +if Speed==0 then +MenuText=string.format(MenuTextFormat,Height) +else +MenuText=string.format(MenuTextFormat,Height,Speed) +end +end +self.Menu.HoldAtEscortPosition=self.Menu.HoldAtEscortPosition or{} +self.Menu.HoldAtEscortPosition[#self.Menu.HoldAtEscortPosition+1]={} +self.Menu.HoldAtEscortPosition[#self.Menu.HoldAtEscortPosition].Height=Height +self.Menu.HoldAtEscortPosition[#self.Menu.HoldAtEscortPosition].Speed=Speed +self.Menu.HoldAtEscortPosition[#self.Menu.HoldAtEscortPosition].MenuText=MenuText +return self +end +function AI_ESCORT:SetFlightMenuHoldAtLeaderPosition() +for _,MenuHoldAtLeaderPosition in pairs(self.Menu.HoldAtLeaderPosition)do +local FlightMenuReportNavigation=MENU_GROUP:New(self.PlayerGroup,"Navigation",self.FlightMenu) +local FlightMenuHoldAtLeaderPosition=MENU_GROUP_COMMAND +:New( +self.PlayerGroup, +MenuHoldAtLeaderPosition.MenuText, +FlightMenuReportNavigation, +AI_ESCORT._FlightHoldPosition, +self, +self.PlayerGroup, +MenuHoldAtLeaderPosition.Height, +MenuHoldAtLeaderPosition.Speed +) +end +return self +end +function AI_ESCORT:SetEscortMenuHoldAtLeaderPosition(EscortGroup) +for _,HoldAtLeaderPosition in pairs(self.Menu.HoldAtLeaderPosition)do +if EscortGroup:IsAir()then +local EscortGroupName=EscortGroup:GetName() +local EscortMenuReportNavigation=MENU_GROUP:New(self.PlayerGroup,"Navigation",EscortGroup.EscortMenu) +local EscortMenuHoldAtLeaderPosition=MENU_GROUP_COMMAND +:New( +self.PlayerGroup, +HoldAtLeaderPosition.MenuText, +EscortMenuReportNavigation, +AI_ESCORT._HoldPosition, +self, +self.PlayerGroup, +EscortGroup, +HoldAtLeaderPosition.Height, +HoldAtLeaderPosition.Speed +) +end +end +return self +end +function AI_ESCORT:MenuHoldAtLeaderPosition(Height,Speed,MenuTextFormat) +self:F({Height,Speed,MenuTextFormat}) +if not Height then +Height=30 +end +if not Speed then +Speed=0 +end +local MenuText="" +if not MenuTextFormat then +if Speed==0 then +MenuText=string.format("Rejoin and hold at %d meter",Height) +else +MenuText=string.format("Rejoin and hold at %d meter at %d",Height,Speed) +end +else +if Speed==0 then +MenuText=string.format(MenuTextFormat,Height) +else +MenuText=string.format(MenuTextFormat,Height,Speed) +end +end +self.Menu.HoldAtLeaderPosition=self.Menu.HoldAtLeaderPosition or{} +self.Menu.HoldAtLeaderPosition[#self.Menu.HoldAtLeaderPosition+1]={} +self.Menu.HoldAtLeaderPosition[#self.Menu.HoldAtLeaderPosition].Height=Height +self.Menu.HoldAtLeaderPosition[#self.Menu.HoldAtLeaderPosition].Speed=Speed +self.Menu.HoldAtLeaderPosition[#self.Menu.HoldAtLeaderPosition].MenuText=MenuText +return self +end +function AI_ESCORT:MenuScanForTargets(Height,Seconds,MenuTextFormat) +self:F({Height,Seconds,MenuTextFormat}) +if self.EscortGroup:IsAir()then +if not self.EscortMenuScan then +self.EscortMenuScan=MENU_GROUP:New(self.PlayerGroup,"Scan for targets",self.EscortMenu) +end +if not Height then +Height=100 +end +if not Seconds then +Seconds=30 +end +local MenuText="" +if not MenuTextFormat then +if Seconds==0 then +MenuText=string.format("At %d meter",Height) +else +MenuText=string.format("At %d meter for %d seconds",Height,Seconds) +end +else +if Seconds==0 then +MenuText=string.format(MenuTextFormat,Height) +else +MenuText=string.format(MenuTextFormat,Height,Seconds) +end +end +if not self.EscortMenuScanForTargets then +self.EscortMenuScanForTargets={} +end +self.EscortMenuScanForTargets[#self.EscortMenuScanForTargets+1]=MENU_GROUP_COMMAND +:New( +self.PlayerGroup, +MenuText, +self.EscortMenuScan, +AI_ESCORT._ScanTargets, +self, +30 +) +end +return self +end +function AI_ESCORT:SetFlightMenuFlare() +for _,MenuFlare in pairs(self.Menu.Flare)do +local FlightMenuReportNavigation=MENU_GROUP:New(self.PlayerGroup,"Navigation",self.FlightMenu) +local FlightMenuFlare=MENU_GROUP:New(self.PlayerGroup,MenuFlare.MenuText,FlightMenuReportNavigation) +local FlightMenuFlareGreenFlight=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Release green flare",FlightMenuFlare,AI_ESCORT._FlightFlare,self,FLARECOLOR.Green,"Released a green flare!") +local FlightMenuFlareRedFlight=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Release red flare",FlightMenuFlare,AI_ESCORT._FlightFlare,self,FLARECOLOR.Red,"Released a red flare!") +local FlightMenuFlareWhiteFlight=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Release white flare",FlightMenuFlare,AI_ESCORT._FlightFlare,self,FLARECOLOR.White,"Released a white flare!") +local FlightMenuFlareYellowFlight=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Release yellow flare",FlightMenuFlare,AI_ESCORT._FlightFlare,self,FLARECOLOR.Yellow,"Released a yellow flare!") +end +return self +end +function AI_ESCORT:SetEscortMenuFlare(EscortGroup) +for _,MenuFlare in pairs(self.Menu.Flare)do +if EscortGroup:IsAir()then +local EscortGroupName=EscortGroup:GetName() +local EscortMenuReportNavigation=MENU_GROUP:New(self.PlayerGroup,"Navigation",EscortGroup.EscortMenu) +local EscortMenuFlare=MENU_GROUP:New(self.PlayerGroup,MenuFlare.MenuText,EscortMenuReportNavigation) +local EscortMenuFlareGreen=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Release green flare",EscortMenuFlare,AI_ESCORT._Flare,self,EscortGroup,FLARECOLOR.Green,"Released a green flare!") +local EscortMenuFlareRed=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Release red flare",EscortMenuFlare,AI_ESCORT._Flare,self,EscortGroup,FLARECOLOR.Red,"Released a red flare!") +local EscortMenuFlareWhite=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Release white flare",EscortMenuFlare,AI_ESCORT._Flare,self,EscortGroup,FLARECOLOR.White,"Released a white flare!") +local EscortMenuFlareYellow=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Release yellow flare",EscortMenuFlare,AI_ESCORT._Flare,self,EscortGroup,FLARECOLOR.Yellow,"Released a yellow flare!") +end +end +return self +end +function AI_ESCORT:MenuFlare(MenuTextFormat) +self:F() +local MenuText="" +if not MenuTextFormat then +MenuText="Flare" +else +MenuText=MenuTextFormat +end +self.Menu.Flare=self.Menu.Flare or{} +self.Menu.Flare[#self.Menu.Flare+1]={} +self.Menu.Flare[#self.Menu.Flare].MenuText=MenuText +return self +end +function AI_ESCORT:SetFlightMenuSmoke() +for _,MenuSmoke in pairs(self.Menu.Smoke)do +local FlightMenuReportNavigation=MENU_GROUP:New(self.PlayerGroup,"Navigation",self.FlightMenu) +local FlightMenuSmoke=MENU_GROUP:New(self.PlayerGroup,MenuSmoke.MenuText,FlightMenuReportNavigation) +local FlightMenuSmokeGreenFlight=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Release green smoke",FlightMenuSmoke,AI_ESCORT._FlightSmoke,self,SMOKECOLOR.Green,"Releasing green smoke!") +local FlightMenuSmokeRedFlight=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Release red smoke",FlightMenuSmoke,AI_ESCORT._FlightSmoke,self,SMOKECOLOR.Red,"Releasing red smoke!") +local FlightMenuSmokeWhiteFlight=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Release white smoke",FlightMenuSmoke,AI_ESCORT._FlightSmoke,self,SMOKECOLOR.White,"Releasing white smoke!") +local FlightMenuSmokeOrangeFlight=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Release orange smoke",FlightMenuSmoke,AI_ESCORT._FlightSmoke,self,SMOKECOLOR.Orange,"Releasing orange smoke!") +local FlightMenuSmokeBlueFlight=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Release blue smoke",FlightMenuSmoke,AI_ESCORT._FlightSmoke,self,SMOKECOLOR.Blue,"Releasing blue smoke!") +end +return self +end +function AI_ESCORT:SetEscortMenuSmoke(EscortGroup) +for _,MenuSmoke in pairs(self.Menu.Smoke)do +if EscortGroup:IsAir()then +local EscortGroupName=EscortGroup:GetName() +local EscortMenuReportNavigation=MENU_GROUP:New(self.PlayerGroup,"Navigation",EscortGroup.EscortMenu) +local EscortMenuSmoke=MENU_GROUP:New(self.PlayerGroup,MenuSmoke.MenuText,EscortMenuReportNavigation) +local EscortMenuSmokeGreen=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Release green smoke",EscortMenuSmoke,AI_ESCORT._Smoke,self,EscortGroup,SMOKECOLOR.Green,"Releasing green smoke!") +local EscortMenuSmokeRed=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Release red smoke",EscortMenuSmoke,AI_ESCORT._Smoke,self,EscortGroup,SMOKECOLOR.Red,"Releasing red smoke!") +local EscortMenuSmokeWhite=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Release white smoke",EscortMenuSmoke,AI_ESCORT._Smoke,self,EscortGroup,SMOKECOLOR.White,"Releasing white smoke!") +local EscortMenuSmokeOrange=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Release orange smoke",EscortMenuSmoke,AI_ESCORT._Smoke,self,EscortGroup,SMOKECOLOR.Orange,"Releasing orange smoke!") +local EscortMenuSmokeBlue=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Release blue smoke",EscortMenuSmoke,AI_ESCORT._Smoke,self,EscortGroup,SMOKECOLOR.Blue,"Releasing blue smoke!") +end +end +return self +end +function AI_ESCORT:MenuSmoke(MenuTextFormat) +self:F() +local MenuText="" +if not MenuTextFormat then +MenuText="Smoke" +else +MenuText=MenuTextFormat +end +self.Menu.Smoke=self.Menu.Smoke or{} +self.Menu.Smoke[#self.Menu.Smoke+1]={} +self.Menu.Smoke[#self.Menu.Smoke].MenuText=MenuText +return self +end +function AI_ESCORT:SetFlightMenuReportType() +local FlightMenuReportTargets=MENU_GROUP:New(self.PlayerGroup,"Report targets",self.FlightMenu) +local MenuStamp=FlightMenuReportTargets:GetStamp() +local FlightReportType=self:GetFlightReportType() +if FlightReportType~=self.__Enum.ReportType.All then +local FlightMenuReportTargetsAll=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Report all targets",FlightMenuReportTargets,AI_ESCORT._FlightSwitchReportTypeAll,self) +:SetTag("ReportType") +:SetStamp(MenuStamp) +end +if FlightReportType==self.__Enum.ReportType.All or FlightReportType~=self.__Enum.ReportType.Airborne then +local FlightMenuReportTargetsAirborne=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Report airborne targets",FlightMenuReportTargets,AI_ESCORT._FlightSwitchReportTypeAirborne,self) +:SetTag("ReportType") +:SetStamp(MenuStamp) +end +if FlightReportType==self.__Enum.ReportType.All or FlightReportType~=self.__Enum.ReportType.GroundRadar then +local FlightMenuReportTargetsGroundRadar=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Report gound radar targets",FlightMenuReportTargets,AI_ESCORT._FlightSwitchReportTypeGroundRadar,self) +:SetTag("ReportType") +:SetStamp(MenuStamp) +end +if FlightReportType==self.__Enum.ReportType.All or FlightReportType~=self.__Enum.ReportType.Ground then +local FlightMenuReportTargetsGround=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Report ground targets",FlightMenuReportTargets,AI_ESCORT._FlightSwitchReportTypeGround,self) +:SetTag("ReportType") +:SetStamp(MenuStamp) +end +FlightMenuReportTargets:RemoveSubMenus(MenuStamp,"ReportType") +end +function AI_ESCORT:SetFlightMenuTargets() +local FlightMenuReportTargets=MENU_GROUP:New(self.PlayerGroup,"Report targets",self.FlightMenu) +local FlightMenuReportTargetsNow=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Report targets now!",FlightMenuReportTargets,AI_ESCORT._FlightReportNearbyTargetsNow,self) +local FlightMenuReportTargetsOn=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Report targets on",FlightMenuReportTargets,AI_ESCORT._FlightSwitchReportNearbyTargets,self,true) +local FlightMenuReportTargetsOff=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Report targets off",FlightMenuReportTargets,AI_ESCORT._FlightSwitchReportNearbyTargets,self,false) +self.FlightMenuAttack=MENU_GROUP:New(self.PlayerGroup,"Attack targets",self.FlightMenu) +local FlightMenuAttackNearby=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Attack nearest targets",self.FlightMenuAttack,AI_ESCORT._FlightAttackNearestTarget,self):SetTag("Attack") +local FlightMenuAttackNearbyAir=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Attack nearest airborne targets",self.FlightMenuAttack,AI_ESCORT._FlightAttackNearestTarget,self,self.__Enum.ReportType.Air):SetTag("Attack") +local FlightMenuAttackNearbyGround=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Attack nearest ground targets",self.FlightMenuAttack,AI_ESCORT._FlightAttackNearestTarget,self,self.__Enum.ReportType.Ground):SetTag("Attack") +for _,MenuTargets in pairs(self.Menu.Targets)do +MenuTargets.FlightReportTargetsScheduler=SCHEDULER:New(self,self._FlightReportTargetsScheduler,{},MenuTargets.Interval,MenuTargets.Interval) +end +return self +end +function AI_ESCORT:SetEscortMenuTargets(EscortGroup) +for _,MenuTargets in pairs(self.Menu.Targets)do +if EscortGroup:IsAir()then +local EscortGroupName=EscortGroup:GetName() +EscortGroup.EscortMenuReportNearbyTargetsNow=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Report targets",EscortGroup.EscortMenu,AI_ESCORT._ReportNearbyTargetsNow,self,EscortGroup,true) +EscortGroup.ReportTargetsScheduler=SCHEDULER:New(self,self._ReportTargetsScheduler,{EscortGroup},1,MenuTargets.Interval) +EscortGroup.ResumeScheduler=SCHEDULER:New(self,self._ResumeScheduler,{EscortGroup},1,60) +end +end +return self +end +function AI_ESCORT:MenuTargets(Seconds) +self:F({Seconds}) +if not Seconds then +Seconds=30 +end +self.Menu.Targets=self.Menu.Targets or{} +self.Menu.Targets[#self.Menu.Targets+1]={} +self.Menu.Targets[#self.Menu.Targets].Interval=Seconds +return self +end +function AI_ESCORT:MenuAssistedAttack() +self:F() +self.EscortGroupSet:ForSomeGroupAlive( +function(EscortGroup) +if not EscortGroup:IsAir()then +self.EscortMenuTargetAssistance=MENU_GROUP:New(self.PlayerGroup,"Request assistance from",EscortGroup.EscortMenu) +end +end +) +return self +end +function AI_ESCORT:SetFlightMenuROE() +for _,MenuROE in pairs(self.Menu.ROE)do +local FlightMenuROE=MENU_GROUP:New(self.PlayerGroup,"Rule Of Engagement",self.FlightMenu) +local FlightMenuROEHoldFire=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Hold fire",FlightMenuROE,AI_ESCORT._FlightROEHoldFire,self,"Holding weapons!") +local FlightMenuROEReturnFire=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Return fire",FlightMenuROE,AI_ESCORT._FlightROEReturnFire,self,"Returning fire!") +local FlightMenuROEOpenFire=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Open Fire",FlightMenuROE,AI_ESCORT._FlightROEOpenFire,self,"Open fire at designated targets!") +local FlightMenuROEWeaponFree=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Engage all targets",FlightMenuROE,AI_ESCORT._FlightROEWeaponFree,self,"Engaging all targets!") +end +return self +end +function AI_ESCORT:SetEscortMenuROE(EscortGroup) +for _,MenuROE in pairs(self.Menu.ROE)do +if EscortGroup:IsAir()then +local EscortGroupName=EscortGroup:GetName() +local EscortMenuROE=MENU_GROUP:New(self.PlayerGroup,"Rule Of Engagement",EscortGroup.EscortMenu) +if EscortGroup:OptionROEHoldFirePossible()then +local EscortMenuROEHoldFire=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Hold fire",EscortMenuROE,AI_ESCORT._ROE,self,EscortGroup,EscortGroup.OptionROEHoldFire,"Holding weapons!") +end +if EscortGroup:OptionROEReturnFirePossible()then +local EscortMenuROEReturnFire=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Return fire",EscortMenuROE,AI_ESCORT._ROE,self,EscortGroup,EscortGroup.OptionROEReturnFire,"Returning fire!") +end +if EscortGroup:OptionROEOpenFirePossible()then +EscortGroup.EscortMenuROEOpenFire=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Open Fire",EscortMenuROE,AI_ESCORT._ROE,self,EscortGroup,EscortGroup.OptionROEOpenFire,"Opening fire on designated targets!!") +end +if EscortGroup:OptionROEWeaponFreePossible()then +EscortGroup.EscortMenuROEWeaponFree=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Engage all targets",EscortMenuROE,AI_ESCORT._ROE,self,EscortGroup,EscortGroup.OptionROEWeaponFree,"Opening fire on targets of opportunity!") +end +end +end +return self +end +function AI_ESCORT:MenuROE() +self:F() +self.Menu.ROE=self.Menu.ROE or{} +self.Menu.ROE[#self.Menu.ROE+1]={} +return self +end +function AI_ESCORT:SetFlightMenuROT() +for _,MenuROT in pairs(self.Menu.ROT)do +local FlightMenuROT=MENU_GROUP:New(self.PlayerGroup,"Reaction On Threat",self.FlightMenu) +local FlightMenuROTNoReaction=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Fight until death",FlightMenuROT,AI_ESCORT._FlightROTNoReaction,self,"Fighting until death!") +local FlightMenuROTPassiveDefense=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Use flares, chaff and jammers",FlightMenuROT,AI_ESCORT._FlightROTPassiveDefense,self,"Defending using jammers, chaff and flares!") +local FlightMenuROTEvadeFire=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Open fire",FlightMenuROT,AI_ESCORT._FlightROTEvadeFire,self,"Evading on enemy fire!") +local FlightMenuROTVertical=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Avoid radar and evade fire",FlightMenuROT,AI_ESCORT._FlightROTVertical,self,"Evading on enemy fire with vertical manoeuvres!") +end +return self +end +function AI_ESCORT:SetEscortMenuROT(EscortGroup) +for _,MenuROT in pairs(self.Menu.ROT)do +if EscortGroup:IsAir()then +local EscortGroupName=EscortGroup:GetName() +local EscortMenuROT=MENU_GROUP:New(self.PlayerGroup,"Reaction On Threat",EscortGroup.EscortMenu) +if not EscortGroup.EscortMenuEvasion then +if EscortGroup:OptionROTNoReactionPossible()then +local EscortMenuEvasionNoReaction=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Fight until death",EscortMenuROT,AI_ESCORT._ROT,self,EscortGroup,EscortGroup.OptionROTNoReaction,"Fighting until death!") +end +if EscortGroup:OptionROTPassiveDefensePossible()then +local EscortMenuEvasionPassiveDefense=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Use flares, chaff and jammers",EscortMenuROT,AI_ESCORT._ROT,self,EscortGroup,EscortGroup.OptionROTPassiveDefense,"Defending using jammers, chaff and flares!") +end +if EscortGroup:OptionROTEvadeFirePossible()then +local EscortMenuEvasionEvadeFire=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Open fire",EscortMenuROT,AI_ESCORT._ROT,self,EscortGroup,EscortGroup.OptionROTEvadeFire,"Evading on enemy fire!") +end +if EscortGroup:OptionROTVerticalPossible()then +local EscortMenuOptionEvasionVertical=MENU_GROUP_COMMAND:New(self.PlayerGroup,"Avoid radar and evade fire",EscortMenuROT,AI_ESCORT._ROT,self,EscortGroup,EscortGroup.OptionROTVertical,"Evading on enemy fire with vertical manoeuvres!") +end +end +end +end +return self +end +function AI_ESCORT:MenuROT(MenuTextFormat) +self:F(MenuTextFormat) +self.Menu.ROT=self.Menu.ROT or{} +self.Menu.ROT[#self.Menu.ROT+1]={} +return self +end +function AI_ESCORT:SetEscortMenuResumeMission(EscortGroup) +self:F() +if EscortGroup:IsAir()then +local EscortGroupName=EscortGroup:GetName() +EscortGroup.EscortMenuResumeMission=MENU_GROUP:New(self.PlayerGroup,"Resume from",EscortGroup.EscortMenu) +end +return self +end +function AI_ESCORT:_HoldPosition(OrbitGroup,EscortGroup,OrbitHeight,OrbitSeconds) +local EscortUnit=self.PlayerUnit +local OrbitUnit=OrbitGroup:GetUnit(1) +self:SetFlightModeMission(EscortGroup) +local PointFrom={} +local GroupVec3=EscortGroup:GetUnit(1):GetVec3() +PointFrom={} +PointFrom.x=GroupVec3.x +PointFrom.y=GroupVec3.z +PointFrom.speed=250 +PointFrom.type=AI.Task.WaypointType.TURNING_POINT +PointFrom.alt=GroupVec3.y +PointFrom.alt_type=AI.Task.AltitudeType.BARO +local OrbitPoint=OrbitUnit:GetVec2() +local PointTo={} +PointTo.x=OrbitPoint.x +PointTo.y=OrbitPoint.y +PointTo.speed=250 +PointTo.type=AI.Task.WaypointType.TURNING_POINT +PointTo.alt=OrbitHeight +PointTo.alt_type=AI.Task.AltitudeType.BARO +PointTo.task=EscortGroup:TaskOrbitCircleAtVec2(OrbitPoint,OrbitHeight,0) +local Points={PointFrom,PointTo} +EscortGroup:OptionROEHoldFire() +EscortGroup:OptionROTPassiveDefense() +EscortGroup:SetTask(EscortGroup:TaskRoute(Points),1) +EscortGroup:MessageTypeToGroup("Orbiting at current location.",MESSAGE.Type.Information,EscortUnit:GetGroup()) +end +function AI_ESCORT:_FlightHoldPosition(OrbitGroup,OrbitHeight,OrbitSeconds) +local EscortUnit=self.PlayerUnit +self.EscortGroupSet:ForEachGroupAlive( +function(EscortGroup,OrbitGroup) +if EscortGroup:IsAir()then +if OrbitGroup==nil then +OrbitGroup=EscortGroup +end +self:_HoldPosition(OrbitGroup,EscortGroup,OrbitHeight,OrbitSeconds) +end +end,OrbitGroup +) +end +function AI_ESCORT:_JoinUp(EscortGroup) +local EscortUnit=self.PlayerUnit +self:SetFlightModeFormation(EscortGroup) +EscortGroup:MessageTypeToGroup("Joining up!",MESSAGE.Type.Information,EscortUnit:GetGroup()) +end +function AI_ESCORT:_FlightJoinUp() +self.EscortGroupSet:ForEachGroupAlive( +function(EscortGroup) +if EscortGroup:IsAir()then +self:_JoinUp(EscortGroup) +end +end +) +end +function AI_ESCORT:_EscortFormationTrail(EscortGroup,XStart,XSpace,YStart) +self:FormationTrail(XStart,XSpace,YStart) +end +function AI_ESCORT:_FlightFormationTrail(XStart,XSpace,YStart) +self.EscortGroupSet:ForEachGroupAlive( +function(EscortGroup) +if EscortGroup:IsAir()then +self:_EscortFormationTrail(EscortGroup,XStart,XSpace,YStart) +end +end +) +end +function AI_ESCORT:_EscortFormationStack(EscortGroup,XStart,XSpace,YStart,YSpace) +self:FormationStack(XStart,XSpace,YStart,YSpace) +end +function AI_ESCORT:_FlightFormationStack(XStart,XSpace,YStart,YSpace) +self.EscortGroupSet:ForEachGroupAlive( +function(EscortGroup) +if EscortGroup:IsAir()then +self:_EscortFormationStack(EscortGroup,XStart,XSpace,YStart,YSpace) +end +end +) +end +function AI_ESCORT:_Flare(EscortGroup,Color,Message) +local EscortUnit=self.PlayerUnit +EscortGroup:GetUnit(1):Flare(Color) +EscortGroup:MessageTypeToGroup(Message,MESSAGE.Type.Information,EscortUnit:GetGroup()) +end +function AI_ESCORT:_FlightFlare(Color,Message) +self.EscortGroupSet:ForEachGroupAlive( +function(EscortGroup) +if EscortGroup:IsAir()then +self:_Flare(EscortGroup,Color,Message) +end +end +) +end +function AI_ESCORT:_Smoke(EscortGroup,Color,Message) +local EscortUnit=self.PlayerUnit +EscortGroup:GetUnit(1):Smoke(Color) +EscortGroup:MessageTypeToGroup(Message,MESSAGE.Type.Information,EscortUnit:GetGroup()) +end +function AI_ESCORT:_FlightSmoke(Color,Message) +self.EscortGroupSet:ForEachGroupAlive( +function(EscortGroup) +if EscortGroup:IsAir()then +self:_Smoke(EscortGroup,Color,Message) +end +end +) +end +function AI_ESCORT:_ReportNearbyTargetsNow(EscortGroup) +local EscortUnit=self.PlayerUnit +self:_ReportTargetsScheduler(EscortGroup) +end +function AI_ESCORT:_FlightReportNearbyTargetsNow() +self:_FlightReportTargetsScheduler() +end +function AI_ESCORT:_FlightSwitchReportNearbyTargets(ReportTargets) +self.EscortGroupSet:ForEachGroupAlive( +function(EscortGroup) +if EscortGroup:IsAir()then +self:_EscortSwitchReportNearbyTargets(EscortGroup,ReportTargets) +end +end +) +end +function AI_ESCORT:SetFlightReportType(ReportType) +self.FlightReportType=ReportType +end +function AI_ESCORT:GetFlightReportType() +return self.FlightReportType +end +function AI_ESCORT:_FlightSwitchReportTypeAll() +self:SetFlightReportType(self.__Enum.ReportType.All) +self:SetFlightMenuReportType() +local EscortGroup=self.EscortGroupSet:GetFirst() +EscortGroup:MessageTypeToGroup("Reporting all targets.",MESSAGE.Type.Information,self.PlayerGroup) +end +function AI_ESCORT:_FlightSwitchReportTypeAirborne() +self:SetFlightReportType(self.__Enum.ReportType.Airborne) +self:SetFlightMenuReportType() +local EscortGroup=self.EscortGroupSet:GetFirst() +EscortGroup:MessageTypeToGroup("Reporting airborne targets.",MESSAGE.Type.Information,self.PlayerGroup) +end +function AI_ESCORT:_FlightSwitchReportTypeGroundRadar() +self:SetFlightReportType(self.__Enum.ReportType.Ground) +self:SetFlightMenuReportType() +local EscortGroup=self.EscortGroupSet:GetFirst() +EscortGroup:MessageTypeToGroup("Reporting ground radar targets.",MESSAGE.Type.Information,self.PlayerGroup) +end +function AI_ESCORT:_FlightSwitchReportTypeGround() +self:SetFlightReportType(self.__Enum.ReportType.Ground) +self:SetFlightMenuReportType() +local EscortGroup=self.EscortGroupSet:GetFirst() +EscortGroup:MessageTypeToGroup("Reporting ground targets.",MESSAGE.Type.Information,self.PlayerGroup) +end +function AI_ESCORT:_ScanTargets(ScanDuration) +local EscortGroup=self.EscortGroup +local EscortUnit=self.PlayerUnit +self.FollowScheduler:Stop(self.FollowSchedule) +if EscortGroup:IsHelicopter()then +EscortGroup:PushTask( +EscortGroup:TaskControlled( +EscortGroup:TaskOrbitCircle(200,20), +EscortGroup:TaskCondition(nil,nil,nil,nil,ScanDuration,nil) +),1) +elseif EscortGroup:IsAirPlane()then +EscortGroup:PushTask( +EscortGroup:TaskControlled( +EscortGroup:TaskOrbitCircle(1000,500), +EscortGroup:TaskCondition(nil,nil,nil,nil,ScanDuration,nil) +),1) +end +EscortGroup:MessageToClient("Scanning targets for "..ScanDuration.." seconds.",ScanDuration,EscortUnit) +if self.EscortMode==AI_ESCORT.MODE.FOLLOW then +self.FollowScheduler:Start(self.FollowSchedule) +end +end +function AI_ESCORT.___Resume(EscortGroup,self) +self:F({self=self}) +local PlayerGroup=self.PlayerGroup +EscortGroup:OptionROEHoldFire() +EscortGroup:OptionROTVertical() +EscortGroup:SetState(EscortGroup,"Mode",EscortGroup:GetState(EscortGroup,"PreviousMode")) +if EscortGroup:GetState(EscortGroup,"Mode")==self.__Enum.Mode.Mission then +EscortGroup:MessageTypeToGroup("Resuming route.",MESSAGE.Type.Information,PlayerGroup) +else +EscortGroup:MessageTypeToGroup("Rejoining formation.",MESSAGE.Type.Information,PlayerGroup) +end +end +function AI_ESCORT:_ResumeMission(EscortGroup,WayPoint) +self:SetFlightModeMission(EscortGroup) +local WayPoints=EscortGroup.MissionRoute +self:T(WayPoint,WayPoints) +for WayPointIgnore=1,WayPoint do +table.remove(WayPoints,1) +end +EscortGroup:SetTask(EscortGroup:TaskRoute(WayPoints),1) +EscortGroup:MessageTypeToGroup("Resuming mission from waypoint ",MESSAGE.Type.Information,self.PlayerGroup) +end +function AI_ESCORT:_AttackTarget(EscortGroup,DetectedItem) +self:F(EscortGroup) +self:SetFlightModeAttack(EscortGroup) +if EscortGroup:IsAir()then +EscortGroup:OptionROEOpenFire() +EscortGroup:OptionROTVertical() +EscortGroup:SetState(EscortGroup,"Escort",self) +local DetectedSet=self.Detection:GetDetectedItemSet(DetectedItem) +local Tasks={} +local AttackUnitTasks={} +DetectedSet:ForEachUnit( +function(DetectedUnit,Tasks) +if DetectedUnit:IsAlive()then +AttackUnitTasks[#AttackUnitTasks+1]=EscortGroup:TaskAttackUnit(DetectedUnit) +end +end,Tasks +) +Tasks[#Tasks+1]=EscortGroup:TaskCombo(AttackUnitTasks) +Tasks[#Tasks+1]=EscortGroup:TaskFunction("AI_ESCORT.___Resume",self) +EscortGroup:PushTask( +EscortGroup:TaskCombo( +Tasks +),1 +) +else +local DetectedSet=self.Detection:GetDetectedItemSet(DetectedItem) +local Tasks={} +DetectedSet:ForEachUnit( +function(DetectedUnit,Tasks) +if DetectedUnit:IsAlive()then +Tasks[#Tasks+1]=EscortGroup:TaskFireAtPoint(DetectedUnit:GetVec2(),50) +end +end,Tasks +) +EscortGroup:PushTask( +EscortGroup:TaskCombo( +Tasks +),1 +) +end +local DetectedTargetsReport=REPORT:New("Engaging target:\n") +local DetectedItemReportSummary=self.Detection:DetectedItemReportSummary(DetectedItem,self.PlayerGroup,_DATABASE:GetPlayerSettings(self.PlayerUnit:GetPlayerName())) +local ReportSummary=DetectedItemReportSummary:Text(", ") +DetectedTargetsReport:AddIndent(ReportSummary,"-") +EscortGroup:MessageTypeToGroup(DetectedTargetsReport:Text(),MESSAGE.Type.Information,self.PlayerGroup) +end +function AI_ESCORT:_FlightAttackTarget(DetectedItem) +self.EscortGroupSet:ForEachGroupAlive( +function(EscortGroup,DetectedItem) +if EscortGroup:IsAir()then +self:_AttackTarget(EscortGroup,DetectedItem) +end +end,DetectedItem +) +end +function AI_ESCORT:_FlightAttackNearestTarget(TargetType) +self.Detection:Detect() +self:_FlightReportTargetsScheduler() +local EscortGroup=self.EscortGroupSet:GetFirst() +local AttackDetectedItem=nil +local DetectedItems=self.Detection:GetDetectedItems() +for DetectedItemIndex,DetectedItem in UTILS.spairs(DetectedItems,function(t,a,b)return self:Distance(self.PlayerUnit,t[a])0 +local HasAir=DetectedItemSet:HasAirUnits()>0 +local FlightReportType=self:GetFlightReportType() +if(TargetType and TargetType==self.__Enum.ReportType.Ground and HasGround)or +(TargetType and TargetType==self.__Enum.ReportType.Air and HasAir)or +(TargetType==nil)then +AttackDetectedItem=DetectedItem +break +end +end +if AttackDetectedItem then +self:_FlightAttackTarget(AttackDetectedItem) +else +EscortGroup:MessageTypeToGroup("Nothing to attack!",MESSAGE.Type.Information,self.PlayerGroup) +end +end +function AI_ESCORT:_AssistTarget(EscortGroup,DetectedItem) +local EscortUnit=self.PlayerUnit +local DetectedSet=self.Detection:GetDetectedItemSet(DetectedItem) +local Tasks={} +DetectedSet:ForEachUnit( +function(DetectedUnit,Tasks) +if DetectedUnit:IsAlive()then +Tasks[#Tasks+1]=EscortGroup:TaskFireAtPoint(DetectedUnit:GetVec2(),50) +end +end,Tasks +) +EscortGroup:SetTask( +EscortGroup:TaskCombo( +Tasks +),1 +) +EscortGroup:MessageTypeToGroup("Assisting attack!",MESSAGE.Type.Information,EscortUnit:GetGroup()) +end +function AI_ESCORT:_ROE(EscortGroup,EscortROEFunction,EscortROEMessage) +pcall(function()EscortROEFunction(EscortGroup)end) +EscortGroup:MessageTypeToGroup(EscortROEMessage,MESSAGE.Type.Information,self.PlayerGroup) +end +function AI_ESCORT:_FlightROEHoldFire(EscortROEMessage) +self.EscortGroupSet:ForEachGroupAlive( +function(EscortGroup) +self:_ROE(EscortGroup,EscortGroup.OptionROEHoldFire,EscortROEMessage) +end +) +end +function AI_ESCORT:_FlightROEOpenFire(EscortROEMessage) +self.EscortGroupSet:ForEachGroupAlive( +function(EscortGroup) +self:_ROE(EscortGroup,EscortGroup.OptionROEOpenFire,EscortROEMessage) +end +) +end +function AI_ESCORT:_FlightROEReturnFire(EscortROEMessage) +self.EscortGroupSet:ForEachGroupAlive( +function(EscortGroup) +self:_ROE(EscortGroup,EscortGroup.OptionROEReturnFire,EscortROEMessage) +end +) +end +function AI_ESCORT:_FlightROEWeaponFree(EscortROEMessage) +self.EscortGroupSet:ForEachGroupAlive( +function(EscortGroup) +self:_ROE(EscortGroup,EscortGroup.OptionROEWeaponFree,EscortROEMessage) +end +) +end +function AI_ESCORT:_ROT(EscortGroup,EscortROTFunction,EscortROTMessage) +pcall(function()EscortROTFunction(EscortGroup)end) +EscortGroup:MessageTypeToGroup(EscortROTMessage,MESSAGE.Type.Information,self.PlayerGroup) +end +function AI_ESCORT:_FlightROTNoReaction(EscortROTMessage) +self.EscortGroupSet:ForEachGroupAlive( +function(EscortGroup) +self:_ROT(EscortGroup,EscortGroup.OptionROTNoReaction,EscortROTMessage) +end +) +end +function AI_ESCORT:_FlightROTPassiveDefense(EscortROTMessage) +self.EscortGroupSet:ForEachGroupAlive( +function(EscortGroup) +self:_ROT(EscortGroup,EscortGroup.OptionROTPassiveDefense,EscortROTMessage) +end +) +end +function AI_ESCORT:_FlightROTEvadeFire(EscortROTMessage) +self.EscortGroupSet:ForEachGroupAlive( +function(EscortGroup) +self:_ROT(EscortGroup,EscortGroup.OptionROTEvadeFire,EscortROTMessage) +end +) +end +function AI_ESCORT:_FlightROTVertical(EscortROTMessage) +self.EscortGroupSet:ForEachGroupAlive( +function(EscortGroup) +self:_ROT(EscortGroup,EscortGroup.OptionROTVertical,EscortROTMessage) +end +) +end +function AI_ESCORT:RegisterRoute() +self:F() +local EscortGroup=self.EscortGroup +local TaskPoints=EscortGroup:GetTaskRoute() +self:T(TaskPoints) +return TaskPoints +end +function AI_ESCORT:_ResumeScheduler(EscortGroup) +self:F(EscortGroup:GetName()) +if EscortGroup:IsAlive()and self.PlayerUnit:IsAlive()then +local EscortGroupName=EscortGroup:GetCallsign() +if EscortGroup.EscortMenuResumeMission then +EscortGroup.EscortMenuResumeMission:RemoveSubMenus() +local TaskPoints=EscortGroup.MissionRoute +for WayPointID,WayPoint in pairs(TaskPoints)do +local EscortVec3=EscortGroup:GetVec3() +local Distance=((WayPoint.x-EscortVec3.x)^2+ +(WayPoint.y-EscortVec3.z)^2 +)^0.5/1000 +MENU_GROUP_COMMAND:New(self.PlayerGroup,"Waypoint "..WayPointID.." at "..string.format("%.2f",Distance).."km",EscortGroup.EscortMenuResumeMission,AI_ESCORT._ResumeMission,self,EscortGroup,WayPointID) +end +end +end +end +function AI_ESCORT:Distance(PlayerUnit,DetectedItem) +local DetectedCoordinate=self.Detection:GetDetectedItemCoordinate(DetectedItem) +local PlayerCoordinate=PlayerUnit:GetCoordinate() +return DetectedCoordinate:Get3DDistance(PlayerCoordinate) +end +function AI_ESCORT:_ReportTargetsScheduler(EscortGroup,Report) +self:F(EscortGroup:GetName()) +if EscortGroup:IsAlive()and self.PlayerUnit:IsAlive()then +local EscortGroupName=EscortGroup:GetCallsign() +local DetectedTargetsReport=REPORT:New("Reporting targets:\n") +if EscortGroup.EscortMenuTargetAssistance then +EscortGroup.EscortMenuTargetAssistance:RemoveSubMenus() +end +local DetectedItems=self.Detection:GetDetectedItems() +local ClientEscortTargets=self.Detection +local TimeUpdate=timer.getTime() +local EscortMenuAttackTargets=MENU_GROUP:New(self.PlayerGroup,"Attack targets",EscortGroup.EscortMenu) +local DetectedTargets=false +for DetectedItemIndex,DetectedItem in UTILS.spairs(DetectedItems,function(t,a,b)return self:Distance(self.PlayerUnit,t[a])0 +local HasGroundRadar=HasGround and DetectedItemSet:HasRadar()>0 +local HasAir=DetectedItemSet:HasAirUnits()>0 +local FlightReportType=self:GetFlightReportType() +if(FlightReportType==self.__Enum.ReportType.All)or +(FlightReportType==self.__Enum.ReportType.Airborne and HasAir)or +(FlightReportType==self.__Enum.ReportType.Ground and HasGround)or +(FlightReportType==self.__Enum.ReportType.GroundRadar and HasGroundRadar)then +DetectedTargets=true +local DetectedMenu=self.Detection:DetectedItemReportMenu(DetectedItem,EscortGroup,_DATABASE:GetPlayerSettings(self.PlayerUnit:GetPlayerName())):Text("\n") +local DetectedItemReportSummary=self.Detection:DetectedItemReportSummary(DetectedItem,EscortGroup,_DATABASE:GetPlayerSettings(self.PlayerUnit:GetPlayerName())) +local ReportSummary=DetectedItemReportSummary:Text(", ") +DetectedTargetsReport:AddIndent(ReportSummary,"-") +if EscortGroup:IsAir()then +MENU_GROUP_COMMAND:New(self.PlayerGroup, +DetectedMenu, +EscortMenuAttackTargets, +AI_ESCORT._AttackTarget, +self, +EscortGroup, +DetectedItem +):SetTag("Escort"):SetTime(TimeUpdate) +else +if self.EscortMenuTargetAssistance then +local MenuTargetAssistance=MENU_GROUP:New(self.PlayerGroup,EscortGroupName,EscortGroup.EscortMenuTargetAssistance) +MENU_GROUP_COMMAND:New(self.PlayerGroup, +DetectedMenu, +MenuTargetAssistance, +AI_ESCORT._AssistTarget, +self, +EscortGroup, +DetectedItem +) +end +end +end +end +EscortMenuAttackTargets:RemoveSubMenus(TimeUpdate,"Escort") +if Report then +if DetectedTargets then +EscortGroup:MessageTypeToGroup(DetectedTargetsReport:Text("\n"),MESSAGE.Type.Information,self.PlayerGroup) +else +EscortGroup:MessageTypeToGroup("No targets detected.",MESSAGE.Type.Information,self.PlayerGroup) +end +end +return true +end +return false +end +function AI_ESCORT:_FlightReportTargetsScheduler() +self:F("FlightReportTargetScheduler") +local EscortGroup=self.EscortGroupSet:GetFirst() +local DetectedTargetsReport=REPORT:New("Reporting your targets:\n") +if EscortGroup and(self.PlayerUnit:IsAlive()and EscortGroup:IsAlive())then +local TimeUpdate=timer.getTime() +local DetectedItems=self.Detection:GetDetectedItems() +local DetectedTargets=false +local ClientEscortTargets=self.Detection +for DetectedItemIndex,DetectedItem in UTILS.spairs(DetectedItems,function(t,a,b)return self:Distance(self.PlayerUnit,t[a])0 +local HasGroundRadar=HasGround and DetectedItemSet:HasRadar()>0 +local HasAir=DetectedItemSet:HasAirUnits()>0 +local FlightReportType=self:GetFlightReportType() +if(FlightReportType==self.__Enum.ReportType.All)or +(FlightReportType==self.__Enum.ReportType.Airborne and HasAir)or +(FlightReportType==self.__Enum.ReportType.Ground and HasGround)or +(FlightReportType==self.__Enum.ReportType.GroundRadar and HasGroundRadar)then +DetectedTargets=true +local DetectedItemReportMenu=self.Detection:DetectedItemReportMenu(DetectedItem,self.PlayerGroup,_DATABASE:GetPlayerSettings(self.PlayerUnit:GetPlayerName())) +local ReportMenuText=DetectedItemReportMenu:Text(", ") +MENU_GROUP_COMMAND:New(self.PlayerGroup, +ReportMenuText, +self.FlightMenuAttack, +AI_ESCORT._FlightAttackTarget, +self, +DetectedItem +):SetTag("Flight"):SetTime(TimeUpdate) +local DetectedItemReportSummary=self.Detection:DetectedItemReportSummary(DetectedItem,self.PlayerGroup,_DATABASE:GetPlayerSettings(self.PlayerUnit:GetPlayerName())) +local ReportSummary=DetectedItemReportSummary:Text(", ") +DetectedTargetsReport:AddIndent(ReportSummary,"-") +end +end +self.FlightMenuAttack:RemoveSubMenus(TimeUpdate,"Flight") +if DetectedTargets then +EscortGroup:MessageTypeToGroup(DetectedTargetsReport:Text("\n"),MESSAGE.Type.Information,self.PlayerGroup) +end +return true +end +return false +end +AI_ESCORT_REQUEST={ +ClassName="AI_ESCORT_REQUEST", +} +function AI_ESCORT_REQUEST:New(EscortUnit,EscortSpawn,EscortAirbase,EscortName,EscortBriefing) +local EscortGroupSet=SET_GROUP:New():FilterDeads():FilterCrashes() +local self=BASE:Inherit(self,AI_ESCORT:New(EscortUnit,EscortGroupSet,EscortName,EscortBriefing)) +self.EscortGroupSet=EscortGroupSet +self.EscortSpawn=EscortSpawn +self.EscortAirbase=EscortAirbase +self.LeaderGroup=self.PlayerUnit:GetGroup() +self.Detection=DETECTION_AREAS:New(self.EscortGroupSet,5000) +self.Detection:__Start(30) +self.SpawnMode=self.__Enum.Mode.Mission +return self +end +function AI_ESCORT_REQUEST:SpawnEscort() +local EscortGroup=self.EscortSpawn:SpawnAtAirbase(self.EscortAirbase,SPAWN.Takeoff.Hot) +self:ScheduleOnce(0.1, +function(EscortGroup) +EscortGroup:OptionROTVertical() +EscortGroup:OptionROEHoldFire() +self.EscortGroupSet:AddGroup(EscortGroup) +local LeaderEscort=self.EscortGroupSet:GetFirst() +local Report=REPORT:New() +Report:Add("Joining Up "..self.EscortGroupSet:GetUnitTypeNames():Text(", ").." from "..LeaderEscort:GetCoordinate():ToString(self.EscortUnit)) +LeaderEscort:MessageTypeToGroup(Report:Text(),MESSAGE.Type.Information,self.PlayerUnit) +self:SetFlightModeFormation(EscortGroup) +self:FormationTrail() +self:_InitFlightMenus() +self:_InitEscortMenus(EscortGroup) +self:_InitEscortRoute(EscortGroup) +function EscortGroup:OnEventDeadOrCrash(EventData) +self:F({"EventDead",EventData}) +self.EscortMenu:Remove() +end +EscortGroup:HandleEvent(EVENTS.Dead,EscortGroup.OnEventDeadOrCrash) +EscortGroup:HandleEvent(EVENTS.Crash,EscortGroup.OnEventDeadOrCrash) +end,EscortGroup +) +end +function AI_ESCORT_REQUEST:onafterStart(EscortGroupSet) +self:F() +if not self.MenuRequestEscort then +self.MainMenu=MENU_GROUP:New(self.PlayerGroup,self.EscortName) +self.MenuRequestEscort=MENU_GROUP_COMMAND:New(self.LeaderGroup,"Request new escort ",self.MainMenu, +function() +self:SpawnEscort() +end +) +end +self:GetParent(self).onafterStart(self,EscortGroupSet) +self:HandleEvent(EVENTS.Dead,self.OnEventDeadOrCrash) +self:HandleEvent(EVENTS.Crash,self.OnEventDeadOrCrash) +end +function AI_ESCORT_REQUEST:onafterStop(EscortGroupSet) +self:F() +EscortGroupSet:ForEachGroup( +function(EscortGroup) +EscortGroup:WayPointInitialize() +EscortGroup:OptionROTVertical() +EscortGroup:OptionROEOpenFire() +end +) +self.Detection:Stop() +self.MainMenu:Remove() +end +function AI_ESCORT_REQUEST:SetEscortSpawnMission() +self.SpawnMode=self.__Enum.Mode.Mission +end +AI_ESCORT_DISPATCHER={ +ClassName="AI_ESCORT_DISPATCHER", +} +AI_ESCORT_DISPATCHER.AI_Escorts={} +function AI_ESCORT_DISPATCHER:New(CarrierSet,EscortSpawn,EscortAirbase,EscortName,EscortBriefing) +local self=BASE:Inherit(self,FSM:New()) +self.CarrierSet=CarrierSet +self.EscortSpawn=EscortSpawn +self.EscortAirbase=EscortAirbase +self.EscortName=EscortName +self.EscortBriefing=EscortBriefing +self:SetStartState("Idle") +self:AddTransition("Monitoring","Monitor","Monitoring") +self:AddTransition("Idle","Start","Monitoring") +self:AddTransition("Monitoring","Stop","Idle") +function self.CarrierSet.OnAfterRemoved(CarrierSet,From,Event,To,CarrierName,Carrier) +self:F({Carrier=Carrier:GetName()}) +end +return self +end +function AI_ESCORT_DISPATCHER:onafterStart(From,Event,To) +self:HandleEvent(EVENTS.Birth) +self:HandleEvent(EVENTS.PlayerLeaveUnit,self.OnEventExit) +self:HandleEvent(EVENTS.Crash,self.OnEventExit) +self:HandleEvent(EVENTS.Dead,self.OnEventExit) +end +function AI_ESCORT_DISPATCHER:OnEventExit(EventData) +local PlayerGroupName=EventData.IniGroupName +local PlayerGroup=EventData.IniGroup +local PlayerUnit=EventData.IniUnit +self:I({EscortAirbase=self.EscortAirbase}) +self:I({PlayerGroupName=PlayerGroupName}) +self:I({PlayerGroup=PlayerGroup}) +self:I({FirstGroup=self.CarrierSet:GetFirst()}) +self:I({FindGroup=self.CarrierSet:FindGroup(PlayerGroupName)}) +if self.CarrierSet:FindGroup(PlayerGroupName)then +if self.AI_Escorts[PlayerGroupName]then +self.AI_Escorts[PlayerGroupName]:Stop() +self.AI_Escorts[PlayerGroupName]=nil +end +end +end +function AI_ESCORT_DISPATCHER:OnEventBirth(EventData) +local PlayerGroupName=EventData.IniGroupName +local PlayerGroup=EventData.IniGroup +local PlayerUnit=EventData.IniUnit +self:I({EscortAirbase=self.EscortAirbase}) +self:I({PlayerGroupName=PlayerGroupName}) +self:I({PlayerGroup=PlayerGroup}) +self:I({FirstGroup=self.CarrierSet:GetFirst()}) +self:I({FindGroup=self.CarrierSet:FindGroup(PlayerGroupName)}) +if self.CarrierSet:FindGroup(PlayerGroupName)then +if not self.AI_Escorts[PlayerGroupName]then +local LeaderUnit=PlayerUnit +local EscortGroup=self.EscortSpawn:SpawnAtAirbase(self.EscortAirbase,SPAWN.Takeoff.Hot) +self:I({EscortGroup=EscortGroup}) +self:ScheduleOnce(1, +function(EscortGroup) +local EscortSet=SET_GROUP:New() +EscortSet:AddGroup(EscortGroup) +self.AI_Escorts[PlayerGroupName]=AI_ESCORT:New(LeaderUnit,EscortSet,self.EscortName,self.EscortBriefing) +self.AI_Escorts[PlayerGroupName]:FormationTrail(0,100,0) +if EscortGroup:IsHelicopter()then +self.AI_Escorts[PlayerGroupName]:MenusHelicopters() +else +self.AI_Escorts[PlayerGroupName]:MenusAirplanes() +end +self.AI_Escorts[PlayerGroupName]:__Start(0.1) +end,EscortGroup +) +end +end +end +AI_ESCORT_DISPATCHER_REQUEST={ +ClassName="AI_ESCORT_DISPATCHER_REQUEST", +} +AI_ESCORT_DISPATCHER_REQUEST.AI_Escorts={} +function AI_ESCORT_DISPATCHER_REQUEST:New(CarrierSet,EscortSpawn,EscortAirbase,EscortName,EscortBriefing) +local self=BASE:Inherit(self,FSM:New()) +self.CarrierSet=CarrierSet +self.EscortSpawn=EscortSpawn +self.EscortAirbase=EscortAirbase +self.EscortName=EscortName +self.EscortBriefing=EscortBriefing +self:SetStartState("Idle") +self:AddTransition("Monitoring","Monitor","Monitoring") +self:AddTransition("Idle","Start","Monitoring") +self:AddTransition("Monitoring","Stop","Idle") +function self.CarrierSet.OnAfterRemoved(CarrierSet,From,Event,To,CarrierName,Carrier) +self:F({Carrier=Carrier:GetName()}) +end +return self +end +function AI_ESCORT_DISPATCHER_REQUEST:onafterStart(From,Event,To) +self:HandleEvent(EVENTS.Birth) +self:HandleEvent(EVENTS.PlayerLeaveUnit,self.OnEventExit) +self:HandleEvent(EVENTS.Crash,self.OnEventExit) +self:HandleEvent(EVENTS.Dead,self.OnEventExit) +end +function AI_ESCORT_DISPATCHER_REQUEST:OnEventExit(EventData) +local PlayerGroupName=EventData.IniGroupName +local PlayerGroup=EventData.IniGroup +local PlayerUnit=EventData.IniUnit +if self.CarrierSet:FindGroup(PlayerGroupName)then +if self.AI_Escorts[PlayerGroupName]then +self.AI_Escorts[PlayerGroupName]:Stop() +self.AI_Escorts[PlayerGroupName]=nil +end +end +end +function AI_ESCORT_DISPATCHER_REQUEST:OnEventBirth(EventData) +local PlayerGroupName=EventData.IniGroupName +local PlayerGroup=EventData.IniGroup +local PlayerUnit=EventData.IniUnit +if self.CarrierSet:FindGroup(PlayerGroupName)then +if not self.AI_Escorts[PlayerGroupName]then +local LeaderUnit=PlayerUnit +self:ScheduleOnce(0.1, +function() +self.AI_Escorts[PlayerGroupName]=AI_ESCORT_REQUEST:New(LeaderUnit,self.EscortSpawn,self.EscortAirbase,self.EscortName,self.EscortBriefing) +self.AI_Escorts[PlayerGroupName]:FormationTrail(0,100,0) +if PlayerGroup:IsHelicopter()then +self.AI_Escorts[PlayerGroupName]:MenusHelicopters() +else +self.AI_Escorts[PlayerGroupName]:MenusAirplanes() +end +self.AI_Escorts[PlayerGroupName]:__Start(0.1) +end +) +end +end +end +AI_CARGO={ +ClassName="AI_CARGO", +Coordinate=nil, +Carrier_Cargo={}, +} +function AI_CARGO:New(Carrier,CargoSet) +local self=BASE:Inherit(self,FSM_CONTROLLABLE:New(Carrier)) +self.CargoSet=CargoSet +self.CargoCarrier=Carrier +self:SetStartState("Unloaded") +self:AddTransition("Unloaded","Pickup","*") +self:AddTransition("Loaded","Deploy","*") +self:AddTransition("*","Load","Boarding") +self:AddTransition("Boarding","Board","Boarding") +self:AddTransition("Loaded","Board","Loaded") +self:AddTransition("Boarding","Loaded","Boarding") +self:AddTransition("Boarding","PickedUp","Loaded") +self:AddTransition("Loaded","Unload","Unboarding") +self:AddTransition("Unboarding","Unboard","Unboarding") +self:AddTransition("Unboarding","Unloaded","Unboarding") +self:AddTransition("Unboarding","Deployed","Unloaded") +for _,CarrierUnit in pairs(Carrier:GetUnits())do +local CarrierUnit=CarrierUnit +CarrierUnit:SetCargoBayWeightLimit() +end +self.Transporting=false +self.Relocating=false +return self +end +function AI_CARGO:IsTransporting() +return self.Transporting==true +end +function AI_CARGO:IsRelocating() +return self.Relocating==true +end +function AI_CARGO:onafterPickup(APC,From,Event,To,Coordinate,Speed,Height,PickupZone) +self.Transporting=false +self.Relocating=true +end +function AI_CARGO:onafterDeploy(APC,From,Event,To,Coordinate,Speed,Height,DeployZone) +self.Relocating=false +self.Transporting=true +end +function AI_CARGO:onbeforeLoad(Carrier,From,Event,To,PickupZone) +self:F({Carrier,From,Event,To}) +local Boarding=false +local LoadInterval=2 +local LoadDelay=1 +local Carrier_List={} +local Carrier_Weight={} +if Carrier and Carrier:IsAlive()then +self.Carrier_Cargo={} +for _,CarrierUnit in pairs(Carrier:GetUnits())do +local CarrierUnit=CarrierUnit +local CargoBayFreeWeight=CarrierUnit:GetCargoBayFreeWeight() +self:F({CargoBayFreeWeight=CargoBayFreeWeight}) +Carrier_List[#Carrier_List+1]=CarrierUnit +Carrier_Weight[CarrierUnit]=CargoBayFreeWeight +end +local Carrier_Count=#Carrier_List +local Carrier_Index=1 +local Loaded=false +for _,Cargo in UTILS.spairs(self.CargoSet:GetSet(),function(t,a,b)return t[a]:GetWeight()>t[b]:GetWeight()end)do +local Cargo=Cargo +self:F({IsUnLoaded=Cargo:IsUnLoaded(),IsDeployed=Cargo:IsDeployed(),Cargo:GetName(),Carrier:GetName()}) +for Carrier_Loop=1,#Carrier_List do +local CarrierUnit=Carrier_List[Carrier_Index] +Carrier_Index=Carrier_Index+1 +if Carrier_Index>Carrier_Count then +Carrier_Index=1 +end +if Cargo:IsUnLoaded()and not Cargo:IsDeployed()then +if Cargo:IsInLoadRadius(CarrierUnit:GetCoordinate())then +self:F({"In radius",CarrierUnit:GetName()}) +local CargoWeight=Cargo:GetWeight() +local CarrierSpace=Carrier_Weight[CarrierUnit] +if CarrierSpace>CargoWeight then +Carrier:RouteStop() +Cargo:__Board(-LoadDelay,CarrierUnit) +self:__Board(LoadDelay,Cargo,CarrierUnit,PickupZone) +LoadDelay=LoadDelay+Cargo:GetCount()*LoadInterval +self.Carrier_Cargo[Cargo]=CarrierUnit +Boarding=true +Carrier_Weight[CarrierUnit]=Carrier_Weight[CarrierUnit]-CargoWeight +Loaded=true +break +else +self:T(string.format("WARNING: Cargo too heavy for carrier %s. Cargo=%.1f > %.1f free space",tostring(CarrierUnit:GetName()),CargoWeight,CarrierSpace)) +end +end +end +end +end +if not Loaded==true then +self.Relocating=false +end +end +return Boarding +end +function AI_CARGO:onbeforeReload(Carrier,From,Event,To) +self:F({Carrier,From,Event,To}) +local Boarding=false +local LoadInterval=2 +local LoadDelay=1 +local Carrier_List={} +local Carrier_Weight={} +if Carrier and Carrier:IsAlive()then +for _,CarrierUnit in pairs(Carrier:GetUnits())do +local CarrierUnit=CarrierUnit +Carrier_List[#Carrier_List+1]=CarrierUnit +end +local Carrier_Count=#Carrier_List +local Carrier_Index=1 +local Loaded=false +for Cargo,CarrierUnit in pairs(self.Carrier_Cargo)do +local Cargo=Cargo +self:F({IsUnLoaded=Cargo:IsUnLoaded(),IsDeployed=Cargo:IsDeployed(),Cargo:GetName(),Carrier:GetName()}) +for Carrier_Loop=1,#Carrier_List do +local CarrierUnit=Carrier_List[Carrier_Index] +Carrier_Index=Carrier_Index+1 +if Carrier_Index>Carrier_Count then +Carrier_Index=1 +end +if Cargo:IsUnLoaded()and not Cargo:IsDeployed()then +Carrier:RouteStop() +Cargo:__Board(-LoadDelay,CarrierUnit) +self:__Board(LoadDelay,Cargo,CarrierUnit) +LoadDelay=LoadDelay+Cargo:GetCount()*LoadInterval +self.Carrier_Cargo[Cargo]=CarrierUnit +Boarding=true +Loaded=true +end +end +end +if not Loaded==true then +self.Relocating=false +end +end +return Boarding +end +function AI_CARGO:onafterBoard(Carrier,From,Event,To,Cargo,CarrierUnit,PickupZone) +self:F({Carrier,From,Event,To,Cargo,CarrierUnit:GetName()}) +if Carrier and Carrier:IsAlive()and From=="Boarding"then +self:F({IsLoaded=Cargo:IsLoaded(),Cargo:GetName(),Carrier:GetName()}) +if not Cargo:IsLoaded()and not Cargo:IsDestroyed()then +self:__Board(-10,Cargo,CarrierUnit,PickupZone) +return +end +end +self:__Loaded(0.1,Cargo,CarrierUnit,PickupZone) +end +function AI_CARGO:onafterLoaded(Carrier,From,Event,To,Cargo,PickupZone) +self:F({Carrier,From,Event,To}) +local Loaded=true +if Carrier and Carrier:IsAlive()then +for Cargo,CarrierUnit in pairs(self.Carrier_Cargo)do +local Cargo=Cargo +self:F({IsLoaded=Cargo:IsLoaded(),IsDestroyed=Cargo:IsDestroyed(),Cargo:GetName(),Carrier:GetName()}) +if not Cargo:IsLoaded()and not Cargo:IsDestroyed()then +Loaded=false +end +end +end +if Loaded then +self:__PickedUp(0.1,PickupZone) +end +end +function AI_CARGO:onafterPickedUp(Carrier,From,Event,To,PickupZone) +self:F({Carrier,From,Event,To}) +Carrier:RouteResume() +local HasCargo=false +if Carrier and Carrier:IsAlive()then +for Cargo,CarrierUnit in pairs(self.Carrier_Cargo)do +HasCargo=true +break +end +end +self.Relocating=false +if HasCargo then +self:F("Transporting") +self.Transporting=true +end +end +function AI_CARGO:onafterUnload(Carrier,From,Event,To,DeployZone,Defend) +self:F({Carrier,From,Event,To,DeployZone,Defend=Defend}) +local UnboardInterval=5 +local UnboardDelay=5 +if Carrier and Carrier:IsAlive()then +for _,CarrierUnit in pairs(Carrier:GetUnits())do +local CarrierUnit=CarrierUnit +Carrier:RouteStop() +for _,Cargo in pairs(CarrierUnit:GetCargo())do +self:F({Cargo=Cargo:GetName(),Isloaded=Cargo:IsLoaded()}) +if Cargo:IsLoaded()then +Cargo:__UnBoard(UnboardDelay) +UnboardDelay=UnboardDelay+Cargo:GetCount()*UnboardInterval +self:__Unboard(UnboardDelay,Cargo,CarrierUnit,DeployZone,Defend) +if not Defend==true then +Cargo:SetDeployed(true) +end +end +end +end +end +end +function AI_CARGO:onafterUnboard(Carrier,From,Event,To,Cargo,CarrierUnit,DeployZone,Defend) +self:F({Carrier,From,Event,To,Cargo:GetName(),DeployZone=DeployZone,Defend=Defend}) +if Carrier and Carrier:IsAlive()and From=="Unboarding"then +if not Cargo:IsUnLoaded()then +self:__Unboard(10,Cargo,CarrierUnit,DeployZone,Defend) +return +end +end +self:Unloaded(Cargo,CarrierUnit,DeployZone,Defend) +end +function AI_CARGO:onafterUnloaded(Carrier,From,Event,To,Cargo,CarrierUnit,DeployZone,Defend) +self:F({Carrier,From,Event,To,Cargo:GetName(),DeployZone=DeployZone,Defend=Defend}) +local AllUnloaded=true +if Carrier and Carrier:IsAlive()then +for _,CarrierUnit in pairs(Carrier:GetUnits())do +local CarrierUnit=CarrierUnit +local IsEmpty=CarrierUnit:IsCargoEmpty() +self:I({IsEmpty=IsEmpty}) +if not IsEmpty then +AllUnloaded=false +break +end +end +if AllUnloaded==true then +if DeployZone==true then +self.Carrier_Cargo={} +end +self.CargoCarrier=Carrier +end +end +if AllUnloaded==true then +self:__Deployed(5,DeployZone,Defend) +end +end +function AI_CARGO:onafterDeployed(Carrier,From,Event,To,DeployZone,Defend) +self:F({Carrier,From,Event,To,DeployZone=DeployZone,Defend=Defend}) +if not Defend==true then +self.Transporting=false +else +self:F("Defending") +end +end +AI_CARGO_APC={ +ClassName="AI_CARGO_APC", +Coordinate=nil, +} +function AI_CARGO_APC:New(APC,CargoSet,CombatRadius) +local self=BASE:Inherit(self,AI_CARGO:New(APC,CargoSet)) +self:AddTransition("*","Monitor","*") +self:AddTransition("*","Follow","Following") +self:AddTransition("*","Guard","Unloaded") +self:AddTransition("*","Home","*") +self:AddTransition("*","Reload","Boarding") +self:AddTransition("*","Destroyed","Destroyed") +self:SetCombatRadius(CombatRadius) +self:SetCarrier(APC) +return self +end +function AI_CARGO_APC:SetCarrier(CargoCarrier) +self.CargoCarrier=CargoCarrier +self.CargoCarrier:SetState(self.CargoCarrier,"AI_CARGO_APC",self) +CargoCarrier:HandleEvent(EVENTS.Dead) +function CargoCarrier:OnEventDead(EventData) +self:F({"dead"}) +local AICargoTroops=self:GetState(self,"AI_CARGO_APC") +self:F({AICargoTroops=AICargoTroops}) +if AICargoTroops then +self:F({}) +if not AICargoTroops:Is("Loaded")then +AICargoTroops:Destroyed() +end +end +end +self.Zone=ZONE_UNIT:New(self.CargoCarrier:GetName().."-Zone",self.CargoCarrier,self.CombatRadius) +self.Coalition=self.CargoCarrier:GetCoalition() +self:SetControllable(CargoCarrier) +self:Guard() +return self +end +function AI_CARGO_APC:SetOffRoad(Offroad,Formation) +self:SetPickupOffRoad(Offroad,Formation) +self:SetDeployOffRoad(Offroad,Formation) +return self +end +function AI_CARGO_APC:SetPickupOffRoad(Offroad,Formation) +self.pickupOffroad=Offroad +self.pickupFormation=Formation or ENUMS.Formation.Vehicle.OffRoad +return self +end +function AI_CARGO_APC:SetDeployOffRoad(Offroad,Formation) +self.deployOffroad=Offroad +self.deployFormation=Formation or ENUMS.Formation.Vehicle.OffRoad +return self +end +function AI_CARGO_APC:FindCarrier(Coordinate,Radius) +local CoordinateZone=ZONE_RADIUS:New("Zone",Coordinate:GetVec2(),Radius) +CoordinateZone:Scan({Object.Category.UNIT}) +for _,DCSUnit in pairs(CoordinateZone:GetScannedUnits())do +local NearUnit=UNIT:Find(DCSUnit) +self:F({NearUnit=NearUnit}) +if not NearUnit:GetState(NearUnit,"AI_CARGO_APC")then +local Attributes=NearUnit:GetDesc() +self:F({Desc=Attributes}) +if NearUnit:HasAttribute("Trucks")then +return NearUnit:GetGroup() +end +end +end +return nil +end +function AI_CARGO_APC:SetCombatRadius(CombatRadius) +self.CombatRadius=CombatRadius or 0 +if self.CombatRadius>0 then +self:__Monitor(-5) +end +return self +end +function AI_CARGO_APC:FollowToCarrier(Me,APCUnit,CargoGroup) +local InfantryGroup=CargoGroup:GetGroup() +self:F({self=self:GetClassNameAndID(),InfantryGroup=InfantryGroup:GetName()}) +if APCUnit:IsAlive()then +if InfantryGroup:IsPartlyInZone(ZONE_UNIT:New("Radius",APCUnit,25))then +Me:Guard() +else +self:F({InfantryGroup=InfantryGroup:GetName()}) +if InfantryGroup:IsAlive()then +self:F({InfantryGroup=InfantryGroup:GetName()}) +local Waypoints={} +local FromCoord=InfantryGroup:GetCoordinate() +local FromGround=FromCoord:WaypointGround(10,"Diamond") +self:F({FromGround=FromGround}) +table.insert(Waypoints,FromGround) +local ToCoord=APCUnit:GetCoordinate():GetRandomCoordinateInRadius(10,5) +local ToGround=ToCoord:WaypointGround(10,"Diamond") +self:F({ToGround=ToGround}) +table.insert(Waypoints,ToGround) +local TaskRoute=InfantryGroup:TaskFunction("AI_CARGO_APC.FollowToCarrier",Me,APCUnit,CargoGroup) +self:F({Waypoints=Waypoints}) +local Waypoint=Waypoints[#Waypoints] +InfantryGroup:SetTaskWaypoint(Waypoint,TaskRoute) +InfantryGroup:Route(Waypoints,1) +end +end +end +end +function AI_CARGO_APC:onafterMonitor(APC,From,Event,To) +self:F({APC,From,Event,To,IsTransporting=self:IsTransporting()}) +if self.CombatRadius>0 then +if APC and APC:IsAlive()then +if self.CarrierCoordinate then +if self:IsTransporting()==true then +local Coordinate=APC:GetCoordinate() +if self:Is("Unloaded")or self:Is("Loaded")then +self.Zone:Scan({Object.Category.UNIT}) +if self.Zone:IsAllInZoneOfCoalition(self.Coalition)then +if self:Is("Unloaded")then +self:Reload() +end +else +if self:Is("Loaded")then +self:__Unload(1,nil,true) +else +if self:Is("Unloaded")then +end +self:F("I am here"..self:GetCurrentState()) +if self:Is("Following")then +for Cargo,APCUnit in pairs(self.Carrier_Cargo)do +local Cargo=Cargo +local APCUnit=APCUnit +if Cargo:IsAlive()then +if not Cargo:IsNear(APCUnit,40)then +APCUnit:RouteStop() +self.CarrierStopped=true +else +if self.CarrierStopped then +if Cargo:IsNear(APCUnit,25)then +APCUnit:RouteResume() +self.CarrierStopped=nil +end +end +end +end +end +end +end +end +end +end +end +self.CarrierCoordinate=APC:GetCoordinate() +end +self:__Monitor(-5) +end +end +function AI_CARGO_APC:onafterFollow(APC,From,Event,To) +self:F({APC,From,Event,To}) +self:F("Follow") +if APC and APC:IsAlive()then +for Cargo,APCUnit in pairs(self.Carrier_Cargo)do +local Cargo=Cargo +if Cargo:IsUnLoaded()then +self:FollowToCarrier(self,APCUnit,Cargo) +APCUnit:RouteResume() +end +end +end +end +function AI_CARGO_APC._Pickup(APC,self,Coordinate,Speed,PickupZone) +APC:F({"AI_CARGO_APC._Pickup:",APC:GetName()}) +if APC:IsAlive()then +self:Load(PickupZone) +end +end +function AI_CARGO_APC._Deploy(APC,self,Coordinate,DeployZone) +APC:F({"AI_CARGO_APC._Deploy:",APC}) +if APC:IsAlive()then +self:Unload(DeployZone) +end +end +function AI_CARGO_APC:onafterPickup(APC,From,Event,To,Coordinate,Speed,Height,PickupZone) +if APC and APC:IsAlive()then +if Coordinate then +self.RoutePickup=true +local _speed=Speed or APC:GetSpeedMax()*0.5 +local Waypoints={} +if self.pickupOffroad then +Waypoints[1]=APC:GetCoordinate():WaypointGround(Speed,self.pickupFormation) +Waypoints[2]=Coordinate:WaypointGround(_speed,self.pickupFormation,DCSTasks) +else +Waypoints=APC:TaskGroundOnRoad(Coordinate,_speed,ENUMS.Formation.Vehicle.OffRoad,true) +end +local TaskFunction=APC:TaskFunction("AI_CARGO_APC._Pickup",self,Coordinate,Speed,PickupZone) +local Waypoint=Waypoints[#Waypoints] +APC:SetTaskWaypoint(Waypoint,TaskFunction) +APC:Route(Waypoints,1) +else +AI_CARGO_APC._Pickup(APC,self,Coordinate,Speed,PickupZone) +end +self:GetParent(self,AI_CARGO_APC).onafterPickup(self,APC,From,Event,To,Coordinate,Speed,Height,PickupZone) +end +end +function AI_CARGO_APC:onafterDeploy(APC,From,Event,To,Coordinate,Speed,Height,DeployZone) +if APC and APC:IsAlive()then +self.RouteDeploy=true +local speedmax=APC:GetSpeedMax() +local _speed=Speed or speedmax*0.5 +_speed=math.min(_speed,speedmax) +local Waypoints={} +if self.deployOffroad then +Waypoints[1]=APC:GetCoordinate():WaypointGround(Speed,self.deployFormation) +Waypoints[2]=Coordinate:WaypointGround(_speed,self.deployFormation,DCSTasks) +else +Waypoints=APC:TaskGroundOnRoad(Coordinate,_speed,ENUMS.Formation.Vehicle.OffRoad,true) +end +local TaskFunction=APC:TaskFunction("AI_CARGO_APC._Deploy",self,Coordinate,DeployZone) +local Waypoint=Waypoints[#Waypoints] +APC:SetTaskWaypoint(Waypoint,TaskFunction) +APC:Route(Waypoints,1) +self:GetParent(self,AI_CARGO_APC).onafterDeploy(self,APC,From,Event,To,Coordinate,Speed,Height,DeployZone) +end +end +function AI_CARGO_APC:onafterUnloaded(Carrier,From,Event,To,Cargo,CarrierUnit,DeployZone,Defend) +self:F({Carrier,From,Event,To,DeployZone=DeployZone,Defend=Defend}) +self:GetParent(self,AI_CARGO_APC).onafterUnloaded(self,Carrier,From,Event,To,Cargo,CarrierUnit,DeployZone,Defend) +if Defend==true then +self.Zone:Scan({Object.Category.UNIT}) +if not self.Zone:IsAllInZoneOfCoalition(self.Coalition)then +local AttackUnits=self.Zone:GetScannedUnits() +local Move={} +local CargoGroup=Cargo.CargoObject +Move[#Move+1]=CargoGroup:GetCoordinate():WaypointGround(70,"Custom") +for UnitId,AttackUnit in pairs(AttackUnits)do +local MooseUnit=UNIT:Find(AttackUnit) +if MooseUnit:GetCoalition()~=CargoGroup:GetCoalition()then +Move[#Move+1]=MooseUnit:GetCoordinate():WaypointGround(70,"Line abreast") +self:F({MooseUnit=MooseUnit:GetName(),CargoGroup=CargoGroup:GetName()}) +end +end +CargoGroup:RoutePush(Move,0.1) +end +end +end +function AI_CARGO_APC:onafterDeployed(APC,From,Event,To,DeployZone,Defend) +self:F({APC,From,Event,To,DeployZone=DeployZone,Defend=Defend}) +self:__Guard(0.1) +self:GetParent(self,AI_CARGO_APC).onafterDeployed(self,APC,From,Event,To,DeployZone,Defend) +end +function AI_CARGO_APC:onafterHome(APC,From,Event,To,Coordinate,Speed,Height,HomeZone) +if APC and APC:IsAlive()~=nil then +self.RouteHome=true +Speed=Speed or APC:GetSpeedMax()*0.5 +local Waypoints=APC:TaskGroundOnRoad(Coordinate,Speed,"Line abreast",true) +self:F({Waypoints=Waypoints}) +local Waypoint=Waypoints[#Waypoints] +APC:Route(Waypoints,1) +end +end +AI_CARGO_HELICOPTER={ +ClassName="AI_CARGO_HELICOPTER", +Coordinate=nil, +} +AI_CARGO_QUEUE={} +function AI_CARGO_HELICOPTER:New(Helicopter,CargoSet) +local self=BASE:Inherit(self,AI_CARGO:New(Helicopter,CargoSet)) +self.Zone=ZONE_GROUP:New(Helicopter:GetName(),Helicopter,300) +self:SetStartState("Unloaded") +self:AddTransition("Unloaded","Pickup","*") +self:AddTransition("Loaded","Deploy","*") +self:AddTransition("*","Loaded","Loaded") +self:AddTransition("Unboarding","Pickup","Unloaded") +self:AddTransition("Unloaded","Unboard","Unloaded") +self:AddTransition("Unloaded","Unloaded","Unloaded") +self:AddTransition("*","PickedUp","*") +self:AddTransition("*","Landed","*") +self:AddTransition("*","Queue","*") +self:AddTransition("*","Orbit","*") +self:AddTransition("*","Home","*") +self:AddTransition("*","Destroyed","Destroyed") +Helicopter:HandleEvent(EVENTS.Crash, +function(Helicopter,EventData) +AI_CARGO_QUEUE[Helicopter]=nil +end +) +Helicopter:HandleEvent(EVENTS.Land, +function(Helicopter,EventData) +self:ScheduleOnce(60, +function(Helicopter) +AI_CARGO_QUEUE[Helicopter]=nil +end,Helicopter +) +end +) +self:SetCarrier(Helicopter) +return self +end +function AI_CARGO_HELICOPTER:SetCarrier(Helicopter) +local AICargo=self +self.Helicopter=Helicopter +self.Helicopter:SetState(self.Helicopter,"AI_CARGO_HELICOPTER",self) +self.RoutePickup=false +self.RouteDeploy=false +Helicopter:HandleEvent(EVENTS.Dead) +Helicopter:HandleEvent(EVENTS.Hit) +Helicopter:HandleEvent(EVENTS.Land) +function Helicopter:OnEventDead(EventData) +local AICargoTroops=self:GetState(self,"AI_CARGO_HELICOPTER") +self:F({AICargoTroops=AICargoTroops}) +if AICargoTroops then +self:F({}) +if not AICargoTroops:Is("Loaded")then +AICargoTroops:Destroyed() +end +end +end +function Helicopter:OnEventLand(EventData) +AICargo:Landed() +end +self.Coalition=self.Helicopter:GetCoalition() +self:SetControllable(Helicopter) +return self +end +function AI_CARGO_HELICOPTER:onafterLanded(Helicopter,From,Event,To) +self:F({From,Event,To}) +Helicopter:F({Name=Helicopter:GetName()}) +if Helicopter and Helicopter:IsAlive()then +self:F({Helicopter:GetName(),Height=Helicopter:GetHeight(true),Velocity=Helicopter:GetVelocityKMH()}) +if self.RoutePickup==true then +if Helicopter:GetHeight(true)<=5.5 and Helicopter:GetVelocityKMH()<10 then +self:Load(self.PickupZone) +self.RoutePickup=false +end +end +if self.RouteDeploy==true then +if Helicopter:GetHeight(true)<=5.5 and Helicopter:GetVelocityKMH()<10 then +self:Unload(self.DeployZone) +self.RouteDeploy=false +end +end +end +end +function AI_CARGO_HELICOPTER:onafterQueue(Helicopter,From,Event,To,Coordinate,Speed,DeployZone) +self:F({From,Event,To,Coordinate,Speed,DeployZone}) +local HelicopterInZone=false +if Helicopter and Helicopter:IsAlive()==true then +local Distance=Coordinate:DistanceFromPointVec2(Helicopter:GetCoordinate()) +if Distance>2000 then +self:__Queue(-10,Coordinate,Speed,DeployZone) +else +local ZoneFree=true +for Helicopter,ZoneQueue in pairs(AI_CARGO_QUEUE)do +local ZoneQueue=ZoneQueue +if ZoneQueue:IsCoordinateInZone(Coordinate)then +ZoneFree=false +end +end +self:F({ZoneFree=ZoneFree}) +if ZoneFree==true then +local ZoneQueue=ZONE_RADIUS:New(Helicopter:GetName(),Coordinate:GetVec2(),100) +AI_CARGO_QUEUE[Helicopter]=ZoneQueue +local Route={} +local CoordinateTo=Coordinate +local landheight=CoordinateTo:GetLandHeight() +CoordinateTo.y=landheight+50 +local WaypointTo=CoordinateTo:WaypointAir( +"RADIO", +POINT_VEC3.RoutePointType.TurningPoint, +POINT_VEC3.RoutePointAction.TurningPoint, +50, +true +) +Route[#Route+1]=WaypointTo +local Tasks={} +Tasks[#Tasks+1]=Helicopter:TaskLandAtVec2(CoordinateTo:GetVec2()) +Route[#Route].task=Helicopter:TaskCombo(Tasks) +Route[#Route+1]=WaypointTo +Helicopter:Route(Route,0) +self.DeployZone=DeployZone +else +self:__Queue(-10,Coordinate,Speed,DeployZone) +end +end +else +AI_CARGO_QUEUE[Helicopter]=nil +end +end +function AI_CARGO_HELICOPTER:onafterOrbit(Helicopter,From,Event,To,Coordinate) +self:F({From,Event,To,Coordinate}) +if Helicopter and Helicopter:IsAlive()then +local Route={} +local CoordinateTo=Coordinate +local landheight=CoordinateTo:GetLandHeight() +CoordinateTo.y=landheight+50 +local WaypointTo=CoordinateTo:WaypointAir("RADIO",POINT_VEC3.RoutePointType.TurningPoint,POINT_VEC3.RoutePointAction.TurningPoint,50,true) +Route[#Route+1]=WaypointTo +local Tasks={} +Tasks[#Tasks+1]=Helicopter:TaskOrbitCircle(math.random(30,80),150,CoordinateTo:GetRandomCoordinateInRadius(800,500)) +Route[#Route].task=Helicopter:TaskCombo(Tasks) +Route[#Route+1]=WaypointTo +Helicopter:Route(Route,0) +end +end +function AI_CARGO_HELICOPTER:onafterDeployed(Helicopter,From,Event,To,DeployZone) +self:F({From,Event,To,DeployZone=DeployZone}) +self:Orbit(Helicopter:GetCoordinate(),50) +self:ScheduleOnce(30, +function(Helicopter) +AI_CARGO_QUEUE[Helicopter]=nil +end,Helicopter +) +self:GetParent(self,AI_CARGO_HELICOPTER).onafterDeployed(self,Helicopter,From,Event,To,DeployZone) +end +function AI_CARGO_HELICOPTER:onafterPickup(Helicopter,From,Event,To,Coordinate,Speed,Height,PickupZone) +self:F({Coordinate,Speed,Height,PickupZone}) +if Helicopter and Helicopter:IsAlive()~=nil then +Helicopter:Activate() +self.RoutePickup=true +Coordinate.y=Height +local _speed=Speed or Helicopter:GetSpeedMax()*0.5 +local Route={} +local CoordinateFrom=Helicopter:GetCoordinate() +local WaypointFrom=CoordinateFrom:WaypointAir("RADIO",POINT_VEC3.RoutePointType.TurningPoint,POINT_VEC3.RoutePointAction.TurningPoint,_speed,true) +local CoordinateTo=Coordinate +local landheight=CoordinateTo:GetLandHeight() +CoordinateTo.y=landheight+50 +local WaypointTo=CoordinateTo:WaypointAir("RADIO",POINT_VEC3.RoutePointType.TurningPoint,POINT_VEC3.RoutePointAction.TurningPoint,_speed,true) +Route[#Route+1]=WaypointFrom +Route[#Route+1]=WaypointTo +Helicopter:WayPointInitialize(Route) +local Tasks={} +Tasks[#Tasks+1]=Helicopter:TaskLandAtVec2(CoordinateTo:GetVec2()) +Route[#Route].task=Helicopter:TaskCombo(Tasks) +Route[#Route+1]=WaypointTo +Helicopter:Route(Route,1) +self.PickupZone=PickupZone +self:GetParent(self,AI_CARGO_HELICOPTER).onafterPickup(self,Helicopter,From,Event,To,Coordinate,Speed,Height,PickupZone) +end +end +function AI_CARGO_HELICOPTER:_Deploy(AICargoHelicopter,Coordinate,DeployZone) +AICargoHelicopter:__Queue(-10,Coordinate,100,DeployZone) +end +function AI_CARGO_HELICOPTER:onafterDeploy(Helicopter,From,Event,To,Coordinate,Speed,Height,DeployZone) +self:F({From,Event,To,Coordinate,Speed,Height,DeployZone}) +if Helicopter and Helicopter:IsAlive()~=nil then +self.RouteDeploy=true +local Route={} +Coordinate.y=Height +local _speed=Speed or Helicopter:GetSpeedMax()*0.5 +local CoordinateFrom=Helicopter:GetCoordinate() +local WaypointFrom=CoordinateFrom:WaypointAir("RADIO",POINT_VEC3.RoutePointType.TurningPoint,POINT_VEC3.RoutePointAction.TurningPoint,_speed,true) +Route[#Route+1]=WaypointFrom +Route[#Route+1]=WaypointFrom +local CoordinateTo=Coordinate +local landheight=CoordinateTo:GetLandHeight() +CoordinateTo.y=landheight+50 +local WaypointTo=CoordinateTo:WaypointAir("RADIO",POINT_VEC3.RoutePointType.TurningPoint,POINT_VEC3.RoutePointAction.TurningPoint,_speed,true) +Route[#Route+1]=WaypointTo +Route[#Route+1]=WaypointTo +Helicopter:WayPointInitialize(Route) +local Tasks={} +Tasks[#Tasks+1]=Helicopter:TaskFunction("AI_CARGO_HELICOPTER._Deploy",self,Coordinate,DeployZone) +Tasks[#Tasks+1]=Helicopter:TaskOrbitCircle(math.random(30,100),_speed,CoordinateTo:GetRandomCoordinateInRadius(800,500)) +Route[#Route].task=Helicopter:TaskCombo(Tasks) +Route[#Route+1]=WaypointTo +Helicopter:Route(Route,0) +self:GetParent(self,AI_CARGO_HELICOPTER).onafterDeploy(self,Helicopter,From,Event,To,Coordinate,Speed,Height,DeployZone) +end +end +function AI_CARGO_HELICOPTER:onafterHome(Helicopter,From,Event,To,Coordinate,Speed,Height,HomeZone) +self:F({From,Event,To,Coordinate,Speed,Height}) +if Helicopter and Helicopter:IsAlive()~=nil then +self.RouteHome=true +local Route={} +Height=Height or 50 +Speed=Speed or Helicopter:GetSpeedMax()*0.5 +local CoordinateFrom=Helicopter:GetCoordinate() +local WaypointFrom=CoordinateFrom:WaypointAir("RADIO",POINT_VEC3.RoutePointType.TurningPoint,POINT_VEC3.RoutePointAction.TurningPoint,Speed,true) +Route[#Route+1]=WaypointFrom +local CoordinateTo=Coordinate +local landheight=CoordinateTo:GetLandHeight() +CoordinateTo.y=landheight+Height +local WaypointTo=CoordinateTo:WaypointAir("RADIO",POINT_VEC3.RoutePointType.TurningPoint,POINT_VEC3.RoutePointAction.TurningPoint,Speed,true) +Route[#Route+1]=WaypointTo +Helicopter:WayPointInitialize(Route) +local Tasks={} +Tasks[#Tasks+1]=Helicopter:TaskLandAtVec2(CoordinateTo:GetVec2()) +Route[#Route].task=Helicopter:TaskCombo(Tasks) +Route[#Route+1]=WaypointTo +Helicopter:Route(Route,0) +end +end +AI_CARGO_AIRPLANE={ +ClassName="AI_CARGO_AIRPLANE", +Coordinate=nil, +} +function AI_CARGO_AIRPLANE:New(Airplane,CargoSet) +local self=BASE:Inherit(self,AI_CARGO:New(Airplane,CargoSet)) +self:AddTransition("*","Landed","*") +self:AddTransition("*","Home","*") +self:AddTransition("*","Destroyed","Destroyed") +self:SetCarrier(Airplane) +return self +end +function AI_CARGO_AIRPLANE:SetCarrier(Airplane) +local AICargo=self +self.Airplane=Airplane +self.Airplane:SetState(self.Airplane,"AI_CARGO_AIRPLANE",self) +self.RoutePickup=false +self.RouteDeploy=false +Airplane:HandleEvent(EVENTS.Dead) +Airplane:HandleEvent(EVENTS.Hit) +Airplane:HandleEvent(EVENTS.EngineShutdown) +function Airplane:OnEventDead(EventData) +local AICargoTroops=self:GetState(self,"AI_CARGO_AIRPLANE") +self:F({AICargoTroops=AICargoTroops}) +if AICargoTroops then +self:F({}) +if not AICargoTroops:Is("Loaded")then +AICargoTroops:Destroyed() +end +end +end +function Airplane:OnEventHit(EventData) +local AICargoTroops=self:GetState(self,"AI_CARGO_AIRPLANE") +if AICargoTroops then +self:F({OnHitLoaded=AICargoTroops:Is("Loaded")}) +if AICargoTroops:Is("Loaded")or AICargoTroops:Is("Boarding")then +AICargoTroops:Unload() +end +end +end +function Airplane:OnEventEngineShutdown(EventData) +AICargo.Relocating=false +AICargo:Landed(self.Airplane) +end +self.Coalition=self.Airplane:GetCoalition() +self:SetControllable(Airplane) +return self +end +function AI_CARGO_AIRPLANE:FindCarrier(Coordinate,Radius) +local CoordinateZone=ZONE_RADIUS:New("Zone",Coordinate:GetVec2(),Radius) +CoordinateZone:Scan({Object.Category.UNIT}) +for _,DCSUnit in pairs(CoordinateZone:GetScannedUnits())do +local NearUnit=UNIT:Find(DCSUnit) +self:F({NearUnit=NearUnit}) +if not NearUnit:GetState(NearUnit,"AI_CARGO_AIRPLANE")then +local Attributes=NearUnit:GetDesc() +self:F({Desc=Attributes}) +if NearUnit:HasAttribute("Trucks")then +self:SetCarrier(NearUnit) +break +end +end +end +end +function AI_CARGO_AIRPLANE:onafterLanded(Airplane,From,Event,To) +self:F({Airplane,From,Event,To}) +if Airplane and Airplane:IsAlive()~=nil then +if self.RoutePickup==true then +self:Load(self.PickupZone) +end +if self.RouteDeploy==true then +self:Unload() +self.RouteDeploy=false +end +end +end +function AI_CARGO_AIRPLANE:onafterPickup(Airplane,From,Event,To,Coordinate,Speed,Height,PickupZone) +if Airplane and Airplane:IsAlive()then +local airbasepickup=Coordinate:GetClosestAirbase() +self.PickupZone=PickupZone or ZONE_AIRBASE:New(airbasepickup:GetName()) +local ClosestAirbase,DistToAirbase=Airplane:GetCoordinate():GetClosestAirbase() +if Airplane:InAir()then +self.Airbase=nil +else +self.Airbase=ClosestAirbase +end +local Airbase=self.PickupZone:GetAirbase() +local Dist=Airbase:GetCoordinate():Get2DDistance(ClosestAirbase:GetCoordinate()) +if Airplane:InAir()or Dist>500 then +self:Route(Airplane,Airbase,Speed,Height) +self.Airbase=Airbase +self.RoutePickup=true +else +self.RoutePickup=true +self:Landed() +end +self:GetParent(self,AI_CARGO_AIRPLANE).onafterPickup(self,Airplane,From,Event,To,Coordinate,Speed,Height,self.PickupZone) +end +end +function AI_CARGO_AIRPLANE:onafterDeploy(Airplane,From,Event,To,Coordinate,Speed,Height,DeployZone) +if Airplane and Airplane:IsAlive()~=nil then +local Airbase=Coordinate:GetClosestAirbase() +if DeployZone then +Airbase=DeployZone:GetAirbase() +end +if Airplane:IsAlive()==false then +Airplane:SetCommand({id='Start',params={}}) +end +self:Route(Airplane,Airbase,Speed,Height) +self.RouteDeploy=true +self.Airbase=Airbase +self:GetParent(self,AI_CARGO_AIRPLANE).onafterDeploy(self,Airplane,From,Event,To,Coordinate,Speed,Height,DeployZone) +end +end +function AI_CARGO_AIRPLANE:onafterUnload(Airplane,From,Event,To,DeployZone) +local UnboardInterval=10 +local UnboardDelay=10 +if Airplane and Airplane:IsAlive()then +for _,AirplaneUnit in pairs(Airplane:GetUnits())do +local Cargos=AirplaneUnit:GetCargo() +for CargoID,Cargo in pairs(Cargos)do +local Angle=180 +local CargoCarrierHeading=Airplane:GetHeading() +local CargoDeployHeading=((CargoCarrierHeading+Angle)>=360)and(CargoCarrierHeading+Angle-360)or(CargoCarrierHeading+Angle) +self:T({CargoCarrierHeading,CargoDeployHeading}) +local CargoDeployCoordinate=Airplane:GetPointVec2():Translate(150,CargoDeployHeading) +Cargo:__UnBoard(UnboardDelay,CargoDeployCoordinate) +UnboardDelay=UnboardDelay+UnboardInterval +Cargo:SetDeployed(true) +self:__Unboard(UnboardDelay,Cargo,AirplaneUnit,DeployZone) +end +end +end +end +function AI_CARGO_AIRPLANE:Route(Airplane,Airbase,Speed,Height,Uncontrolled) +if Airplane and Airplane:IsAlive()then +local Takeoff=SPAWN.Takeoff.Cold +local Template=Airplane:GetTemplate() +if Template==nil then +return +end +local Points={} +local AirbasePointVec2=Airbase:GetPointVec2() +local ToWaypoint=AirbasePointVec2:WaypointAir(POINT_VEC3.RoutePointAltType.BARO,"Land","Landing",Speed or Airplane:GetSpeedMax()*0.8,true,Airbase) +if self.Airbase then +Template.route.points[2]=ToWaypoint +Airplane:RespawnAtCurrentAirbase(Template,Takeoff,Uncontrolled) +else +local GroupPoint=Airplane:GetVec2() +local FromWaypoint={} +FromWaypoint.x=GroupPoint.x +FromWaypoint.y=GroupPoint.y +FromWaypoint.type="Turning Point" +FromWaypoint.action="Turning Point" +FromWaypoint.speed=Airplane:GetSpeedMax()*0.8 +Points[1]=FromWaypoint +Points[2]=ToWaypoint +local PointVec3=Airplane:GetPointVec3() +Template.x=PointVec3.x +Template.y=PointVec3.z +Template.route.points=Points +local GroupSpawned=Airplane:Respawn(Template) +end +end +end +function AI_CARGO_AIRPLANE:onafterHome(Airplane,From,Event,To,Coordinate,Speed,Height,HomeZone) +if Airplane and Airplane:IsAlive()then +self.RouteHome=true +local HomeBase=HomeZone:GetAirbase() +self.Airbase=HomeBase +self:Route(Airplane,HomeBase,Speed,Height) +end +end +AI_CARGO_SHIP={ +ClassName="AI_CARGO_SHIP", +Coordinate=nil +} +function AI_CARGO_SHIP:New(Ship,CargoSet,CombatRadius,ShippingLane) +local self=BASE:Inherit(self,AI_CARGO:New(Ship,CargoSet)) +self:AddTransition("*","Monitor","*") +self:AddTransition("*","Destroyed","Destroyed") +self:AddTransition("*","Home","*") +self:SetCombatRadius(0) +self:SetShippingLane(ShippingLane) +self:SetCarrier(Ship) +return self +end +function AI_CARGO_SHIP:SetCarrier(CargoCarrier) +self.CargoCarrier=CargoCarrier +self.CargoCarrier:SetState(self.CargoCarrier,"AI_CARGO_SHIP",self) +CargoCarrier:HandleEvent(EVENTS.Dead) +function CargoCarrier:OnEventDead(EventData) +self:F({"dead"}) +local AICargoTroops=self:GetState(self,"AI_CARGO_SHIP") +self:F({AICargoTroops=AICargoTroops}) +if AICargoTroops then +self:F({}) +if not AICargoTroops:Is("Loaded")then +AICargoTroops:Destroyed() +end +end +end +self.Zone=ZONE_UNIT:New(self.CargoCarrier:GetName().."-Zone",self.CargoCarrier,self.CombatRadius) +self.Coalition=self.CargoCarrier:GetCoalition() +self:SetControllable(CargoCarrier) +return self +end +function AI_CARGO_SHIP:FindCarrier(Coordinate,Radius) +local CoordinateZone=ZONE_RADIUS:New("Zone",Coordinate:GetVec2(),Radius) +CoordinateZone:Scan({Object.Category.UNIT}) +for _,DCSUnit in pairs(CoordinateZone:GetScannedUnits())do +local NearUnit=UNIT:Find(DCSUnit) +self:F({NearUnit=NearUnit}) +if not NearUnit:GetState(NearUnit,"AI_CARGO_SHIP")then +local Attributes=NearUnit:GetDesc() +self:F({Desc=Attributes}) +if NearUnit:HasAttributes("Trucks")then +return NearUnit:GetGroup() +end +end +end +return nil +end +function AI_CARGO_SHIP:SetShippingLane(ShippingLane) +self.ShippingLane=ShippingLane +return self +end +function AI_CARGO_SHIP:SetCombatRadius(CombatRadius) +self.CombatRadius=CombatRadius or 0 +return self +end +function AI_CARGO_SHIP:FollowToCarrier(Me,ShipUnit,CargoGroup) +local InfantryGroup=CargoGroup:GetGroup() +self:F({self=self:GetClassNameAndID(),InfantryGroup=InfantryGroup:GetName()}) +if ShipUnit:IsAlive()then +if InfantryGroup:IsPartlyInZone(ZONE_UNIT:New("Radius",ShipUnit,1000))then +Me:Guard() +else +self:F({InfantryGroup=InfantryGroup:GetName()}) +if InfantryGroup:IsAlive()then +self:F({InfantryGroup=InfantryGroup:GetName()}) +local Waypoints={} +local FromCoord=InfantryGroup:GetCoordinate() +local FromGround=FromCoord:WaypointGround(10,"Diamond") +self:F({FromGround=FromGround}) +table.insert(Waypoints,FromGround) +local ToCoord=ShipUnit:GetCoordinate():GetRandomCoordinateInRadius(10,5) +local ToGround=ToCoord:WaypointGround(10,"Diamond") +self:F({ToGround=ToGround}) +table.insert(Waypoints,ToGround) +local TaskRoute=InfantryGroup:TaskFunction("AI_CARGO_SHIP.FollowToCarrier",Me,ShipUnit,CargoGroup) +self:F({Waypoints=Waypoints}) +local Waypoint=Waypoints[#Waypoints] +InfantryGroup:SetTaskWaypoint(Waypoint,TaskRoute) +InfantryGroup:Route(Waypoints,1) +end +end +end +end +function AI_CARGO_SHIP:onafterMonitor(Ship,From,Event,To) +self:F({Ship,From,Event,To,IsTransporting=self:IsTransporting()}) +if self.CombatRadius>0 then +if Ship and Ship:IsAlive()then +if self.CarrierCoordinate then +if self:IsTransporting()==true then +local Coordinate=Ship:GetCoordinate() +if self:Is("Unloaded")or self:Is("Loaded")then +self.Zone:Scan({Object.Category.UNIT}) +if self.Zone:IsAllInZoneOfCoalition(self.Coalition)then +if self:Is("Unloaded")then +self:Reload() +end +else +if self:Is("Loaded")then +self:__Unload(1,nil,true) +else +if self:Is("Unloaded")then +end +self:F("I am here"..self:GetCurrentState()) +if self:Is("Following")then +for Cargo,ShipUnit in pairs(self.Carrier_Cargo)do +local Cargo=Cargo +local ShipUnit=ShipUnit +if Cargo:IsAlive()then +if not Cargo:IsNear(ShipUnit,40)then +ShipUnit:RouteStop() +self.CarrierStopped=true +else +if self.CarrierStopped then +if Cargo:IsNear(ShipUnit,25)then +ShipUnit:RouteResume() +self.CarrierStopped=nil +end +end +end +end +end +end +end +end +end +end +end +self.CarrierCoordinate=Ship:GetCoordinate() +end +self:__Monitor(-5) +end +end +function AI_CARGO_SHIP._Pickup(Ship,self,Coordinate,Speed,PickupZone) +Ship:F({"AI_CARGO_Ship._Pickup:",Ship:GetName()}) +if Ship:IsAlive()then +self:Load(PickupZone) +end +end +function AI_CARGO_SHIP._Deploy(Ship,self,Coordinate,DeployZone) +Ship:F({"AI_CARGO_Ship._Deploy:",Ship}) +if Ship:IsAlive()then +self:Unload(DeployZone) +end +end +function AI_CARGO_SHIP:onafterPickup(Ship,From,Event,To,Coordinate,Speed,Height,PickupZone) +if Ship and Ship:IsAlive()then +AI_CARGO_SHIP._Pickup(Ship,self,Coordinate,Speed,PickupZone) +self:GetParent(self,AI_CARGO_SHIP).onafterPickup(self,Ship,From,Event,To,Coordinate,Speed,Height,PickupZone) +end +end +function AI_CARGO_SHIP:onafterDeploy(Ship,From,Event,To,Coordinate,Speed,Height,DeployZone) +if Ship and Ship:IsAlive()then +Speed=Speed or Ship:GetSpeedMax()*0.8 +local lane=self.ShippingLane +if lane then +local Waypoints={} +for i=1,#lane do +local coord=lane[i] +local Waypoint=coord:WaypointGround(_speed) +table.insert(Waypoints,Waypoint) +end +local TaskFunction=Ship:TaskFunction("AI_CARGO_SHIP._Deploy",self,Coordinate,DeployZone) +local Waypoint=Waypoints[#Waypoints] +Ship:SetTaskWaypoint(Waypoint,TaskFunction) +Ship:Route(Waypoints,1) +self:GetParent(self,AI_CARGO_SHIP).onafterDeploy(self,Ship,From,Event,To,Coordinate,Speed,Height,DeployZone) +else +self:E(self.lid.."ERROR: No shipping lane defined for Naval Transport!") +end +end +end +function AI_CARGO_SHIP:onafterUnload(Ship,From,Event,To,DeployZone,Defend) +self:F({Ship,From,Event,To,DeployZone,Defend=Defend}) +local UnboardInterval=5 +local UnboardDelay=5 +if Ship and Ship:IsAlive()then +for _,ShipUnit in pairs(Ship:GetUnits())do +local ShipUnit=ShipUnit +Ship:RouteStop() +for _,Cargo in pairs(ShipUnit:GetCargo())do +self:F({Cargo=Cargo:GetName(),Isloaded=Cargo:IsLoaded()}) +if Cargo:IsLoaded()then +local unboardCoord=DeployZone:GetRandomPointVec2() +Cargo:__UnBoard(UnboardDelay,unboardCoord,1000) +UnboardDelay=UnboardDelay+Cargo:GetCount()*UnboardInterval +self:__Unboard(UnboardDelay,Cargo,ShipUnit,DeployZone,Defend) +if not Defend==true then +Cargo:SetDeployed(true) +end +end +end +end +end +end +function AI_CARGO_SHIP:onafterHome(Ship,From,Event,To,Coordinate,Speed,Height,HomeZone) +if Ship and Ship:IsAlive()then +self.RouteHome=true +Speed=Speed or Ship:GetSpeedMax()*0.8 +local lane=self.ShippingLane +if lane then +local Waypoints={} +for i=#lane,1,-1 do +local coord=lane[i] +local Waypoint=coord:WaypointGround(_speed) +table.insert(Waypoints,Waypoint) +end +local Waypoint=Waypoints[#Waypoints] +Ship:Route(Waypoints,1) +else +self:E(self.lid.."ERROR: No shipping lane defined for Naval Transport!") +end +end +end +AI_CARGO_DISPATCHER={ +ClassName="AI_CARGO_DISPATCHER", +AI_Cargo={}, +PickupCargo={} +} +AI_CARGO_DISPATCHER.AI_Cargo={} +AI_CARGO_DISPATCHER.PickupCargo={} +function AI_CARGO_DISPATCHER:New(CarrierSet,CargoSet,PickupZoneSet,DeployZoneSet) +local self=BASE:Inherit(self,FSM:New()) +self.SetCarrier=CarrierSet +self.SetCargo=CargoSet +self.PickupZoneSet=PickupZoneSet +self.DeployZoneSet=DeployZoneSet +self:SetStartState("Idle") +self:AddTransition("Monitoring","Monitor","Monitoring") +self:AddTransition("Idle","Start","Monitoring") +self:AddTransition("Monitoring","Stop","Idle") +self:AddTransition("Monitoring","Pickup","Monitoring") +self:AddTransition("Monitoring","Load","Monitoring") +self:AddTransition("Monitoring","Loading","Monitoring") +self:AddTransition("Monitoring","Loaded","Monitoring") +self:AddTransition("Monitoring","PickedUp","Monitoring") +self:AddTransition("Monitoring","Transport","Monitoring") +self:AddTransition("Monitoring","Deploy","Monitoring") +self:AddTransition("Monitoring","Unload","Monitoring") +self:AddTransition("Monitoring","Unloading","Monitoring") +self:AddTransition("Monitoring","Unloaded","Monitoring") +self:AddTransition("Monitoring","Deployed","Monitoring") +self:AddTransition("Monitoring","Home","Monitoring") +self:SetMonitorTimeInterval(30) +self:SetDeployRadius(500,200) +self.PickupCargo={} +self.CarrierHome={} +function self.SetCarrier.OnAfterRemoved(SetCarrier,From,Event,To,CarrierName,Carrier) +self:F({Carrier=Carrier:GetName()}) +self.PickupCargo[Carrier]=nil +self.CarrierHome[Carrier]=nil +end +return self +end +function AI_CARGO_DISPATCHER:SetMonitorTimeInterval(MonitorTimeInterval) +self.MonitorTimeInterval=MonitorTimeInterval +return self +end +function AI_CARGO_DISPATCHER:SetHomeZone(HomeZone) +self.HomeZone=HomeZone +return self +end +function AI_CARGO_DISPATCHER:SetPickupRadius(OuterRadius,InnerRadius) +OuterRadius=OuterRadius or 0 +InnerRadius=InnerRadius or OuterRadius +self.PickupOuterRadius=OuterRadius +self.PickupInnerRadius=InnerRadius +return self +end +function AI_CARGO_DISPATCHER:SetPickupSpeed(MaxSpeed,MinSpeed) +MaxSpeed=MaxSpeed or 999 +MinSpeed=MinSpeed or MaxSpeed +self.PickupMinSpeed=MinSpeed +self.PickupMaxSpeed=MaxSpeed +return self +end +function AI_CARGO_DISPATCHER:SetDeployRadius(OuterRadius,InnerRadius) +OuterRadius=OuterRadius or 0 +InnerRadius=InnerRadius or OuterRadius +self.DeployOuterRadius=OuterRadius +self.DeployInnerRadius=InnerRadius +return self +end +function AI_CARGO_DISPATCHER:SetDeploySpeed(MaxSpeed,MinSpeed) +MaxSpeed=MaxSpeed or 999 +MinSpeed=MinSpeed or MaxSpeed +self.DeployMinSpeed=MinSpeed +self.DeployMaxSpeed=MaxSpeed +return self +end +function AI_CARGO_DISPATCHER:SetPickupHeight(MaxHeight,MinHeight) +MaxHeight=MaxHeight or 200 +MinHeight=MinHeight or MaxHeight +self.PickupMinHeight=MinHeight +self.PickupMaxHeight=MaxHeight +return self +end +function AI_CARGO_DISPATCHER:SetDeployHeight(MaxHeight,MinHeight) +MaxHeight=MaxHeight or 200 +MinHeight=MinHeight or MaxHeight +self.DeployMinHeight=MinHeight +self.DeployMaxHeight=MaxHeight +return self +end +function AI_CARGO_DISPATCHER:onafterMonitor() +self:F("Carriers") +self.SetCarrier:Flush() +for CarrierGroupName,Carrier in pairs(self.SetCarrier:GetSet())do +local Carrier=Carrier +if Carrier:IsAlive()~=nil then +local AI_Cargo=self.AI_Cargo[Carrier] +if not AI_Cargo then +self.AI_Cargo[Carrier]=self:AICargo(Carrier,self.SetCargo,self.CombatRadius) +AI_Cargo=self.AI_Cargo[Carrier] +function AI_Cargo.OnAfterPickup(AI_Cargo,CarrierGroup,From,Event,To,Coordinate,Speed,Height,PickupZone) +self:Pickup(CarrierGroup,Coordinate,Speed,Height,PickupZone) +end +function AI_Cargo.OnAfterLoad(AI_Cargo,CarrierGroup,From,Event,To,PickupZone) +self:Load(CarrierGroup,PickupZone) +end +function AI_Cargo.OnAfterBoard(AI_Cargo,CarrierGroup,From,Event,To,Cargo,CarrierUnit,PickupZone) +self:Loading(CarrierGroup,Cargo,CarrierUnit,PickupZone) +end +function AI_Cargo.OnAfterLoaded(AI_Cargo,CarrierGroup,From,Event,To,Cargo,CarrierUnit,PickupZone) +self:Loaded(CarrierGroup,Cargo,CarrierUnit,PickupZone) +end +function AI_Cargo.OnAfterPickedUp(AI_Cargo,CarrierGroup,From,Event,To,PickupZone) +self:PickedUp(CarrierGroup,PickupZone) +self:Transport(CarrierGroup) +end +function AI_Cargo.OnAfterDeploy(AI_Cargo,CarrierGroup,From,Event,To,Coordinate,Speed,Height,DeployZone) +self:Deploy(CarrierGroup,Coordinate,Speed,Height,DeployZone) +end +function AI_Cargo.OnAfterUnload(AI_Cargo,Carrier,From,Event,To,Cargo,CarrierUnit,DeployZone) +self:Unloading(Carrier,Cargo,CarrierUnit,DeployZone) +end +function AI_Cargo.OnAfterUnboard(AI_Cargo,CarrierGroup,From,Event,To,Cargo,CarrierUnit,DeployZone) +self:Unloading(CarrierGroup,Cargo,CarrierUnit,DeployZone) +end +function AI_Cargo.OnAfterUnloaded(AI_Cargo,Carrier,From,Event,To,Cargo,CarrierUnit,DeployZone) +self:Unloaded(Carrier,Cargo,CarrierUnit,DeployZone) +end +function AI_Cargo.OnAfterDeployed(AI_Cargo,Carrier,From,Event,To,DeployZone) +self:Deployed(Carrier,DeployZone) +end +function AI_Cargo.OnAfterHome(AI_Cargo,Carrier,From,Event,To,Coordinate,Speed,Height,HomeZone) +self:Home(Carrier,Coordinate,Speed,Height,HomeZone) +end +end +self:T({Carrier=CarrierGroupName,IsRelocating=AI_Cargo:IsRelocating(),IsTransporting=AI_Cargo:IsTransporting()}) +if AI_Cargo:IsRelocating()==false and AI_Cargo:IsTransporting()==false then +local PickupCargo=nil +local PickupZone=nil +self.SetCargo:Flush() +for CargoName,Cargo in UTILS.spairs(self.SetCargo:GetSet(),function(t,a,b)return t[a]:GetWeight()=Cargo:GetWeight()then +self.PickupCargo[Carrier]=CargoCoordinate +PickupCargo=Cargo +break +else +local text=string.format("WARNING: Cargo %s is too heavy to be loaded into transport. Cargo weight %.1f > %.1f load capacity of carrier %s.", +tostring(Cargo:GetName()),Cargo:GetWeight(),LargestLoadCapacity,tostring(Carrier:GetName())) +self:I(text) +end +end +end +end +end +if PickupCargo then +self.CarrierHome[Carrier]=nil +local PickupCoordinate=PickupCargo:GetCoordinate():GetRandomCoordinateInRadius(self.PickupOuterRadius,self.PickupInnerRadius) +AI_Cargo:Pickup(PickupCoordinate,math.random(self.PickupMinSpeed,self.PickupMaxSpeed),math.random(self.PickupMinHeight,self.PickupMaxHeight),PickupZone) +break +else +if self.HomeZone then +if not self.CarrierHome[Carrier]then +self.CarrierHome[Carrier]=true +AI_Cargo:Home(self.HomeZone:GetRandomPointVec2(),math.random(self.PickupMinSpeed,self.PickupMaxSpeed),math.random(self.PickupMinHeight,self.PickupMaxHeight),self.HomeZone) +end +end +end +end +end +end +self:__Monitor(self.MonitorTimeInterval) +end +function AI_CARGO_DISPATCHER:onafterStart(From,Event,To) +self:__Monitor(-1) +end +function AI_CARGO_DISPATCHER:onafterTransport(From,Event,To,Carrier,Cargo) +if self.DeployZoneSet then +if self.AI_Cargo[Carrier]:IsTransporting()==true then +local DeployZone=self.DeployZoneSet:GetRandomZone() +local DeployCoordinate=DeployZone:GetCoordinate():GetRandomCoordinateInRadius(self.DeployOuterRadius,self.DeployInnerRadius) +self.AI_Cargo[Carrier]:__Deploy(0.1,DeployCoordinate,math.random(self.DeployMinSpeed,self.DeployMaxSpeed),math.random(self.DeployMinHeight,self.DeployMaxHeight),DeployZone) +end +end +self:F({Carrier=Carrier:GetName(),PickupCargo=self.PickupCargo}) +self.PickupCargo[Carrier]=nil +end +AI_CARGO_DISPATCHER_APC={ +ClassName="AI_CARGO_DISPATCHER_APC", +} +function AI_CARGO_DISPATCHER_APC:New(APCSet,CargoSet,PickupZoneSet,DeployZoneSet,CombatRadius) +local self=BASE:Inherit(self,AI_CARGO_DISPATCHER:New(APCSet,CargoSet,PickupZoneSet,DeployZoneSet)) +self:SetDeploySpeed(120,70) +self:SetPickupSpeed(120,70) +self:SetPickupRadius(0,0) +self:SetDeployRadius(0,0) +self:SetPickupHeight() +self:SetDeployHeight() +self:SetCombatRadius(CombatRadius) +return self +end +function AI_CARGO_DISPATCHER_APC:AICargo(APC,CargoSet) +local aicargoapc=AI_CARGO_APC:New(APC,CargoSet,self.CombatRadius) +aicargoapc:SetDeployOffRoad(self.deployOffroad,self.deployFormation) +aicargoapc:SetPickupOffRoad(self.pickupOffroad,self.pickupFormation) +return aicargoapc +end +function AI_CARGO_DISPATCHER_APC:SetCombatRadius(CombatRadius) +self.CombatRadius=CombatRadius or 0 +return self +end +function AI_CARGO_DISPATCHER_APC:SetOffRoad(Offroad,Formation) +self:SetPickupOffRoad(Offroad,Formation) +self:SetDeployOffRoad(Offroad,Formation) +return self +end +function AI_CARGO_DISPATCHER_APC:SetPickupOffRoad(Offroad,Formation) +self.pickupOffroad=Offroad +self.pickupFormation=Formation or ENUMS.Formation.Vehicle.OffRoad +return self +end +function AI_CARGO_DISPATCHER_APC:SetDeployOffRoad(Offroad,Formation) +self.deployOffroad=Offroad +self.deployFormation=Formation or ENUMS.Formation.Vehicle.OffRoad +return self +end +AI_CARGO_DISPATCHER_HELICOPTER={ +ClassName="AI_CARGO_DISPATCHER_HELICOPTER", +} +function AI_CARGO_DISPATCHER_HELICOPTER:New(HelicopterSet,CargoSet,PickupZoneSet,DeployZoneSet) +local self=BASE:Inherit(self,AI_CARGO_DISPATCHER:New(HelicopterSet,CargoSet,PickupZoneSet,DeployZoneSet)) +self:SetPickupSpeed(350,150) +self:SetDeploySpeed(350,150) +self:SetPickupRadius(0,0) +self:SetDeployRadius(0,0) +self:SetPickupHeight(500,200) +self:SetDeployHeight(500,200) +return self +end +function AI_CARGO_DISPATCHER_HELICOPTER:AICargo(Helicopter,CargoSet) +return AI_CARGO_HELICOPTER:New(Helicopter,CargoSet) +end +AI_CARGO_DISPATCHER_AIRPLANE={ +ClassName="AI_CARGO_DISPATCHER_AIRPLANE", +} +function AI_CARGO_DISPATCHER_AIRPLANE:New(AirplaneSet,CargoSet,PickupZoneSet,DeployZoneSet) +local self=BASE:Inherit(self,AI_CARGO_DISPATCHER:New(AirplaneSet,CargoSet,PickupZoneSet,DeployZoneSet)) +self:SetPickupSpeed(1200,600) +self:SetDeploySpeed(1200,600) +self:SetPickupRadius(0,0) +self:SetDeployRadius(0,0) +self:SetPickupHeight(8000,6000) +self:SetDeployHeight(8000,6000) +self:SetMonitorTimeInterval(600) +return self +end +function AI_CARGO_DISPATCHER_AIRPLANE:AICargo(Airplane,CargoSet) +return AI_CARGO_AIRPLANE:New(Airplane,CargoSet) +end +AI_CARGO_DISPATCHER_SHIP={ +ClassName="AI_CARGO_DISPATCHER_SHIP" +} +function AI_CARGO_DISPATCHER_SHIP:New(ShipSet,CargoSet,PickupZoneSet,DeployZoneSet,ShippingLane) +local self=BASE:Inherit(self,AI_CARGO_DISPATCHER:New(ShipSet,CargoSet,PickupZoneSet,DeployZoneSet)) +self:SetPickupSpeed(60,10) +self:SetDeploySpeed(60,10) +self:SetPickupRadius(500,6000) +self:SetDeployRadius(500,6000) +self:SetPickupHeight(0,0) +self:SetDeployHeight(0,0) +self:SetShippingLane(ShippingLane) +self:SetMonitorTimeInterval(600) +return self +end +function AI_CARGO_DISPATCHER_SHIP:SetShippingLane(ShippingLane) +self.ShippingLane=ShippingLane +return self +end +function AI_CARGO_DISPATCHER_SHIP:AICargo(Ship,CargoSet) +return AI_CARGO_SHIP:New(Ship,CargoSet,0,self.ShippingLane) +end +do +ACT_ASSIGN={ +ClassName="ACT_ASSIGN", +} +function ACT_ASSIGN:New() +local self=BASE:Inherit(self,FSM_PROCESS:New("ACT_ASSIGN")) +self:AddTransition("UnAssigned","Start","Waiting") +self:AddTransition("Waiting","Assign","Assigned") +self:AddTransition("Waiting","Reject","Rejected") +self:AddTransition("*","Fail","Failed") +self:AddEndState("Assigned") +self:AddEndState("Rejected") +self:AddEndState("Failed") +self:SetStartState("UnAssigned") +return self +end +end +do +ACT_ASSIGN_ACCEPT={ +ClassName="ACT_ASSIGN_ACCEPT", +} +function ACT_ASSIGN_ACCEPT:New(TaskBriefing) +local self=BASE:Inherit(self,ACT_ASSIGN:New()) +self.TaskBriefing=TaskBriefing +return self +end +function ACT_ASSIGN_ACCEPT:Init(FsmAssign) +self.TaskBriefing=FsmAssign.TaskBriefing +end +function ACT_ASSIGN_ACCEPT:onafterStart(ProcessUnit,Task,From,Event,To) +self:__Assign(1) +end +function ACT_ASSIGN_ACCEPT:onenterAssigned(ProcessUnit,Task,From,Event,To,TaskGroup) +self.Task:Assign(ProcessUnit,ProcessUnit:GetPlayerName()) +end +end +do +ACT_ASSIGN_MENU_ACCEPT={ +ClassName="ACT_ASSIGN_MENU_ACCEPT", +} +function ACT_ASSIGN_MENU_ACCEPT:New(TaskBriefing) +local self=BASE:Inherit(self,ACT_ASSIGN:New()) +self.TaskBriefing=TaskBriefing +return self +end +function ACT_ASSIGN_MENU_ACCEPT:Init(TaskBriefing) +self.TaskBriefing=TaskBriefing +return self +end +function ACT_ASSIGN_MENU_ACCEPT:onafterStart(ProcessUnit,Task,From,Event,To) +self:GetCommandCenter():MessageToGroup("Task "..self.Task:GetName().." has been assigned to you and your group!\nRead the briefing and use the Radio Menu (F10) / Task ... CONFIRMATION menu to accept or reject the task.\nYou have 2 minutes to accept, or the task assignment will be cancelled!",ProcessUnit:GetGroup(),120) +local TaskGroup=ProcessUnit:GetGroup() +self.Menu=MENU_GROUP:New(TaskGroup,"Task "..self.Task:GetName().." CONFIRMATION") +self.MenuAcceptTask=MENU_GROUP_COMMAND:New(TaskGroup,"Accept task "..self.Task:GetName(),self.Menu,self.MenuAssign,self,TaskGroup) +self.MenuRejectTask=MENU_GROUP_COMMAND:New(TaskGroup,"Reject task "..self.Task:GetName(),self.Menu,self.MenuReject,self,TaskGroup) +self:__Reject(120,TaskGroup) +end +function ACT_ASSIGN_MENU_ACCEPT:MenuAssign(TaskGroup) +self:__Assign(-1,TaskGroup) +end +function ACT_ASSIGN_MENU_ACCEPT:MenuReject(TaskGroup) +self:__Reject(-1,TaskGroup) +end +function ACT_ASSIGN_MENU_ACCEPT:onafterAssign(ProcessUnit,Task,From,Event,To,TaskGroup) +self.Menu:Remove() +end +function ACT_ASSIGN_MENU_ACCEPT:onafterReject(ProcessUnit,Task,From,Event,To,TaskGroup) +self:F({TaskGroup=TaskGroup}) +self.Menu:Remove() +self.Task:RejectGroup(TaskGroup) +end +function ACT_ASSIGN_MENU_ACCEPT:onenterAssigned(ProcessUnit,Task,From,Event,To,TaskGroup) +self.Task:Assign(ProcessUnit,ProcessUnit:GetPlayerName()) +end +end +do +ACT_ROUTE={ +ClassName="ACT_ROUTE", +} +function ACT_ROUTE:New() +local self=BASE:Inherit(self,FSM_PROCESS:New("ACT_ROUTE")) +self:AddTransition("*","Reset","None") +self:AddTransition("None","Start","Routing") +self:AddTransition("*","Report","*") +self:AddTransition("Routing","Route","Routing") +self:AddTransition("Routing","Pause","Pausing") +self:AddTransition("Routing","Arrive","Arrived") +self:AddTransition("*","Cancel","Cancelled") +self:AddTransition("Arrived","Success","Success") +self:AddTransition("*","Fail","Failed") +self:AddTransition("","","") +self:AddTransition("","","") +self:AddEndState("Arrived") +self:AddEndState("Failed") +self:AddEndState("Cancelled") +self:SetStartState("None") +self:SetRouteMode("C") +return self +end +function ACT_ROUTE:SetMenuCancel(MenuGroup,MenuText,ParentMenu,MenuTime,MenuTag) +self.CancelMenuGroupCommand=MENU_GROUP_COMMAND:New( +MenuGroup, +MenuText, +ParentMenu, +self.MenuCancel, +self +):SetTime(MenuTime):SetTag(MenuTag) +ParentMenu:SetTime(MenuTime) +ParentMenu:Remove(MenuTime,MenuTag) +return self +end +function ACT_ROUTE:SetRouteMode(RouteMode) +self.RouteMode=RouteMode +return self +end +function ACT_ROUTE:GetRouteText(Controllable) +local RouteText="" +local Coordinate=nil +if self.Coordinate then +Coordinate=self.Coordinate +end +if self.Zone then +Coordinate=self.Zone:GetPointVec3(self.Altitude) +Coordinate:SetHeading(self.Heading) +end +local Task=self:GetTask() +local CC=self:GetTask():GetMission():GetCommandCenter() +if CC then +if CC:IsModeWWII()then +local ShortestDistance=0 +local ShortestReferencePoint=nil +local ShortestReferenceName="" +self:F({CC.ReferencePoints}) +for ZoneName,Zone in pairs(CC.ReferencePoints)do +self:F({ZoneName=ZoneName}) +local Zone=Zone +local ZoneCoord=Zone:GetCoordinate() +local ZoneDistance=ZoneCoord:Get2DDistance(Coordinate) +self:F({ShortestDistance,ShortestReferenceName}) +if ShortestDistance==0 or ZoneDistance=self.DisplayInterval then +self:T({HasArrived=HasArrived}) +if not HasArrived then +self:Report() +end +self.DisplayCount=1 +else +self.DisplayCount=self.DisplayCount+1 +end +if HasArrived then +self:__Arrive(1) +else +self:__Route(1) +end +return HasArrived +end +return false +end +end +do +ACT_ROUTE_POINT={ +ClassName="ACT_ROUTE_POINT", +} +function ACT_ROUTE_POINT:New(Coordinate,Range) +local self=BASE:Inherit(self,ACT_ROUTE:New()) +self.Coordinate=Coordinate +self.Range=Range or 0 +self.DisplayInterval=30 +self.DisplayCount=30 +self.DisplayMessage=true +self.DisplayTime=10 +return self +end +function ACT_ROUTE_POINT:Init(FsmRoute) +self.Coordinate=FsmRoute.Coordinate +self.Range=FsmRoute.Range or 0 +self.DisplayInterval=30 +self.DisplayCount=30 +self.DisplayMessage=true +self.DisplayTime=10 +self:SetStartState("None") +end +function ACT_ROUTE_POINT:SetCoordinate(Coordinate) +self:F2({Coordinate}) +self.Coordinate=Coordinate +end +function ACT_ROUTE_POINT:GetCoordinate() +self:F2({self.Coordinate}) +return self.Coordinate +end +function ACT_ROUTE_POINT:SetRange(Range) +self:F2({Range}) +self.Range=Range or 10000 +end +function ACT_ROUTE_POINT:GetRange() +self:F2({self.Range}) +return self.Range +end +function ACT_ROUTE_POINT:onfuncHasArrived(ProcessUnit) +if ProcessUnit:IsAlive()then +local Distance=self.Coordinate:Get2DDistance(ProcessUnit:GetCoordinate()) +if Distance<=self.Range then +local RouteText="Task \""..self:GetTask():GetName().."\", you have arrived." +self:GetCommandCenter():MessageTypeToGroup(RouteText,ProcessUnit:GetGroup(),MESSAGE.Type.Information) +return true +end +end +return false +end +function ACT_ROUTE_POINT:onafterReport(ProcessUnit,From,Event,To) +local RouteText="Task \""..self:GetTask():GetName().."\", "..self:GetRouteText(ProcessUnit) +self:GetCommandCenter():MessageTypeToGroup(RouteText,ProcessUnit:GetGroup(),MESSAGE.Type.Update) +end +end +do +ACT_ROUTE_ZONE={ +ClassName="ACT_ROUTE_ZONE", +} +function ACT_ROUTE_ZONE:New(Zone) +local self=BASE:Inherit(self,ACT_ROUTE:New()) +self.Zone=Zone +self.DisplayInterval=30 +self.DisplayCount=30 +self.DisplayMessage=true +self.DisplayTime=10 +return self +end +function ACT_ROUTE_ZONE:Init(FsmRoute) +self.Zone=FsmRoute.Zone +self.DisplayInterval=30 +self.DisplayCount=30 +self.DisplayMessage=true +self.DisplayTime=10 +end +function ACT_ROUTE_ZONE:SetZone(Zone,Altitude,Heading) +self.Zone=Zone +self.Altitude=Altitude +self.Heading=Heading +end +function ACT_ROUTE_ZONE:GetZone() +return self.Zone +end +function ACT_ROUTE_ZONE:onfuncHasArrived(ProcessUnit) +if ProcessUnit:IsInZone(self.Zone)then +local RouteText="Task \""..self:GetTask():GetName().."\", you have arrived within the zone." +self:GetCommandCenter():MessageTypeToGroup(RouteText,ProcessUnit:GetGroup(),MESSAGE.Type.Information) +end +return ProcessUnit:IsInZone(self.Zone) +end +function ACT_ROUTE_ZONE:onafterReport(ProcessUnit,From,Event,To) +self:F({ProcessUnit=ProcessUnit}) +local RouteText="Task \""..self:GetTask():GetName().."\", "..self:GetRouteText(ProcessUnit) +self:GetCommandCenter():MessageTypeToGroup(RouteText,ProcessUnit:GetGroup(),MESSAGE.Type.Update) +end +end +do +ACT_ACCOUNT={ +ClassName="ACT_ACCOUNT", +TargetSetUnit=nil, +} +function ACT_ACCOUNT:New() +local self=BASE:Inherit(self,FSM_PROCESS:New()) +self:AddTransition("Assigned","Start","Waiting") +self:AddTransition("*","Wait","Waiting") +self:AddTransition("*","Report","Report") +self:AddTransition("*","Event","Account") +self:AddTransition("Account","Player","AccountForPlayer") +self:AddTransition("Account","Other","AccountForOther") +self:AddTransition({"Account","AccountForPlayer","AccountForOther"},"More","Wait") +self:AddTransition({"Account","AccountForPlayer","AccountForOther"},"NoMore","Accounted") +self:AddTransition("*","Fail","Failed") +self:AddEndState("Failed") +self:SetStartState("Assigned") +return self +end +function ACT_ACCOUNT:onafterStart(ProcessUnit,From,Event,To) +self:HandleEvent(EVENTS.Dead,self.onfuncEventDead) +self:HandleEvent(EVENTS.Crash,self.onfuncEventCrash) +self:HandleEvent(EVENTS.Hit) +self:__Wait(1) +end +function ACT_ACCOUNT:onenterWaiting(ProcessUnit,From,Event,To) +if self.DisplayCount>=self.DisplayInterval then +self:Report() +self.DisplayCount=1 +else +self.DisplayCount=self.DisplayCount+1 +end +return true +end +function ACT_ACCOUNT:onafterEvent(ProcessUnit,From,Event,To,Event) +self:__NoMore(1) +end +end +do +ACT_ACCOUNT_DEADS={ +ClassName="ACT_ACCOUNT_DEADS", +} +function ACT_ACCOUNT_DEADS:New() +local self=BASE:Inherit(self,ACT_ACCOUNT:New()) +self.DisplayInterval=30 +self.DisplayCount=30 +self.DisplayMessage=true +self.DisplayTime=10 +self.DisplayCategory="HQ" +return self +end +function ACT_ACCOUNT_DEADS:Init(FsmAccount) +self.Task=self:GetTask() +self.TaskName=self.Task:GetName() +end +function ACT_ACCOUNT_DEADS:onenterReport(ProcessUnit,Task,From,Event,To) +local MessageText="Your group with assigned "..self.TaskName.." task has "..Task.TargetSetUnit:GetUnitTypesText().." targets left to be destroyed." +self:GetCommandCenter():MessageTypeToGroup(MessageText,ProcessUnit:GetGroup(),MESSAGE.Type.Information) +end +function ACT_ACCOUNT_DEADS:onafterEvent(ProcessUnit,Task,From,Event,To,EventData) +self:T({ProcessUnit:GetName(),Task:GetName(),From,Event,To,EventData}) +if Task.TargetSetUnit:FindUnit(EventData.IniUnitName)then +local PlayerName=ProcessUnit:GetPlayerName() +local PlayerHit=self.PlayerHits and self.PlayerHits[EventData.IniUnitName] +if PlayerHit==PlayerName then +self:Player(EventData) +else +self:Other(EventData) +end +end +end +function ACT_ACCOUNT_DEADS:onenterAccountForPlayer(ProcessUnit,Task,From,Event,To,EventData) +self:T({ProcessUnit:GetName(),Task:GetName(),From,Event,To,EventData}) +local TaskGroup=ProcessUnit:GetGroup() +Task.TargetSetUnit:Remove(EventData.IniUnitName) +local MessageText="You have destroyed a target.\nYour group assigned with task "..self.TaskName.." has\n"..Task.TargetSetUnit:Count().." targets ( "..Task.TargetSetUnit:GetUnitTypesText().." ) left to be destroyed." +self:GetCommandCenter():MessageTypeToGroup(MessageText,ProcessUnit:GetGroup(),MESSAGE.Type.Information) +local PlayerName=ProcessUnit:GetPlayerName() +Task:AddProgress(PlayerName,"Destroyed "..EventData.IniTypeName,timer.getTime(),1) +if Task.TargetSetUnit:Count()>0 then +self:__More(1) +else +self:__NoMore(1) +end +end +function ACT_ACCOUNT_DEADS:onenterAccountForOther(ProcessUnit,Task,From,Event,To,EventData) +self:T({ProcessUnit:GetName(),Task:GetName(),From,Event,To,EventData}) +local TaskGroup=ProcessUnit:GetGroup() +Task.TargetSetUnit:Remove(EventData.IniUnitName) +local MessageText="One of the task targets has been destroyed.\nYour group assigned with task "..self.TaskName.." has\n"..Task.TargetSetUnit:Count().." targets ( "..Task.TargetSetUnit:GetUnitTypesText().." ) left to be destroyed." +self:GetCommandCenter():MessageTypeToGroup(MessageText,ProcessUnit:GetGroup(),MESSAGE.Type.Information) +if Task.TargetSetUnit:Count()>0 then +self:__More(1) +else +self:__NoMore(1) +end +end +function ACT_ACCOUNT_DEADS:OnEventHit(EventData) +self:T({"EventDead",EventData}) +if EventData.IniPlayerName and EventData.TgtDCSUnitName then +self.PlayerHits=self.PlayerHits or{} +self.PlayerHits[EventData.TgtDCSUnitName]=EventData.IniPlayerName +end +end +function ACT_ACCOUNT_DEADS:onfuncEventDead(EventData) +self:T({"EventDead",EventData}) +if EventData.IniDCSUnit then +self:Event(EventData) +end +end +function ACT_ACCOUNT_DEADS:onfuncEventCrash(EventData) +self:T({"EventDead",EventData}) +if EventData.IniDCSUnit then +self:Event(EventData) +end +end +end +do +ACT_ASSIST={ +ClassName="ACT_ASSIST", +} +function ACT_ASSIST:New() +local self=BASE:Inherit(self,FSM_PROCESS:New("ACT_ASSIST")) +self:AddTransition("None","Start","AwaitSmoke") +self:AddTransition("AwaitSmoke","Next","Smoking") +self:AddTransition("Smoking","Next","AwaitSmoke") +self:AddTransition("*","Stop","Success") +self:AddTransition("*","Fail","Failed") +self:AddEndState("Failed") +self:AddEndState("Success") +self:SetStartState("None") +return self +end +function ACT_ASSIST:onafterStart(ProcessUnit,From,Event,To) +local ProcessGroup=ProcessUnit:GetGroup() +local MissionMenu=self:GetMission():GetMenu(ProcessGroup) +local function MenuSmoke(MenuParam) +local self=MenuParam.self +local SmokeColor=MenuParam.SmokeColor +self.SmokeColor=SmokeColor +self:__Next(1) +end +self.Menu=MENU_GROUP:New(ProcessGroup,"Target acquisition",MissionMenu) +self.MenuSmokeBlue=MENU_GROUP_COMMAND:New(ProcessGroup,"Drop blue smoke on targets",self.Menu,MenuSmoke,{self=self,SmokeColor=SMOKECOLOR.Blue}) +self.MenuSmokeGreen=MENU_GROUP_COMMAND:New(ProcessGroup,"Drop green smoke on targets",self.Menu,MenuSmoke,{self=self,SmokeColor=SMOKECOLOR.Green}) +self.MenuSmokeOrange=MENU_GROUP_COMMAND:New(ProcessGroup,"Drop Orange smoke on targets",self.Menu,MenuSmoke,{self=self,SmokeColor=SMOKECOLOR.Orange}) +self.MenuSmokeRed=MENU_GROUP_COMMAND:New(ProcessGroup,"Drop Red smoke on targets",self.Menu,MenuSmoke,{self=self,SmokeColor=SMOKECOLOR.Red}) +self.MenuSmokeWhite=MENU_GROUP_COMMAND:New(ProcessGroup,"Drop White smoke on targets",self.Menu,MenuSmoke,{self=self,SmokeColor=SMOKECOLOR.White}) +end +function ACT_ASSIST:onafterStop(ProcessUnit,From,Event,To) +self.Menu:Remove() +end +end +do +ACT_ASSIST_SMOKE_TARGETS_ZONE={ +ClassName="ACT_ASSIST_SMOKE_TARGETS_ZONE", +} +function ACT_ASSIST_SMOKE_TARGETS_ZONE:New(TargetSetUnit,TargetZone) +local self=BASE:Inherit(self,ACT_ASSIST:New()) +self.TargetSetUnit=TargetSetUnit +self.TargetZone=TargetZone +return self +end +function ACT_ASSIST_SMOKE_TARGETS_ZONE:Init(FsmSmoke) +self.TargetSetUnit=FsmSmoke.TargetSetUnit +self.TargetZone=FsmSmoke.TargetZone +end +function ACT_ASSIST_SMOKE_TARGETS_ZONE:Init(TargetSetUnit,TargetZone) +self.TargetSetUnit=TargetSetUnit +self.TargetZone=TargetZone +return self +end +function ACT_ASSIST_SMOKE_TARGETS_ZONE:onenterSmoking(ProcessUnit,From,Event,To) +self.TargetSetUnit:ForEachUnit( +function(SmokeUnit) +if math.random(1,(100*self.TargetSetUnit:Count())/4)<=100 then +SCHEDULER:New(self, +function() +if SmokeUnit:IsAlive()then +SmokeUnit:Smoke(self.SmokeColor,150) +end +end,{},math.random(10,60) +) +end +end +) +end +end +COMMANDCENTER={ +ClassName="COMMANDCENTER", +CommandCenterName="", +CommandCenterCoalition=nil, +CommandCenterPositionable=nil, +Name="", +ReferencePoints={}, +ReferenceNames={}, +CommunicationMode="80", +} +COMMANDCENTER.AutoAssignMethods={ +["Random"]=1, +["Distance"]=2, +["Priority"]=3, +} +function COMMANDCENTER:New(CommandCenterPositionable,CommandCenterName) +local self=BASE:Inherit(self,BASE:New()) +self.CommandCenterPositionable=CommandCenterPositionable +self.CommandCenterName=CommandCenterName or CommandCenterPositionable:GetName() +self.CommandCenterCoalition=CommandCenterPositionable:GetCoalition() +self.Missions={} +self:SetAutoAssignTasks(false) +self:SetAutoAcceptTasks(true) +self:SetAutoAssignMethod(COMMANDCENTER.AutoAssignMethods.Distance) +self:SetFlashStatus(false) +self:HandleEvent(EVENTS.Birth, +function(self,EventData) +if EventData.IniObjectCategory==1 then +local EventGroup=GROUP:Find(EventData.IniDCSGroup) +if EventGroup and EventGroup:IsAlive()and self:HasGroup(EventGroup)then +local CommandCenterMenu=MENU_GROUP:New(EventGroup,self:GetText()) +local MenuReporting=MENU_GROUP:New(EventGroup,"Missions Reports",CommandCenterMenu) +local MenuMissionsSummary=MENU_GROUP_COMMAND:New(EventGroup,"Missions Status Report",MenuReporting,self.ReportSummary,self,EventGroup) +local MenuMissionsDetails=MENU_GROUP_COMMAND:New(EventGroup,"Missions Players Report",MenuReporting,self.ReportMissionsPlayers,self,EventGroup) +self:ReportSummary(EventGroup) +local PlayerUnit=EventData.IniUnit +for MissionID,Mission in pairs(self:GetMissions())do +local Mission=Mission +local PlayerGroup=EventData.IniGroup +Mission:JoinUnit(PlayerUnit,PlayerGroup) +end +self:SetMenu() +end +end +end +) +self:HandleEvent(EVENTS.MissionEnd, +function(self,EventData) +local PlayerUnit=EventData.IniUnit +for MissionID,Mission in pairs(self:GetMissions())do +local Mission=Mission +Mission:Stop() +end +end +) +self:HandleEvent(EVENTS.PlayerLeaveUnit, +function(self,EventData) +local PlayerUnit=EventData.IniUnit +for MissionID,Mission in pairs(self:GetMissions())do +local Mission=Mission +if Mission:IsENGAGED()then +Mission:AbortUnit(PlayerUnit) +end +end +end +) +self:HandleEvent(EVENTS.Crash, +function(self,EventData) +local PlayerUnit=EventData.IniUnit +for MissionID,Mission in pairs(self:GetMissions())do +local Mission=Mission +if Mission:IsENGAGED()then +Mission:CrashUnit(PlayerUnit) +end +end +end +) +self:SetMenu() +_SETTINGS:SetSystemMenu(CommandCenterPositionable) +self:SetCommandMenu() +return self +end +function COMMANDCENTER:GetName() +return self.CommandCenterName +end +function COMMANDCENTER:GetText() +return"Command Center ["..self.CommandCenterName.."]" +end +function COMMANDCENTER:GetShortText() +return"CC ["..self.CommandCenterName.."]" +end +function COMMANDCENTER:GetCoalition() +return self.CommandCenterCoalition +end +function COMMANDCENTER:GetPositionable() +return self.CommandCenterPositionable +end +function COMMANDCENTER:GetMissions() +return self.Missions or{} +end +function COMMANDCENTER:AddMission(Mission) +self.Missions[Mission]=Mission +return Mission +end +function COMMANDCENTER:RemoveMission(Mission) +self.Missions[Mission]=nil +return Mission +end +function COMMANDCENTER:SetReferenceZones(ReferenceZonePrefix) +local MatchPattern="(.*)#(.*)" +self:F({MatchPattern=MatchPattern}) +for ReferenceZoneName in pairs(_DATABASE.ZONENAMES)do +local ZoneName,ReferenceName=string.match(ReferenceZoneName,MatchPattern) +self:F({ZoneName=ZoneName,ReferenceName=ReferenceName}) +if ZoneName and ReferenceName and ZoneName==ReferenceZonePrefix then +self.ReferencePoints[ReferenceZoneName]=ZONE:New(ReferenceZoneName) +self.ReferenceNames[ReferenceZoneName]=ReferenceName +end +end +return self +end +function COMMANDCENTER:SetModeWWII() +self.CommunicationMode="WWII" +return self +end +function COMMANDCENTER:IsModeWWII() +return self.CommunicationMode=="WWII" +end +function COMMANDCENTER:SetMenu() +self:F2() +local MenuTime=timer.getTime() +for MissionID,Mission in pairs(self:GetMissions()or{})do +local Mission=Mission +Mission:SetMenu(MenuTime) +end +for MissionID,Mission in pairs(self:GetMissions()or{})do +Mission=Mission +Mission:RemoveMenu(MenuTime) +end +end +function COMMANDCENTER:GetMenu(TaskGroup) +local MenuTime=timer.getTime() +self.CommandCenterMenus=self.CommandCenterMenus or{} +local CommandCenterMenu +local CommandCenterText=self:GetText() +CommandCenterMenu=MENU_GROUP:New(TaskGroup,CommandCenterText):SetTime(MenuTime) +self.CommandCenterMenus[TaskGroup]=CommandCenterMenu +if self.AutoAssignTasks==false then +local AssignTaskMenu=MENU_GROUP_COMMAND:New(TaskGroup,"Assign Task",CommandCenterMenu,self.AssignTask,self,TaskGroup):SetTime(MenuTime):SetTag("AutoTask") +end +CommandCenterMenu:Remove(MenuTime,"AutoTask") +return self.CommandCenterMenus[TaskGroup] +end +function COMMANDCENTER:AssignTask(TaskGroup) +local Tasks={} +local AssignPriority=99999999 +local AutoAssignMethod=self.AutoAssignMethod +for MissionID,Mission in pairs(self:GetMissions())do +local Mission=Mission +local MissionTasks=Mission:GetGroupTasks(TaskGroup) +for MissionTaskName,MissionTask in pairs(MissionTasks or{})do +local MissionTask=MissionTask +if MissionTask:IsStatePlanned()or MissionTask:IsStateReplanned()or MissionTask:IsStateAssigned()then +local TaskPriority=MissionTask:GetAutoAssignPriority(self.AutoAssignMethod,self,TaskGroup) +if TaskPriority Adding TASK ",MissionName=self:GetName(),TaskName=TaskName}) +self.Tasks[TaskName]=Task +self:GetCommandCenter():SetMenu() +return Task +end +function MISSION:RemoveTask(Task) +local TaskName=Task:GetTaskName() +self:I({"<== Removing TASK ",MissionName=self:GetName(),TaskName=TaskName}) +self:F(TaskName) +self.Tasks[TaskName]=self.Tasks[TaskName]or{n=0} +self.Tasks[TaskName]=nil +Task=nil +collectgarbage() +self:GetCommandCenter():SetMenu() +return nil +end +function MISSION:IsCOMPLETED() +return self:Is("COMPLETED") +end +function MISSION:IsIDLE() +return self:Is("IDLE") +end +function MISSION:IsENGAGED() +return self:Is("ENGAGED") +end +function MISSION:IsFAILED() +return self:Is("FAILED") +end +function MISSION:IsHOLD() +return self:Is("HOLD") +end +function MISSION:HasGroup(TaskGroup) +local Has=false +for TaskID,Task in pairs(self:GetTasks())do +local Task=Task +if Task:HasGroup(TaskGroup)then +Has=true +break +end +end +return Has +end +function MISSION:GetTasksRemaining() +local TasksRemaining=0 +for TaskID,Task in pairs(self:GetTasks())do +local Task=Task +if Task:IsStateSuccess()or Task:IsStateFailed()then +else +TasksRemaining=TasksRemaining+1 +end +end +return TasksRemaining +end +function MISSION:GetTaskTypes() +local TaskTypeList={} +local TasksRemaining=0 +for TaskID,Task in pairs(self:GetTasks())do +local Task=Task +local TaskType=Task:GetType() +TaskTypeList[TaskType]=TaskType +end +return TaskTypeList +end +function MISSION:AddPlayerName(PlayerName) +self.PlayerNames=self.PlayerNames or{} +self.PlayerNames[PlayerName]=PlayerName +return self +end +function MISSION:GetPlayerNames() +return self.PlayerNames +end +function MISSION:ReportBriefing() +local Report=REPORT:New() +local Name=self:GetText() +local Status="<"..self:GetState()..">" +Report:Add(string.format('%s - %s - Mission Briefing Report',Name,Status)) +Report:Add(self.MissionBriefing) +return Report:Text() +end +function MISSION:ReportPlayersPerTask(ReportGroup) +local Report=REPORT:New() +local Name=self:GetText() +local Status="<"..self:GetState()..">" +Report:Add(string.format('%s - %s - Players per Task Report',Name,Status)) +local PlayerList={} +for TaskID,Task in pairs(self:GetTasks())do +local Task=Task +local PlayerNames=Task:GetPlayerNames() +for PlayerName,PlayerGroup in pairs(PlayerNames)do +PlayerList[PlayerName]=Task:GetName() +end +end +for PlayerName,TaskName in pairs(PlayerList)do +Report:Add(string.format(' - Player (%s): Task "%s"',PlayerName,TaskName)) +end +return Report:Text() +end +function MISSION:ReportPlayersProgress(ReportGroup) +local Report=REPORT:New() +local Name=self:GetText() +local Status="<"..self:GetState()..">" +Report:Add(string.format('%s - %s - Players per Task Progress Report',Name,Status)) +local PlayerList={} +for TaskID,Task in pairs(self:GetTasks())do +local Task=Task +local TaskName=Task:GetName() +local Goal=Task:GetGoal() +PlayerList[TaskName]=PlayerList[TaskName]or{} +if Goal then +local TotalContributions=Goal:GetTotalContributions() +local PlayerContributions=Goal:GetPlayerContributions() +self:F({TotalContributions=TotalContributions,PlayerContributions=PlayerContributions}) +for PlayerName,PlayerContribution in pairs(PlayerContributions)do +PlayerList[TaskName][PlayerName]=string.format('Player (%s): Task "%s": %d%%',PlayerName,TaskName,PlayerContributions[PlayerName]*100/TotalContributions) +end +else +PlayerList[TaskName]["_"]=string.format('Player (---): Task "%s": %d%%',TaskName,0) +end +end +for TaskName,TaskData in pairs(PlayerList)do +for PlayerName,TaskText in pairs(TaskData)do +Report:Add(string.format(' - %s',TaskText)) +end +end +return Report:Text() +end +function MISSION:MarkTargetLocations(ReportGroup) +local Report=REPORT:New() +local Name=self:GetText() +local Status="<"..self:GetState()..">" +Report:Add(string.format('%s - %s - All Tasks are marked on the map. Select a Task from the Mission Menu and Join the Task!!!',Name,Status)) +for TaskID,Task in UTILS.spairs(self:GetTasks(),function(t,a,b)return t[a]:ReportOrder(ReportGroup)" +Report:Add(string.format('%s - %s - Task Overview Report',Name,Status)) +for TaskID,Task in UTILS.spairs(self:GetTasks(),function(t,a,b)return t[a]:ReportOrder(ReportGroup)" +Report:Add(string.format('%s - %s - %s Tasks Report',Name,Status,TaskStatus)) +local Tasks=0 +for TaskID,Task in UTILS.spairs(self:GetTasks(),function(t,a,b)return t[a]:ReportOrder(ReportGroup)=8 then +break +end +end +return Report:Text() +end +function MISSION:ReportDetails(ReportGroup) +local Report=REPORT:New() +local Name=self:GetText() +local Status="<"..self:GetState()..">" +Report:Add(string.format('%s - %s - Task Detailed Report',Name,Status)) +local TasksRemaining=0 +for TaskID,Task in pairs(self:GetTasks())do +local Task=Task +Report:Add(string.rep("-",140)) +Report:Add(Task:ReportDetails(ReportGroup)) +end +return Report:Text() +end +function MISSION:GetTasks() +return self.Tasks or{} +end +function MISSION:GetGroupTasks(TaskGroup) +local Tasks={} +for TaskID,Task in pairs(self:GetTasks())do +local Task=Task +if Task:HasGroup(TaskGroup)then +Tasks[#Tasks+1]=Task +end +end +return Tasks +end +function MISSION:MenuReportBriefing(ReportGroup) +local Report=self:ReportBriefing() +self:GetCommandCenter():MessageTypeToGroup(Report,ReportGroup,MESSAGE.Type.Briefing) +end +function MISSION:MenuMarkTargetLocations(ReportGroup) +local Report=self:MarkTargetLocations(ReportGroup) +self:GetCommandCenter():MessageTypeToGroup(Report,ReportGroup,MESSAGE.Type.Overview) +end +function MISSION:MenuReportTasksSummary(ReportGroup) +local Report=self:ReportSummary(ReportGroup) +self:GetCommandCenter():MessageTypeToGroup(Report,ReportGroup,MESSAGE.Type.Overview) +end +function MISSION:MenuReportTasksPerStatus(ReportGroup,TaskStatus) +local Report=self:ReportOverview(ReportGroup,TaskStatus) +self:GetCommandCenter():MessageTypeToGroup(Report,ReportGroup,MESSAGE.Type.Overview) +end +function MISSION:MenuReportPlayersPerTask(ReportGroup) +local Report=self:ReportPlayersPerTask() +self:GetCommandCenter():MessageTypeToGroup(Report,ReportGroup,MESSAGE.Type.Overview) +end +function MISSION:MenuReportPlayersProgress(ReportGroup) +local Report=self:ReportPlayersProgress() +self:GetCommandCenter():MessageTypeToGroup(Report,ReportGroup,MESSAGE.Type.Overview) +end +TASK={ +ClassName="TASK", +TaskScheduler=nil, +ProcessClasses={}, +Processes={}, +Players=nil, +Scores={}, +Menu={}, +SetGroup=nil, +FsmTemplate=nil, +Mission=nil, +CommandCenter=nil, +TimeOut=0, +AssignedGroups={}, +} +function TASK:New(Mission,SetGroupAssign,TaskName,TaskType,TaskBriefing) +local self=BASE:Inherit(self,FSM_TASK:New(TaskName)) +self:SetStartState("Planned") +self:AddTransition("Planned","Assign","Assigned") +self:AddTransition("Assigned","AssignUnit","Assigned") +self:AddTransition("Assigned","Success","Success") +self:AddTransition("Assigned","Hold","Hold") +self:AddTransition("Assigned","Fail","Failed") +self:AddTransition({"Planned","Assigned"},"Abort","Aborted") +self:AddTransition("Assigned","Cancel","Cancelled") +self:AddTransition("Assigned","Goal","*") +self.Fsm={} +local Fsm=self:GetUnitProcess() +Fsm:SetStartState("Planned") +Fsm:AddProcess("Planned","Accept",ACT_ASSIGN_ACCEPT:New(self.TaskBriefing),{Assigned="Assigned",Rejected="Reject"}) +Fsm:AddTransition("Assigned","Assigned","*") +self:AddTransition("*","PlayerCrashed","*") +self:AddTransition("*","PlayerAborted","*") +self:AddTransition("*","PlayerRejected","*") +self:AddTransition("*","PlayerDead","*") +self:AddTransition({"Failed","Aborted","Cancelled"},"Replan","Planned") +self:AddTransition("*","TimeOut","Cancelled") +self:F("New TASK "..TaskName) +self.Processes={} +self.Mission=Mission +self.CommandCenter=Mission:GetCommandCenter() +self.SetGroup=SetGroupAssign +self:SetType(TaskType) +self:SetName(TaskName) +self:SetID(Mission:GetNextTaskID(self)) +self:SetBriefing(TaskBriefing) +self.TaskInfo=TASKINFO:New(self) +self.TaskProgress={} +return self +end +function TASK:GetUnitProcess(TaskUnit) +if TaskUnit then +return self:GetStateMachine(TaskUnit) +else +self.FsmTemplate=self.FsmTemplate or FSM_PROCESS:New() +return self.FsmTemplate +end +end +function TASK:SetUnitProcess(FsmTemplate) +self.FsmTemplate=FsmTemplate +end +function TASK:JoinUnit(PlayerUnit,PlayerGroup) +self:F({PlayerUnit=PlayerUnit,PlayerGroup=PlayerGroup}) +local PlayerUnitAdded=false +local PlayerGroups=self:GetGroups() +if PlayerGroups:IsIncludeObject(PlayerGroup)then +if self:IsStatePlanned()or self:IsStateReplanned()then +end +if self:IsStateAssigned()then +local IsGroupAssigned=self:IsGroupAssigned(PlayerGroup) +self:F({IsGroupAssigned=IsGroupAssigned}) +if IsGroupAssigned then +self:AssignToUnit(PlayerUnit) +self:MessageToGroups(PlayerUnit:GetPlayerName().." joined Task "..self:GetName()) +end +end +end +return PlayerUnitAdded +end +function TASK:RejectGroup(PlayerGroup) +local PlayerGroups=self:GetGroups() +if PlayerGroups:IsIncludeObject(PlayerGroup)then +if self:IsStatePlanned()then +local IsGroupAssigned=self:IsGroupAssigned(PlayerGroup) +if IsGroupAssigned then +local PlayerName=PlayerGroup:GetUnit(1):GetPlayerName() +self:GetMission():GetCommandCenter():MessageToGroup("Task "..self:GetName().." has been rejected! We will select another task.",PlayerGroup) +self:UnAssignFromGroup(PlayerGroup) +self:PlayerRejected(PlayerGroup:GetUnit(1)) +end +end +end +return self +end +function TASK:AbortGroup(PlayerGroup) +local PlayerGroups=self:GetGroups() +if PlayerGroups:IsIncludeObject(PlayerGroup)then +if self:IsStateAssigned()then +local IsGroupAssigned=self:IsGroupAssigned(PlayerGroup) +if IsGroupAssigned then +local PlayerName=PlayerGroup:GetUnit(1):GetPlayerName() +self:UnAssignFromGroup(PlayerGroup) +PlayerGroups:Flush(self) +local IsRemaining=false +for GroupName,AssignedGroup in pairs(PlayerGroups:GetSet()or{})do +if self:IsGroupAssigned(AssignedGroup)==true then +IsRemaining=true +self:F({Task=self:GetName(),IsRemaining=IsRemaining}) +break +end +end +self:F({Task=self:GetName(),IsRemaining=IsRemaining}) +if IsRemaining==false then +self:Abort() +end +self:PlayerAborted(PlayerGroup:GetUnit(1)) +end +end +end +return self +end +function TASK:CrashGroup(PlayerGroup) +self:F({PlayerGroup=PlayerGroup}) +local PlayerGroups=self:GetGroups() +if PlayerGroups:IsIncludeObject(PlayerGroup)then +if self:IsStateAssigned()then +local IsGroupAssigned=self:IsGroupAssigned(PlayerGroup) +self:F({IsGroupAssigned=IsGroupAssigned}) +if IsGroupAssigned then +local PlayerName=PlayerGroup:GetUnit(1):GetPlayerName() +self:MessageToGroups(PlayerName.." crashed! ") +self:UnAssignFromGroup(PlayerGroup) +PlayerGroups:Flush(self) +local IsRemaining=false +for GroupName,AssignedGroup in pairs(PlayerGroups:GetSet()or{})do +if self:IsGroupAssigned(AssignedGroup)==true then +IsRemaining=true +self:F({Task=self:GetName(),IsRemaining=IsRemaining}) +break +end +end +self:F({Task=self:GetName(),IsRemaining=IsRemaining}) +if IsRemaining==false then +self:Abort() +end +self:PlayerCrashed(PlayerGroup:GetUnit(1)) +end +end +end +return self +end +function TASK:GetMission() +return self.Mission +end +function TASK:GetGroups() +return self.SetGroup +end +function TASK:AddGroups(GroupSet) +GroupSet=GroupSet or SET_GROUP:New() +self.SetGroup:ForEachGroup( +function(GroupItem) +GroupSet:Add(GroupItem:GetName(),GroupItem) +end +) +return GroupSet +end +do +function TASK:IsGroupAssigned(TaskGroup) +local TaskGroupName=TaskGroup:GetName() +if self.AssignedGroups[TaskGroupName]then +return true +end +return false +end +function TASK:SetGroupAssigned(TaskGroup) +local TaskName=self:GetName() +local TaskGroupName=TaskGroup:GetName() +self.AssignedGroups[TaskGroupName]=TaskGroup +self:F(string.format("Task %s is assigned to %s",TaskName,TaskGroupName)) +self:GetMission():SetGroupAssigned(TaskGroup) +local SetAssignedGroups=self:GetGroups() +return self +end +function TASK:ClearGroupAssignment(TaskGroup) +local TaskName=self:GetName() +local TaskGroupName=TaskGroup:GetName() +self.AssignedGroups[TaskGroupName]=nil +self:GetMission():ClearGroupAssignment(TaskGroup) +local SetAssignedGroups=self:GetGroups() +SetAssignedGroups:ForEachGroup( +function(AssignedGroup) +if self:IsGroupAssigned(AssignedGroup)then +else +end +end +) +return self +end +end +do +function TASK:SetAssignMethod(AcceptClass) +local ProcessTemplate=self:GetUnitProcess() +ProcessTemplate:SetProcess("Planned","Accept",AcceptClass) +end +function TASK:AssignToGroup(TaskGroup) +self:F(TaskGroup:GetName()) +local TaskGroupName=TaskGroup:GetName() +local Mission=self:GetMission() +local CommandCenter=Mission:GetCommandCenter() +self:SetGroupAssigned(TaskGroup) +local TaskUnits=TaskGroup:GetUnits() +for UnitID,UnitData in pairs(TaskUnits)do +local TaskUnit=UnitData +local PlayerName=TaskUnit:GetPlayerName() +self:F(PlayerName) +if PlayerName~=nil and PlayerName~=""then +self:AssignToUnit(TaskUnit) +CommandCenter:MessageToGroup( +string.format('Task "%s": Briefing for player (%s):\n%s', +self:GetName(), +PlayerName, +self:GetBriefing() +),TaskGroup +) +end +end +CommandCenter:SetMenu() +self:MenuFlashTaskStatus(TaskGroup,self:GetMission():GetCommandCenter().FlashStatus) +return self +end +function TASK:UnAssignFromGroup(TaskGroup) +self:F2({TaskGroup=TaskGroup:GetName()}) +self:ClearGroupAssignment(TaskGroup) +local TaskUnits=TaskGroup:GetUnits() +for UnitID,UnitData in pairs(TaskUnits)do +local TaskUnit=UnitData +local PlayerName=TaskUnit:GetPlayerName() +if PlayerName~=nil and PlayerName~=""then +self:UnAssignFromUnit(TaskUnit) +end +end +local Mission=self:GetMission() +local CommandCenter=Mission:GetCommandCenter() +CommandCenter:SetMenu() +self:MenuFlashTaskStatus(TaskGroup,false) +end +end +function TASK:HasGroup(FindGroup) +local SetAttackGroup=self:GetGroups() +return SetAttackGroup:FindGroup(FindGroup:GetName()) +end +function TASK:AssignToUnit(TaskUnit) +self:F(TaskUnit:GetName()) +local FsmTemplate=self:GetUnitProcess() +local FsmUnit=self:SetStateMachine(TaskUnit,FsmTemplate:Copy(TaskUnit,self)) +FsmUnit:SetStartState("Planned") +FsmUnit:Accept() +return self +end +function TASK:UnAssignFromUnit(TaskUnit) +self:F(TaskUnit:GetName()) +self:RemoveStateMachine(TaskUnit) +self:RemoveTaskControlMenu(TaskUnit) +return self +end +function TASK:SetTimeOut(Timer) +self:F(Timer) +self.TimeOut=Timer +self:__TimeOut(self.TimeOut) +return self +end +function TASK:MessageToGroups(Message) +self:F({Message=Message}) +local Mission=self:GetMission() +local CC=Mission:GetCommandCenter() +for TaskGroupName,TaskGroup in pairs(self.SetGroup:GetSet())do +TaskGroup=TaskGroup +if TaskGroup:IsAlive()==true then +CC:MessageToGroup(Message,TaskGroup,TaskGroup:GetName()) +end +end +end +function TASK:SendBriefingToAssignedGroups() +self:F2() +for TaskGroupName,TaskGroup in pairs(self.SetGroup:GetSet())do +if TaskGroup:IsAlive()then +if self:IsGroupAssigned(TaskGroup)then +TaskGroup:Message(self.TaskBriefing,60) +end +end +end +end +function TASK:UnAssignFromGroups() +self:F2() +for TaskGroupName,TaskGroup in pairs(self.SetGroup:GetSet())do +if TaskGroup:IsAlive()==true then +if self:IsGroupAssigned(TaskGroup)then +self:UnAssignFromGroup(TaskGroup) +end +end +end +end +function TASK:HasAliveUnits() +self:F() +for TaskGroupID,TaskGroup in pairs(self.SetGroup:GetSet())do +if TaskGroup:IsAlive()==true then +if self:IsStateAssigned()then +if self:IsGroupAssigned(TaskGroup)then +for TaskUnitID,TaskUnit in pairs(TaskGroup:GetUnits())do +if TaskUnit:IsAlive()then +self:T({HasAliveUnits=true}) +return true +end +end +end +end +end +end +self:T({HasAliveUnits=false}) +return false +end +function TASK:SetMenu(MenuTime) +self:F({self:GetName(),MenuTime}) +for TaskGroupID,TaskGroupData in pairs(self.SetGroup:GetSet())do +local TaskGroup=TaskGroupData +if TaskGroup:IsAlive()==true and TaskGroup:GetPlayerNames()then +local Mission=self:GetMission() +local MissionMenu=Mission:GetMenu(TaskGroup) +if MissionMenu then +self:SetMenuForGroup(TaskGroup,MenuTime) +end +end +end +end +function TASK:SetMenuForGroup(TaskGroup,MenuTime) +if self:IsStatePlanned()or self:IsStateAssigned()then +self:SetPlannedMenuForGroup(TaskGroup,MenuTime) +if self:IsGroupAssigned(TaskGroup)then +self:SetAssignedMenuForGroup(TaskGroup,MenuTime) +end +end +end +function TASK:SetPlannedMenuForGroup(TaskGroup,MenuTime) +self:F(TaskGroup:GetName()) +local Mission=self:GetMission() +local MissionName=Mission:GetName() +local MissionMenu=Mission:GetMenu(TaskGroup) +local TaskType=self:GetType() +local TaskPlayerCount=self:GetPlayerCount() +local TaskPlayerString=string.format(" (%dp)",TaskPlayerCount) +local TaskText=string.format("%s",self:GetName()) +local TaskName=string.format("%s",self:GetName()) +self.MenuPlanned=self.MenuPlanned or{} +self.MenuPlanned[TaskGroup]=MENU_GROUP_DELAYED:New(TaskGroup,"Join Planned Task",MissionMenu,Mission.MenuReportTasksPerStatus,Mission,TaskGroup,"Planned"):SetTime(MenuTime):SetTag("Tasking") +local TaskTypeMenu=MENU_GROUP_DELAYED:New(TaskGroup,TaskType,self.MenuPlanned[TaskGroup]):SetTime(MenuTime):SetTag("Tasking") +local TaskTypeMenu=MENU_GROUP_DELAYED:New(TaskGroup,TaskText,TaskTypeMenu):SetTime(MenuTime):SetTag("Tasking") +if not Mission:IsGroupAssigned(TaskGroup)then +local JoinTaskMenu=MENU_GROUP_COMMAND_DELAYED:New(TaskGroup,string.format("Join Task"),TaskTypeMenu,self.MenuAssignToGroup,self,TaskGroup):SetTime(MenuTime):SetTag("Tasking") +local MarkTaskMenu=MENU_GROUP_COMMAND_DELAYED:New(TaskGroup,string.format("Mark Task Location on Map"),TaskTypeMenu,self.MenuMarkToGroup,self,TaskGroup):SetTime(MenuTime):SetTag("Tasking") +end +local ReportTaskMenu=MENU_GROUP_COMMAND_DELAYED:New(TaskGroup,string.format("Report Task Details"),TaskTypeMenu,self.MenuTaskStatus,self,TaskGroup):SetTime(MenuTime):SetTag("Tasking") +return self +end +function TASK:SetAssignedMenuForGroup(TaskGroup,MenuTime) +self:F({TaskGroup:GetName(),MenuTime}) +local TaskType=self:GetType() +local TaskPlayerCount=self:GetPlayerCount() +local TaskPlayerString=string.format(" (%dp)",TaskPlayerCount) +local TaskText=string.format("%s%s",self:GetName(),TaskPlayerString) +local TaskName=string.format("%s",self:GetName()) +for UnitName,TaskUnit in pairs(TaskGroup:GetPlayerUnits())do +local TaskUnit=TaskUnit +if TaskUnit then +local MenuControl=self:GetTaskControlMenu(TaskUnit) +local TaskControl=MENU_GROUP:New(TaskGroup,"Control Task",MenuControl):SetTime(MenuTime):SetTag("Tasking") +if self:IsStateAssigned()then +local TaskMenu=MENU_GROUP_COMMAND:New(TaskGroup,string.format("Abort Task"),TaskControl,self.MenuTaskAbort,self,TaskGroup):SetTime(MenuTime):SetTag("Tasking") +end +local MarkMenu=MENU_GROUP_COMMAND:New(TaskGroup,string.format("Mark Task Location on Map"),TaskControl,self.MenuMarkToGroup,self,TaskGroup):SetTime(MenuTime):SetTag("Tasking") +local TaskTypeMenu=MENU_GROUP_COMMAND:New(TaskGroup,string.format("Report Task Details"),TaskControl,self.MenuTaskStatus,self,TaskGroup):SetTime(MenuTime):SetTag("Tasking") +if not self.FlashTaskStatus then +local TaskFlashStatusMenu=MENU_GROUP_COMMAND:New(TaskGroup,string.format("Flash Task Details"),TaskControl,self.MenuFlashTaskStatus,self,TaskGroup,true):SetTime(MenuTime):SetTag("Tasking") +else +local TaskFlashStatusMenu=MENU_GROUP_COMMAND:New(TaskGroup,string.format("Stop Flash Task Details"),TaskControl,self.MenuFlashTaskStatus,self,TaskGroup,nil):SetTime(MenuTime):SetTag("Tasking") +end +end +end +return self +end +function TASK:RemoveMenu(MenuTime) +self:F({self:GetName(),MenuTime}) +for TaskGroupID,TaskGroup in pairs(self.SetGroup:GetSet())do +if TaskGroup:IsAlive()==true then +local TaskGroup=TaskGroup +if TaskGroup:IsAlive()==true and TaskGroup:GetPlayerNames()then +self:RefreshMenus(TaskGroup,MenuTime) +end +end +end +end +function TASK:RefreshMenus(TaskGroup,MenuTime) +self:F({TaskGroup:GetName(),MenuTime}) +local Mission=self:GetMission() +local MissionName=Mission:GetName() +local MissionMenu=Mission:GetMenu(TaskGroup) +local TaskName=self:GetName() +self.MenuPlanned=self.MenuPlanned or{} +local PlannedMenu=self.MenuPlanned[TaskGroup] +self.MenuAssigned=self.MenuAssigned or{} +local AssignedMenu=self.MenuAssigned[TaskGroup] +if PlannedMenu then +self.MenuPlanned[TaskGroup]=PlannedMenu:Remove(MenuTime,"Tasking") +PlannedMenu:Set() +end +if AssignedMenu then +self.MenuAssigned[TaskGroup]=AssignedMenu:Remove(MenuTime,"Tasking") +AssignedMenu:Set() +end +end +function TASK:RemoveAssignedMenuForGroup(TaskGroup) +self:F() +local Mission=self:GetMission() +local MissionName=Mission:GetName() +local MissionMenu=Mission:GetMenu(TaskGroup) +if MissionMenu then +MissionMenu:RemoveSubMenus() +end +end +function TASK:MenuAssignToGroup(TaskGroup) +self:F("Join Task menu selected") +self:AssignToGroup(TaskGroup) +end +function TASK:MenuMarkToGroup(TaskGroup) +self:F() +self:UpdateTaskInfo(self.DetectedItem) +local TargetCoordinates=self.TaskInfo:GetData("Coordinates") +if TargetCoordinates then +for TargetCoordinateID,TargetCoordinate in pairs(TargetCoordinates)do +local Report=REPORT:New():SetIndent(0) +self.TaskInfo:Report(Report,"M",TaskGroup,self) +local MarkText=Report:Text(", ") +self:F({Coordinate=TargetCoordinate,MarkText=MarkText}) +TargetCoordinate:MarkToGroup(MarkText,TaskGroup) +end +else +local TargetCoordinate=self.TaskInfo:GetData("Coordinate") +if TargetCoordinate then +local Report=REPORT:New():SetIndent(0) +self.TaskInfo:Report(Report,"M",TaskGroup,self) +local MarkText=Report:Text(", ") +self:F({Coordinate=TargetCoordinate,MarkText=MarkText}) +TargetCoordinate:MarkToGroup(MarkText,TaskGroup) +end +end +end +function TASK:MenuTaskStatus(TaskGroup) +if TaskGroup:IsAlive()then +local ReportText=self:ReportDetails(TaskGroup) +self:T(ReportText) +self:GetMission():GetCommandCenter():MessageTypeToGroup(ReportText,TaskGroup,MESSAGE.Type.Detailed) +end +end +function TASK:MenuFlashTaskStatus(TaskGroup,Flash) +self.FlashTaskStatus=Flash +if self.FlashTaskStatus then +self.FlashTaskScheduler,self.FlashTaskScheduleID=SCHEDULER:New(self,self.MenuTaskStatus,{TaskGroup},0,60) +else +if self.FlashTaskScheduler then +self.FlashTaskScheduler:Stop(self.FlashTaskScheduleID) +self.FlashTaskScheduler=nil +self.FlashTaskScheduleID=nil +end +end +end +function TASK:MenuTaskAbort(TaskGroup) +self:AbortGroup(TaskGroup) +end +function TASK:GetTaskName() +return self.TaskName +end +function TASK:GetTaskBriefing() +return self.TaskBriefing +end +function TASK:GetProcessTemplate(ProcessName) +local ProcessTemplate=self.ProcessClasses[ProcessName] +return ProcessTemplate +end +function TASK:FailProcesses(TaskUnitName) +for ProcessID,ProcessData in pairs(self.Processes[TaskUnitName])do +local Process=ProcessData +Process.Fsm:Fail() +end +end +function TASK:SetStateMachine(TaskUnit,Fsm) +self:F2({TaskUnit,self.Fsm[TaskUnit]~=nil,Fsm:GetClassNameAndID()}) +self.Fsm[TaskUnit]=Fsm +return Fsm +end +function TASK:GetStateMachine(TaskUnit) +self:F2({TaskUnit,self.Fsm[TaskUnit]~=nil}) +return self.Fsm[TaskUnit] +end +function TASK:RemoveStateMachine(TaskUnit) +self:F({TaskUnit=TaskUnit:GetName(),HasFsm=(self.Fsm[TaskUnit]~=nil)}) +if self.Fsm[TaskUnit]then +self.Fsm[TaskUnit]:Remove() +self.Fsm[TaskUnit]=nil +end +collectgarbage() +self:F("Garbage Collected, Processes should be finalized now ...") +end +function TASK:HasStateMachine(TaskUnit) +self:F({TaskUnit,self.Fsm[TaskUnit]~=nil}) +return(self.Fsm[TaskUnit]~=nil) +end +function TASK:GetScoring() +return self.Mission:GetScoring() +end +function TASK:GetTaskIndex() +local TaskType=self:GetType() +local TaskName=self:GetName() +return TaskType.."."..TaskName +end +function TASK:SetName(TaskName) +self.TaskName=TaskName +end +function TASK:GetName() +return self.TaskName +end +function TASK:SetType(TaskType) +self.TaskType=TaskType +end +function TASK:GetType() +return self.TaskType +end +function TASK:SetID(TaskID) +self.TaskID=TaskID +end +function TASK:GetID() +return self.TaskID +end +function TASK:StateSuccess() +self:SetState(self,"State","Success") +return self +end +function TASK:IsStateSuccess() +return self:Is("Success") +end +function TASK:StateFailed() +self:SetState(self,"State","Failed") +return self +end +function TASK:IsStateFailed() +return self:Is("Failed") +end +function TASK:StatePlanned() +self:SetState(self,"State","Planned") +return self +end +function TASK:IsStatePlanned() +return self:Is("Planned") +end +function TASK:StateAborted() +self:SetState(self,"State","Aborted") +return self +end +function TASK:IsStateAborted() +return self:Is("Aborted") +end +function TASK:StateCancelled() +self:SetState(self,"State","Cancelled") +return self +end +function TASK:IsStateCancelled() +return self:Is("Cancelled") +end +function TASK:StateAssigned() +self:SetState(self,"State","Assigned") +return self +end +function TASK:IsStateAssigned() +return self:Is("Assigned") +end +function TASK:StateHold() +self:SetState(self,"State","Hold") +return self +end +function TASK:IsStateHold() +return self:Is("Hold") +end +function TASK:StateReplanned() +self:SetState(self,"State","Replanned") +return self +end +function TASK:IsStateReplanned() +return self:Is("Replanned") +end +function TASK:GetStateString() +return self:GetState(self,"State") +end +function TASK:SetBriefing(TaskBriefing) +self:F(TaskBriefing) +self.TaskBriefing=TaskBriefing +return self +end +function TASK:GetBriefing() +return self.TaskBriefing +end +function TASK:onenterAssigned(From,Event,To,PlayerUnit,PlayerName) +if From~="Assigned"then +local PlayerNames=self:GetPlayerNames() +local PlayerText=REPORT:New() +for PlayerName,TaskName in pairs(PlayerNames)do +PlayerText:Add(PlayerName) +end +self:GetMission():GetCommandCenter():MessageToCoalition("Task "..self:GetName().." is assigned to players "..PlayerText:Text(",")..". Good Luck!") +self:SetGoalTotal() +if self.Dispatcher then +self:F("Firing Assign event ") +self.Dispatcher:Assign(self,PlayerUnit,PlayerName) +end +self:GetMission():__Start(1) +self:__Goal(-10,PlayerUnit,PlayerName) +self:SetMenu() +self:F({"--> Task Assigned",TaskName=self:GetName(),Mission=self:GetMission():GetName()}) +self:F({"--> Task Player Names",PlayerNames=PlayerNames}) +end +end +function TASK:onenterSuccess(From,Event,To) +self:F({"<-> Task Replanned",TaskName=self:GetName(),Mission=self:GetMission():GetName()}) +self:F({"<-> Task Player Names",PlayerNames=self:GetPlayerNames()}) +self:GetMission():GetCommandCenter():MessageToCoalition("Task "..self:GetName().." is successful! Good job!") +self:UnAssignFromGroups() +self:GetMission():__MissionGoals(1) +end +function TASK:onenterAborted(From,Event,To) +self:F({"<-- Task Aborted",TaskName=self:GetName(),Mission=self:GetMission():GetName()}) +self:F({"<-- Task Player Names",PlayerNames=self:GetPlayerNames()}) +if From~="Aborted"then +self:GetMission():GetCommandCenter():MessageToCoalition("Task "..self:GetName().." has been aborted! Task may be replanned.") +self:__Replan(5) +self:SetMenu() +end +end +function TASK:onenterCancelled(From,Event,To) +self:F({"<-- Task Cancelled",TaskName=self:GetName(),Mission=self:GetMission():GetName()}) +self:F({"<-- Player Names",PlayerNames=self:GetPlayerNames()}) +if From~="Cancelled"then +self:GetMission():GetCommandCenter():MessageToCoalition("Task "..self:GetName().." has been cancelled! The tactical situation has changed.") +self:UnAssignFromGroups() +self:SetMenu() +end +end +function TASK:onafterReplan(From,Event,To) +self:F({"Task Replanned",TaskName=self:GetName(),Mission=self:GetMission():GetName()}) +self:F({"Task Player Names",PlayerNames=self:GetPlayerNames()}) +self:GetMission():GetCommandCenter():MessageToCoalition("Replanning Task "..self:GetName()..".") +self:SetMenu() +end +function TASK:onenterFailed(From,Event,To) +self:F({"Task Failed",TaskName=self:GetName(),Mission=self:GetMission():GetName()}) +self:F({"Task Player Names",PlayerNames=self:GetPlayerNames()}) +self:GetMission():GetCommandCenter():MessageToCoalition("Task "..self:GetName().." has failed!") +self:UnAssignFromGroups() +end +function TASK:onstatechange(From,Event,To) +if self:IsTrace()then +end +if self.Scores[To]then +local Scoring=self:GetScoring() +if Scoring then +self:F({self.Scores[To].ScoreText,self.Scores[To].Score}) +Scoring:_AddMissionScore(self.Mission,self.Scores[To].ScoreText,self.Scores[To].Score) +end +end +end +function TASK:onenterPlanned(From,Event,To) +if not self.TimeOut==0 then +self.__TimeOut(self.TimeOut) +end +end +function TASK:onbeforeTimeOut(From,Event,To) +if From=="Planned"then +self:RemoveMenu() +return true +end +return false +end +do +function TASK:SetGoal(Goal) +self.Goal=Goal +end +function TASK:GetGoal() +return self.Goal +end +function TASK:SetDispatcher(Dispatcher) +self.Dispatcher=Dispatcher +end +function TASK:SetDetection(Detection,DetectedItem) +self:F({DetectedItem,Detection}) +self.Detection=Detection +self.DetectedItem=DetectedItem +end +end +do +function TASK:ReportSummary(ReportGroup) +self:UpdateTaskInfo(self.DetectedItem) +local Report=REPORT:New() +Report:Add("Task "..self:GetName()) +Report:Add("State: <"..self:GetState()..">") +self.TaskInfo:Report(Report,"S",ReportGroup,self) +return Report:Text(', ') +end +function TASK:ReportOverview(ReportGroup) +self:UpdateTaskInfo(self.DetectedItem) +local TaskName=self:GetName() +local Report=REPORT:New() +self.TaskInfo:Report(Report,"O",ReportGroup,self) +return Report:Text() +end +function TASK:GetPlayerCount() +local PlayerCount=0 +for TaskGroupID,PlayerGroup in pairs(self:GetGroups():GetSet())do +local PlayerGroup=PlayerGroup +if PlayerGroup:IsAlive()==true then +if self:IsGroupAssigned(PlayerGroup)then +local PlayerNames=PlayerGroup:GetPlayerNames() +PlayerCount=PlayerCount+((PlayerNames)and#PlayerNames or 0) +end +end +end +return PlayerCount +end +function TASK:GetPlayerNames() +local PlayerNameMap={} +for TaskGroupID,PlayerGroup in pairs(self:GetGroups():GetSet())do +local PlayerGroup=PlayerGroup +if PlayerGroup:IsAlive()==true then +if self:IsGroupAssigned(PlayerGroup)then +local PlayerNames=PlayerGroup:GetPlayerNames() +for PlayerNameID,PlayerName in pairs(PlayerNames or{})do +PlayerNameMap[PlayerName]=PlayerGroup +end +end +end +end +return PlayerNameMap +end +function TASK:ReportDetails(ReportGroup) +self:UpdateTaskInfo(self.DetectedItem) +local Report=REPORT:New():SetIndent(3) +local Name=self:GetName() +local Status="<"..self:GetState()..">" +Report:Add("Task "..Name.." - "..Status.." - Detailed Report") +local PlayerNames=self:GetPlayerNames() +local PlayerReport=REPORT:New() +for PlayerName,PlayerGroup in pairs(PlayerNames)do +PlayerReport:Add("Players group "..PlayerGroup:GetCallsign()..": "..PlayerName) +end +local Players=PlayerReport:Text() +if Players~=""then +Report:AddIndent("Players assigned:","-") +Report:AddIndent(Players) +end +self.TaskInfo:Report(Report,"D",ReportGroup,self) +return Report:Text() +end +end +do +function TASK:AddProgress(PlayerName,ProgressText,ProgressTime,ProgressPoints) +self.TaskProgress=self.TaskProgress or{} +self.TaskProgress[ProgressTime]=self.TaskProgress[ProgressTime]or{} +self.TaskProgress[ProgressTime].PlayerName=PlayerName +self.TaskProgress[ProgressTime].ProgressText=ProgressText +self.TaskProgress[ProgressTime].ProgressPoints=ProgressPoints +self:GetMission():AddPlayerName(PlayerName) +return self +end +function TASK:GetPlayerProgress(PlayerName) +local ProgressPlayer=0 +for ProgressTime,ProgressData in pairs(self.TaskProgress)do +if PlayerName==ProgressData.PlayerName then +ProgressPlayer=ProgressPlayer+ProgressData.ProgressPoints +end +end +return ProgressPlayer +end +function TASK:SetScoreOnProgress(PlayerName,Score,TaskUnit) +self:F({PlayerName,Score,TaskUnit}) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +ProcessUnit:AddScoreProcess("Engaging","Account","AccountPlayer","Player "..PlayerName.." has achieved progress.",Score) +return self +end +function TASK:SetScoreOnSuccess(PlayerName,Score,TaskUnit) +self:F({PlayerName,Score,TaskUnit}) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +ProcessUnit:AddScore("Success","The task is a success!",Score) +return self +end +function TASK:SetScoreOnFail(PlayerName,Penalty,TaskUnit) +self:F({PlayerName,Penalty,TaskUnit}) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +ProcessUnit:AddScore("Failed","The task is a failure!",Penalty) +return self +end +end +do +function TASK:InitTaskControlMenu(TaskUnit) +self.TaskControlMenuTime=timer.getTime() +return self.TaskControlMenuTime +end +function TASK:GetTaskControlMenu(TaskUnit,TaskName) +TaskName=TaskName or"" +local TaskGroup=TaskUnit:GetGroup() +local TaskPlayerCount=TaskGroup:GetPlayerCount() +if TaskPlayerCount<=1 then +self.TaskControlMenu=MENU_GROUP:New(TaskUnit:GetGroup(),"Task "..self:GetName().." control"):SetTime(self.TaskControlMenuTime) +else +self.TaskControlMenu=MENU_GROUP:New(TaskUnit:GetGroup(),"Task "..self:GetName().." control for "..TaskUnit:GetPlayerName()):SetTime(self.TaskControlMenuTime) +end +return self.TaskControlMenu +end +function TASK:RemoveTaskControlMenu(TaskUnit) +if self.TaskControlMenu then +self.TaskControlMenu:Remove() +self.TaskControlMenu=nil +end +end +function TASK:RefreshTaskControlMenu(TaskUnit,MenuTime,MenuTag) +if self.TaskControlMenu then +self.TaskControlMenu:Remove(MenuTime,MenuTag) +end +end +end +TASKINFO={ +ClassName="TASKINFO", +} +TASKINFO.Detail="" +function TASKINFO:New(Task) +local self=BASE:Inherit(self,BASE:New()) +self.Task=Task +self.VolatileInfo=SET_BASE:New() +self.PersistentInfo=SET_BASE:New() +self.Info=self.VolatileInfo +return self +end +function TASKINFO:AddInfo(Key,Data,Order,Detail,Keep,ShowKey,Type) +self.VolatileInfo:Add(Key,{Data=Data,Order=Order,Detail=Detail,ShowKey=ShowKey,Type=Type}) +if Keep==true then +self.PersistentInfo:Add(Key,{Data=Data,Order=Order,Detail=Detail,ShowKey=ShowKey,Type=Type}) +end +return self +end +function TASKINFO:GetInfo(Key) +local Object=self:Get(Key) +return Object.Data,Object.Order,Object.Detail +end +function TASKINFO:GetData(Key) +local Object=self.Info:Get(Key) +return Object and Object.Data +end +function TASKINFO:AddText(Key,Text,Order,Detail,Keep) +self:AddInfo(Key,Text,Order,Detail,Keep) +return self +end +function TASKINFO:AddTaskName(Order,Detail,Keep) +self:AddInfo("TaskName",self.Task:GetName(),Order,Detail,Keep) +return self +end +function TASKINFO:AddCoordinate(Coordinate,Order,Detail,Keep,ShowKey,Name) +self:AddInfo(Name or"Coordinate",Coordinate,Order,Detail,Keep,ShowKey,"Coordinate") +return self +end +function TASKINFO:GetCoordinate(Name) +return self:GetData(Name or"Coordinate") +end +function TASKINFO:AddCoordinates(Coordinates,Order,Detail,Keep) +self:AddInfo("Coordinates",Coordinates,Order,Detail,Keep) +return self +end +function TASKINFO:AddThreat(ThreatText,ThreatLevel,Order,Detail,Keep) +self:AddInfo("Threat"," ["..string.rep("■",ThreatLevel)..string.rep("□",10-ThreatLevel).."]:"..ThreatText,Order,Detail,Keep) +return self +end +function TASKINFO:GetThreat() +self:GetInfo("Threat") +return self +end +function TASKINFO:AddTargetCount(TargetCount,Order,Detail,Keep) +self:AddInfo("Counting",string.format("%d",TargetCount),Order,Detail,Keep) +return self +end +function TASKINFO:AddTargets(TargetCount,TargetTypes,Order,Detail,Keep) +self:AddInfo("Targets",string.format("%d of %s",TargetCount,TargetTypes),Order,Detail,Keep) +return self +end +function TASKINFO:GetTargets() +self:GetInfo("Targets") +return self +end +function TASKINFO:AddQFEAtCoordinate(Coordinate,Order,Detail,Keep) +self:AddInfo("QFE",Coordinate,Order,Detail,Keep) +return self +end +function TASKINFO:AddTemperatureAtCoordinate(Coordinate,Order,Detail,Keep) +self:AddInfo("Temperature",Coordinate,Order,Detail,Keep) +return self +end +function TASKINFO:AddWindAtCoordinate(Coordinate,Order,Detail,Keep) +self:AddInfo("Wind",Coordinate,Order,Detail,Keep) +return self +end +function TASKINFO:AddCargo(Cargo,Order,Detail,Keep) +self:AddInfo("Cargo",Cargo,Order,Detail,Keep) +return self +end +function TASKINFO:AddCargoSet(SetCargo,Order,Detail,Keep) +local CargoReport=REPORT:New() +CargoReport:Add("") +SetCargo:ForEachCargo( +function(Cargo) +CargoReport:Add(string.format(' - %s (%s) %s - status %s ',Cargo:GetName(),Cargo:GetType(),Cargo:GetTransportationMethod(),Cargo:GetCurrentState())) +end +) +self:AddInfo("Cargo",CargoReport:Text(),Order,Detail,Keep) +return self +end +function TASKINFO:Report(Report,Detail,ReportGroup,Task) +local Line=0 +local LineReport=REPORT:New() +if not self.Task:IsStatePlanned()and not self.Task:IsStateAssigned()then +self.Info=self.PersistentInfo +end +for Key,Data in UTILS.spairs(self.Info.Set,function(t,a,b)return t[a].Order0 then +local TargetSetUnit=SET_UNIT:New() +TargetSetUnit:SetDatabase(DetectedSet) +TargetSetUnit:FilterHasSEAD() +TargetSetUnit:FilterOnce() +return TargetSetUnit +end +return nil +end +function TASK_A2G_DISPATCHER:EvaluateCAS(DetectedItem) +self:F({DetectedItem.ItemID}) +local DetectedSet=DetectedItem.Set +local DetectedZone=DetectedItem.Zone +local GroundUnitCount=DetectedSet:HasGroundUnits() +local FriendliesNearBy=self.Detection:IsFriendliesNearBy(DetectedItem,Unit.Category.GROUND_UNIT) +local RadarCount=DetectedSet:HasSEAD() +if RadarCount==0 and GroundUnitCount>0 and FriendliesNearBy==true then +local TargetSetUnit=SET_UNIT:New() +TargetSetUnit:SetDatabase(DetectedSet) +TargetSetUnit:FilterOnce() +return TargetSetUnit +end +return nil +end +function TASK_A2G_DISPATCHER:EvaluateBAI(DetectedItem,FriendlyCoalition) +self:F({DetectedItem.ItemID}) +local DetectedSet=DetectedItem.Set +local DetectedZone=DetectedItem.Zone +local GroundUnitCount=DetectedSet:HasGroundUnits() +local FriendliesNearBy=self.Detection:IsFriendliesNearBy(DetectedItem,Unit.Category.GROUND_UNIT) +local RadarCount=DetectedSet:HasSEAD() +if RadarCount==0 and GroundUnitCount>0 and FriendliesNearBy==false then +local TargetSetUnit=SET_UNIT:New() +TargetSetUnit:SetDatabase(DetectedSet) +TargetSetUnit:FilterOnce() +return TargetSetUnit +end +return nil +end +function TASK_A2G_DISPATCHER:RemoveTask(TaskIndex) +self.Mission:RemoveTask(self.Tasks[TaskIndex]) +self.Tasks[TaskIndex]=nil +end +function TASK_A2G_DISPATCHER:EvaluateRemoveTask(Mission,Task,TaskIndex,DetectedItemChanged) +if Task then +if(Task:IsStatePlanned()and DetectedItemChanged==true)or Task:IsStateCancelled()then +self:RemoveTask(TaskIndex) +end +end +return Task +end +function TASK_A2G_DISPATCHER:ProcessDetected(Detection) +self:F() +local AreaMsg={} +local TaskMsg={} +local ChangeMsg={} +local Mission=self.Mission +if Mission:IsIDLE()or Mission:IsENGAGED()then +local TaskReport=REPORT:New() +for TaskIndex,TaskData in pairs(self.Tasks)do +local Task=TaskData +if Task:IsStatePlanned()then +local DetectedItem=Detection:GetDetectedItemByIndex(TaskIndex) +if not DetectedItem then +local TaskText=Task:GetName() +for TaskGroupID,TaskGroup in pairs(self.SetGroup:GetSet())do +if self.FlashNewTask then +Mission:GetCommandCenter():MessageToGroup(string.format("Obsolete A2G task %s for %s removed.",TaskText,Mission:GetShortText()),TaskGroup) +end +end +Task=self:RemoveTask(TaskIndex) +end +end +end +for DetectedItemID,DetectedItem in pairs(Detection:GetDetectedItems())do +local DetectedItem=DetectedItem +local DetectedSet=DetectedItem.Set +local DetectedZone=DetectedItem.Zone +local DetectedItemID=DetectedItem.ID +local TaskIndex=DetectedItem.Index +local DetectedItemChanged=DetectedItem.Changed +self:F({DetectedItemChanged=DetectedItemChanged,DetectedItemID=DetectedItemID,TaskIndex=TaskIndex}) +local Task=self.Tasks[TaskIndex] +if Task then +if Task:IsStateAssigned()then +if DetectedItemChanged==true then +local TargetsReport=REPORT:New() +local TargetSetUnit=self:EvaluateSEAD(DetectedItem) +if TargetSetUnit then +if Task:IsInstanceOf(TASK_A2G_SEAD)then +Task:SetTargetSetUnit(TargetSetUnit) +Task:SetDetection(Detection,DetectedItem) +Task:UpdateTaskInfo(DetectedItem) +TargetsReport:Add(Detection:GetChangeText(DetectedItem)) +else +Task:Cancel() +end +else +local TargetSetUnit=self:EvaluateCAS(DetectedItem) +if TargetSetUnit then +if Task:IsInstanceOf(TASK_A2G_CAS)then +Task:SetTargetSetUnit(TargetSetUnit) +Task:SetDetection(Detection,DetectedItem) +Task:UpdateTaskInfo(DetectedItem) +TargetsReport:Add(Detection:GetChangeText(DetectedItem)) +else +Task:Cancel() +Task=self:RemoveTask(TaskIndex) +end +else +local TargetSetUnit=self:EvaluateBAI(DetectedItem) +if TargetSetUnit then +if Task:IsInstanceOf(TASK_A2G_BAI)then +Task:SetTargetSetUnit(TargetSetUnit) +Task:SetDetection(Detection,DetectedItem) +Task:UpdateTaskInfo(DetectedItem) +TargetsReport:Add(Detection:GetChangeText(DetectedItem)) +else +Task:Cancel() +Task=self:RemoveTask(TaskIndex) +end +end +end +end +for TaskGroupID,TaskGroup in pairs(self.SetGroup:GetSet())do +local TargetsText=TargetsReport:Text(", ") +if(Mission:IsGroupAssigned(TaskGroup))and TargetsText~=""and self.FlashNewTask then +Mission:GetCommandCenter():MessageToGroup(string.format("Task %s has change of targets:\n %s",Task:GetName(),TargetsText),TaskGroup) +end +end +end +end +end +if Task then +if Task:IsStatePlanned()then +if DetectedItemChanged==true then +if Task:IsInstanceOf(TASK_A2G_SEAD)then +local TargetSetUnit=self:EvaluateSEAD(DetectedItem) +if TargetSetUnit then +Task:SetTargetSetUnit(TargetSetUnit) +Task:SetDetection(Detection,DetectedItem) +Task:UpdateTaskInfo(DetectedItem) +else +Task:Cancel() +Task=self:RemoveTask(TaskIndex) +end +else +if Task:IsInstanceOf(TASK_A2G_CAS)then +local TargetSetUnit=self:EvaluateCAS(DetectedItem) +if TargetSetUnit then +Task:SetTargetSetUnit(TargetSetUnit) +Task:SetDetection(Detection,DetectedItem) +Task:UpdateTaskInfo(DetectedItem) +else +Task:Cancel() +Task=self:RemoveTask(TaskIndex) +end +else +if Task:IsInstanceOf(TASK_A2G_BAI)then +local TargetSetUnit=self:EvaluateBAI(DetectedItem) +if TargetSetUnit then +Task:SetTargetSetUnit(TargetSetUnit) +Task:SetDetection(Detection,DetectedItem) +Task:UpdateTaskInfo(DetectedItem) +else +Task:Cancel() +Task=self:RemoveTask(TaskIndex) +end +else +Task:Cancel() +Task=self:RemoveTask(TaskIndex) +end +end +end +end +end +end +if not Task then +local TargetSetUnit=self:EvaluateSEAD(DetectedItem) +if TargetSetUnit then +Task=TASK_A2G_SEAD:New(Mission,self.SetGroup,string.format("SEAD.%03d",DetectedItemID),TargetSetUnit) +DetectedItem.DesignateMenuName=string.format("SEAD.%03d",DetectedItemID) +Task:SetDetection(Detection,DetectedItem) +end +if not Task then +local TargetSetUnit=self:EvaluateCAS(DetectedItem) +if TargetSetUnit then +Task=TASK_A2G_CAS:New(Mission,self.SetGroup,string.format("CAS.%03d",DetectedItemID),TargetSetUnit) +DetectedItem.DesignateMenuName=string.format("CAS.%03d",DetectedItemID) +Task:SetDetection(Detection,DetectedItem) +end +if not Task then +local TargetSetUnit=self:EvaluateBAI(DetectedItem,self.Mission:GetCommandCenter():GetPositionable():GetCoalition()) +if TargetSetUnit then +Task=TASK_A2G_BAI:New(Mission,self.SetGroup,string.format("BAI.%03d",DetectedItemID),TargetSetUnit) +DetectedItem.DesignateMenuName=string.format("BAI.%03d",DetectedItemID) +Task:SetDetection(Detection,DetectedItem) +end +end +end +if Task then +self.Tasks[TaskIndex]=Task +Task:SetTargetZone(DetectedZone) +Task:SetDispatcher(self) +Task:UpdateTaskInfo(DetectedItem) +Mission:AddTask(Task) +function Task.OnEnterSuccess(Task,From,Event,To) +self:Success(Task) +end +function Task.OnEnterCancelled(Task,From,Event,To) +self:Cancelled(Task) +end +function Task.OnEnterFailed(Task,From,Event,To) +self:Failed(Task) +end +function Task.OnEnterAborted(Task,From,Event,To) +self:Aborted(Task) +end +TaskReport:Add(Task:GetName()) +else +self:F("This should not happen") +end +end +Detection:AcceptChanges(DetectedItem) +end +Mission:GetCommandCenter():SetMenu() +local TaskText=TaskReport:Text(", ") +for TaskGroupID,TaskGroup in pairs(self.SetGroup:GetSet())do +if(not Mission:IsGroupAssigned(TaskGroup))and TaskText~=""and self.FlashNewTask then +Mission:GetCommandCenter():MessageToGroup(string.format("%s has tasks %s. Subscribe to a task using the radio menu.",Mission:GetShortText(),TaskText),TaskGroup) +end +end +end +return true +end +end +do +TASK_A2G={ +ClassName="TASK_A2G", +} +function TASK_A2G:New(Mission,SetGroup,TaskName,TargetSetUnit,TaskType,TaskBriefing) +local self=BASE:Inherit(self,TASK:New(Mission,SetGroup,TaskName,TaskType,TaskBriefing)) +self:F() +self.TargetSetUnit=TargetSetUnit +self.TaskType=TaskType +local Fsm=self:GetUnitProcess() +Fsm:AddTransition("Assigned","RouteToRendezVous","RoutingToRendezVous") +Fsm:AddProcess("RoutingToRendezVous","RouteToRendezVousPoint",ACT_ROUTE_POINT:New(),{Arrived="ArriveAtRendezVous"}) +Fsm:AddProcess("RoutingToRendezVous","RouteToRendezVousZone",ACT_ROUTE_ZONE:New(),{Arrived="ArriveAtRendezVous"}) +Fsm:AddTransition({"Arrived","RoutingToRendezVous"},"ArriveAtRendezVous","ArrivedAtRendezVous") +Fsm:AddTransition({"ArrivedAtRendezVous","HoldingAtRendezVous"},"Engage","Engaging") +Fsm:AddTransition({"ArrivedAtRendezVous","HoldingAtRendezVous"},"HoldAtRendezVous","HoldingAtRendezVous") +Fsm:AddProcess("Engaging","Account",ACT_ACCOUNT_DEADS:New(),{}) +Fsm:AddTransition("Engaging","RouteToTarget","Engaging") +Fsm:AddProcess("Engaging","RouteToTargetZone",ACT_ROUTE_ZONE:New(),{}) +Fsm:AddProcess("Engaging","RouteToTargetPoint",ACT_ROUTE_POINT:New(),{}) +Fsm:AddTransition("Engaging","RouteToTargets","Engaging") +Fsm:AddTransition("Rejected","Reject","Aborted") +Fsm:AddTransition("Failed","Fail","Failed") +function Fsm:onafterAssigned(TaskUnit,Task) +self:F({TaskUnit=TaskUnit,Task=Task and Task:GetClassNameAndID()}) +self:RouteToRendezVous() +end +function Fsm:onafterRouteToRendezVous(TaskUnit,Task) +self:F({TaskUnit=TaskUnit,Task=Task and Task:GetClassNameAndID()}) +if Task:GetRendezVousZone(TaskUnit)then +self:__RouteToRendezVousZone(0.1) +else +if Task:GetRendezVousCoordinate(TaskUnit)then +self:__RouteToRendezVousPoint(0.1) +else +self:__ArriveAtRendezVous(0.1) +end +end +end +function Fsm:OnAfterArriveAtRendezVous(TaskUnit,Task) +self:F({TaskUnit=TaskUnit,Task=Task and Task:GetClassNameAndID()}) +self:__Engage(0.1) +end +function Fsm:onafterEngage(TaskUnit,Task) +self:F({self}) +self:__Account(0.1) +self:__RouteToTarget(0.1) +self:__RouteToTargets(-10) +end +function Fsm:onafterRouteToTarget(TaskUnit,Task) +self:F({TaskUnit=TaskUnit,Task=Task and Task:GetClassNameAndID()}) +if Task:GetTargetZone(TaskUnit)then +self:__RouteToTargetZone(0.1) +else +local TargetUnit=Task.TargetSetUnit:GetFirst() +if TargetUnit then +local Coordinate=TargetUnit:GetPointVec3() +self:T({TargetCoordinate=Coordinate,Coordinate:GetX(),Coordinate:GetY(),Coordinate:GetZ()}) +Task:SetTargetCoordinate(Coordinate,TaskUnit) +end +self:__RouteToTargetPoint(0.1) +end +end +function Fsm:onafterRouteToTargets(TaskUnit,Task) +self:F({TaskUnit=TaskUnit,Task=Task and Task:GetClassNameAndID()}) +local TargetUnit=Task.TargetSetUnit:GetFirst() +if TargetUnit then +Task:SetTargetCoordinate(TargetUnit:GetCoordinate(),TaskUnit) +end +self:__RouteToTargets(-10) +end +return self +end +function TASK_A2G:SetTargetSetUnit(TargetSetUnit) +self.TargetSetUnit=TargetSetUnit +end +function TASK_A2G:GetPlannedMenuText() +return self:GetStateString().." - "..self:GetTaskName().." ( "..self.TargetSetUnit:GetUnitTypesText().." )" +end +function TASK_A2G:SetRendezVousCoordinate(RendezVousCoordinate,RendezVousRange,TaskUnit) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +local ActRouteRendezVous=ProcessUnit:GetProcess("RoutingToRendezVous","RouteToRendezVousPoint") +ActRouteRendezVous:SetCoordinate(RendezVousCoordinate) +ActRouteRendezVous:SetRange(RendezVousRange) +end +function TASK_A2G:GetRendezVousCoordinate(TaskUnit) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +local ActRouteRendezVous=ProcessUnit:GetProcess("RoutingToRendezVous","RouteToRendezVousPoint") +return ActRouteRendezVous:GetCoordinate(),ActRouteRendezVous:GetRange() +end +function TASK_A2G:SetRendezVousZone(RendezVousZone,TaskUnit) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +local ActRouteRendezVous=ProcessUnit:GetProcess("RoutingToRendezVous","RouteToRendezVousZone") +ActRouteRendezVous:SetZone(RendezVousZone) +end +function TASK_A2G:GetRendezVousZone(TaskUnit) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +local ActRouteRendezVous=ProcessUnit:GetProcess("RoutingToRendezVous","RouteToRendezVousZone") +return ActRouteRendezVous:GetZone() +end +function TASK_A2G:SetTargetCoordinate(TargetCoordinate,TaskUnit) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +local ActRouteTarget=ProcessUnit:GetProcess("Engaging","RouteToTargetPoint") +ActRouteTarget:SetCoordinate(TargetCoordinate) +end +function TASK_A2G:GetTargetCoordinate(TaskUnit) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +local ActRouteTarget=ProcessUnit:GetProcess("Engaging","RouteToTargetPoint") +return ActRouteTarget:GetCoordinate() +end +function TASK_A2G:SetTargetZone(TargetZone,TaskUnit) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +local ActRouteTarget=ProcessUnit:GetProcess("Engaging","RouteToTargetZone") +ActRouteTarget:SetZone(TargetZone) +end +function TASK_A2G:GetTargetZone(TaskUnit) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +local ActRouteTarget=ProcessUnit:GetProcess("Engaging","RouteToTargetZone") +return ActRouteTarget:GetZone() +end +function TASK_A2G:SetGoalTotal() +self.GoalTotal=self.TargetSetUnit:Count() +end +function TASK_A2G:GetGoalTotal() +return self.GoalTotal +end +function TASK_A2G:ReportOrder(ReportGroup) +self:UpdateTaskInfo(self.DetectedItem) +local Coordinate=self.TaskInfo:GetData("Coordinate") +local Distance=ReportGroup:GetCoordinate():Get2DDistance(Coordinate) +return Distance +end +function TASK_A2G:onafterGoal(TaskUnit,From,Event,To) +local TargetSetUnit=self.TargetSetUnit +if TargetSetUnit:Count()==0 then +self:Success() +end +self:__Goal(-10) +end +function TASK_A2G:UpdateTaskInfo(DetectedItem) +if self:IsStatePlanned()or self:IsStateAssigned()then +local TargetCoordinate=DetectedItem and self.Detection:GetDetectedItemCoordinate(DetectedItem)or self.TargetSetUnit:GetFirst():GetCoordinate() +self.TaskInfo:AddTaskName(0,"MSOD") +self.TaskInfo:AddCoordinate(TargetCoordinate,1,"SOD") +local ThreatLevel,ThreatText +if DetectedItem then +ThreatLevel,ThreatText=self.Detection:GetDetectedItemThreatLevel(DetectedItem) +else +ThreatLevel,ThreatText=self.TargetSetUnit:CalculateThreatLevelA2G() +end +self.TaskInfo:AddThreat(ThreatText,ThreatLevel,10,"MOD",true) +if self.Detection then +local DetectedItemsCount=self.TargetSetUnit:Count() +local ReportTypes=REPORT:New() +local TargetTypes={} +for TargetUnitName,TargetUnit in pairs(self.TargetSetUnit:GetSet())do +local TargetType=self.Detection:GetDetectedUnitTypeName(TargetUnit) +if not TargetTypes[TargetType]then +TargetTypes[TargetType]=TargetType +ReportTypes:Add(TargetType) +end +end +self.TaskInfo:AddTargetCount(DetectedItemsCount,11,"O",true) +self.TaskInfo:AddTargets(DetectedItemsCount,ReportTypes:Text(", "),20,"D",true) +else +local DetectedItemsCount=self.TargetSetUnit:Count() +local DetectedItemsTypes=self.TargetSetUnit:GetTypeNames() +self.TaskInfo:AddTargetCount(DetectedItemsCount,11,"O",true) +self.TaskInfo:AddTargets(DetectedItemsCount,DetectedItemsTypes,20,"D",true) +end +self.TaskInfo:AddQFEAtCoordinate(TargetCoordinate,30,"MOD") +self.TaskInfo:AddTemperatureAtCoordinate(TargetCoordinate,31,"MD") +self.TaskInfo:AddWindAtCoordinate(TargetCoordinate,32,"MD") +end +end +function TASK_A2G:GetAutoAssignPriority(AutoAssignMethod,CommandCenter,TaskGroup) +if AutoAssignMethod==COMMANDCENTER.AutoAssignMethods.Random then +return math.random(1,9) +elseif AutoAssignMethod==COMMANDCENTER.AutoAssignMethods.Distance then +local Coordinate=self.TaskInfo:GetData("Coordinate") +local Distance=Coordinate:Get2DDistance(CommandCenter:GetPositionable():GetCoordinate()) +self:F({Distance=Distance}) +return math.floor(Distance) +elseif AutoAssignMethod==COMMANDCENTER.AutoAssignMethods.Priority then +return 1 +end +return 0 +end +end +do +TASK_A2G_SEAD={ +ClassName="TASK_A2G_SEAD", +} +function TASK_A2G_SEAD:New(Mission,SetGroup,TaskName,TargetSetUnit,TaskBriefing) +local self=BASE:Inherit(self,TASK_A2G:New(Mission,SetGroup,TaskName,TargetSetUnit,"SEAD",TaskBriefing)) +self:F() +Mission:AddTask(self) +self:SetBriefing( +TaskBriefing or +"Execute a Suppression of Enemy Air Defenses." +) +return self +end +function TASK_A2G_SEAD:SetScoreOnProgress(PlayerName,Score,TaskUnit) +self:F({PlayerName,Score,TaskUnit}) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +ProcessUnit:AddScoreProcess("Engaging","Account","AccountForPlayer","Player "..PlayerName.." has SEADed a target.",Score) +return self +end +function TASK_A2G_SEAD:SetScoreOnSuccess(PlayerName,Score,TaskUnit) +self:F({PlayerName,Score,TaskUnit}) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +ProcessUnit:AddScore("Success","All radar emitting targets have been successfully SEADed!",Score) +return self +end +function TASK_A2G_SEAD:SetScoreOnFail(PlayerName,Penalty,TaskUnit) +self:F({PlayerName,Penalty,TaskUnit}) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +ProcessUnit:AddScore("Failed","The SEADing has failed!",Penalty) +return self +end +end +do +TASK_A2G_BAI={ +ClassName="TASK_A2G_BAI", +} +function TASK_A2G_BAI:New(Mission,SetGroup,TaskName,TargetSetUnit,TaskBriefing) +local self=BASE:Inherit(self,TASK_A2G:New(Mission,SetGroup,TaskName,TargetSetUnit,"BAI",TaskBriefing)) +self:F() +Mission:AddTask(self) +self:SetBriefing( +TaskBriefing or +"Execute a Battlefield Air Interdiction of a group of enemy targets." +) +return self +end +function TASK_A2G_BAI:SetScoreOnProgress(PlayerName,Score,TaskUnit) +self:F({PlayerName,Score,TaskUnit}) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +ProcessUnit:AddScoreProcess("Engaging","Account","AccountForPlayer","Player "..PlayerName.." has destroyed a target in Battlefield Air Interdiction (BAI).",Score) +return self +end +function TASK_A2G_BAI:SetScoreOnSuccess(PlayerName,Score,TaskUnit) +self:F({PlayerName,Score,TaskUnit}) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +ProcessUnit:AddScore("Success","All targets have been successfully destroyed! The Battlefield Air Interdiction (BAI) is a success!",Score) +return self +end +function TASK_A2G_BAI:SetScoreOnFail(PlayerName,Penalty,TaskUnit) +self:F({PlayerName,Penalty,TaskUnit}) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +ProcessUnit:AddScore("Failed","The Battlefield Air Interdiction (BAI) has failed!",Penalty) +return self +end +end +do +TASK_A2G_CAS={ +ClassName="TASK_A2G_CAS", +} +function TASK_A2G_CAS:New(Mission,SetGroup,TaskName,TargetSetUnit,TaskBriefing) +local self=BASE:Inherit(self,TASK_A2G:New(Mission,SetGroup,TaskName,TargetSetUnit,"CAS",TaskBriefing)) +self:F() +Mission:AddTask(self) +self:SetBriefing( +TaskBriefing or +"Execute a Close Air Support for a group of enemy targets. ".. +"Beware of friendlies at the vicinity! " +) +return self +end +function TASK_A2G_CAS:SetScoreOnProgress(PlayerName,Score,TaskUnit) +self:F({PlayerName,Score,TaskUnit}) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +ProcessUnit:AddScoreProcess("Engaging","Account","AccountForPlayer","Player "..PlayerName.." has destroyed a target in Close Air Support (CAS).",Score) +return self +end +function TASK_A2G_CAS:SetScoreOnSuccess(PlayerName,Score,TaskUnit) +self:F({PlayerName,Score,TaskUnit}) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +ProcessUnit:AddScore("Success","All targets have been successfully destroyed! The Close Air Support (CAS) was a success!",Score) +return self +end +function TASK_A2G_CAS:SetScoreOnFail(PlayerName,Penalty,TaskUnit) +self:F({PlayerName,Penalty,TaskUnit}) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +ProcessUnit:AddScore("Failed","The Close Air Support (CAS) has failed!",Penalty) +return self +end +end +do +TASK_A2A_DISPATCHER={ +ClassName="TASK_A2A_DISPATCHER", +Mission=nil, +Detection=nil, +Tasks={}, +SweepZones={}, +} +function TASK_A2A_DISPATCHER:New(Mission,SetGroup,Detection) +local self=BASE:Inherit(self,DETECTION_MANAGER:New(SetGroup,Detection)) +self.Detection=Detection +self.Mission=Mission +self.FlashNewTask=false +self.Detection:FilterCategories(Unit.Category.AIRPLANE,Unit.Category.HELICOPTER) +self.Detection:InitDetectRadar(true) +self.Detection:SetRefreshTimeInterval(30) +self:AddTransition("Started","Assign","Started") +self:__Start(5) +return self +end +function TASK_A2A_DISPATCHER:SetEngageRadius(EngageRadius) +self.Detection:SetFriendliesRange(EngageRadius or 100000) +return self +end +function TASK_A2A_DISPATCHER:SetSendMessages(onoff) +self.FlashNewTask=onoff +end +function TASK_A2A_DISPATCHER:EvaluateINTERCEPT(DetectedItem) +self:F({DetectedItem.ItemID}) +local DetectedSet=DetectedItem.Set +local DetectedZone=DetectedItem.Zone +if DetectedItem.IsDetected==true then +local TargetSetUnit=SET_UNIT:New() +TargetSetUnit:SetDatabase(DetectedSet) +TargetSetUnit:FilterOnce() +return TargetSetUnit +end +return nil +end +function TASK_A2A_DISPATCHER:EvaluateSWEEP(DetectedItem) +self:F({DetectedItem.ItemID}) +local DetectedSet=DetectedItem.Set +local DetectedZone=DetectedItem.Zone +if DetectedItem.IsDetected==false then +local TargetSetUnit=SET_UNIT:New() +TargetSetUnit:SetDatabase(DetectedSet) +TargetSetUnit:FilterOnce() +return TargetSetUnit +end +return nil +end +function TASK_A2A_DISPATCHER:EvaluateENGAGE(DetectedItem) +self:F({DetectedItem.ItemID}) +local DetectedSet=DetectedItem.Set +local DetectedZone=DetectedItem.Zone +local PlayersCount,PlayersReport=self:GetPlayerFriendliesNearBy(DetectedItem) +if PlayersCount>0 and DetectedItem.IsDetected==true then +local TargetSetUnit=SET_UNIT:New() +TargetSetUnit:SetDatabase(DetectedSet) +TargetSetUnit:FilterOnce() +return TargetSetUnit +end +return nil +end +function TASK_A2A_DISPATCHER:EvaluateRemoveTask(Mission,Task,Detection,DetectedItem,DetectedItemIndex,DetectedItemChanged) +if Task then +if Task:IsStatePlanned()then +local TaskName=Task:GetName() +local TaskType=TaskName:match("(%u+)%.%d+") +self:T2({TaskType=TaskType}) +local Remove=false +local IsPlayers=Detection:IsPlayersNearBy(DetectedItem) +if TaskType=="ENGAGE"then +if IsPlayers==false then +Remove=true +end +end +if TaskType=="INTERCEPT"then +if IsPlayers==true then +Remove=true +end +if DetectedItem.IsDetected==false then +Remove=true +end +end +if TaskType=="SWEEP"then +if DetectedItem.IsDetected==true then +Remove=true +end +end +local DetectedSet=DetectedItem.Set +if DetectedSet:Count()==0 then +Remove=true +end +if DetectedItemChanged==true or Remove then +Task=self:RemoveTask(DetectedItemIndex) +end +end +end +return Task +end +function TASK_A2A_DISPATCHER:GetFriendliesNearBy(DetectedItem) +local DetectedSet=DetectedItem.Set +local FriendlyUnitsNearBy=self.Detection:GetFriendliesNearBy(DetectedItem,Unit.Category.AIRPLANE) +local FriendlyTypes={} +local FriendliesCount=0 +if FriendlyUnitsNearBy then +local DetectedTreatLevel=DetectedSet:CalculateThreatLevelA2G() +for FriendlyUnitName,FriendlyUnitData in pairs(FriendlyUnitsNearBy)do +local FriendlyUnit=FriendlyUnitData +if FriendlyUnit:IsAirPlane()then +local FriendlyUnitThreatLevel=FriendlyUnit:GetThreatLevel() +FriendliesCount=FriendliesCount+1 +local FriendlyType=FriendlyUnit:GetTypeName() +FriendlyTypes[FriendlyType]=FriendlyTypes[FriendlyType]and(FriendlyTypes[FriendlyType]+1)or 1 +if DetectedTreatLevel0 then +for FriendlyType,FriendlyTypeCount in pairs(FriendlyTypes)do +FriendlyTypesReport:Add(string.format("%d of %s",FriendlyTypeCount,FriendlyType)) +end +else +FriendlyTypesReport:Add("-") +end +return FriendliesCount,FriendlyTypesReport +end +function TASK_A2A_DISPATCHER:GetPlayerFriendliesNearBy(DetectedItem) +local DetectedSet=DetectedItem.Set +local PlayersNearBy=self.Detection:GetPlayersNearBy(DetectedItem) +local PlayerTypes={} +local PlayersCount=0 +if PlayersNearBy then +local DetectedTreatLevel=DetectedSet:CalculateThreatLevelA2G() +for PlayerUnitName,PlayerUnitData in pairs(PlayersNearBy)do +local PlayerUnit=PlayerUnitData +local PlayerName=PlayerUnit:GetPlayerName() +if PlayerUnit:IsAirPlane()and PlayerName~=nil then +local FriendlyUnitThreatLevel=PlayerUnit:GetThreatLevel() +PlayersCount=PlayersCount+1 +local PlayerType=PlayerUnit:GetTypeName() +PlayerTypes[PlayerName]=PlayerType +if DetectedTreatLevel0 then +for PlayerName,PlayerType in pairs(PlayerTypes)do +PlayerTypesReport:Add(string.format('"%s" in %s',PlayerName,PlayerType)) +end +else +PlayerTypesReport:Add("-") +end +return PlayersCount,PlayerTypesReport +end +function TASK_A2A_DISPATCHER:RemoveTask(TaskIndex) +self.Mission:RemoveTask(self.Tasks[TaskIndex]) +self.Tasks[TaskIndex]=nil +end +function TASK_A2A_DISPATCHER:ProcessDetected(Detection) +self:F() +local AreaMsg={} +local TaskMsg={} +local ChangeMsg={} +local Mission=self.Mission +if Mission:IsIDLE()or Mission:IsENGAGED()then +local TaskReport=REPORT:New() +for TaskIndex,TaskData in pairs(self.Tasks)do +local Task=TaskData +if Task:IsStatePlanned()then +local DetectedItem=Detection:GetDetectedItemByIndex(TaskIndex) +if not DetectedItem then +local TaskText=Task:GetName() +for TaskGroupID,TaskGroup in pairs(self.SetGroup:GetSet())do +Mission:GetCommandCenter():MessageToGroup(string.format("Obsolete A2A task %s for %s removed.",TaskText,Mission:GetShortText()),TaskGroup) +end +Task=self:RemoveTask(TaskIndex) +end +end +end +for DetectedItemID,DetectedItem in pairs(Detection:GetDetectedItems())do +local DetectedItem=DetectedItem +local DetectedSet=DetectedItem.Set +local DetectedCount=DetectedSet:Count() +local DetectedZone=DetectedItem.Zone +local DetectedID=DetectedItem.ID +local TaskIndex=DetectedItem.Index +local DetectedItemChanged=DetectedItem.Changed +local Task=self.Tasks[TaskIndex] +Task=self:EvaluateRemoveTask(Mission,Task,Detection,DetectedItem,TaskIndex,DetectedItemChanged) +if not Task and DetectedCount>0 then +local TargetSetUnit=self:EvaluateENGAGE(DetectedItem) +if TargetSetUnit then +Task=TASK_A2A_ENGAGE:New(Mission,self.SetGroup,string.format("ENGAGE.%03d",DetectedID),TargetSetUnit) +Task:SetDetection(Detection,DetectedItem) +Task:UpdateTaskInfo(DetectedItem) +else +local TargetSetUnit=self:EvaluateINTERCEPT(DetectedItem) +if TargetSetUnit then +Task=TASK_A2A_INTERCEPT:New(Mission,self.SetGroup,string.format("INTERCEPT.%03d",DetectedID),TargetSetUnit) +Task:SetDetection(Detection,DetectedItem) +Task:UpdateTaskInfo(DetectedItem) +else +local TargetSetUnit=self:EvaluateSWEEP(DetectedItem) +if TargetSetUnit then +Task=TASK_A2A_SWEEP:New(Mission,self.SetGroup,string.format("SWEEP.%03d",DetectedID),TargetSetUnit) +Task:SetDetection(Detection,DetectedItem) +Task:UpdateTaskInfo(DetectedItem) +end +end +end +if Task then +self.Tasks[TaskIndex]=Task +Task:SetTargetZone(DetectedZone,DetectedItem.Coordinate.y,DetectedItem.Coordinate.Heading) +Task:SetDispatcher(self) +Mission:AddTask(Task) +function Task.OnEnterSuccess(Task,From,Event,To) +self:Success(Task) +end +function Task.OnEnterCancelled(Task,From,Event,To) +self:Cancelled(Task) +end +function Task.OnEnterFailed(Task,From,Event,To) +self:Failed(Task) +end +function Task.OnEnterAborted(Task,From,Event,To) +self:Aborted(Task) +end +TaskReport:Add(Task:GetName()) +else +self:F("This should not happen") +end +end +if Task then +local FriendliesCount,FriendliesReport=self:GetFriendliesNearBy(DetectedItem,Unit.Category.AIRPLANE) +Task.TaskInfo:AddText("Friendlies",string.format("%d ( %s )",FriendliesCount,FriendliesReport:Text(",")),40,"MOD") +local PlayersCount,PlayersReport=self:GetPlayerFriendliesNearBy(DetectedItem) +Task.TaskInfo:AddText("Players",string.format("%d ( %s )",PlayersCount,PlayersReport:Text(",")),40,"MOD") +end +Detection:AcceptChanges(DetectedItem) +end +Mission:GetCommandCenter():SetMenu() +local TaskText=TaskReport:Text(", ") +for TaskGroupID,TaskGroup in pairs(self.SetGroup:GetSet())do +if(not Mission:IsGroupAssigned(TaskGroup))and TaskText~=""and(self.FlashNewTask)then +Mission:GetCommandCenter():MessageToGroup(string.format("%s has tasks %s. Subscribe to a task using the radio menu.",Mission:GetShortText(),TaskText),TaskGroup) +end +end +end +return true +end +end +do +TASK_A2A={ +ClassName="TASK_A2A", +} +function TASK_A2A:New(Mission,SetAttack,TaskName,TargetSetUnit,TaskType,TaskBriefing) +local self=BASE:Inherit(self,TASK:New(Mission,SetAttack,TaskName,TaskType,TaskBriefing)) +self:F() +self.TargetSetUnit=TargetSetUnit +self.TaskType=TaskType +local Fsm=self:GetUnitProcess() +Fsm:AddTransition("Assigned","RouteToRendezVous","RoutingToRendezVous") +Fsm:AddProcess("RoutingToRendezVous","RouteToRendezVousPoint",ACT_ROUTE_POINT:New(),{Arrived="ArriveAtRendezVous"}) +Fsm:AddProcess("RoutingToRendezVous","RouteToRendezVousZone",ACT_ROUTE_ZONE:New(),{Arrived="ArriveAtRendezVous"}) +Fsm:AddTransition({"Arrived","RoutingToRendezVous"},"ArriveAtRendezVous","ArrivedAtRendezVous") +Fsm:AddTransition({"ArrivedAtRendezVous","HoldingAtRendezVous"},"Engage","Engaging") +Fsm:AddTransition({"ArrivedAtRendezVous","HoldingAtRendezVous"},"HoldAtRendezVous","HoldingAtRendezVous") +Fsm:AddProcess("Engaging","Account",ACT_ACCOUNT_DEADS:New(),{}) +Fsm:AddTransition("Engaging","RouteToTarget","Engaging") +Fsm:AddProcess("Engaging","RouteToTargetZone",ACT_ROUTE_ZONE:New(),{}) +Fsm:AddProcess("Engaging","RouteToTargetPoint",ACT_ROUTE_POINT:New(),{}) +Fsm:AddTransition("Engaging","RouteToTargets","Engaging") +Fsm:AddTransition("Rejected","Reject","Aborted") +Fsm:AddTransition("Failed","Fail","Failed") +function Fsm:OnLeaveAssigned(TaskUnit,Task) +self:F({TaskUnit=TaskUnit,Task=Task and Task:GetClassNameAndID()}) +self:SelectAction() +end +function Fsm:onafterRouteToRendezVous(TaskUnit,Task) +self:F({TaskUnit=TaskUnit,Task=Task and Task:GetClassNameAndID()}) +if Task:GetRendezVousZone(TaskUnit)then +self:__RouteToRendezVousZone(0.1) +else +if Task:GetRendezVousCoordinate(TaskUnit)then +self:__RouteToRendezVousPoint(0.1) +else +self:__ArriveAtRendezVous(0.1) +end +end +end +function Fsm:OnAfterArriveAtRendezVous(TaskUnit,Task) +self:F({TaskUnit=TaskUnit,Task=Task and Task:GetClassNameAndID()}) +self:__Engage(0.1) +end +function Fsm:onafterEngage(TaskUnit,Task) +self:F({self}) +self:__Account(0.1) +self:__RouteToTarget(0.1) +self:__RouteToTargets(-10) +end +function Fsm:onafterRouteToTarget(TaskUnit,Task) +self:F({TaskUnit=TaskUnit,Task=Task and Task:GetClassNameAndID()}) +if Task:GetTargetZone(TaskUnit)then +self:__RouteToTargetZone(0.1) +else +local TargetUnit=Task.TargetSetUnit:GetFirst() +if TargetUnit then +local Coordinate=TargetUnit:GetPointVec3() +self:T({TargetCoordinate=Coordinate,Coordinate:GetX(),Coordinate:GetAlt(),Coordinate:GetZ()}) +Task:SetTargetCoordinate(Coordinate,TaskUnit) +end +self:__RouteToTargetPoint(0.1) +end +end +function Fsm:onafterRouteToTargets(TaskUnit,Task) +self:F({TaskUnit=TaskUnit,Task=Task and Task:GetClassNameAndID()}) +local TargetUnit=Task.TargetSetUnit:GetFirst() +if TargetUnit then +Task:SetTargetCoordinate(TargetUnit:GetCoordinate(),TaskUnit) +end +self:__RouteToTargets(-10) +end +return self +end +function TASK_A2A:SetTargetSetUnit(TargetSetUnit) +self.TargetSetUnit=TargetSetUnit +end +function TASK_A2A:GetPlannedMenuText() +return self:GetStateString().." - "..self:GetTaskName().." ( "..self.TargetSetUnit:GetUnitTypesText().." )" +end +function TASK_A2A:SetRendezVousCoordinate(RendezVousCoordinate,RendezVousRange,TaskUnit) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +local ActRouteRendezVous=ProcessUnit:GetProcess("RoutingToRendezVous","RouteToRendezVousPoint") +ActRouteRendezVous:SetCoordinate(RendezVousCoordinate) +ActRouteRendezVous:SetRange(RendezVousRange) +end +function TASK_A2A:GetRendezVousCoordinate(TaskUnit) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +local ActRouteRendezVous=ProcessUnit:GetProcess("RoutingToRendezVous","RouteToRendezVousPoint") +return ActRouteRendezVous:GetCoordinate(),ActRouteRendezVous:GetRange() +end +function TASK_A2A:SetRendezVousZone(RendezVousZone,TaskUnit) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +local ActRouteRendezVous=ProcessUnit:GetProcess("RoutingToRendezVous","RouteToRendezVousZone") +ActRouteRendezVous:SetZone(RendezVousZone) +end +function TASK_A2A:GetRendezVousZone(TaskUnit) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +local ActRouteRendezVous=ProcessUnit:GetProcess("RoutingToRendezVous","RouteToRendezVousZone") +return ActRouteRendezVous:GetZone() +end +function TASK_A2A:SetTargetCoordinate(TargetCoordinate,TaskUnit) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +local ActRouteTarget=ProcessUnit:GetProcess("Engaging","RouteToTargetPoint") +ActRouteTarget:SetCoordinate(TargetCoordinate) +end +function TASK_A2A:GetTargetCoordinate(TaskUnit) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +local ActRouteTarget=ProcessUnit:GetProcess("Engaging","RouteToTargetPoint") +return ActRouteTarget:GetCoordinate() +end +function TASK_A2A:SetTargetZone(TargetZone,Altitude,Heading,TaskUnit) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +local ActRouteTarget=ProcessUnit:GetProcess("Engaging","RouteToTargetZone") +ActRouteTarget:SetZone(TargetZone,Altitude,Heading) +end +function TASK_A2A:GetTargetZone(TaskUnit) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +local ActRouteTarget=ProcessUnit:GetProcess("Engaging","RouteToTargetZone") +return ActRouteTarget:GetZone() +end +function TASK_A2A:SetGoalTotal() +self.GoalTotal=self.TargetSetUnit:Count() +end +function TASK_A2A:GetGoalTotal() +return self.GoalTotal +end +function TASK_A2A:ReportOrder(ReportGroup) +self:UpdateTaskInfo(self.DetectedItem) +local Coordinate=self.TaskInfo:GetData("Coordinate") +local Distance=ReportGroup:GetCoordinate():Get2DDistance(Coordinate) +return Distance +end +function TASK_A2A:onafterGoal(TaskUnit,From,Event,To) +local TargetSetUnit=self.TargetSetUnit +if TargetSetUnit:Count()==0 then +self:Success() +end +self:__Goal(-10) +end +function TASK_A2A:UpdateTaskInfo(DetectedItem) +if self:IsStatePlanned()or self:IsStateAssigned()then +local TargetCoordinate=DetectedItem and self.Detection:GetDetectedItemCoordinate(DetectedItem)or self.TargetSetUnit:GetFirst():GetCoordinate() +self.TaskInfo:AddTaskName(0,"MSOD") +self.TaskInfo:AddCoordinate(TargetCoordinate,1,"SOD") +local ThreatLevel,ThreatText +if DetectedItem then +ThreatLevel,ThreatText=self.Detection:GetDetectedItemThreatLevel(DetectedItem) +else +ThreatLevel,ThreatText=self.TargetSetUnit:CalculateThreatLevelA2G() +end +self.TaskInfo:AddThreat(ThreatText,ThreatLevel,10,"MOD",true) +if self.Detection then +local DetectedItemsCount=self.TargetSetUnit:Count() +local ReportTypes=REPORT:New() +local TargetTypes={} +for TargetUnitName,TargetUnit in pairs(self.TargetSetUnit:GetSet())do +local TargetType=self.Detection:GetDetectedUnitTypeName(TargetUnit) +if not TargetTypes[TargetType]then +TargetTypes[TargetType]=TargetType +ReportTypes:Add(TargetType) +end +end +self.TaskInfo:AddTargetCount(DetectedItemsCount,11,"O",true) +self.TaskInfo:AddTargets(DetectedItemsCount,ReportTypes:Text(", "),20,"D",true) +else +local DetectedItemsCount=self.TargetSetUnit:Count() +local DetectedItemsTypes=self.TargetSetUnit:GetTypeNames() +self.TaskInfo:AddTargetCount(DetectedItemsCount,11,"O",true) +self.TaskInfo:AddTargets(DetectedItemsCount,DetectedItemsTypes,20,"D",true) +end +end +end +function TASK_A2A:GetAutoAssignPriority(AutoAssignMethod,CommandCenter,TaskGroup) +if AutoAssignMethod==COMMANDCENTER.AutoAssignMethods.Random then +return math.random(1,9) +elseif AutoAssignMethod==COMMANDCENTER.AutoAssignMethods.Distance then +local Coordinate=self.TaskInfo:GetData("Coordinate") +local Distance=Coordinate:Get2DDistance(CommandCenter:GetPositionable():GetCoordinate()) +return math.floor(Distance) +elseif AutoAssignMethod==COMMANDCENTER.AutoAssignMethods.Priority then +return 1 +end +return 0 +end +end +do +TASK_A2A_INTERCEPT={ +ClassName="TASK_A2A_INTERCEPT", +} +function TASK_A2A_INTERCEPT:New(Mission,SetGroup,TaskName,TargetSetUnit,TaskBriefing) +local self=BASE:Inherit(self,TASK_A2A:New(Mission,SetGroup,TaskName,TargetSetUnit,"INTERCEPT",TaskBriefing)) +self:F() +Mission:AddTask(self) +self:SetBriefing( +TaskBriefing or +"Intercept incoming intruders.\n" +) +return self +end +function TASK_A2A_INTERCEPT:SetScoreOnProgress(PlayerName,Score,TaskUnit) +self:F({PlayerName,Score,TaskUnit}) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +ProcessUnit:AddScoreProcess("Engaging","Account","AccountForPlayer","Player "..PlayerName.." has intercepted a target.",Score) +return self +end +function TASK_A2A_INTERCEPT:SetScoreOnSuccess(PlayerName,Score,TaskUnit) +self:F({PlayerName,Score,TaskUnit}) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +ProcessUnit:AddScore("Success","All targets have been successfully intercepted!",Score) +return self +end +function TASK_A2A_INTERCEPT:SetScoreOnFail(PlayerName,Penalty,TaskUnit) +self:F({PlayerName,Penalty,TaskUnit}) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +ProcessUnit:AddScore("Failed","The intercept has failed!",Penalty) +return self +end +end +do +TASK_A2A_SWEEP={ +ClassName="TASK_A2A_SWEEP", +} +function TASK_A2A_SWEEP:New(Mission,SetGroup,TaskName,TargetSetUnit,TaskBriefing) +local self=BASE:Inherit(self,TASK_A2A:New(Mission,SetGroup,TaskName,TargetSetUnit,"SWEEP",TaskBriefing)) +self:F() +Mission:AddTask(self) +self:SetBriefing( +TaskBriefing or +"Perform a fighter sweep. Incoming intruders were detected and could be hiding at the location.\n" +) +return self +end +function TASK_A2A_SWEEP:onafterGoal(TaskUnit,From,Event,To) +local TargetSetUnit=self.TargetSetUnit +if TargetSetUnit:Count()==0 then +self:Success() +end +self:__Goal(-10) +end +function TASK_A2A_SWEEP:SetScoreOnProgress(PlayerName,Score,TaskUnit) +self:F({PlayerName,Score,TaskUnit}) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +ProcessUnit:AddScoreProcess("Engaging","Account","AccountForPlayer","Player "..PlayerName.." has sweeped a target.",Score) +return self +end +function TASK_A2A_SWEEP:SetScoreOnSuccess(PlayerName,Score,TaskUnit) +self:F({PlayerName,Score,TaskUnit}) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +ProcessUnit:AddScore("Success","All targets have been successfully sweeped!",Score) +return self +end +function TASK_A2A_SWEEP:SetScoreOnFail(PlayerName,Penalty,TaskUnit) +self:F({PlayerName,Penalty,TaskUnit}) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +ProcessUnit:AddScore("Failed","The sweep has failed!",Penalty) +return self +end +end +do +TASK_A2A_ENGAGE={ +ClassName="TASK_A2A_ENGAGE", +} +function TASK_A2A_ENGAGE:New(Mission,SetGroup,TaskName,TargetSetUnit,TaskBriefing) +local self=BASE:Inherit(self,TASK_A2A:New(Mission,SetGroup,TaskName,TargetSetUnit,"ENGAGE",TaskBriefing)) +self:F() +Mission:AddTask(self) +self:SetBriefing( +TaskBriefing or +"Bogeys are nearby! Players close by are ordered to ENGAGE the intruders!\n" +) +return self +end +function TASK_A2A_ENGAGE:SetScoreOnProgress(PlayerName,Score,TaskUnit) +self:F({PlayerName,Score,TaskUnit}) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +ProcessUnit:AddScoreProcess("Engaging","Account","AccountForPlayer","Player "..PlayerName.." has engaged and destroyed a target.",Score) +return self +end +function TASK_A2A_ENGAGE:SetScoreOnSuccess(PlayerName,Score,TaskUnit) +self:F({PlayerName,Score,TaskUnit}) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +ProcessUnit:AddScore("Success","All targets have been successfully engaged!",Score) +return self +end +function TASK_A2A_ENGAGE:SetScoreOnFail(PlayerName,Penalty,TaskUnit) +self:F({PlayerName,Penalty,TaskUnit}) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +ProcessUnit:AddScore("Failed","The target engagement has failed!",Penalty) +return self +end +end +do +TASK_CARGO={ +ClassName="TASK_CARGO", +} +function TASK_CARGO:New(Mission,SetGroup,TaskName,SetCargo,TaskType,TaskBriefing) +local self=BASE:Inherit(self,TASK:New(Mission,SetGroup,TaskName,TaskType,TaskBriefing)) +self:F({Mission,SetGroup,TaskName,SetCargo,TaskType}) +self.SetCargo=SetCargo +self.TaskType=TaskType +self.SmokeColor=SMOKECOLOR.Red +self.CargoItemCount={} +self.CargoLimit=10 +self.DeployZones={} +self:AddTransition("*","CargoDeployed","*") +self:AddTransition("*","CargoPickedUp","*") +local Fsm=self:GetUnitProcess() +Fsm:AddTransition({"Planned","Assigned","Cancelled","WaitingForCommand","ArrivedAtPickup","ArrivedAtDeploy","Boarded","UnBoarded","Loaded","UnLoaded","Landed","Boarding"},"SelectAction","*") +Fsm:AddTransition("*","RouteToPickup","RoutingToPickup") +Fsm:AddProcess("RoutingToPickup","RouteToPickupPoint",ACT_ROUTE_POINT:New(),{Arrived="ArriveAtPickup",Cancelled="CancelRouteToPickup"}) +Fsm:AddTransition("Arrived","ArriveAtPickup","ArrivedAtPickup") +Fsm:AddTransition("Cancelled","CancelRouteToPickup","Cancelled") +Fsm:AddTransition("*","RouteToDeploy","RoutingToDeploy") +Fsm:AddProcess("RoutingToDeploy","RouteToDeployZone",ACT_ROUTE_ZONE:New(),{Arrived="ArriveAtDeploy",Cancelled="CancelRouteToDeploy"}) +Fsm:AddTransition("Arrived","ArriveAtDeploy","ArrivedAtDeploy") +Fsm:AddTransition("Cancelled","CancelRouteToDeploy","Cancelled") +Fsm:AddTransition({"ArrivedAtPickup","ArrivedAtDeploy","Landing"},"Land","Landing") +Fsm:AddTransition("Landing","Landed","Landed") +Fsm:AddTransition("*","PrepareBoarding","AwaitBoarding") +Fsm:AddTransition("AwaitBoarding","Board","Boarding") +Fsm:AddTransition("Boarding","Boarded","Boarded") +Fsm:AddTransition("*","Load","Loaded") +Fsm:AddTransition("*","PrepareUnBoarding","AwaitUnBoarding") +Fsm:AddTransition("AwaitUnBoarding","UnBoard","UnBoarding") +Fsm:AddTransition("UnBoarding","UnBoarded","UnBoarded") +Fsm:AddTransition("*","Unload","Unloaded") +Fsm:AddTransition("*","Planned","Planned") +Fsm:AddTransition("Deployed","Success","Success") +Fsm:AddTransition("Rejected","Reject","Aborted") +Fsm:AddTransition("Failed","Fail","Failed") +function Fsm:OnAfterAssigned(TaskUnit,Task) +self:F({TaskUnit=TaskUnit,Task=Task and Task:GetClassNameAndID()}) +self:SelectAction() +end +function Fsm:onafterSelectAction(TaskUnit,Task) +local TaskUnitName=TaskUnit:GetName() +local MenuTime=Task:InitTaskControlMenu(TaskUnit) +local MenuControl=Task:GetTaskControlMenu(TaskUnit) +Task.SetCargo:ForEachCargo( +function(Cargo) +if Cargo:IsAlive()then +local TaskGroup=TaskUnit:GetGroup() +if Cargo:IsUnLoaded()then +local CargoBayFreeWeight=TaskUnit:GetCargoBayFreeWeight() +local CargoWeight=Cargo:GetWeight() +self:F({CargoBayFreeWeight=CargoBayFreeWeight}) +if CargoBayFreeWeight>CargoWeight then +if Cargo:IsInReportRadius(TaskUnit:GetPointVec2())then +local NotInDeployZones=true +for DeployZoneName,DeployZone in pairs(Task.DeployZones)do +if Cargo:IsInZone(DeployZone)then +NotInDeployZones=false +end +end +if NotInDeployZones then +if not TaskUnit:InAir()then +if Cargo:CanBoard()==true then +if Cargo:IsInLoadRadius(TaskUnit:GetPointVec2())then +Cargo:Report("Ready for boarding.","board",TaskUnit:GetGroup()) +local BoardMenu=MENU_GROUP:New(TaskGroup,"Board cargo",MenuControl):SetTime(MenuTime):SetTag("Cargo") +MENU_GROUP_COMMAND:New(TaskUnit:GetGroup(),Cargo.Name,BoardMenu,self.MenuBoardCargo,self,Cargo):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent() +else +Cargo:Report("Board at "..Cargo:GetCoordinate():ToString(TaskUnit:GetGroup().."."),"reporting",TaskUnit:GetGroup()) +end +else +if Cargo:CanLoad()==true then +if Cargo:IsInLoadRadius(TaskUnit:GetPointVec2())then +Cargo:Report("Ready for loading.","load",TaskUnit:GetGroup()) +local LoadMenu=MENU_GROUP:New(TaskGroup,"Load cargo",MenuControl):SetTime(MenuTime):SetTag("Cargo") +MENU_GROUP_COMMAND:New(TaskUnit:GetGroup(),Cargo.Name,LoadMenu,self.MenuLoadCargo,self,Cargo):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent() +else +Cargo:Report("Load at "..Cargo:GetCoordinate():ToString(TaskUnit:GetGroup()).." within "..Cargo.NearRadius..".","reporting",TaskUnit:GetGroup()) +end +else +if Cargo:CanSlingload()==true then +if Cargo:IsInLoadRadius(TaskUnit:GetPointVec2())then +Cargo:Report("Ready for sling loading.","slingload",TaskUnit:GetGroup()) +local SlingloadMenu=MENU_GROUP:New(TaskGroup,"Slingload cargo",MenuControl):SetTime(MenuTime):SetTag("Cargo") +MENU_GROUP_COMMAND:New(TaskUnit:GetGroup(),Cargo.Name,SlingloadMenu,self.MenuLoadCargo,self,Cargo):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent() +else +Cargo:Report("Slingload at "..Cargo:GetCoordinate():ToString(TaskUnit:GetGroup())..".","reporting",TaskUnit:GetGroup()) +end +end +end +end +else +Cargo:ReportResetAll(TaskUnit:GetGroup()) +end +end +else +if not Cargo:IsDeployed()==true then +local RouteToPickupMenu=MENU_GROUP:New(TaskGroup,"Route to pickup cargo",MenuControl):SetTime(MenuTime):SetTag("Cargo") +Cargo:ReportResetAll(TaskUnit:GetGroup()) +if Cargo:CanBoard()==true then +if not Cargo:IsInLoadRadius(TaskUnit:GetPointVec2())then +local BoardMenu=MENU_GROUP:New(TaskGroup,"Board cargo",RouteToPickupMenu):SetTime(MenuTime):SetTag("Cargo") +MENU_GROUP_COMMAND:New(TaskUnit:GetGroup(),Cargo.Name,BoardMenu,self.MenuRouteToPickup,self,Cargo):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent() +end +else +if Cargo:CanLoad()==true then +if not Cargo:IsInLoadRadius(TaskUnit:GetPointVec2())then +local LoadMenu=MENU_GROUP:New(TaskGroup,"Load cargo",RouteToPickupMenu):SetTime(MenuTime):SetTag("Cargo") +MENU_GROUP_COMMAND:New(TaskUnit:GetGroup(),Cargo.Name,LoadMenu,self.MenuRouteToPickup,self,Cargo):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent() +end +else +if Cargo:CanSlingload()==true then +if not Cargo:IsInLoadRadius(TaskUnit:GetPointVec2())then +local SlingloadMenu=MENU_GROUP:New(TaskGroup,"Slingload cargo",RouteToPickupMenu):SetTime(MenuTime):SetTag("Cargo") +MENU_GROUP_COMMAND:New(TaskUnit:GetGroup(),Cargo.Name,SlingloadMenu,self.MenuRouteToPickup,self,Cargo):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent() +end +end +end +end +end +end +end +for DeployZoneName,DeployZone in pairs(Task.DeployZones)do +if Cargo:IsInZone(DeployZone)then +Task:I({CargoIsDeployed=Task.CargoDeployed and"true"or"false"}) +if Cargo:IsDeployed()==false then +Cargo:SetDeployed(true) +Task:I({CargoIsAlive=Cargo:IsAlive()and"true"or"false"}) +if Cargo:IsAlive()then +Task:CargoDeployed(TaskUnit,Cargo,DeployZone) +end +end +end +end +end +if Cargo:IsLoaded()==true and Cargo:IsLoadedInCarrier(TaskUnit)==true then +if not TaskUnit:InAir()then +if Cargo:CanUnboard()==true then +local UnboardMenu=MENU_GROUP:New(TaskGroup,"Unboard cargo",MenuControl):SetTime(MenuTime):SetTag("Cargo") +MENU_GROUP_COMMAND:New(TaskUnit:GetGroup(),Cargo.Name,UnboardMenu,self.MenuUnboardCargo,self,Cargo):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent() +else +if Cargo:CanUnload()==true then +local UnloadMenu=MENU_GROUP:New(TaskGroup,"Unload cargo",MenuControl):SetTime(MenuTime):SetTag("Cargo") +MENU_GROUP_COMMAND:New(TaskUnit:GetGroup(),Cargo.Name,UnloadMenu,self.MenuUnloadCargo,self,Cargo):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent() +end +end +end +end +for DeployZoneName,DeployZone in pairs(Task.DeployZones)do +if not Cargo:IsInZone(DeployZone)then +local RouteToDeployMenu=MENU_GROUP:New(TaskGroup,"Route to deploy cargo",MenuControl):SetTime(MenuTime):SetTag("Cargo") +MENU_GROUP_COMMAND:New(TaskUnit:GetGroup(),"Zone "..DeployZoneName,RouteToDeployMenu,self.MenuRouteToDeploy,self,DeployZone):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent() +end +end +end +end +) +Task:RefreshTaskControlMenu(TaskUnit,MenuTime,"Cargo") +self:__SelectAction(-1) +end +function Fsm:OnLeaveWaitingForCommand(TaskUnit,Task) +self:F({TaskUnit=TaskUnit,Task=Task and Task:GetClassNameAndID()}) +end +function Fsm:MenuBoardCargo(Cargo) +self:__PrepareBoarding(1.0,Cargo) +end +function Fsm:MenuLoadCargo(Cargo) +self:__Load(1.0,Cargo) +end +function Fsm:MenuUnboardCargo(Cargo,DeployZone) +self:__PrepareUnBoarding(1.0,Cargo,DeployZone) +end +function Fsm:MenuUnloadCargo(Cargo,DeployZone) +self:__Unload(1.0,Cargo,DeployZone) +end +function Fsm:MenuRouteToPickup(Cargo) +self:__RouteToPickup(1.0,Cargo) +end +function Fsm:MenuRouteToDeploy(DeployZone) +self:__RouteToDeploy(1.0,DeployZone) +end +function Fsm:onafterRouteToPickup(TaskUnit,Task,From,Event,To,Cargo) +self:F({TaskUnit=TaskUnit,Task=Task and Task:GetClassNameAndID()}) +if Cargo:IsAlive()then +self.Cargo=Cargo +Task:SetCargoPickup(self.Cargo,TaskUnit) +self:__RouteToPickupPoint(-0.1) +end +end +function Fsm:onafterArriveAtPickup(TaskUnit,Task) +self:F({TaskUnit=TaskUnit,Task=Task and Task:GetClassNameAndID()}) +if self.Cargo:IsAlive()then +if TaskUnit:IsAir()then +Task:GetMission():GetCommandCenter():MessageToGroup("Land",TaskUnit:GetGroup()) +self:__Land(-0.1,"Pickup") +else +self:__SelectAction(-0.1) +end +end +end +function Fsm:onafterCancelRouteToPickup(TaskUnit,Task) +self:F({TaskUnit=TaskUnit,Task=Task and Task:GetClassNameAndID()}) +Task:GetMission():GetCommandCenter():MessageToGroup("Cancelled routing to Cargo "..self.Cargo:GetName(),TaskUnit:GetGroup()) +self:__SelectAction(-0.1) +end +function Fsm:onafterRouteToDeploy(TaskUnit,Task,From,Event,To,DeployZone) +self:F({TaskUnit=TaskUnit,Task=Task and Task:GetClassNameAndID()}) +self:F(DeployZone) +self.DeployZone=DeployZone +Task:SetDeployZone(self.DeployZone,TaskUnit) +self:__RouteToDeployZone(-0.1) +end +function Fsm:onafterArriveAtDeploy(TaskUnit,Task) +self:F({TaskUnit=TaskUnit,Task=Task and Task:GetClassNameAndID()}) +if TaskUnit:IsAir()then +Task:GetMission():GetCommandCenter():MessageToGroup("Land",TaskUnit:GetGroup()) +self:__Land(-0.1,"Deploy") +else +self:__SelectAction(-0.1) +end +end +function Fsm:onafterCancelRouteToDeploy(TaskUnit,Task) +self:F({TaskUnit=TaskUnit,Task=Task and Task:GetClassNameAndID()}) +Task:GetMission():GetCommandCenter():MessageToGroup("Cancelled routing to deploy zone "..self.DeployZone:GetName(),TaskUnit:GetGroup()) +self:__SelectAction(-0.1) +end +function Fsm:onafterLand(TaskUnit,Task,From,Event,To,Action) +self:F({TaskUnit=TaskUnit,Task=Task and Task:GetClassNameAndID()}) +if Action=="Pickup"then +if self.Cargo:IsAlive()then +if self.Cargo:IsInReportRadius(TaskUnit:GetPointVec2())then +if TaskUnit:InAir()then +self:__Land(-10,Action) +else +Task:GetMission():GetCommandCenter():MessageToGroup("Landed at pickup location...",TaskUnit:GetGroup()) +self:__Landed(-0.1,Action) +end +else +self:__RouteToPickup(-0.1,self.Cargo) +end +end +else +if TaskUnit:IsAlive()then +if TaskUnit:IsInZone(self.DeployZone)then +if TaskUnit:InAir()then +self:__Land(-10,Action) +else +Task:GetMission():GetCommandCenter():MessageToGroup("Landed at deploy zone "..self.DeployZone:GetName(),TaskUnit:GetGroup()) +self:__Landed(-0.1,Action) +end +else +self:__RouteToDeploy(-0.1,self.Cargo) +end +end +end +end +function Fsm:onafterLanded(TaskUnit,Task,From,Event,To,Action) +self:F({TaskUnit=TaskUnit,Task=Task and Task:GetClassNameAndID()}) +if Action=="Pickup"then +if self.Cargo:IsAlive()then +if self.Cargo:IsInReportRadius(TaskUnit:GetPointVec2())then +if TaskUnit:InAir()then +self:__Land(-0.1,Action) +else +self:__SelectAction(-0.1) +end +else +self:__RouteToPickup(-0.1,self.Cargo) +end +end +else +if TaskUnit:IsAlive()then +if TaskUnit:IsInZone(self.DeployZone)then +if TaskUnit:InAir()then +self:__Land(-10,Action) +else +self:__SelectAction(-0.1) +end +else +self:__RouteToDeploy(-0.1,self.Cargo) +end +end +end +end +function Fsm:onafterPrepareBoarding(TaskUnit,Task,From,Event,To,Cargo) +self:F({TaskUnit=TaskUnit,Task=Task and Task:GetClassNameAndID()}) +if Cargo and Cargo:IsAlive()then +self:__Board(-0.1,Cargo) +end +end +function Fsm:onafterBoard(TaskUnit,Task,From,Event,To,Cargo) +self:F({TaskUnit=TaskUnit,Task=Task and Task:GetClassNameAndID()}) +function Cargo:OnEnterLoaded(From,Event,To,TaskUnit,TaskProcess) +self:F({From,Event,To,TaskUnit,TaskProcess}) +TaskProcess:__Boarded(0.1,self) +end +if Cargo:IsAlive()then +if Cargo:IsInLoadRadius(TaskUnit:GetPointVec2())then +if TaskUnit:InAir()then +else +Cargo:MessageToGroup("Boarding ...",TaskUnit:GetGroup()) +if not Cargo:IsBoarding()then +Cargo:Board(TaskUnit,nil,self) +end +end +else +end +end +end +function Fsm:onafterBoarded(TaskUnit,Task,From,Event,To,Cargo) +local TaskUnitName=TaskUnit:GetName() +self:F({TaskUnit=TaskUnitName,Task=Task and Task:GetClassNameAndID()}) +Cargo:MessageToGroup("Boarded cargo "..Cargo:GetName(),TaskUnit:GetGroup()) +self:__Load(-0.1,Cargo) +end +function Fsm:onafterLoad(TaskUnit,Task,From,Event,To,Cargo) +local TaskUnitName=TaskUnit:GetName() +self:F({TaskUnit=TaskUnitName,Task=Task and Task:GetClassNameAndID()}) +if not Cargo:IsLoaded()then +Cargo:Load(TaskUnit) +end +Cargo:MessageToGroup("Loaded cargo "..Cargo:GetName(),TaskUnit:GetGroup()) +TaskUnit:AddCargo(Cargo) +Task:CargoPickedUp(TaskUnit,Cargo) +self:SelectAction(-1) +end +function Fsm:onafterPrepareUnBoarding(TaskUnit,Task,From,Event,To,Cargo) +self:F({TaskUnit=TaskUnit,Task=Task and Task:GetClassNameAndID(),From,Event,To,Cargo}) +self.Cargo=Cargo +self.DeployZone=nil +if Cargo:IsAlive()then +for DeployZoneName,DeployZone in pairs(Task.DeployZones)do +if Cargo:IsInZone(DeployZone)then +self.DeployZone=DeployZone +break +end +end +self:__UnBoard(-0.1,Cargo,self.DeployZone) +end +end +function Fsm:onafterUnBoard(TaskUnit,Task,From,Event,To,Cargo,DeployZone) +self:F({TaskUnit=TaskUnit,Task=Task and Task:GetClassNameAndID(),From,Event,To,Cargo,DeployZone}) +function self.Cargo:OnEnterUnLoaded(From,Event,To,DeployZone,TaskProcess) +self:F({From,Event,To,DeployZone,TaskProcess}) +TaskProcess:__UnBoarded(-0.1) +end +if self.Cargo:IsAlive()then +self.Cargo:MessageToGroup("UnBoarding ...",TaskUnit:GetGroup()) +if DeployZone then +self.Cargo:UnBoard(DeployZone:GetCoordinate():GetRandomCoordinateInRadius(25,10),400,self) +else +self.Cargo:UnBoard(TaskUnit:GetCoordinate():GetRandomCoordinateInRadius(25,10),400,self) +end +end +end +function Fsm:onafterUnBoarded(TaskUnit,Task) +local TaskUnitName=TaskUnit:GetName() +self:F({TaskUnit=TaskUnitName,Task=Task and Task:GetClassNameAndID()}) +self.Cargo:MessageToGroup("UnBoarded cargo "..self.Cargo:GetName(),TaskUnit:GetGroup()) +self:Unload(self.Cargo) +end +function Fsm:onafterUnload(TaskUnit,Task,From,Event,To,Cargo,DeployZone) +local TaskUnitName=TaskUnit:GetName() +self:F({TaskUnit=TaskUnitName,Task=Task and Task:GetClassNameAndID()}) +if not Cargo:IsUnLoaded()then +if DeployZone then +Cargo:UnLoad(DeployZone:GetCoordinate():GetRandomCoordinateInRadius(25,10),400,self) +else +Cargo:UnLoad(TaskUnit:GetCoordinate():GetRandomCoordinateInRadius(25,10),400,self) +end +end +TaskUnit:RemoveCargo(Cargo) +Cargo:MessageToGroup("Unloaded cargo "..Cargo:GetName(),TaskUnit:GetGroup()) +self:Planned() +self:__SelectAction(1) +end +return self +end +function TASK_CARGO:SetCargoLimit(CargoLimit) +self.CargoLimit=CargoLimit +return self +end +function TASK_CARGO:SetSmokeColor(SmokeColor) +if SmokeColor==nil then +self.SmokeColor=SMOKECOLOR.Red +elseif type(SmokeColor)=="number"then +self:F2(SmokeColor) +if SmokeColor>0 and SmokeColor<=5 then +self.SmokeColor=SMOKECOLOR.SmokeColor +end +end +end +function TASK_CARGO:GetSmokeColor() +return self.SmokeColor +end +function TASK_CARGO:GetPlannedMenuText() +return self:GetStateString().." - "..self:GetTaskName().." ( "..self.TargetSetUnit:GetUnitTypesText().." )" +end +function TASK_CARGO:GetCargoSet() +return self.SetCargo +end +function TASK_CARGO:GetDeployZones() +return self.DeployZones +end +function TASK_CARGO:SetCargoPickup(Cargo,TaskUnit) +self:F({Cargo,TaskUnit}) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +local MenuTime=self:InitTaskControlMenu(TaskUnit) +local MenuControl=self:GetTaskControlMenu(TaskUnit) +local ActRouteCargo=ProcessUnit:GetProcess("RoutingToPickup","RouteToPickupPoint") +ActRouteCargo:Reset() +ActRouteCargo:SetCoordinate(Cargo:GetCoordinate()) +ActRouteCargo:SetRange(Cargo:GetLoadRadius()) +ActRouteCargo:SetMenuCancel(TaskUnit:GetGroup(),"Cancel Routing to Cargo "..Cargo:GetName(),MenuControl,MenuTime,"Cargo") +ActRouteCargo:Start() +return self +end +function TASK_CARGO:SetDeployZone(DeployZone,TaskUnit) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +local MenuTime=self:InitTaskControlMenu(TaskUnit) +local MenuControl=self:GetTaskControlMenu(TaskUnit) +local ActRouteDeployZone=ProcessUnit:GetProcess("RoutingToDeploy","RouteToDeployZone") +ActRouteDeployZone:Reset() +ActRouteDeployZone:SetZone(DeployZone) +ActRouteDeployZone:SetMenuCancel(TaskUnit:GetGroup(),"Cancel Routing to Deploy Zone"..DeployZone:GetName(),MenuControl,MenuTime,"Cargo") +ActRouteDeployZone:Start() +return self +end +function TASK_CARGO:AddDeployZone(DeployZone,TaskUnit) +self.DeployZones[DeployZone:GetName()]=DeployZone +return self +end +function TASK_CARGO:RemoveDeployZone(DeployZone,TaskUnit) +self.DeployZones[DeployZone:GetName()]=nil +return self +end +function TASK_CARGO:SetDeployZones(DeployZones,TaskUnit) +for DeployZoneID,DeployZone in pairs(DeployZones or{})do +self.DeployZones[DeployZone:GetName()]=DeployZone +end +return self +end +function TASK_CARGO:GetTargetZone(TaskUnit) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +local ActRouteTarget=ProcessUnit:GetProcess("Engaging","RouteToTargetZone") +return ActRouteTarget:GetZone() +end +function TASK_CARGO:SetScoreOnProgress(Text,Score,TaskUnit) +self:F({Text,Score,TaskUnit}) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +ProcessUnit:AddScoreProcess("Engaging","Account","Account",Text,Score) +return self +end +function TASK_CARGO:SetScoreOnSuccess(Text,Score,TaskUnit) +self:F({Text,Score,TaskUnit}) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +ProcessUnit:AddScore("Success",Text,Score) +return self +end +function TASK_CARGO:SetScoreOnFail(Text,Penalty,TaskUnit) +self:F({Text,Score,TaskUnit}) +local ProcessUnit=self:GetUnitProcess(TaskUnit) +ProcessUnit:AddScore("Failed",Text,Penalty) +return self +end +function TASK_CARGO:SetGoalTotal() +self.GoalTotal=self.SetCargo:Count() +end +function TASK_CARGO:GetGoalTotal() +return self.GoalTotal +end +function TASK_CARGO:UpdateTaskInfo() +if self:IsStatePlanned()or self:IsStateAssigned()then +self.TaskInfo:AddTaskName(0,"MSOD") +self.TaskInfo:AddCargoSet(self.SetCargo,10,"SOD",true) +local Coordinates={} +for CargoName,Cargo in pairs(self.SetCargo:GetSet())do +local Cargo=Cargo +if not Cargo:IsLoaded()then +Coordinates[#Coordinates+1]=Cargo:GetCoordinate() +end +end +self.TaskInfo:AddCoordinates(Coordinates,1,"M") +end +end +function TASK_CARGO:ReportOrder(ReportGroup) +return 0 +end +function TASK_CARGO:GetAutoAssignPriority(AutoAssignMethod,TaskGroup) +if AutoAssignMethod==COMMANDCENTER.AutoAssignMethods.Random then +return math.random(1,9) +elseif AutoAssignMethod==COMMANDCENTER.AutoAssignMethods.Distance then +return 0 +elseif AutoAssignMethod==COMMANDCENTER.AutoAssignMethods.Priority then +return 1 +end +return 0 +end +end +do +TASK_CARGO_TRANSPORT={ +ClassName="TASK_CARGO_TRANSPORT", +} +function TASK_CARGO_TRANSPORT:New(Mission,SetGroup,TaskName,SetCargo,TaskBriefing) +local self=BASE:Inherit(self,TASK_CARGO:New(Mission,SetGroup,TaskName,SetCargo,"Transport",TaskBriefing)) +self:F() +Mission:AddTask(self) +local Fsm=self:GetUnitProcess() +local CargoReport=REPORT:New("Transport Cargo. The following cargo needs to be transported including initial positions:") +SetCargo:ForEachCargo( +function(Cargo) +local CargoType=Cargo:GetType() +local CargoName=Cargo:GetName() +local CargoCoordinate=Cargo:GetCoordinate() +CargoReport:Add(string.format('- "%s" (%s) at %s',CargoName,CargoType,CargoCoordinate:ToStringMGRS())) +end +) +self:SetBriefing( +TaskBriefing or +CargoReport:Text() +) +return self +end +function TASK_CARGO_TRANSPORT:ReportOrder(ReportGroup) +return 0 +end +function TASK_CARGO_TRANSPORT:IsAllCargoTransported() +local CargoSet=self:GetCargoSet() +local Set=CargoSet:GetSet() +local DeployZones=self:GetDeployZones() +local CargoDeployed=true +for CargoID,CargoData in pairs(Set)do +local Cargo=CargoData +self:F({Cargo=Cargo:GetName(),CargoDeployed=Cargo:IsDeployed()}) +if Cargo:IsDeployed()then +else +CargoDeployed=false +end +end +self:F({CargoDeployed=CargoDeployed}) +return CargoDeployed +end +function TASK_CARGO_TRANSPORT:onafterGoal(TaskUnit,From,Event,To) +local CargoSet=self.CargoSet +if self:IsAllCargoTransported()then +self:Success() +end +self:__Goal(-10) +end +end +do +TASK_CARGO_CSAR={ +ClassName="TASK_CARGO_CSAR", +} +function TASK_CARGO_CSAR:New(Mission,SetGroup,TaskName,SetCargo,TaskBriefing) +local self=BASE:Inherit(self,TASK_CARGO:New(Mission,SetGroup,TaskName,SetCargo,"CSAR",TaskBriefing)) +self:F() +Mission:AddTask(self) +self:AddTransition("*","CargoPickedUp","*") +self:AddTransition("*","CargoDeployed","*") +self:F({CargoDeployed=self.CargoDeployed~=nil and"true"or"false"}) +local Fsm=self:GetUnitProcess() +local CargoReport=REPORT:New("Rescue a downed pilot from the following position:") +SetCargo:ForEachCargo( +function(Cargo) +local CargoType=Cargo:GetType() +local CargoName=Cargo:GetName() +local CargoCoordinate=Cargo:GetCoordinate() +CargoReport:Add(string.format('- "%s" (%s) at %s',CargoName,CargoType,CargoCoordinate:ToStringMGRS())) +end +) +self:SetBriefing( +TaskBriefing or +CargoReport:Text() +) +return self +end +function TASK_CARGO_CSAR:ReportOrder(ReportGroup) +return 0 +end +function TASK_CARGO_CSAR:IsAllCargoTransported() +local CargoSet=self:GetCargoSet() +local Set=CargoSet:GetSet() +local DeployZones=self:GetDeployZones() +local CargoDeployed=true +for CargoID,CargoData in pairs(Set)do +local Cargo=CargoData +self:F({Cargo=Cargo:GetName(),CargoDeployed=Cargo:IsDeployed()}) +if Cargo:IsDeployed()then +else +CargoDeployed=false +end +end +self:F({CargoDeployed=CargoDeployed}) +return CargoDeployed +end +function TASK_CARGO_CSAR:onafterGoal(TaskUnit,From,Event,To) +local CargoSet=self.CargoSet +if self:IsAllCargoTransported()then +self:Success() +end +self:__Goal(-10) +end +end +do +TASK_CARGO_DISPATCHER={ +ClassName="TASK_CARGO_DISPATCHER", +Mission=nil, +Tasks={}, +CSAR={}, +CSARSpawned=0, +Transport={}, +TransportCount=0, +} +function TASK_CARGO_DISPATCHER:New(Mission,SetGroup) +local self=BASE:Inherit(self,TASK_MANAGER:New(SetGroup)) +self.Mission=Mission +self:AddTransition("Started","Assign","Started") +self:AddTransition("Started","CargoPickedUp","Started") +self:AddTransition("Started","CargoDeployed","Started") +self:SetCSARRadius() +self:__StartTasks(5) +self.MaxCSAR=nil +self.CountCSAR=0 +self:HandleEvent(EVENTS.Ejection) +return self +end +function TASK_CARGO_DISPATCHER:SetCSARZones(SetZonesCSAR) +self.SetZonesCSAR=SetZonesCSAR +end +function TASK_CARGO_DISPATCHER:SetMaxCSAR(MaxCSAR) +self.MaxCSAR=MaxCSAR +end +function TASK_CARGO_DISPATCHER:OnEventEjection(EventData) +self:F({EventData=EventData}) +if self.CSARTasks==true then +local CSARCoordinate=EventData.IniUnit:GetCoordinate() +local CSARCoalition=EventData.IniUnit:GetCoalition() +local CSARCountry=EventData.IniUnit:GetCountry() +local CSARHeading=EventData.IniUnit:GetHeading() +if CSARCoalition==self.Mission:GetCommandCenter():GetCoalition()then +if not self.SetZonesCSAR or(self.SetZonesCSAR and self.SetZonesCSAR:IsCoordinateInZone(CSARCoordinate))then +if not self.MaxCSAR or(self.MaxCSAR and self.CountCSAR