env.info('*** MOOSE GITHUB Commit Hash ID: 2025-04-03T14:21:58+02:00-b7b6c1ea19b5fe0685a71aa1ac72f69c9c46f696 ***') if not MOOSE_DEVELOPMENT_FOLDER then MOOSE_DEVELOPMENT_FOLDER='Scripts' end ModuleLoader=MOOSE_DEVELOPMENT_FOLDER..'/Moose/Modules.lua' if io then local f=io.open(ModuleLoader,"r") if f~=nil then io.close(f) env.info('*** MOOSE DYNAMIC INCLUDE START *** ') local base=_G __Moose={} __Moose.Include=function(IncludeFile) if not __Moose.Includes[IncludeFile]then __Moose.Includes[IncludeFile]=IncludeFile local f=assert(base.loadfile(IncludeFile)) if f==nil then error("Moose: Could not load Moose file "..IncludeFile) else env.info("Moose: "..IncludeFile.." dynamically loaded.") return f() end end end __Moose.Includes={} __Moose.Include(MOOSE_DEVELOPMENT_FOLDER..'/Moose/Modules.lua') BASE:TraceOnOff(true) env.info('*** MOOSE INCLUDE END *** ') do return end end else env.info('*** MOOSE DYNAMIC INCLUDE NOT POSSIBLE (Desanitize io to use it) *** ') end env.info('*** MOOSE STATIC INCLUDE START *** ') ENUMS={} env.setErrorMessageBoxEnabled(false) ENUMS.ROE={ WeaponFree=0, OpenFireWeaponFree=1, OpenFire=2, ReturnFire=3, WeaponHold=4, } ENUMS.ROT={ NoReaction=0, PassiveDefense=1, EvadeFire=2, BypassAndEscape=3, AllowAbortMission=4, } ENUMS.AlarmState={ Auto=0, Green=1, Red=2, } ENUMS.WeaponFlag={ LGB=2, TvGB=4, SNSGB=8, HEBomb=16, Penetrator=32, NapalmBomb=64, FAEBomb=128, ClusterBomb=256, Dispencer=512, CandleBomb=1024, ParachuteBomb=2147483648, LightRocket=2048, MarkerRocket=4096, CandleRocket=8192, HeavyRocket=16384, AntiRadarMissile=32768, AntiShipMissile=65536, AntiTankMissile=131072, FireAndForgetASM=262144, LaserASM=524288, TeleASM=1048576, CruiseMissile=2097152, AntiRadarMissile2=1073741824, SRAM=4194304, MRAAM=8388608, LRAAM=16777216, IR_AAM=33554432, SAR_AAM=67108864, AR_AAM=134217728, GunPod=268435456, BuiltInCannon=536870912, GuidedBomb=14, AnyUnguidedBomb=2147485680, AnyBomb=2147485694, AnyRocket=30720, GuidedASM=1572864, TacticalASM=1835008, AnyASM=4161536, AnyASM2=1077903360, AnyAAM=264241152, AnyAutonomousMissile=36012032, AnyMissile=268402688, Cannons=805306368, Torpedo=4294967296, Auto=3221225470, AutoDCS=1073741822, AnyAG=2956984318, AnyAA=264241152, AnyUnguided=2952822768, AnyGuided=268402702, } ENUMS.WeaponType={} ENUMS.WeaponType.Bomb={ LGB=2, TvGB=4, SNSGB=8, HEBomb=16, Penetrator=32, NapalmBomb=64, FAEBomb=128, ClusterBomb=256, Dispencer=512, CandleBomb=1024, ParachuteBomb=2147483648, GuidedBomb=14, AnyUnguidedBomb=2147485680, AnyBomb=2147485694, } ENUMS.WeaponType.Rocket={ LightRocket=2048, MarkerRocket=4096, CandleRocket=8192, HeavyRocket=16384, AnyRocket=30720, } ENUMS.WeaponType.Gun={ GunPod=268435456, BuiltInCannon=536870912, Cannons=805306368, } ENUMS.WeaponType.Missile={ AntiRadarMissile=32768, AntiShipMissile=65536, AntiTankMissile=131072, FireAndForgetASM=262144, LaserASM=524288, TeleASM=1048576, CruiseMissile=2097152, AntiRadarMissile2=1073741824, GuidedASM=1572864, TacticalASM=1835008, AnyASM=4161536, AnyASM2=1077903360, AnyAutonomousMissile=36012032, AnyMissile=268402688, } ENUMS.WeaponType.AAM={ SRAM=4194304, MRAAM=8388608, LRAAM=16777216, IR_AAM=33554432, SAR_AAM=67108864, AR_AAM=134217728, AnyAAM=264241152, } ENUMS.WeaponType.Torpedo={ Torpedo=4294967296, } ENUMS.WeaponType.Any={ Weapon=3221225470, AG=2956984318, AA=264241152, Unguided=2952822768, Guided=268402702, } ENUMS.MissionTask={ NOTHING="Nothing", AFAC="AFAC", ANTISHIPSTRIKE="Antiship Strike", AWACS="AWACS", CAP="CAP", CAS="CAS", ESCORT="Escort", GROUNDESCORT="Ground escort", FIGHTERSWEEP="Fighter Sweep", GROUNDATTACK="Ground Attack", INTERCEPT="Intercept", PINPOINTSTRIKE="Pinpoint Strike", RECONNAISSANCE="Reconnaissance", REFUELING="Refueling", RUNWAYATTACK="Runway Attack", SEAD="SEAD", TRANSPORT="Transport", } ENUMS.Formation={} ENUMS.Formation.FixedWing={} ENUMS.Formation.FixedWing.LineAbreast={} ENUMS.Formation.FixedWing.LineAbreast.Close=65537 ENUMS.Formation.FixedWing.LineAbreast.Open=65538 ENUMS.Formation.FixedWing.LineAbreast.Group=65539 ENUMS.Formation.FixedWing.Trail={} ENUMS.Formation.FixedWing.Trail.Close=131073 ENUMS.Formation.FixedWing.Trail.Open=131074 ENUMS.Formation.FixedWing.Trail.Group=131075 ENUMS.Formation.FixedWing.Wedge={} ENUMS.Formation.FixedWing.Wedge.Close=196609 ENUMS.Formation.FixedWing.Wedge.Open=196610 ENUMS.Formation.FixedWing.Wedge.Group=196611 ENUMS.Formation.FixedWing.EchelonRight={} ENUMS.Formation.FixedWing.EchelonRight.Close=262145 ENUMS.Formation.FixedWing.EchelonRight.Open=262146 ENUMS.Formation.FixedWing.EchelonRight.Group=262147 ENUMS.Formation.FixedWing.EchelonLeft={} ENUMS.Formation.FixedWing.EchelonLeft.Close=327681 ENUMS.Formation.FixedWing.EchelonLeft.Open=327682 ENUMS.Formation.FixedWing.EchelonLeft.Group=327683 ENUMS.Formation.FixedWing.FingerFour={} ENUMS.Formation.FixedWing.FingerFour.Close=393217 ENUMS.Formation.FixedWing.FingerFour.Open=393218 ENUMS.Formation.FixedWing.FingerFour.Group=393219 ENUMS.Formation.FixedWing.Spread={} ENUMS.Formation.FixedWing.Spread.Close=458753 ENUMS.Formation.FixedWing.Spread.Open=458754 ENUMS.Formation.FixedWing.Spread.Group=458755 ENUMS.Formation.FixedWing.BomberElement={} ENUMS.Formation.FixedWing.BomberElement.Close=786433 ENUMS.Formation.FixedWing.BomberElement.Open=786434 ENUMS.Formation.FixedWing.BomberElement.Group=786435 ENUMS.Formation.FixedWing.BomberElementHeight={} ENUMS.Formation.FixedWing.BomberElementHeight.Close=851968 ENUMS.Formation.FixedWing.FighterVic={} ENUMS.Formation.FixedWing.FighterVic.Close=917505 ENUMS.Formation.FixedWing.FighterVic.Open=917506 ENUMS.Formation.RotaryWing={} ENUMS.Formation.RotaryWing.Column={} ENUMS.Formation.RotaryWing.Column.D70=720896 ENUMS.Formation.RotaryWing.Wedge={} ENUMS.Formation.RotaryWing.Wedge.D70=8 ENUMS.Formation.RotaryWing.FrontRight={} ENUMS.Formation.RotaryWing.FrontRight.D300=655361 ENUMS.Formation.RotaryWing.FrontRight.D600=655362 ENUMS.Formation.RotaryWing.FrontLeft={} ENUMS.Formation.RotaryWing.FrontLeft.D300=655617 ENUMS.Formation.RotaryWing.FrontLeft.D600=655618 ENUMS.Formation.RotaryWing.EchelonRight={} ENUMS.Formation.RotaryWing.EchelonRight.D70=589825 ENUMS.Formation.RotaryWing.EchelonRight.D300=589826 ENUMS.Formation.RotaryWing.EchelonRight.D600=589827 ENUMS.Formation.RotaryWing.EchelonLeft={} ENUMS.Formation.RotaryWing.EchelonLeft.D70=590081 ENUMS.Formation.RotaryWing.EchelonLeft.D300=590082 ENUMS.Formation.RotaryWing.EchelonLeft.D600=590083 ENUMS.Formation.Vehicle={} ENUMS.Formation.Vehicle.Vee="Vee" ENUMS.Formation.Vehicle.EchelonRight="EchelonR" ENUMS.Formation.Vehicle.OffRoad="Off Road" ENUMS.Formation.Vehicle.Rank="Rank" ENUMS.Formation.Vehicle.EchelonLeft="EchelonL" ENUMS.Formation.Vehicle.OnRoad="On Road" ENUMS.Formation.Vehicle.Cone="Cone" ENUMS.Formation.Vehicle.Diamond="Diamond" ENUMS.FormationOld={} ENUMS.FormationOld.FixedWing={} ENUMS.FormationOld.FixedWing.LineAbreast=1 ENUMS.FormationOld.FixedWing.Trail=2 ENUMS.FormationOld.FixedWing.Wedge=3 ENUMS.FormationOld.FixedWing.EchelonRight=4 ENUMS.FormationOld.FixedWing.EchelonLeft=5 ENUMS.FormationOld.FixedWing.FingerFour=6 ENUMS.FormationOld.FixedWing.SpreadFour=7 ENUMS.FormationOld.FixedWing.BomberElement=12 ENUMS.FormationOld.FixedWing.BomberElementHeight=13 ENUMS.FormationOld.FixedWing.FighterVic=14 ENUMS.FormationOld.RotaryWing={} ENUMS.FormationOld.RotaryWing.Wedge=8 ENUMS.FormationOld.RotaryWing.Echelon=9 ENUMS.FormationOld.RotaryWing.Front=10 ENUMS.FormationOld.RotaryWing.Column=11 ENUMS.Morse={} ENUMS.Morse.A="* -" ENUMS.Morse.B="- * * *" ENUMS.Morse.C="- * - *" ENUMS.Morse.D="- * *" ENUMS.Morse.E="*" ENUMS.Morse.F="* * - *" ENUMS.Morse.G="- - *" ENUMS.Morse.H="* * * *" ENUMS.Morse.I="* *" ENUMS.Morse.J="* - - -" ENUMS.Morse.K="- * -" ENUMS.Morse.L="* - * *" ENUMS.Morse.M="- -" ENUMS.Morse.N="- *" ENUMS.Morse.O="- - -" ENUMS.Morse.P="* - - *" ENUMS.Morse.Q="- - * -" ENUMS.Morse.R="* - *" ENUMS.Morse.S="* * *" ENUMS.Morse.T="-" ENUMS.Morse.U="* * -" ENUMS.Morse.V="* * * -" ENUMS.Morse.W="* - -" ENUMS.Morse.X="- * * -" ENUMS.Morse.Y="- * - -" ENUMS.Morse.Z="- - * *" ENUMS.Morse.N1="* - - - -" ENUMS.Morse.N2="* * - - -" ENUMS.Morse.N3="* * * - -" ENUMS.Morse.N4="* * * * -" ENUMS.Morse.N5="* * * * *" ENUMS.Morse.N6="- * * * *" ENUMS.Morse.N7="- - * * *" ENUMS.Morse.N8="- - - * *" ENUMS.Morse.N9="- - - - *" ENUMS.Morse.N0="- - - - -" ENUMS.Morse[" "]=" " ENUMS.ISOLang= { Arabic='AR', Chinese='ZH', English='EN', French='FR', German='DE', Russian='RU', Spanish='ES', Japanese='JA', Italian='IT', } ENUMS.Phonetic= { A='Alpha', B='Bravo', C='Charlie', D='Delta', E='Echo', F='Foxtrot', G='Golf', H='Hotel', I='India', J='Juliett', K='Kilo', L='Lima', M='Mike', N='November', O='Oscar', P='Papa', Q='Quebec', R='Romeo', S='Sierra', T='Tango', U='Uniform', V='Victor', W='Whiskey', X='Xray', Y='Yankee', Z='Zulu', } ENUMS.ReportingName= { NATO={ Dragon="JF-17", Fagot="MiG-15", Farmer="MiG-19", Felon="Su-57", Fencer="Su-24", Fishbed="MiG-21", Fitter="Su-17", Flogger="MiG-23", Flogger_D="MiG-27", Flagon="Su-15", Foxbat="MiG-25", Fulcrum="MiG-29", Foxhound="MiG-31", Flanker="Su-27", Flanker_C="Su-30", Flanker_E="Su-35", Flanker_F="Su-37", Flanker_L="J-11A", Firebird="J-10", Sea_Flanker="Su-33", Fullback="Su-34", Frogfoot="Su-25", Tomcat="F-14", Mirage="Mirage", Codling="Yak-40", Maya="L-39", Warthog="A-10", Skyhawk="A-4E", Viggen="AJS37", Harrier_B="AV8BNA", Harrier="AV-8B", Spirit="B-2", Aviojet="C-101", Nighthawk="F-117A", Eagle="F-15C", Mudhen="F-15E", Viper="F-16", Phantom="F-4E", Tiger="F-5", Sabre="F-86", Hornet="A-18", Hawk="Hawk", Albatros="L-39", Goshawk="T-45", Starfighter="F-104", Tornado="Tornado", Atlas="A400", Lancer="B1-B", Stratofortress="B-52H", Hercules="C-130", Super_Hercules="Hercules", Globemaster="C-17", Greyhound="C-2A", Galaxy="C-5", Hawkeye="E-2D", Sentry="E-3A", Stratotanker="KC-135", Gasstation="KC-135MPRS", Extender="KC-10", Orion="P-3C", Viking="S-3B", Osprey="V-22", Badger="H6-J", Bear_J="Tu-142", Bear="Tu-95", Blinder="Tu-22", Blackjack="Tu-160", Clank="An-30", Curl="An-26", Candid="IL-76", Midas="IL-78", Mainstay="A-50", Mainring="KJ-2000", Yak="Yak-52", Helix="Ka-27", Shark="Ka-50", Hind="Mi-24", Halo="Mi-26", Hip="Mi-8", Havoc="Mi-28", Gazelle="SA342", Huey="UH-1H", Cobra="AH-1", Apache="AH-64", Chinook="CH-47", Sea_Stallion="CH-53", Kiowa="OH-58", Seahawk="SH-60", Blackhawk="UH-60", Sea_King="S-61", UCAV="WingLoong", Reaper="MQ-9", Predator="MQ-1A", } } ENUMS.Link16Power={ none=0, low=1, medium=2, high=3, } ENUMS.Storage={ weapons={ missiles={}, bombs={}, nurs={}, containers={}, droptanks={}, adapters={}, torpedoes={}, Gazelle={}, CH47={}, OH58={}, UH1H={}, AH64D={}, } } ENUMS.Storage.weapons.nurs.SNEB_TYPE253_F1B="weapons.nurs.SNEB_TYPE253_F1B" ENUMS.Storage.weapons.missiles.P_24T="weapons.missiles.P_24T" ENUMS.Storage.weapons.bombs.BLU_3B_OLD="weapons.bombs.BLU-3B_OLD" ENUMS.Storage.weapons.missiles.AGM_154="weapons.missiles.AGM_154" ENUMS.Storage.weapons.nurs.HYDRA_70_M151_M433="weapons.nurs.HYDRA_70_M151_M433" ENUMS.Storage.weapons.bombs.SAM_Avenger_M1097_Skid_7090lb="weapons.bombs.SAM Avenger M1097 Skid [7090lb]" ENUMS.Storage.weapons.bombs.British_GP_250LB_Bomb_Mk5="weapons.bombs.British_GP_250LB_Bomb_Mk5" ENUMS.Storage.weapons.containers.OV10_SMOKE="weapons.containers.{OV10_SMOKE}" ENUMS.Storage.weapons.bombs.BLU_4B_OLD="weapons.bombs.BLU-4B_OLD" ENUMS.Storage.weapons.bombs.FAB_500M54="weapons.bombs.FAB-500M54" ENUMS.Storage.weapons.bombs.GBU_38="weapons.bombs.GBU_38" ENUMS.Storage.weapons.containers.F_15E_AXQ_14_DATALINK="weapons.containers.F-15E_AXQ-14_DATALINK" ENUMS.Storage.weapons.bombs.BEER_BOMB="weapons.bombs.BEER_BOMB" ENUMS.Storage.weapons.bombs.P_50T="weapons.bombs.P-50T" ENUMS.Storage.weapons.nurs.C_8CM_GN="weapons.nurs.C_8CM_GN" ENUMS.Storage.weapons.bombs.FAB_500SL="weapons.bombs.FAB-500SL" ENUMS.Storage.weapons.bombs.KAB_1500Kr="weapons.bombs.KAB_1500Kr" ENUMS.Storage.weapons.bombs.two50_2="weapons.bombs.250-2" ENUMS.Storage.weapons.droptanks.Spitfire_tank_1="weapons.droptanks.Spitfire_tank_1" ENUMS.Storage.weapons.missiles.AGM_65G="weapons.missiles.AGM_65G" ENUMS.Storage.weapons.missiles.AGM_65A="weapons.missiles.AGM_65A" ENUMS.Storage.weapons.containers.Hercules_JATO="weapons.containers.Hercules_JATO" ENUMS.Storage.weapons.nurs.HYDRA_70_M259="weapons.nurs.HYDRA_70_M259" ENUMS.Storage.weapons.missiles.AGM_84E="weapons.missiles.AGM_84E" ENUMS.Storage.weapons.bombs.AN_M30A1="weapons.bombs.AN_M30A1" ENUMS.Storage.weapons.nurs.C_25="weapons.nurs.C_25" ENUMS.Storage.weapons.containers.AV8BNA_ALQ164="weapons.containers.AV8BNA_ALQ164" ENUMS.Storage.weapons.containers.lav_25="weapons.containers.lav-25" ENUMS.Storage.weapons.missiles.P_60="weapons.missiles.P_60" ENUMS.Storage.weapons.bombs.FAB_1500="weapons.bombs.FAB_1500" ENUMS.Storage.weapons.droptanks.FuelTank_350L="weapons.droptanks.FuelTank_350L" ENUMS.Storage.weapons.bombs.AAA_Vulcan_M163_Skid_21577lb="weapons.bombs.AAA Vulcan M163 Skid [21577lb]" ENUMS.Storage.weapons.missiles.Kormoran="weapons.missiles.Kormoran" ENUMS.Storage.weapons.droptanks.HB_F14_EXT_DROPTANK_EMPTY="weapons.droptanks.HB_F14_EXT_DROPTANK_EMPTY" ENUMS.Storage.weapons.droptanks.FuelTank_150L="weapons.droptanks.FuelTank_150L" ENUMS.Storage.weapons.missiles.Rb_15F_for_A_I="weapons.missiles.Rb 15F (for A.I.)" ENUMS.Storage.weapons.missiles.RB75T="weapons.missiles.RB75T" ENUMS.Storage.weapons.missiles.Vikhr_M="weapons.missiles.Vikhr_M" ENUMS.Storage.weapons.nurs.FFAR_M156_WP="weapons.nurs.FFAR M156 WP" ENUMS.Storage.weapons.nurs.British_HE_60LBSAPNo2_3INCHNo1="weapons.nurs.British_HE_60LBSAPNo2_3INCHNo1" ENUMS.Storage.weapons.missiles.DWS39_MJ2="weapons.missiles.DWS39_MJ2" ENUMS.Storage.weapons.bombs.HEBOMBD="weapons.bombs.HEBOMBD" ENUMS.Storage.weapons.missiles.CATM_9M="weapons.missiles.CATM_9M" ENUMS.Storage.weapons.bombs.Mk_81="weapons.bombs.Mk_81" ENUMS.Storage.weapons.droptanks.Drop_Tank_300_Liter="weapons.droptanks.Drop_Tank_300_Liter" ENUMS.Storage.weapons.containers.HMMWV_M1025="weapons.containers.HMMWV_M1025" ENUMS.Storage.weapons.bombs.SAM_CHAPARRAL_Air_21624lb="weapons.bombs.SAM CHAPARRAL Air [21624lb]" ENUMS.Storage.weapons.missiles.AGM_154A="weapons.missiles.AGM_154A" ENUMS.Storage.weapons.bombs.Mk_84AIR_TP="weapons.bombs.Mk_84AIR_TP" ENUMS.Storage.weapons.bombs.GBU_31_V_3B="weapons.bombs.GBU_31_V_3B" ENUMS.Storage.weapons.nurs.C_8CM_WH="weapons.nurs.C_8CM_WH" ENUMS.Storage.weapons.missiles.Matra_Super_530D="weapons.missiles.Matra Super 530D" ENUMS.Storage.weapons.nurs.ARF8M3TPSM="weapons.nurs.ARF8M3TPSM" ENUMS.Storage.weapons.missiles.TGM_65H="weapons.missiles.TGM_65H" ENUMS.Storage.weapons.nurs.M8rocket="weapons.nurs.M8rocket" ENUMS.Storage.weapons.bombs.GBU_27="weapons.bombs.GBU_27" ENUMS.Storage.weapons.missiles.AGR_20A="weapons.missiles.AGR_20A" ENUMS.Storage.weapons.missiles.LS_6_250="weapons.missiles.LS-6-250" ENUMS.Storage.weapons.droptanks.M2KC_RPL_522_EMPTY="weapons.droptanks.M2KC_RPL_522_EMPTY" ENUMS.Storage.weapons.droptanks.M2KC_02_RPL541="weapons.droptanks.M2KC_02_RPL541" ENUMS.Storage.weapons.missiles.AGM_45="weapons.missiles.AGM_45" ENUMS.Storage.weapons.missiles.AGM_84A="weapons.missiles.AGM_84A" ENUMS.Storage.weapons.bombs.APC_BTR_80_Air_23936lb="weapons.bombs.APC BTR-80 Air [23936lb]" ENUMS.Storage.weapons.missiles.P_33E="weapons.missiles.P_33E" ENUMS.Storage.weapons.missiles.Ataka_9M120="weapons.missiles.Ataka_9M120" ENUMS.Storage.weapons.bombs.MK76="weapons.bombs.MK76" ENUMS.Storage.weapons.bombs.AB_250_2_SD_2="weapons.bombs.AB_250_2_SD_2" ENUMS.Storage.weapons.missiles.Rb_05A="weapons.missiles.Rb 05A" ENUMS.Storage.weapons.bombs.ART_GVOZDIKA_34720lb="weapons.bombs.ART GVOZDIKA [34720lb]" ENUMS.Storage.weapons.bombs.Generic_Crate_20000lb="weapons.bombs.Generic Crate [20000lb]" ENUMS.Storage.weapons.bombs.FAB_100SV="weapons.bombs.FAB_100SV" ENUMS.Storage.weapons.bombs.BetAB_500="weapons.bombs.BetAB_500" ENUMS.Storage.weapons.droptanks.M2KC_02_RPL541_EMPTY="weapons.droptanks.M2KC_02_RPL541_EMPTY" ENUMS.Storage.weapons.droptanks.PTB600_MIG15="weapons.droptanks.PTB600_MIG15" ENUMS.Storage.weapons.missiles.Rb_24J="weapons.missiles.Rb 24J" ENUMS.Storage.weapons.nurs.C_8CM_BU="weapons.nurs.C_8CM_BU" ENUMS.Storage.weapons.nurs.SNEB_TYPE259E_F1B="weapons.nurs.SNEB_TYPE259E_F1B" ENUMS.Storage.weapons.nurs.WGr21="weapons.nurs.WGr21" ENUMS.Storage.weapons.bombs.SAMP250HD="weapons.bombs.SAMP250HD" ENUMS.Storage.weapons.containers.alq_184long="weapons.containers.alq-184long" ENUMS.Storage.weapons.nurs.SNEB_TYPE259E_H1="weapons.nurs.SNEB_TYPE259E_H1" ENUMS.Storage.weapons.bombs.British_SAP_250LB_Bomb_Mk5="weapons.bombs.British_SAP_250LB_Bomb_Mk5" ENUMS.Storage.weapons.bombs.Transport_UAZ_469_Air_3747lb="weapons.bombs.Transport UAZ-469 Air [3747lb]" ENUMS.Storage.weapons.bombs.Mk_83CT="weapons.bombs.Mk_83CT" ENUMS.Storage.weapons.missiles.AIM_7P="weapons.missiles.AIM-7P" ENUMS.Storage.weapons.missiles.AT_6="weapons.missiles.AT_6" ENUMS.Storage.weapons.nurs.SNEB_TYPE254_H1_GREEN="weapons.nurs.SNEB_TYPE254_H1_GREEN" ENUMS.Storage.weapons.nurs.SNEB_TYPE250_F1B="weapons.nurs.SNEB_TYPE250_F1B" ENUMS.Storage.weapons.containers.U22A="weapons.containers.U22A" ENUMS.Storage.weapons.bombs.British_GP_250LB_Bomb_Mk1="weapons.bombs.British_GP_250LB_Bomb_Mk1" ENUMS.Storage.weapons.bombs.CBU_105="weapons.bombs.CBU_105" ENUMS.Storage.weapons.droptanks.FW_190_Fuel_Tank="weapons.droptanks.FW-190_Fuel-Tank" ENUMS.Storage.weapons.missiles.X_58="weapons.missiles.X_58" ENUMS.Storage.weapons.missiles.BK90_MJ1_MJ2="weapons.missiles.BK90_MJ1_MJ2" ENUMS.Storage.weapons.missiles.TGM_65D="weapons.missiles.TGM_65D" ENUMS.Storage.weapons.containers.BRD_4_250="weapons.containers.BRD-4-250" ENUMS.Storage.weapons.missiles.P_73="weapons.missiles.P_73" ENUMS.Storage.weapons.bombs.AN_M66="weapons.bombs.AN_M66" ENUMS.Storage.weapons.bombs.APC_LAV_25_Air_22520lb="weapons.bombs.APC LAV-25 Air [22520lb]" ENUMS.Storage.weapons.missiles.AIM_7MH="weapons.missiles.AIM-7MH" ENUMS.Storage.weapons.containers.MB339_TravelPod="weapons.containers.MB339_TravelPod" ENUMS.Storage.weapons.bombs.GBU_12="weapons.bombs.GBU_12" ENUMS.Storage.weapons.bombs.SC_250_T3_J="weapons.bombs.SC_250_T3_J" ENUMS.Storage.weapons.missiles.KD_20="weapons.missiles.KD-20" ENUMS.Storage.weapons.missiles.AGM_86C="weapons.missiles.AGM_86C" ENUMS.Storage.weapons.missiles.X_35="weapons.missiles.X_35" ENUMS.Storage.weapons.bombs.MK106="weapons.bombs.MK106" ENUMS.Storage.weapons.bombs.BETAB_500S="weapons.bombs.BETAB-500S" ENUMS.Storage.weapons.nurs.C_5="weapons.nurs.C_5" ENUMS.Storage.weapons.nurs.S_24B="weapons.nurs.S-24B" ENUMS.Storage.weapons.bombs.British_MC_500LB_Bomb_Mk2="weapons.bombs.British_MC_500LB_Bomb_Mk2" ENUMS.Storage.weapons.containers.ANAWW_13="weapons.containers.ANAWW_13" ENUMS.Storage.weapons.droptanks.droptank_108_gal="weapons.droptanks.droptank_108_gal" ENUMS.Storage.weapons.droptanks.DFT_300_GAL_A4E_LR="weapons.droptanks.DFT_300_GAL_A4E_LR" ENUMS.Storage.weapons.bombs.CBU_87="weapons.bombs.CBU_87" ENUMS.Storage.weapons.missiles.GAR_8="weapons.missiles.GAR-8" ENUMS.Storage.weapons.bombs.BELOUGA="weapons.bombs.BELOUGA" ENUMS.Storage.weapons.containers.EclairM_33="weapons.containers.{EclairM_33}" ENUMS.Storage.weapons.bombs.ART_2S9_NONA_Air_19140lb="weapons.bombs.ART 2S9 NONA Air [19140lb]" ENUMS.Storage.weapons.bombs.BR_250="weapons.bombs.BR_250" ENUMS.Storage.weapons.bombs.IAB_500="weapons.bombs.IAB-500" ENUMS.Storage.weapons.containers.AN_ASQ_228="weapons.containers.AN_ASQ_228" ENUMS.Storage.weapons.missiles.P_27P="weapons.missiles.P_27P" ENUMS.Storage.weapons.bombs.SD_250_Stg="weapons.bombs.SD_250_Stg" ENUMS.Storage.weapons.missiles.R_530F_IR="weapons.missiles.R_530F_IR" ENUMS.Storage.weapons.bombs.British_SAP_500LB_Bomb_Mk5="weapons.bombs.British_SAP_500LB_Bomb_Mk5" ENUMS.Storage.weapons.bombs.FAB_250M54="weapons.bombs.FAB-250M54" ENUMS.Storage.weapons.containers.M2KC_AAF="weapons.containers.{M2KC_AAF}" ENUMS.Storage.weapons.missiles.CM_802AKG_AI="weapons.missiles.CM-802AKG_AI" ENUMS.Storage.weapons.bombs.CBU_103="weapons.bombs.CBU_103" ENUMS.Storage.weapons.containers.US_M10_SMOKE_TANK_RED="weapons.containers.{US_M10_SMOKE_TANK_RED}" ENUMS.Storage.weapons.missiles.X_29T="weapons.missiles.X_29T" ENUMS.Storage.weapons.bombs.HEMTT_TFFT_34400lb="weapons.bombs.HEMTT TFFT [34400lb]" ENUMS.Storage.weapons.missiles.C_701IR="weapons.missiles.C-701IR" ENUMS.Storage.weapons.containers.fullCargoSeats="weapons.containers.fullCargoSeats" ENUMS.Storage.weapons.bombs.GBU_15_V_31_B="weapons.bombs.GBU_15_V_31_B" ENUMS.Storage.weapons.bombs.APC_M1043_HMMWV_Armament_Air_7023lb="weapons.bombs.APC M1043 HMMWV Armament Air [7023lb]" ENUMS.Storage.weapons.missiles.PL_5EII="weapons.missiles.PL-5EII" ENUMS.Storage.weapons.bombs.SC_250_T1_L2="weapons.bombs.SC_250_T1_L2" ENUMS.Storage.weapons.torpedoes.mk46torp_name="weapons.torpedoes.mk46torp_name" ENUMS.Storage.weapons.containers.F_15E_AAQ_33_XR_ATP_SE="weapons.containers.F-15E_AAQ-33_XR_ATP-SE" ENUMS.Storage.weapons.missiles.AIM_7="weapons.missiles.AIM_7" ENUMS.Storage.weapons.missiles.AGM_122="weapons.missiles.AGM_122" ENUMS.Storage.weapons.bombs.HEBOMB="weapons.bombs.HEBOMB" ENUMS.Storage.weapons.bombs.CBU_97="weapons.bombs.CBU_97" ENUMS.Storage.weapons.bombs.MK_81SE="weapons.bombs.MK-81SE" ENUMS.Storage.weapons.nurs.Zuni_127="weapons.nurs.Zuni_127" ENUMS.Storage.weapons.containers.M2KC_AGF="weapons.containers.{M2KC_AGF}" ENUMS.Storage.weapons.droptanks.Hercules_ExtFuelTank="weapons.droptanks.Hercules_ExtFuelTank" ENUMS.Storage.weapons.containers.SMOKE_WHITE="weapons.containers.{SMOKE_WHITE}" ENUMS.Storage.weapons.droptanks.droptank_150_gal="weapons.droptanks.droptank_150_gal" ENUMS.Storage.weapons.nurs.HYDRA_70_WTU1B="weapons.nurs.HYDRA_70_WTU1B" ENUMS.Storage.weapons.missiles.GB_6_SFW="weapons.missiles.GB-6-SFW" ENUMS.Storage.weapons.missiles.KD_63="weapons.missiles.KD-63" ENUMS.Storage.weapons.bombs.GBU_28="weapons.bombs.GBU_28" ENUMS.Storage.weapons.nurs.C_8CM_YE="weapons.nurs.C_8CM_YE" ENUMS.Storage.weapons.droptanks.HB_F14_EXT_DROPTANK="weapons.droptanks.HB_F14_EXT_DROPTANK" ENUMS.Storage.weapons.missiles.Super_530F="weapons.missiles.Super_530F" ENUMS.Storage.weapons.missiles.Ataka_9M220="weapons.missiles.Ataka_9M220" ENUMS.Storage.weapons.bombs.BDU_33="weapons.bombs.BDU_33" ENUMS.Storage.weapons.bombs.British_GP_250LB_Bomb_Mk4="weapons.bombs.British_GP_250LB_Bomb_Mk4" ENUMS.Storage.weapons.missiles.TOW="weapons.missiles.TOW" ENUMS.Storage.weapons.bombs.ATGM_M1045_HMMWV_TOW_Air_7183lb="weapons.bombs.ATGM M1045 HMMWV TOW Air [7183lb]" ENUMS.Storage.weapons.missiles.X_25MR="weapons.missiles.X_25MR" ENUMS.Storage.weapons.droptanks.fueltank230="weapons.droptanks.fueltank230" ENUMS.Storage.weapons.droptanks.PTB_490C_MIG21="weapons.droptanks.PTB-490C-MIG21" ENUMS.Storage.weapons.bombs.M1025_HMMWV_Air_6160lb="weapons.bombs.M1025 HMMWV Air [6160lb]" ENUMS.Storage.weapons.nurs.SNEB_TYPE254_F1B_GREEN="weapons.nurs.SNEB_TYPE254_F1B_GREEN" ENUMS.Storage.weapons.missiles.R_550="weapons.missiles.R_550" ENUMS.Storage.weapons.bombs.KAB_1500LG="weapons.bombs.KAB_1500LG" ENUMS.Storage.weapons.missiles.AGM_84D="weapons.missiles.AGM_84D" ENUMS.Storage.weapons.missiles.YJ_83K="weapons.missiles.YJ-83K" ENUMS.Storage.weapons.missiles.AIM_54C_Mk47="weapons.missiles.AIM_54C_Mk47" ENUMS.Storage.weapons.missiles.BRM_1_90MM="weapons.missiles.BRM-1_90MM" ENUMS.Storage.weapons.missiles.Ataka_9M120F="weapons.missiles.Ataka_9M120F" ENUMS.Storage.weapons.droptanks.Eleven00L_Tank="weapons.droptanks.1100L Tank" ENUMS.Storage.weapons.bombs.BAP_100="weapons.bombs.BAP_100" ENUMS.Storage.weapons.adapters.lau_88="weapons.adapters.lau-88" ENUMS.Storage.weapons.missiles.P_40T="weapons.missiles.P_40T" ENUMS.Storage.weapons.missiles.GB_6="weapons.missiles.GB-6" ENUMS.Storage.weapons.bombs.FAB_250M54TU="weapons.bombs.FAB-250M54TU" ENUMS.Storage.weapons.missiles.DWS39_MJ1="weapons.missiles.DWS39_MJ1" ENUMS.Storage.weapons.missiles.CM_802AKG="weapons.missiles.CM-802AKG" ENUMS.Storage.weapons.bombs.FAB_250="weapons.bombs.FAB_250" ENUMS.Storage.weapons.missiles.C_802AK="weapons.missiles.C_802AK" ENUMS.Storage.weapons.bombs.SD_500_A="weapons.bombs.SD_500_A" ENUMS.Storage.weapons.bombs.GBU_32_V_2B="weapons.bombs.GBU_32_V_2B" ENUMS.Storage.weapons.containers.marder="weapons.containers.marder" ENUMS.Storage.weapons.missiles.ADM_141B="weapons.missiles.ADM_141B" ENUMS.Storage.weapons.bombs.ROCKEYE="weapons.bombs.ROCKEYE" ENUMS.Storage.weapons.missiles.BK90_MJ1="weapons.missiles.BK90_MJ1" ENUMS.Storage.weapons.containers.BTR_80="weapons.containers.BTR-80" ENUMS.Storage.weapons.bombs.SAM_ROLAND_ADS_34720lb="weapons.bombs.SAM ROLAND ADS [34720lb]" ENUMS.Storage.weapons.containers.wmd7="weapons.containers.wmd7" ENUMS.Storage.weapons.missiles.C_701T="weapons.missiles.C-701T" ENUMS.Storage.weapons.missiles.AIM_7E_2="weapons.missiles.AIM-7E-2" ENUMS.Storage.weapons.nurs.HVAR="weapons.nurs.HVAR" ENUMS.Storage.weapons.containers.HMMWV_M1043="weapons.containers.HMMWV_M1043" ENUMS.Storage.weapons.droptanks.PTB_800_MIG21="weapons.droptanks.PTB-800-MIG21" ENUMS.Storage.weapons.missiles.AGM_114="weapons.missiles.AGM_114" ENUMS.Storage.weapons.bombs.APC_M1126_Stryker_ICV_29542lb="weapons.bombs.APC M1126 Stryker ICV [29542lb]" ENUMS.Storage.weapons.bombs.APC_M113_Air_21624lb="weapons.bombs.APC M113 Air [21624lb]" ENUMS.Storage.weapons.bombs.M_117="weapons.bombs.M_117" ENUMS.Storage.weapons.missiles.AGM_65D="weapons.missiles.AGM_65D" ENUMS.Storage.weapons.droptanks.MB339_TT320_L="weapons.droptanks.MB339_TT320_L" ENUMS.Storage.weapons.missiles.AGM_86="weapons.missiles.AGM_86" ENUMS.Storage.weapons.bombs.BDU_45LGB="weapons.bombs.BDU_45LGB" ENUMS.Storage.weapons.missiles.AGM_65H="weapons.missiles.AGM_65H" ENUMS.Storage.weapons.nurs.RS_82="weapons.nurs.RS-82" ENUMS.Storage.weapons.nurs.SNEB_TYPE252_F1B="weapons.nurs.SNEB_TYPE252_F1B" ENUMS.Storage.weapons.bombs.BL_755="weapons.bombs.BL_755" ENUMS.Storage.weapons.containers.F_15E_AAQ_28_LITENING="weapons.containers.F-15E_AAQ-28_LITENING" ENUMS.Storage.weapons.nurs.SNEB_TYPE256_F1B="weapons.nurs.SNEB_TYPE256_F1B" ENUMS.Storage.weapons.missiles.AGM_84H="weapons.missiles.AGM_84H" ENUMS.Storage.weapons.missiles.AIM_54="weapons.missiles.AIM_54" ENUMS.Storage.weapons.missiles.X_31A="weapons.missiles.X_31A" ENUMS.Storage.weapons.bombs.KAB_500Kr="weapons.bombs.KAB_500Kr" ENUMS.Storage.weapons.containers.SPS_141_100="weapons.containers.SPS-141-100" ENUMS.Storage.weapons.missiles.BK90_MJ2="weapons.missiles.BK90_MJ2" ENUMS.Storage.weapons.missiles.Super_530D="weapons.missiles.Super_530D" ENUMS.Storage.weapons.bombs.CBU_52B="weapons.bombs.CBU_52B" ENUMS.Storage.weapons.droptanks.PTB_450="weapons.droptanks.PTB-450" ENUMS.Storage.weapons.bombs.IFV_MCV_80_34720lb="weapons.bombs.IFV MCV-80 [34720lb]" ENUMS.Storage.weapons.containers.Two_c9="weapons.containers.2-c9" ENUMS.Storage.weapons.missiles.AIM_9JULI="weapons.missiles.AIM-9JULI" ENUMS.Storage.weapons.droptanks.MB339_TT500_R="weapons.droptanks.MB339_TT500_R" ENUMS.Storage.weapons.nurs.C_8CM="weapons.nurs.C_8CM" ENUMS.Storage.weapons.containers.BARAX="weapons.containers.BARAX" ENUMS.Storage.weapons.missiles.P_40R="weapons.missiles.P_40R" ENUMS.Storage.weapons.missiles.YJ_12="weapons.missiles.YJ-12" ENUMS.Storage.weapons.missiles.CM_802AKG="weapons.missiles.CM_802AKG" ENUMS.Storage.weapons.nurs.SNEB_TYPE254_H1_YELLOW="weapons.nurs.SNEB_TYPE254_H1_YELLOW" ENUMS.Storage.weapons.bombs.Durandal="weapons.bombs.Durandal" ENUMS.Storage.weapons.droptanks.i16_eft="weapons.droptanks.i16_eft" ENUMS.Storage.weapons.droptanks.AV8BNA_AERO1D_EMPTY="weapons.droptanks.AV8BNA_AERO1D_EMPTY" ENUMS.Storage.weapons.containers.Hercules_Battle_Station_TGP="weapons.containers.Hercules_Battle_Station_TGP" ENUMS.Storage.weapons.nurs.C_8CM_VT="weapons.nurs.C_8CM_VT" ENUMS.Storage.weapons.missiles.PL_12="weapons.missiles.PL-12" ENUMS.Storage.weapons.missiles.R_3R="weapons.missiles.R-3R" ENUMS.Storage.weapons.bombs.GBU_54_V_1B="weapons.bombs.GBU_54_V_1B" ENUMS.Storage.weapons.droptanks.MB339_TT320_R="weapons.droptanks.MB339_TT320_R" ENUMS.Storage.weapons.bombs.RN_24="weapons.bombs.RN-24" ENUMS.Storage.weapons.containers.Twoc6m="weapons.containers.2c6m" ENUMS.Storage.weapons.bombs.ARV_BRDM_2_Air_12320lb="weapons.bombs.ARV BRDM-2 Air [12320lb]" ENUMS.Storage.weapons.bombs.ARV_BRDM_2_Skid_12210lb="weapons.bombs.ARV BRDM-2 Skid [12210lb]" ENUMS.Storage.weapons.nurs.SNEB_TYPE251_F1B="weapons.nurs.SNEB_TYPE251_F1B" ENUMS.Storage.weapons.missiles.X_41="weapons.missiles.X_41" ENUMS.Storage.weapons.containers.MIG21_SMOKE_WHITE="weapons.containers.{MIG21_SMOKE_WHITE}" ENUMS.Storage.weapons.bombs.MK_82AIR="weapons.bombs.MK_82AIR" ENUMS.Storage.weapons.missiles.R_530F_EM="weapons.missiles.R_530F_EM" ENUMS.Storage.weapons.bombs.SAMP400LD="weapons.bombs.SAMP400LD" ENUMS.Storage.weapons.bombs.FAB_50="weapons.bombs.FAB_50" ENUMS.Storage.weapons.bombs.AB_250_2_SD_10A="weapons.bombs.AB_250_2_SD_10A" ENUMS.Storage.weapons.missiles.ADM_141A="weapons.missiles.ADM_141A" ENUMS.Storage.weapons.containers.KBpod="weapons.containers.KBpod" ENUMS.Storage.weapons.bombs.British_GP_500LB_Bomb_Mk4="weapons.bombs.British_GP_500LB_Bomb_Mk4" ENUMS.Storage.weapons.missiles.AGM_65E="weapons.missiles.AGM_65E" ENUMS.Storage.weapons.containers.sa342_dipole_antenna="weapons.containers.sa342_dipole_antenna" ENUMS.Storage.weapons.bombs.OFAB_100_Jupiter="weapons.bombs.OFAB-100 Jupiter" ENUMS.Storage.weapons.nurs.SNEB_TYPE257_F1B="weapons.nurs.SNEB_TYPE257_F1B" ENUMS.Storage.weapons.missiles.Rb_04E_for_A_I="weapons.missiles.Rb 04E (for A.I.)" ENUMS.Storage.weapons.bombs.AN_M66A2="weapons.bombs.AN-M66A2" ENUMS.Storage.weapons.missiles.P_27T="weapons.missiles.P_27T" ENUMS.Storage.weapons.droptanks.LNS_VIG_XTANK="weapons.droptanks.LNS_VIG_XTANK" ENUMS.Storage.weapons.missiles.R_55="weapons.missiles.R-55" ENUMS.Storage.weapons.torpedoes.YU_6="weapons.torpedoes.YU-6" ENUMS.Storage.weapons.bombs.British_MC_250LB_Bomb_Mk2="weapons.bombs.British_MC_250LB_Bomb_Mk2" ENUMS.Storage.weapons.droptanks.PTB_120_F86F35="weapons.droptanks.PTB_120_F86F35" ENUMS.Storage.weapons.missiles.PL_8B="weapons.missiles.PL-8B" ENUMS.Storage.weapons.droptanks.F_15E_Drop_Tank_Empty="weapons.droptanks.F-15E_Drop_Tank_Empty" ENUMS.Storage.weapons.nurs.British_HE_60LBFNo1_3INCHNo1="weapons.nurs.British_HE_60LBFNo1_3INCHNo1" ENUMS.Storage.weapons.missiles.P_77="weapons.missiles.P_77" ENUMS.Storage.weapons.torpedoes.LTF_5B="weapons.torpedoes.LTF_5B" ENUMS.Storage.weapons.missiles.R_3S="weapons.missiles.R-3S" ENUMS.Storage.weapons.nurs.SNEB_TYPE253_H1="weapons.nurs.SNEB_TYPE253_H1" ENUMS.Storage.weapons.missiles.PL_8A="weapons.missiles.PL-8A" ENUMS.Storage.weapons.bombs.APC_BTR_82A_Skid_24888lb="weapons.bombs.APC BTR-82A Skid [24888lb]" ENUMS.Storage.weapons.containers.Sborka="weapons.containers.Sborka" ENUMS.Storage.weapons.missiles.AGM_65L="weapons.missiles.AGM_65L" ENUMS.Storage.weapons.missiles.X_28="weapons.missiles.X_28" ENUMS.Storage.weapons.missiles.TGM_65G="weapons.missiles.TGM_65G" ENUMS.Storage.weapons.nurs.SNEB_TYPE257_H1="weapons.nurs.SNEB_TYPE257_H1" ENUMS.Storage.weapons.missiles.RB75B="weapons.missiles.RB75B" ENUMS.Storage.weapons.missiles.X_25ML="weapons.missiles.X_25ML" ENUMS.Storage.weapons.droptanks.FPU_8A="weapons.droptanks.FPU_8A" ENUMS.Storage.weapons.bombs.BLG66="weapons.bombs.BLG66" ENUMS.Storage.weapons.nurs.C_8CM_RD="weapons.nurs.C_8CM_RD" ENUMS.Storage.weapons.containers.EclairM_06="weapons.containers.{EclairM_06}" ENUMS.Storage.weapons.bombs.RBK_500AO="weapons.bombs.RBK_500AO" ENUMS.Storage.weapons.missiles.AIM_9P="weapons.missiles.AIM-9P" ENUMS.Storage.weapons.bombs.British_GP_500LB_Bomb_Mk4_Short="weapons.bombs.British_GP_500LB_Bomb_Mk4_Short" ENUMS.Storage.weapons.containers.MB339_Vinten="weapons.containers.MB339_Vinten" ENUMS.Storage.weapons.missiles.Rb_15F="weapons.missiles.Rb 15F" ENUMS.Storage.weapons.nurs.ARAKM70BHE="weapons.nurs.ARAKM70BHE" ENUMS.Storage.weapons.bombs.AAA_Vulcan_M163_Air_21666lb="weapons.bombs.AAA Vulcan M163 Air [21666lb]" ENUMS.Storage.weapons.missiles.X_29L="weapons.missiles.X_29L" ENUMS.Storage.weapons.containers.F14_LANTIRN_TP="weapons.containers.{F14-LANTIRN-TP}" ENUMS.Storage.weapons.bombs.FAB_250_M62="weapons.bombs.FAB-250-M62" ENUMS.Storage.weapons.missiles.AIM_120C="weapons.missiles.AIM_120C" ENUMS.Storage.weapons.bombs.EWR_SBORKA_Air_21624lb="weapons.bombs.EWR SBORKA Air [21624lb]" ENUMS.Storage.weapons.bombs.SAMP250LD="weapons.bombs.SAMP250LD" ENUMS.Storage.weapons.droptanks.Spitfire_slipper_tank="weapons.droptanks.Spitfire_slipper_tank" ENUMS.Storage.weapons.missiles.LS_6_500="weapons.missiles.LS-6-500" ENUMS.Storage.weapons.bombs.GBU_31_V_4B="weapons.bombs.GBU_31_V_4B" ENUMS.Storage.weapons.droptanks.PTB400_MIG15="weapons.droptanks.PTB400_MIG15" ENUMS.Storage.weapons.containers.m_113="weapons.containers.m-113" ENUMS.Storage.weapons.bombs.SPG_M1128_Stryker_MGS_33036lb="weapons.bombs.SPG M1128 Stryker MGS [33036lb]" ENUMS.Storage.weapons.missiles.AIM_9L="weapons.missiles.AIM-9L" ENUMS.Storage.weapons.missiles.AIM_9X="weapons.missiles.AIM_9X" ENUMS.Storage.weapons.nurs.C_8="weapons.nurs.C_8" ENUMS.Storage.weapons.bombs.SAM_CHAPARRAL_Skid_21516lb="weapons.bombs.SAM CHAPARRAL Skid [21516lb]" ENUMS.Storage.weapons.missiles.P_27TE="weapons.missiles.P_27TE" ENUMS.Storage.weapons.bombs.ODAB_500PM="weapons.bombs.ODAB-500PM" ENUMS.Storage.weapons.bombs.MK77mod1_WPN="weapons.bombs.MK77mod1-WPN" ENUMS.Storage.weapons.droptanks.PTB400_MIG19="weapons.droptanks.PTB400_MIG19" ENUMS.Storage.weapons.torpedoes.Mark_46="weapons.torpedoes.Mark_46" ENUMS.Storage.weapons.containers.rightSeat="weapons.containers.rightSeat" ENUMS.Storage.weapons.containers.US_M10_SMOKE_TANK_ORANGE="weapons.containers.{US_M10_SMOKE_TANK_ORANGE}" ENUMS.Storage.weapons.bombs.SAB_100MN="weapons.bombs.SAB_100MN" ENUMS.Storage.weapons.nurs.FFAR_Mk5_HEAT="weapons.nurs.FFAR Mk5 HEAT" ENUMS.Storage.weapons.bombs.IFV_TPZ_FUCH_33440lb="weapons.bombs.IFV TPZ FUCH [33440lb]" ENUMS.Storage.weapons.bombs.IFV_M2A2_Bradley_34720lb="weapons.bombs.IFV M2A2 Bradley [34720lb]" ENUMS.Storage.weapons.bombs.MK77mod0_WPN="weapons.bombs.MK77mod0-WPN" ENUMS.Storage.weapons.containers.ASO_2="weapons.containers.ASO-2" ENUMS.Storage.weapons.bombs.Mk_84AIR_GP="weapons.bombs.Mk_84AIR_GP" ENUMS.Storage.weapons.nurs.S_24A="weapons.nurs.S-24A" ENUMS.Storage.weapons.bombs.RBK_250_275_AO_1SCH="weapons.bombs.RBK_250_275_AO_1SCH" ENUMS.Storage.weapons.bombs.Transport_Tigr_Skid_15730lb="weapons.bombs.Transport Tigr Skid [15730lb]" ENUMS.Storage.weapons.missiles.AIM_7F="weapons.missiles.AIM-7F" ENUMS.Storage.weapons.bombs.CBU_99="weapons.bombs.CBU_99" ENUMS.Storage.weapons.bombs.LUU_2B="weapons.bombs.LUU_2B" ENUMS.Storage.weapons.bombs.FAB_500TA="weapons.bombs.FAB-500TA" ENUMS.Storage.weapons.missiles.AGR_20_M282="weapons.missiles.AGR_20_M282" ENUMS.Storage.weapons.droptanks.MB339_FT330="weapons.droptanks.MB339_FT330" ENUMS.Storage.weapons.bombs.SAMP125LD="weapons.bombs.SAMP125LD" ENUMS.Storage.weapons.missiles.X_25MP="weapons.missiles.X_25MP" ENUMS.Storage.weapons.nurs.SNEB_TYPE252_H1="weapons.nurs.SNEB_TYPE252_H1" ENUMS.Storage.weapons.missiles.AGM_65F="weapons.missiles.AGM_65F" ENUMS.Storage.weapons.missiles.AIM_9P5="weapons.missiles.AIM-9P5" ENUMS.Storage.weapons.bombs.Transport_Tigr_Air_15900lb="weapons.bombs.Transport Tigr Air [15900lb]" ENUMS.Storage.weapons.nurs.SNEB_TYPE254_H1_RED="weapons.nurs.SNEB_TYPE254_H1_RED" ENUMS.Storage.weapons.nurs.FFAR_Mk1_HE="weapons.nurs.FFAR Mk1 HE" ENUMS.Storage.weapons.nurs.SPRD_99="weapons.nurs.SPRD-99" ENUMS.Storage.weapons.bombs.BIN_200="weapons.bombs.BIN_200" ENUMS.Storage.weapons.bombs.BLU_4B_GROUP="weapons.bombs.BLU_4B_GROUP" ENUMS.Storage.weapons.bombs.GBU_24="weapons.bombs.GBU_24" ENUMS.Storage.weapons.missiles.Rb_04E="weapons.missiles.Rb 04E" ENUMS.Storage.weapons.missiles.Rb_74="weapons.missiles.Rb 74" ENUMS.Storage.weapons.containers.leftSeat="weapons.containers.leftSeat" ENUMS.Storage.weapons.bombs.LS_6_100="weapons.bombs.LS-6-100" ENUMS.Storage.weapons.bombs.Transport_URAL_375_14815lb="weapons.bombs.Transport URAL-375 [14815lb]" ENUMS.Storage.weapons.containers.US_M10_SMOKE_TANK_GREEN="weapons.containers.{US_M10_SMOKE_TANK_GREEN}" ENUMS.Storage.weapons.missiles.X_22="weapons.missiles.X_22" ENUMS.Storage.weapons.containers.FAS="weapons.containers.FAS" ENUMS.Storage.weapons.nurs.S_25_O="weapons.nurs.S-25-O" ENUMS.Storage.weapons.droptanks.para="weapons.droptanks.para" ENUMS.Storage.weapons.droptanks.F_15E_Drop_Tank="weapons.droptanks.F-15E_Drop_Tank" ENUMS.Storage.weapons.droptanks.M2KC_08_RPL541_EMPTY="weapons.droptanks.M2KC_08_RPL541_EMPTY" ENUMS.Storage.weapons.missiles.X_31P="weapons.missiles.X_31P" ENUMS.Storage.weapons.bombs.RBK_500U="weapons.bombs.RBK_500U" ENUMS.Storage.weapons.missiles.AIM_54A_Mk47="weapons.missiles.AIM_54A_Mk47" ENUMS.Storage.weapons.droptanks.oiltank="weapons.droptanks.oiltank" ENUMS.Storage.weapons.missiles.AGM_154B="weapons.missiles.AGM_154B" ENUMS.Storage.weapons.containers.MB339_SMOKE_POD="weapons.containers.MB339_SMOKE-POD" ENUMS.Storage.weapons.containers.ECM_POD_L_175V="weapons.containers.{ECM_POD_L_175V}" ENUMS.Storage.weapons.droptanks.PTB_580G_F1="weapons.droptanks.PTB_580G_F1" ENUMS.Storage.weapons.containers.EclairM_15="weapons.containers.{EclairM_15}" ENUMS.Storage.weapons.containers.F_15E_AAQ_13_LANTIRN="weapons.containers.F-15E_AAQ-13_LANTIRN" ENUMS.Storage.weapons.droptanks.Eight00L_Tank_Empty="weapons.droptanks.800L Tank Empty" ENUMS.Storage.weapons.containers.One6c_hts_pod="weapons.containers.16c_hts_pod" ENUMS.Storage.weapons.bombs.AN_M81="weapons.bombs.AN-M81" ENUMS.Storage.weapons.droptanks.Mosquito_Drop_Tank_100gal="weapons.droptanks.Mosquito_Drop_Tank_100gal" ENUMS.Storage.weapons.droptanks.Mosquito_Drop_Tank_50gal="weapons.droptanks.Mosquito_Drop_Tank_50gal" ENUMS.Storage.weapons.droptanks.DFT_150_GAL_A4E="weapons.droptanks.DFT_150_GAL_A4E" ENUMS.Storage.weapons.missiles.AIM_9="weapons.missiles.AIM_9" ENUMS.Storage.weapons.bombs.IFV_BTR_D_Air_18040lb="weapons.bombs.IFV BTR-D Air [18040lb]" ENUMS.Storage.weapons.containers.EclairM_42="weapons.containers.{EclairM_42}" ENUMS.Storage.weapons.bombs.KAB_1500T="weapons.bombs.KAB_1500T" ENUMS.Storage.weapons.droptanks.PTB_490_MIG21="weapons.droptanks.PTB-490-MIG21" ENUMS.Storage.weapons.droptanks.PTB_200_F86F35="weapons.droptanks.PTB_200_F86F35" ENUMS.Storage.weapons.droptanks.PTB760_MIG19="weapons.droptanks.PTB760_MIG19" ENUMS.Storage.weapons.bombs.GBU_43_B_MOAB="weapons.bombs.GBU-43/B(MOAB)" ENUMS.Storage.weapons.torpedoes.G7A_T1="weapons.torpedoes.G7A_T1" ENUMS.Storage.weapons.bombs.IFV_BMD_1_Air_18040lb="weapons.bombs.IFV BMD-1 Air [18040lb]" ENUMS.Storage.weapons.bombs.SAM_LINEBACKER_34720lb="weapons.bombs.SAM LINEBACKER [34720lb]" ENUMS.Storage.weapons.containers.ais_pod_t50_r="weapons.containers.ais-pod-t50_r" ENUMS.Storage.weapons.containers.CE2_SMOKE_WHITE="weapons.containers.{CE2_SMOKE_WHITE}" ENUMS.Storage.weapons.droptanks.fuel_tank_230="weapons.droptanks.fuel_tank_230" ENUMS.Storage.weapons.droptanks.M2KC_RPL_522="weapons.droptanks.M2KC_RPL_522" ENUMS.Storage.weapons.missiles.AGM_130="weapons.missiles.AGM_130" ENUMS.Storage.weapons.droptanks.Eight00L_Tank="weapons.droptanks.800L Tank" ENUMS.Storage.weapons.bombs.IFV_BTR_D_Skid_17930lb="weapons.bombs.IFV BTR-D Skid [17930lb]" ENUMS.Storage.weapons.containers.bmp_1="weapons.containers.bmp-1" ENUMS.Storage.weapons.bombs.GBU_31="weapons.bombs.GBU_31" ENUMS.Storage.weapons.containers.aaq_28LEFT_litening="weapons.containers.aaq-28LEFT litening" ENUMS.Storage.weapons.missiles.Kh_66_Grom="weapons.missiles.Kh-66_Grom" ENUMS.Storage.weapons.containers.MIG21_SMOKE_RED="weapons.containers.{MIG21_SMOKE_RED}" ENUMS.Storage.weapons.containers.U22="weapons.containers.U22" ENUMS.Storage.weapons.bombs.IFV_BMD_1_Skid_17930lb="weapons.bombs.IFV BMD-1 Skid [17930lb]" ENUMS.Storage.weapons.droptanks.Bidon="weapons.droptanks.Bidon" ENUMS.Storage.weapons.bombs.GBU_31_V_2B="weapons.bombs.GBU_31_V_2B" ENUMS.Storage.weapons.bombs.Mk_82Y="weapons.bombs.Mk_82Y" ENUMS.Storage.weapons.containers.pl5eii="weapons.containers.pl5eii" ENUMS.Storage.weapons.bombs.RBK_500U_OAB_2_5RT="weapons.bombs.RBK_500U_OAB_2_5RT" ENUMS.Storage.weapons.bombs.British_GP_500LB_Bomb_Mk5="weapons.bombs.British_GP_500LB_Bomb_Mk5" ENUMS.Storage.weapons.containers.Eclair="weapons.containers.{Eclair}" ENUMS.Storage.weapons.nurs.S5MO_HEFRAG_FFAR="weapons.nurs.S5MO_HEFRAG_FFAR" ENUMS.Storage.weapons.bombs.BETAB_500M="weapons.bombs.BETAB-500M" ENUMS.Storage.weapons.bombs.Transport_M818_16000lb="weapons.bombs.Transport M818 [16000lb]" ENUMS.Storage.weapons.bombs.British_MC_250LB_Bomb_Mk1="weapons.bombs.British_MC_250LB_Bomb_Mk1" ENUMS.Storage.weapons.nurs.SNEB_TYPE251_H1="weapons.nurs.SNEB_TYPE251_H1" ENUMS.Storage.weapons.bombs.TYPE_200A="weapons.bombs.TYPE-200A" ENUMS.Storage.weapons.nurs.HYDRA_70_M151="weapons.nurs.HYDRA_70_M151" ENUMS.Storage.weapons.bombs.IFV_BMP_3_32912lb="weapons.bombs.IFV BMP-3 [32912lb]" ENUMS.Storage.weapons.bombs.APC_MTLB_Air_26400lb="weapons.bombs.APC MTLB Air [26400lb]" ENUMS.Storage.weapons.nurs.HYDRA_70_M229="weapons.nurs.HYDRA_70_M229" ENUMS.Storage.weapons.bombs.BDU_45="weapons.bombs.BDU_45" ENUMS.Storage.weapons.bombs.OFAB_100_120TU="weapons.bombs.OFAB-100-120TU" ENUMS.Storage.weapons.missiles.AIM_9J="weapons.missiles.AIM-9J" ENUMS.Storage.weapons.nurs.ARF8M3API="weapons.nurs.ARF8M3API" ENUMS.Storage.weapons.bombs.BetAB_500ShP="weapons.bombs.BetAB_500ShP" ENUMS.Storage.weapons.nurs.C_8OFP2="weapons.nurs.C_8OFP2" ENUMS.Storage.weapons.bombs.GBU_10="weapons.bombs.GBU_10" ENUMS.Storage.weapons.bombs.APC_MTLB_Skid_26290lb="weapons.bombs.APC MTLB Skid [26290lb]" ENUMS.Storage.weapons.nurs.SNEB_TYPE254_F1B_RED="weapons.nurs.SNEB_TYPE254_F1B_RED" ENUMS.Storage.weapons.missiles.X_65="weapons.missiles.X_65" ENUMS.Storage.weapons.missiles.R_550_M1="weapons.missiles.R_550_M1" ENUMS.Storage.weapons.missiles.AGM_65K="weapons.missiles.AGM_65K" ENUMS.Storage.weapons.nurs.SNEB_TYPE254_F1B_YELLOW="weapons.nurs.SNEB_TYPE254_F1B_YELLOW" ENUMS.Storage.weapons.missiles.AGM_88="weapons.missiles.AGM_88" ENUMS.Storage.weapons.nurs.C_8OM="weapons.nurs.C_8OM" ENUMS.Storage.weapons.bombs.SAM_ROLAND_LN_34720b="weapons.bombs.SAM ROLAND LN [34720b]" ENUMS.Storage.weapons.missiles.AIM_120="weapons.missiles.AIM_120" ENUMS.Storage.weapons.missiles.HOT3_MBDA="weapons.missiles.HOT3_MBDA" ENUMS.Storage.weapons.missiles.R_13M="weapons.missiles.R-13M" ENUMS.Storage.weapons.missiles.AIM_54C_Mk60="weapons.missiles.AIM_54C_Mk60" ENUMS.Storage.weapons.bombs.AAA_GEPARD_34720lb="weapons.bombs.AAA GEPARD [34720lb]" ENUMS.Storage.weapons.missiles.R_13M1="weapons.missiles.R-13M1" ENUMS.Storage.weapons.bombs.APC_Cobra_Air_10912lb="weapons.bombs.APC Cobra Air [10912lb]" ENUMS.Storage.weapons.bombs.RBK_250="weapons.bombs.RBK_250" ENUMS.Storage.weapons.bombs.SC_500_J="weapons.bombs.SC_500_J" ENUMS.Storage.weapons.missiles.AGM_114K="weapons.missiles.AGM_114K" ENUMS.Storage.weapons.missiles.ALARM="weapons.missiles.ALARM" ENUMS.Storage.weapons.bombs.Mk_83="weapons.bombs.Mk_83" ENUMS.Storage.weapons.missiles.AGM_65B="weapons.missiles.AGM_65B" ENUMS.Storage.weapons.bombs.MK_82SNAKEYE="weapons.bombs.MK_82SNAKEYE" ENUMS.Storage.weapons.nurs.HYDRA_70_MK1="weapons.nurs.HYDRA_70_MK1" ENUMS.Storage.weapons.bombs.BLG66_BELOUGA="weapons.bombs.BLG66_BELOUGA" ENUMS.Storage.weapons.containers.EclairM_51="weapons.containers.{EclairM_51}" ENUMS.Storage.weapons.missiles.AIM_54A_Mk60="weapons.missiles.AIM_54A_Mk60" ENUMS.Storage.weapons.droptanks.DFT_300_GAL_A4E="weapons.droptanks.DFT_300_GAL_A4E" ENUMS.Storage.weapons.bombs.ATGM_M1134_Stryker_30337lb="weapons.bombs.ATGM M1134 Stryker [30337lb]" ENUMS.Storage.weapons.bombs.BAT_120="weapons.bombs.BAT-120" ENUMS.Storage.weapons.missiles.DWS39_MJ1_MJ2="weapons.missiles.DWS39_MJ1_MJ2" ENUMS.Storage.weapons.containers.SPRD="weapons.containers.SPRD" ENUMS.Storage.weapons.bombs.BR_500="weapons.bombs.BR_500" ENUMS.Storage.weapons.bombs.British_GP_500LB_Bomb_Mk1="weapons.bombs.British_GP_500LB_Bomb_Mk1" ENUMS.Storage.weapons.bombs.BDU_50HD="weapons.bombs.BDU_50HD" ENUMS.Storage.weapons.missiles.RS2US="weapons.missiles.RS2US" ENUMS.Storage.weapons.bombs.IFV_BMP_2_25168lb="weapons.bombs.IFV BMP-2 [25168lb]" ENUMS.Storage.weapons.bombs.SAMP400HD="weapons.bombs.SAMP400HD" ENUMS.Storage.weapons.containers.Hercules_Battle_Station="weapons.containers.Hercules_Battle_Station" ENUMS.Storage.weapons.bombs.AN_M64="weapons.bombs.AN_M64" ENUMS.Storage.weapons.containers.rearCargoSeats="weapons.containers.rearCargoSeats" ENUMS.Storage.weapons.bombs.Mk_82="weapons.bombs.Mk_82" ENUMS.Storage.weapons.missiles.AKD_10="weapons.missiles.AKD-10" ENUMS.Storage.weapons.bombs.BDU_50LGB="weapons.bombs.BDU_50LGB" ENUMS.Storage.weapons.missiles.SD_10="weapons.missiles.SD-10" ENUMS.Storage.weapons.containers.IRDeflector="weapons.containers.IRDeflector" ENUMS.Storage.weapons.bombs.FAB_500="weapons.bombs.FAB_500" ENUMS.Storage.weapons.bombs.KAB_500="weapons.bombs.KAB_500" ENUMS.Storage.weapons.nurs.S_5M="weapons.nurs.S-5M" ENUMS.Storage.weapons.missiles.MICA_R="weapons.missiles.MICA_R" ENUMS.Storage.weapons.missiles.X_59M="weapons.missiles.X_59M" ENUMS.Storage.weapons.nurs.UG_90MM="weapons.nurs.UG_90MM" ENUMS.Storage.weapons.bombs.LYSBOMB="weapons.bombs.LYSBOMB" ENUMS.Storage.weapons.nurs.R4M="weapons.nurs.R4M" ENUMS.Storage.weapons.containers.dlpod_akg="weapons.containers.dlpod_akg" ENUMS.Storage.weapons.missiles.LD_10="weapons.missiles.LD-10" ENUMS.Storage.weapons.bombs.SC_50="weapons.bombs.SC_50" ENUMS.Storage.weapons.nurs.HYDRA_70_MK5="weapons.nurs.HYDRA_70_MK5" ENUMS.Storage.weapons.bombs.FAB_100M="weapons.bombs.FAB_100M" ENUMS.Storage.weapons.missiles.Rb_24="weapons.missiles.Rb 24" ENUMS.Storage.weapons.bombs.BDU_45B="weapons.bombs.BDU_45B" ENUMS.Storage.weapons.missiles.GB_6_HE="weapons.missiles.GB-6-HE" ENUMS.Storage.weapons.missiles.KD_63B="weapons.missiles.KD-63B" ENUMS.Storage.weapons.missiles.P_27PE="weapons.missiles.P_27PE" ENUMS.Storage.weapons.droptanks.PTB300_MIG15="weapons.droptanks.PTB300_MIG15" ENUMS.Storage.weapons.bombs.Two50_3="weapons.bombs.250-3" ENUMS.Storage.weapons.bombs.SC_500_L2="weapons.bombs.SC_500_L2" ENUMS.Storage.weapons.containers.HMMWV_M1045="weapons.containers.HMMWV_M1045" ENUMS.Storage.weapons.bombs.FAB_500M54TU="weapons.bombs.FAB-500M54TU" ENUMS.Storage.weapons.containers.US_M10_SMOKE_TANK_YELLOW="weapons.containers.{US_M10_SMOKE_TANK_YELLOW}" ENUMS.Storage.weapons.containers.EclairM_60="weapons.containers.{EclairM_60}" ENUMS.Storage.weapons.bombs.SAB_250_200="weapons.bombs.SAB_250_200" ENUMS.Storage.weapons.bombs.FAB_100="weapons.bombs.FAB_100" ENUMS.Storage.weapons.bombs.KAB_500S="weapons.bombs.KAB_500S" ENUMS.Storage.weapons.missiles.AGM_45A="weapons.missiles.AGM_45A" ENUMS.Storage.weapons.missiles.Kh25MP_PRGS1VP="weapons.missiles.Kh25MP_PRGS1VP" ENUMS.Storage.weapons.nurs.S5M1_HEFRAG_FFAR="weapons.nurs.S5M1_HEFRAG_FFAR" ENUMS.Storage.weapons.containers.kg600="weapons.containers.kg600" ENUMS.Storage.weapons.bombs.AN_M65="weapons.bombs.AN_M65" ENUMS.Storage.weapons.bombs.AN_M57="weapons.bombs.AN_M57" ENUMS.Storage.weapons.bombs.BLU_3B_GROUP="weapons.bombs.BLU_3B_GROUP" ENUMS.Storage.weapons.bombs.BAP_100="weapons.bombs.BAP-100" ENUMS.Storage.weapons.containers.HEMTT="weapons.containers.HEMTT" ENUMS.Storage.weapons.bombs.British_MC_500LB_Bomb_Mk1_Short="weapons.bombs.British_MC_500LB_Bomb_Mk1_Short" ENUMS.Storage.weapons.nurs.ARAKM70BAP="weapons.nurs.ARAKM70BAP" ENUMS.Storage.weapons.missiles.AGM_119="weapons.missiles.AGM_119" ENUMS.Storage.weapons.missiles.MMagicII="weapons.missiles.MMagicII" ENUMS.Storage.weapons.bombs.AB_500_1_SD_10A="weapons.bombs.AB_500_1_SD_10A" ENUMS.Storage.weapons.nurs.HYDRA_70_M282="weapons.nurs.HYDRA_70_M282" ENUMS.Storage.weapons.droptanks.DFT_400_GAL_A4E="weapons.droptanks.DFT_400_GAL_A4E" ENUMS.Storage.weapons.nurs.HYDRA_70_M257="weapons.nurs.HYDRA_70_M257" ENUMS.Storage.weapons.droptanks.AV8BNA_AERO1D="weapons.droptanks.AV8BNA_AERO1D" ENUMS.Storage.weapons.containers.US_M10_SMOKE_TANK_BLUE="weapons.containers.{US_M10_SMOKE_TANK_BLUE}" ENUMS.Storage.weapons.nurs.ARF8M3HEI="weapons.nurs.ARF8M3HEI" ENUMS.Storage.weapons.bombs.RN_28="weapons.bombs.RN-28" ENUMS.Storage.weapons.bombs.Squad_30_x_Soldier_7950lb="weapons.bombs.Squad 30 x Soldier [7950lb]" ENUMS.Storage.weapons.containers.uaz_469="weapons.containers.uaz-469" ENUMS.Storage.weapons.containers.Otokar_Cobra="weapons.containers.Otokar_Cobra" ENUMS.Storage.weapons.bombs.APC_BTR_82A_Air_24998lb="weapons.bombs.APC BTR-82A Air [24998lb]" ENUMS.Storage.weapons.nurs.HYDRA_70_M274="weapons.nurs.HYDRA_70_M274" ENUMS.Storage.weapons.missiles.P_24R="weapons.missiles.P_24R" ENUMS.Storage.weapons.nurs.HYDRA_70_MK61="weapons.nurs.HYDRA_70_MK61" ENUMS.Storage.weapons.missiles.Igla_1E="weapons.missiles.Igla_1E" ENUMS.Storage.weapons.missiles.C_802AK="weapons.missiles.C-802AK" ENUMS.Storage.weapons.nurs.C_24="weapons.nurs.C_24" ENUMS.Storage.weapons.droptanks.M2KC_08_RPL541="weapons.droptanks.M2KC_08_RPL541" ENUMS.Storage.weapons.nurs.C_13="weapons.nurs.C_13" ENUMS.Storage.weapons.droptanks.droptank_110_gal="weapons.droptanks.droptank_110_gal" ENUMS.Storage.weapons.bombs.Mk_84="weapons.bombs.Mk_84" ENUMS.Storage.weapons.missiles.Sea_Eagle="weapons.missiles.Sea_Eagle" ENUMS.Storage.weapons.droptanks.PTB_1200_F1="weapons.droptanks.PTB_1200_F1" ENUMS.Storage.weapons.nurs.SNEB_TYPE256_H1="weapons.nurs.SNEB_TYPE256_H1" ENUMS.Storage.weapons.containers.MATRA_PHIMAT="weapons.containers.MATRA-PHIMAT" ENUMS.Storage.weapons.containers.smoke_pod="weapons.containers.smoke_pod" ENUMS.Storage.weapons.containers.F_15E_AAQ_14_LANTIRN="weapons.containers.F-15E_AAQ-14_LANTIRN" ENUMS.Storage.weapons.containers.EclairM_24="weapons.containers.{EclairM_24}" ENUMS.Storage.weapons.bombs.GBU_16="weapons.bombs.GBU_16" ENUMS.Storage.weapons.nurs.HYDRA_70_M156="weapons.nurs.HYDRA_70_M156" ENUMS.Storage.weapons.missiles.R_60="weapons.missiles.R-60" ENUMS.Storage.weapons.containers.zsu_23_4="weapons.containers.zsu-23-4" ENUMS.Storage.weapons.missiles.RB75="weapons.missiles.RB75" ENUMS.Storage.weapons.missiles.Mistral="weapons.missiles.Mistral" ENUMS.Storage.weapons.droptanks.MB339_TT500_L="weapons.droptanks.MB339_TT500_L" ENUMS.Storage.weapons.bombs.SAM_SA_13_STRELA_21624lb="weapons.bombs.SAM SA-13 STRELA [21624lb]" ENUMS.Storage.weapons.bombs.SAM_Avenger_M1097_Air_7200lb="weapons.bombs.SAM Avenger M1097 Air [7200lb]" ENUMS.Storage.weapons.droptanks.Eleven00L_Tank_Empty="weapons.droptanks.1100L Tank Empty" ENUMS.Storage.weapons.bombs.AN_M88="weapons.bombs.AN-M88" ENUMS.Storage.weapons.missiles.S_25L="weapons.missiles.S_25L" ENUMS.Storage.weapons.nurs.British_AP_25LBNo1_3INCHNo1="weapons.nurs.British_AP_25LBNo1_3INCHNo1" ENUMS.Storage.weapons.bombs.BDU_50LD="weapons.bombs.BDU_50LD" ENUMS.Storage.weapons.bombs.AGM_62="weapons.bombs.AGM_62" ENUMS.Storage.weapons.containers.US_M10_SMOKE_TANK_WHITE="weapons.containers.{US_M10_SMOKE_TANK_WHITE}" ENUMS.Storage.weapons.missiles.MICA_T="weapons.missiles.MICA_T" ENUMS.Storage.weapons.containers.HVAR_rocket="weapons.containers.HVAR_rocket" ENUMS.Storage.weapons.containers.LANTIRN="weapons.containers.LANTIRN" ENUMS.Storage.weapons.missiles.AGM_78B="weapons.missiles.AGM_78B" ENUMS.Storage.weapons.containers.uh_60l_pilot="weapons.containers.uh-60l_pilot" ENUMS.Storage.weapons.missiles.AIM_92E="weapons.missiles.AIM-92E" ENUMS.Storage.weapons.missiles.KD_63B="weapons.missiles.KD_63B" ENUMS.Storage.weapons.bombs.Type_200A="weapons.bombs.Type_200A" ENUMS.Storage.weapons.missiles.HB_AIM_7E_2="weapons.missiles.HB-AIM-7E-2" ENUMS.Storage.weapons.containers.Spear="weapons.containers.Spear" ENUMS.Storage.weapons.missiles.LS_6="weapons.missiles.LS_6" ENUMS.Storage.weapons.containers.HB_ALE_40_0_120="weapons.containers.HB_ALE_40_0_120" ENUMS.Storage.weapons.containers.Fantasm="weapons.containers.Fantasm" ENUMS.Storage.weapons.nurs.FFAR_Mk61="weapons.nurs.FFAR_Mk61" ENUMS.Storage.weapons.bombs.HB_F4E_GBU15V1="weapons.bombs.HB_F4E_GBU15V1" ENUMS.Storage.weapons.containers.HB_F14_EXT_AN_APQ_167="weapons.containers.HB_F14_EXT_AN_APQ-167" ENUMS.Storage.weapons.nurs.LWL_RP="weapons.nurs.LWL_RP" ENUMS.Storage.weapons.bombs.AGM_62_I="weapons.bombs.AGM_62_I" ENUMS.Storage.weapons.containers.ETHER="weapons.containers.ETHER" ENUMS.Storage.weapons.containers.TANGAZH="weapons.containers.TANGAZH" ENUMS.Storage.weapons.bombs.LYSBOMB_11086="weapons.bombs.LYSBOMB 11086" ENUMS.Storage.weapons.containers.Stub_Wing="weapons.containers.Stub_Wing" ENUMS.Storage.weapons.missiles.AIM_9E="weapons.missiles.AIM-9E" ENUMS.Storage.weapons.missiles.C_701T="weapons.missiles.C_701T" ENUMS.Storage.weapons.bombs.BAP_100="weapons.bombs.BAP_100" ENUMS.Storage.weapons.missiles.CM_802AKG="weapons.missiles.CM-802AKG" ENUMS.Storage.weapons.missiles.CM_400AKG="weapons.missiles.CM-400AKG" ENUMS.Storage.weapons.missiles.C_802AK="weapons.missiles.C_802AK" ENUMS.Storage.weapons.missiles.KD_63="weapons.missiles.KD_63" ENUMS.Storage.weapons.containers.HB_ORD_Pave_Spike_Fast="weapons.containers.HB_ORD_Pave_Spike_Fast" ENUMS.Storage.weapons.missiles.SPIKE_ER2="weapons.missiles.SPIKE_ER2" ENUMS.Storage.weapons.containers.KINGAL="weapons.containers.KINGAL" ENUMS.Storage.weapons.containers.LANTIRN_F14_TARGET="weapons.containers.LANTIRN-F14-TARGET" ENUMS.Storage.weapons.containers.SPS_141="weapons.containers.SPS-141" ENUMS.Storage.weapons.bombs.BLU_3B_GROUP="weapons.bombs.BLU-3B_GROUP" ENUMS.Storage.weapons.containers.HB_ALE_40_30_0="weapons.containers.HB_ALE_40_30_0" ENUMS.Storage.weapons.droptanks.HB_HIGH_PERFORMANCE_CENTERLINE_600_GAL="weapons.droptanks.HB_HIGH_PERFORMANCE_CENTERLINE_600_GAL" ENUMS.Storage.weapons.containers.ALQ_184="weapons.containers.ALQ-184" ENUMS.Storage.weapons.missiles.AGM_45B="weapons.missiles.AGM_45B" ENUMS.Storage.weapons.bombs.BLU_3_GROUP="weapons.bombs.BLU-3_GROUP" ENUMS.Storage.weapons.missiles.SPIKE_ER="weapons.missiles.SPIKE_ER" ENUMS.Storage.weapons.nurs.ARAKM70BAPPX="weapons.nurs.ARAKM70BAPPX" ENUMS.Storage.weapons.bombs.LYSBOMB_11088="weapons.bombs.LYSBOMB 11088" ENUMS.Storage.weapons.bombs.LYSBOMB_11087="weapons.bombs.LYSBOMB 11087" ENUMS.Storage.weapons.missiles.KD_20="weapons.missiles.KD_20" ENUMS.Storage.weapons.droptanks.HB_F_4E_EXT_WingTank="weapons.droptanks.HB_F-4E_EXT_WingTank" ENUMS.Storage.weapons.missiles.Rb_04="weapons.missiles.Rb_04" ENUMS.Storage.weapons.containers.AAQ_33="weapons.containers.AAQ-33" ENUMS.Storage.weapons.droptanks.HB_F_4E_EXT_Center_Fuel_Tank_EMPTY="weapons.droptanks.HB_F-4E_EXT_Center_Fuel_Tank_EMPTY" ENUMS.Storage.weapons.droptanks.HB_F_4E_EXT_WingTank_R_EMPTY="weapons.droptanks.HB_F-4E_EXT_WingTank_R_EMPTY" ENUMS.Storage.weapons.droptanks.HB_F_4E_EXT_WingTank_EMPTY="weapons.droptanks.HB_F-4E_EXT_WingTank_EMPTY" ENUMS.Storage.weapons.containers.uh_60l_copilot="weapons.containers.uh-60l_copilot" ENUMS.Storage.weapons.droptanks.JAYHAWK_80gal_Fuel_Tankv2="weapons.droptanks.JAYHAWK_80gal_Fuel_Tankv2" ENUMS.Storage.weapons.containers.supply_m134="weapons.containers.supply_m134" ENUMS.Storage.weapons.containers.Seahawk_Pylon="weapons.containers.Seahawk_Pylon" ENUMS.Storage.weapons.nurs.LWL_MPP="weapons.nurs.LWL_MPP" ENUMS.Storage.weapons.nurs.S_5KP="weapons.nurs.S_5KP" ENUMS.Storage.weapons.missiles.AIM_92J="weapons.missiles.AIM-92J" ENUMS.Storage.weapons.missiles.HB_AIM_7E="weapons.missiles.HB-AIM-7E" ENUMS.Storage.weapons.containers.ALQ_131="weapons.containers.ALQ-131" ENUMS.Storage.weapons.containers.HB_F14_EXT_TARPS="weapons.containers.HB_F14_EXT_TARPS" ENUMS.Storage.weapons.containers.MH60_SOAR="weapons.containers.MH60_SOAR" ENUMS.Storage.weapons.missiles.YJ_83="weapons.missiles.YJ-83" ENUMS.Storage.weapons.bombs.GBU_8_B="weapons.bombs.GBU_8_B" ENUMS.Storage.weapons.containers.HB_F14_EXT_ECA="weapons.containers.HB_F14_EXT_ECA" ENUMS.Storage.weapons.bombs.BAP_100="weapons.bombs.BAP-100" ENUMS.Storage.weapons.nurs.M261_MPSM_Rocket="weapons.nurs.M261_MPSM_Rocket" ENUMS.Storage.weapons.droptanks.SEAHAWK_120_Fuel_Tank="weapons.droptanks.SEAHAWK_120_Fuel_Tank" ENUMS.Storage.weapons.containers.SHPIL="weapons.containers.SHPIL" ENUMS.Storage.weapons.bombs.GBU_39="weapons.bombs.GBU_39" ENUMS.Storage.weapons.nurs.S_5M="weapons.nurs.S_5M" ENUMS.Storage.weapons.containers.HB_ALE_40_15_90="weapons.containers.HB_ALE_40_15_90" ENUMS.Storage.weapons.missiles.AIM_7E="weapons.missiles.AIM-7E" ENUMS.Storage.weapons.missiles.AIM_9P3="weapons.missiles.AIM-9P3" ENUMS.Storage.weapons.missiles.AGM_12B="weapons.missiles.AGM_12B" ENUMS.Storage.weapons.missiles.CM_802AKG="weapons.missiles.CM_802AKG" ENUMS.Storage.weapons.droptanks.JAYHAWK_120_Fuel_Dual_Tank="weapons.droptanks.JAYHAWK_120_Fuel_Dual_Tank" ENUMS.Storage.weapons.droptanks.HB_F_4E_EXT_Center_Fuel_Tank="weapons.droptanks.HB_F-4E_EXT_Center_Fuel_Tank" ENUMS.Storage.weapons.containers.PAVETACK="weapons.containers.PAVETACK" ENUMS.Storage.weapons.missiles.LS_6_500="weapons.missiles.LS_6_500" ENUMS.Storage.weapons.bombs.LYSBOMB_11089="weapons.bombs.LYSBOMB 11089" ENUMS.Storage.weapons.bombs.BLU_4B_GROUP="weapons.bombs.BLU-4B_GROUP" ENUMS.Storage.weapons.containers.ah_64d_radar="weapons.containers.ah-64d_radar" ENUMS.Storage.weapons.containers.F_18_LDT_POD="weapons.containers.F-18-LDT-POD" ENUMS.Storage.weapons.containers.HB_ALE_40_30_60="weapons.containers.HB_ALE_40_30_60" ENUMS.Storage.weapons.bombs.LS_6_100="weapons.bombs.LS_6_100" ENUMS.Storage.weapons.droptanks.HB_F_4E_EXT_WingTank_R="weapons.droptanks.HB_F-4E_EXT_WingTank_R" ENUMS.Storage.weapons.containers.SORBCIJA_R="weapons.containers.SORBCIJA_R" ENUMS.Storage.weapons.missiles.CATM_65K="weapons.missiles.CATM_65K" ENUMS.Storage.weapons.containers.HB_ORD_Pave_Spike="weapons.containers.HB_ORD_Pave_Spike" ENUMS.Storage.weapons.containers.RobbieTank1="weapons.containers.RobbieTank1" ENUMS.Storage.weapons.containers.SKY_SHADOW="weapons.containers.SKY_SHADOW" ENUMS.Storage.weapons.containers.SORBCIJA_L="weapons.containers.SORBCIJA_L" ENUMS.Storage.weapons.containers.Pavehawk="weapons.containers.Pavehawk" ENUMS.Storage.weapons.bombs.BLG66_EG="weapons.bombs.BLG66_EG" ENUMS.Storage.weapons.missiles.AGM_12C_ED="weapons.missiles.AGM_12C_ED" ENUMS.Storage.weapons.missiles.AIM_92C="weapons.missiles.AIM-92C" ENUMS.Storage.weapons.containers.MPS_410="weapons.containers.MPS-410" ENUMS.Storage.weapons.missiles.HJ_12="weapons.missiles.HJ-12" ENUMS.Storage.weapons.containers.AAQ_28_LITENING="weapons.containers.AAQ-28_LITENING" ENUMS.Storage.weapons.containers.F_18_FLIR_POD="weapons.containers.F-18-FLIR-POD" ENUMS.Storage.weapons.bombs.BLU_3B_GROUP="weapons.bombs.BLU_3B_GROUP" ENUMS.Storage.weapons.containers.UH60L_Jayhawk="weapons.containers.UH60L_Jayhawk" ENUMS.Storage.weapons.containers.BOZ_100="weapons.containers.BOZ-100" ENUMS.Storage.weapons.missiles.AGM_78A="weapons.missiles.AGM_78A" ENUMS.Storage.weapons.missiles.LAU_61_APKWS_M282="weapons.missiles.LAU_61_APKWS_M282" ENUMS.Storage.weapons.bombs.BAP_100="weapons.bombs.BAP-100" ENUMS.Storage.weapons.missiles.CM_802AKG="weapons.missiles.CM-802AKG" ENUMS.Storage.weapons.bombs.BLU_3B_GROUP="weapons.bombs.BLU_3B_GROUP" ENUMS.Storage.weapons.bombs.BLU_4B_GROUP="weapons.bombs.BLU-4B_GROUP" ENUMS.Storage.weapons.nurs.S_5M="weapons.nurs.S_5M" ENUMS.Storage.weapons.missiles.AGM_12A="weapons.missiles.AGM_12A" ENUMS.Storage.weapons.droptanks.JAYHAWK_120_Fuel_Tank="weapons.droptanks.JAYHAWK_120_Fuel_Tank" ENUMS.Storage.weapons.bombs.GBU_15_V_1_B="weapons.bombs.GBU_15_V_1_B" ENUMS.Storage.weapons.missiles.HYDRA_70_M151_APKWS={4,4,8,292} ENUMS.Storage.weapons.missiles.HYDRA_70_M282_APKWS={4,4,8,293} ENUMS.Storage.weapons.bombs.BAP100="weapons.bombs.BAP_100" ENUMS.Storage.weapons.bombs.BLU3B_GROUP="weapons.bombs.BLU-3B_GROUP" ENUMS.Storage.weapons.missiles.CM_802AKG="weapons.missiles.CM_802AKG" ENUMS.Storage.weapons.bombs.BLU_4B_GROUP="weapons.bombs.BLU_4B_GROUP" ENUMS.Storage.weapons.nurs.S5M="weapons.nurs.S-5M" ENUMS.Storage.weapons.Gazelle.HMP400_100RDS={4,15,46,1771} ENUMS.Storage.weapons.Gazelle.HMP400_200RDS={4,15,46,1770} ENUMS.Storage.weapons.Gazelle.HMP400_400RDS={4,15,46,1769} ENUMS.Storage.weapons.Gazelle.GIAT_M261_AP={4,15,46,1768} ENUMS.Storage.weapons.Gazelle.GIAT_M261_SAPHEI={4,15,46,1767} ENUMS.Storage.weapons.Gazelle.GIAT_M261_HE={4,15,46,1766} ENUMS.Storage.weapons.Gazelle.GIAT_M261_HEAP={4,15,46,1765} ENUMS.Storage.weapons.Gazelle.GIAT_M261_APHE={4,15,46,1764} ENUMS.Storage.weapons.Gazelle.GAZELLE_IR_DEFLECTOR={4,15,47,680} ENUMS.Storage.weapons.Gazelle.GAZELLE_FAS_SANDFILTER={4,15,47,679} ENUMS.Storage.weapons.CH47.CH47_PORT_M60D={4,15,46,2489} ENUMS.Storage.weapons.CH47.CH47_STBD_M60D={4,15,46,2488} ENUMS.Storage.weapons.CH47.CH47_AFT_M60D={4,15,46,2490} ENUMS.Storage.weapons.CH47.CH47_PORT_M134D={4,15,46,2494} ENUMS.Storage.weapons.CH47.CH47_STBD_M134D={4,15,46,2495} ENUMS.Storage.weapons.CH47.CH47_AFT_M3M={4,15,46,2496} ENUMS.Storage.weapons.CH47.CH47_PORT_M240H={4,15,46,2492} ENUMS.Storage.weapons.CH47.CH47_STBD_M240H={4,15,46,2491} ENUMS.Storage.weapons.CH47.CH47_AFT_M240H={4,15,46,2493} ENUMS.Storage.weapons.UH1H.M134_MiniGun_Right={4,15,46,161} ENUMS.Storage.weapons.UH1H.M134_MiniGun_Left={4,15,46,160} ENUMS.Storage.weapons.UH1H.M134_MiniGun_Right_Door={4,15,46,175} ENUMS.Storage.weapons.UH1H.M60_MG_Right_Door={4,15,46,177} ENUMS.Storage.weapons.UH1H.M134_MiniGun_Left_Door={4,15,46,174} ENUMS.Storage.weapons.UH1H.M60_MG_Left_Door={4,15,46,176} ENUMS.Storage.weapons.OH58.FIM92={4,4,7,449} ENUMS.Storage.weapons.OH58.MG_M3P100={4,15,46,2611} ENUMS.Storage.weapons.OH58.MG_M3P200={4,15,46,2610} ENUMS.Storage.weapons.OH58.MG_M3P300={4,15,46,2609} ENUMS.Storage.weapons.OH58.MG_M3P400={4,15,46,2608} ENUMS.Storage.weapons.OH58.MG_M3P500={4,15,46,2607} ENUMS.Storage.weapons.OH58.Smk_Grenade_Blue={4,5,9,488} ENUMS.Storage.weapons.OH58.Smk_Grenade_Green={4,5,9,489} ENUMS.Storage.weapons.OH58.Smk_Grenade_Red={4,5,9,487} ENUMS.Storage.weapons.OH58.Smk_Grenade_Violet={4,5,9,490} ENUMS.Storage.weapons.OH58.Smk_Grenade_White={4,5,9,492} ENUMS.Storage.weapons.OH58.Smk_Grenade_Yellow={4,5,9,491} ENUMS.Storage.weapons.AH64D.AN_APG78={4,15,44,2114} ENUMS.Storage.weapons.AH64D.Internal_Aux_FuelTank={1,3,43,1700} ENUMS.FARPType={ FARP="FARP", INVISIBLE="INVISIBLE", HELIPADSINGLE="HELIPADSINGLE", PADSINGLE="PADSINGLE", } ENUMS.FARPObjectTypeNamesAndShape={ [ENUMS.FARPType.FARP]={TypeName="FARP",ShapeName="FARPS"}, [ENUMS.FARPType.INVISIBLE]={TypeName="Invisible FARP",ShapeName="invisiblefarp"}, [ENUMS.FARPType.HELIPADSINGLE]={TypeName="SINGLE_HELIPAD",ShapeName="FARP"}, [ENUMS.FARPType.PADSINGLE]={TypeName="FARP_SINGLE_01",ShapeName="FARP_SINGLE_01"}, } 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", MarianaIslands="MarianaIslands", Falklands="Falklands", Sinai="SinaiMap", Kola="Kola", Afghanistan="Afghanistan", Iraq="Iraq" } 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, Navy_One=4, Mauler=5, Bloodhound=6, }, 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, }, F16={ Viper=9, Venom=10, Lobo=11, Cowboy=12, Python=13, Rattler=14, Panther=15, Wolf=16, Weasel=17, Wild=18, Ninja=19, Jedi=20, }, F18={ Hornet=9, Squid=10, Ragin=11, Roman=12, Sting=13, Jury=14, Jokey=15, Ram=16, Hawk=17, Devil=18, Check=19, Snake=20, }, F15E={ Dude=9, Thud=10, Gunny=11, Trek=12, Sniper=13, Sled=14, Best=15, Jazz=16, Rage=17, Tahoe=18, }, B1B={ Bone=9, Dark=10, Vader=11 }, B52={ Buff=9, Dump=10, Kenworth=11, }, TransportAircraft={ Heavy=9, Trash=10, Cargo=11, Ascot=12, }, AH64={ Army_Air=9, Apache=10, Crow=11, Sioux=12, Gatling=13, Gunslinger=14, Hammerhead=15, Bootleg=16, Palehorse=17, Carnivor=18, Saber=19, }, Kiowa={ Anvil=1, Azrael=2, BamBam=3, Blackjack=4, Bootleg=5, BurninStogie=6, Chaos=7, CrazyHorse=8, Crusader=9, Darkhorse=10, Eagle=11, Lighthorse=12, Mustang=13, Outcast=14, Palehorse=15, Pegasus=16, Pistol=17, Roughneck=18, Saber=19, Shamus=20, Spur=21, Stetson=22, Wrath=23, }, } UTILS={ _MarkID=1 } UTILS.IsInstanceOf=function(object,className) if 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]=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]=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 '..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 function UTILS._OneLineSerialize(tbl) if type(tbl)=='table'then local tbl_str={} tbl_str[#tbl_str+1]='{ ' for ind,val in pairs(tbl)do if type(ind)=="number"then tbl_str[#tbl_str+1]='[' tbl_str[#tbl_str+1]=tostring(ind) tbl_str[#tbl_str+1]='] = ' else tbl_str[#tbl_str+1]='[' tbl_str[#tbl_str+1]=UTILS.BasicSerialize(ind) tbl_str[#tbl_str+1]='] = ' end if((type(val)=='number')or(type(val)=='boolean'))then tbl_str[#tbl_str+1]=tostring(val) tbl_str[#tbl_str+1]=', ' elseif type(val)=='string'then tbl_str[#tbl_str+1]=UTILS.BasicSerialize(val) tbl_str[#tbl_str+1]=', ' elseif type(val)=='nil'then tbl_str[#tbl_str+1]='nil, ' elseif type(val)=='table'then else end end tbl_str[#tbl_str+1]='}' return table.concat(tbl_str) else return UTILS.BasicSerialize(tbl) end 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)=='userdata'))then return tostring(s) elseif type(s)=="table"then return UTILS._OneLineSerialize(s) elseif type(s)=='string'then s=string.format('(%s)',s) return s end end end function UTILS.TableLength(T) local count=0 for _ in pairs(T or{})do count=count+1 end return count end function UTILS.PrintTableToLog(table,indent,noprint) local text="\n" if not table or type(table)~="table"then env.warning("No table passed!") return nil end if not indent then indent=0 end for k,v in pairs(table)do if string.find(k," ")then k='"'..k..'"'end if type(v)=="table"and UTILS.TableLength(v)>0 then if not noprint then env.info(string.rep(" ",indent)..tostring(k).." = {") end text=text..string.rep(" ",indent)..tostring(k).." = {\n" text=text..tostring(UTILS.PrintTableToLog(v,indent+1),noprint).."\n" if not noprint then env.info(string.rep(" ",indent).."},") end text=text..string.rep(" ",indent).."},\n" elseif type(v)=="function"then else local value if tostring(v)=="true"or tostring(v)=="false"or tonumber(v)~=nil then value=v else value='"'..tostring(v)..'"' end if not noprint then env.info(string.rep(" ",indent)..tostring(k).." = "..tostring(value)..",\n") end text=text..string.rep(" ",indent)..tostring(k).." = "..tostring(value)..",\n" end end return text end function UTILS.TableShow(tbl,loc,indent,tableshow_tbls) tableshow_tbls=tableshow_tbls or{} loc=loc or"" indent=indent or"" if type(tbl)=='table'then tableshow_tbls[tbl]=loc local tbl_str={} tbl_str[#tbl_str+1]=indent..'{\n' for ind,val in pairs(tbl)do if type(ind)=="number"then tbl_str[#tbl_str+1]=indent tbl_str[#tbl_str+1]=loc..'[' tbl_str[#tbl_str+1]=tostring(ind) tbl_str[#tbl_str+1]='] = ' else tbl_str[#tbl_str+1]=indent tbl_str[#tbl_str+1]=loc..'[' tbl_str[#tbl_str+1]=UTILS.BasicSerialize(ind) tbl_str[#tbl_str+1]='] = ' end if((type(val)=='number')or(type(val)=='boolean'))then tbl_str[#tbl_str+1]=tostring(val) tbl_str[#tbl_str+1]=',\n' elseif type(val)=='string'then tbl_str[#tbl_str+1]=UTILS.BasicSerialize(val) tbl_str[#tbl_str+1]=',\n' elseif type(val)=='nil'then tbl_str[#tbl_str+1]='nil,\n' elseif type(val)=='table'then if tableshow_tbls[val]then tbl_str[#tbl_str+1]=tostring(val)..' already defined: '..tableshow_tbls[val]..',\n' else tableshow_tbls[val]=loc..'['..UTILS.BasicSerialize(ind)..']' tbl_str[#tbl_str+1]=tostring(val)..' ' tbl_str[#tbl_str+1]=UTILS.TableShow(val,loc..'['..UTILS.BasicSerialize(ind)..']',indent..' ',tableshow_tbls) tbl_str[#tbl_str+1]=',\n' end elseif type(val)=='function'then if debug and debug.getinfo then local fcnname=tostring(val) local info=debug.getinfo(val,"S") if info.what=="C"then tbl_str[#tbl_str+1]=string.format('%q',fcnname..', C function')..',\n' else if(string.sub(info.source,1,2)==[[./]])then tbl_str[#tbl_str+1]=string.format('%q',fcnname..', defined in ('..info.linedefined..'-'..info.lastlinedefined..')'..info.source)..',\n' else tbl_str[#tbl_str+1]=string.format('%q',fcnname..', defined in ('..info.linedefined..'-'..info.lastlinedefined..')')..',\n' end end else tbl_str[#tbl_str+1]='a function,\n' end else tbl_str[#tbl_str+1]='unable to serialize value type '..UTILS.BasicSerialize(type(val))..' at index '..tostring(ind) end end tbl_str[#tbl_str+1]=indent..'}' return table.concat(tbl_str) end end function UTILS.Gdump(fname) if lfs and io then local fdir=lfs.writedir()..[[Logs\]]..fname local f=io.open(fdir,'w') f:write(UTILS.TableShow(_G)) f:close() env.info(string.format('Wrote debug data to $1',fdir)) else env.error("WARNING: lfs and/or io not de-sanitized - cannot dump _G!") end end function UTILS.DoString(s) local f,err=loadstring(s) if f then return true,f() else return false,err 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) if type(knots)=="number"then return knots/1.94384 else return 0 end end UTILS.CelsiusToFahrenheit=function(Celcius) return Celcius*9/5+32 end UTILS.hPa2inHg=function(hPa) return hPa*0.0295299830714 end UTILS.IasToTas=function(ias,altitude,oatcorr) oatcorr=oatcorr or 0.017 local tas=ias+(ias*oatcorr*UTILS.MetersToFeet(altitude)/1000) return tas end UTILS.TasToIas=function(tas,altitude,oatcorr) oatcorr=oatcorr or 0.017 local ias=tas/(1+oatcorr*UTILS.MetersToFeet(altitude)/1000) return ias 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.tostringLLM2KData=function(lat,lon,acc) 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 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 latHemi..string.format('%02d:',latDeg)..string.format(minFrmtStr,latMin),lonHemi..string.format('%02d:',lonDeg)..string.format(minFrmtStr,lonMin) end UTILS.tostringMGRS=function(MGRS,acc) if acc<=0 then return MGRS.UTMZone..' '..MGRS.MGRSDigraph else if acc>5 then acc=5 end 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.RemoveMark(MarkID,Delay) if Delay and Delay>0 then TIMER:New(UTILS.RemoveMark,MarkID):Start(Delay) else if MarkID then trigger.action.removeMark(MarkID) end end 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.GetCharacters(str) local chars={} for i=1,#str do local c=str:sub(i,i) table.insert(chars,c) end return chars end function UTILS.SecondsToClock(seconds,short) if seconds==nil then return nil end local seconds=tonumber(seconds)or 0 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.Vec2Dot(a,b) return a.x*b.x+a.y*b.y end function UTILS.VecNorm(a) return math.sqrt(UTILS.VecDot(a,a)) end function UTILS.Vec2Norm(a) return math.sqrt(UTILS.Vec2Dot(a,a)) end function UTILS.VecDist2D(a,b) local d=math.huge if(not a)or(not b)then return d end local c={x=b.x-a.x,y=b.y-a.y} d=math.sqrt(c.x*c.x+c.y*c.y) return d end function UTILS.VecDist3D(a,b) local d=math.huge if(not a)or(not b)then return d end local c={x=b.x-a.x,y=b.y-a.y,z=b.z-a.z} 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.VecSubtract(a,b) return UTILS.VecSubstract(a,b) end function UTILS.Vec2Substract(a,b) return{x=a.x-b.x,y=a.y-b.y} end function UTILS.Vec2Subtract(a,b) return UTILS.Vec2Substract(a,b) 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.Vec2Add(a,b) return{x=a.x+b.x,y=a.y+b.y} 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.Vec2Hdg(a) local h=math.deg(math.atan2(a.y,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.HdgTo(a,b) local dz=b.z-a.z local dx=b.x-a.x local heading=math.deg(math.atan2(dz,dx)) if heading<0 then heading=360+heading end return heading 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.Vec2Translate(a,distance,angle) local SX=a.x local SY=a.y 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=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.Vec2Rotate2D(a,angle) local phi=math.rad(angle) local x=a.x local y=a.y local X=x*math.cos(phi)-y*math.sin(phi) local Y=x*math.sin(phi)+y*math.cos(phi) local A={x=X,y=Y} 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.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 elseif map==DCSMAP.MarianaIslands then declination=2 elseif map==DCSMAP.Falklands then declination=12 elseif map==DCSMAP.Sinai then declination=4.8 elseif map==DCSMAP.Kola then declination=15 elseif map==DCSMAP.Afghanistan then declination=3 elseif map==DCSMAP.Iraq then declination=4.4 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.GetCoalitionEnemy(Coalition,Neutral) local Coalitions={} if Coalition then if Coalition==coalition.side.RED then Coalitions={coalition.side.BLUE} elseif Coalition==coalition.side.BLUE then Coalitions={coalition.side.RED} elseif Coalition==coalition.side.NEUTRAL then Coalitions={coalition.side.RED,coalition.side.BLUE} end end if Neutral then table.insert(Coalitions,coalition.side.NEUTRAL) end return Coalitions end 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.GetReportingName(Typename) local typename=string.lower(Typename) for name,value in pairs(ENUMS.ReportingName.NATO)do local svalue=string.lower(value) if string.find(typename,svalue,1,true)then return name end end return"Bogey" 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 for name,value in pairs(CALLSIGN.B1B)do if value==Callsign then return name end end for name,value in pairs(CALLSIGN.B52)do if value==Callsign then return name end end for name,value in pairs(CALLSIGN.F15E)do if value==Callsign then return name end end for name,value in pairs(CALLSIGN.F16)do if value==Callsign then return name end end for name,value in pairs(CALLSIGN.F18)do if value==Callsign then return name end end for name,value in pairs(CALLSIGN.FARP)do if value==Callsign then return name end end for name,value in pairs(CALLSIGN.TransportAircraft)do if value==Callsign then return name end end for name,value in pairs(CALLSIGN.AH64)do if value==Callsign then return name end end for name,value in pairs(CALLSIGN.Kiowa)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 elseif theatre==DCSMAP.MarianaIslands then return 10 elseif theatre==DCSMAP.Falklands then return-3 elseif theatre==DCSMAP.Sinai then return 2 elseif theatre==DCSMAP.Kola then return 3 elseif theatre==DCSMAP.Afghanistan then return 4.5 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/S" elseif cosH<-1 then return"N/R" 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 local ts=0 local t=os.date("*t") local s=t.sec local m=t.min*60 local h=t.hour*3600 ts=s+m+h return ts else return nil end end function UTILS.ShuffleTable(t) if t==nil or type(t)~="table"then BASE:I("Error in ShuffleTable: Missing or wrong type 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 function UTILS.GetRandomTableElement(t,replace) if t==nil or type(t)~="table"then BASE:I("Error in ShuffleTable: Missing or wrong type of Argument") return end math.random() math.random() math.random() local r=math.random(#t) local element=t[r] if not replace then table.remove(t,r) end return element end function UTILS.IsLoadingDoorOpen(unit_name) local unit=Unit.getByName(unit_name) if unit~=nil then local type_name=unit:getTypeName() BASE:T("TypeName = "..type_name) if type_name=="Mi-8MT"and(unit:getDrawArgumentValue(38)==1 or unit:getDrawArgumentValue(86)==1 or unit:getDrawArgumentValue(250)<0)then BASE:T(unit_name.." Cargo doors are open or cargo door not present") return true end if type_name=="Mi-24P"and(unit:getDrawArgumentValue(38)==1 or unit:getDrawArgumentValue(86)==1)then BASE:T(unit_name.." a side door is open") return true end if type_name=="UH-1H"and(unit:getDrawArgumentValue(43)==1 or unit:getDrawArgumentValue(44)==1)then BASE:T(unit_name.." a side door is open ") return true end if string.find(type_name,"SA342")and(unit:getDrawArgumentValue(34)==1)then BASE:T(unit_name.." front door(s) are open or doors removed") return true end if string.find(type_name,"Hercules")and(unit:getDrawArgumentValue(1215)==1 and unit:getDrawArgumentValue(1216)==1)then BASE:T(unit_name.." rear doors are open") return true end if string.find(type_name,"Hercules")and(unit:getDrawArgumentValue(1220)==1 or unit:getDrawArgumentValue(1221)==1)then BASE:T(unit_name.." para doors are open") return true end if string.find(type_name,"Hercules")and(unit:getDrawArgumentValue(1217)==1)then BASE:T(unit_name.." side door is open") return true end if type_name=="Bell-47"then BASE:T(unit_name.." door is open") return true end if type_name=="UH-60L"and(unit:getDrawArgumentValue(401)==1 or unit:getDrawArgumentValue(402)==1)then BASE:T(unit_name.." cargo door is open") return true end if type_name=="UH-60L"and(unit:getDrawArgumentValue(38)>0 or unit:getDrawArgumentValue(400)==1)then BASE:T(unit_name.." front door(s) are open") return true end if type_name=="AH-64D_BLK_II"then BASE:T(unit_name.." front door(s) are open") return true end if type_name=="Bronco-OV-10A"then BASE:T(unit_name.." front door(s) are open") return true end if type_name=="MH-60R"and(unit:getDrawArgumentValue(403)>0 or unit:getDrawArgumentValue(403)==-1)then BASE:T(unit_name.." cargo door is open") return true end if type_name=="OH58D"then BASE:T(unit_name.." front door(s) are open") return true end if type_name=="CH-47Fbl1"and(unit:getDrawArgumentValue(86)>0.5)then BASE:T(unit_name.." rear cargo door is open") return true end return false end return nil end function UTILS.GenerateFMFrequencies() local FreeFMFrequencies={} for _first=3,7 do for _second=0,5 do for _third=0,9 do local _frequency=((100*_first)+(10*_second)+_third)*100000 table.insert(FreeFMFrequencies,_frequency) end end end return FreeFMFrequencies end function UTILS.GenerateVHFrequencies() local _skipFrequencies={ 214,243,264,273,274,288,291.5,295,297.5, 300.5,304,305,307,309.5,310,311,312,312.5,316,317, 320,323,324,325,326,328,329,330,332,335,336,337, 340,342,343,346,348,351,352,353,358, 360,363,364,365,368,372.5,373,374, 380,381,384,385,387,389,391,395,396,399, 403,404,410,412,414,418,420,423, 430,432,435,440,445, 450,455,462,470,485,490, 507,515,520,525,528,540,550,560,563,570,577,580,595, 602,625,641,662,670,680,682,690, 705,720,722,730,735,740,745,750,770,795, 822,830,862,866, 905,907,920,935,942,950,995, 1000,1025,1030,1050,1065,1116,1175,1182,1210,1215 } local FreeVHFFrequencies={} local _start=200000 while _start<400000 do local _found=false for _,value in pairs(_skipFrequencies)do if value*1000==_start then _found=true break end end if _found==false then table.insert(FreeVHFFrequencies,_start) end _start=_start+10000 end _start=400000 while _start<850000 do local _found=false for _,value in pairs(_skipFrequencies)do if value*1000==_start then _found=true break end end if _found==false then table.insert(FreeVHFFrequencies,_start) end _start=_start+10000 end _start=850000 while _start<=999000 do local _found=false for _,value in pairs(_skipFrequencies)do if value*1000==_start then _found=true break end end if _found==false then table.insert(FreeVHFFrequencies,_start) end _start=_start+50000 end return FreeVHFFrequencies end function UTILS.GenerateUHFrequencies(Start,End) local FreeUHFFrequencies={} local _start=220000000 if not Start then while _start<399000000 do if _start~=243000000 then table.insert(FreeUHFFrequencies,_start) end _start=_start+500000 end else local myend=End*1000000 or 399000000 local mystart=Start*1000000 or 220000000 while _start<399000000 do if _start~=243000000 and(_startmyend)then print(_start) table.insert(FreeUHFFrequencies,_start) end _start=_start+500000 end end return FreeUHFFrequencies end function UTILS.GenerateLaserCodes() local jtacGeneratedLaserCodes={} 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 table.insert(jtacGeneratedLaserCodes,_code) break end end _count=_count+1 end return jtacGeneratedLaserCodes end function UTILS.EnsureTable(Object,ReturnNil) if Object then if type(Object)~="table"then Object={Object} end else if ReturnNil then return nil else Object={} end end return Object end function UTILS.SaveToFile(Path,Filename,Data) if not io then BASE:E("ERROR: io not desanitized. Can't save current file.") return false end if Path==nil and not lfs then BASE:E("WARNING: lfs not desanitized. File will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.") end local path=nil if lfs then path=Path or lfs.writedir() end local filename=Filename if path~=nil then filename=path.."\\"..filename end local f=assert(io.open(filename,"wb")) f:write(Data) f:close() return true end function UTILS.LoadFromFile(Path,Filename) if not io then BASE:E("ERROR: io not desanitized. Can't save current state.") return false end if Path==nil and not lfs then BASE:E("WARNING: lfs not desanitized. Loading will look into your DCS installation root directory rather than your \"Saved Games\\DCS\" folder.") end local path=nil if lfs then path=Path or lfs.writedir() end local filename=Filename if path~=nil then filename=path.."\\"..filename end local exists=UTILS.CheckFileExists(Path,Filename) if not exists then BASE:I(string.format("ERROR: File %s does not exist!",filename)) return false end local file=assert(io.open(filename,"rb")) local loadeddata={} for line in file:lines()do loadeddata[#loadeddata+1]=line end file:close() return true,loadeddata end function UTILS.CheckFileExists(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 BASE:E("ERROR: io not desanitized.") return false end if Path==nil and not lfs then BASE:E("WARNING: lfs not desanitized. Loading will look into your DCS installation root directory rather than your \"Saved Games\\DCS\" folder.") end local path=nil if lfs then path=Path or lfs.writedir() end local filename=Filename if path~=nil then filename=path.."\\"..filename end local exists=_fileexists(filename) if not exists then BASE:E(string.format("ERROR: File %s does not exist!",filename)) return false else return true end end function UTILS.GetCountPerTypeName(Group) local units=Group:GetUnits() local TypeNameTable={} for _,_unt in pairs(units)do local unit=_unt local typen=unit:GetTypeName() if not TypeNameTable[typen]then TypeNameTable[typen]=1 else TypeNameTable[typen]=TypeNameTable[typen]+1 end end return TypeNameTable end function UTILS.SaveStationaryListOfGroups(List,Path,Filename,Structured) local filename=Filename or"StateListofGroups" local data="--Save Stationary List of Groups: "..Filename.."\n" for _,_group in pairs(List)do local group=GROUP:FindByName(_group) if group and group:IsAlive()then local units=group:CountAliveUnits() local position=group:GetVec3() if Structured then local structure=UTILS.GetCountPerTypeName(group) local strucdata="" for typen,anzahl in pairs(structure)do strucdata=strucdata..typen.."=="..anzahl..";" end data=string.format("%s%s,%d,%d,%d,%d,%s\n",data,_group,units,position.x,position.y,position.z,strucdata) else data=string.format("%s%s,%d,%d,%d,%d\n",data,_group,units,position.x,position.y,position.z) end else data=string.format("%s%s,0,0,0,0\n",data,_group) end end local outcome=UTILS.SaveToFile(Path,Filename,data) return outcome end function UTILS.SaveSetOfGroups(Set,Path,Filename,Structured) local filename=Filename or"SetOfGroups" local data="--Save SET of groups: "..Filename.."\n" local List=Set:GetSetObjects() for _,_group in pairs(List)do local group=_group if group and group:IsAlive()then local name=group:GetName() local template=string.gsub(name,"-(.+)$","") if string.find(name,"AID")then template=string.gsub(name,"(.AID.%d+$","") end if string.find(template,"#")then template=string.gsub(name,"#(%d+)$","") end local units=group:CountAliveUnits() local position=group:GetVec3() if Structured then local structure=UTILS.GetCountPerTypeName(group) local strucdata="" for typen,anzahl in pairs(structure)do strucdata=strucdata..typen.."=="..anzahl..";" end data=string.format("%s%s,%s,%d,%d,%d,%d,%s\n",data,name,template,units,position.x,position.y,position.z,strucdata) else data=string.format("%s%s,%s,%d,%d,%d,%d\n",data,name,template,units,position.x,position.y,position.z) end end end local outcome=UTILS.SaveToFile(Path,Filename,data) return outcome end function UTILS.SaveSetOfStatics(Set,Path,Filename) local filename=Filename or"SetOfStatics" local data="--Save SET of statics: "..Filename.."\n" local List=Set:GetSetObjects() for _,_group in pairs(List)do local group=_group if group and group:IsAlive()then local name=group:GetName() local position=group:GetVec3() data=string.format("%s%s,%d,%d,%d\n",data,name,position.x,position.y,position.z) end end local outcome=UTILS.SaveToFile(Path,Filename,data) return outcome end function UTILS.SaveStationaryListOfStatics(List,Path,Filename) local filename=Filename or"StateListofStatics" local data="--Save Stationary List of Statics: "..Filename.."\n" for _,_group in pairs(List)do local group=STATIC:FindByName(_group,false) if group and group:IsAlive()then local position=group:GetVec3() data=string.format("%s%s,1,%d,%d,%d\n",data,_group,position.x,position.y,position.z) else data=string.format("%s%s,0,0,0,0\n",data,_group) end end local outcome=UTILS.SaveToFile(Path,Filename,data) return outcome end function UTILS.LoadStationaryListOfGroups(Path,Filename,Reduce,Structured,Cinematic,Effect,Density) local fires={} local function Smokers(name,coord,effect,density) local eff=math.random(8) if type(effect)=="number"then eff=effect end coord:BigSmokeAndFire(eff,density,name) table.insert(fires,name) end local function Cruncher(group,typename,anzahl) local units=group:GetUnits() local reduced=0 for _,_unit in pairs(units)do local typo=_unit:GetTypeName() if typename==typo then if Cinematic then local coordinate=_unit:GetCoordinate() local name=_unit:GetName() Smokers(name,coordinate,Effect,Density) end _unit:Destroy(false) reduced=reduced+1 if reduced==anzahl then break end end end end local reduce=true if Reduce==false then reduce=false end local filename=Filename or"StateListofGroups" local datatable={} if UTILS.CheckFileExists(Path,filename)then local outcome,loadeddata=UTILS.LoadFromFile(Path,Filename) table.remove(loadeddata,1) for _id,_entry in pairs(loadeddata)do local dataset=UTILS.Split(_entry,",") local groupname=dataset[1] local size=tonumber(dataset[2]) local posx=tonumber(dataset[3]) local posy=tonumber(dataset[4]) local posz=tonumber(dataset[5]) local structure=dataset[6] local coordinate=COORDINATE:NewFromVec3({x=posx,y=posy,z=posz}) local data={groupname=groupname,size=size,coordinate=coordinate,group=GROUP:FindByName(groupname)} if reduce then local actualgroup=GROUP:FindByName(groupname) if actualgroup and actualgroup:IsAlive()and actualgroup:CountAliveUnits()>size then if Structured and structure then local loadedstructure={} local strcset=UTILS.Split(structure,";") for _,_data in pairs(strcset)do local datasplit=UTILS.Split(_data,"==") loadedstructure[datasplit[1]]=tonumber(datasplit[2]) end local originalstructure=UTILS.GetCountPerTypeName(actualgroup) for _name,_number in pairs(originalstructure)do local loadednumber=0 if loadedstructure[_name]then loadednumber=loadedstructure[_name] end local reduce=false if loadednumber<_number then reduce=true end if reduce then Cruncher(actualgroup,_name,_number-loadednumber) end end else local reduction=actualgroup:CountAliveUnits()-size local units=actualgroup:GetUnits() local units2=UTILS.ShuffleTable(units) for i=1,reduction do units2[i]:Destroy(false) end end end end table.insert(datatable,data) end else return nil end return datatable,fires end function UTILS.LoadSetOfGroups(Path,Filename,Spawn,Structured,Cinematic,Effect,Density) local fires={} local usedtemplates={} local spawn=true if Spawn==false then spawn=false end local filename=Filename or"SetOfGroups" local setdata=SET_GROUP:New() local datatable={} local function Smokers(name,coord,effect,density) local eff=math.random(8) if type(effect)=="number"then eff=effect end coord:BigSmokeAndFire(eff,density,name) table.insert(fires,name) end local function Cruncher(group,typename,anzahl) local units=group:GetUnits() local reduced=0 for _,_unit in pairs(units)do local typo=_unit:GetTypeName() if typename==typo then if Cinematic then local coordinate=_unit:GetCoordinate() local name=_unit:GetName() Smokers(name,coordinate,Effect,Density) end _unit:Destroy(false) reduced=reduced+1 if reduced==anzahl then break end end end end local function PostSpawn(args) local spwndgrp=args[1] local size=args[2] local structure=args[3] setdata:AddObject(spwndgrp) local actualsize=spwndgrp:CountAliveUnits() if actualsize>size then if Structured and structure then local loadedstructure={} local strcset=UTILS.Split(structure,";") for _,_data in pairs(strcset)do local datasplit=UTILS.Split(_data,"==") loadedstructure[datasplit[1]]=tonumber(datasplit[2]) end local originalstructure=UTILS.GetCountPerTypeName(spwndgrp) for _name,_number in pairs(originalstructure)do local loadednumber=0 if loadedstructure[_name]then loadednumber=loadedstructure[_name] end local reduce=false if loadednumber<_number then reduce=true end if reduce then Cruncher(spwndgrp,_name,_number-loadednumber) end end else local reduction=actualsize-size local units=spwndgrp:GetUnits() local units2=UTILS.ShuffleTable(units) for i=1,reduction do units2[i]:Destroy(false) end end end end local function MultiUse(Data) local template=Data.template if template and usedtemplates[template]and usedtemplates[template].used and usedtemplates[template].used>1 then if not usedtemplates[template].done then local spwnd=0 local spawngrp=SPAWN:New(template) spawngrp:InitLimit(0,usedtemplates[template].used) for _,_entry in pairs(usedtemplates[template].data)do spwnd=spwnd+1 local sgrp=spawngrp:SpawnFromCoordinate(_entry.coordinate,spwnd) BASE:ScheduleOnce(0.5,PostSpawn,{sgrp,_entry.size,_entry.structure}) end usedtemplates[template].done=true end return true else return false end end if UTILS.CheckFileExists(Path,filename)then local outcome,loadeddata=UTILS.LoadFromFile(Path,Filename) table.remove(loadeddata,1) for _id,_entry in pairs(loadeddata)do local dataset=UTILS.Split(_entry,",") local groupname=dataset[1] local template=dataset[2] local size=tonumber(dataset[3]) local posx=tonumber(dataset[4]) local posy=tonumber(dataset[5]) local posz=tonumber(dataset[6]) local structure=dataset[7] local coordinate=COORDINATE:NewFromVec3({x=posx,y=posy,z=posz}) local group=nil if size>0 then local data={groupname=groupname,size=size,coordinate=coordinate,template=template,structure=structure} table.insert(datatable,data) if usedtemplates[template]then usedtemplates[template].used=usedtemplates[template].used+1 table.insert(usedtemplates[template].data,data) else usedtemplates[template]={ data={}, used=1, done=false, } table.insert(usedtemplates[template].data,data) end end end for _id,_entry in pairs(datatable)do if spawn and not MultiUse(_entry)and _entry.size>0 then local group=SPAWN:New(_entry.template) local sgrp=group:SpawnFromCoordinate(_entry.coordinate) BASE:ScheduleOnce(0.5,PostSpawn,{sgrp,_entry.size,_entry.structure}) end end else return nil end if spawn then return setdata,fires else return datatable end end function UTILS.LoadSetOfStatics(Path,Filename) local filename=Filename or"SetOfStatics" local datatable=SET_STATIC:New() if UTILS.CheckFileExists(Path,filename)then local outcome,loadeddata=UTILS.LoadFromFile(Path,Filename) table.remove(loadeddata,1) for _id,_entry in pairs(loadeddata)do local dataset=UTILS.Split(_entry,",") local staticname=dataset[1] local StaticObject=STATIC:FindByName(staticname,false) if StaticObject then datatable:AddObject(StaticObject) end end else return nil end return datatable end function UTILS.LoadStationaryListOfStatics(Path,Filename,Reduce,Dead,Cinematic,Effect,Density) local fires={} local reduce=true if Reduce==false then reduce=false end local filename=Filename or"StateListofStatics" local datatable={} if UTILS.CheckFileExists(Path,filename)then local outcome,loadeddata=UTILS.LoadFromFile(Path,Filename) table.remove(loadeddata,1) for _id,_entry in pairs(loadeddata)do local dataset=UTILS.Split(_entry,",") local staticname=dataset[1] local size=tonumber(dataset[2]) local posx=tonumber(dataset[3]) local posy=tonumber(dataset[4]) local posz=tonumber(dataset[5]) local coordinate=COORDINATE:NewFromVec3({x=posx,y=posy,z=posz}) local data={staticname=staticname,size=size,coordinate=coordinate,static=STATIC:FindByName(staticname,false)} table.insert(datatable,data) if size==0 and reduce then local static=STATIC:FindByName(staticname,false) if static then if Dead then local deadobject=SPAWNSTATIC:NewFromStatic(staticname,static:GetCountry()) deadobject:InitDead(true) local heading=static:GetHeading() local coord=static:GetCoordinate() static:Destroy(false) deadobject:SpawnFromCoordinate(coord,heading,staticname) if Cinematic then local effect=math.random(8) if type(Effect)=="number"then effect=Effect end coord:BigSmokeAndFire(effect,Density,staticname) table.insert(fires,staticname) end else static:Destroy(false) end end end end else return nil end return datatable,fires end function UTILS.BearingToCardinal(Heading) if Heading>=0 and Heading<=22 then return"North" elseif Heading>=23 and Heading<=66 then return"North-East" elseif Heading>=67 and Heading<=101 then return"East" elseif Heading>=102 and Heading<=146 then return"South-East" elseif Heading>=147 and Heading<=201 then return"South" elseif Heading>=202 and Heading<=246 then return"South-West" elseif Heading>=247 and Heading<=291 then return"West" elseif Heading>=292 and Heading<=338 then return"North-West" elseif Heading>=339 then return"North" end end function UTILS.ToStringBRAANATO(FromGrp,ToGrp) local BRAANATO="Merged." local GroupNumber=ToGrp:GetSize() local GroupWords="Singleton" if GroupNumber==2 then GroupWords="Two-Ship" elseif GroupNumber>=3 then GroupWords="Heavy" end local grpLeadUnit=ToGrp:GetUnit(1) local tgtCoord=grpLeadUnit:GetCoordinate() local currentCoord=FromGrp:GetCoordinate() local hdg=UTILS.Round(ToGrp:GetHeading()/100,1)*100 local bearing=UTILS.Round(currentCoord:HeadingTo(tgtCoord),0) local rangeMetres=tgtCoord:Get2DDistance(currentCoord) local rangeNM=UTILS.Round(UTILS.MetersToNM(rangeMetres),0) local aspect=tgtCoord:ToStringAspect(currentCoord) local alt=UTILS.Round(UTILS.MetersToFeet(grpLeadUnit:GetAltitude())/1000,0) local track=UTILS.BearingToCardinal(hdg) if rangeNM>3 then if aspect==""then BRAANATO=string.format("%s, BRA, %03d, %d miles, Angels %d, Track %s",GroupWords,bearing,rangeNM,alt,track) else BRAANATO=string.format("%s, BRAA, %03d, %d miles, Angels %d, %s, Track %s",GroupWords,bearing,rangeNM,alt,aspect,track) end end return BRAANATO end function UTILS.IsInTable(Table,Object,Key) for key,object in pairs(Table)do if Key then if Object[Key]==object[Key]then return true end else if object==Object then return true end end end return false end function UTILS.IsAnyInTable(Table,Objects,Key) for _,Object in pairs(UTILS.EnsureTable(Objects))do for key,object in pairs(Table)do if Key then if Object[Key]==object[Key]then return true end else if object==Object then return true end end end end return false end function UTILS.PlotRacetrack(Coordinate,Altitude,Speed,Heading,Leg,Coalition,Color,Alpha,LineType,ReadOnly) local fix_coordinate=Coordinate local altitude=Altitude local speed=Speed or 350 local heading=Heading or 270 local leg_distance=Leg or 10 local coalition=Coalition or-1 local color=Color or{1,0,0} local alpha=Alpha or 1 local lineType=LineType or 1 speed=UTILS.IasToTas(speed,UTILS.FeetToMeters(altitude),oatcorr) local turn_radius=0.0211*speed-3.01 local point_two=fix_coordinate:Translate(UTILS.NMToMeters(leg_distance),heading,true,false) local point_three=point_two:Translate(UTILS.NMToMeters(turn_radius)*2,heading-90,true,false) local point_four=fix_coordinate:Translate(UTILS.NMToMeters(turn_radius)*2,heading-90,true,false) local circle_center_fix_four=point_two:Translate(UTILS.NMToMeters(turn_radius),heading-90,true,false) local circle_center_two_three=fix_coordinate:Translate(UTILS.NMToMeters(turn_radius),heading-90,true,false) fix_coordinate:LineToAll(point_two,coalition,color,alpha,lineType) point_four:LineToAll(point_three,coalition,color,alpha,lineType) circle_center_fix_four:CircleToAll(UTILS.NMToMeters(turn_radius),coalition,color,alpha,nil,0,lineType) circle_center_two_three:CircleToAll(UTILS.NMToMeters(turn_radius),coalition,color,alpha,nil,0,lineType) end function UTILS.TimeNow() return UTILS.SecondsToClock(timer.getAbsTime(),false,false) end function UTILS.TimeDifferenceInSeconds(start_time,end_time) return UTILS.ClockToSeconds(end_time)-UTILS.ClockToSeconds(start_time) end function UTILS.TimeLaterThan(time_string) if timer.getAbsTime()>UTILS.ClockToSeconds(time_string)then return true end return false end function UTILS.TimeBefore(time_string) if timer.getAbsTime()max then value=max end return value end function UTILS.ClampAngle(value) if value>360 then return value-360 end if value<0 then return value+360 end return value end function UTILS.RemapValue(value,old_min,old_max,new_min,new_max) new_min=new_min or 0 new_max=new_max or 100 local old_range=old_max-old_min local new_range=new_max-new_min local percentage=(value-old_min)/old_range return(new_range*percentage)+new_min end function UTILS.RandomPointInTriangle(pt1,pt2,pt3) local pt={math.random(),math.random()} table.sort(pt) local s=pt[1] local t=pt[2]-pt[1] local u=1-pt[2] return{x=s*pt1.x+t*pt2.x+u*pt3.x, y=s*pt1.y+t*pt2.y+u*pt3.y} end function UTILS.AngleBetween(angle,min,max) angle=(360+(angle%360))%360 min=(360+min%360)%360 max=(360+max%360)%360 if min0 then for _,property in pairs(zone["properties"])do return_table[property["key"]]=property["value"] end return return_table else BASE:I(string.format("%s doesn't have any properties",zone_name)) return{} end end end end function UTILS.RotatePointAroundPivot(point,pivot,angle) local radians=math.rad(angle) local x=point.x-pivot.x local y=point.y-pivot.y local rotated_x=x*math.cos(radians)-y*math.sin(radians) local rotatex_y=x*math.sin(radians)+y*math.cos(radians) local original_x=rotated_x+pivot.x local original_y=rotatex_y+pivot.y return{x=original_x,y=original_y} end function UTILS.UniqueName(base) base=base or"" local ran=tostring(math.random(0,1000000)) if base==""then return ran end return base.."_"..ran end function string.startswith(str,value) return string.sub(str,1,string.len(value))==value end function string.endswith(str,value) return value==""or str:sub(-#value)==value end function string.split(input,separator) local parts={} for part in input:gmatch("[^"..separator.."]+")do table.insert(parts,part) end return parts end function string.contains(str,value) return string.match(str,value) end function table.move_object(obj,from_table,to_table) local index for i,v in pairs(from_table)do if v==obj then index=i end end if index then local moved=table.remove(from_table,index) table.insert_unique(to_table,moved) end end function table.contains(tbl,element) if element==nil or tbl==nil then return false end local index=1 while tbl[index]do if tbl[index]==element then return true end index=index+1 end return false end function table.contains_key(tbl,key) if tbl[key]~=nil then return true else return false end end function table.insert_unique(tbl,element) if element==nil or tbl==nil then return end if not table.contains(tbl,element)then table.insert(tbl,element) end end function table.remove_by_value(tbl,element) local indices_to_remove={} local index=1 for _,value in pairs(tbl)do if value==element then table.insert(indices_to_remove,index) end index=index+1 end for _,idx in pairs(indices_to_remove)do table.remove(tbl,idx) end end function table.remove_key(table,key) local element=table[key] table[key]=nil return element end function table.index_of(table,element) for i,v in ipairs(table)do if v==element then return i end end return nil end function table.length(T) local count=0 for _ in pairs(T)do count=count+1 end return count end function table.slice(tbl,first,last) local sliced={} local start=first or 1 local stop=last or table.length(tbl) local count=1 for key,value in pairs(tbl)do if count>=start and count<=stop then sliced[key]=value end count=count+1 end return sliced end function table.count_value(tbl,value) local count=0 for _,item in pairs(tbl)do if item==value then count=count+1 end end return count end function table.combine(t1,t2) if t1==nil and t2==nil then BASE:E("Both tables were empty!") end if t1==nil then return t2 end if t2==nil then return t1 end for i=1,#t2 do t1[#t1+1]=t2[i] end return t1 end function table.merge(t1,t2) for k,v in pairs(t2)do if(type(v)=="table")and(type(t1[k]or false)=="table")then table.merge(t1[k],t2[k]) else t1[k]=v end end return t1 end function table.add(tbl,item) tbl[#tbl+1]=item end function table.shuffle(tbl) local new_table={} for _,value in ipairs(tbl)do local pos=math.random(1,#new_table+1) table.insert(new_table,pos,value) end return new_table end function table.find_key_value_pair(tbl,key,value) for k,v in pairs(tbl)do if type(v)=="table"then local result=table.find_key_value_pair(v,key,value) if result~=nil then return result end elseif k==key and v==value then return tbl end end return nil end function UTILS.DecimalToOctal(Number) if Number<8 then return Number end local number=tonumber(Number) local octal="" local n=1 while number>7 do local number1=number%8 octal=string.format("%d",number1)..octal local number2=math.abs(number/8) if number2<8 then octal=string.format("%d",number2)..octal end number=number2 n=n+1 end return tonumber(octal) end function UTILS.OctalToDecimal(Number) return tonumber(Number,8) end function UTILS.HexToRGBA(hex_string) local hexNumber=tonumber(string.sub(hex_string,3),16) local alpha=hexNumber%256 hexNumber=(hexNumber-alpha)/256 local blue=hexNumber%256 hexNumber=(hexNumber-blue)/256 local green=hexNumber%256 hexNumber=(hexNumber-green)/256 local red=hexNumber%256 return{R=red,G=green,B=blue,A=alpha} end function UTILS.SaveSetOfOpsGroups(Set,Path,Filename,Structured) local filename=Filename or"SetOfGroups" local data="--Save SET of groups: (name,legion,template,alttemplate,units,position.x,position.y,position.z,strucdata) "..Filename.."\n" local List=Set:GetSetObjects() for _,_group in pairs(List)do local group=_group:GetGroup() if group and group:IsAlive()then local name=group:GetName() local template=string.gsub(name,"(.AID.%d+$","") if string.find(template,"#")then template=string.gsub(name,"#(%d+)$","") end local alttemplate=_group.templatename or"none" local legiono=_group.legion local legion="none" if legiono and type(legiono)=="table"and legiono.ClassName then legion=legiono:GetName() local asset=legiono:GetAssetByName(name) alttemplate=asset.templatename end local units=group:CountAliveUnits() local position=group:GetVec3() if Structured then local structure=UTILS.GetCountPerTypeName(group) local strucdata="" for typen,anzahl in pairs(structure)do strucdata=strucdata..typen.."=="..anzahl..";" end data=string.format("%s%s,%s,%s,%s,%d,%d,%d,%d,%s\n",data,name,legion,template,alttemplate,units,position.x,position.y,position.z,strucdata) else data=string.format("%s%s,%s,%s,%s,%d,%d,%d,%d\n",data,name,legion,template,alttemplate,units,position.x,position.y,position.z) end end end local outcome=UTILS.SaveToFile(Path,Filename,data) return outcome end function UTILS.LoadSetOfOpsGroups(Path,Filename) local filename=Filename or"SetOfGroups" local datatable={} if UTILS.CheckFileExists(Path,filename)then local outcome,loadeddata=UTILS.LoadFromFile(Path,Filename) table.remove(loadeddata,1) for _id,_entry in pairs(loadeddata)do local dataset=UTILS.Split(_entry,",") local groupname=dataset[1] local legion=dataset[2] local template=dataset[3] local alttemplate=dataset[4] local size=tonumber(dataset[5]) local posx=tonumber(dataset[6]) local posy=tonumber(dataset[7]) local posz=tonumber(dataset[8]) local structure=dataset[9] local coordinate=COORDINATE:NewFromVec3({x=posx,y=posy,z=posz}) if size>0 then local data={groupname=groupname,size=size,coordinate=coordinate,template=template,structure=structure,legion=legion,alttemplate=alttemplate} table.insert(datatable,data) end end else return nil end return datatable end function UTILS.ClockHeadingString(refHdg,tgtHdg) local relativeAngle=tgtHdg-refHdg if relativeAngle<0 then relativeAngle=relativeAngle+360 end local clockPos=math.ceil((relativeAngle%360)/30) return clockPos.." o'clock" end function UTILS.MGRSStringToSRSFriendly(Text,Slow) local Text=string.gsub(Text,"MGRS ","") Text=string.gsub(Text,"%s+","") Text=string.gsub(Text,"([%a%d])","%1;") Text=string.gsub(Text,"A","Alpha") Text=string.gsub(Text,"B","Bravo") Text=string.gsub(Text,"C","Charlie") Text=string.gsub(Text,"D","Delta") Text=string.gsub(Text,"E","Echo") Text=string.gsub(Text,"F","Foxtrot") Text=string.gsub(Text,"G","Golf") Text=string.gsub(Text,"H","Hotel") Text=string.gsub(Text,"I","India") Text=string.gsub(Text,"J","Juliett") Text=string.gsub(Text,"K","Kilo") Text=string.gsub(Text,"L","Lima") Text=string.gsub(Text,"M","Mike") Text=string.gsub(Text,"N","November") Text=string.gsub(Text,"O","Oscar") Text=string.gsub(Text,"P","Papa") Text=string.gsub(Text,"Q","Quebec") Text=string.gsub(Text,"R","Romeo") Text=string.gsub(Text,"S","Sierra") Text=string.gsub(Text,"T","Tango") Text=string.gsub(Text,"U","Uniform") Text=string.gsub(Text,"V","Victor") Text=string.gsub(Text,"W","Whiskey") Text=string.gsub(Text,"X","Xray") Text=string.gsub(Text,"Y","Yankee") Text=string.gsub(Text,"Z","Zulu") Text=string.gsub(Text,"0","zero") Text=string.gsub(Text,"9","niner") if Slow then Text=''..Text..'' end Text="MGRS;"..Text return Text end function UTILS.ReadCSV(filename) if not UTILS.FileExists(filename)then env.error("File does not exist") return nil end local function _loadfile(filename) local f=io.open(filename,"rb") if f then local data=f:read("*all") f:close() return data else BASE:E(string.format("WARNING: Could read data from file %s!",tostring(filename))) return nil end end local data=_loadfile(filename) local lines=UTILS.Split(data,"\n") for _,line in pairs(lines)do line=string.gsub(line,"[\n\r]","") end local sep=";" local columns=UTILS.Split(lines[1],sep) table.remove(lines,1) local csvdata={} for i,line in pairs(lines)do line=string.gsub(line,"[\n\r]","") local row={} for j,value in pairs(UTILS.Split(line,sep))do local key=string.gsub(columns[j],"[\n\r]","") row[key]=value end table.insert(csvdata,row) end return csvdata end function UTILS.LCGRandomSeed(seed) UTILS.lcg={ seed=seed or math.random(1,2^32-1), a=1664525, c=1013904223, m=2^32 } end function UTILS.LCGRandom() if UTILS.lcg==nil then UTILS.LCGRandomSeed() end UTILS.lcg.seed=(UTILS.lcg.a*UTILS.lcg.seed+UTILS.lcg.c)%UTILS.lcg.m return UTILS.lcg.seed/UTILS.lcg.m end function UTILS.SpawnFARPAndFunctionalStatics(Name,Coordinate,FARPType,Coalition,Country,CallSign,Frequency,Modulation,ADF,SpawnRadius,VehicleTemplate,Liquids,Equipment) local farplocation=Coordinate local farptype=FARPType or ENUMS.FARPType.FARP local Coalition=Coalition or coalition.side.BLUE local callsign=CallSign or CALLSIGN.FARP.Berlin local freq=Frequency or 127.5 local mod=Modulation or radio.modulation.AM local radius=SpawnRadius or 100 if radius<0 or radius>150 then radius=100 end local liquids=Liquids or 10 liquids=liquids*1000 local equip=Equipment or 10 local statictypes=ENUMS.FARPObjectTypeNamesAndShape[farptype]or{TypeName="FARP",ShapeName="FARPS"} local STypeName=statictypes.TypeName local SShapeName=statictypes.ShapeName local Country=Country or(Coalition==coalition.side.BLUE and country.id.USA or country.id.RUSSIA) local ReturnObjects={} local newfarp=SPAWNSTATIC:NewFromType(STypeName,"Heliports",Country) newfarp:InitShape(SShapeName) newfarp:InitFARP(callsign,freq,mod) local spawnedfarp=newfarp:SpawnFromCoordinate(farplocation,0,Name) table.insert(ReturnObjects,spawnedfarp) local FARPStaticObjectsNato={ ["FUEL"]={TypeName="FARP Fuel Depot",ShapeName="GSM Rus",Category="Fortifications"}, ["AMMO"]={TypeName="FARP Ammo Dump Coating",ShapeName="SetkaKP",Category="Fortifications"}, ["TENT"]={TypeName="FARP Tent",ShapeName="PalatkaB",Category="Fortifications"}, ["WINDSOCK"]={TypeName="Windsock",ShapeName="H-Windsock_RW",Category="Fortifications"}, } local farpobcount=0 for _name,_object in pairs(FARPStaticObjectsNato)do local objloc=farplocation:Translate(radius,farpobcount*30) local heading=objloc:HeadingTo(farplocation) local newobject=SPAWNSTATIC:NewFromType(_object.TypeName,_object.Category,Country) newobject:InitShape(_object.ShapeName) newobject:InitHeading(heading) newobject:SpawnFromCoordinate(objloc,farpobcount*30,_name.." - "..Name) table.insert(ReturnObjects,newobject) farpobcount=farpobcount+1 end if VehicleTemplate and type(VehicleTemplate)=="string"then local vcoordinate=farplocation:Translate(radius,farpobcount*30) local heading=vcoordinate:HeadingTo(farplocation) local vehicles=SPAWN:NewWithAlias(VehicleTemplate,"FARP Vehicles - "..Name) vehicles:InitGroupHeading(heading) vehicles:InitCountry(Country) vehicles:InitCoalition(Coalition) vehicles:InitDelayOff() local spawnedvehicle=vehicles:SpawnFromCoordinate(vcoordinate) table.insert(ReturnObjects,spawnedvehicle) end local newWH=STORAGE:New(Name) if liquids and liquids>0 then newWH:SetLiquid(STORAGE.Liquid.DIESEL,liquids) newWH:SetLiquid(STORAGE.Liquid.GASOLINE,liquids) newWH:SetLiquid(STORAGE.Liquid.JETFUEL,liquids) newWH:SetLiquid(STORAGE.Liquid.MW50,liquids) end if equip and equip>0 then for cat,nitem in pairs(ENUMS.Storage.weapons)do for name,item in pairs(nitem)do newWH:SetItem(item,equip) end end end local ADFName if ADF and type(ADF)=="number"then local ADFFreq=ADF*1000 local Sound="l10n/DEFAULT/beacon.ogg" local vec3=farplocation:GetVec3() ADFName=Name.." ADF "..tostring(ADF).."KHz" trigger.action.radioTransmission(Sound,vec3,0,true,ADFFreq,250,ADFName) end return ReturnObjects,ADFName end function UTILS.Vec2toVec3(vec,y) if not vec.z then if vec.alt and not y then y=vec.alt elseif not y then y=0 end return{x=vec.x,y=y,z=vec.y} else return{x=vec.x,y=vec.y,z=vec.z} end end function UTILS.GetNorthCorrection(gPoint) local point=UTILS.DeepCopy(gPoint) 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 function UTILS.GetDHMS(timeInSec) if timeInSec and type(timeInSec)=='number'then local tbl={d=0,h=0,m=0,s=0} if timeInSec>86400 then while timeInSec>86400 do tbl.d=tbl.d+1 timeInSec=timeInSec-86400 end end if timeInSec>3600 then while timeInSec>3600 do tbl.h=tbl.h+1 timeInSec=timeInSec-3600 end end if timeInSec>60 then while timeInSec>60 do tbl.m=tbl.m+1 timeInSec=timeInSec-60 end end tbl.s=timeInSec return tbl else BASE:E("No number handed!") return end end function UTILS.GetDirectionRadians(vec,point) local dir=math.atan2(vec.z,vec.x) if point then dir=dir+UTILS.GetNorthCorrection(point) end if dir<0 then dir=dir+2*math.pi end return dir end function UTILS.IsPointInPolygon(point,poly,maxalt) point=UTILS.Vec2toVec3(point) local px=point.x local pz=point.z local cn=0 local newpoly=UTILS.DeepCopy(poly) if not maxalt or(point.y<=maxalt)then local polysize=#newpoly newpoly[#newpoly+1]=newpoly[1] newpoly[1]=UTILS.Vec2toVec3(newpoly[1]) for k=1,polysize do newpoly[k+1]=UTILS.Vec2toVec3(newpoly[k+1]) if((newpoly[k].z<=pz)and(newpoly[k+1].z>pz))or((newpoly[k].z>pz)and(newpoly[k+1].z<=pz))then local vt=(pz-newpoly[k].z)/(newpoly[k+1].z-newpoly[k].z) if(px5000 then value=5000 end return world.weather.setFogThickness(value) end function UTILS.Weather.RemoveFog() return world.weather.setFogThickness(0) end function UTILS.Weather.GetFogVisibilityDistanceMax() return world.weather.getFogVisibilityDistance() end function UTILS.Weather.SetFogVisibilityDistance(Thickness) local value=Thickness if value<100 then value=100 elseif value>100000 then value=100000 end return world.weather.setFogVisibilityDistance(value) end function UTILS.Weather.SetFogAnimation(AnimationKeys) return world.weather.setFogAnimation(AnimationKeys) end function UTILS.Weather.StopFogAnimation() return world.weather.setFogAnimation({}) 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 de-sanitized!") 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) 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 do FIFO={ ClassName="FIFO", lid="", version="0.0.5", counter=0, pointer=0, stackbypointer={}, stackbyid={} } function FIFO:New() local self=BASE:Inherit(self,BASE:New()) self.pointer=0 self.counter=0 self.stackbypointer={} self.stackbyid={} self.uniquecounter=0 self.lid=string.format("%s (%s) | ","FiFo",self.version) self:T(self.lid.."Created.") return self end function FIFO:Clear() self:T(self.lid.."Clear") self.pointer=0 self.counter=0 self.stackbypointer=nil self.stackbyid=nil self.stackbypointer={} self.stackbyid={} self.uniquecounter=0 return self end function FIFO:Push(Object,UniqueID) self:T(self.lid.."Push") self:T({Object,UniqueID}) self.pointer=self.pointer+1 self.counter=self.counter+1 local uniID=UniqueID if not UniqueID then self.uniquecounter=self.uniquecounter+1 uniID=self.uniquecounter end self.stackbyid[uniID]={pointer=self.pointer,data=Object,uniqueID=uniID} self.stackbypointer[self.pointer]={pointer=self.pointer,data=Object,uniqueID=uniID} return self end function FIFO:Pull() self:T(self.lid.."Pull") if self.counter==0 then return nil end local object=self.stackbypointer[1].data self.stackbypointer[1]=nil self.counter=self.counter-1 self:Flatten() return object end function FIFO:PullByPointer(Pointer) self:T(self.lid.."PullByPointer "..tostring(Pointer)) if self.counter==0 then return nil end local object=self.stackbypointer[Pointer] self.stackbypointer[Pointer]=nil if object then self.stackbyid[object.uniqueID]=nil end self.counter=self.counter-1 self:Flatten() if object then return object.data else return nil end end function FIFO:ReadByPointer(Pointer) self:T(self.lid.."ReadByPointer "..tostring(Pointer)) if self.counter==0 or not Pointer or not self.stackbypointer[Pointer]then return nil end local object=self.stackbypointer[Pointer] if object then return object.data else return nil end end function FIFO:ReadByID(UniqueID) self:T(self.lid.."ReadByID "..tostring(UniqueID)) if self.counter==0 or not UniqueID or not self.stackbyid[UniqueID]then return nil end local object=self.stackbyid[UniqueID] if object then return object.data else return nil end end function FIFO:PullByID(UniqueID) self:T(self.lid.."PullByID "..tostring(UniqueID)) if self.counter==0 then return nil end local object=self.stackbyid[UniqueID] if object then return self:PullByPointer(object.pointer) else return nil end end function FIFO:Flatten() self:T(self.lid.."Flatten") local pointerstack={} local idstack={} local counter=0 for _ID,_entry in pairs(self.stackbypointer)do counter=counter+1 pointerstack[counter]={pointer=counter,data=_entry.data,uniqueID=_entry.uniqueID} end for _ID,_entry in pairs(pointerstack)do idstack[_entry.uniqueID]={pointer=_entry.pointer,data=_entry.data,uniqueID=_entry.uniqueID} end self.stackbypointer=nil self.stackbypointer=pointerstack self.stackbyid=nil self.stackbyid=idstack self.counter=counter self.pointer=counter return self end function FIFO:IsEmpty() self:T(self.lid.."IsEmpty") return self.counter==0 and true or false end function FIFO:GetSize() self:T(self.lid.."GetSize") return self.counter end function FIFO:Count() self:T(self.lid.."Count") return self.counter end function FIFO:IsNotEmpty() self:T(self.lid.."IsNotEmpty") return not self:IsEmpty() end function FIFO:GetPointerStack() self:T(self.lid.."GetPointerStack") return self.stackbypointer end function FIFO:HasUniqueID(UniqueID) self:T(self.lid.."HasUniqueID") if self.stackbyid[UniqueID]~=nil then return true else return false end end function FIFO:GetIDStack() self:T(self.lid.."GetIDStack") return self.stackbyid end function FIFO:GetIDStackSorted() self:T(self.lid.."GetIDStackSorted") local stack=self:GetIDStack() local idstack={} for _id,_entry in pairs(stack)do idstack[#idstack+1]=_id self:T({"pre",_id}) end local function sortID(a,b) return a=1 then self:_F(Arguments,DebugInfoCurrent,DebugInfoFrom) end end end function BASE:F2(Arguments) if BASE.Debug and _TraceOnOff==true and _TraceLevel>=2 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==true and _TraceLevel>=3 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,BASE:_Serialize(Arguments))) end end end function BASE:T(Arguments) if BASE.Debug and _TraceOnOff==true 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==true and _TraceLevel>=2 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==true and _TraceLevel>=3 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,UTILS.BasicSerialize(Arguments))) else env.info(string.format("%1s:%30s%05d(%s)","E",self.ClassName,self.ClassID,UTILS.BasicSerialize(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,UTILS.BasicSerialize(Arguments))) else env.info(string.format("%1s:%30s%05d(%s)","I",self.ClassName,self.ClassID,UTILS.BasicSerialize(Arguments))) 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_score result=%s",tostring(isGen),tostring(isAny),tostring(isAll),tostring(self.negateResult),tostring(result))) return result end function CONDITION:_EvalConditionsAll(functions) local gotone=false for _,_condition in pairs(functions or{})do local condition=_condition gotone=true local istrue=condition.func(unpack(condition.arg)) if not istrue then return false end end return true end function CONDITION:_EvalConditionsAny(functions) local gotone=false for _,_condition in pairs(functions or{})do local condition=_condition gotone=true local istrue=condition.func(unpack(condition.arg)) if istrue then return true end end if gotone then return false else return true end end function CONDITION:_CreateCondition(Ftype,Function,...) self.functionCounter=self.functionCounter+1 local condition={} condition.uid=self.functionCounter condition.type=Ftype or 0 condition.persistence=self.defaultPersist condition.func=Function condition.arg={} if arg then condition.arg=arg end return condition end function CONDITION.IsTimeGreater(Time,Absolute) local Tnow=nil if Absolute then Tnow=timer.getAbsTime() else Tnow=timer.getTime() end if Tnow>Time then return true else return false end return nil end function CONDITION.IsRandomSuccess(Probability) Probability=Probability or 50 math.random() math.random() math.random() local N=math.random()*100 if N0 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 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 world.event.S_EVENT_NEW_DYNAMIC_CARGO=world.event.S_EVENT_MAX+1008 world.event.S_EVENT_DYNAMIC_CARGO_LOADED=world.event.S_EVENT_MAX+1009 world.event.S_EVENT_DYNAMIC_CARGO_UNLOADED=world.event.S_EVENT_MAX+1010 world.event.S_EVENT_DYNAMIC_CARGO_REMOVED=world.event.S_EVENT_MAX+1011 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, AIAbortMission=world.event.S_EVENT_AI_ABORT_MISSION or-1, DayNight=world.event.S_EVENT_DAYNIGHT or-1, FlightTime=world.event.S_EVENT_FLIGHT_TIME or-1, SelfKillPilot=world.event.S_EVENT_PLAYER_SELF_KILL_PILOT or-1, PlayerCaptureAirfield=world.event.S_EVENT_PLAYER_CAPTURE_AIRFIELD or-1, EmergencyLanding=world.event.S_EVENT_EMERGENCY_LANDING or-1, UnitCreateTask=world.event.S_EVENT_UNIT_CREATE_TASK or-1, UnitDeleteTask=world.event.S_EVENT_UNIT_DELETE_TASK or-1, SimulationStart=world.event.S_EVENT_SIMULATION_START or-1, WeaponRearm=world.event.S_EVENT_WEAPON_REARM or-1, WeaponDrop=world.event.S_EVENT_WEAPON_DROP or-1, UnitTaskComplete=world.event.S_EVENT_UNIT_TASK_COMPLETE or-1, UnitTaskStage=world.event.S_EVENT_UNIT_TASK_STAGE or-1, MacExtraScore=world.event.S_EVENT_MAC_EXTRA_SCORE or-1, MissionRestart=world.event.S_EVENT_MISSION_RESTART or-1, MissionWinner=world.event.S_EVENT_MISSION_WINNER or-1, RunwayTakeoff=world.event.S_EVENT_RUNWAY_TAKEOFF or-1, RunwayTouch=world.event.S_EVENT_RUNWAY_TOUCH or-1, MacLMSRestart=world.event.S_EVENT_MAC_LMS_RESTART or-1, SimulationFreeze=world.event.S_EVENT_SIMULATION_FREEZE or-1, SimulationUnfreeze=world.event.S_EVENT_SIMULATION_UNFREEZE or-1, HumanAircraftRepairStart=world.event.S_EVENT_HUMAN_AIRCRAFT_REPAIR_START or-1, HumanAircraftRepairFinish=world.event.S_EVENT_HUMAN_AIRCRAFT_REPAIR_FINISH or-1, NewDynamicCargo=world.event.S_EVENT_NEW_DYNAMIC_CARGO or-1, DynamicCargoLoaded=world.event.S_EVENT_DYNAMIC_CARGO_LOADED or-1, DynamicCargoUnloaded=world.event.S_EVENT_DYNAMIC_CARGO_UNLOADED or-1, DynamicCargoRemoved=world.event.S_EVENT_DYNAMIC_CARGO_REMOVED 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" }, [EVENTS.AIAbortMission]={ Order=1, Side="I", Event="OnEventAIAbortMission", Text="S_EVENT_AI_ABORT_MISSION" }, [EVENTS.DayNight]={ Order=1, Event="OnEventDayNight", Text="S_EVENT_DAYNIGHT" }, [EVENTS.FlightTime]={ Order=1, Event="OnEventFlightTime", Text="S_EVENT_FLIGHT_TIME" }, [EVENTS.SelfKillPilot]={ Order=1, Side="I", Event="OnEventSelfKillPilot", Text="S_EVENT_PLAYER_SELF_KILL_PILOT" }, [EVENTS.PlayerCaptureAirfield]={ Order=1, Event="OnEventPlayerCaptureAirfield", Text="S_EVENT_PLAYER_CAPTURE_AIRFIELD" }, [EVENTS.EmergencyLanding]={ Order=1, Side="I", Event="OnEventEmergencyLanding", Text="S_EVENT_EMERGENCY_LANDING" }, [EVENTS.UnitCreateTask]={ Order=1, Event="OnEventUnitCreateTask", Text="S_EVENT_UNIT_CREATE_TASK" }, [EVENTS.UnitDeleteTask]={ Order=1, Event="OnEventUnitDeleteTask", Text="S_EVENT_UNIT_DELETE_TASK" }, [EVENTS.SimulationStart]={ Order=1, Event="OnEventSimulationStart", Text="S_EVENT_SIMULATION_START" }, [EVENTS.WeaponRearm]={ Order=1, Side="I", Event="OnEventWeaponRearm", Text="S_EVENT_WEAPON_REARM" }, [EVENTS.WeaponDrop]={ Order=1, Side="I", Event="OnEventWeaponDrop", Text="S_EVENT_WEAPON_DROP" }, [EVENTS.UnitTaskStage]={ Order=1, Side="I", Event="OnEventUnitTaskStage", Text="S_EVENT_UNIT_TASK_STAGE " }, [EVENTS.MacExtraScore]={ Order=1, Side="I", Event="OnEventMacExtraScore", Text="S_EVENT_MAC_EXTRA_SCOREP" }, [EVENTS.MissionRestart]={ Order=1, Side="I", Event="OnEventMissionRestart", Text="S_EVENT_MISSION_RESTART" }, [EVENTS.MissionWinner]={ Order=1, Side="I", Event="OnEventMissionWinner", Text="S_EVENT_MISSION_WINNER" }, [EVENTS.RunwayTakeoff]={ Order=1, Side="I", Event="OnEventRunwayTakeoff", Text="S_EVENT_RUNWAY_TAKEOFF" }, [EVENTS.RunwayTouch]={ Order=1, Side="I", Event="OnEventRunwayTouch", Text="S_EVENT_RUNWAY_TOUCH" }, [EVENTS.MacLMSRestart]={ Order=1, Side="I", Event="OnEventMacLMSRestart", Text="S_EVENT_MAC_LMS_RESTART" }, [EVENTS.SimulationFreeze]={ Order=1, Side="I", Event="OnEventSimulationFreeze", Text="S_EVENT_SIMULATION_FREEZE" }, [EVENTS.SimulationUnfreeze]={ Order=1, Side="I", Event="OnEventSimulationUnfreeze", Text="S_EVENT_SIMULATION_UNFREEZE" }, [EVENTS.HumanAircraftRepairStart]={ Order=1, Side="I", Event="OnEventHumanAircraftRepairStart", Text="S_EVENT_HUMAN_AIRCRAFT_REPAIR_START" }, [EVENTS.HumanAircraftRepairFinish]={ Order=1, Side="I", Event="OnEventHumanAircraftRepairFinish", Text="S_EVENT_HUMAN_AIRCRAFT_REPAIR_FINISH" }, [EVENTS.NewDynamicCargo]={ Order=1, Side="I", Event="OnEventNewDynamicCargo", Text="S_EVENT_NEW_DYNAMIC_CARGO" }, [EVENTS.DynamicCargoLoaded]={ Order=1, Side="I", Event="OnEventDynamicCargoLoaded", Text="S_EVENT_DYNAMIC_CARGO_LOADED" }, [EVENTS.DynamicCargoUnloaded]={ Order=1, Side="I", Event="OnEventDynamicCargoUnloaded", Text="S_EVENT_DYNAMIC_CARGO_UNLOADED" }, [EVENTS.DynamicCargoRemoved]={ Order=1, Side="I", Event="OnEventDynamicCargoRemoved", Text="S_EVENT_DYNAMIC_CARGO_REMOVED" }, } 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 function EVENT:CreateEventNewDynamicCargo(DynamicCargo) self:F({DynamicCargo}) local Event={ id=EVENTS.NewDynamicCargo, time=timer.getTime(), dynamiccargo=DynamicCargo, initiator=DynamicCargo:GetDCSObject(), } world.onEvent(Event) end function EVENT:CreateEventDynamicCargoLoaded(DynamicCargo) self:F({DynamicCargo}) local Event={ id=EVENTS.DynamicCargoLoaded, time=timer.getTime(), dynamiccargo=DynamicCargo, initiator=DynamicCargo:GetDCSObject(), } world.onEvent(Event) end function EVENT:CreateEventDynamicCargoUnloaded(DynamicCargo) self:F({DynamicCargo}) local Event={ id=EVENTS.DynamicCargoUnloaded, time=timer.getTime(), dynamiccargo=DynamicCargo, initiator=DynamicCargo:GetDCSObject(), } world.onEvent(Event) end function EVENT:CreateEventDynamicCargoRemoved(DynamicCargo) self:F({DynamicCargo}) local Event={ id=EVENTS.DynamicCargoRemoved, time=timer.getTime(), dynamiccargo=DynamicCargo, initiator=DynamicCargo: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=Object.getCategory(Event.initiator) 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 local Unit=UNIT:FindByName(Event.IniDCSUnitName) if Unit then Event.IniObjectCategory=Object.Category.UNIT end elseif 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=Event.IniUnit and Event.IniUnit.GroupName or"" Event.IniGroupName=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() if Event.IniPlayerName then local PID=NET.GetPlayerIDByName(nil,Event.IniPlayerName) if PID then Event.IniPlayerUCID=net.get_player_info(tonumber(PID),'ucid') end end Event.IniCoalition=Event.IniDCSUnit:getCoalition() Event.IniTypeName=Event.IniDCSUnit:getTypeName() Event.IniCategory=Event.IniDCSUnit:getDesc().category elseif Event.IniObjectCategory==Object.Category.CARGO then Event.IniDCSUnit=Event.initiator Event.IniDCSUnitName=Event.IniDCSUnit:getName() Event.IniUnitName=Event.IniDCSUnitName if string.match(Event.IniUnitName,".+|%d%d:%d%d|PKG%d+")then Event.IniDynamicCargo=DYNAMICCARGO:FindByName(Event.IniUnitName) Event.IniDynamicCargoName=Event.IniUnitName Event.IniPlayerName=string.match(Event.IniUnitName,"^(.+)|%d%d:%d%d|PKG%d+") else Event.IniUnit=CARGO:FindByName(Event.IniDCSUnitName) end Event.IniCoalition=Event.IniDCSUnit:getCoalition() Event.IniCategory=Event.IniDCSUnit:getDesc().category Event.IniTypeName=Event.IniDCSUnit:getTypeName() elseif Event.IniObjectCategory==Object.Category.SCENERY then Event.IniDCSUnit=Event.initiator Event.IniDCSUnitName=Event.IniDCSUnit.getName and Event.IniDCSUnit:getName()or"Scenery no name "..math.random(1,20000) Event.IniUnitName=Event.IniDCSUnitName Event.IniUnit=SCENERY:Register(Event.IniDCSUnitName,Event.initiator) Event.IniCategory=Event.IniDCSUnit.getDesc and Event.IniDCSUnit:getDesc().category Event.IniTypeName=Event.initiator:isExist()and Event.IniDCSUnit:getTypeName()or"SCENERY" elseif 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() if not Event.IniUnit then _DATABASE:_RegisterAirbase(Event.initiator) Event.IniUnit=AIRBASE:FindByName(Event.IniDCSUnitName) end end end if Event.target then Event.TgtObjectCategory=Object.getCategory(Event.target) 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() if Event.TgtPlayerName then local PID=NET.GetPlayerIDByName(nil,Event.TgtPlayerName) if PID then Event.TgtPlayerUCID=net.get_player_info(tonumber(PID),'ucid') end end Event.TgtCoalition=Event.TgtDCSUnit:getCoalition() Event.TgtCategory=Event.TgtDCSUnit:getDesc().category Event.TgtTypeName=Event.TgtDCSUnit:getTypeName() elseif Event.TgtObjectCategory==Object.Category.STATIC then Event.TgtDCSUnit=Event.target if Event.target.isExist and Event.target:isExist()and Event.id~=33 then Event.TgtDCSUnitName=Event.TgtDCSUnit:getName() if Event.TgtDCSUnitName and Event.TgtDCSUnitName~=""then 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() end 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 elseif Event.TgtObjectCategory==Object.Category.SCENERY then Event.TgtDCSUnit=Event.target Event.TgtDCSUnitName=Event.TgtDCSUnit.getName and Event.TgtDCSUnit.getName()or nil if Event.TgtDCSUnitName~=nil then 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 end if Event.weapon and type(Event.weapon)=="table"and Event.weapon.isExist and Event.weapon:isExist()then Event.Weapon=Event.weapon Event.WeaponName=Event.weapon:isExist()and Event.weapon:getTypeName()or"Unknown Weapon" Event.WeaponUNIT=CLIENT:Find(Event.Weapon,'',true) Event.WeaponPlayerName=Event.WeaponUNIT and Event.Weapon.getPlayerName and Event.Weapon:getPlayerName() Event.WeaponCoalition=Event.WeaponUNIT and Event.Weapon.getCoalition and Event.Weapon:getCoalition() Event.WeaponCategory=Event.WeaponUNIT and Event.Weapon.getDesc and Event.Weapon:getDesc().category Event.WeaponTypeName=Event.WeaponUNIT and Event.Weapon.getTypeName and Event.Weapon:getTypeName() end if Event.place then if Event.id==EVENTS.LandingAfterEjection then else if Event.place:isExist()and Object.getCategory(Event.place)~=Object.Category.SCENERY then Event.Place=AIRBASE:Find(Event.place) Event.PlaceName=Event.Place:GetName() end 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.IniCoalition=Event.coalition Event.MarkGroupID=Event.groupID end if Event.cargo then Event.Cargo=Event.cargo Event.CargoName=Event.cargo.Name end if Event.dynamiccargo then Event.IniDynamicCargo=Event.dynamiccargo Event.IniDynamicCargoName=Event.IniDynamicCargo.StaticName if Event.IniDynamicCargo.Owner or Event.IniUnitName then Event.IniPlayerName=Event.IniDynamicCargo.Owner or string.match(Event.IniUnitName or"None|00:00|PKG00","^(.+)|%d%d:%d%d|PKG%d+") end 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 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=Event.IniGroup or GROUP:FindByName(Event.IniDCSGroupName) Event.TgtGroup=Event.TgtGroup or 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 or Event.id==EVENTS.UnitLost 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 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 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 or Event.id==EVENTS.UnitLost 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 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 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 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 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() self:SetLocale("en") 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:SetLocale(Locale) self.Locale=Locale or"en" end function SETTINGS:GetLocale() return self.Locale or _SETTINGS:GetLocale() 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:IsImperial()) 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()or"None" local PlayerMenu=MENU_GROUP:New(PlayerGroup,'Settings "'..PlayerName..'"') self.PlayerMenu=PlayerMenu self:T(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) 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() if self.Group[MenuGroupName]and self.Group[MenuGroupName].Menus and self.Group[MenuGroupName].Menus[Path]then return self.Group[MenuGroupName].Menus[Path] end end return nil end function MENU_INDEX:SetGroupMenu(Group,Path,Menu) local MenuGroupName=Group:GetName() 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 return self 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 return self 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 return self 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 return self 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 return self 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 return self end function MENU_GROUP:RefreshAndOrderByTag() do missionCommands.removeItemForGroup(self.GroupID,self.MenuPath) missionCommands.addSubMenuForGroup(self.GroupID,self.MenuText,self.MenuParentPath) local MenuTable={} for MenuText,Menu in pairs(self.Menus or{})do local tag=Menu.MenuTag or math.random(1,10000) MenuTable[#MenuTable+1]={Tag=tag,Enty=Menu} end table.sort(MenuTable,function(k1,k2)return k1.tag0 then self:ScheduleOnce(Delay,ZONE_BASE.UndrawZone,self) else if self.DrawID then if type(self.DrawID)~="table"then UTILS.RemoveMark(self.DrawID) else for _,mark_id in pairs(self.DrawID)do UTILS.RemoveMark(mark_id) end end end end return self end function ZONE_BASE:GetDrawID() return self.DrawID end function ZONE_BASE:SmokeZone(SmokeColor) end function ZONE_BASE:SetZoneProbability(ZoneProbability) self.ZoneProbability=ZoneProbability or 1 return self end function ZONE_BASE:GetZoneProbability() return self.ZoneProbability end function ZONE_BASE:GetZoneMaybe() local Randomization=math.random() if Randomization<=self.ZoneProbability then return self else return nil end end function ZONE_BASE:SetCheckTime(seconds) self.Checktime=seconds or 5 return self end function ZONE_BASE:Trigger(Objects) self:SetStartState("TriggerStopped") self:AddTransition("TriggerStopped","TriggerStart","TriggerRunning") self:AddTransition("*","EnteredZone","*") self:AddTransition("*","LeftZone","*") self:AddTransition("*","ZoneEmpty","*") self:AddTransition("*","ObjectDead","*") self:AddTransition("*","TriggerRunCheck","*") self:AddTransition("*","TriggerStop","TriggerStopped") self:TriggerStart() self.checkobjects=Objects self.ObjectsInZone=false if UTILS.IsInstanceOf(Objects,"SET_BASE")then self.objectset=Objects.Set else self.objectset={Objects} end self:_TriggerCheck(true) self:__TriggerRunCheck(self.Checktime) return self end function ZONE_BASE:_TriggerCheck(fromstart) local objectset=self.objectset or{} if fromstart then for _,_object in pairs(objectset)do local obj=_object if not obj.TriggerInZone then obj.TriggerInZone={} obj.TriggerZoneDeadNotification=false end if obj and obj:IsAlive()and self:IsCoordinateInZone(obj:GetCoordinate())then obj.TriggerInZone[self.ZoneName]=true self.ObjectsInZone=true else obj.TriggerInZone[self.ZoneName]=false end end else local objcount=0 for _,_object in pairs(objectset)do local obj=_object if obj and obj:IsAlive()then if not obj.TriggerInZone then obj.TriggerInZone={} end if not obj.TriggerInZone[self.ZoneName]then obj.TriggerInZone[self.ZoneName]=false end local inzone=self:IsCoordinateInZone(obj:GetCoordinate()) if inzone and obj.TriggerInZone[self.ZoneName]then objcount=objcount+1 self.ObjectsInZone=true obj.TriggerZoneDeadNotification=false end if inzone and not obj.TriggerInZone[self.ZoneName]then self:__EnteredZone(0.5,obj) obj.TriggerInZone[self.ZoneName]=true objcount=objcount+1 self.ObjectsInZone=true obj.TriggerZoneDeadNotification=false elseif(not inzone)and obj.TriggerInZone[self.ZoneName]then self:__LeftZone(0.5,obj) obj.TriggerInZone[self.ZoneName]=false else end else if not obj.TriggerZoneDeadNotification==true then obj.TriggerInZone=nil self:__ObjectDead(0.5,obj) obj.TriggerZoneDeadNotification=true end end end if objcount==0 and self.ObjectsInZone==true then self.ObjectsInZone=false self:__ZoneEmpty(0.5) end end return self end function ZONE_BASE:onafterTriggerRunCheck(From,Event,To) if self:GetState()~="TriggerStopped"then self:_TriggerCheck() self:__TriggerRunCheck(self.Checktime) end return self end function ZONE_BASE:GetProperty(PropertyName) return self.Properties[PropertyName] end function ZONE_BASE:GetAllProperties() return self.Properties end ZONE_RADIUS={ ClassName="ZONE_RADIUS", } function ZONE_RADIUS:New(ZoneName,Vec2,Radius,DoNotRegisterZone) local self=BASE:Inherit(self,ZONE_BASE:New(ZoneName)) self.Radius=Radius self.Vec2=Vec2 if not DoNotRegisterZone then _EVENTDISPATCHER:CreateEventNewZone(self) end 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:DrawZone(Coalition,Color,Alpha,FillColor,FillAlpha,LineType,ReadOnly) local coordinate=self:GetCoordinate() local Radius=self:GetRadius() Color=Color or self:GetColorRGB() Alpha=Alpha or 1 FillColor=FillColor or UTILS.DeepCopy(Color) FillAlpha=FillAlpha or self:GetColorAlpha() self.DrawID=coordinate:CircleToAll(Radius,Coalition,Color,Alpha,FillColor,FillAlpha,LineType,ReadOnly) return self end function ZONE_RADIUS:BoundZone(Points,CountryID,UnBound) local Point={} local Vec2=self:GetVec2() local countryID=CountryID or country.id.USA 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) 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() COORDINATE:New(Point.x,AddHeight,Point.y):Smoke(SmokeColor) end return self end function ZONE_RADIUS:FlareZone(FlareColor,Points,Azimuth,AddHeight) 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() COORDINATE:New(Point.x,AddHeight,Point.y):Flare(FlareColor,Azimuth) end return self end function ZONE_RADIUS:GetRadius() return self.Radius end function ZONE_RADIUS:SetRadius(Radius) self.Radius=Radius return self.Radius end function ZONE_RADIUS:GetVec2() return self.Vec2 end function ZONE_RADIUS:SetVec2(Vec2) self.Vec2=Vec2 return self.Vec2 end function ZONE_RADIUS:GetVec3(Height) Height=Height or 0 local Vec2=self:GetVec2() local Vec3={x=Vec2.x,y=land.getHeight(self:GetVec2())+Height,z=Vec2.y} return Vec3 end function ZONE_RADIUS:Scan(ObjectCategories,UnitCategories) self.ScanData={} self.ScanData.Coalitions={} self.ScanData.Scenery={} self.ScanData.SceneryTable={} self.ScanData.Units={} local ZoneCoord=self:GetCoordinate() local ZoneRadius=self:GetRadius() local SphereSearch={ id=world.VolumeType.SPHERE, params={ point=ZoneCoord:GetVec3(), radius=ZoneRadius, } } local function EvaluateZone(ZoneObject) if ZoneObject then local ObjectCategory=Object.getCategory(ZoneObject) 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 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(tostring(SceneryName),ZoneObject) table.insert(self.ScanData.SceneryTable,self.ScanData.Scenery[SceneryType][SceneryName]) end end return true end world.searchObjects(ObjectCategories,SphereSearch,EvaluateZone) end function ZONE_RADIUS:RemoveJunk() local radius=self.Radius local vec3=self:GetVec3() local volS={ id=world.VolumeType.SPHERE, params={point=vec3,radius=radius} } local n=world.removeJunk(volS) return n 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(),false) 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:GetScannedSceneryObjects() return self.ScanData.SceneryTable end function ZONE_RADIUS:GetScannedSetScenery() local scenery=SET_SCENERY:New() local objects=self:GetScannedSceneryObjects() for _,_obj in pairs(objects)do scenery:AddScenery(_obj) end return 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() local SphereSearch={ id=world.VolumeType.SPHERE, params={ point=ZoneCoord:GetVec3(), radius=ZoneRadius, } } 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) if not Vec2 then return false end 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) if not Vec3 then return false end local InZone=self:IsVec2InZone({x=Vec3.x,y=Vec3.z}) return InZone end function ZONE_RADIUS:GetRandomVec2(inner,outer,surfacetypes) local Vec2=self:GetVec2() local _inner=inner or 0 local _outer=outer or self:GetRadius() if surfacetypes and type(surfacetypes)~="table"then surfacetypes={surfacetypes} end local function _getpoint() local point={} 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) return point end local function _checkSurface(point) local stype=land.getSurfaceType(point) for _,sf in pairs(surfacetypes)do if sf==stype then return true end end return false end local point=_getpoint() if surfacetypes then local N=1;local Nmax=100;local gotit=false while gotit==false and N<=Nmax do gotit=_checkSurface(point) if gotit then else point=_getpoint() N=N+1 end end end return point end function ZONE_RADIUS:GetRandomPointVec2(inner,outer) local PointVec2=COORDINATE:NewFromVec2(self:GetRandomVec2(inner,outer)) return PointVec2 end function ZONE_RADIUS:GetRandomVec3(inner,outer) local Vec2=self:GetRandomVec2(inner,outer) return{x=Vec2.x,y=self.y,z=Vec2.y} end function ZONE_RADIUS:GetRandomPointVec3(inner,outer) local PointVec3=COORDINATE:NewFromVec2(self:GetRandomVec2(inner,outer)) return PointVec3 end function ZONE_RADIUS:GetRandomCoordinate(inner,outer,surfacetypes) local vec2=self:GetRandomVec2(inner,outer,surfacetypes) local Coordinate=COORDINATE:NewFromVec2(vec2) return Coordinate end function ZONE_RADIUS:GetRandomCoordinateWithoutBuildings(inner,outer,distance,markbuildings,markfinal) local dist=distance or 100 local objects={} if self.ScanData and self.ScanData.Scenery then objects=self:GetScannedScenery() else self:Scan({Object.Category.SCENERY}) objects=self:GetScannedScenery() end local T0=timer.getTime() local T1=timer.getTime() local buildings={} local buildingzones={} if self.ScanData and self.ScanData.BuildingCoordinates then buildings=self.ScanData.BuildingCoordinates buildingzones=self.ScanData.BuildingZones else for _,_object in pairs(objects)do for _,_scen in pairs(_object)do local scenery=_scen local description=scenery:GetDesc() if description and description.attributes and description.attributes.Buildings then if markbuildings then MARKER:New(scenery:GetCoordinate(),"Building"):ToAll() end buildings[#buildings+1]=scenery:GetCoordinate() local bradius=scenery:GetBoundingRadius()or dist local bzone=ZONE_RADIUS:New("Building-"..math.random(1,100000),scenery:GetVec2(),bradius,false) buildingzones[#buildingzones+1]=bzone end end end self.ScanData.BuildingCoordinates=buildings self.ScanData.BuildingZones=buildingzones end local rcoord=nil local found=true local iterations=0 for i=1,1000 do iterations=iterations+1 rcoord=self:GetRandomCoordinate(inner,outer) found=true for _,_coord in pairs(buildingzones)do local zone=_coord if zone:IsPointVec2InZone(rcoord)then found=false break end end if found then if markfinal then MARKER:New(rcoord,"FREE"):ToAll() end break end end if not found then local rcoord=nil local found=true local iterations=0 for i=1,1000 do iterations=iterations+1 rcoord=self:GetRandomCoordinate(inner,outer) found=true for _,_coord in pairs(buildings)do local coord=_coord if coord:Get3DDistance(rcoord)0)or(d2>0)or(d3>0) return not(has_neg and has_pos) end function _ZONE_TRIANGLE:GetRandomVec2(points) points=points or self.Points local pt={math.random(),math.random()} table.sort(pt) local s=pt[1] local t=pt[2]-pt[1] local u=1-pt[2] return{x=s*points[1].x+t*points[2].x+u*points[3].x, y=s*points[1].y+t*points[2].y+u*points[3].y} end function _ZONE_TRIANGLE:Draw(Coalition,Color,Alpha,FillColor,FillAlpha,LineType,ReadOnly) Coalition=Coalition or-1 Color=Color or{1,0,0} Alpha=Alpha or 1 FillColor=FillColor or Color if not FillColor then UTILS.DeepCopy(Color)end FillAlpha=FillAlpha or Alpha if not FillAlpha then FillAlpha=1 end for i=1,#self.Coords do local c1=self.Coords[i] local c2=self.Coords[i%#self.Coords+1] local id=c1:LineToAll(c2,Coalition,Color,Alpha,LineType,ReadOnly) self.DrawID[#self.DrawID+1]=id end local newID=self.Coords[1]:MarkupToAllFreeForm({self.Coords[2],self.Coords[3]},Coalition,Color,Alpha,FillColor,FillAlpha,LineType,ReadOnly) self.DrawID[#self.DrawID+1]=newID return self.DrawID end function _ZONE_TRIANGLE:Fill(Coalition,FillColor,FillAlpha,ReadOnly) Coalition=Coalition or-1 FillColor=FillColor FillAlpha=FillAlpha local newID=self.Coords[1]:MarkupToAllFreeForm({self.Coords[2],self.Coords[3]},Coalition,nil,nil,FillColor,FillAlpha,0,nil) self.DrawID[#self.DrawID+1]=newID return self.DrawID end ZONE_POLYGON_BASE={ ClassName="ZONE_POLYGON_BASE", _Triangles={}, SurfaceArea=0, DrawID={}, FillTriangles={}, Borderlines={}, } function ZONE_POLYGON_BASE:New(ZoneName,PointsArray) local self=BASE:Inherit(self,ZONE_BASE:New(ZoneName)) 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 self._Triangles=self:_Triangulate() self.SurfaceArea=self:_CalculateSurfaceArea() end return self end function ZONE_POLYGON_BASE:_Triangulate() local points=self._.Polygon local triangles={} local function get_orientation(shape_points) local sum=0 for i=1,#shape_points do local j=i%#shape_points+1 sum=sum+(shape_points[j].x-shape_points[i].x)*(shape_points[j].y+shape_points[i].y) end return sum>=0 and"clockwise"or"counter-clockwise" end local function ensure_clockwise(shape_points) local orientation=get_orientation(shape_points) if orientation=="counter-clockwise"then local reversed={} for i=#shape_points,1,-1 do table.insert(reversed,shape_points[i]) end return reversed end return shape_points end local function is_clockwise(p1,p2,p3) local cross_product=(p2.x-p1.x)*(p3.y-p1.y)-(p2.y-p1.y)*(p3.x-p1.x) return cross_product<0 end local function divide_recursively(shape_points) if#shape_points==3 then table.insert(triangles,_ZONE_TRIANGLE:New(shape_points[1],shape_points[2],shape_points[3])) elseif#shape_points>3 then for i,p1 in ipairs(shape_points)do local p2=shape_points[(i%#shape_points)+1] local p3=shape_points[(i+1)%#shape_points+1] local triangle=_ZONE_TRIANGLE:New(p1,p2,p3) local is_ear=true if not is_clockwise(p1,p2,p3)then is_ear=false else for _,point in ipairs(shape_points)do if point~=p1 and point~=p2 and point~=p3 and triangle:ContainsPoint(point)then is_ear=false break end end end if is_ear then local is_valid_triangle=true for _,point in ipairs(points)do if point~=p1 and point~=p2 and point~=p3 and triangle:ContainsPoint(point)then is_valid_triangle=false break end end if is_valid_triangle then table.insert(triangles,triangle) local remaining_points={} for j,point in ipairs(shape_points)do if point~=p2 then table.insert(remaining_points,point) end end divide_recursively(remaining_points) break end else end end end end points=ensure_clockwise(points) divide_recursively(points) return triangles 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 self._Triangles=self:_Triangulate() self.SurfaceArea=self:_CalculateSurfaceArea() 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 self._Triangles=self:_Triangulate() self.SurfaceArea=self:_CalculateSurfaceArea() return self end function ZONE_POLYGON_BASE:_CalculateSurfaceArea() local area=0 for _,triangle in pairs(self._Triangles)do area=area+triangle.SurfaceArea end return area end function ZONE_POLYGON_BASE:GetVec2() local Bounds=self:GetBoundingSquare() return{x=(Bounds.x2+Bounds.x1)/2,y=(Bounds.y2+Bounds.y1)/2} end function ZONE_POLYGON_BASE:GetVertexVec2(Index) return self._.Polygon[Index or 1] end function ZONE_POLYGON_BASE:GetVertexVec3(Index) local vec2=self:GetVertexVec2(Index) if vec2 then local vec3={x=vec2.x,y=land.getHeight(vec2),z=vec2.y} return vec3 end return nil end function ZONE_POLYGON_BASE:GetVertexCoordinate(Index) local vec2=self:GetVertexVec2(Index) if vec2 then local coord=COORDINATE:NewFromVec2(vec2) return coord end return nil end function ZONE_POLYGON_BASE:GetVerticiesVec2() return self._.Polygon end function ZONE_POLYGON_BASE:GetVerticiesVec3() local coords={} for i,vec2 in ipairs(self._.Polygon)do local vec3={x=vec2.x,y=land.getHeight(vec2),z=vec2.y} table.insert(coords,vec3) end return coords end function ZONE_POLYGON_BASE:GetVerticiesCoordinates() local coords={} for i,vec2 in ipairs(self._.Polygon)do local coord=COORDINATE:NewFromVec2(vec2) table.insert(coords,coord) end return coords end function ZONE_POLYGON_BASE:Flush() 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 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:DrawZone(Coalition,Color,Alpha,FillColor,FillAlpha,LineType,ReadOnly,IncludeTriangles) if self._.Polygon and#self._.Polygon>=3 then Coalition=Coalition or self:GetDrawCoalition() self:SetDrawCoalition(Coalition) Color=Color or self:GetColorRGB() Alpha=Alpha or self:GetColorAlpha() FillColor=FillColor or self:GetFillColorRGB() FillAlpha=FillAlpha or self:GetFillColorAlpha() if FillColor then self:ReFill(FillColor,FillAlpha) end if Color then self:ReDrawBorderline(Color,Alpha,LineType) end end if false then local coords=self:GetVerticiesCoordinates() local coord=coords[1] table.remove(coords,1) coord:MarkupToAllFreeForm(coords,Coalition,Color,Alpha,FillColor,FillAlpha,LineType,ReadOnly,"Drew Polygon") if true then return end end return self end function ZONE_POLYGON_BASE:ReFill(Color,Alpha) local color=Color or self:GetFillColorRGB()or{1,0,0} local alpha=Alpha or self:GetFillColorAlpha()or 1 local coalition=self:GetDrawCoalition()or-1 if#self.FillTriangles>0 then for _,triangle in pairs(self._Triangles)do triangle:UndrawZone() end for _,_value in pairs(self.FillTriangles)do table.remove_by_value(self.DrawID,_value) end self.FillTriangles=nil self.FillTriangles={} end for _,triangle in pairs(self._Triangles)do local draw_ids=triangle:Fill(coalition,color,alpha,nil) self.FillTriangles=draw_ids table.combine(self.DrawID,draw_ids) end return self end function ZONE_POLYGON_BASE:ReDrawBorderline(Color,Alpha,LineType) local color=Color or self:GetFillColorRGB()or{1,0,0} local alpha=Alpha or self:GetFillColorAlpha()or 1 local coalition=self:GetDrawCoalition()or-1 local linetype=LineType or 1 if#self.Borderlines>0 then for _,MarkID in pairs(self.Borderlines)do trigger.action.removeMark(MarkID) end for _,_value in pairs(self.Borderlines)do table.remove_by_value(self.DrawID,_value) end self.Borderlines=nil self.Borderlines={} end local coords=self:GetVerticiesCoordinates() for i=1,#coords do local c1=coords[i] local c2=coords[i%#coords+1] local newID=c1:LineToAll(c2,coalition,color,alpha,linetype,nil) self.DrawID[#self.DrawID+1]=newID self.Borderlines[#self.Borderlines+1]=newID end return self end function ZONE_POLYGON_BASE:GetSurfaceArea() return self.SurfaceArea end function ZONE_POLYGON_BASE:GetRadius() local center=self:GetVec2() local radius=0 for _,_vec2 in pairs(self._.Polygon)do local vec2=_vec2 local r=UTILS.VecDist2D(center,vec2) if r>radius then radius=r end end return radius end function ZONE_POLYGON_BASE:GetZoneRadius(ZoneName,DoNotRegisterZone) local center=self:GetVec2() local radius=self:GetRadius() local zone=ZONE_RADIUS:New(ZoneName or self.ZoneName,center,radius,DoNotRegisterZone) return zone end function ZONE_POLYGON_BASE:GetZoneQuad(ZoneName,DoNotRegisterZone) local vec1,vec3=self:GetBoundingVec2() local vec2={x=vec1.x,y=vec3.y} local vec4={x=vec3.x,y=vec1.y} local zone=ZONE_POLYGON_BASE:New(ZoneName or self.ZoneName,{vec1,vec2,vec3,vec4}) return zone end function ZONE_POLYGON_BASE:RemoveJunk(Height) Height=Height or 1000 local vec2SW,vec2NE=self:GetBoundingVec2() local vec3SW={x=vec2SW.x,y=-Height,z=vec2SW.y} local vec3NE={x=vec2NE.x,y=Height,z=vec2NE.y} local volume={ id=world.VolumeType.BOX, params={ min=vec3SW, max=vec3SW } } local n=world.removeJunk(volume) return n end function ZONE_POLYGON_BASE:SmokeZone(SmokeColor,Segments) Segments=Segments or 10 local i=1 local j=#self._.Polygon while i<=#self._.Polygon do 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) COORDINATE:New(PointX,0,PointY):Smoke(SmokeColor) end j=i i=i+1 end return self end function ZONE_POLYGON_BASE:FlareZone(FlareColor,Segments,Azimuth,AddHeight) Segments=Segments or 10 AddHeight=AddHeight or 0 local i=1 local j=#self._.Polygon while i<=#self._.Polygon do 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) COORDINATE:New(PointX,AddHeight,PointY):Flare(FlareColor,Azimuth) end j=i i=i+1 end return self end function ZONE_POLYGON_BASE:IsVec2InZone(Vec2) if not Vec2 then return false end local Next local Prev local InPolygon=false Next=1 Prev=#self._.Polygon while Next<=#self._.Polygon do 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 Prev=Next Next=Next+1 end return InPolygon end function ZONE_POLYGON_BASE:IsVec3InZone(Vec3) if not Vec3 then return false end local InZone=self:IsVec2InZone({x=Vec3.x,y=Vec3.z}) return InZone end function ZONE_POLYGON_BASE:GetRandomVec2() local weights={} for _,triangle in pairs(self._Triangles)do weights[triangle]=triangle.SurfaceArea/self.SurfaceArea end local random_weight=math.random() local accumulated_weight=0 for triangle,weight in pairs(weights)do accumulated_weight=accumulated_weight+weight if accumulated_weight>=random_weight then return triangle:GetRandomVec2() end end end function ZONE_POLYGON_BASE:GetRandomPointVec2() local PointVec2=COORDINATE:NewFromVec2(self:GetRandomVec2()) return PointVec2 end function ZONE_POLYGON_BASE:GetRandomPointVec3() local PointVec3=COORDINATE:NewFromVec2(self:GetRandomVec2()) return PointVec3 end function ZONE_POLYGON_BASE:GetRandomCoordinate() local Coordinate=COORDINATE:NewFromVec2(self:GetRandomVec2()) 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 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=(y2self._.Polygon[i].x)and self._.Polygon[i].x or x1 x2=(x2self._.Polygon[i].y)and self._.Polygon[i].y or y1 y2=(y21 and self:GetScannedCoalition(Coalition)==true end function ZONE_POLYGON:IsNoneInZoneOfCoalition(Coalition) return self:GetScannedCoalition(Coalition)==nil end function ZONE_POLYGON:IsNoneInZone() return self:CountScannedCoalitions()==0 end end do ZONE_ELASTIC={ ClassName="ZONE_ELASTIC", points={}, setGroups={} } function ZONE_ELASTIC:New(ZoneName,Points) local self=BASE:Inherit(self,ZONE_POLYGON_BASE:New(ZoneName,Points)) _EVENTDISPATCHER:CreateEventNewZone(self) if Points then self.points=Points end return self end function ZONE_ELASTIC:AddVertex2D(Vec2) table.insert(self.points,Vec2) return self end function ZONE_ELASTIC:RemoveVertex2D(Vec2) local found=false local findex=0 for _id,_vec2 in pairs(self.points)do if _vec2.x==Vec2.x and _vec2.y==Vec2.y then found=true findex=_id break end end if found==true and findex>0 then table.remove(self.points,findex) end return self end function ZONE_ELASTIC:RemoveVertex3D(Vec3) return self:RemoveVertex2D({x=Vec3.x,y=Vec3.z}) end function ZONE_ELASTIC:AddVertex3D(Vec3) table.insert(self.points,{x=Vec3.x,y=Vec3.z}) return self end function ZONE_ELASTIC:AddSetGroup(GroupSet) table.insert(self.setGroups,GroupSet) return self end function ZONE_ELASTIC:Update(Delay,Draw) local points=UTILS.DeepCopy(self.points or{}) if self.setGroups then for _,_setGroup in pairs(self.setGroups)do local setGroup=_setGroup for _,_group in pairs(setGroup.Set)do local group=_group if group and group:IsAlive()then table.insert(points,group:GetVec2()) end end end end self._.Polygon=self:_ConvexHull(points) self._Triangles=self:_Triangulate() self.SurfaceArea=self:_CalculateSurfaceArea() if Draw~=false then if self.DrawID or Draw==true then self:UndrawZone() self:DrawZone() end end return self end function ZONE_ELASTIC:StartUpdate(Tstart,dT,Tstop,Draw) self.updateID=self:ScheduleRepeat(Tstart,dT,0,Tstop,ZONE_ELASTIC.Update,self,0,Draw) return self end function ZONE_ELASTIC:StopUpdate(Delay) if Delay and Delay>0 then self:ScheduleOnce(Delay,ZONE_ELASTIC.StopUpdate,self) else if self.updateID then self:ScheduleStop(self.updateID) self.updateID=nil end end return self end function ZONE_ELASTIC:_ConvexHull(pl) if#pl==0 then return{} end table.sort(pl,function(left,right) return left.x(b.y-a.y)*(c.x-a.x) end for i,pt in pairs(pl)do while#h>=2 and not ccw(h[#h-1],h[#h],pt)do table.remove(h,#h) end table.insert(h,pt) end local t=#h+1 for i=#pl,1,-1 do local pt=pl[i] while#h>=t and not ccw(h[#h-1],h[#h],pt)do table.remove(h,#h) end table.insert(h,pt) end table.remove(h,#h) return h end end ZONE_OVAL={ ClassName="OVAL", ZoneName="", MajorAxis=nil, MinorAxis=nil, Angle=0, DrawPoly=nil } function ZONE_OVAL:New(name,vec2,major_axis,minor_axis,angle) self=BASE:Inherit(self,ZONE_BASE:New()) self.ZoneName=name self.CenterVec2=vec2 self.MajorAxis=major_axis self.MinorAxis=minor_axis self.Angle=angle or 0 _DATABASE:AddZone(name,self) return self end function ZONE_OVAL:NewFromDrawing(DrawingName) self=BASE:Inherit(self,ZONE_BASE:New(DrawingName)) for _,layer in pairs(env.mission.drawings.layers)do for _,object in pairs(layer["objects"])do if string.find(object["name"],DrawingName,1,true)then if object["polygonMode"]=="oval"then self.CenterVec2={x=object["mapX"],y=object["mapY"]} self.MajorAxis=object["r1"] self.MinorAxis=object["r2"] self.Angle=object["angle"] end end end end _DATABASE:AddZone(DrawingName,self) return self end function ZONE_OVAL:GetMajorAxis() return self.MajorAxis end function ZONE_OVAL:GetMinorAxis() return self.MinorAxis end function ZONE_OVAL:GetAngle() return self.Angle end function ZONE_OVAL:GetVec2() return self.CenterVec2 end function ZONE_OVAL:IsVec2InZone(vec2) local cos,sin=math.cos,math.sin local dx=vec2.x-self.CenterVec2.x local dy=vec2.y-self.CenterVec2.y local rx=dx*cos(self.Angle)+dy*sin(self.Angle) local ry=-dx*sin(self.Angle)+dy*cos(self.Angle) return rx*rx/(self.MajorAxis*self.MajorAxis)+ry*ry/(self.MinorAxis*self.MinorAxis)<=1 end function ZONE_OVAL:GetBoundingSquare() local min_x=self.CenterVec2.x-self.MajorAxis local min_y=self.CenterVec2.y-self.MinorAxis local max_x=self.CenterVec2.x+self.MajorAxis local max_y=self.CenterVec2.y+self.MinorAxis return{ {x=min_x,y=min_x},{x=max_x,y=min_y},{x=max_x,y=max_y},{x=min_x,y=max_y} } end function ZONE_OVAL:PointsOnEdge(num_points) num_points=num_points or 40 local points={} local dtheta=2*math.pi/num_points for i=0,num_points-1 do local theta=i*dtheta local x=self.CenterVec2.x+self.MajorAxis*math.cos(theta)*math.cos(self.Angle)-self.MinorAxis*math.sin(theta)*math.sin(self.Angle) local y=self.CenterVec2.y+self.MajorAxis*math.cos(theta)*math.sin(self.Angle)+self.MinorAxis*math.sin(theta)*math.cos(self.Angle) table.insert(points,{x=x,y=y}) end return points end function ZONE_OVAL:GetRandomVec2() local theta=math.rad(self.Angle) local random_point=math.sqrt(math.random()) local phi=math.random()*2*math.pi local x_c=random_point*math.cos(phi) local y_c=random_point*math.sin(phi) local x_e=x_c*self.MajorAxis local y_e=y_c*self.MinorAxis local rx=(x_e*math.cos(theta)-y_e*math.sin(theta))+self.CenterVec2.x local ry=(x_e*math.sin(theta)+y_e*math.cos(theta))+self.CenterVec2.y return{x=rx,y=ry} end function ZONE_OVAL:GetRandomPointVec2() return COORDINATE:NewFromVec2(self:GetRandomVec2()) end function ZONE_OVAL:GetRandomPointVec3() return COORDINATE:NewFromVec3(self:GetRandomVec2()) end function ZONE_OVAL:DrawZone(Coalition,Color,Alpha,FillColor,FillAlpha,LineType) Coalition=Coalition or self:GetDrawCoalition() self:SetDrawCoalition(Coalition) Color=Color or self:GetColorRGB() Alpha=Alpha or 1 self:SetColor(Color,Alpha) FillColor=FillColor or self:GetFillColorRGB() if not FillColor then UTILS.DeepCopy(Color) end FillAlpha=FillAlpha or self:GetFillColorAlpha() if not FillAlpha then FillAlpha=0.15 end LineType=LineType or 1 self:SetFillColor(FillColor,FillAlpha) self.DrawPoly=ZONE_POLYGON:NewFromPointsArray(self.ZoneName,self:PointsOnEdge(80)) self.DrawPoly:DrawZone(Coalition,Color,Alpha,FillColor,FillAlpha,LineType) end function ZONE_OVAL:UndrawZone() if self.DrawPoly then self.DrawPoly:UndrawZone() end end do ZONE_AIRBASE={ ClassName="ZONE_AIRBASE", } function ZONE_AIRBASE:New(AirbaseName,Radius) Radius=Radius or 4000 local Airbase=AIRBASE:FindByName(AirbaseName) local self=BASE:Inherit(self,ZONE_RADIUS:New(AirbaseName,Airbase:GetVec2(),Radius,true)) self._.ZoneAirbase=Airbase self._.ZoneVec2Cache=self._.ZoneAirbase:GetVec2() if Airbase:IsShip()then self.isShip=true self.isHelipad=false self.isAirdrome=false elseif Airbase:IsHelipad()then self.isShip=false self.isHelipad=true self.isAirdrome=false elseif Airbase:IsAirdrome()then self.isShip=false self.isHelipad=false self.isAirdrome=true end _EVENTDISPATCHER:CreateEventNewZone(self) return self end function ZONE_AIRBASE:GetAirbase() return self._.ZoneAirbase end function ZONE_AIRBASE:GetVec2() local ZoneVec2=nil if self._.ZoneAirbase:IsAlive()then ZoneVec2=self._.ZoneAirbase:GetVec2() self._.ZoneVec2Cache=ZoneVec2 else ZoneVec2=self._.ZoneVec2Cache end return ZoneVec2 end function ZONE_AIRBASE:GetRandomPointVec2(inner,outer) local PointVec2=COORDINATE:NewFromVec2(self:GetRandomVec2()) return PointVec2 end end ZONE_DETECTION={ ClassName="ZONE_DETECTION", } function ZONE_DETECTION:New(ZoneName,Detection,Radius) local self=BASE:Inherit(self,ZONE_BASE:New(ZoneName)) self:F({ZoneName,Detection,Radius}) self.Detection=Detection self.Radius=Radius return self end function ZONE_DETECTION: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_DETECTION: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() COORDINATE:New(Point.x,AddHeight,Point.y):Smoke(SmokeColor) end return self end function ZONE_DETECTION: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() COORDINATE:New(Point.x,AddHeight,Point.y):Flare(FlareColor,Azimuth) end return self end function ZONE_DETECTION:GetRadius() self:F2(self.ZoneName) self:T2({self.Radius}) return self.Radius end function ZONE_DETECTION:SetRadius(Radius) self:F2(self.ZoneName) self.Radius=Radius self:T2({self.Radius}) return self.Radius end function ZONE_DETECTION:IsVec2InZone(Vec2) self:F2(Vec2) local Coordinates=self.Detection:GetDetectedItemCoordinates() for CoordinateID,Coordinate in pairs(Coordinates)do local ZoneVec2=Coordinate:GetVec2() if ZoneVec2 then if((Vec2.x-ZoneVec2.x)^2+(Vec2.y-ZoneVec2.y)^2)^0.5<=self:GetRadius()then return true end end end return false end function ZONE_DETECTION:IsVec3InZone(Vec3) self:F2(Vec3) local InZone=self:IsVec2InZone({x=Vec3.x,y=Vec3.z}) return InZone end DATABASE={ ClassName="DATABASE", Templates={ Units={}, Groups={}, Statics={}, ClientsByName={}, ClientsByID={}, }, UNITS={}, UNITS_Index={}, STATICS={}, GROUPS={}, PLAYERS={}, PLAYERSJOINED={}, PLAYERUNITS={}, CLIENTS={}, CARGOS={}, AIRBASES={}, COUNTRY_ID={}, COUNTRY_NAME={}, NavPoints={}, PLAYERSETTINGS={}, ZONENAMES={}, HITS={}, DESTROYS={}, ZONES={}, ZONES_GOAL={}, WAREHOUSES={}, FLIGHTGROUPS={}, FLIGHTCONTROLS={}, OPSZONES={}, PATHLINES={}, STORAGES={}, STNS={}, SADL={}, DYNAMICCARGO={}, } local _DATABASECoalition= { [1]="Red", [2]="Blue", [3]="Neutral", } local _DATABASECategory= { ["plane"]=Unit.Category.AIRPLANE, ["helicopter"]=Unit.Category.HELICOPTER, ["vehicle"]=Unit.Category.GROUND_UNIT, ["ship"]=Unit.Category.SHIP, ["static"]=Unit.Category.STRUCTURE, } function DATABASE:New() local self=BASE:Inherit(self,BASE:New()) self:SetEventPriority(1) self:HandleEvent(EVENTS.Birth,self._EventOnBirth) self:HandleEvent(EVENTS.PlayerEnterUnit,self._EventOnPlayerEnterUnit) self:HandleEvent(EVENTS.Dead,self._EventOnDeadOrCrash) self:HandleEvent(EVENTS.Crash,self._EventOnDeadOrCrash) self:HandleEvent(EVENTS.RemoveUnit,self._EventOnDeadOrCrash) self:HandleEvent(EVENTS.UnitLost,self._EventOnDeadOrCrash) self:HandleEvent(EVENTS.Hit,self.AccountHits) self:HandleEvent(EVENTS.NewCargo) self:HandleEvent(EVENTS.DeleteCargo) self:HandleEvent(EVENTS.NewZone) self:HandleEvent(EVENTS.DeleteZone) self:HandleEvent(EVENTS.PlayerLeaveUnit,self._EventOnPlayerLeaveUnit) self:HandleEvent(EVENTS.DynamicCargoRemoved,self._EventOnDynamicCargoRemoved) self:_RegisterTemplates() self:_RegisterGroupsAndUnits() self:_RegisterClients() self:_RegisterStatics() self.UNITS_Position=0 return self end function DATABASE:FindUnit(UnitName) local UnitFound=self.UNITS[UnitName] return UnitFound end function DATABASE:AddUnit(DCSUnitName,force) local DCSunitName=DCSUnitName if type(DCSunitName)=="number"then DCSunitName=string.format("%d",DCSUnitName)end if not self.UNITS[DCSunitName]or force==true then self:T({"Add UNIT:",DCSunitName}) self.UNITS[DCSunitName]=UNIT:Register(DCSunitName) end return self.UNITS[DCSunitName] end function DATABASE:DeleteUnit(DCSUnitName) self:T("DeleteUnit "..tostring(DCSUnitName)) self.UNITS[DCSUnitName]=nil end function DATABASE:AddStatic(DCSStaticName) if not self.STATICS[DCSStaticName]then self.STATICS[DCSStaticName]=STATIC:Register(DCSStaticName) end return self.STATICS[DCSStaticName] end function DATABASE:DeleteStatic(DCSStaticName) self.STATICS[DCSStaticName]=nil end function DATABASE:FindStatic(StaticName) local StaticFound=self.STATICS[StaticName] return StaticFound end function DATABASE:AddDynamicCargo(Name) if not self.DYNAMICCARGO[Name]then self.DYNAMICCARGO[Name]=DYNAMICCARGO:Register(Name) end return self.DYNAMICCARGO[Name] end function DATABASE:FindDynamicCargo(DynamicCargoName) local StaticFound=self.DYNAMICCARGO[DynamicCargoName] return StaticFound end function DATABASE:DeleteDynamicCargo(DynamicCargoName) self.DYNAMICCARGO[DynamicCargoName]=nil return self end function DATABASE:AddAirbase(AirbaseName) if not self.AIRBASES[AirbaseName]then self.AIRBASES[AirbaseName]=AIRBASE:Register(AirbaseName) end return self.AIRBASES[AirbaseName] end function DATABASE:DeleteAirbase(AirbaseName) self.AIRBASES[AirbaseName]=nil end function DATABASE:FindAirbase(AirbaseName) local AirbaseFound=self.AIRBASES[AirbaseName] return AirbaseFound end function DATABASE:AddStorage(AirbaseName) if not self.STORAGES[AirbaseName]then self.STORAGES[AirbaseName]=STORAGE:New(AirbaseName) end return self.STORAGES[AirbaseName] end function DATABASE:DeleteStorage(AirbaseName) self.STORAGES[AirbaseName]=nil end function DATABASE:FindStorage(AirbaseName) local storage=self.STORAGES[AirbaseName] return storage end do function DATABASE:FindZone(ZoneName) local ZoneFound=self.ZONES[ZoneName] return ZoneFound end function DATABASE:AddZone(ZoneName,Zone) if not self.ZONES[ZoneName]then self.ZONES[ZoneName]=Zone end end function DATABASE:DeleteZone(ZoneName) self.ZONES[ZoneName]=nil end function DATABASE:AddPathline(PathlineName,Pathline) if not self.PATHLINES[PathlineName]then self.PATHLINES[PathlineName]=Pathline end end function DATABASE:FindPathline(PathlineName) local pathline=self.PATHLINES[PathlineName] return pathline end function DATABASE:DeletePathline(PathlineName) self.PATHLINES[PathlineName]=nil return self end function DATABASE:_RegisterZones() for ZoneID,ZoneData in pairs(env.mission.triggers.zones)do local ZoneName=ZoneData.name local color=ZoneData.color or{1,0,0,0.15} local Zone=nil if ZoneData.type==0 then self:I(string.format("Register ZONE: %s (Circular)",ZoneName)) Zone=ZONE:New(ZoneName) else self:I(string.format("Register ZONE: %s (Polygon, Quad)",ZoneName)) Zone=ZONE_POLYGON:NewFromPointsArray(ZoneName,ZoneData.verticies) end if Zone then Zone.Color=color Zone.ZoneID=ZoneData.zoneId local ZoneProperties=ZoneData.properties or nil Zone.Properties={} if ZoneName and ZoneProperties then for _,ZoneProp in ipairs(ZoneProperties)do if ZoneProp.key then Zone.Properties[ZoneProp.key]=ZoneProp.value end end end self.ZONENAMES[ZoneName]=ZoneName self:AddZone(ZoneName,Zone) end end for ZoneGroupName,ZoneGroup in pairs(self.GROUPS)do if ZoneGroupName:match("#ZONE_POLYGON")then local ZoneName1=ZoneGroupName:match("(.*)#ZONE_POLYGON") local ZoneName2=ZoneGroupName:match(".*#ZONE_POLYGON(.*)") local ZoneName=ZoneName1..(ZoneName2 or"") self:I(string.format("Register ZONE: %s (Polygon)",ZoneName)) local Zone_Polygon=ZONE_POLYGON:New(ZoneName,ZoneGroup) Zone_Polygon:SetColor({1,0,0},0.15) self.ZONENAMES[ZoneName]=ZoneName self:AddZone(ZoneName,Zone_Polygon) end end if env.mission.drawings and env.mission.drawings.layers then for layerID,layerData in pairs(env.mission.drawings.layers or{})do for objectID,objectData in pairs(layerData.objects or{})do if objectData.polygonMode and(objectData.polygonMode=="free")and objectData.points and#objectData.points>=4 then local ZoneName=objectData.name or"Unknown free Polygon Drawing" local vec2={x=objectData.mapX,y=objectData.mapY} local points=UTILS.DeepCopy(objectData.points) for i,_point in pairs(points)do local point=_point points[i]=UTILS.Vec2Add(point,vec2) end table.remove(points,#points) self:I(string.format("Register ZONE: %s (Polygon (free) drawing with %d vertices)",ZoneName,#points)) local Zone=ZONE_POLYGON:NewFromPointsArray(ZoneName,points) Zone:SetColor({1,0,0},0.15) Zone:SetFillColor({1,0,0},0.15) if objectData.colorString then local color=string.gsub(objectData.colorString,"^0x","") local r=tonumber(string.sub(color,1,2),16)/255 local g=tonumber(string.sub(color,3,4),16)/255 local b=tonumber(string.sub(color,5,6),16)/255 local a=tonumber(string.sub(color,7,8),16)/255 Zone:SetColor({r,g,b},a) end if objectData.fillColorString then local color=string.gsub(objectData.colorString,"^0x","") local r=tonumber(string.sub(color,1,2),16)/255 local g=tonumber(string.sub(color,3,4),16)/255 local b=tonumber(string.sub(color,5,6),16)/255 local a=tonumber(string.sub(color,7,8),16)/255 Zone:SetFillColor({r,g,b},a) end self.ZONENAMES[ZoneName]=ZoneName self:AddZone(ZoneName,Zone) elseif objectData.polygonMode and objectData.polygonMode=="rect"then local ZoneName=objectData.name or"Unknown rect Polygon Drawing" local vec2={x=objectData.mapX,y=objectData.mapY} local w=objectData.width local h=objectData.height local points={} points[1]={x=vec2.x-h/2,y=vec2.y+w/2} points[2]={x=vec2.x+h/2,y=vec2.y+w/2} points[3]={x=vec2.x+h/2,y=vec2.y-w/2} points[4]={x=vec2.x-h/2,y=vec2.y-w/2} self:I(string.format("Register ZONE: %s (Polygon (rect) drawing with %d vertices)",ZoneName,#points)) local Zone=ZONE_POLYGON:NewFromPointsArray(ZoneName,points) Zone:SetColor({1,0,0},0.15) if objectData.colorString then local color=string.gsub(objectData.colorString,"^0x","") local r=tonumber(string.sub(color,1,2),16)/255 local g=tonumber(string.sub(color,3,4),16)/255 local b=tonumber(string.sub(color,5,6),16)/255 local a=tonumber(string.sub(color,7,8),16)/255 Zone:SetColor({r,g,b},a) end if objectData.fillColorString then local color=string.gsub(objectData.colorString,"^0x","") local r=tonumber(string.sub(color,1,2),16)/255 local g=tonumber(string.sub(color,3,4),16)/255 local b=tonumber(string.sub(color,5,6),16)/255 local a=tonumber(string.sub(color,7,8),16)/255 Zone:SetFillColor({r,g,b},a) end self.ZONENAMES[ZoneName]=ZoneName self:AddZone(ZoneName,Zone) elseif objectData.lineMode and(objectData.lineMode=="segments"or objectData.lineMode=="segment"or objectData.lineMode=="free")and objectData.points and#objectData.points>=2 then local Name=objectData.name or"Unknown Line Drawing" local vec2={x=objectData.mapX,y=objectData.mapY} local points=UTILS.DeepCopy(objectData.points) for i,_point in pairs(points)do local point=_point points[i]=UTILS.Vec2Add(point,vec2) end self:I(string.format("Register PATHLINE: %s (Line drawing with %d points)",Name,#points)) local Pathline=PATHLINE:NewFromVec2Array(Name,points) self:AddPathline(Name,Pathline) end end end end end end do function DATABASE:FindZoneGoal(ZoneName) local ZoneFound=self.ZONES_GOAL[ZoneName] return ZoneFound end function DATABASE:AddZoneGoal(ZoneName,Zone) if not self.ZONES_GOAL[ZoneName]then self.ZONES_GOAL[ZoneName]=Zone end end function DATABASE:DeleteZoneGoal(ZoneName) self.ZONES_GOAL[ZoneName]=nil end end do function DATABASE:FindOpsZone(ZoneName) local ZoneFound=self.OPSZONES[ZoneName] return ZoneFound end function DATABASE:AddOpsZone(OpsZone) if OpsZone then local ZoneName=OpsZone:GetName() if not self.OPSZONES[ZoneName]then self.OPSZONES[ZoneName]=OpsZone end end end function DATABASE:DeleteOpsZone(ZoneName) self.OPSZONES[ZoneName]=nil end end do function DATABASE:AddCargo(Cargo) if not self.CARGOS[Cargo.Name]then self.CARGOS[Cargo.Name]=Cargo end end function DATABASE:DeleteCargo(CargoName) self.CARGOS[CargoName]=nil end function DATABASE:FindCargo(CargoName) local CargoFound=self.CARGOS[CargoName] return CargoFound end function DATABASE:IsCargo(TemplateName) TemplateName=env.getValueDictByKey(TemplateName) local Cargo=TemplateName:match("#(CARGO)") return Cargo and Cargo=="CARGO" end function DATABASE:_RegisterCargos() local Groups=UTILS.DeepCopy(self.GROUPS) for CargoGroupName,CargoGroup in pairs(Groups)do if self:IsCargo(CargoGroupName)then local CargoInfo=CargoGroupName:match("#CARGO(.*)") local CargoParam=CargoInfo and CargoInfo:match("%((.*)%)") local CargoName1=CargoGroupName:match("(.*)#CARGO%(.*%)") local CargoName2=CargoGroupName:match(".*#CARGO%(.*%)(.*)") local CargoName=CargoName1..(CargoName2 or"") local Type=CargoParam and CargoParam:match("T=([%a%d ]+),?") local Name=CargoParam and CargoParam:match("N=([%a%d]+),?")or CargoName local LoadRadius=CargoParam and tonumber(CargoParam:match("RR=([%a%d]+),?")) local NearRadius=CargoParam and tonumber(CargoParam:match("NR=([%a%d]+),?")) self:I({"Register CargoGroup:",Type=Type,Name=Name,LoadRadius=LoadRadius,NearRadius=NearRadius}) CARGO_GROUP:New(CargoGroup,Type,Name,LoadRadius,NearRadius) end end for CargoStaticName,CargoStatic in pairs(self.STATICS)do if self:IsCargo(CargoStaticName)then local CargoInfo=CargoStaticName:match("#CARGO(.*)") local CargoParam=CargoInfo and CargoInfo:match("%((.*)%)") local CargoName=CargoStaticName:match("(.*)#CARGO") local Type=CargoParam and CargoParam:match("T=([%a%d ]+),?") local Category=CargoParam and CargoParam:match("C=([%a%d ]+),?") local Name=CargoParam and CargoParam:match("N=([%a%d]+),?")or CargoName local LoadRadius=CargoParam and tonumber(CargoParam:match("RR=([%a%d]+),?")) local NearRadius=CargoParam and tonumber(CargoParam:match("NR=([%a%d]+),?")) if Category=="SLING"then self:I({"Register CargoSlingload:",Type=Type,Name=Name,LoadRadius=LoadRadius,NearRadius=NearRadius}) CARGO_SLINGLOAD:New(CargoStatic,Type,Name,LoadRadius,NearRadius) else if Category=="CRATE"then self:I({"Register CargoCrate:",Type=Type,Name=Name,LoadRadius=LoadRadius,NearRadius=NearRadius}) CARGO_CRATE:New(CargoStatic,Type,Name,LoadRadius,NearRadius) end end end end end end function DATABASE:FindClient(ClientName) local ClientFound=self.CLIENTS[ClientName] return ClientFound end function DATABASE:AddClient(ClientName,Force) local DCSUnitName=ClientName if type(DCSUnitName)=="number"then DCSUnitName=string.format("%d",ClientName)end if not self.CLIENTS[DCSUnitName]or Force==true then self.CLIENTS[DCSUnitName]=CLIENT:Register(DCSUnitName) end return self.CLIENTS[DCSUnitName] end function DATABASE:FindGroup(GroupName) local GroupFound=self.GROUPS[GroupName] if GroupFound==nil and GroupName~=nil and self.Templates.Groups[GroupName]==nil then self:_RegisterDynamicGroup(GroupName) return self.GROUPS[GroupName] end return GroupFound end function DATABASE:AddGroup(GroupName,force) if not self.GROUPS[GroupName]or force==true then self:T({"Add GROUP:",GroupName}) self.GROUPS[GroupName]=GROUP:Register(GroupName) end return self.GROUPS[GroupName] end function DATABASE:AddPlayer(UnitName,PlayerName) if type(UnitName)=="number"then UnitName=string.format("%d",UnitName)end if PlayerName then self:I({"Add player for unit:",UnitName,PlayerName}) self.PLAYERS[PlayerName]=UnitName self.PLAYERUNITS[PlayerName]=self:FindUnit(UnitName) self.PLAYERSJOINED[PlayerName]=PlayerName end end function DATABASE:_FindPlayerNameByUnitName(UnitName) if UnitName then for playername,unitname in pairs(self.PLAYERS)do if unitname==UnitName and self.PLAYERUNITS[playername]and self.PLAYERUNITS[playername]:IsAlive()then return playername,self.PLAYERUNITS[playername] end end end return nil end function DATABASE:DeletePlayer(UnitName,PlayerName) if PlayerName then self:T({"Clean player:",PlayerName}) self.PLAYERS[PlayerName]=nil self.PLAYERUNITS[PlayerName]=nil end end function DATABASE:GetPlayers() return self.PLAYERS end function DATABASE:GetPlayerUnits() return self.PLAYERUNITS end function DATABASE:GetPlayersJoined() return self.PLAYERSJOINED end function DATABASE:Spawn(SpawnTemplate) self:F(SpawnTemplate.name) self:T({SpawnTemplate.SpawnCountryID,SpawnTemplate.SpawnCategoryID}) local SpawnCoalitionID=SpawnTemplate.CoalitionID local SpawnCountryID=SpawnTemplate.CountryID local SpawnCategoryID=SpawnTemplate.CategoryID SpawnTemplate.CoalitionID=nil SpawnTemplate.CountryID=nil SpawnTemplate.CategoryID=nil self:_RegisterGroupTemplate(SpawnTemplate,SpawnCoalitionID,SpawnCategoryID,SpawnCountryID,SpawnTemplate.name) self:T3(SpawnTemplate) coalition.addGroup(SpawnCountryID,SpawnCategoryID,SpawnTemplate) SpawnTemplate.CoalitionID=SpawnCoalitionID SpawnTemplate.CountryID=SpawnCountryID SpawnTemplate.CategoryID=SpawnCategoryID local SpawnGroup=self:AddGroup(SpawnTemplate.name) for UnitID,UnitData in pairs(SpawnTemplate.units)do self:AddUnit(UnitData.name) end return SpawnGroup end function DATABASE:SetStatusGroup(GroupName,Status) self:F2(Status) self.Templates.Groups[GroupName].Status=Status end function DATABASE:GetStatusGroup(GroupName) self:F2(GroupName) if self.Templates.Groups[GroupName]then return self.Templates.Groups[GroupName].Status else return"" end end function DATABASE:_RegisterGroupTemplate(GroupTemplate,CoalitionSide,CategoryID,CountryID,GroupName) local GroupTemplateName=GroupName or env.getValueDictByKey(GroupTemplate.name) if not self.Templates.Groups[GroupTemplateName]then self.Templates.Groups[GroupTemplateName]={} self.Templates.Groups[GroupTemplateName].Status=nil end if GroupTemplate.route and GroupTemplate.route.spans then GroupTemplate.route.spans=nil end GroupTemplate.CategoryID=CategoryID GroupTemplate.CoalitionID=CoalitionSide GroupTemplate.CountryID=CountryID self.Templates.Groups[GroupTemplateName].GroupName=GroupTemplateName self.Templates.Groups[GroupTemplateName].Template=GroupTemplate self.Templates.Groups[GroupTemplateName].groupId=GroupTemplate.groupId self.Templates.Groups[GroupTemplateName].UnitCount=#GroupTemplate.units self.Templates.Groups[GroupTemplateName].Units=GroupTemplate.units self.Templates.Groups[GroupTemplateName].CategoryID=CategoryID self.Templates.Groups[GroupTemplateName].CoalitionID=CoalitionSide self.Templates.Groups[GroupTemplateName].CountryID=CountryID local UnitNames={} for unit_num,UnitTemplate in pairs(GroupTemplate.units)do UnitTemplate.name=env.getValueDictByKey(UnitTemplate.name) self.Templates.Units[UnitTemplate.name]={} self.Templates.Units[UnitTemplate.name].UnitName=UnitTemplate.name self.Templates.Units[UnitTemplate.name].Template=UnitTemplate self.Templates.Units[UnitTemplate.name].GroupName=GroupTemplateName self.Templates.Units[UnitTemplate.name].GroupTemplate=GroupTemplate self.Templates.Units[UnitTemplate.name].GroupId=GroupTemplate.groupId self.Templates.Units[UnitTemplate.name].CategoryID=CategoryID self.Templates.Units[UnitTemplate.name].CoalitionID=CoalitionSide self.Templates.Units[UnitTemplate.name].CountryID=CountryID if UnitTemplate.skill and(UnitTemplate.skill=="Client"or UnitTemplate.skill=="Player")then self.Templates.ClientsByName[UnitTemplate.name]=UnitTemplate self.Templates.ClientsByName[UnitTemplate.name].CategoryID=CategoryID self.Templates.ClientsByName[UnitTemplate.name].CoalitionID=CoalitionSide self.Templates.ClientsByName[UnitTemplate.name].CountryID=CountryID self.Templates.ClientsByID[UnitTemplate.unitId]=UnitTemplate end if UnitTemplate.AddPropAircraft then if UnitTemplate.AddPropAircraft.STN_L16 then local stn=UTILS.OctalToDecimal(UnitTemplate.AddPropAircraft.STN_L16) if stn==nil or stn<1 then self:E("WARNING: Invalid STN "..tostring(UnitTemplate.AddPropAircraft.STN_L16).." for "..UnitTemplate.name) else self.STNS[stn]=UnitTemplate.name self:I("Register STN "..tostring(UnitTemplate.AddPropAircraft.STN_L16).." for "..UnitTemplate.name) end end if UnitTemplate.AddPropAircraft.SADL_TN then local sadl=UTILS.OctalToDecimal(UnitTemplate.AddPropAircraft.SADL_TN) if sadl==nil or sadl<1 then self:E("WARNING: Invalid SADL "..tostring(UnitTemplate.AddPropAircraft.SADL_TN).." for "..UnitTemplate.name) else self.SADL[sadl]=UnitTemplate.name self:I("Register SADL "..tostring(UnitTemplate.AddPropAircraft.SADL_TN).." for "..UnitTemplate.name) end end end UnitNames[#UnitNames+1]=self.Templates.Units[UnitTemplate.name].UnitName end self:T({Group=self.Templates.Groups[GroupTemplateName].GroupName, Coalition=self.Templates.Groups[GroupTemplateName].CoalitionID, Category=self.Templates.Groups[GroupTemplateName].CategoryID, Country=self.Templates.Groups[GroupTemplateName].CountryID, Units=UnitNames } ) end function DATABASE:GetNextSTN(octal,unitname) local first=UTILS.OctalToDecimal(octal)or 0 if self.STNS[first]==unitname then return octal end local nextoctal=77777 local found=false if 32767-first<10 then first=0 end for i=first+1,32767 do if self.STNS[i]==nil then found=true nextoctal=UTILS.DecimalToOctal(i) self.STNS[i]=unitname self:T("Register STN "..tostring(nextoctal).." for "..unitname) break end end if not found then self:E(string.format("WARNING: No next free STN past %05d found!",octal)) local NewSTNS={} for _id,_name in pairs(self.STNS)do if self.UNITS[_name]~=nil then NewSTNS[_id]=_name end end self.STNS=nil self.STNS=NewSTNS end return nextoctal end function DATABASE:GetNextSADL(octal,unitname) local first=UTILS.OctalToDecimal(octal)or 0 if self.SADL[first]==unitname then return octal end local nextoctal=7777 local found=false if 4095-first<10 then first=0 end for i=first+1,4095 do if self.STNS[i]==nil then found=true nextoctal=UTILS.DecimalToOctal(i) self.SADL[i]=unitname self:T("Register SADL "..tostring(nextoctal).." for "..unitname) break end end if not found then self:E(string.format("WARNING: No next free SADL past %04d found!",octal)) local NewSTNS={} for _id,_name in pairs(self.SADL)do if self.UNITS[_name]~=nil then NewSTNS[_id]=_name end end self.SADL=nil self.SADL=NewSTNS end return nextoctal end function DATABASE:GetGroupTemplate(GroupName) local GroupTemplate=nil if self.Templates.Groups[GroupName]then GroupTemplate=self.Templates.Groups[GroupName].Template GroupTemplate.SpawnCoalitionID=self.Templates.Groups[GroupName].CoalitionID GroupTemplate.SpawnCategoryID=self.Templates.Groups[GroupName].CategoryID GroupTemplate.SpawnCountryID=self.Templates.Groups[GroupName].CountryID end return GroupTemplate end function DATABASE:_RegisterStaticTemplate(StaticTemplate,CoalitionID,CategoryID,CountryID) local StaticTemplate=UTILS.DeepCopy(StaticTemplate) local StaticTemplateGroupName=env.getValueDictByKey(StaticTemplate.name) local StaticTemplateName=StaticTemplate.units[1].name self.Templates.Statics[StaticTemplateName]=self.Templates.Statics[StaticTemplateName]or{} StaticTemplate.CategoryID=CategoryID StaticTemplate.CoalitionID=CoalitionID StaticTemplate.CountryID=CountryID self.Templates.Statics[StaticTemplateName].StaticName=StaticTemplateGroupName self.Templates.Statics[StaticTemplateName].GroupTemplate=StaticTemplate self.Templates.Statics[StaticTemplateName].UnitTemplate=StaticTemplate.units[1] self.Templates.Statics[StaticTemplateName].CategoryID=CategoryID self.Templates.Statics[StaticTemplateName].CoalitionID=CoalitionID self.Templates.Statics[StaticTemplateName].CountryID=CountryID self:T({Static=self.Templates.Statics[StaticTemplateName].StaticName, Coalition=self.Templates.Statics[StaticTemplateName].CoalitionID, Category=self.Templates.Statics[StaticTemplateName].CategoryID, Country=self.Templates.Statics[StaticTemplateName].CountryID } ) self:AddStatic(StaticTemplateName) return self end function DATABASE:_GetGenericStaticCargoGroupTemplate(Name,Typename,Mass,Coalition,Country) local StaticTemplate={} StaticTemplate.name=Name or"None" StaticTemplate.units={[1]={ name=Name, resourcePayload={ ["weapons"]={}, ["aircrafts"]={}, ["gasoline"]=0, ["diesel"]=0, ["methanol_mixture"]=0, ["jet_fuel"]=0, }, ["mass"]=Mass or 0, ["category"]="Cargos", ["canCargo"]=true, ["type"]=Typename or"container_cargo", ["rate"]=100, ["y"]=0, ["x"]=0, ["heading"]=0, }} StaticTemplate.CategoryID="static" StaticTemplate.CoalitionID=Coalition or coalition.side.BLUE StaticTemplate.CountryID=Country or country.id.GERMANY return StaticTemplate end function DATABASE:GetStaticGroupTemplate(StaticName) if self.Templates.Statics[StaticName]then local StaticTemplate=self.Templates.Statics[StaticName].GroupTemplate return StaticTemplate,self.Templates.Statics[StaticName].CoalitionID,self.Templates.Statics[StaticName].CategoryID,self.Templates.Statics[StaticName].CountryID else self:E("ERROR: Static group template does NOT exist for static "..tostring(StaticName)) return nil end end function DATABASE:GetStaticUnitTemplate(StaticName) if self.Templates.Statics[StaticName]then local UnitTemplate=self.Templates.Statics[StaticName].UnitTemplate return UnitTemplate,self.Templates.Statics[StaticName].CoalitionID,self.Templates.Statics[StaticName].CategoryID,self.Templates.Statics[StaticName].CountryID else self:E("ERROR: Static unit template does NOT exist for static "..tostring(StaticName)) return nil end end function DATABASE:GetGroupNameFromUnitName(UnitName) if self.Templates.Units[UnitName]then return self.Templates.Units[UnitName].GroupName else self:E("ERROR: Unit template does not exist for unit "..tostring(UnitName)) return nil end end function DATABASE:GetGroupTemplateFromUnitName(UnitName) if self.Templates.Units[UnitName]then return self.Templates.Units[UnitName].GroupTemplate else self:E("ERROR: Unit template does not exist for unit "..tostring(UnitName)) return nil end end function DATABASE:GetUnitTemplateFromUnitName(UnitName) if self.Templates.Units[UnitName]then return self.Templates.Units[UnitName] else self:E("ERROR: Unit template does not exist for unit "..tostring(UnitName)) return nil end end function DATABASE:GetCoalitionFromClientTemplate(ClientName) if self.Templates.ClientsByName[ClientName]then return self.Templates.ClientsByName[ClientName].CoalitionID end self:E("WARNING: Template does not exist for client "..tostring(ClientName)) return nil end function DATABASE:GetCategoryFromClientTemplate(ClientName) if self.Templates.ClientsByName[ClientName]then return self.Templates.ClientsByName[ClientName].CategoryID end self:E("WARNING: Template does not exist for client "..tostring(ClientName)) return nil end function DATABASE:GetCountryFromClientTemplate(ClientName) if self.Templates.ClientsByName[ClientName]then return self.Templates.ClientsByName[ClientName].CountryID end self:E("WARNING: Template does not exist for client "..tostring(ClientName)) return nil end function DATABASE:GetCoalitionFromAirbase(AirbaseName) return self.AIRBASES[AirbaseName]:GetCoalition() end function DATABASE:GetCategoryFromAirbase(AirbaseName) return self.AIRBASES[AirbaseName]:GetAirbaseCategory() end function DATABASE:_RegisterPlayers() local CoalitionsData={AlivePlayersRed=coalition.getPlayers(coalition.side.RED),AlivePlayersBlue=coalition.getPlayers(coalition.side.BLUE),AlivePlayersNeutral=coalition.getPlayers(coalition.side.NEUTRAL)} for CoalitionId,CoalitionData in pairs(CoalitionsData)do for UnitId,UnitData in pairs(CoalitionData)do self:T3({"UnitData:",UnitData}) if UnitData and UnitData:isExist()then local UnitName=UnitData:getName() local PlayerName=UnitData:getPlayerName() if not self.PLAYERS[PlayerName]then self:I({"Add player for unit:",UnitName,PlayerName}) self:AddPlayer(UnitName,PlayerName) end end end end return self end function DATABASE:_RegisterDynamicGroup(Groupname) local DCSGroup=Group.getByName(Groupname) if DCSGroup and DCSGroup:isExist()then local DCSGroupName=DCSGroup:getName() self:I(string.format("Register Group: %s",tostring(DCSGroupName))) self:AddGroup(DCSGroupName,true) for DCSUnitId,DCSUnit in pairs(DCSGroup:getUnits())do local DCSUnitName=DCSUnit:getName() self:I(string.format("Register Unit: %s",tostring(DCSUnitName))) self:AddUnit(tostring(DCSUnitName),true) end else self:E({"Group does not exist: ",DCSGroup}) end return self end function DATABASE:_RegisterGroupsAndUnits() local CoalitionsData={GroupsRed=coalition.getGroups(coalition.side.RED),GroupsBlue=coalition.getGroups(coalition.side.BLUE),GroupsNeutral=coalition.getGroups(coalition.side.NEUTRAL)} for CoalitionId,CoalitionData in pairs(CoalitionsData)do for DCSGroupId,DCSGroup in pairs(CoalitionData)do if DCSGroup:isExist()then local DCSGroupName=DCSGroup:getName() self:I(string.format("Register Group: %s",tostring(DCSGroupName))) self:AddGroup(DCSGroupName) for DCSUnitId,DCSUnit in pairs(DCSGroup:getUnits())do local DCSUnitName=DCSUnit:getName() self:I(string.format("Register Unit: %s",tostring(DCSUnitName))) self:AddUnit(DCSUnitName) end else self:E({"Group does not exist: ",DCSGroup}) end end end return self end function DATABASE:_RegisterClients() for ClientName,ClientTemplate in pairs(self.Templates.ClientsByName)do self:I(string.format("Register Client: %s",tostring(ClientName))) local client=self:AddClient(ClientName) client.SpawnCoord=COORDINATE:New(ClientTemplate.x,ClientTemplate.alt,ClientTemplate.y) end return self end function DATABASE:_RegisterStatics() local CoalitionsData={GroupsRed=coalition.getStaticObjects(coalition.side.RED),GroupsBlue=coalition.getStaticObjects(coalition.side.BLUE),GroupsNeutral=coalition.getStaticObjects(coalition.side.NEUTRAL)} for CoalitionId,CoalitionData in pairs(CoalitionsData)do for DCSStaticId,DCSStatic in pairs(CoalitionData)do if DCSStatic:isExist()then local DCSStaticName=DCSStatic:getName() self:I(string.format("Register Static: %s",tostring(DCSStaticName))) self:AddStatic(DCSStaticName) else self:E({"Static does not exist: ",DCSStatic}) end end end return self end function DATABASE:_RegisterAirbases() for DCSAirbaseId,DCSAirbase in pairs(world.getAirbases())do self:_RegisterAirbase(DCSAirbase) end return self end function DATABASE:_RegisterAirbase(airbase) local IsSyria=UTILS.GetDCSMap()=="Syria"and true or false local countHSyria=0 if airbase then local DCSAirbaseName=airbase:getName() if IsSyria and DCSAirbaseName=="H"and countHSyria>0 then return self elseif IsSyria and DCSAirbaseName=="H"and countHSyria==0 then countHSyria=countHSyria+1 end local airbaseID=airbase:getID() local airbase=self:AddAirbase(DCSAirbaseName) local airbaseUID=airbase:GetID(true) local typename=airbase:GetTypeName() local category=airbase.category if category==Airbase.Category.SHIP and typename=="FARP_SINGLE_01"then category=Airbase.Category.HELIPAD end local text=string.format("Register %s: %s (UID=%d), Runways=%d, Parking=%d [",AIRBASE.CategoryName[category],tostring(DCSAirbaseName),airbaseUID,#airbase.runways,airbase.NparkingTotal) for _,terminalType in pairs(AIRBASE.TerminalType)do if airbase.NparkingTerminal and airbase.NparkingTerminal[terminalType]then text=text..string.format("%d=%d ",terminalType,airbase.NparkingTerminal[terminalType]) end end text=text.."]" self:I(text) end return self end function DATABASE:_EventOnBirth(Event) self:T({Event}) if Event.IniDCSUnit then if Event.IniObjectCategory==Object.Category.STATIC then self:AddStatic(Event.IniDCSUnitName) elseif Event.IniObjectCategory==Object.Category.CARGO and string.match(Event.IniUnitName,".+|%d%d:%d%d|PKG%d+")then local cargo=self:AddDynamicCargo(Event.IniDCSUnitName) self:I(string.format("Adding dynamic cargo %s",tostring(Event.IniDCSUnitName))) self:CreateEventNewDynamicCargo(cargo) else if Event.IniObjectCategory==Object.Category.UNIT then self:AddUnit(Event.IniDCSUnitName) self:AddGroup(Event.IniDCSGroupName) local DCSAirbase=Airbase.getByName(Event.IniDCSUnitName) if DCSAirbase then self:I(string.format("Adding airbase %s",tostring(Event.IniDCSUnitName))) self:AddAirbase(Event.IniDCSUnitName) end end end if Event.IniObjectCategory==Object.Category.UNIT then Event.IniGroup=self:FindGroup(Event.IniDCSGroupName) Event.IniUnit=self:FindUnit(Event.IniDCSUnitName) local client=self.CLIENTS[Event.IniDCSUnitName] if client then end local PlayerName=Event.IniUnit:GetPlayerName() if PlayerName then self:I(string.format("Player '%s' joined unit '%s' of group '%s'",tostring(PlayerName),tostring(Event.IniDCSUnitName),tostring(Event.IniDCSGroupName))) if client==nil or(client and client:CountPlayers()==0)then client=self:AddClient(Event.IniDCSUnitName,true) end client:AddPlayer(PlayerName) if not self.PLAYERS[PlayerName]then self:AddPlayer(Event.IniUnitName,PlayerName) end local function SetPlayerSettings(self,PlayerName,IniUnit) local Settings=SETTINGS:Set(PlayerName) Settings:SetPlayerMenu(IniUnit) self:CreateEventPlayerEnterAircraft(IniUnit) end self:ScheduleOnce(1,SetPlayerSettings,self,PlayerName,Event.IniUnit) end end end end function DATABASE:_EventOnDeadOrCrash(Event) if Event.IniDCSUnit then local name=Event.IniDCSUnitName if Event.IniObjectCategory==3 then if self.STATICS[Event.IniDCSUnitName]then self:DeleteStatic(Event.IniDCSUnitName) end if self.UNITS[Event.IniDCSUnitName]then self:T("STATIC Event for UNIT "..tostring(Event.IniDCSUnitName)) local DCSUnit=_DATABASE:FindUnit(Event.IniDCSUnitName) self:T({DCSUnit}) if DCSUnit then return end end else if Event.IniObjectCategory==1 then if self.UNITS[Event.IniDCSUnitName]then self:ScheduleOnce(1,self.DeleteUnit,self,Event.IniDCSUnitName) end local client=self.CLIENTS[name] if client then client:RemovePlayers() end end end local airbase=self.AIRBASES[Event.IniDCSUnitName] if airbase and(airbase:IsHelipad()or airbase:IsShip())then self:DeleteAirbase(Event.IniDCSUnitName) end end self:AccountDestroys(Event) end function DATABASE:_EventOnPlayerEnterUnit(Event) self:F2({Event}) if Event.IniDCSUnit then if Event.IniObjectCategory==1 and Event.IniGroup and Event.IniGroup:IsGround()then local IsPlayer=Event.IniDCSUnit:getPlayerName() if IsPlayer then self:I(string.format("Player '%s' joined GROUND unit '%s' of group '%s'",tostring(Event.IniPlayerName),tostring(Event.IniDCSUnitName),tostring(Event.IniDCSGroupName))) local client=self.CLIENTS[Event.IniDCSUnitName] if not client then client=self:AddClient(Event.IniDCSUnitName) end client:AddPlayer(Event.IniPlayerName) if not self.PLAYERS[Event.IniPlayerName]then self:AddPlayer(Event.IniUnitName,Event.IniPlayerName) end local Settings=SETTINGS:Set(Event.IniPlayerName) Settings:SetPlayerMenu(Event.IniUnit) end end end end function DATABASE:_EventOnDynamicCargoRemoved(Event) self:T({Event}) if Event.IniDynamicCargoName then self:DeleteDynamicCargo(Event.IniDynamicCargoName) end end function DATABASE:_EventOnPlayerLeaveUnit(Event) self:F2({Event}) local function FindPlayerName(UnitName) local playername=nil for _name,_unitname in pairs(self.PLAYERS)do if _unitname==UnitName then playername=_name break end end return playername end if Event.IniUnit then if Event.IniObjectCategory==1 then local PlayerName=Event.IniPlayerName or Event.IniUnit:GetPlayerName()or FindPlayerName(Event.IniUnitName) if PlayerName then self:I(string.format("Player '%s' left unit %s",tostring(PlayerName),tostring(Event.IniUnitName))) local Settings=SETTINGS:Set(PlayerName) Settings:RemovePlayerMenu(Event.IniUnit) self:DeletePlayer(Event.IniUnit,PlayerName) local client=self.CLIENTS[Event.IniDCSUnitName] if client then client:RemovePlayer(PlayerName) end end end end end function DATABASE:ForEach(IteratorFunction,FinalizeFunction,arg,Set) self:F2(arg) local function CoRoutine() local Count=0 for ObjectID,Object in pairs(Set)do self:T2(Object) IteratorFunction(Object,unpack(arg)) Count=Count+1 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 if FinalizeFunction then FinalizeFunction(unpack(arg)) end return false end Schedule() return self end function DATABASE:ForEachStatic(IteratorFunction,FinalizeFunction,...) self:F2(arg) self:ForEach(IteratorFunction,FinalizeFunction,arg,self.STATICS) return self end function DATABASE:ForEachUnit(IteratorFunction,FinalizeFunction,...) self:F2(arg) self:ForEach(IteratorFunction,FinalizeFunction,arg,self.UNITS) return self end function DATABASE:ForEachGroup(IteratorFunction,FinalizeFunction,...) self:F2(arg) self:ForEach(IteratorFunction,FinalizeFunction,arg,self.GROUPS) return self end function DATABASE:ForEachPlayer(IteratorFunction,FinalizeFunction,...) self:F2(arg) self:ForEach(IteratorFunction,FinalizeFunction,arg,self.PLAYERS) return self end function DATABASE:ForEachPlayerJoined(IteratorFunction,FinalizeFunction,...) self:F2(arg) self:ForEach(IteratorFunction,FinalizeFunction,arg,self.PLAYERSJOINED) return self end function DATABASE:ForEachPlayerUnit(IteratorFunction,FinalizeFunction,...) self:F2(arg) self:ForEach(IteratorFunction,FinalizeFunction,arg,self.PLAYERUNITS) return self end function DATABASE:ForEachClient(IteratorFunction,FinalizeFunction,...) self:F2(arg) self:ForEach(IteratorFunction,FinalizeFunction,arg,self.CLIENTS) return self end function DATABASE:ForEachCargo(IteratorFunction,FinalizeFunction,...) self:F2(arg) self:ForEach(IteratorFunction,FinalizeFunction,arg,self.CARGOS) return self end function DATABASE:OnEventNewCargo(EventData) self:F2({EventData}) if EventData.Cargo then self:AddCargo(EventData.Cargo) end end function DATABASE:OnEventDeleteCargo(EventData) self:F2({EventData}) if EventData.Cargo then self:DeleteCargo(EventData.Cargo.Name) end end function DATABASE:OnEventNewZone(EventData) self:F2({EventData}) if EventData.Zone then self:AddZone(EventData.Zone.ZoneName,EventData.Zone) end end function DATABASE:OnEventDeleteZone(EventData) self:F2({EventData}) if EventData.Zone then self:DeleteZone(EventData.Zone.ZoneName) end end function DATABASE:GetPlayerSettings(PlayerName) self:F2({PlayerName}) return self.PLAYERSETTINGS[PlayerName] end function DATABASE:SetPlayerSettings(PlayerName,Settings) self:F2({PlayerName,Settings}) self.PLAYERSETTINGS[PlayerName]=Settings end function DATABASE:AddOpsGroup(opsgroup) self.FLIGHTGROUPS[opsgroup.groupname]=opsgroup end function DATABASE:GetOpsGroup(groupname) if type(groupname)=="string"then else groupname=groupname:GetName() end return self.FLIGHTGROUPS[groupname] end function DATABASE:FindOpsGroup(groupname) if type(groupname)=="string"then else groupname=groupname:GetName() end return self.FLIGHTGROUPS[groupname] end function DATABASE:FindOpsGroupFromUnit(unitname) local unit=nil local groupname if type(unitname)=="string"then unit=UNIT:FindByName(unitname) else unit=unitname end if unit then groupname=unit:GetGroup():GetName() end if groupname then return self.FLIGHTGROUPS[groupname] else return nil end end function DATABASE:AddFlightControl(flightcontrol) self:F2({flightcontrol}) self.FLIGHTCONTROLS[flightcontrol.airbasename]=flightcontrol end function DATABASE:GetFlightControl(airbasename) return self.FLIGHTCONTROLS[airbasename] end function DATABASE:_RegisterTemplates() self:F2() self.Navpoints={} self.UNITS={} for CoalitionName,coa_data in pairs(env.mission.coalition)do self:T({CoalitionName=CoalitionName}) if(CoalitionName=='red'or CoalitionName=='blue'or CoalitionName=='neutrals')and type(coa_data)=='table'then local CoalitionSide=coalition.side[string.upper(CoalitionName)] if CoalitionName=="red"then CoalitionSide=coalition.side.RED elseif CoalitionName=="blue"then CoalitionSide=coalition.side.BLUE else CoalitionSide=coalition.side.NEUTRAL end self.Navpoints[CoalitionName]={} if coa_data.nav_points then for nav_ind,nav_data in pairs(coa_data.nav_points)do if type(nav_data)=='table'then self.Navpoints[CoalitionName][nav_ind]=UTILS.DeepCopy(nav_data) self.Navpoints[CoalitionName][nav_ind]['name']=nav_data.callsignStr self.Navpoints[CoalitionName][nav_ind]['point']={} self.Navpoints[CoalitionName][nav_ind]['point']['x']=nav_data.x self.Navpoints[CoalitionName][nav_ind]['point']['y']=0 self.Navpoints[CoalitionName][nav_ind]['point']['z']=nav_data.y end end end if coa_data.country then for cntry_id,cntry_data in pairs(coa_data.country)do local CountryName=string.upper(cntry_data.name) local CountryID=cntry_data.id self.COUNTRY_ID[CountryName]=CountryID self.COUNTRY_NAME[CountryID]=CountryName if type(cntry_data)=='table'then 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"or obj_type_name=="static"then local CategoryName=obj_type_name 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,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, } 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:FilterFunction(ConditionFunction,...) local condition={} condition.func=ConditionFunction condition.arg={} if arg then condition.arg=arg end if not self.Filter.Functions then self.Filter.Functions={}end table.insert(self.Filter.Functions,condition) return self end function SET_BASE:_EvalFilterFunctions(Object) for _,_condition in pairs(self.Filter.Functions or{})do local condition=_condition if condition.func(Object,unpack(condition.arg))==false then return false end end return true end function SET_BASE:Clear(TriggerEvent) for Name,Object in pairs(self.Set)do self:Remove(Name,not TriggerEvent) end return self end function SET_BASE:_Find(ObjectName) local ObjectFound=self.Set[ObjectName] return ObjectFound end function SET_BASE:GetSet() return self.Set or{} end function SET_BASE:GetSetNames() local Names={} for Name,Object in pairs(self.Set)do table.insert(Names,Name) end return Names end function SET_BASE:GetSetObjects() local Objects={} for Name,Object in pairs(self.Set)do table.insert(Objects,Object) end return Objects end function SET_BASE:Remove(ObjectName,NoTriggerEvent) local TriggerEvent=true if NoTriggerEvent then TriggerEvent=false else TriggerEvent=true end 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 TriggerEvent then self:Removed(ObjectName,Object) end end end function SET_BASE:Add(ObjectName,Object) if not ObjectName or ObjectName==""then self:E("SET_BASE:Add - Invalid ObjectName handed") self:E({ObjectName=ObjectName,Object=Object}) return self end if self.Set[ObjectName]then self:Remove(ObjectName,true) end self.Set[ObjectName]=Object table.insert(self.Index,ObjectName) self:Added(ObjectName,Object) return self end function SET_BASE:AddObject(Object) self:Add(Object.ObjectName,Object) end function SET_BASE:SortByName() local function sort(a,b) return aThreatMax then ThreatMax=threat end end return ThreatMax end function SET_BASE:FilterOnce() for ObjectName,Object in pairs(self.Database)do if self:IsIncludeObject(Object)then self:Add(ObjectName,Object) else self:Remove(ObjectName,true) end end return self end function SET_BASE:FilterClear() for key,value in pairs(self.Filter)do self.Filter[key]={} 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(Coordinate) local NearestObject=nil local ClosestDistance=nil for ObjectID,ObjectData in pairs(self.Set)do if NearestObject==nil then NearestObject=ObjectData ClosestDistance=Coordinate:DistanceFromPointVec2(ObjectData:GetCoordinate()) else local Distance=Coordinate:DistanceFromPointVec2(ObjectData:GetCoordinate()) if Distance=Limit then break end end return true end local co=CoRoutine local function Schedule() local status,res=co() 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) return true end function SET_BASE:IsInSet(Object) local outcome=false local name=Object:GetName() self:ForEach( function(object) if object:GetName()==name then outcome=true end end ) return outcome end function SET_BASE:IsNotInSet(Object) return not self:IsInSet(Object) end function SET_BASE:GetObjectNames() local ObjectNames="" for ObjectName,Object in pairs(self.Set)do ObjectNames=ObjectNames..ObjectName..", " end return ObjectNames end function SET_BASE:Flush(MasterObject) local ObjectNames="" for ObjectName,Object in pairs(self.Set)do ObjectNames=ObjectNames..ObjectName..", " end return ObjectNames end end do SET_GROUP={ ClassName="SET_GROUP", Filter={ Coalitions=nil, Categories=nil, Countries=nil, GroupPrefixes=nil, Zones=nil, Functions=nil, Alive=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() 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() 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,DontSetCargoBayLimit) self:Add(group:GetName(),group) if not DontSetCargoBayLimit then for UnitID,UnitData in pairs(group:GetUnits()or{})do if UnitData and UnitData:IsAlive()then UnitData:SetCargoBayWeightLimit() end end 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(Coordinate) local NearestGroup=nil local ClosestDistance=nil local Set=self:GetAliveSet() for ObjectID,ObjectData in pairs(Set)do if NearestGroup==nil then NearestGroup=ObjectData ClosestDistance=Coordinate:DistanceFromPointVec2(ObjectData:GetCoordinate()) else local Distance=Coordinate:DistanceFromPointVec2(ObjectData:GetCoordinate()) if DistanceMaxThreatLevelA2G then MaxThreatLevelA2G=ThreatLevelA2G MaxThreatText=ThreatText end end return MaxThreatLevelA2G,MaxThreatText end function SET_UNIT:GetCoordinate() local function GetSetVec3(units) local x=0 local y=0 local z=0 local n=0 for _,unit in pairs(units)do local vec3=nil if unit and unit:IsAlive()then vec3=unit:GetVec3() end if vec3 then x=x+vec3.x y=y+vec3.y z=z+vec3.z n=n+1 end end if n>0 then local Vec3={x=x/n,y=y/n,z=z/n} return Vec3 end return nil end local Coordinate=nil local Vec3=GetSetVec3(self.Set) if Vec3 then Coordinate=COORDINATE:NewFromVec3(Vec3) end if Coordinate then local heading=self:GetHeading()or 0 local velocity=self:GetVelocity()or 0 Coordinate:SetHeading(heading) Coordinate:SetVelocity(velocity) end return Coordinate end function SET_UNIT:GetVelocity() local Coordinate=self:GetFirst():GetCoordinate() local MaxVelocity=0 for UnitName,UnitData in pairs(self:GetSet())do local Unit=UnitData local Coordinate=Unit:GetCoordinate() 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) 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 if HasSensors then RadarCount=RadarCount+1 end end return RadarCount end function SET_UNIT:HasSEAD() 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() if HasSEAD then SEADCount=SEADCount+1 end end end return SEADCount end function SET_UNIT:HasGroundUnits() 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() 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) 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) 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 and MUnitInclude then local MUnitCoalition=false for CoalitionID,CoalitionName in pairs(self.Filter.Coalitions)do 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 and MUnitInclude then local MUnitCategory=false for CategoryID,CategoryName in pairs(self.Filter.Categories)do 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 and MUnitInclude then local MUnitType=false for TypeID,TypeName in pairs(self.Filter.Types)do if TypeName==MUnit:GetTypeName()then MUnitType=true end end MUnitInclude=MUnitInclude and MUnitType end if self.Filter.Countries and MUnitInclude then local MUnitCountry=false for CountryID,CountryName in pairs(self.Filter.Countries)do if country.id[CountryName]==MUnit:GetCountry()then MUnitCountry=true end end MUnitInclude=MUnitInclude and MUnitCountry end if self.Filter.UnitPrefixes and MUnitInclude then local MUnitPrefix=false for UnitPrefixId,UnitPrefix in pairs(self.Filter.UnitPrefixes)do if string.find(MUnit:GetName(),UnitPrefix,1)then MUnitPrefix=true end end MUnitInclude=MUnitInclude and MUnitPrefix end if self.Filter.RadarTypes and MUnitInclude then local MUnitRadar=false for RadarTypeID,RadarType in pairs(self.Filter.RadarTypes)do if MUnit:HasSensors(Unit.SensorType.RADAR,RadarType)==true then if MUnit:GetRadar()==true then end MUnitRadar=true end end MUnitInclude=MUnitInclude and MUnitRadar end if self.Filter.SEAD and MUnitInclude then local MUnitSEAD=false if MUnit:HasSEAD()==true then MUnitSEAD=true end MUnitInclude=MUnitInclude and MUnitSEAD end end if self.Filter.Zones and MUnitInclude then local MGroupZone=false for ZoneName,Zone in pairs(self.Filter.Zones)do if MUnit:IsInZone(Zone)then MGroupZone=true end end MUnitInclude=MUnitInclude and MGroupZone end if self.Filter.Functions and MUnitInclude then local MUnitFunc=self:_EvalFilterFunctions(MUnit) MUnitInclude=MUnitInclude and MUnitFunc end 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, Zones=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:Add(AddStatic:GetName(),AddStatic) return self end function SET_STATIC:AddStaticsByName(AddStaticNames) local AddStaticNamesArray=(type(AddStaticNames)=="table")and AddStaticNames or{AddStaticNames} 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:FilterZones(Zones) if not self.Filter.Zones then self.Filter.Zones={} end local zones={} if Zones.ClassName and Zones.ClassName=="SET_ZONE"then zones=Zones.Set elseif type(Zones)~="table"or(type(Zones)=="table"and Zones.ClassName)then self:E("***** FilterZones needs either a table of ZONE Objects or a SET_ZONE as parameter!") return self else zones=Zones end for _,Zone in pairs(zones)do local zonename=Zone:GetName() self.Filter.Zones[zonename]=Zone 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.UnitLost,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) if Event.IniObjectCategory==Object.Category.STATIC then if not self.Database[Event.IniDCSUnitName]then self.Database[Event.IniDCSUnitName]=STATIC:Register(Event.IniDCSUnitName) end end return Event.IniDCSUnitName,self.Database[Event.IniDCSUnitName] end function SET_STATIC:FindInDatabase(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:ForEach(IteratorFunction,arg,self:GetSet()) return self end end function SET_STATIC:ForEachStatic(IteratorFunction,...) self:ForEach(IteratorFunction,arg,self:GetSet()) return self end function SET_STATIC:ForEachStaticCompletelyInZone(ZoneObject,IteratorFunction,...) 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: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() 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() 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 return MaxThreatLevelA2G,MaxThreatText end function SET_STATIC:IsIncludeObject(MStatic) local MStaticInclude=true if self.Filter.Coalitions then local MStaticCoalition=false for CoalitionID,CoalitionName in pairs(self.Filter.Coalitions)do 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 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 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 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 if string.find(MStatic:GetName(),StaticPrefix,1)then MStaticPrefix=true end end MStaticInclude=MStaticInclude and MStaticPrefix end if self.Filter.Zones then local MStaticZone=false for ZoneName,Zone in pairs(self.Filter.Zones)do if MStatic and MStatic:IsInZone(Zone)then MStaticZone=true end end MStaticInclude=MStaticInclude and MStaticZone end if self.Filter.Functions and MStaticInclude then local MClientFunc=self:_EvalFilterFunctions(MStatic) MStaticInclude=MStaticInclude and MClientFunc end 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 function SET_STATIC:GetClosestStatic(Coordinate,Coalitions) local Set=self:GetSet() local dmin=math.huge local gmin=nil for GroupID,GroupData in pairs(Set)do local group=GroupData if group and group:IsAlive()and(Coalitions==nil or UTILS.IsAnyInTable(Coalitions,group:GetCoalition()))then local coord=group:GetCoord() local d=UTILS.VecDist3D(Coordinate,coord) if d1 then x=x/count y=y/count z=z/count end local coord=COORDINATE:New(x,y,z) return coord end function SET_ZONE:IsIncludeObject(MZone) local MZoneInclude=true if MZone then local MZoneName=MZone:GetName() if self.Filter.Prefixes then local MZonePrefix=false for ZonePrefixId,ZonePrefix in pairs(self.Filter.Prefixes)do if string.find(MZoneName,ZonePrefix,1)then MZonePrefix=true end end MZoneInclude=MZoneInclude and MZonePrefix end end if self.Filter.Functions and MZoneInclude then local MClientFunc=self:_EvalFilterFunctions(MZone) MZoneInclude=MZoneInclude and MClientFunc end return MZoneInclude end function SET_ZONE:OnEventNewZone(EventData) if EventData.Zone then if EventData.Zone and self:IsIncludeObject(EventData.Zone)then self:Add(EventData.Zone.ZoneName,EventData.Zone) end end end function SET_ZONE:OnEventDeleteZone(EventData) if EventData.Zone then local Zone=_DATABASE:FindZone(EventData.Zone.ZoneName) if Zone and Zone.ZoneName then if Zone.NoDestroy then else self:Remove(Zone.ZoneName) end end end end function SET_ZONE:IsCoordinateInZone(Coordinate) for _,Zone in pairs(self:GetSet())do local Zone=Zone if Zone:IsCoordinateInZone(Coordinate)then return Zone end end return nil end function SET_ZONE:GetClosestZone(Coordinate) local dmin=math.huge local zmin=nil for _,Zone in pairs(self:GetSet())do local Zone=Zone local d=Zone:Get2DDistance(Coordinate) if dx2)and Coordinate.x or x2 y1=(Coordinate.yy2)and Coordinate.y or y2 z1=(Coordinate.yz2)and Coordinate.z or z2 end Coordinate.x=(x2-x1)/2+x1 Coordinate.y=(y2-y1)/2+y1 Coordinate.z=(z2-z1)/2+z1 return Coordinate end function SET_SCENERY:IsIncludeObject(MScenery) local MSceneryInclude=true if MScenery then local MSceneryName=MScenery:GetName() if self.Filter.Prefixes then local MSceneryPrefix=false for ZonePrefixId,ZonePrefix in pairs(self.Filter.Prefixes)do if string.find(MSceneryName,ZonePrefix,1)then MSceneryPrefix=true end end MSceneryInclude=MSceneryInclude and MSceneryPrefix end if self.Filter.Zones then local MSceneryZone=false for ZoneName,Zone in pairs(self.Filter.Zones)do local coord=MScenery:GetCoordinate() if coord and Zone:IsCoordinateInZone(coord)then MSceneryZone=true end end MSceneryInclude=MSceneryInclude and MSceneryZone end if self.Filter.SceneryRoles then local MSceneryRole=false local Role=MScenery:GetProperty("ROLE")or"none" for ZoneRoleId,ZoneRole in pairs(self.Filter.SceneryRoles)do if ZoneRole==Role then MSceneryRole=true end end MSceneryInclude=MSceneryInclude and MSceneryRole end end if self.Filter.Functions and MSceneryInclude then local MClientFunc=self:_EvalFilterFunctions(MScenery) MSceneryInclude=MSceneryInclude and MClientFunc end return MSceneryInclude end function SET_SCENERY:FilterOnce() for ObjectName,Object in pairs(self:GetSet())do if self:IsIncludeObject(Object)then self:Add(ObjectName,Object) else self:Remove(ObjectName,true) end end return self end function SET_SCENERY:GetLife0() local life0=0 self:ForEachScenery( function(obj) local Obj=obj life0=life0+Obj:GetLife0() end ) return life0 end function SET_SCENERY:GetLife() local life=0 self:ForEachScenery( function(obj) local Obj=obj life=life+Obj:GetLife() end ) return life end function SET_SCENERY:GetRelativeLife() local life=self:GetLife() local life0=self:GetLife0() local rlife=math.floor((life/life0)*100) return rlife end end do SET_DYNAMICCARGO={ ClassName="SET_DYNAMICCARGO", Filter={}, Set={}, List={}, Index={}, Database=nil, CallScheduler=nil, Filter={ Coalitions=nil, Types=nil, Countries=nil, StaticPrefixes=nil, Zones=nil, }, FilterMeta={ Coalitions={ red=coalition.side.RED, blue=coalition.side.BLUE, neutral=coalition.side.NEUTRAL, } }, ZoneTimerInterval=20, ZoneTimer=nil, } function SET_DYNAMICCARGO:New() local self=BASE:Inherit(self,SET_BASE:New(_DATABASE.DYNAMICCARGO)) return self end function SET_DYNAMICCARGO:IsIncludeObject(DCargo) local DCargoInclude=true if self.Filter.Coalitions then local DCargoCoalition=false for CoalitionID,CoalitionName in pairs(self.Filter.Coalitions)do if self.FilterMeta.Coalitions[CoalitionName]and self.FilterMeta.Coalitions[CoalitionName]==DCargo:GetCoalition()then DCargoCoalition=true end end DCargoInclude=DCargoInclude and DCargoCoalition end if self.Filter.Types then local DCargoType=false for TypeID,TypeName in pairs(self.Filter.Types)do if TypeName==DCargo:GetTypeName()then DCargoType=true end end DCargoInclude=DCargoInclude and DCargoType end if self.Filter.Countries then local DCargoCountry=false for CountryID,CountryName in pairs(self.Filter.Countries)do if country.id[CountryName]==DCargo:GetCountry()then DCargoCountry=true end end DCargoInclude=DCargoInclude and DCargoCountry end if self.Filter.StaticPrefixes then local DCargoPrefix=false for StaticPrefixId,StaticPrefix in pairs(self.Filter.StaticPrefixes)do if string.find(DCargo:GetName(),StaticPrefix,1)then DCargoPrefix=true end end DCargoInclude=DCargoInclude and DCargoPrefix end if self.Filter.Zones then local DCargoZone=false for ZoneName,Zone in pairs(self.Filter.Zones)do if DCargo and DCargo:IsInZone(Zone)then DCargoZone=true end end DCargoInclude=DCargoInclude and DCargoZone end if self.Filter.Functions and DCargoInclude then local MClientFunc=self:_EvalFilterFunctions(DCargo) DCargoInclude=DCargoInclude and MClientFunc end return DCargoInclude end function SET_DYNAMICCARGO: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_DYNAMICCARGO: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_DYNAMICCARGO: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_DYNAMICCARGO: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_DYNAMICCARGO:FilterNamePattern(Patterns) return self:FilterPrefixes(Patterns) end function SET_DYNAMICCARGO:FilterIsLoaded() self:FilterFunction( function(cargo) if cargo and cargo.CargoState and cargo.CargoState==DYNAMICCARGO.State.LOADED then return true else return false end end ) return self end function SET_DYNAMICCARGO:FilterIsUnloaded() self:FilterFunction( function(cargo) if cargo and cargo.CargoState and cargo.CargoState==DYNAMICCARGO.State.UNLOADED then return true else return false end end ) return self end function SET_DYNAMICCARGO:FilterIsNew() self:FilterFunction( function(cargo) if cargo and cargo.CargoState and cargo.CargoState==DYNAMICCARGO.State.NEW then return true else return false end end ) return self end function SET_DYNAMICCARGO:FilterCurrentOwner(PlayerName) self:FilterFunction( function(cargo) if cargo and cargo.Owner and string.find(cargo.Owner,PlayerName,1,true)then return true else return false end end ) return self end function SET_DYNAMICCARGO:FilterZones(Zones) if not self.Filter.Zones then self.Filter.Zones={} end local zones={} if Zones.ClassName and Zones.ClassName=="SET_ZONE"then zones=Zones.Set elseif type(Zones)~="table"or(type(Zones)=="table"and Zones.ClassName)then self:E("***** FilterZones needs either a table of ZONE Objects or a SET_ZONE as parameter!") return self else zones=Zones end for _,Zone in pairs(zones)do local zonename=Zone:GetName() self.Filter.Zones[zonename]=Zone end return self end function SET_DYNAMICCARGO:FilterStart() if _DATABASE then self:HandleEvent(EVENTS.NewDynamicCargo,self._EventHandlerDCAdd) self:HandleEvent(EVENTS.DynamicCargoRemoved,self._EventHandlerDCRemove) if self.Filter.Zones then self.ZoneTimer=TIMER:New(self._ContinousZoneFilter,self) local timing=self.ZoneTimerInterval or 30 self.ZoneTimer:Start(timing,timing) end self:_FilterStart() end return self end function SET_DYNAMICCARGO:FilterStop() if _DATABASE then self:UnHandleEvent(EVENTS.NewDynamicCargo) self:UnHandleEvent(EVENTS.DynamicCargoRemoved) if self.ZoneTimer and self.ZoneTimer:IsRunning()then self.ZoneTimer:Stop() end end return self end function SET_DYNAMICCARGO:_ContinousZoneFilter() local Database=_DATABASE.DYNAMICCARGO for ObjectName,Object in pairs(Database)do if self:IsIncludeObject(Object)and self:IsNotInSet(Object)then self:Add(ObjectName,Object) elseif(not self:IsIncludeObject(Object))and self:IsInSet(Object)then self:Remove(ObjectName) end end return self end function SET_DYNAMICCARGO:_EventHandlerDCAdd(Event) if Event.IniDynamicCargo and Event.IniDynamicCargoName then if not _DATABASE.DYNAMICCARGO[Event.IniDynamicCargoName]then _DATABASE:AddDynamicCargo(Event.IniDynamicCargoName) end local ObjectName,Object=self:FindInDatabase(Event) if Object and self:IsIncludeObject(Object)then self:Add(ObjectName,Object) end end return self end function SET_DYNAMICCARGO:_EventHandlerDCRemove(Event) if Event.IniDCSUnitName then local ObjectName,Object=self:FindInDatabase(Event) if ObjectName then self:Remove(ObjectName) end end return self end function SET_DYNAMICCARGO:FindInDatabase(Event) return Event.IniDCSUnitName,self.Set[Event.IniDCSUnitName] end function SET_DYNAMICCARGO:FilterZoneTimer(Seconds) self.ZoneTimerInterval=Seconds or 30 return self end function SET_DYNAMICCARGO:FilterDeads() return self end function SET_DYNAMICCARGO:FilterCrashes() return self end function SET_DYNAMICCARGO:GetOwnerNames() local owners={} self:ForEach( function(cargo) if cargo and cargo.Owner then table.insert(owners,cargo.Owner,cargo.Owner) end end ) return owners end function SET_DYNAMICCARGO:GetStorageObjects() local owners={} self:ForEach( function(cargo) if cargo and cargo.warehouse then table.insert(owners,cargo.StaticName,cargo.warehouse) end end ) return owners end function SET_DYNAMICCARGO:GetOwnerClientObjects() local owners={} self:ForEach( function(cargo) if cargo and cargo.Owner then local client=CLIENT:FindByPlayerName(cargo.Owner) if client then table.insert(owners,cargo.Owner,client) end end end ) return owners end end do COORDINATE={ ClassName="COORDINATE", } COORDINATE.WaypointAltType={ BARO="BARO", RADIO="RADIO", } COORDINATE.WaypointAction={ TurningPoint="Turning Point", FlyoverPoint="Fly Over Point", FromParkingArea="From Parking Area", FromParkingAreaHot="From Parking Area Hot", FromGroundAreaHot="From Ground Area Hot", FromGroundArea="From Ground Area", FromRunway="From Runway", Landing="Landing", LandingReFuAr="LandingReFuAr", } COORDINATE.WaypointType={ TakeOffParking="TakeOffParking", TakeOffParkingHot="TakeOffParkingHot", TakeOff="TakeOffParkingHot", TakeOffGroundHot="TakeOffGroundHot", TakeOffGround="TakeOffGround", TurningPoint="Turning Point", Land="Land", LandingReFuAr="LandingReFuAr", } function COORDINATE:New(x,y,z) local self=BASE:Inherit(self,BASE:New()) self.x=x self.y=y self.z=z return self end function COORDINATE:NewFromCoordinate(Coordinate) local self=BASE:Inherit(self,BASE:New()) self.x=Coordinate.x self.y=Coordinate.y self.z=Coordinate.z return self end function COORDINATE:NewFromVec2(Vec2,LandHeightAdd) local LandHeight=land.getHeight(Vec2) LandHeightAdd=LandHeightAdd or 0 LandHeight=LandHeight+LandHeightAdd local self=self:New(Vec2.x,LandHeight,Vec2.y) return self end function COORDINATE:NewFromVec3(Vec3) local self=self:New(Vec3.x,Vec3.y,Vec3.z) self:F2(self) return self end function COORDINATE:NewFromWaypoint(Waypoint) local self=self:New(Waypoint.x,Waypoint.alt,Waypoint.y) return self end function COORDINATE:GetCoordinate() return self end function COORDINATE:GetVec3() return{x=self.x,y=self.y,z=self.z} end function COORDINATE:GetVec2() return{x=self.x,y=self.z} end function COORDINATE:UpdateFromVec3(Vec3) self.x=Vec3.x self.y=Vec3.y self.z=Vec3.z return self end function COORDINATE:UpdateFromCoordinate(Coordinate) self.x=Coordinate.x self.y=Coordinate.y self.z=Coordinate.z return self end function COORDINATE:UpdateFromVec2(Vec2) self.x=Vec2.x self.z=Vec2.y return self end function COORDINATE:GetMagneticDeclination(Month,Year) local decl=UTILS.GetMagneticDeclination() if require then local magvar=require('magvar') if magvar then local date,year,month,day=UTILS.GetDCSMissionDate() magvar.init(Month or month,Year or year) local lat,lon=self:GetLLDDM() decl=magvar.get_mag_decl(lat,lon) if decl then decl=math.deg(decl) end end else self:T("The require package is not available. Using constant value for magnetic declination") end return decl end function COORDINATE:NewFromLLDD(latitude,longitude,altitude) local vec3=coord.LLtoLO(latitude,longitude) local _coord=self:NewFromVec3(vec3) if altitude==nil then _coord.y=self:GetLandHeight() else _coord.y=altitude end return _coord end function COORDINATE:IsAtCoordinate2D(Coordinate,Precision) self:F({Coordinate=Coordinate:GetVec2()}) self:F({self=self:GetVec2()}) local x=Coordinate.x local z=Coordinate.z return x-Precision<=self.x and x+Precision>=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=Object.getCategory(ZoneObject) 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:ScanStatics(radius) local _,_,_,_,statics=self:ScanObjects(radius,false,true,false) local set=SET_STATIC:New() for _,stat in pairs(statics)do set:AddStatic(STATIC:Find(stat)) end return set end function COORDINATE:FindClosestStatic(radius) local units=self:ScanStatics(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}) local coord=COORDINATE:NewFromVec2(self:GetRandomVec2InRadius(OuterRadius,InnerRadius)) return coord 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) if TargetCoordinate then return{x=TargetCoordinate.x-self.x,y=TargetCoordinate.y-self.y,z=TargetCoordinate.z-self.z} else return{x=0,y=0,z=0} end 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) if f>1 then local norm=UTILS.VecNorm(vec) f=Fraction/norm end 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) if not TargetCoordinate then return 1000000 end local a=self:GetVec2() if not TargetCoordinate.ClassName then TargetCoordinate=COORDINATE:NewFromVec3(TargetCoordinate) end local b=TargetCoordinate:GetVec2() local norm=UTILS.VecDist2D(a,b) 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.CelsiusToFahrenheit(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:GetWindVec3(height,turbulence) local landheight=self:GetLandHeight()+0.1 local point={x=self.x,y=math.max(height or self.y,landheight),z=self.z} local wind=nil if turbulence then wind=atmosphere.getWindWithTurbulence(point) else wind=atmosphere.getWind(point) end return wind end function COORDINATE:GetWind(height,turbulence) local wind=self:GetWindVec3(height,turbulence) local direction=UTILS.VecHdg(wind) if direction>180 then direction=direction-180 else direction=direction+180 end local strength=UTILS.VecNorm(wind) 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:GetX() return self.x end function COORDINATE:GetY() if self:IsInstanceOf("POINT_VEC2")then return self.z end return self.y end function COORDINATE:GetZ() return self.z end function COORDINATE:SetX(x) self.x=x return self end function COORDINATE:SetY(y) if self:IsInstanceOf("POINT_VEC2")then self.z=y else self.y=y end return self end function COORDINATE:SetZ(z) self.z=z return self end function COORDINATE:AddX(x) self.x=self.x+x return self end function COORDINATE:GetLat() return self.x end function COORDINATE:SetLat(x) self.x=x return self end function COORDINATE:GetLon() return self.z end function COORDINATE:SetLon(z) self.z=z return self end function COORDINATE:GetAlt() return self.y~=0 or land.getHeight({x=self.x,y=self.z}) end function COORDINATE:SetAlt(Altitude) self.y=Altitude or land.getHeight({x=self.x,y=self.z}) return self end function COORDINATE:AddAlt(Altitude) self.y=land.getHeight({x=self.x,y=self.z})+Altitude or 0 return self end function COORDINATE:GetRandomPointVec2InRadius(OuterRadius,InnerRadius) self:F2({OuterRadius,InnerRadius}) return COORDINATE:NewFromVec2(self:GetRandomVec2InRadius(OuterRadius,InnerRadius)) end function COORDINATE:AddY(y) if self:IsInstanceOf("POINT_VEC2")then return self:AddZ(y) else self.y=self.y+y end return self end function COORDINATE:AddZ(z) self.z=self.z+z return self 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={x=TargetCoordinate.x,y=TargetCoordinate.y,z=TargetCoordinate.z} local SourceVec3=self:GetVec3() local dist=UTILS.VecDist3D(TargetVec3,SourceVec3) return dist end function COORDINATE:GetBearingText(AngleRadians,Precision,Settings,MagVar) local Settings=Settings or _SETTINGS local AngleDegrees=UTILS.Round(UTILS.ToDegree(AngleRadians),Precision) local s=string.format('%03d°',AngleDegrees) if MagVar then local variation=self:GetMagneticDeclination()or 0 local AngleMagnetic=AngleDegrees-variation if AngleMagnetic<0 then AngleMagnetic=360-AngleMagnetic end s=string.format('%03d°M|%03d°',AngleMagnetic,AngleDegrees) end return s end function COORDINATE:GetDistanceText(Distance,Settings,Language,Precision) local Settings=Settings or _SETTINGS local Language=Language or Settings.Locale or _SETTINGS.Locale or"EN" Language=string.lower(Language) local Precision=Precision or 0 local DistanceText if Settings:IsMetric()then if Language=="en"then DistanceText=" for "..UTILS.Round(Distance/1000,Precision).." km" elseif Language=="ru"then DistanceText=" за "..UTILS.Round(Distance/1000,Precision).." километров" end else if Language=="en"then DistanceText=" for "..UTILS.Round(UTILS.MetersToNM(Distance),Precision).." miles" elseif Language=="ru"then DistanceText=" за "..UTILS.Round(UTILS.MetersToNM(Distance),Precision).." миль" end end return DistanceText end function COORDINATE:GetAltitudeText(Settings,Language) local Altitude=self.y local Settings=Settings or _SETTINGS local Language=Language or Settings.Locale or _SETTINGS.Locale or"EN" Language=string.lower(Language) 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,MagVar,Precision) local Settings=Settings or _SETTINGS Precision=Precision or 0 local BearingText=self:GetBearingText(AngleRadians,0,Settings,MagVar) local DistanceText=self:GetDistanceText(Distance,Settings,Language,Precision) local BRText=BearingText..DistanceText return BRText end function COORDINATE:GetBRAText(AngleRadians,Distance,Settings,Language,MagVar) local Settings=Settings or _SETTINGS local BearingText=self:GetBearingText(AngleRadians,0,Settings,MagVar) local DistanceText=self:GetDistanceText(Distance,Settings,Language,0) 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:SetAtLandheight() local alt=self:GetLandHeight() 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=false 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=false 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=false 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:GetClosestAirbase(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) 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,Delay) Power=Power or 1000 if Delay and Delay>0 then self:ScheduleOnce(Delay,self.IlluminationBomb,self,Power) else trigger.action.illuminationBomb(self:GetVec3(),Power) end return self end function COORDINATE:Smoke(SmokeColor,name) self:F2({SmokeColor}) self.firename=name or"Smoke-"..math.random(1,100000) trigger.action.smoke(self:GetVec3(),SmokeColor,self.firename) end function COORDINATE:StopSmoke(name) self:StopBigSmokeAndFire(name) 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,name) self:F2({preset=preset,density=density}) density=density or 0.5 self.firename=name or"Fire-"..math.random(1,10000) trigger.action.effectSmokeBig(self:GetVec3(),preset,density,self.firename) end function COORDINATE:StopBigSmokeAndFire(name) name=name or self.firename trigger.action.effectSmokeStop(name) end function COORDINATE:BigSmokeAndFireSmall(density,name) self:F2({density=density}) density=density or 0.5 self:BigSmokeAndFire(BIGSMOKEPRESET.SmallSmokeAndFire,density,name) end function COORDINATE:BigSmokeAndFireMedium(density,name) self:F2({density=density}) density=density or 0.5 self:BigSmokeAndFire(BIGSMOKEPRESET.MediumSmokeAndFire,density,name) end function COORDINATE:BigSmokeAndFireLarge(density,name) self:F2({density=density}) density=density or 0.5 self:BigSmokeAndFire(BIGSMOKEPRESET.LargeSmokeAndFire,density,name) end function COORDINATE:BigSmokeAndFireHuge(density,name) self:F2({density=density}) density=density or 0.5 self:BigSmokeAndFire(BIGSMOKEPRESET.HugeSmokeAndFire,density,name) end function COORDINATE:BigSmokeSmall(density,name) self:F2({density=density}) density=density or 0.5 self:BigSmokeAndFire(BIGSMOKEPRESET.SmallSmoke,density,name) end function COORDINATE:BigSmokeMedium(density,name) self:F2({density=density}) density=density or 0.5 self:BigSmokeAndFire(BIGSMOKEPRESET.MediumSmoke,density,name) end function COORDINATE:BigSmokeLarge(density,name) self:F2({density=density}) density=density or 0.5 self:BigSmokeAndFire(BIGSMOKEPRESET.LargeSmoke,density,name) end function COORDINATE:BigSmokeHuge(density,name) self:F2({density=density}) density=density or 0.5 self:BigSmokeAndFire(BIGSMOKEPRESET.HugeSmoke,density,name) 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,Color,Alpha,LineType,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,Color,Alpha,FillColor,FillAlpha,LineType,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 UTILS.DeepCopy(Color) FillColor[4]=FillAlpha or 0.15 trigger.action.circleToAll(Coalition,MarkID,vec3,Radius,Color,FillColor,LineType,ReadOnly,Text or"") return MarkID end end function COORDINATE:RectToAll(Endpoint,Coalition,Color,Alpha,FillColor,FillAlpha,LineType,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 FillColor=FillColor or UTILS.DeepCopy(Color) FillColor[4]=FillAlpha or 0.15 trigger.action.rectToAll(Coalition,MarkID,self:GetVec3(),vec3,Color,FillColor,LineType,ReadOnly,Text or"") return MarkID end function COORDINATE:QuadToAll(Coord2,Coord3,Coord4,Coalition,Color,Alpha,FillColor,FillAlpha,LineType,ReadOnly,Text) local MarkID=UTILS.GetMarkID() if ReadOnly==nil then ReadOnly=false end local point1=self:GetVec3() local point2=Coord2:GetVec3() local point3=Coord3:GetVec3() local point4=Coord4:GetVec3() Coalition=Coalition or-1 Color=Color or{1,0,0} Color[4]=Alpha or 1.0 LineType=LineType or 1 FillColor=FillColor or UTILS.DeepCopy(Color) FillColor[4]=FillAlpha or 0.15 trigger.action.quadToAll(Coalition,MarkID,point1,point2,point3,point4,Color,FillColor,LineType,ReadOnly,Text or"") return MarkID end function COORDINATE:MarkupToAllFreeForm(Coordinates,Coalition,Color,Alpha,FillColor,FillAlpha,LineType,ReadOnly,Text) local MarkID=UTILS.GetMarkID() if ReadOnly==nil then ReadOnly=false end Coalition=Coalition or-1 Color=Color or{1,0,0} Color[4]=Alpha or 1.0 LineType=LineType or 1 FillColor=FillColor or UTILS.DeepCopy(Color) FillColor[4]=FillAlpha or 0.15 local vecs={} vecs[1]=self:GetVec3() for i,coord in ipairs(Coordinates)do vecs[i+1]=coord:GetVec3() end if#vecs<3 then self:E("ERROR: A free form polygon needs at least three points!") elseif#vecs==3 then trigger.action.markupToAll(7,Coalition,MarkID,vecs[1],vecs[2],vecs[3],Color,FillColor,LineType,ReadOnly,Text or"") elseif#vecs==4 then trigger.action.markupToAll(7,Coalition,MarkID,vecs[1],vecs[2],vecs[3],vecs[4],Color,FillColor,LineType,ReadOnly,Text or"") elseif#vecs==5 then trigger.action.markupToAll(7,Coalition,MarkID,vecs[1],vecs[2],vecs[3],vecs[4],vecs[5],Color,FillColor,LineType,ReadOnly,Text or"") elseif#vecs==6 then trigger.action.markupToAll(7,Coalition,MarkID,vecs[1],vecs[2],vecs[3],vecs[4],vecs[5],vecs[6],Color,FillColor,LineType,ReadOnly,Text or"") elseif#vecs==7 then trigger.action.markupToAll(7,Coalition,MarkID,vecs[1],vecs[2],vecs[3],vecs[4],vecs[5],vecs[6],vecs[7],Color,FillColor,LineType,ReadOnly,Text or"") elseif#vecs==8 then trigger.action.markupToAll(7,Coalition,MarkID,vecs[1],vecs[2],vecs[3],vecs[4],vecs[5],vecs[6],vecs[7],vecs[8],Color,FillColor,LineType,ReadOnly,Text or"") elseif#vecs==9 then trigger.action.markupToAll(7,Coalition,MarkID,vecs[1],vecs[2],vecs[3],vecs[4],vecs[5],vecs[6],vecs[7],vecs[8],vecs[9],Color,FillColor,LineType,ReadOnly,Text or"") elseif#vecs==10 then trigger.action.markupToAll(7,Coalition,MarkID,vecs[1],vecs[2],vecs[3],vecs[4],vecs[5],vecs[6],vecs[7],vecs[8],vecs[9],vecs[10],Color,FillColor,LineType,ReadOnly,Text or"") elseif#vecs==11 then trigger.action.markupToAll(7,Coalition,MarkID,vecs[1],vecs[2],vecs[3],vecs[4],vecs[5],vecs[6],vecs[7],vecs[8],vecs[9],vecs[10], vecs[11], Color,FillColor,LineType,ReadOnly,Text or"") elseif#vecs==12 then trigger.action.markupToAll(7,Coalition,MarkID,vecs[1],vecs[2],vecs[3],vecs[4],vecs[5],vecs[6],vecs[7],vecs[8],vecs[9],vecs[10], vecs[11],vecs[12], Color,FillColor,LineType,ReadOnly,Text or"") elseif#vecs==13 then trigger.action.markupToAll(7,Coalition,MarkID,vecs[1],vecs[2],vecs[3],vecs[4],vecs[5],vecs[6],vecs[7],vecs[8],vecs[9],vecs[10], vecs[11],vecs[12],vecs[13], Color,FillColor,LineType,ReadOnly,Text or"") elseif#vecs==14 then trigger.action.markupToAll(7,Coalition,MarkID,vecs[1],vecs[2],vecs[3],vecs[4],vecs[5],vecs[6],vecs[7],vecs[8],vecs[9],vecs[10], vecs[11],vecs[12],vecs[13],vecs[14], Color,FillColor,LineType,ReadOnly,Text or"") elseif#vecs==15 then trigger.action.markupToAll(7,Coalition,MarkID,vecs[1],vecs[2],vecs[3],vecs[4],vecs[5],vecs[6],vecs[7],vecs[8],vecs[9],vecs[10], vecs[11],vecs[12],vecs[13],vecs[14],vecs[15], Color,FillColor,LineType,ReadOnly,Text or"") else local s=string.format("trigger.action.markupToAll(7, %d, %d,",Coalition,MarkID) for _,vec in pairs(vecs)do s=s..string.format("{x=%.1f, y=%.1f, z=%.1f},",vec.x,vec.y,vec.z) end s=s..string.format("{%.3f, %.3f, %.3f, %.3f},",Color[1],Color[2],Color[3],Color[4]) s=s..string.format("{%.3f, %.3f, %.3f, %.3f},",FillColor[1],FillColor[2],FillColor[3],FillColor[4]) s=s..string.format("%d,",LineType or 1) s=s..string.format("%s",tostring(ReadOnly)) if Text and type(Text)=="string"and string.len(Text)>0 then s=s..string.format(", \"%s\"",tostring(Text)) end s=s..")" local success=UTILS.DoString(s) if not success then self:E("ERROR: Could not draw polygon") env.info(s) end end return MarkID end function COORDINATE:TextToAll(Text,Coalition,Color,Alpha,FillColor,FillAlpha,FontSize,ReadOnly) local MarkID=UTILS.GetMarkID() if ReadOnly==nil then ReadOnly=false end Coalition=Coalition or-1 Color=Color or{1,0,0} Color[4]=Alpha or 1.0 FillColor=FillColor or UTILS.DeepCopy(Color) FillColor[4]=FillAlpha or 0.3 FontSize=FontSize or 14 trigger.action.textToAll(Coalition,MarkID,self:GetVec3(),Color,FillColor,FontSize,ReadOnly,Text or"Hello World") return MarkID end function COORDINATE:ArrowToAll(Endpoint,Coalition,Color,Alpha,FillColor,FillAlpha,LineType,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 FillColor=FillColor or UTILS.DeepCopy(Color) FillColor[4]=FillAlpha or 0.15 trigger.action.arrowToAll(Coalition,MarkID,vec3,self:GetVec3(),Color,FillColor,LineType,ReadOnly,Text or"") return MarkID 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 or type(sunrise)=="string"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) if sunrise=="N/R"then return false end if sunrise=="N/S"then return true end 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 or type(sunrise)=="string"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,MagVar,Precision) local DirectionVec3=FromCoordinate:GetDirectionVec3(self) local AngleRadians=self:GetAngleRadians(DirectionVec3) local Distance=self:Get2DDistance(FromCoordinate) return"BR, "..self:GetBRText(AngleRadians,Distance,Settings,nil,MagVar,Precision) end function COORDINATE:ToStringBRA(FromCoordinate,Settings,MagVar) 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,nil,MagVar) end function COORDINATE:ToStringBRAANATO(FromCoordinate,Bogey,Spades,SSML,Angels,Zeros) local BRAANATO="Merged." local currentCoord=FromCoordinate local DirectionVec3=FromCoordinate:GetDirectionVec3(self) local AngleRadians=self:GetAngleRadians(DirectionVec3) local bearing=UTILS.Round(UTILS.ToDegree(AngleRadians),0) local magnetic=self:GetMagneticDeclination()or 0 bearing=bearing-magnetic local rangeMetres=self:Get2DDistance(currentCoord) local rangeNM=UTILS.Round(UTILS.MetersToNM(rangeMetres),0) local aspect=self:ToStringAspect(currentCoord) local alt=UTILS.Round(UTILS.MetersToFeet(self.y)/1000,0) local alttext=string.format("%d thousand",alt) if Angels then alttext=string.format("Angels %d",alt) end if alt<1 then alttext="very low" end local track="Maneuver" if self.Heading then track=UTILS.BearingToCardinal(self.Heading)or"North" end if rangeNM>3 then if SSML then if Zeros then bearing=string.format("%03d",bearing) local AngleDegText=string.gsub(bearing,"%d","%1 ") AngleDegText=string.gsub(AngleDegText," $","") AngleDegText=string.gsub(AngleDegText,"0","zero") if aspect==""then BRAANATO=string.format("brah %s, %d miles, %s, Track %s",AngleDegText,rangeNM,alttext,track) else BRAANATO=string.format("brah %s, %d miles, %s, %s, Track %s",AngleDegText,rangeNM,alttext,aspect,track) end else if aspect==""then BRAANATO=string.format("brah %03d, %d miles, %s, Track %s",bearing,rangeNM,alttext,track) else BRAANATO=string.format("brah %03d, %d miles, %s, %s, Track %s",bearing,rangeNM,alttext,aspect,track) end end if Bogey and Spades then BRAANATO=BRAANATO..", Bogey, Spades." elseif Bogey then BRAANATO=BRAANATO..", Bogey." elseif Spades then BRAANATO=BRAANATO..", Spades." else BRAANATO=BRAANATO.."." end else if aspect==""then BRAANATO=string.format("BRA %03d, %d miles, %s, Track %s",bearing,rangeNM,alttext,track) else BRAANATO=string.format("BRAA %03d, %d miles, %s, %s, Track %s",bearing,rangeNM,alttext,aspect,track) end if Bogey and Spades then BRAANATO=BRAANATO..", Bogey, Spades." elseif Bogey then BRAANATO=BRAANATO..", Bogey." elseif Spades then BRAANATO=BRAANATO..", Spades." else BRAANATO=BRAANATO.."." end end end return BRAANATO end function COORDINATE.GetBullseyeCoordinate(Coalition) return COORDINATE:NewFromVec3(coalition.getMainRefPoint(Coalition)) end function COORDINATE:ToStringBULLS(Coalition,Settings,MagVar) 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,nil,MagVar) 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:ToStringLL(Settings) local LL_Accuracy=Settings and Settings.LL_Accuracy or _SETTINGS.LL_Accuracy local lat,lon=coord.LOtoLL(self:GetVec3()) return string.format('%f',lat)..' '..string.format('%f',lon) 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:NewFromMGRSString(MGRSString) local myparts=UTILS.Split(MGRSString," ") local northing=tostring(myparts[5])or"" local easting=tostring(myparts[4])or"" if string.len(easting)<5 then easting=easting..string.rep("0",5-string.len(easting))end if string.len(northing)<5 then northing=northing..string.rep("0",5-string.len(northing))end local MGRS={ UTMZone=myparts[2], MGRSDigraph=myparts[3], Easting=easting, Northing=northing, } local lat,lon=coord.MGRStoLL(MGRS) local point=coord.LLtoLO(lat,lon,0) local coord=COORDINATE:NewFromVec2({x=point.x,y=point.z}) return coord end function COORDINATE:NewFromMGRS(UTMZone,MGRSDigraph,Easting,Northing) if string.len(Easting)<5 then Easting=tostring(Easting..string.rep("0",5-string.len(Easting)))end if string.len(Northing)<5 then Northing=tostring(Northing..string.rep("0",5-string.len(Northing)))end local MGRS={ UTMZone=UTMZone, MGRSDigraph=MGRSDigraph, Easting=tostring(Easting), Northing=tostring(Northing), } local lat,lon=coord.MGRStoLL(MGRS) local point=coord.LLtoLO(lat,lon,0) local coord=COORDINATE:NewFromVec2({x=point.x,y=point.z}) return coord end function COORDINATE:ToStringFromRP(ReferenceCoord,ReferenceName,Controllable,Settings,MagVar) 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,nil,MagVar).." 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,nil,MagVar).." from "..ReferenceName end return nil end function COORDINATE:ToStringFromRPShort(ReferenceCoord,ReferenceName,Controllable,Settings,MagVar) 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 self:GetBRText(AngleRadians,Distance,Settings,nil,MagVar).." from "..ReferenceName else local DirectionVec3=ReferenceCoord:GetDirectionVec3(self) local AngleRadians=self:GetAngleRadians(DirectionVec3) local Distance=self:Get2DDistance(ReferenceCoord) return self:GetBRText(AngleRadians,Distance,Settings,nil,MagVar).." from "..ReferenceName end return nil end function COORDINATE:ToStringA2G(Controllable,Settings,MagVar) 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,MagVar)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,MagVar) 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,MagVar) else return self:ToStringMGRS(Settings) end end if Settings:IsA2A_BULLS()then local Coalition=Controllable:GetCoalition() return self:ToStringBULLS(Coalition,Settings,MagVar) end if Settings:IsA2A_LL_DMS()then return self:ToStringLLDMS(Settings) end if Settings:IsA2A_LL_DDM()then return self:ToStringLLDDM(Settings) end if Settings:IsA2A_MGRS()then return self:ToStringMGRS(Settings) end return nil end function COORDINATE:ToString(Controllable,Settings) local Settings=Settings or(Controllable and _DATABASE:GetPlayerSettings(Controllable:GetPlayerName()))or _SETTINGS local ModeA2A=nil 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 function COORDINATE:IsInSteepArea(Radius,Minelevation) local steep=false local elev=Minelevation or 8 local bdelta=0 local h0=self:GetLandHeight() local radius=Radius or 50 local diam=radius*2 for i=0,150,30 do local polar=math.fmod(i+180,360) local c1=self:Translate(radius,i,false,false) local c2=self:Translate(radius,polar,false,false) local h1=c1:GetLandHeight() local h2=c2:GetLandHeight() local d1=math.abs(h1-h2) local d2=math.abs(h0-h1) local d3=math.abs(h0-h2) local dm=d1>d2 and d1 or d2 local dm1=dm>d3 and dm or d3 bdelta=dm1>bdelta and dm1 or bdelta self:T(string.format("d1=%d, d2=%d, d3=%d, max delta=%d",d1,d2,d3,bdelta)) end local steepness=bdelta/(radius/100) if steepness>=elev then steep=true end return steep,math.floor(steepness) end function COORDINATE:IsInFlatArea(Radius,Minelevation) local steep,elev=self:IsInSteepArea(Radius,Minelevation) local flat=not steep return flat,elev end function COORDINATE:GetRandomPointVec3InRadius(OuterRadius,InnerRadius) return COORDINATE:NewFromVec3(self:GetRandomVec3InRadius(OuterRadius,InnerRadius)) 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 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 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(Text,Duration,Category,ClearScreen) local self=BASE:Inherit(self,BASE:New()) self:F({Text,Duration,Category}) self.MessageType=nil if Category and Category~=""then if Category:sub(-1)~="\n"then self.MessageCategory=Category..": " else self.MessageCategory=Category:sub(1,-2)..":\n" end else self.MessageCategory="" end self.ClearScreen=false if ClearScreen~=nil then self.ClearScreen=ClearScreen end self.MessageDuration=Duration or 5 self.MessageTime=timer.getTime() self.MessageText=Text: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) self:ToUnit(Client,Settings) 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:ToUnit(Unit,Settings) self:F(Unit.IdentifiableName) if Unit then if self.MessageType then local Settings=Settings or(Unit and _DATABASE:GetPlayerSettings(Unit: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) local ID=Unit:GetID() trigger.action.outTextForUnit(Unit:GetID(),self.MessageCategory..self.MessageText:gsub("\n$",""):gsub("\n$",""),self.MessageDuration,self.ClearScreen) end end return self end function MESSAGE:ToCountry(Country,Settings) self:F(Country) if Country then 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.outTextForCountry(Country,self.MessageCategory..self.MessageText:gsub("\n$",""):gsub("\n$",""),self.MessageDuration,self.ClearScreen) end end return self end function MESSAGE:ToCountryIf(Country,Condition,Settings) self:F(Country) if Country and Condition==true then self:ToCountry(Country,Settings) 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.MessageCategory..self.MessageText:gsub("\n$",""):gsub("\n$",""),self.MessageDuration,self.ClearScreen) end end self.CoalitionSide=CoalitionSide 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,Delay) self:F() if Delay and Delay>0 then self:ScheduleOnce(Delay,MESSAGE.ToAll,self,Settings,0) else 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 end return self end function MESSAGE:ToAllIf(Condition) if Condition and Condition==true then self:ToAll() end return self end function MESSAGE:ToLog() env.info(self.MessageCategory..self.MessageText:gsub("\n$",""):gsub("\n$","")) return self end function MESSAGE:ToLogIf(Condition) if Condition and Condition==true then env.info(self.MessageCategory..self.MessageText:gsub("\n$",""):gsub("\n$","")) end return self end _MESSAGESRS={} function MESSAGE.SetMSRS(PathToSRS,Port,PathToCredentials,Frequency,Modulation,Gender,Culture,Voice,Coalition,Volume,Label,Coordinate,Backend) _MESSAGESRS.PathToSRS=PathToSRS or MSRS.path or"C:\\Program Files\\DCS-SimpleRadio-Standalone" _MESSAGESRS.frequency=Frequency or MSRS.frequencies or 243 _MESSAGESRS.modulation=Modulation or MSRS.modulations or radio.modulation.AM _MESSAGESRS.MSRS=MSRS:New(_MESSAGESRS.PathToSRS,_MESSAGESRS.frequency,_MESSAGESRS.modulation) _MESSAGESRS.coalition=Coalition or MSRS.coalition or coalition.side.NEUTRAL _MESSAGESRS.MSRS:SetCoalition(_MESSAGESRS.coalition) _MESSAGESRS.coordinate=Coordinate if Coordinate then _MESSAGESRS.MSRS:SetCoordinate(Coordinate) end if Backend then _MESSAGESRS.MSRS:SetBackend(Backend) end _MESSAGESRS.Culture=Culture or MSRS.culture or"en-GB" _MESSAGESRS.MSRS:SetCulture(Culture) _MESSAGESRS.Gender=Gender or MSRS.gender or"female" _MESSAGESRS.MSRS:SetGender(Gender) if PathToCredentials then _MESSAGESRS.MSRS:SetProviderOptionsGoogle(PathToCredentials) _MESSAGESRS.MSRS:SetProvider(MSRS.Provider.GOOGLE) end _MESSAGESRS.label=Label or MSRS.Label or"MESSAGE" _MESSAGESRS.MSRS:SetLabel(_MESSAGESRS.label) _MESSAGESRS.port=Port or MSRS.port or 5002 _MESSAGESRS.MSRS:SetPort(_MESSAGESRS.port) _MESSAGESRS.volume=Volume or MSRS.volume or 1 _MESSAGESRS.MSRS:SetVolume(_MESSAGESRS.volume) if Voice then _MESSAGESRS.MSRS:SetVoice(Voice)end _MESSAGESRS.voice=Voice or MSRS.voice _MESSAGESRS.SRSQ=MSRSQUEUE:New(_MESSAGESRS.label) end function MESSAGE:ToSRS(frequency,modulation,gender,culture,voice,coalition,volume,coordinate) local tgender=gender or _MESSAGESRS.Gender if _MESSAGESRS.SRSQ then if voice then _MESSAGESRS.MSRS:SetVoice(voice or _MESSAGESRS.voice) end if coordinate then _MESSAGESRS.MSRS:SetCoordinate(coordinate) end local category=string.gsub(self.MessageCategory,":","") _MESSAGESRS.SRSQ:NewTransmission(self.MessageText,nil,_MESSAGESRS.MSRS,0.5,1,nil,nil,nil,frequency or _MESSAGESRS.frequency,modulation or _MESSAGESRS.modulation,gender or _MESSAGESRS.Gender,culture or _MESSAGESRS.Culture,nil,volume or _MESSAGESRS.volume,category,coordinate or _MESSAGESRS.coordinate) end return self end function MESSAGE:ToSRSBlue(frequency,modulation,gender,culture,voice,volume,coordinate) self:ToSRS(frequency,modulation,gender,culture,voice,coalition.side.BLUE,volume,coordinate) return self end function MESSAGE:ToSRSRed(frequency,modulation,gender,culture,voice,volume,coordinate) self:ToSRS(frequency,modulation,gender,culture,voice,coalition.side.RED,volume,coordinate) return self end function MESSAGE:ToSRSAll(frequency,modulation,gender,culture,voice,volume,coordinate) self:ToSRS(frequency,modulation,gender,culture,voice,coalition.side.NEUTRAL,volume,coordinate) 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._Transitions[Transition]=Transition self:_eventmap(self.Events,Transition) end function FSM:GetTransitions() return self._Transitions or{} end function FSM:AddProcess(From,Event,Process,ReturnEvents) 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 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) 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:T3("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 %.3f sec SCHEDULED with CallID=%s",EventName,DelaySeconds,tostring(CallID))) else self:T2(string.format("NEGATIVE Event %s delayed by %.3f 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 %.3f 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 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 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 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) 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 NewFsm:AddEndState(EndState) end for ScoreID,Score in pairs(self:GetScores())do 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.Set 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 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()) 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.SpawnInitModexPrefix=nil self.SpawnInitModexPostfix=nil self.SpawnInitAirbase=nil self.TweakedTemplate=false self.SpawnRandomCallsign=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()) 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.SpawnInitModexPrefix=nil self.SpawnInitModexPostfix=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,NoMooseNamingPostfix) local self=BASE:Inherit(self,BASE:New()) if SpawnTemplatePrefix==nil or SpawnTemplatePrefix==""then BASE:I("ERROR: in function NewFromTemplate, required parameter SpawnTemplatePrefix is not set") return nil end if SpawnTemplate then self.SpawnTemplate=UTILS.DeepCopy(SpawnTemplate) self.SpawnTemplatePrefix=SpawnTemplatePrefix self.SpawnAliasPrefix=SpawnAliasPrefix or SpawnTemplatePrefix self.SpawnTemplate.name=SpawnTemplatePrefix 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.SpawnInitModexPrefix=nil self.SpawnInitModexPostfix=nil self.SpawnInitAirbase=nil self.TweakedTemplate=true self.MooseNameing=true if NoMooseNamingPostfix==true then self.MooseNameing=false end 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.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.SpawnInitKeepUnitNames=false if KeepUnitNames==true then self.SpawnInitKeepUnitNames=true end return self end function SPAWN:InitLateActivated(LateActivated) self.LateActivated=LateActivated or true return self end function SPAWN:InitAirbase(AirbaseName,Takeoff,TerminalType) self.SpawnInitAirbase=AIRBASE:FindByName(AirbaseName) self.SpawnInitTakeoff=Takeoff or SPAWN.Takeoff.Hot self.SpawnInitTerminalType=TerminalType return self end function SPAWN:InitHeading(HeadingMin,HeadingMax) 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.SpawnInitCountry=Country return self end function SPAWN:InitCategory(Category) self.SpawnInitCategory=Category return self end function SPAWN:InitLivery(Livery) self.SpawnInitLivery=Livery return self end function SPAWN:InitSkill(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:InitSTN(Octal) self.SpawnInitSTN=Octal or 77777 local num=UTILS.OctalToDecimal(Octal) if num==nil or num<1 then self:E("WARNING - STN "..tostring(Octal).." is not valid!") return self end if _DATABASE.STNS[num]~=nil then self:E("WARNING - STN already assigned: "..tostring(Octal).." is used for ".._DATABASE.STNS[Octal]) end return self end function SPAWN:InitSADL(Octal) self.SpawnInitSADL=Octal or 7777 local num=UTILS.OctalToDecimal(Octal) if num==nil or num<1 then self:E("WARNING - SADL "..tostring(Octal).." is not valid!") return self end if _DATABASE.SADL[num]~=nil then self:E("WARNING - SADL already assigned: "..tostring(Octal).." is used for ".._DATABASE.SADL[Octal]) end return self end function SPAWN:InitSpeedMps(MPS) if MPS==nil or tonumber(MPS)<0 then MPS=125 end self.InitSpeed=MPS return self end function SPAWN:InitSpeedKnots(Knots) if Knots==nil or tonumber(Knots)<0 then Knots=300 end self.InitSpeed=UTILS.KnotsToMps(Knots) return self end function SPAWN:InitSpeedKph(KPH) if KPH==nil or tonumber(KPH)<0 then KPH=UTILS.KnotsToKmph(300) end self.InitSpeed=UTILS.KmphToMps(KPH) return self end function SPAWN:InitRadioCommsOnOff(switch) self.SpawnInitRadio=switch or true return self end function SPAWN:InitRadioFrequency(frequency) self.SpawnInitFreq=frequency return self end function SPAWN:InitRadioModulation(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,prefix,postfix) if modex then self.SpawnInitModex=tonumber(modex) end self.SpawnInitModexPrefix=prefix self.SpawnInitModexPostfix=postfix return self end function SPAWN:InitRandomizeRoute(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.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.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:InitSetUnitRelativePositions(Positions) self.SpawnUnitsWithRelativePositions=true self.UnitsRelativePositions=Positions return self end function SPAWN:InitSetUnitAbsolutePositions(Positions) self.SpawnUnitsWithAbsolutePositions=true self.UnitsAbsolutePositions=Positions return self end function SPAWN:InitRandomizeTemplate(SpawnTemplatePrefixTable) local temptable={} for _,_temp in pairs(SpawnTemplatePrefixTable)do temptable[#temptable+1]=_temp end self.SpawnTemplatePrefixTable=UTILS.ShuffleTable(temptable) self.SpawnRandomizeTemplate=true for SpawnGroupID=1,self.SpawnMaxGroups do self:_RandomizeTemplate(SpawnGroupID,RandomizePositionInZone) end return self end function SPAWN:InitRandomizeTemplateSet(SpawnTemplateSet,RandomizePositionInZone) local setnames=SpawnTemplateSet:GetSetNames() self:InitRandomizeTemplate(setnames,RandomizePositionInZone) return self end function SPAWN:InitRandomizeTemplatePrefixes(SpawnTemplatePrefixes,RandomizePositionInZone) local SpawnTemplateSet=SET_GROUP:New():FilterPrefixes(SpawnTemplatePrefixes):FilterOnce() self:InitRandomizeTemplateSet(SpawnTemplateSet,RandomizePositionInZone) return self end function SPAWN:InitGrouping(Grouping) self.SpawnGrouping=Grouping return self end function SPAWN:InitRandomizeZones(SpawnZoneTable,RandomizePositionInZone) local temptable={} for _,_temp in pairs(SpawnZoneTable)do temptable[#temptable+1]=_temp end self.SpawnZoneTable=UTILS.ShuffleTable(temptable) self.SpawnRandomizeZones=true for SpawnGroupID=1,self.SpawnMaxGroups do self:_RandomizeZones(SpawnGroupID,RandomizePositionInZone) end return self end function SPAWN:InitRandomizeCallsign() self.SpawnRandomCallsign=true return self end function SPAWN:InitCallSign(ID,Name,Minor,Major) local Name=Name or"Enfield" self.SpawnInitCallSign=true self.SpawnInitCallSignID=ID or 1 self.SpawnInitCallSignMinor=Minor or 1 self.SpawnInitCallSignMajor=Major or 1 self.SpawnInitCallSignName=string.lower(Name):gsub("^%l",string.upper) return self end function SPAWN:InitPositionCoordinate(Coordinate) self:InitPositionVec2(Coordinate:GetVec2()) return self end function SPAWN:InitPositionVec2(Vec2) self.SpawnInitPosition=Vec2 self.SpawnFromNewPosition=true for SpawnGroupID=1,self.SpawnMaxGroups do self:_SetInitialPosition(SpawnGroupID) end return self end function SPAWN:InitRepeat() self.Repeat=true self.RepeatOnEngineShutDown=false self.RepeatOnLanding=true return self end function SPAWN:InitRepeatOnLanding(WaitingTime) self:InitRepeat() self.RepeatOnEngineShutDown=false self.RepeatOnLanding=true self.RepeatOnLandingTime=(WaitingTime and WaitingTime>3)and WaitingTime or 3 return self end function SPAWN:InitRepeatOnEngineShutDown() self:InitRepeat() self.RepeatOnEngineShutDown=true self.RepeatOnLanding=false return self end function SPAWN:InitCleanUp(SpawnCleanUpInterval) self.SpawnCleanUpInterval=SpawnCleanUpInterval self.SpawnCleanUpTimeStamps={} local SpawnGroup,SpawnCursor=self:GetFirstAliveGroup() self.CleanUpScheduler=SCHEDULER:New(self,self._SpawnCleanUpScheduler,{},1,SpawnCleanUpInterval,0.2) return self end function SPAWN:InitArray(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.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) self:HandleEvent(EVENTS.UnitLost,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 function SPAWN:StopRepeat() if self.Repeat then self:UnHandleEvent(EVENTS.Takeoff) self:UnHandleEvent(EVENTS.Land) end if self.RepeatOnEngineShutDown then self:UnHandleEvent(EVENTS.EngineShutdown) end self.Repeat=false self.RepeatOnEngineShutDown=false self.RepeatOnLanding=false 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:InitHiddenOnMap(OnOff) self.SpawnHiddenOnMap=OnOff==false and false or true return self end function SPAWN:InitHiddenOnMFD() self.SpawnHiddenOnMFD=true return self end function SPAWN:InitHiddenOnPlanner() self.SpawnHiddenOnPlanner=true return self end function SPAWN:Spawn() 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) 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 and SpawnGroup.ReSpawnFunction then SpawnGroup:ReSpawnFunction() end if SpawnGroup then SpawnGroup:ResetEvents()end return SpawnGroup end function SPAWN:SetSpawnIndex(SpawnIndex) self.SpawnIndex=SpawnIndex or 0 end function SPAWN:SpawnWithIndex(SpawnIndex,NoBirth) if self:_GetSpawnIndex(SpawnIndex)then if self.SpawnFromNewPosition then self:_SetInitialPosition(SpawnIndex) end if self.SpawnGroups[self.SpawnIndex].Visible then self.SpawnGroups[self.SpawnIndex].Group:Activate() else local SpawnTemplate=self.SpawnGroups[self.SpawnIndex].SpawnTemplate local SpawnZone=self.SpawnGroups[self.SpawnIndex].SpawnZone if SpawnTemplate then local PointVec3=COORDINATE:New(SpawnTemplate.route.points[1].x,SpawnTemplate.route.points[1].alt,SpawnTemplate.route.points[1].y) 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) end end if self.SpawnRandomizeUnits then for UnitID=1,#SpawnTemplate.units do local RandomVec2=PointVec3:GetRandomVec2InRadius(self.SpawnOuterRadius,self.SpawnInnerRadius) if(SpawnZone)then local inZone=SpawnZone:IsVec2InZone(RandomVec2) local numTries=1 while(not inZone)and(numTries<20)do if not inZone then RandomVec2=PointVec3:GetRandomVec2InRadius(self.SpawnOuterRadius,self.SpawnInnerRadius) numTries=numTries+1 inZone=SpawnZone:IsVec2InZone(RandomVec2) end end if(not inZone)then RandomVec2=SpawnZone:GetRandomVec2() end end SpawnTemplate.units[UnitID].x=RandomVec2.x SpawnTemplate.units[UnitID].y=RandomVec2.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 not self.SpawnRandomizeUnits then 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 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.SpawnUnitsWithRelativePositions and self.UnitsRelativePositions then local BaseX=SpawnTemplate.units[1].x or 0 local BaseY=SpawnTemplate.units[1].y or 0 local BaseZ=SpawnTemplate.units[1].z or 0 for UnitID=1,#SpawnTemplate.units do if self.UnitsRelativePositions[UnitID].heading then SpawnTemplate.units[UnitID].heading=math.rad(self.UnitsRelativePositions[UnitID].heading or 0) end SpawnTemplate.units[UnitID].x=BaseX+(self.UnitsRelativePositions[UnitID].x or 0) SpawnTemplate.units[UnitID].y=BaseY+(self.UnitsRelativePositions[UnitID].y or 0) if self.UnitsRelativePositions[UnitID].z then SpawnTemplate.units[UnitID].z=BaseZ+(self.UnitsRelativePositions[UnitID].z or 0) end end end if self.SpawnUnitsWithAbsolutePositions and self.UnitsAbsolutePositions then for UnitID=1,#SpawnTemplate.units do if self.UnitsAbsolutePositions[UnitID].heading then SpawnTemplate.units[UnitID].heading=math.rad(self.UnitsAbsolutePositions[UnitID].heading or 0) end SpawnTemplate.units[UnitID].x=self.UnitsAbsolutePositions[UnitID].x or 0 SpawnTemplate.units[UnitID].y=self.UnitsAbsolutePositions[UnitID].y or 0 if self.UnitsAbsolutePositions[UnitID].z then SpawnTemplate.units[UnitID].z=self.UnitsAbsolutePositions[UnitID].z or 0 end 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 local modexnumber=string.format("%03d",self.SpawnInitModex+(UnitID-1)) if self.SpawnInitModexPrefix then modexnumber=self.SpawnInitModexPrefix..modexnumber end if self.SpawnInitModexPostfix then modexnumber=modexnumber..self.SpawnInitModexPostfix end SpawnTemplate.units[UnitID].onboard_num=modexnumber 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 if self.SpawnHiddenOnPlanner then SpawnTemplate.hiddenOnPlanner=true end if self.SpawnHiddenOnMFD then SpawnTemplate.hiddenOnMFD=true end if self.SpawnHiddenOnMap then SpawnTemplate.hidden=self.SpawnHiddenOnMap 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.Crash,self._OnDeadOrCrash) self:HandleEvent(EVENTS.UnitLost,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.3) end end self.SpawnGroups[self.SpawnIndex].Spawned=true self.SpawnGroups[self.SpawnIndex].Group.TemplateDonor=self.SpawnTemplatePrefix return self.SpawnGroups[self.SpawnIndex].Group else end return nil end function SPAWN:SpawnScheduled(SpawnTime,SpawnTimeVariation,WithDelay) local SpawnTime=SpawnTime or 60 local SpawnTimeVariation=SpawnTimeVariation or 0.5 if SpawnTime<15 then self:E("****SPAWN SCHEDULED****\nWARNING - Setting a very low SpawnTime heavily impacts your mission performance and CPU time, it is NOT useful to check the alive state of an object every "..tostring(SpawnTime).." seconds.\nSetting to 15 second intervals.\n*****") SpawnTime=15 end if SpawnTimeVariation>1 or SpawnTimeVariation<0 then SpawnTimeVariation=0.5 end if SpawnTime~=nil and SpawnTimeVariation~=nil then local InitialDelay=0 if WithDelay or 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.SpawnScheduler:Start() return self end function SPAWN:SpawnScheduleStop() self.SpawnScheduler:Stop() return self end function SPAWN:OnSpawnGroup(SpawnCallBackFunction,...) self.SpawnFunctionHook=SpawnCallBackFunction self.SpawnFunctionArguments={} if arg then self.SpawnFunctionArguments=arg end return self end function SPAWN:SpawnAtAirbase(SpawnAirbase,Takeoff,TakeoffAltitude,TerminalType,EmergencyAirSpawn,Parkingdata) local PointVec3=SpawnAirbase:GetCoordinate() Takeoff=Takeoff or SPAWN.Takeoff.Hot if EmergencyAirSpawn==nil then EmergencyAirSpawn=true end if self:_GetSpawnIndex(self.SpawnIndex+1)then local SpawnTemplate=self.SpawnGroups[self.SpawnIndex].SpawnTemplate if SpawnTemplate then local group=GROUP:FindByName(self.SpawnTemplatePrefix) local unit=group:GetUnit(1) 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=unit: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() if AirbaseCategory==Airbase.Category.SHIP then SpawnPoint.linkUnit=AirbaseID SpawnPoint.helipadId=AirbaseID elseif AirbaseCategory==Airbase.Category.HELIPAD then SpawnPoint.linkUnit=AirbaseID SpawnPoint.helipadId=AirbaseID else 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) local autoparking=false if SpawnAirbase.isAirdrome then autoparking=false else autoparking=true end local parkingspots={} local parkingindex={} local spots if spawnonground and not SpawnTemplate.parked then local nfree=0 local termtype=TerminalType if Takeoff==SPAWN.Takeoff.Runway then if SpawnAirbase.isShip 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 autoparking then nfree=SpawnAirbase:GetFreeParkingSpotsNumber(termtype,true) spots=SpawnAirbase:GetFreeParkingSpotsTable(termtype,true) elseif Parkingdata~=nil then nfree=#Parkingdata spots=Parkingdata else if ishelo then if termtype==nil then spots=SpawnAirbase:FindFreeParkingSpotForAircraft(group,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 else 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())) autoparking=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 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 autoparking then SpawnTemplate.units[UnitID].x=PointVec3.x SpawnTemplate.units[UnitID].y=PointVec3.z SpawnTemplate.units[UnitID].alt=PointVec3.y else SpawnTemplate.units[UnitID].x=parkingspots[UnitID].x SpawnTemplate.units[UnitID].y=parkingspots[UnitID].z SpawnTemplate.units[UnitID].alt=parkingspots[UnitID].y end else 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 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 self:ScheduleOnce(5,BASE.CreateEventTakeoff,{GroupSpawned,timer.getTime(),UnitSpawned:GetDCSObject()}) end end return GroupSpawned end end return nil end function SPAWN:SpawnAtParkingSpot(Airbase,Spots,Takeoff) if type(Spots)~="table"then Spots={Spots} end if type(Airbase)=="string"then Airbase=AIRBASE:FindByName(Airbase) 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) local PointVec3=SpawnAirbase:GetCoordinate() local Takeoff=SPAWN.Takeoff.Cold local SpawnTemplate=self.SpawnGroups[SpawnIndex].SpawnTemplate if SpawnTemplate then local GroupAlive=self:GetGroupFromIndex(SpawnIndex) 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() 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) 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 nfree=SpawnAirbase:GetFreeParkingSpotsNumber(termtype,true) spots=SpawnAirbase:GetFreeParkingSpotsTable(termtype,true) else if ishelo then if termtype==nil then 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 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 SpawnTemplate.units[UnitID].x=PointVec3.x SpawnTemplate.units[UnitID].y=PointVec3.z SpawnTemplate.units[UnitID].alt=PointVec3.y else SpawnTemplate.units[UnitID].x=parkingspots[UnitID].x SpawnTemplate.units[UnitID].y=parkingspots[UnitID].z SpawnTemplate.units[UnitID].alt=parkingspots[UnitID].y end else 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 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: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) local PointVec3=COORDINATE:NewFromVec3(Vec3) if SpawnIndex then else SpawnIndex=self.SpawnIndex+1 end if self:_GetSpawnIndex(SpawnIndex)then local SpawnTemplate=self.SpawnGroups[self.SpawnIndex].SpawnTemplate if SpawnTemplate then 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 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) return self:SpawnFromVec3(Coordinate:GetVec3(),SpawnIndex) end function SPAWN:SpawnFromPointVec3(PointVec3,SpawnIndex) return self:SpawnFromVec3(PointVec3:GetVec3(),SpawnIndex) end function SPAWN:SpawnFromVec2(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) return self:SpawnFromVec2(PointVec2:GetVec2(),MinHeight,MaxHeight,SpawnIndex) end function SPAWN:SpawnFromUnit(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) 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) 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) local SpawnPrefix=self.SpawnTemplatePrefix if self.SpawnAliasPrefix then SpawnPrefix=self.SpawnAliasPrefix end if SpawnIndex then local SpawnName=string.format('%s#%03d',SpawnPrefix,SpawnIndex) return SpawnName else return SpawnPrefix end end function SPAWN:GetFirstAliveGroup() 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) 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() 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) 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) local GroupName=SpawnGroup:GetName() if GroupName then local SpawnPrefix=self:_GetPrefixFromGroupName(GroupName) return SpawnPrefix end return nil end function SPAWN:_GetPrefixFromGroupName(SpawnGroupName) if SpawnGroupName then local SpawnPrefix=string.match(SpawnGroupName,".*#") if SpawnPrefix then SpawnPrefix=SpawnPrefix:sub(1,-2) end return SpawnPrefix end return nil end function SPAWN:GetSpawnIndexFromGroup(SpawnGroup) local IndexString=string.match(SpawnGroup:GetName(),"#(%d*)$"):sub(2) local Index=tonumber(IndexString) self:T3(IndexString,Index) return Index end function SPAWN:_GetLastIndex() return self.SpawnMaxGroups end function SPAWN:_InitializeSpawnGroups(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) 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) local SpawnTemplate=nil if _DATABASE.Templates.Groups[SpawnTemplatePrefix]==nil then error('No Template exists for SpawnTemplatePrefix = '..SpawnTemplatePrefix) end local Template=_DATABASE.Templates.Groups[SpawnTemplatePrefix].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) local SpawnTemplate if self.TweakedTemplate~=nil and self.TweakedTemplate==true then BASE:I("WARNING: You are using a tweaked template.") SpawnTemplate=self.SpawnTemplate if self.MooseNameing==true then SpawnTemplate.name=self:SpawnGroupName(SpawnIndex) else SpawnTemplate.name=self:SpawnGroupName() end 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 if UnitAmount>self.SpawnGrouping then for UnitID=self.SpawnGrouping+1,UnitAmount do SpawnTemplate.units[UnitID]=nil end else if UnitAmount1 then octal=_DATABASE:GetNextSTN(self.SpawnInitSTN,SpawnTemplate.units[UnitID].name) end SpawnTemplate.units[UnitID].AddPropAircraft.STN_L16=string.format("%05d",octal) else if tonumber(SpawnTemplate.units[UnitID].AddPropAircraft.STN_L16)~=nil then local octal=SpawnTemplate.units[UnitID].AddPropAircraft.STN_L16 local num=UTILS.OctalToDecimal(octal) if _DATABASE.STNS[num]~=nil or UnitID>1 then octal=_DATABASE:GetNextSTN(octal,SpawnTemplate.units[UnitID].name) end SpawnTemplate.units[UnitID].AddPropAircraft.STN_L16=string.format("%05d",octal) else local OSTN=_DATABASE:GetNextSTN(1,SpawnTemplate.units[UnitID].name) SpawnTemplate.units[UnitID].AddPropAircraft.STN_L16=string.format("%05d",OSTN) end end end if SpawnTemplate.units[UnitID].AddPropAircraft.SADL_TN then if self.SpawnInitSADL then local octal=self.SpawnInitSADL if UnitID>1 then octal=_DATABASE:GetNextSADL(self.SpawnInitSADL,SpawnTemplate.units[UnitID].name) end SpawnTemplate.units[UnitID].AddPropAircraft.SADL_TN=string.format("%04d",octal) else if tonumber(SpawnTemplate.units[UnitID].AddPropAircraft.SADL_TN)~=nil then local octal=SpawnTemplate.units[UnitID].AddPropAircraft.SADL_TN local num=UTILS.OctalToDecimal(octal) self.SpawnInitSADL=num if _DATABASE.SADL[num]~=nil or UnitID>1 then octal=_DATABASE:GetNextSADL(self.SpawnInitSADL,SpawnTemplate.units[UnitID].name) end SpawnTemplate.units[UnitID].AddPropAircraft.SADL_TN=string.format("%04d",octal) else local OSTN=_DATABASE:GetNextSADL(1,SpawnTemplate.units[UnitID].name) SpawnTemplate.units[UnitID].AddPropAircraft.SADL_TN=string.format("%04d",OSTN) end end end if SpawnTemplate.units[UnitID].AddPropAircraft.VoiceCallsignNumber and type(Callsign)~="number"then SpawnTemplate.units[UnitID].AddPropAircraft.VoiceCallsignNumber=SpawnTemplate.units[UnitID].callsign[2]..SpawnTemplate.units[UnitID].callsign[3] end if SpawnTemplate.units[UnitID].AddPropAircraft.VoiceCallsignLabel and type(Callsign)~="number"then local CallsignName=SpawnTemplate.units[UnitID].callsign["name"] CallsignName=string.match(CallsignName,"^(%a+)") local label="NY" if not string.find(CallsignName," ")then label=string.upper(string.match(CallsignName,"^%a")..string.match(CallsignName,"%a$")) end SpawnTemplate.units[UnitID].AddPropAircraft.VoiceCallsignLabel=label end if SpawnTemplate.units[UnitID].datalinks and SpawnTemplate.units[UnitID].datalinks.Link16 and SpawnTemplate.units[UnitID].datalinks.Link16.settings then SpawnTemplate.units[UnitID].datalinks.Link16.settings.flightLead=UnitID==1 and true or false end if SpawnTemplate.units[UnitID].datalinks and SpawnTemplate.units[UnitID].datalinks.SADL and SpawnTemplate.units[UnitID].datalinks.SADL.settings then SpawnTemplate.units[UnitID].datalinks.SADL.settings.flightLead=UnitID==1 and true or false end end end for UnitID=1,#SpawnTemplate.units do if SpawnTemplate.units[UnitID].datalinks and SpawnTemplate.units[UnitID].datalinks.Link16 and SpawnTemplate.units[UnitID].datalinks.Link16.network then local team={} local isF16=string.find(SpawnTemplate.units[UnitID].type,"F-16",1,true)and true or false for ID=1,#SpawnTemplate.units do local member={} member.missionUnitId=ID if isF16 then member.TDOA=true end table.insert(team,member) end SpawnTemplate.units[UnitID].datalinks.Link16.network.teamMembers=team end end self:T3({"Template:",SpawnTemplate}) return SpawnTemplate end function SPAWN:_RandomizeRoute(SpawnIndex) if self.SpawnRandomizeRoute then local SpawnTemplate=self.SpawnGroups[SpawnIndex].SpawnTemplate local RouteCount=#SpawnTemplate.route.points for t=self.SpawnRandomizeRouteStartPoint+1,(RouteCount-self.SpawnRandomizeRouteEndPoint)do SpawnTemplate.route.points[t].x=SpawnTemplate.route.points[t].x+math.random(self.SpawnRandomizeRouteRadius*-1,self.SpawnRandomizeRouteRadius) SpawnTemplate.route.points[t].y=SpawnTemplate.route.points[t].y+math.random(self.SpawnRandomizeRouteRadius*-1,self.SpawnRandomizeRouteRadius) if SpawnTemplate.CategoryID==Group.Category.AIRPLANE or SpawnTemplate.CategoryID==Group.Category.HELICOPTER then if SpawnTemplate.route.points[t].alt and self.SpawnRandomizeRouteHeight then SpawnTemplate.route.points[t].alt=SpawnTemplate.route.points[t].alt+math.random(1,self.SpawnRandomizeRouteHeight) end else SpawnTemplate.route.points[t].alt=nil end end end self:_RandomizeZones(SpawnIndex) return self end function SPAWN:_RandomizeTemplate(SpawnIndex) if self.SpawnRandomizeTemplate then self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix=self.SpawnTemplatePrefixTable[math.random(1,#self.SpawnTemplatePrefixTable)] self.SpawnGroups[SpawnIndex].SpawnTemplate=self:_Prepare(self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix,SpawnIndex) self.SpawnGroups[SpawnIndex].SpawnTemplate.route=UTILS.DeepCopy(self.SpawnTemplate.route) self.SpawnGroups[SpawnIndex].SpawnTemplate.x=self.SpawnTemplate.x self.SpawnGroups[SpawnIndex].SpawnTemplate.y=self.SpawnTemplate.y self.SpawnGroups[SpawnIndex].SpawnTemplate.start_time=self.SpawnTemplate.start_time local OldX=self.SpawnGroups[SpawnIndex].SpawnTemplate.units[1].x local OldY=self.SpawnGroups[SpawnIndex].SpawnTemplate.units[1].y for UnitID=1,#self.SpawnGroups[SpawnIndex].SpawnTemplate.units do self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].heading=self.SpawnTemplate.units[1].heading self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].x=self.SpawnTemplate.units[1].x+(self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].x-OldX) self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].y=self.SpawnTemplate.units[1].y+(self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].y-OldY) self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].alt=self.SpawnTemplate.units[1].alt end end self:_RandomizeRoute(SpawnIndex) return self end function SPAWN:_SetInitialPosition(SpawnIndex) if self.SpawnFromNewPosition then local SpawnVec2=self.SpawnInitPosition local SpawnTemplate=self.SpawnGroups[SpawnIndex].SpawnTemplate 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 local SY=UnitTemplate.y local BX=SpawnTemplate.route.points[1].x local BY=SpawnTemplate.route.points[1].y local TX=SpawnVec2.x+(SX-BX) local TY=SpawnVec2.y+(SY-BY) UnitTemplate.x=TX UnitTemplate.y=TY end SpawnTemplate.route.points[1].x=SpawnVec2.x SpawnTemplate.route.points[1].y=SpawnVec2.y SpawnTemplate.x=SpawnVec2.x SpawnTemplate.y=SpawnVec2.y end return self end function SPAWN:_RandomizeZones(SpawnIndex,RandomizePositionInZone) if self.SpawnRandomizeZones then local SpawnZone=nil while not SpawnZone do local ZoneID=math.random(#self.SpawnZoneTable) SpawnZone=self.SpawnZoneTable[ZoneID]:GetZoneMaybe() end local SpawnVec2=SpawnZone:GetVec2() if RandomizePositionInZone~=false then SpawnVec2=SpawnZone:GetRandomVec2() end local SpawnTemplate=self.SpawnGroups[SpawnIndex].SpawnTemplate self.SpawnGroups[SpawnIndex].SpawnZone=SpawnZone for UnitID=1,#SpawnTemplate.units 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=SpawnVec2.x+(SX-BX) local TY=SpawnVec2.y+(SY-BY) UnitTemplate.x=TX UnitTemplate.y=TY end SpawnTemplate.x=SpawnVec2.x SpawnTemplate.y=SpawnVec2.y SpawnTemplate.route.points[1].x=SpawnVec2.x SpawnTemplate.route.points[1].y=SpawnVec2.y end return self end function SPAWN:_TranslateRotate(SpawnIndex,SpawnRootX,SpawnRootY,SpawnX,SpawnY,SpawnAngle) local TranslatedX=SpawnX local TranslatedY=SpawnY local RotatedX=-TranslatedX*math.cos(math.rad(SpawnAngle))+TranslatedY*math.sin(math.rad(SpawnAngle)) local RotatedY=TranslatedX*math.sin(math.rad(SpawnAngle))+TranslatedY*math.cos(math.rad(SpawnAngle)) self.SpawnGroups[SpawnIndex].SpawnTemplate.x=SpawnRootX-RotatedX self.SpawnGroups[SpawnIndex].SpawnTemplate.y=SpawnRootY+RotatedY local SpawnUnitCount=table.getn(self.SpawnGroups[SpawnIndex].SpawnTemplate.units) for u=1,SpawnUnitCount do local TranslatedX=SpawnX local TranslatedY=SpawnY-10*(u-1) local RotatedX=-TranslatedX*math.cos(math.rad(SpawnAngle))+TranslatedY*math.sin(math.rad(SpawnAngle)) local RotatedY=TranslatedX*math.sin(math.rad(SpawnAngle))+TranslatedY*math.cos(math.rad(SpawnAngle)) self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].x=SpawnRootX-RotatedX self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].y=SpawnRootY+RotatedY self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading=self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading+math.rad(SpawnAngle) end return self end function SPAWN:_GetSpawnIndex(SpawnIndex) if(self.SpawnMaxGroups==0)or(SpawnIndex<=self.SpawnMaxGroups)then if(self.SpawnMaxUnitsAlive==0)or(self.AliveUnits+#self.SpawnTemplate.units<=self.SpawnMaxUnitsAlive)or self.UnControlled==true then if SpawnIndex and SpawnIndex>=self.SpawnCount+1 then self.SpawnCount=self.SpawnCount+1 SpawnIndex=self.SpawnCount end self.SpawnIndex=SpawnIndex if not self.SpawnGroups[self.SpawnIndex]then self:_InitializeSpawnGroups(self.SpawnIndex) end else return nil end else return nil end return self.SpawnIndex end function SPAWN:_OnBirth(EventData) local SpawnGroup=EventData.IniGroup if SpawnGroup then local EventPrefix=self:_GetPrefixFromGroup(SpawnGroup) if EventPrefix then if EventPrefix==self.SpawnTemplatePrefix or(self.SpawnAliasPrefix and EventPrefix==self.SpawnAliasPrefix)then self.AliveUnits=self.AliveUnits+1 end end end end function SPAWN:_CountAliveUnits() local count=0 if self.SpawnAliasPrefix then if not self.SpawnAliasPrefixEscaped then self.SpawnAliasPrefixEscaped=string.gsub(self.SpawnAliasPrefix,"[%p%s]",".")end local SpawnAliasPrefix=self.SpawnAliasPrefixEscaped local agroups=GROUP:FindAllByMatching(SpawnAliasPrefix) for _,_grp in pairs(agroups)do local game=self:_GetPrefixFromGroupName(_grp.GroupName) if game and game==self.SpawnAliasPrefix then count=count+_grp:CountAliveUnits() end end else if not self.SpawnTemplatePrefixEscaped then self.SpawnTemplatePrefixEscaped=string.gsub(self.SpawnTemplatePrefix,"[%p%s]",".")end local SpawnTemplatePrefix=self.SpawnTemplatePrefixEscaped local groups=GROUP:FindAllByMatching(SpawnTemplatePrefix) for _,_grp in pairs(groups)do local game=self:_GetPrefixFromGroupName(_grp.GroupName) if game and game==self.SpawnTemplatePrefix then count=count+_grp:CountAliveUnits() end end end self.AliveUnits=count return self end function SPAWN:_OnDeadOrCrash(EventData) local unit=UNIT:FindByName(EventData.IniUnitName) if unit then local EventPrefix=self:_GetPrefixFromGroupName(unit.GroupName) if EventPrefix then if EventPrefix==self.SpawnTemplatePrefix or(self.SpawnAliasPrefix and EventPrefix==self.SpawnAliasPrefix)and self.AliveUnits>0 then self:ScheduleOnce(1,self._CountAliveUnits,self) end end end end function SPAWN:_OnTakeOff(EventData) local SpawnGroup=EventData.IniGroup if SpawnGroup then local EventPrefix=self:_GetPrefixFromGroup(SpawnGroup) if EventPrefix then if EventPrefix==self.SpawnTemplatePrefix or(self.SpawnAliasPrefix and EventPrefix==self.SpawnAliasPrefix)then SpawnGroup:SetState(SpawnGroup,"Spawn_Landed",false) end end end end function SPAWN:_OnLand(EventData) local SpawnGroup=EventData.IniGroup if SpawnGroup then local EventPrefix=self:_GetPrefixFromGroup(SpawnGroup) if EventPrefix then if EventPrefix==self.SpawnTemplatePrefix or(self.SpawnAliasPrefix and EventPrefix==self.SpawnAliasPrefix)then SpawnGroup:SetState(SpawnGroup,"Spawn_Landed",true) if self.RepeatOnLanding then local SpawnGroupIndex=self:GetSpawnIndexFromGroup(SpawnGroup) SCHEDULER:New(nil,self.ReSpawn,{self,SpawnGroupIndex},self.RepeatOnLandingTime or 3) end end end end end function SPAWN:_OnEngineShutDown(EventData) local SpawnGroup=EventData.IniGroup if SpawnGroup then local EventPrefix=self:_GetPrefixFromGroup(SpawnGroup) if EventPrefix then if EventPrefix==self.SpawnTemplatePrefix or(self.SpawnAliasPrefix and EventPrefix==self.SpawnAliasPrefix)then local Landed=SpawnGroup:GetState(SpawnGroup,"Spawn_Landed") if Landed and self.RepeatOnEngineShutDown then local SpawnGroupIndex=self:GetSpawnIndexFromGroup(SpawnGroup) SCHEDULER:New(nil,self.ReSpawn,{self,SpawnGroupIndex},3) end end end end end function SPAWN:_Scheduler() self:F2({"_Scheduler",self.SpawnTemplatePrefix,self.SpawnAliasPrefix,self.SpawnIndex,self.SpawnMaxGroups,self.SpawnMaxUnitsAlive}) self:Spawn() return true end function SPAWN:_SpawnCleanUpScheduler() local SpawnGroup,SpawnCursor=self:GetFirstAliveGroup() local IsHelo=false while SpawnGroup do IsHelo=SpawnGroup:IsHelicopter() local SpawnUnits=SpawnGroup:GetUnits() for UnitID,UnitData in pairs(SpawnUnits)do local SpawnUnit=UnitData local SpawnUnitName=SpawnUnit:GetName() self.SpawnCleanUpTimeStamps[SpawnUnitName]=self.SpawnCleanUpTimeStamps[SpawnUnitName]or{} local Stamp=self.SpawnCleanUpTimeStamps[SpawnUnitName] if Stamp.Vec2 then if(SpawnUnit:InAir()==false and SpawnUnit:GetVelocityKMH()<1)or IsHelo then local NewVec2=SpawnUnit:GetVec2()or{x=0,y=0} if(Stamp.Vec2.x==NewVec2.x and Stamp.Vec2.y==NewVec2.y)or(SpawnUnit:GetLife()<=1)then if Stamp.Time+self.SpawnCleanUpInterval0 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)) local status=pcall( function() timer.removeFunction(self.tid) end ) if status then self:T2(self.lid..string.format("Stopped timer!")) else self:E(self.lid..string.format("WARNING: Could not remove timer function! isrunning=%s",tostring(self.isrunning))) end self.isrunning=false end end return self end function TIMER:SetMaxFunctionCalls(Nmax) self.ncallsMax=Nmax return self end function TIMER:SetTimeInterval(dT) self.dT=dT 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.RecceName=self.Recce:GetName() self.LaseScheduler=SCHEDULER:New(self) self:SetEventPriority(5) self.Lasing=false return self end function SPOT:onafterLaseOn(From,Event,To,Target,LaserCode,Duration) self:T({From,Event,To}) self:T2({"LaseOn",Target,LaserCode,Duration}) local function StopLase(self) self:LaseOff() end self.Target=Target self.TargetName=Target:GetName() self.LaserCode=LaserCode self.Lasing=true local RecceDcsUnit=self.Recce:GetDCSObject() local relativespot=self.relstartpos or{x=0,y=2,z=0} self.SpotIR=Spot.createInfraRed(RecceDcsUnit,relativespot,Target:GetPointVec3():AddY(1):GetVec3()) self.SpotLaser=Spot.createLaser(RecceDcsUnit,relativespot,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) return self end function SPOT:onafterLaseOnCoordinate(From,Event,To,Coordinate,LaserCode,Duration) self:T2({"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) return self end function SPOT:OnEventDead(EventData) self:T2({Dead=EventData.IniDCSUnitName,Target=self.Target}) if self.Target then if EventData.IniDCSUnitName==self.TargetName then self:F({"Target dead ",self.TargetName}) self:Destroyed() self:LaseOff() end end if self.Recce then if EventData.IniDCSUnitName==self.RecceName then self:F({"Recce dead ",self.RecceName}) self:LaseOff() end end return self end function SPOT:onafterLasing(From,Event,To) self:T({From,Event,To}) if self.Lasing then if self.Target and self.Target:IsAlive()then self.SpotIR:setPoint(self.Target:GetPointVec3():AddY(1):AddY(math.random(-100,100)/200):AddX(math.random(-100,100)/200):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)/200,y=self.TargetCoord.y+math.random(-100,100)/200,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.2) else self:F({"Target is not alive",self.Target:IsAlive()}) end end return self end function SPOT:onafterLaseOff(From,Event,To) self:T({From,Event,To}) self:T2({"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 function SPOT:SetRelativeStartPosition(position) self.relstartpos=position or{x=0,y=2,z=0} return self end end MARKEROPS_BASE={ ClassName="MARKEROPS", Tag="mytag", Keywords={}, version="0.1.3", debug=false, Casesensitive=true, } function MARKEROPS_BASE:New(Tagname,Keywords,Casesensitive) local self=BASE:Inherit(self,FSM:New()) self.lid=string.format("MARKEROPS_BASE %s | ",tostring(self.version)) self.Tag=Tagname or"mytag" self.Keywords=Keywords or{} self.debug=false self.Casesensitive=true if Casesensitive and Casesensitive==false then self.Casesensitive=false end self:SetStartState("Stopped") self:AddTransition("Stopped","Start","Running") self:AddTransition("*","MarkAdded","*") self:AddTransition("*","MarkChanged","*") self:AddTransition("*","MarkDeleted","*") self:AddTransition("Running","Stop","Stopped") self:HandleEvent(EVENTS.MarkAdded,self.OnEventMark) self:HandleEvent(EVENTS.MarkChange,self.OnEventMark) self:HandleEvent(EVENTS.MarkRemoved,self.OnEventMark) self:I(self.lid..string.format("started for %s",self.Tag)) self:__Start(1) return self end function MARKEROPS_BASE:OnEventMark(Event) self:T({Event}) if Event==nil or Event.idx==nil then self:E("Skipping onEvent. Event or Event.idx unknown.") return true end local vec3={y=Event.pos.y,x=Event.pos.x,z=Event.pos.z} local coord=COORDINATE:NewFromVec3(vec3) if self.debug then local coordtext=coord:ToStringLLDDM() local text=tostring(Event.text) local m=MESSAGE:New(string.format("Mark added at %s with text: %s",coordtext,text),10,"Info",false):ToAll() end local coalition=Event.MarkCoalition if Event.id==world.event.S_EVENT_MARK_ADDED then self:T({event="S_EVENT_MARK_ADDED",carrier=Event.IniGroupName,vec3=Event.pos}) local Eventtext=tostring(Event.text) if Eventtext~=nil then if self:_MatchTag(Eventtext)then local matchtable=self:_MatchKeywords(Eventtext) self:MarkAdded(Eventtext,matchtable,coord,Event.idx,coalition,Event.PlayerName,Event) end end elseif Event.id==world.event.S_EVENT_MARK_CHANGE then self:T({event="S_EVENT_MARK_CHANGE",carrier=Event.IniGroupName,vec3=Event.pos}) local Eventtext=tostring(Event.text) if Eventtext~=nil then if self:_MatchTag(Eventtext)then local matchtable=self:_MatchKeywords(Eventtext) self:MarkChanged(Eventtext,matchtable,coord,Event.idx,coalition,Event.PlayerName,Event) end end elseif Event.id==world.event.S_EVENT_MARK_REMOVED then self:T({event="S_EVENT_MARK_REMOVED",carrier=Event.IniGroupName,vec3=Event.pos}) local Eventtext=tostring(Event.text) if Eventtext~=nil then if self:_MatchTag(Eventtext)then self:MarkDeleted() end end end end function MARKEROPS_BASE:_MatchTag(Eventtext) local matches=false if not self.Casesensitive then local type=string.lower(self.Tag) if string.find(string.lower(Eventtext),type)then matches=true end else local type=self.Tag if string.find(Eventtext,type)then matches=true end end return matches end function MARKEROPS_BASE:_MatchKeywords(Eventtext) local matchtable={} local keytable=self.Keywords for _index,_word in pairs(keytable)do if string.find(string.lower(Eventtext),string.lower(_word))then table.insert(matchtable,_word) end end return matchtable end function MARKEROPS_BASE:onbeforeMarkAdded(From,Event,To,Text,Keywords,Coord,MarkerID,CoalitionNumber) self:T({self.lid,From,Event,To,Text,Keywords,Coord:ToStringLLDDM()}) end function MARKEROPS_BASE:onbeforeMarkChanged(From,Event,To,Text,Keywords,Coord,MarkerID,CoalitionNumber) self:T({self.lid,From,Event,To,Text,Keywords,Coord:ToStringLLDDM()}) end function MARKEROPS_BASE:onbeforeMarkDeleted(From,Event,To) self:T({self.lid,From,Event,To}) end function MARKEROPS_BASE:onenterStopped(From,Event,To) self:T({self.lid,From,Event,To}) self:UnHandleEvent(EVENTS.MarkAdded) self:UnHandleEvent(EVENTS.MarkChange) self:UnHandleEvent(EVENTS.MarkRemoved) end TEXTANDSOUND={ ClassName="TEXTANDSOUND", version="0.0.1", lid="", locale="en", entries={}, textclass="", } function TEXTANDSOUND:New(ClassName,Defaultlocale) local self=BASE:Inherit(self,BASE:New()) self.lid=string.format("%s (%s) | ",self.ClassName,self.version) self.locale=Defaultlocale or(_SETTINGS:GetLocale()or"en") self.textclass=ClassName or"none" self.entries={} local initentry={} initentry.Classname=ClassName initentry.Data={} initentry.Locale=self.locale self.entries[self.locale]=initentry self:I(self.lid.."Instantiated.") self:T({self.entries[self.locale]}) return self end function TEXTANDSOUND:AddEntry(Locale,ID,Text,Soundfile,Soundlength,Subtitle) self:T(self.lid.."AddEntry") local locale=Locale or self.locale local dataentry={} dataentry.ID=ID or"1" dataentry.Text=Text or"none" dataentry.Soundfile=Soundfile dataentry.Soundlength=Soundlength or 0 dataentry.Subtitle=Subtitle if not self.entries[locale]then local initentry={} initentry.Classname=self.textclass initentry.Data={} initentry.Locale=locale self.entries[locale]=initentry end self.entries[locale].Data[ID]=dataentry self:T({self.entries[locale].Data}) return self end function TEXTANDSOUND:GetEntry(ID,Locale) self:T(self.lid.."GetEntry") local locale=Locale or self.locale if not self.entries[locale]then locale=self.locale end local Text,Soundfile,Soundlength,Subtitle=nil,nil,0,nil if self.entries[locale]then if self.entries[locale].Data then local data=self.entries[locale].Data[ID] if data then Text=data.Text Soundfile=data.Soundfile Soundlength=data.Soundlength Subtitle=data.Subtitle elseif self.entries[self.locale].Data[ID]then local data=self.entries[self.locale].Data[ID] Text=data.Text Soundfile=data.Soundfile Soundlength=data.Soundlength Subtitle=data.Subtitle end end else return nil,nil,0,nil end return Text,Soundfile,Soundlength,Subtitle end function TEXTANDSOUND:GetDefaultLocale() self:T(self.lid.."GetDefaultLocale") return self.locale end function TEXTANDSOUND:SetDefaultLocale(locale) self:T(self.lid.."SetDefaultLocale") self.locale=locale or"en" return self end function TEXTANDSOUND:HasLocale(Locale) self:T(self.lid.."HasLocale") return self.entries[Locale]and true or false end function TEXTANDSOUND:FlushToLog() self:I(self.lid.."Flushing entries:") local text=string.format("Textclass: %s | Default Locale: %s",self.textclass,self.locale) for _,_entry in pairs(self.entries)do local entry=_entry local text=string.format("Textclassname: %s | Locale: %s",entry.Classname,entry.Locale) self:I(text) for _ID,_data in pairs(entry.Data)do local data=_data local text=string.format("ID: %s\nText: %s\nSoundfile: %s With length: %d\nSubtitle: %s",tostring(_ID),data.Text or"none",data.Soundfile or"none",data.Soundlength or 0,data.Subtitle or"none") self:I(text) end end return self end PATHLINE={ ClassName="PATHLINE", lid=nil, points={}, } PATHLINE.version="0.1.1" function PATHLINE:New(Name) local self=BASE:Inherit(self,BASE:New()) self.name=Name or"Unknown Path" self.lid=string.format("PATHLINE %s | ",Name) return self end function PATHLINE:NewFromVec2Array(Name,Vec2Array) local self=PATHLINE:New(Name) for i=1,#Vec2Array do self:AddPointFromVec2(Vec2Array[i]) end return self end function PATHLINE:NewFromVec3Array(Name,Vec3Array) local self=PATHLINE:New(Name) for i=1,#Vec3Array do self:AddPointFromVec3(Vec3Array[i]) end return self end function PATHLINE:FindByName(Name) local pathline=_DATABASE:FindPathline(Name) return pathline end function PATHLINE:AddPointFromVec2(Vec2) if Vec2 then local point=self:_CreatePoint(Vec2) table.insert(self.points,point) end return self end function PATHLINE:AddPointFromVec3(Vec3) if Vec3 then local point=self:_CreatePoint(Vec3) table.insert(self.points,point) end return self end function PATHLINE:GetName() return self.name end function PATHLINE:GetNumberOfPoints() local N=#self.points return N end function PATHLINE:GetPoints() return self.points end function PATHLINE:GetPoints3D() local vecs={} for _,_point in pairs(self.points)do local point=_point table.insert(vecs,point.vec3) end return vecs end function PATHLINE:GetPoints2D() local vecs={} for _,_point in pairs(self.points)do local point=_point table.insert(vecs,point.vec2) end return vecs end function PATHLINE:GetCoordinates() local vecs={} for _,_point in pairs(self.points)do local point=_point local coord=COORDINATE:NewFromVec3(point.vec3) table.insert(vecs,coord) end return vecs end function PATHLINE:GetPointFromIndex(n) local N=self:GetNumberOfPoints() n=n or 1 local point=nil if n>=1 and n<=N then point=self.points[n] else self:E(self.lid..string.format("ERROR: No point in pathline for N=%s",tostring(n))) end return point end function PATHLINE:GetPoint3DFromIndex(n) local point=self:GetPointFromIndex(n) if point then return point.vec3 end return nil end function PATHLINE:GetPoint2DFromIndex(n) local point=self:GetPointFromIndex(n) if point then return point.vec2 end return nil end function PATHLINE:MarkPoints(Switch) for i,_point in pairs(self.points)do local point=_point if Switch==false then if point.markerID then UTILS.RemoveMark(point.markerID,Delay) end else if point.markerID then UTILS.RemoveMark(point.markerID) end point.markerID=UTILS.GetMarkID() local text=string.format("Pathline %s: Point #%d\nSurface Type=%d\nHeight=%.1f m\nDepth=%.1f m",self.name,i,point.surfaceType,point.landHeight,point.depth) trigger.action.markToAll(point.markerID,text,point.vec3,"") end end end function PATHLINE:_CreatePoint(Vec) local point={} if Vec.z then point.vec3=UTILS.DeepCopy(Vec) point.vec2={x=Vec.x,y=Vec.z} else point.vec2=UTILS.DeepCopy(Vec) point.vec3={x=Vec.x,y=land.getHeight(Vec),z=Vec.y} end point.surfaceType=land.getSurfaceType(point.vec2) point.landHeight,point.depth=land.getSurfaceHeightWithSeabed(point.vec2) point.markerID=nil return point end OBJECT={ ClassName="OBJECT", ObjectName="", } function OBJECT:New(ObjectName) local self=BASE:Inherit(self,BASE:New()) self:F2(ObjectName) self.ObjectName=ObjectName return self end function OBJECT:GetID() local DCSObject=self:GetDCSObject() if DCSObject then local ObjectID=DCSObject:getID() return ObjectID end self:E({"Cannot GetID",Name=self.ObjectName,Class=self:GetClassName()}) return nil end function OBJECT:Destroy() local DCSObject=self:GetDCSObject() if DCSObject then DCSObject:destroy(false) return true end self:E({"Cannot Destroy",Name=self.ObjectName,Class=self:GetClassName()}) return nil end IDENTIFIABLE={ ClassName="IDENTIFIABLE", IdentifiableName="", } local _CategoryName={ [Unit.Category.AIRPLANE]="Airplane", [Unit.Category.HELICOPTER]="Helicopter", [Unit.Category.GROUND_UNIT]="Ground Identifiable", [Unit.Category.SHIP]="Ship", [Unit.Category.STRUCTURE]="Structure", } function IDENTIFIABLE:New(IdentifiableName) local self=BASE:Inherit(self,OBJECT:New(IdentifiableName)) self:F2(IdentifiableName) self.IdentifiableName=IdentifiableName return self end function IDENTIFIABLE:IsAlive() self:F3(self.IdentifiableName) local DCSIdentifiable=self:GetDCSObject() if DCSIdentifiable then local IdentifiableIsAlive=DCSIdentifiable:isExist() return IdentifiableIsAlive end return false end function IDENTIFIABLE:GetName() local IdentifiableName=self.IdentifiableName return IdentifiableName end function IDENTIFIABLE:GetTypeName() self:F2(self.IdentifiableName) local DCSIdentifiable=self:GetDCSObject() if DCSIdentifiable then local IdentifiableTypeName=DCSIdentifiable:getTypeName() self:T3(IdentifiableTypeName) return IdentifiableTypeName end self:F(self.ClassName.." "..self.IdentifiableName.." not found!") return nil end function IDENTIFIABLE:GetCategory() self:F2(self.ObjectName) local DCSObject=self:GetDCSObject() if DCSObject then local ObjectCategory,UnitCategory=DCSObject:getCategory() self:T3(ObjectCategory) return ObjectCategory,UnitCategory end return nil,nil end function IDENTIFIABLE:GetCategoryName() local DCSIdentifiable=self:GetDCSObject() if DCSIdentifiable then local IdentifiableCategoryName=_CategoryName[self:GetDesc().category] return IdentifiableCategoryName end self:F(self.ClassName.." "..self.IdentifiableName.." not found!") return nil end function IDENTIFIABLE:GetCoalition() self:F2(self.IdentifiableName) local DCSIdentifiable=self:GetDCSObject() if DCSIdentifiable then local IdentifiableCoalition=DCSIdentifiable:getCoalition() self:T3(IdentifiableCoalition) return IdentifiableCoalition end self:F(self.ClassName.." "..self.IdentifiableName.." not found!") return nil end function IDENTIFIABLE:GetCoalitionName() self:F2(self.IdentifiableName) local DCSIdentifiable=self:GetDCSObject() if DCSIdentifiable then local IdentifiableCoalition=DCSIdentifiable:getCoalition() self:T3(IdentifiableCoalition) return UTILS.GetCoalitionName(IdentifiableCoalition) end self:F(self.ClassName.." "..self.IdentifiableName.." not found!") return nil end function IDENTIFIABLE:GetCountry() self:F2(self.IdentifiableName) local DCSIdentifiable=self:GetDCSObject() if DCSIdentifiable then local IdentifiableCountry=DCSIdentifiable:getCountry() self:T3(IdentifiableCountry) return IdentifiableCountry end self:F(self.ClassName.." "..self.IdentifiableName.." not found!") return nil end function IDENTIFIABLE:GetCountryName() self:F2(self.IdentifiableName) local countryid=self:GetCountry() for name,id in pairs(country.id)do if countryid==id then return name end end end function IDENTIFIABLE:GetDesc() self:F2(self.IdentifiableName) local DCSIdentifiable=self:GetDCSObject() if DCSIdentifiable then local IdentifiableDesc=DCSIdentifiable:getDesc() self:T2(IdentifiableDesc) return IdentifiableDesc end self:F(self.ClassName.." "..self.IdentifiableName.." not found!") return nil end function IDENTIFIABLE:HasAttribute(AttributeName) self:F2(self.IdentifiableName) local DCSIdentifiable=self:GetDCSObject() if DCSIdentifiable then local IdentifiableHasAttribute=DCSIdentifiable:hasAttribute(AttributeName) self:T2(IdentifiableHasAttribute) return IdentifiableHasAttribute end self:F(self.ClassName.." "..self.IdentifiableName.." not found!") return nil end function IDENTIFIABLE:GetCallsign() return'' end function IDENTIFIABLE:GetThreatLevel() return 0,"Scenery" end POSITIONABLE={ ClassName="POSITIONABLE", PositionableName="", coordinate=nil, pointvec3=nil, } POSITIONABLE.__={} POSITIONABLE.__.Cargo={} function POSITIONABLE:New(PositionableName) local self=BASE:Inherit(self,IDENTIFIABLE:New(PositionableName)) self.PositionableName=PositionableName return self end function POSITIONABLE:Destroy(GenerateEvent) self:F2(self.ObjectName) local DCSObject=self:GetDCSObject() if DCSObject then local UnitGroup=self:GetGroup() local UnitGroupName=UnitGroup:GetName() self:F({UnitGroupName=UnitGroupName}) 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 USERFLAG:New(UnitGroupName):Set(100) DCSObject:destroy() end return nil end function POSITIONABLE:GetDCSObject() return nil end function POSITIONABLE:GetPosition() self:F2(self.PositionableName) local DCSPositionable=self:GetDCSObject() if self:IsInstanceOf("GROUP")then DCSPositionable=self:GetFirstUnitAlive():GetDCSObject() end if DCSPositionable then local PositionablePosition=DCSPositionable:getPosition() self:T3(PositionablePosition) return PositionablePosition end BASE:E({"Cannot GetPosition",Positionable=self,Alive=self:IsAlive()}) return nil end function POSITIONABLE:GetOrientation() local position=self:GetPosition() if position then return position.x,position.y,position.z else BASE:E({"Cannot GetOrientation",Positionable=self,Alive=self:IsAlive()}) return nil,nil,nil end end function POSITIONABLE:GetOrientationX() local position=self:GetPosition() if position then return position.x else BASE:E({"Cannot GetOrientationX",Positionable=self,Alive=self:IsAlive()}) return nil end end function POSITIONABLE:GetOrientationY() local position=self:GetPosition() if position then return position.y else BASE:E({"Cannot GetOrientationY",Positionable=self,Alive=self:IsAlive()}) return nil end end function POSITIONABLE:GetOrientationZ() local position=self:GetPosition() if position then return position.z else BASE:E({"Cannot GetOrientationZ",Positionable=self,Alive=self:IsAlive()}) return nil end end function POSITIONABLE:GetPositionVec3() self:F2(self.PositionableName) local DCSPositionable=self:GetDCSObject() if DCSPositionable then local PositionablePosition=DCSPositionable:getPosition().p self:T3(PositionablePosition) return PositionablePosition end BASE:E({"Cannot GetPositionVec3",Positionable=self,Alive=self:IsAlive()}) return nil end function POSITIONABLE:GetVec3() local DCSPositionable=self:GetDCSObject() if DCSPositionable then local vec3=DCSPositionable:getPoint() return vec3 end self:E({"Cannot get the Positionable DCS Object for GetVec3",Positionable=self,Alive=self:IsAlive()}) return nil end function POSITIONABLE:GetVec2() local DCSPositionable=self:GetDCSObject() if DCSPositionable then local Vec3=DCSPositionable:getPoint() return{x=Vec3.x,y=Vec3.z} end self:E({"Cannot GetVec2",Positionable=self,Alive=self:IsAlive()}) return nil end function POSITIONABLE:GetPointVec2() self:F2(self.PositionableName) local DCSPositionable=self:GetDCSObject() if DCSPositionable then local PositionableVec3=DCSPositionable:getPosition().p local PositionablePointVec2=COORDINATE:NewFromVec3(PositionableVec3) return PositionablePointVec2 end self:E({"Cannot Coordinate",Positionable=self,Alive=self:IsAlive()}) return nil end function POSITIONABLE:GetPointVec3() local DCSPositionable=self:GetDCSObject() if DCSPositionable then local PositionableVec3=self:GetPositionVec3() if false and self.pointvec3 then self.pointvec3.x=PositionableVec3.x self.pointvec3.y=PositionableVec3.y self.pointvec3.z=PositionableVec3.z else self.pointvec3=COORDINATE:NewFromVec3(PositionableVec3) end return self.pointvec3 end BASE:E({"Cannot GetPointVec3",Positionable=self,Alive=self:IsAlive()}) return nil end function POSITIONABLE:GetCoord() local DCSPositionable=self:GetDCSObject() if DCSPositionable then local PositionableVec3=self:GetVec3() if self.coordinate then self.coordinate:UpdateFromVec3(PositionableVec3) else self.coordinate=COORDINATE:NewFromVec3(PositionableVec3) end return self.coordinate end BASE:E({"Cannot GetCoordinate",Positionable=self,Alive=self:IsAlive()}) return nil end function POSITIONABLE:GetCoordinate() local DCSPositionable=self:GetDCSObject() if DCSPositionable then local PositionableVec3=self:GetVec3() local coord=COORDINATE:NewFromVec3(PositionableVec3) local heading=self:GetHeading() coord.Heading=heading return coord end self:E({"Cannot GetCoordinate",Positionable=self,Alive=self:IsAlive()}) return nil end function POSITIONABLE:Explode(power,delay) power=power or 100 if delay and delay>0 then self:ScheduleOnce(delay,POSITIONABLE.Explode,self,power,0) else local coord=self:GetCoord() if coord then coord:Explosion(power) end end return self end function POSITIONABLE:GetOffsetCoordinate(x,y,z) x=x or 0 y=y or 0 z=z or 0 local X=self:GetOrientationX() local Y=self:GetOrientationY() local Z=self:GetOrientationZ() local A={x=x,y=y,z=z} local x={x=X.x*A.x,y=X.y*A.x,z=X.z*A.x} local y={x=Y.x*A.y,y=Y.y*A.y,z=Y.z*A.y} local z={x=Z.x*A.z,y=Z.y*A.z,z=Z.z*A.z} local a={x=x.x+y.x+z.x,y=x.y+y.y+z.y,z=x.z+y.z+z.z} local u=self:GetVec3() local v={x=a.x+u.x,y=a.y+u.y,z=a.z+u.z} local coord=COORDINATE:NewFromVec3(v) return coord end function POSITIONABLE:GetRelativeCoordinate(x,y,z) x=x or 0 y=y or 0 z=z or 0 local selfPos=self:GetVec3() local X=self:GetOrientationX() local Y=self:GetOrientationY() local Z=self:GetOrientationZ() local off={ x=x-selfPos.x, y=y-selfPos.y, z=z-selfPos.z } local res={x=0,y=0,z=0} local mat={ {X.x,Y.x,Z.x,off.x}, {X.y,Y.y,Z.y,off.y}, {X.z,Y.z,Z.z,off.z} } local m=3 local n=4 local h=1 local k=1 while h<=m and k<=n do local v_max=math.abs(mat[h][k]) local i_max=h for i=h,m,1 do local value=math.abs(mat[i][k]) if value>v_max then i_max=i v_max=value end end if mat[i_max][k]==0 then k=k+1 else local tmp=mat[h] mat[h]=mat[i_max] mat[i_max]=tmp for i=h+1,m,1 do local f=mat[i][k]/mat[h][k] mat[i][k]=0 for j=k+1,n,1 do mat[i][j]=mat[i][j]-f*mat[h][j] end end h=h+1 k=k+1 end end res.z=mat[3][4]/mat[3][3] res.y=(mat[2][4]-res.z*mat[2][3])/mat[2][2] res.x=(mat[1][4]-res.y*mat[1][2]-res.z*mat[1][3])/mat[1][1] local coord=COORDINATE:NewFromVec3(res) return coord end function POSITIONABLE:GetRandomVec3(Radius) self:F2(self.PositionableName) local DCSPositionable=self:GetDCSObject() if DCSPositionable then local PositionablePointVec3=DCSPositionable:getPosition().p if Radius then local PositionableRandomVec3={} local angle=math.random()*math.pi*2 PositionableRandomVec3.x=PositionablePointVec3.x+math.cos(angle)*math.random()*Radius PositionableRandomVec3.y=PositionablePointVec3.y PositionableRandomVec3.z=PositionablePointVec3.z+math.sin(angle)*math.random()*Radius self:T3(PositionableRandomVec3) return PositionableRandomVec3 else self:F("Radius is nil, returning the PointVec3 of the POSITIONABLE",PositionablePointVec3) return PositionablePointVec3 end end BASE:E({"Cannot GetRandomVec3",Positionable=self,Alive=self:IsAlive()}) return nil end function POSITIONABLE:GetBoundingBox() self:F2() local DCSPositionable=self:GetDCSObject() if DCSPositionable then local PositionableDesc=DCSPositionable:getDesc() if PositionableDesc then local PositionableBox=PositionableDesc.box return PositionableBox end end BASE:E({"Cannot GetBoundingBox",Positionable=self,Alive=self:IsAlive()}) return nil end function POSITIONABLE:GetObjectSize() local box=self:GetBoundingBox() if box then local x=box.max.x+math.abs(box.min.x) local y=box.max.y+math.abs(box.min.y) local z=box.max.z+math.abs(box.min.z) return math.max(x,z),x,y,z end return 0,0,0,0 end function POSITIONABLE:GetBoundingRadius(MinDist) self:F2() local Box=self:GetBoundingBox() local boxmin=MinDist or 0 if Box then local X=Box.max.x-Box.min.x local Z=Box.max.z-Box.min.z local CX=X/2 local CZ=Z/2 return math.max(math.max(CX,CZ),boxmin) end BASE:T({"Cannot GetBoundingRadius",Positionable=self,Alive=self:IsAlive()}) return nil end function POSITIONABLE:GetAltitude() self:F2() local DCSPositionable=self:GetDCSObject() if DCSPositionable then local PositionablePointVec3=DCSPositionable:getPoint() return PositionablePointVec3.y end BASE:E({"Cannot GetAltitude",Positionable=self,Alive=self:IsAlive()}) return nil end function POSITIONABLE:IsAboveRunway() self:F2(self.PositionableName) local DCSPositionable=self:GetDCSObject() if DCSPositionable then local Vec2=self:GetVec2() local SurfaceType=land.getSurfaceType(Vec2) local IsAboveRunway=SurfaceType==land.SurfaceType.RUNWAY self:T2(IsAboveRunway) return IsAboveRunway end BASE:E({"Cannot IsAboveRunway",Positionable=self,Alive=self:IsAlive()}) return nil end function POSITIONABLE:GetSize() local DCSObject=self:GetDCSObject() if DCSObject then return 1 else return 0 end end function POSITIONABLE:GetHeading() local DCSPositionable=self:GetDCSObject() if DCSPositionable then local PositionablePosition=DCSPositionable:getPosition() if PositionablePosition then local PositionableHeading=math.atan2(PositionablePosition.x.z,PositionablePosition.x.x) if PositionableHeading<0 then PositionableHeading=PositionableHeading+2*math.pi end PositionableHeading=PositionableHeading*180/math.pi return PositionableHeading end end self:E({"Cannot GetHeading",Positionable=self,Alive=self:IsAlive()}) return nil end function POSITIONABLE:IsAir() self:F2() local DCSUnit=self:GetDCSObject() if DCSUnit then local UnitDescriptor=DCSUnit:getDesc() self:T3({UnitDescriptor.category,Unit.Category.AIRPLANE,Unit.Category.HELICOPTER}) local IsAirResult=(UnitDescriptor.category==Unit.Category.AIRPLANE)or(UnitDescriptor.category==Unit.Category.HELICOPTER) self:T3(IsAirResult) return IsAirResult end self:E({"Cannot check IsAir",Positionable=self,Alive=self:IsAlive()}) return nil end function POSITIONABLE:IsGround() self:F2() local DCSUnit=self:GetDCSObject() if DCSUnit then local UnitDescriptor=DCSUnit:getDesc() self:T3({UnitDescriptor.category,Unit.Category.GROUND_UNIT}) local IsGroundResult=(UnitDescriptor.category==Unit.Category.GROUND_UNIT) self:T3(IsGroundResult) return IsGroundResult end self:E({"Cannot check IsGround",Positionable=self,Alive=self:IsAlive()}) return nil end function POSITIONABLE: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 self:E({"Cannot check IsShip",Positionable=self,Alive=self:IsAlive()}) return nil end function POSITIONABLE:IsSubmarine() self:F2() local DCSUnit=self:GetDCSObject() if DCSUnit then local UnitDescriptor=DCSUnit:getDesc() if UnitDescriptor.attributes["Submarines"]==true then return true else return false end end self:E({"Cannot check IsSubmarine",Positionable=self,Alive=self:IsAlive()}) return nil end function POSITIONABLE:InAir() self:F2(self.PositionableName) return nil end function POSITIONABLE:GetVelocity() self:F2(self.PositionableName) local DCSPositionable=self:GetDCSObject() if DCSPositionable then local Velocity=VELOCITY:New(self) return Velocity end BASE:E({"Cannot GetVelocity",Positionable=self,Alive=self:IsAlive()}) return nil end function POSITIONABLE:GetVelocityVec3() self:F2(self.PositionableName) local DCSPositionable=self:GetDCSObject() if DCSPositionable and DCSPositionable:isExist()then local PositionableVelocityVec3=DCSPositionable:getVelocity() self:T3(PositionableVelocityVec3) return PositionableVelocityVec3 end BASE:E({"Cannot GetVelocityVec3",Positionable=self,Alive=self:IsAlive()}) return nil end function POSITIONABLE:GetRelativeVelocity(Positionable) self:F2(self.PositionableName) local v1=self:GetVelocityVec3() local v2=Positionable:GetVelocityVec3() local vtot=UTILS.VecAdd(v1,v2) return UTILS.VecNorm(vtot) end function POSITIONABLE:GetHeight() self:F2(self.PositionableName) local DCSPositionable=self:GetDCSObject() if DCSPositionable and DCSPositionable:isExist()then local PositionablePosition=DCSPositionable:getPosition() if PositionablePosition then local PositionableHeight=PositionablePosition.p.y self:T2(PositionableHeight) return PositionableHeight end end return nil end function POSITIONABLE:GetVelocityKMH() self:F2(self.PositionableName) local DCSPositionable=self:GetDCSObject() if DCSPositionable and DCSPositionable:isExist()then local VelocityVec3=self:GetVelocityVec3() local Velocity=(VelocityVec3.x^2+VelocityVec3.y^2+VelocityVec3.z^2)^0.5 local Velocity=Velocity*3.6 self:T3(Velocity) return Velocity end return 0 end function POSITIONABLE:GetVelocityMPS() self:F2(self.PositionableName) local DCSPositionable=self:GetDCSObject() if DCSPositionable and DCSPositionable:isExist()then local VelocityVec3=self:GetVelocityVec3() local Velocity=(VelocityVec3.x^2+VelocityVec3.y^2+VelocityVec3.z^2)^0.5 self:T3(Velocity) return Velocity end return 0 end function POSITIONABLE:GetVelocityKNOTS() self:F2(self.PositionableName) return UTILS.MpsToKnots(self:GetVelocityMPS()) end function POSITIONABLE:GetAirspeedTrue() local tas=0 local coord=self:GetCoord() if coord then local alt=coord.y local wvec3=coord:GetWindVec3(alt,false) local vvec3=self:GetVelocityVec3() local tasvec3=UTILS.VecSubstract(vvec3,wvec3) tas=UTILS.VecNorm(tasvec3) end return tas end function POSITIONABLE:GetAirspeedIndicated(oatcorr) local tas=self:GetAirspeedTrue() local altitude=self:GetAltitude() local ias=UTILS.TasToIas(tas,altitude,oatcorr) return ias end function POSITIONABLE:GetGroundSpeed() local gs=0 local vel=self:GetVelocityVec3() if vel then local vec2={x=vel.x,y=vel.z} gs=UTILS.Vec2Norm(vel) end return gs end function POSITIONABLE:GetAoA() local unitpos=self:GetPosition() if unitpos then local unitvel=self:GetVelocityVec3() if unitvel and UTILS.VecNorm(unitvel)~=0 then local wind=self:GetCoordinate():GetWindWithTurbulenceVec3() unitvel.x=unitvel.x-wind.x unitvel.y=unitvel.y-wind.y unitvel.z=unitvel.z-wind.z local AxialVel={} AxialVel.x=UTILS.VecDot(unitpos.x,unitvel) AxialVel.y=UTILS.VecDot(unitpos.y,unitvel) AxialVel.z=UTILS.VecDot(unitpos.z,unitvel) local AoA=math.acos(UTILS.VecDot({x=1,y=0,z=0},{x=AxialVel.x,y=AxialVel.y,z=0})/UTILS.VecNorm({x=AxialVel.x,y=AxialVel.y,z=0})) if AxialVel.y>0 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 return nil 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 return nil 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:MessageToUnit(Message,Duration,MessageUnit,Name) self:F2({Message,Duration}) local DCSObject=self:GetDCSObject() if DCSObject then if DCSObject:isExist()then if MessageUnit:IsAlive()then self:GetMessage(Message,Duration,Name):ToUnit(MessageUnit) else BASE:E({"Message not sent to Unit; Unit is not alive...",Message=Message,MessageUnit=MessageUnit}) end else BASE:E({"Message not sent to Unit; Positionable is not alive ...",Message=Message,Positionable=self,MessageUnit=MessageUnit}) end end 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:MessageToUnit(Message,Duration,MessageUnit,Name) self:F2({Message,Duration}) local DCSObject=self:GetDCSObject() if DCSObject then if DCSObject:isExist()then if MessageUnit:IsAlive()then self:GetMessage(Message,Duration,Name):ToUnit(MessageUnit) else BASE:E({"Message not sent to Unit; Unit is not alive...",Message=Message,MessageUnit=MessageUnit}) end else BASE:E({"Message not sent to Unit; Positionable is not alive ...",Message=Message,Positionable=self,MessageUnit=MessageUnit}) 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:MessageToSetUnit(Message,Duration,MessageSetUnit,Name) self:F2({Message,Duration}) local DCSObject=self:GetDCSObject() if DCSObject then if DCSObject:isExist()then MessageSetUnit:ForEachUnit( function(MessageGroup) self:GetMessage(Message,Duration,Name):ToUnit(MessageGroup) end ) end end return nil end function POSITIONABLE:MessageToSetUnit(Message,Duration,MessageSetUnit,Name) self:F2({Message,Duration}) local DCSObject=self:GetDCSObject() if DCSObject then if DCSObject:isExist()then MessageSetUnit:ForEachUnit( function(MessageGroup) self:GetMessage(Message,Duration,Name):ToUnit(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("building 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:GetTroopCapacity() local DCSunit=self:GetDCSObject() local capacity=DCSunit:getDescentCapacity() return capacity 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 POSITIONABLE.DefaultInfantryWeight=95 POSITIONABLE.CargoBayCapacityValues={ ["Air"]={ ["C_130"]=70000, }, ["Naval"]={ ["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, ["speedboat"]=500, ["Seawise_Giant"]=261000000, }, ["Ground"]={ ["AAV7"]=25*POSITIONABLE.DefaultInfantryWeight, ["Bedford_MWD"]=8*POSITIONABLE.DefaultInfantryWeight, ["Blitz_36_6700A"]=10*POSITIONABLE.DefaultInfantryWeight, ["BMD_1"]=9*POSITIONABLE.DefaultInfantryWeight, ["BMP_1"]=8*POSITIONABLE.DefaultInfantryWeight, ["BMP_2"]=7*POSITIONABLE.DefaultInfantryWeight, ["BMP_3"]=8*POSITIONABLE.DefaultInfantryWeight, ["Boman"]=25*POSITIONABLE.DefaultInfantryWeight, ["BTR_80"]=9*POSITIONABLE.DefaultInfantryWeight, ["BTR_82A"]=9*POSITIONABLE.DefaultInfantryWeight, ["BTR_D"]=12*POSITIONABLE.DefaultInfantryWeight, ["Cobra"]=8*POSITIONABLE.DefaultInfantryWeight, ["Land_Rover_101_FC"]=11*POSITIONABLE.DefaultInfantryWeight, ["Land_Rover_109_S3"]=7*POSITIONABLE.DefaultInfantryWeight, ["LAV_25"]=6*POSITIONABLE.DefaultInfantryWeight, ["M_2_Bradley"]=6*POSITIONABLE.DefaultInfantryWeight, ["M1043_HMMWV_Armament"]=4*POSITIONABLE.DefaultInfantryWeight, ["M1045_HMMWV_TOW"]=4*POSITIONABLE.DefaultInfantryWeight, ["M1126_Stryker_ICV"]=9*POSITIONABLE.DefaultInfantryWeight, ["M1134_Stryker_ATGM"]=9*POSITIONABLE.DefaultInfantryWeight, ["M2A1_halftrack"]=9*POSITIONABLE.DefaultInfantryWeight, ["M_113"]=9*POSITIONABLE.DefaultInfantryWeight, ["Marder"]=6*POSITIONABLE.DefaultInfantryWeight, ["MCV_80"]=9*POSITIONABLE.DefaultInfantryWeight, ["MLRS_FDDM"]=4*POSITIONABLE.DefaultInfantryWeight, ["MTLB"]=25*POSITIONABLE.DefaultInfantryWeight, ["GAZ_66"]=8*POSITIONABLE.DefaultInfantryWeight, ["GAZ_3307"]=12*POSITIONABLE.DefaultInfantryWeight, ["GAZ_3308"]=14*POSITIONABLE.DefaultInfantryWeight, ["Grad_FDDM"]=6*POSITIONABLE.DefaultInfantryWeight, ["KAMAZ_Truck"]=12*POSITIONABLE.DefaultInfantryWeight, ["KrAZ6322"]=12*POSITIONABLE.DefaultInfantryWeight, ["M_818"]=12*POSITIONABLE.DefaultInfantryWeight, ["Tigr_233036"]=6*POSITIONABLE.DefaultInfantryWeight, ["TPZ"]=10*POSITIONABLE.DefaultInfantryWeight, ["UAZ_469"]=4*POSITIONABLE.DefaultInfantryWeight, ["Ural_375"]=12*POSITIONABLE.DefaultInfantryWeight, ["Ural_4320_31"]=14*POSITIONABLE.DefaultInfantryWeight, ["Ural_4320_APA_5D"]=10*POSITIONABLE.DefaultInfantryWeight, ["Ural_4320T"]=14*POSITIONABLE.DefaultInfantryWeight, ["ZBD04A"]=7*POSITIONABLE.DefaultInfantryWeight, ["VAB_Mephisto"]=8*POSITIONABLE.DefaultInfantryWeight, ["tt_KORD"]=6*POSITIONABLE.DefaultInfantryWeight, ["tt_DSHK"]=6*POSITIONABLE.DefaultInfantryWeight, ["HL_KORD"]=6*POSITIONABLE.DefaultInfantryWeight, ["HL_DSHK"]=6*POSITIONABLE.DefaultInfantryWeight, ["CCKW_353"]=16*POSITIONABLE.DefaultInfantryWeight, ["MaxxPro_MRAP"]=7*POSITIONABLE.DefaultInfantryWeight, } } function POSITIONABLE:SetCargoBayWeightLimit(WeightLimit) if WeightLimit then self.__.CargoBayWeightLimit=WeightLimit elseif self.__.CargoBayWeightLimit~=nil then else local Desc=self:GetDesc() self:F({Desc=Desc}) local TypeName=Desc.typeName or"Unknown Type" TypeName=string.gsub(TypeName,"[%p%s]","_") if self:IsAir()then local Weights=POSITIONABLE.CargoBayCapacityValues.Air local massMax=Desc.massMax or 0 local maxTakeoff=Weights[TypeName] if maxTakeoff then massMax=maxTakeoff end local massEmpty=Desc.massEmpty or 0 local massFuelMax=Desc.fuelMassMax or 0 local relFuel=math.min(self:GetFuel()or 1.0,1.0) local massFuel=massFuelMax*relFuel local CargoWeight=massMax-(massEmpty+massFuel) self:T(string.format("Setting Cargo bay weight limit [%s]=%d kg (Mass max=%d, empty=%d, fuelMax=%d kg (rel=%.3f), fuel=%d kg",TypeName,CargoWeight,massMax,massEmpty,massFuelMax,relFuel,massFuel)) self.__.CargoBayWeightLimit=CargoWeight elseif self:IsShip()then local Weights=POSITIONABLE.CargoBayCapacityValues.Naval self.__.CargoBayWeightLimit=(Weights[TypeName]or 50000) else local Weights=POSITIONABLE.CargoBayCapacityValues.Ground local CargoBayWeightLimit=(Weights[TypeName]or 0) self.__.CargoBayWeightLimit=CargoBayWeightLimit end end self:F({CargoBayWeightLimit=self.__.CargoBayWeightLimit}) end function POSITIONABLE:GetCargoBayWeightLimit() if self.__.CargoBayWeightLimit==nil then self:SetCargoBayWeightLimit() end return 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:TaskEmptyTask() local DCSTaskWrappedAction={ ["id"]="WrappedAction", ["params"]={ ["action"]={ ["id"]="Script", ["params"]={ ["command"]="", }, }, }, } 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:CommandActivateACLS(UnitID,Name,Delay) local CommandActivateACLS={ id='ActivateACLS', params={ unitId=UnitID or self:GetID(), name=Name or"ACL", } } self:T({CommandActivateACLS}) if Delay and Delay>0 then SCHEDULER:New(nil,self.CommandActivateACLS,{self,UnitID,Name},Delay) else local controller=self:_GetController() controller:setCommand(CommandActivateACLS) end return self end function CONTROLLABLE:CommandDeactivateACLS(Delay) local CommandDeactivateACLS={ id='DeactivateACLS', params={} } if Delay and Delay>0 then SCHEDULER:New(nil,self.CommandDeactivateACLS,{self},Delay) else local controller=self:_GetController() controller:setCommand(CommandDeactivateACLS) end return self end function CONTROLLABLE:CommandActivateICLS(Channel,UnitID,Callsign,Delay) local CommandActivateICLS={ id="ActivateICLS", params={ ["type"]=BEACON.Type.ICLS, ["channel"]=Channel, ["unitId"]=UnitID or self:GetID(), ["callsign"]=Callsign, }, } if Delay and Delay>0 then SCHEDULER:New(nil,self.CommandActivateICLS,{self,Channel,UnitID,Callsign},Delay) else self:SetCommand(CommandActivateICLS) end return self end function CONTROLLABLE:CommandActivateLink4(Frequency,UnitID,Callsign,Delay) local freq=Frequency or 336 local CommandActivateLink4={ id="ActivateLink4", params={ ["frequency"]=freq*1000000, ["unitId"]=UnitID or self:GetID(), ["name"]=Callsign or"LNK", } } self:T({CommandActivateLink4}) if Delay and Delay>0 then SCHEDULER:New(nil,self.CommandActivateLink4,{self,Frequency,UnitID,Callsign},Delay) else local controller=self:_GetController() controller:setCommand(CommandActivateLink4) end return self end function CONTROLLABLE:CommandDeactivateBeacon(Delay) local CommandDeactivateBeacon={id='DeactivateBeacon',params={}} local CommandDeactivateBeacon={id='DeactivateBeacon',params={}} if Delay and Delay>0 then SCHEDULER:New(nil,self.CommandDeactivateBeacon,{self},Delay) else self:SetCommand(CommandDeactivateBeacon) end return self end function CONTROLLABLE:CommandDeactivateLink4(Delay) local CommandDeactivateLink4={id='DeactivateLink4',params={}} if Delay and Delay>0 then SCHEDULER:New(nil,self.CommandDeactivateLink4,{self},Delay) else local controller=self:_GetController() controller:setCommand(CommandDeactivateLink4) 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:CommandSetUnlimitedFuel(OnOff,Delay) local CommandSetFuel={ id='SetUnlimitedFuel', params={ value=OnOff } } if Delay and Delay>0 then SCHEDULER:New(nil,self.CommandSetUnlimitedFuel,{self,OnOff},Delay) else self:SetCommand(CommandSetFuel) end return self end function CONTROLLABLE:CommandSetFrequency(Frequency,Modulation,Power,Delay) local CommandSetFrequency={ id='SetFrequency', params={ frequency=Frequency*1000000, modulation=Modulation or radio.modulation.AM, power=Power or 10, }, } if Delay and Delay>0 then SCHEDULER:New(nil,self.CommandSetFrequency,{self,Frequency,Modulation,Power},Delay) else self:SetCommand(CommandSetFrequency) end return self end function CONTROLLABLE:CommandSetFrequencyForUnit(Frequency,Modulation,Power,UnitID,Delay) local CommandSetFrequencyForUnit={ id='SetFrequencyForUnit', params={ frequency=Frequency*1000000, modulation=Modulation or radio.modulation.AM, unitId=UnitID or self:GetID(), power=Power or 10, }, } if Delay and Delay>0 then SCHEDULER:New(nil,self.CommandSetFrequencyForUnit,{self,Frequency,Modulation,Power,UnitID},Delay) else self:SetCommand(CommandSetFrequencyForUnit) end return self end function CONTROLLABLE:CommandSmokeOnOff(OnOff,Delay) local switch=(OnOff==nil)and true or OnOff local command={ id='SMOKE_ON_OFF', params={ value=switch } } if Delay and Delay>0 then SCHEDULER:New(nil,self.CommandSmokeOnOff,{self,switch},Delay) else self:SetCommand(command) end return self end function CONTROLLABLE:CommandSmokeON(Delay) local command={ id='SMOKE_ON_OFF', params={ value=true } } if Delay and Delay>0 then SCHEDULER:New(nil,self.CommandSmokeON,{self},Delay) else self:SetCommand(command) end return self end function CONTROLLABLE:CommandSmokeOFF(Delay) local command={ id='SMOKE_ON_OFF', params={ value=false } } if Delay and Delay>0 then SCHEDULER:New(nil,self.CommandSmokeOFF,{self},Delay) else self:SetCommand(command) 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:TaskStrafing(Vec2,AttackQty,Length,WeaponType,WeaponExpend,Direction,GroupAttack) local DCSTask={ id='Strafing', params={ point=Vec2, weaponType=WeaponType or 805337088, expend=WeaponExpend or"Auto", attackQty=AttackQty or 1, attackQtyLimit=AttackQty~=nil and true or false, direction=Direction and math.rad(Direction)or 0, directionEnabled=Direction and true or false, groupAttack=GroupAttack or false, length=Length, } } 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, 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={x=Coord.x,y=Coord.z or Coord.y} local P2=nil if CoordRaceTrack then Pattern=AI.Task.OrbitPattern.RACE_TRACK P2={x=CoordRaceTrack.x,y=CoordRaceTrack.z or CoordRaceTrack.y} 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:TaskRecoveryTanker(CarrierGroup,Speed,Altitude,LastWptNumber) local LastWptFlag=type(LastWptNumber)=="number"and true or false local DCSTask={ id="RecoveryTanker", params={ groupId=CarrierGroup:GetID(), speed=Speed, altitude=Altitude, lastWptIndexFlag=LastWptFlag, lastWptIndex=LastWptNumber } } return DCSTask end function CONTROLLABLE:TaskLandAtVec2(Vec2,Duration,CombatLanding,DirectionAfterLand) local DCSTask={ id='Land', params={ point=Vec2, durationFlag=Duration and true or false, duration=Duration, combatLandingFlag=CombatLanding==true and true or false, }, } if DirectionAfterLand~=nil and type(DirectionAfterLand)=="number"then DCSTask.params.directionEnabled=true DCSTask.params.direction=math.rad(DirectionAfterLand) end return DCSTask end function CONTROLLABLE:TaskLandAtZone(Zone,Duration,RandomPoint,CombatLanding,DirectionAfterLand) local Point=RandomPoint and Zone:GetRandomVec2()or Zone:GetVec2() local DCSTask=CONTROLLABLE.TaskLandAtVec2(self,Point,Duration,CombatLanding,DirectionAfterLand) 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:TaskGroundEscort(FollowControllable,LastWaypointIndex,OrbitDistance,TargetTypes) local DCSTask={ id='GroundEscort', params={ groupId=FollowControllable and FollowControllable:GetID()or nil, engagementDistMax=OrbitDistance or 2000, lastWptIndexFlag=LastWaypointIndex and true or false, lastWptIndex=LastWaypointIndex, targetTypes=TargetTypes or{"Ground vehicles"}, lastWptIndexFlagChangedManually=true, }, } return DCSTask end function CONTROLLABLE:TaskEscort(FollowControllable,Vec3,LastWaypointIndex,EngagementDistance,TargetTypes) local DCSTask={ id='Escort', params={ groupId=FollowControllable and FollowControllable:GetID()or nil, 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=1, 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:EnRouteTaskAntiShip(TargetTypes,Priority) local DCSTask={ id='EngageTargets', key="AntiShip", params={ targetTypes=TargetTypes or{"Ships"}, priority=Priority or 0 } } return DCSTask end function CONTROLLABLE:EnRouteTaskSEAD(TargetTypes,Priority) local DCSTask={ id='EngageTargets', key="SEAD", params={ targetTypes=TargetTypes or{"Air Defence"}, priority=Priority or 0 } } return DCSTask end function CONTROLLABLE:EnRouteTaskCAP(TargetTypes,Priority) local DCSTask={ id='EngageTargets', key="CAP", enabled=true, params={ 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='EngageGroup', 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,Frequency,Modulation,CallsignID,CallsignNumber) local DCSTask={ id='FAC_EngageGroup', params={ groupId=AttackGroup:GetID(), weaponType=WeaponType or"Auto", designation=Designation, datalink=Datalink and Datalink or false, frequency=(Frequency or 133)*1000000, modulation=Modulation or radio.modulation.AM, callname=CallsignID, number=CallsignNumber, priority=Priority or 0, }, } return DCSTask end function CONTROLLABLE:EnRouteTaskFAC(Frequency,Modulation,CallsignID,CallsignNumber,Priority) local DCSTask={ id='FAC', params={ frequency=(Frequency or 133)*1000000, modulation=Modulation or radio.modulation.AM, callname=CallsignID, number=CallsignNumber, priority=Priority or 0 } } 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 depth=0 local IsSub=false if PatrolGroup:IsShip()then local navalvec3=FromCoord:GetVec3() if navalvec3.y<0 then depth=navalvec3.y IsSub=true end end local Waypoint=Waypoints[1] local Speed=Waypoint.speed or(20/3.6) local From=FromCoord:WaypointGround(Speed) if IsSub then From=FromCoord:WaypointNaval(Speed,Waypoint.alt) end 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,2) 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 depth=0 local IsSub=false if PatrolGroup:IsShip()then local navalvec3=FromCoord:GetVec3() if navalvec3.y<0 then depth=navalvec3.y IsSub=true end 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={} if IsSub then Route[#Route+1]=FromCoord:WaypointNaval(Speed,depth) Route[#Route+1]=ToCoord:WaypointNaval(Speed,Waypoint.alt) else Route[#Route+1]=FromCoord:WaypointGround(Speed,Formation) Route[#Route+1]=ToCoord:WaypointGround(Speed,Formation) end 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 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 UTILS.DeepCopy(_DATABASE.Templates.Controllables[self.ControllableName].Template) end function CONTROLLABLE:GetTaskRoute() self:F2(self.ControllableName) return 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]=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.OPTIC 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.OPTIC 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,TargetKnowType,TargetKnowDistance,TargetLastTime,TargetLastPos,TargetLastVelocity =Controller:isTargetDetected(DCSObject,DetectionVisual,DetectionOptical,DetectionRadar,DetectionIRST,DetectionRWR,DetectionDLINK) return TargetIsDetected,TargetIsVisible,TargetKnowType,TargetKnowDistance,TargetLastTime,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:OptionEvasionOfARM(Seconds) self:F2({self.ControllableName}) local DCSControllable=self:GetDCSObject() if DCSControllable then local Controller=self:_GetController() if self:IsGround()then if Seconds==nil then Seconds=false end Controller:setOption(AI.Option.Ground.id.EVASION_OF_ARM,Seconds) end end return self end function CONTROLLABLE:OptionFormationInterval(meters) self:F2({self.ControllableName}) local DCSControllable=self:GetDCSObject() if DCSControllable then local Controller=self:_GetController() if self:IsGround()then if meters==nil or meters>100 or meters<0 then meters=50 end Controller:setOption(30,meters) end end return self end function CONTROLLABLE:OptionECM(ECMvalue) 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.ECM_USING,ECMvalue or 1) end end return self end function CONTROLLABLE:OptionECM_Never() self:F2({self.ControllableName}) self:OptionECM(0) return self end function CONTROLLABLE:OptionECM_OnlyLockByRadar() self:F2({self.ControllableName}) self:OptionECM(1) return self end function CONTROLLABLE:OptionECM_DetectedLockByRadar() self:F2({self.ControllableName}) self:OptionECM(2) return self end function CONTROLLABLE:OptionECM_AlwaysOn() self:F2({self.ControllableName}) self:OptionECM(3) 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}) if not self.WayPoints then self:WayPointInitialize() end 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.id.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:SetOptionRadarUsing(Option) self:F2({self.ControllableName}) if self:IsAir()then self:SetOption(AI.Option.Air.id.RADAR_USING,Option) end return self end function CONTROLLABLE:SetOptionRadarUsingNever() self:F2({self.ControllableName}) if self:IsAir()then self:SetOption(AI.Option.Air.id.RADAR_USING,0) end return self end function CONTROLLABLE:SetOptionRadarUsingForAttackOnly() self:F2({self.ControllableName}) if self:IsAir()then self:SetOption(AI.Option.Air.id.RADAR_USING,1) end return self end function CONTROLLABLE:SetOptionRadarUsingForSearchIfRequired() self:F2({self.ControllableName}) if self:IsAir()then self:SetOption(AI.Option.Air.id.RADAR_USING,2) end return self end function CONTROLLABLE:SetOptionRadarUsingForContinousSearch() self:F2({self.ControllableName}) if self:IsAir()then self:SetOption(AI.Option.Air.id.RADAR_USING,3) end return self end function CONTROLLABLE:SetOptionWaypointPassReport(OnOff) self:F2({self.ControllableName}) local onoff=(OnOff==nil or OnOff==true)and false or true if self:IsAir()then self:SetOption(AI.Option.Air.id.PROHIBIT_WP_PASS_REPORT,onoff) end return self end function CONTROLLABLE:SetOptionRadioSilence(OnOff) local onoff=(OnOff==true or OnOff==nil)and true or false self:F2({self.ControllableName}) if self:IsAir()then self:SetOption(AI.Option.Air.id.SILENCE,onoff) end return self end function CONTROLLABLE:SetOptionRadioContact(Objects) self:F2({self.ControllableName}) if not Objects then Objects={"Air"}end if type(Objects)~="table"then Objects={Objects}end if self:IsAir()then self:SetOption(AI.Option.Air.id.OPTION_RADIO_USAGE_CONTACT,Objects) end return self end function CONTROLLABLE:SetOptionRadioEngage(Objects) self:F2({self.ControllableName}) if not Objects then Objects={"Air"}end if type(Objects)~="table"then Objects={Objects}end if self:IsAir()then self:SetOption(AI.Option.Air.id.OPTION_RADIO_USAGE_ENGAGE,Objects) end return self end function CONTROLLABLE:SetOptionRadioKill(Objects) self:F2({self.ControllableName}) if not Objects then Objects={"Air"}end if type(Objects)~="table"then Objects={Objects}end if self:IsAir()then self:SetOption(AI.Option.Air.id.OPTION_RADIO_USAGE_KILL,Objects) end return self end function CONTROLLABLE:RelocateGroundRandomInRadius(speed,radius,onroad,shortcut,formation,onland) self:F2({self.ControllableName}) local _coord=self:GetCoordinate() if not _coord then return self end local _radius=radius or 500 local _speed=speed or 20 local _tocoord=_coord:GetRandomCoordinateInRadius(_radius,100) if onland then for i=1,50 do local island=_tocoord:GetSurfaceType()==land.SurfaceType.LAND and true or false if island then break end _tocoord=_coord:GetRandomCoordinateInRadius(_radius,100) end end local _onroad=onroad or true local _grptsk={} local _candoroad=false local _shortcut=shortcut or false local _formation=formation or"Off Road" if onroad then _grptsk,_candoroad=self:TaskGroundOnRoad(_tocoord,_speed,_formation,_shortcut) self:Route(_grptsk,5) else self:TaskRouteToVec2(_tocoord:GetVec2(),_speed,_formation) 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 function CONTROLLABLE:IsSubmarine() self:F2() local DCSUnit=self:GetDCSObject() if DCSUnit then local UnitDescriptor=DCSUnit:getDesc() if UnitDescriptor.attributes["Submarines"]==true then return true else return false end end return nil end function CONTROLLABLE:SetSpeed(Speed,Keep) self:F2({self.ControllableName}) local speed=Speed or 5 local DCSControllable=self:GetDCSObject() if DCSControllable then local Controller=self:_GetController() if Controller then Controller:setSpeed(speed,Keep) end end return self end function CONTROLLABLE:SetAltitude(Altitude,Keep,AltType) self:F2({self.ControllableName}) local altitude=Altitude or 1000 local DCSControllable=self:GetDCSObject() if DCSControllable then local Controller=self:_GetController() if Controller then if self:IsAir()then Controller:setAltitude(altitude,Keep,AltType) end end end return self end function CONTROLLABLE:TaskAerobatics() local DCSTaskAerobatics={ id="Aerobatics", params={ ["maneuversSequency"]={}, }, ["enabled"]=true, ["auto"]=false, } return DCSTaskAerobatics end function CONTROLLABLE:TaskAerobaticsCandle(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately) local maxrepeats=10 if Repeats>maxrepeats then maxrepeats=Repeats end local usesmoke=UseSmoke and 1 or 0 local startimmediately=StartImmediately and 1 or 0 local CandleTask={ ["name"]="CANDLE", ["params"]={ ["RepeatQty"]={ ["max_v"]=maxrepeats, ["min_v"]=1, ["order"]=1, ["value"]=Repeats or 1, }, ["InitAltitude"]={ ["order"]=2, ["value"]=InitAltitude or 0, }, ["InitSpeed"]={ ["order"]=3, ["value"]=InitSpeed or 0, }, ["UseSmoke"]={ ["order"]=4, ["value"]=usesmoke, }, ["StartImmediatly"]={ ["order"]=5, ["value"]=startimmediately, } } } table.insert(TaskAerobatics.params["maneuversSequency"],CandleTask) return TaskAerobatics end function CONTROLLABLE:TaskAerobaticsEdgeFlight(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately,FlightTime,Side) local maxrepeats=10 local maxflight=200 if Repeats>maxrepeats then maxrepeats=Repeats end local usesmoke=UseSmoke and 1 or 0 local startimmediately=StartImmediately and 1 or 0 local flighttime=FlightTime or 10 if flighttime>200 then maxflight=flighttime end local EdgeTask={ ["name"]="EDGE_FLIGHT", ["params"]={ ["RepeatQty"]={ ["max_v"]=maxrepeats, ["min_v"]=1, ["order"]=1, ["value"]=Repeats or 1, }, ["InitAltitude"]={ ["order"]=2, ["value"]=InitAltitude or 0, }, ["InitSpeed"]={ ["order"]=3, ["value"]=InitSpeed or 0, }, ["UseSmoke"]={ ["order"]=4, ["value"]=usesmoke, }, ["StartImmediatly"]={ ["order"]=5, ["value"]=startimmediately, }, ["FlightTime"]={ ["max_v"]=maxflight, ["min_v"]=1, ["order"]=6, ["step"]=0.1, ["value"]=flighttime or 10, }, ["SIDE"]={ ["order"]=7, ["value"]=Side or 0, }, } } table.insert(TaskAerobatics.params["maneuversSequency"],EdgeTask) return TaskAerobatics end function CONTROLLABLE:TaskAerobaticsWingoverFlight(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately,FlightTime) local maxrepeats=10 local maxflight=200 if Repeats>maxrepeats then maxrepeats=Repeats end local usesmoke=UseSmoke and 1 or 0 local startimmediately=StartImmediately and 1 or 0 local flighttime=FlightTime or 10 if flighttime>200 then maxflight=flighttime end local WingoverTask={ ["name"]="WINGOVER_FLIGHT", ["params"]={ ["RepeatQty"]={ ["max_v"]=maxrepeats, ["min_v"]=1, ["order"]=1, ["value"]=Repeats or 1, }, ["InitAltitude"]={ ["order"]=2, ["value"]=InitAltitude or 0, }, ["InitSpeed"]={ ["order"]=3, ["value"]=InitSpeed or 0, }, ["UseSmoke"]={ ["order"]=4, ["value"]=usesmoke, }, ["StartImmediatly"]={ ["order"]=5, ["value"]=startimmediately, }, ["FlightTime"]={ ["max_v"]=maxflight, ["min_v"]=1, ["order"]=6, ["step"]=0.1, ["value"]=flighttime or 10, }, } } table.insert(TaskAerobatics.params["maneuversSequency"],WingoverTask) return TaskAerobatics end function CONTROLLABLE:TaskAerobaticsLoop(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately) local maxrepeats=10 if Repeats>maxrepeats then maxrepeats=Repeats end local usesmoke=UseSmoke and 1 or 0 local startimmediately=StartImmediately and 1 or 0 local LoopTask={ ["name"]="LOOP", ["params"]={ ["RepeatQty"]={ ["max_v"]=maxrepeats, ["min_v"]=1, ["order"]=1, ["value"]=Repeats or 1, }, ["InitAltitude"]={ ["order"]=2, ["value"]=InitAltitude or 0, }, ["InitSpeed"]={ ["order"]=3, ["value"]=InitSpeed or 0, }, ["UseSmoke"]={ ["order"]=4, ["value"]=usesmoke, }, ["StartImmediatly"]={ ["order"]=5, ["value"]=startimmediately, } } } table.insert(TaskAerobatics.params["maneuversSequency"],LoopTask) return TaskAerobatics end function CONTROLLABLE:TaskAerobaticsHorizontalEight(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately,Side,RollDeg) local maxrepeats=10 if Repeats>maxrepeats then maxrepeats=Repeats end local usesmoke=UseSmoke and 1 or 0 local startimmediately=StartImmediately and 1 or 0 local LoopTask={ ["name"]="HORIZONTAL_EIGHT", ["params"]={ ["RepeatQty"]={ ["max_v"]=maxrepeats, ["min_v"]=1, ["order"]=1, ["value"]=Repeats or 1, }, ["InitAltitude"]={ ["order"]=2, ["value"]=InitAltitude or 0, }, ["InitSpeed"]={ ["order"]=3, ["value"]=InitSpeed or 0, }, ["UseSmoke"]={ ["order"]=4, ["value"]=usesmoke, }, ["StartImmediatly"]={ ["order"]=5, ["value"]=startimmediately, }, ["SIDE"]={ ["order"]=6, ["value"]=Side or 0, }, ["ROLL1"]={ ["order"]=7, ["value"]=RollDeg or 60, }, ["ROLL2"]={ ["order"]=8, ["value"]=RollDeg or 60, }, } } table.insert(TaskAerobatics.params["maneuversSequency"],LoopTask) return TaskAerobatics end function CONTROLLABLE:TaskAerobaticsHammerhead(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately,Side) local maxrepeats=10 if Repeats>maxrepeats then maxrepeats=Repeats end local usesmoke=UseSmoke and 1 or 0 local startimmediately=StartImmediately and 1 or 0 local Task={ ["name"]="HUMMERHEAD", ["params"]={ ["RepeatQty"]={ ["max_v"]=maxrepeats, ["min_v"]=1, ["order"]=1, ["value"]=Repeats or 1, }, ["InitAltitude"]={ ["order"]=2, ["value"]=InitAltitude or 0, }, ["InitSpeed"]={ ["order"]=3, ["value"]=InitSpeed or 0, }, ["UseSmoke"]={ ["order"]=4, ["value"]=usesmoke, }, ["StartImmediatly"]={ ["order"]=5, ["value"]=startimmediately, }, ["SIDE"]={ ["order"]=6, ["value"]=Side or 0, }, } } table.insert(TaskAerobatics.params["maneuversSequency"],Task) return TaskAerobatics end function CONTROLLABLE:TaskAerobaticsSkewedLoop(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately,Side,RollDeg) local maxrepeats=10 if Repeats>maxrepeats then maxrepeats=Repeats end local usesmoke=UseSmoke and 1 or 0 local startimmediately=StartImmediately and 1 or 0 local Task={ ["name"]="SKEWED_LOOP", ["params"]={ ["RepeatQty"]={ ["max_v"]=maxrepeats, ["min_v"]=1, ["order"]=1, ["value"]=Repeats or 1, }, ["InitAltitude"]={ ["order"]=2, ["value"]=InitAltitude or 0, }, ["InitSpeed"]={ ["order"]=3, ["value"]=InitSpeed or 0, }, ["UseSmoke"]={ ["order"]=4, ["value"]=usesmoke, }, ["StartImmediatly"]={ ["order"]=5, ["value"]=startimmediately, }, ["ROLL"]={ ["order"]=6, ["value"]=RollDeg or 60, }, ["SIDE"]={ ["order"]=7, ["value"]=Side or 0, }, } } table.insert(TaskAerobatics.params["maneuversSequency"],Task) return TaskAerobatics end function CONTROLLABLE:TaskAerobaticsTurn(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately,Side,RollDeg,Pull,Angle) local maxrepeats=10 if Repeats>maxrepeats then maxrepeats=Repeats end local usesmoke=UseSmoke and 1 or 0 local startimmediately=StartImmediately and 1 or 0 local Task={ ["name"]="TURN", ["params"]={ ["RepeatQty"]={ ["max_v"]=maxrepeats, ["min_v"]=1, ["order"]=1, ["value"]=Repeats or 1, }, ["InitAltitude"]={ ["order"]=2, ["value"]=InitAltitude or 0, }, ["InitSpeed"]={ ["order"]=3, ["value"]=InitSpeed or 0, }, ["UseSmoke"]={ ["order"]=4, ["value"]=usesmoke, }, ["StartImmediatly"]={ ["order"]=5, ["value"]=startimmediately, }, ["Ny_req"]={ ["order"]=6, ["value"]=Pull or 2, }, ["ROLL"]={ ["order"]=7, ["value"]=RollDeg or 60, }, ["SECTOR"]={ ["order"]=8, ["value"]=Angle or 180, }, ["SIDE"]={ ["order"]=9, ["value"]=Side or 0, }, } } table.insert(TaskAerobatics.params["maneuversSequency"],Task) return TaskAerobatics end function CONTROLLABLE:TaskAerobaticsDive(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately,Angle,FinalAltitude) local maxrepeats=10 local angle=Angle if angle<15 then angle=15 elseif angle>90 then angle=90 end if Repeats>maxrepeats then maxrepeats=Repeats end local usesmoke=UseSmoke and 1 or 0 local startimmediately=StartImmediately and 1 or 0 local Task={ ["name"]="DIVE", ["params"]={ ["RepeatQty"]={ ["max_v"]=maxrepeats, ["min_v"]=1, ["order"]=1, ["value"]=Repeats or 1, }, ["InitAltitude"]={ ["order"]=2, ["value"]=InitAltitude or 5000, }, ["InitSpeed"]={ ["order"]=3, ["value"]=InitSpeed or 0, }, ["UseSmoke"]={ ["order"]=4, ["value"]=usesmoke, }, ["StartImmediatly"]={ ["order"]=5, ["value"]=startimmediately, }, ["Angle"]={ ["max_v"]=90, ["min_v"]=15, ["order"]=6, ["step"]=5, ["value"]=angle or 45, }, ["FinalAltitude"]={ ["order"]=7, ["value"]=FinalAltitude or 1000, }, } } table.insert(TaskAerobatics.params["maneuversSequency"],Task) return TaskAerobatics end function CONTROLLABLE:TaskAerobaticsMilitaryTurn(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately) local maxrepeats=10 if Repeats>maxrepeats then maxrepeats=Repeats end local usesmoke=UseSmoke and 1 or 0 local startimmediately=StartImmediately and 1 or 0 local Task={ ["name"]="MILITARY_TURN", ["params"]={ ["RepeatQty"]={ ["max_v"]=maxrepeats, ["min_v"]=1, ["order"]=1, ["value"]=Repeats or 1, }, ["InitAltitude"]={ ["order"]=2, ["value"]=InitAltitude or 0, }, ["InitSpeed"]={ ["order"]=3, ["value"]=InitSpeed or 0, }, ["UseSmoke"]={ ["order"]=4, ["value"]=usesmoke, }, ["StartImmediatly"]={ ["order"]=5, ["value"]=startimmediately, } } } table.insert(TaskAerobatics.params["maneuversSequency"],Task) return TaskAerobatics end function CONTROLLABLE:TaskAerobaticsImmelmann(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately) local maxrepeats=10 if Repeats>maxrepeats then maxrepeats=Repeats end local usesmoke=UseSmoke and 1 or 0 local startimmediately=StartImmediately and 1 or 0 local Task={ ["name"]="IMMELMAN", ["params"]={ ["RepeatQty"]={ ["max_v"]=maxrepeats, ["min_v"]=1, ["order"]=1, ["value"]=Repeats or 1, }, ["InitAltitude"]={ ["order"]=2, ["value"]=InitAltitude or 0, }, ["InitSpeed"]={ ["order"]=3, ["value"]=InitSpeed or 0, }, ["UseSmoke"]={ ["order"]=4, ["value"]=usesmoke, }, ["StartImmediatly"]={ ["order"]=5, ["value"]=startimmediately, } } } table.insert(TaskAerobatics.params["maneuversSequency"],Task) return TaskAerobatics end function CONTROLLABLE:TaskAerobaticsStraightFlight(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately,FlightTime) local maxrepeats=10 if Repeats>maxrepeats then maxrepeats=Repeats end local maxflight=200 if Repeats>maxrepeats then maxrepeats=Repeats end local flighttime=FlightTime or 10 if flighttime>200 then maxflight=flighttime end local usesmoke=UseSmoke and 1 or 0 local startimmediately=StartImmediately and 1 or 0 local Task={ ["name"]="STRAIGHT_FLIGHT", ["params"]={ ["RepeatQty"]={ ["max_v"]=maxrepeats, ["min_v"]=1, ["order"]=1, ["value"]=Repeats or 1, }, ["InitAltitude"]={ ["order"]=2, ["value"]=InitAltitude or 0, }, ["InitSpeed"]={ ["order"]=3, ["value"]=InitSpeed or 0, }, ["UseSmoke"]={ ["order"]=4, ["value"]=usesmoke, }, ["StartImmediatly"]={ ["order"]=5, ["value"]=startimmediately, }, ["FlightTime"]={ ["max_v"]=maxflight, ["min_v"]=1, ["order"]=6, ["step"]=0.1, ["value"]=flighttime or 10, }, } } table.insert(TaskAerobatics.params["maneuversSequency"],Task) return TaskAerobatics end function CONTROLLABLE:TaskAerobaticsClimb(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately,Angle,FinalAltitude) local maxrepeats=10 if Repeats>maxrepeats then maxrepeats=Repeats end local usesmoke=UseSmoke and 1 or 0 local startimmediately=StartImmediately and 1 or 0 local Task={ ["name"]="CLIMB", ["params"]={ ["RepeatQty"]={ ["max_v"]=maxrepeats, ["min_v"]=1, ["order"]=1, ["value"]=Repeats or 1, }, ["InitAltitude"]={ ["order"]=2, ["value"]=InitAltitude or 0, }, ["InitSpeed"]={ ["order"]=3, ["value"]=InitSpeed or 0, }, ["UseSmoke"]={ ["order"]=4, ["value"]=usesmoke, }, ["StartImmediatly"]={ ["order"]=5, ["value"]=startimmediately, }, ["Angle"]={ ["max_v"]=90, ["min_v"]=15, ["order"]=6, ["step"]=5, ["value"]=Angle or 45, }, ["FinalAltitude"]={ ["order"]=7, ["value"]=FinalAltitude or 5000, }, } } table.insert(TaskAerobatics.params["maneuversSequency"],Task) return TaskAerobatics end function CONTROLLABLE:TaskAerobaticsSpiral(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately,TurnAngle,Roll,Side,UpDown,Angle) local maxrepeats=10 if Repeats>maxrepeats then maxrepeats=Repeats end local usesmoke=UseSmoke and 1 or 0 local startimmediately=StartImmediately and 1 or 0 local updown=UpDown and 1 or 0 local side=Side and 1 or 0 local Task={ ["name"]="SPIRAL", ["params"]={ ["RepeatQty"]={ ["max_v"]=maxrepeats, ["min_v"]=1, ["order"]=1, ["value"]=Repeats or 1, }, ["InitAltitude"]={ ["order"]=2, ["value"]=InitAltitude or 0, }, ["InitSpeed"]={ ["order"]=3, ["value"]=InitSpeed or 0, }, ["UseSmoke"]={ ["order"]=4, ["value"]=usesmoke, }, ["StartImmediatly"]={ ["order"]=5, ["value"]=startimmediately, }, ["SECTOR"]={ ["order"]=6, ["value"]=TurnAngle or 360, }, ["ROLL"]={ ["order"]=7, ["value"]=Roll or 60, }, ["SIDE"]={ ["order"]=8, ["value"]=side or 0, }, ["UPDOWN"]={ ["order"]=9, ["value"]=updown or 0, }, ["Angle"]={ ["max_v"]=90, ["min_v"]=15, ["order"]=10, ["step"]=5, ["value"]=Angle or 45, }, } } table.insert(TaskAerobatics.params["maneuversSequency"],Task) return TaskAerobatics end function CONTROLLABLE:TaskAerobaticsSplitS(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately,FinalSpeed) local maxrepeats=10 if Repeats>maxrepeats then maxrepeats=Repeats end local maxflight=200 if Repeats>maxrepeats then maxrepeats=Repeats end local finalspeed=FinalSpeed or 500 local usesmoke=UseSmoke and 1 or 0 local startimmediately=StartImmediately and 1 or 0 local Task={ ["name"]="SPLIT_S", ["params"]={ ["RepeatQty"]={ ["max_v"]=maxrepeats, ["min_v"]=1, ["order"]=1, ["value"]=Repeats or 1, }, ["InitAltitude"]={ ["order"]=2, ["value"]=InitAltitude or 0, }, ["InitSpeed"]={ ["order"]=3, ["value"]=InitSpeed or 0, }, ["UseSmoke"]={ ["order"]=4, ["value"]=usesmoke, }, ["StartImmediatly"]={ ["order"]=5, ["value"]=startimmediately, }, ["FinalSpeed"]={ ["order"]=6, ["value"]=finalspeed, }, } } table.insert(TaskAerobatics.params["maneuversSequency"],Task) return TaskAerobatics end function CONTROLLABLE:TaskAerobaticsAileronRoll(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately,Side,RollRate,TurnAngle,FixAngle) local maxrepeats=10 if Repeats>maxrepeats then maxrepeats=Repeats end local maxflight=200 if Repeats>maxrepeats then maxrepeats=Repeats end local usesmoke=UseSmoke and 1 or 0 local startimmediately=StartImmediately and 1 or 0 local Task={ ["name"]="AILERON_ROLL", ["params"]={ ["RepeatQty"]={ ["max_v"]=maxrepeats, ["min_v"]=1, ["order"]=1, ["value"]=Repeats or 1, }, ["InitAltitude"]={ ["order"]=2, ["value"]=InitAltitude or 0, }, ["InitSpeed"]={ ["order"]=3, ["value"]=InitSpeed or 0, }, ["UseSmoke"]={ ["order"]=4, ["value"]=usesmoke, }, ["StartImmediatly"]={ ["order"]=5, ["value"]=startimmediately, }, ["SIDE"]={ ["order"]=6, ["value"]=Side or 0, }, ["RollRate"]={ ["max_v"]=450, ["min_v"]=15, ["order"]=7, ["step"]=5, ["value"]=RollRate or 90, }, ["SECTOR"]={ ["order"]=8, ["value"]=TurnAngle or 360, }, ["FIXSECTOR"]={ ["max_v"]=180, ["min_v"]=0, ["order"]=9, ["step"]=5, ["value"]=FixAngle or 0, }, } } table.insert(TaskAerobatics.params["maneuversSequency"],Task) return TaskAerobatics end function CONTROLLABLE:TaskAerobaticsForcedTurn(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately,TurnAngle,Side,FlightTime,MinSpeed) local maxrepeats=10 local flighttime=FlightTime or 30 local maxtime=200 if flighttime>200 then maxtime=flighttime end if Repeats>maxrepeats then maxrepeats=Repeats end local usesmoke=UseSmoke and 1 or 0 local startimmediately=StartImmediately and 1 or 0 local Task={ ["name"]="FORCED_TURN", ["params"]={ ["RepeatQty"]={ ["max_v"]=maxrepeats, ["min_v"]=1, ["order"]=1, ["value"]=Repeats or 1, }, ["InitAltitude"]={ ["order"]=2, ["value"]=InitAltitude or 0, }, ["InitSpeed"]={ ["order"]=3, ["value"]=InitSpeed or 0, }, ["UseSmoke"]={ ["order"]=4, ["value"]=usesmoke, }, ["StartImmediatly"]={ ["order"]=5, ["value"]=startimmediately, }, ["SECTOR"]={ ["order"]=6, ["value"]=TurnAngle or 360, }, ["SIDE"]={ ["order"]=7, ["value"]=Side or 0, }, ["FlightTime"]={ ["max_v"]=maxtime or 200, ["min_v"]=0, ["order"]=8, ["step"]=0.1, ["value"]=flighttime or 30, }, ["MinSpeed"]={ ["max_v"]=3000, ["min_v"]=30, ["order"]=9, ["step"]=10, ["value"]=MinSpeed or 250, }, } } table.insert(TaskAerobatics.params["maneuversSequency"],Task) return TaskAerobatics end function CONTROLLABLE:TaskAerobaticsBarrelRoll(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately,Side,RollRate,TurnAngle) local maxrepeats=10 if Repeats>maxrepeats then maxrepeats=Repeats end local usesmoke=UseSmoke and 1 or 0 local startimmediately=StartImmediately and 1 or 0 local Task={ ["name"]="BARREL_ROLL", ["params"]={ ["RepeatQty"]={ ["max_v"]=maxrepeats, ["min_v"]=1, ["order"]=1, ["value"]=Repeats or 1, }, ["InitAltitude"]={ ["order"]=2, ["value"]=InitAltitude or 0, }, ["InitSpeed"]={ ["order"]=3, ["value"]=InitSpeed or 0, }, ["UseSmoke"]={ ["order"]=4, ["value"]=usesmoke, }, ["StartImmediatly"]={ ["order"]=5, ["value"]=startimmediately, }, ["SIDE"]={ ["order"]=6, ["value"]=Side or 0, }, ["RollRate"]={ ["max_v"]=450, ["min_v"]=15, ["order"]=7, ["step"]=5, ["value"]=RollRate or 90, }, ["SECTOR"]={ ["order"]=8, ["value"]=TurnAngle or 360, }, } } table.insert(TaskAerobatics.params["maneuversSequency"],Task) return TaskAerobatics end function CONTROLLABLE:PatrolRaceTrack(Point1,Point2,Altitude,Speed,Formation,AGL,Delay) local PatrolGroup=self if not self:IsInstanceOf("GROUP")then PatrolGroup=self:GetGroup() end local delay=Delay or 1 self:F({PatrolGroup=PatrolGroup:GetName()}) if PatrolGroup:IsAir()then if Formation then PatrolGroup:SetOption(AI.Option.Air.id.FORMATION,Formation) end local FromCoord=PatrolGroup:GetCoordinate() local ToCoord=Point1:GetCoordinate() if Altitude then local asl=true if AGL then asl=false end FromCoord:SetAltitude(Altitude,asl) ToCoord:SetAltitude(Altitude,asl) end local Route={} Route[#Route+1]=FromCoord:WaypointAir(AltType,COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.TurningPoint,Speed,true,nil,DCSTasks,description,timeReFuAr) Route[#Route+1]=ToCoord:WaypointAir(AltType,COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.TurningPoint,Speed,true,nil,DCSTasks,description,timeReFuAr) local TaskRouteToZone=PatrolGroup:TaskFunction("CONTROLLABLE.PatrolRaceTrack",Point2,Point1,Altitude,Speed,Formation,Delay) PatrolGroup:SetTaskWaypoint(Route[#Route],TaskRouteToZone) PatrolGroup:Route(Route,Delay) end return self end function CONTROLLABLE:NewIRMarker(EnableImmediately,Runtime) self:T2("NewIRMarker") if self:IsInstanceOf("GROUP")then if self.IRMarkerGroup==true then return end self.IRMarkerGroup=true self.IRMarkerUnit=false elseif self:IsInstanceOf("UNIT")then if self.IRMarkerUnit==true then return end self.IRMarkerGroup=false self.IRMarkerUnit=true end self.Runtime=Runtime or 60 if EnableImmediately and EnableImmediately==true then self:EnableIRMarker(Runtime) end return self end function CONTROLLABLE:EnableIRMarker(Runtime) self:T2("EnableIRMarker") if self.IRMarkerGroup==nil then self:NewIRMarker(true,Runtime) return end if self:IsInstanceOf("GROUP")then self:EnableIRMarkerForGroup(Runtime) return end if self.timer and self.timer:IsRunning()then return self end local Runtime=Runtime or self.Runtime self.timer=TIMER:New(CONTROLLABLE._MarkerBlink,self) self.timer:Start(nil,1-math.random(1,5)/10/2,Runtime) self.IRMarkerUnit=true return self end function CONTROLLABLE:DisableIRMarker() self:T2("DisableIRMarker") if self:IsInstanceOf("GROUP")then self:DisableIRMarkerForGroup() return end if self.spot then self.spot=nil end if self.timer and self.timer:IsRunning()then self.timer:Stop() self.timer=nil end if self:IsInstanceOf("GROUP")then self.IRMarkerGroup=nil elseif self:IsInstanceOf("UNIT")then self.IRMarkerUnit=nil end return self end function CONTROLLABLE:EnableIRMarkerForGroup(Runtime) self:T2("EnableIRMarkerForGroup") if self:IsInstanceOf("GROUP") then local units=self:GetUnits()or{} for _,_unit in pairs(units)do _unit:EnableIRMarker(Runtime) end self.IRMarkerGroup=true end return self end function CONTROLLABLE:DisableIRMarkerForGroup() self:T2("DisableIRMarkerForGroup") if self:IsInstanceOf("GROUP")then local units=self:GetUnits()or{} for _,_unit in pairs(units)do _unit:DisableIRMarker() end self.IRMarkerGroup=nil end return self end function CONTROLLABLE:HasIRMarker() self:T2("HasIRMarker") if self:IsInstanceOf("GROUP")then local units=self:GetUnits()or{} for _,_unit in pairs(units)do if _unit.timer and _unit.timer:IsRunning()then return true end end elseif self.timer and self.timer:IsRunning()then return true end return false end function CONTROLLABLE._StopSpot(spot) if spot then spot:destroy() end end function CONTROLLABLE:_MarkerBlink() self:T2("_MarkerBlink") if self:IsAlive()~=true then self:DisableIRMarker() return end self.timer.dT=1-(math.random(1,2)/10/2) local _,_,unitBBHeight,_=self:GetObjectSize() local unitPos=self:GetPositionVec3() if self.timer:IsRunning()then self:T2("Create Spot") local spot=Spot.createInfraRed( self.DCSUnit, {x=0,y=(unitBBHeight+1),z=0}, {x=unitPos.x,y=(unitPos.y+unitBBHeight),z=unitPos.z} ) self.spot=spot local offTimer=nil local offTimer=TIMER:New(CONTROLLABLE._StopSpot,spot) offTimer:Start(0.5) end return self 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_IFV="Ground_IFV", 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:FindByMatching(Pattern) local GroupFound=nil for name,group in pairs(_DATABASE.GROUPS)do if string.match(name,Pattern)then GroupFound=group break end end return GroupFound end function GROUP:FindAllByMatching(Pattern) local GroupsFound={} for name,group in pairs(_DATABASE.GROUPS)do if string.match(name,Pattern)then GroupsFound[#GroupsFound+1]=group end end return GroupsFound end function GROUP:GetDCSObject() local DCSGroup=Group.getByName(self.GroupName) if DCSGroup then self.LastCallDCSObject=timer.getTime() self.DCSObject=DCSGroup return DCSGroup end return nil end function GROUP:GetPositionVec3() local DCSPositionable=self:GetDCSObject() if DCSPositionable then local unit=DCSPositionable:getUnits()[1] if unit then local PositionablePosition=unit:getPosition().p return PositionablePosition end end return nil end function GROUP:IsAlive() local DCSGroup=self:GetDCSObject() if DCSGroup then if DCSGroup:isExist()then local DCSUnit=DCSGroup:getUnit(1) if DCSUnit then local GroupIsAlive=DCSUnit:isActive() return GroupIsAlive end end end return nil end function GROUP:IsActive() local DCSGroup=self:GetDCSObject() if DCSGroup and DCSGroup:isExist()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) if delay and delay>0 then self:ScheduleOnce(delay,GROUP.Destroy,self,GenerateEvent) else local DCSGroup=Group.getByName(self.GroupName) 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() local DCSGroup=self:GetDCSObject() if DCSGroup then local GroupCategory=DCSGroup:getCategory() return GroupCategory end return nil end function GROUP:GetCategoryName() 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() return CategoryNames[GroupCategory] end return nil end function GROUP:GetCoalition() if self.GroupCoalition~=nil then return self.GroupCoalition else local DCSGroup=self:GetDCSObject() if DCSGroup then local GroupCoalition=DCSGroup:getCoalition() self.GroupCoalition=GroupCoalition return GroupCoalition end end return nil end function GROUP:GetCountry() local DCSGroup=self:GetDCSObject() if DCSGroup then local GroupCountry=DCSGroup:getUnit(1):getCountry() return GroupCountry end return nil end function GROUP:HasAttribute(attribute,all) local _units=self:GetUnits() if _units then 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 return nil end function GROUP:GetSpeedMax() 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 or speed0 then self:ScheduleOnce(delay,GROUP.Activate,self) else trigger.action.activateGroup(self:GetDCSObject()) end return self end function GROUP:Deactivate(delay) if delay and delay>0 then self:ScheduleOnce(delay,GROUP.Deactivate,self) else trigger.action.deactivateGroup(self:GetDCSObject()) end return self end function GROUP:GetTypeName() local DCSGroup=self:GetDCSObject() if DCSGroup then local GroupTypeName=DCSGroup:getUnit(1):getTypeName() return(GroupTypeName) end return nil end function GROUP:GetNatoReportingName() local DCSGroup=self:GetDCSObject() if DCSGroup then local GroupTypeName=DCSGroup:getUnit(1):getTypeName() return UTILS.GetReportingName(GroupTypeName) end return"Bogey" end function GROUP:GetPlayerName() local DCSGroup=self:GetDCSObject() if DCSGroup then local PlayerName=DCSGroup:getUnit(1):getPlayerName() return(PlayerName) end return nil end function GROUP:GetCallsign() local DCSGroup=self:GetDCSObject() if DCSGroup then local GroupCallSign=DCSGroup:getUnit(1):getCallsign() 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:GetAverageVec3() local units=self:GetUnits()or{} local x=0;local y=0;local z=0;local n=0 for _,unit in pairs(units)do local vec3=nil if unit and unit:IsAlive()then vec3=unit:GetVec3() end if vec3 then x=x+vec3.x y=y+vec3.y z=z+vec3.z n=n+1 end end if n>0 then local Vec3={x=x/n,y=y/n,z=z/n} return Vec3 else return self:GetVec3() end end function GROUP:GetPointVec2() local FirstUnit=self:GetUnit(1) if FirstUnit then local FirstUnitPointVec2=FirstUnit:GetPointVec2() return FirstUnitPointVec2 end BASE:E({"Cannot get COORDINATE",Group=self,Alive=self:IsAlive()}) return nil end function GROUP:GetAverageCoordinate() local vec3=self:GetAverageVec3() if vec3 then local coord=COORDINATE:NewFromVec3(vec3) local Heading=self:GetHeading() coord.Heading=Heading return coord else local coord=self:GetCoordinate() if coord then return coord else BASE:E({"Cannot GetAverageCoordinate",Group=self,Alive=self:IsAlive()}) return nil end end end function GROUP:GetCoordinate() local vec3=self:GetVec3() local coord if vec3 then coord=COORDINATE:NewFromVec3(vec3) coord.Heading=self:GetHeading()or 0 return coord end local Units=self:GetUnits()or{} for _,_unit in pairs(Units)do local FirstUnit=_unit if FirstUnit and FirstUnit:IsAlive()then local FirstUnitCoordinate=FirstUnit:GetCoordinate() if FirstUnitCoordinate then local Heading=self:GetHeading()or 0 FirstUnitCoordinate.Heading=Heading return FirstUnitCoordinate end end end local DCSGroup=Group.getByName(self.GroupName) if DCSGroup then local DCSUnits=DCSGroup:getUnits()or{} for _,_unit in pairs(DCSUnits)do if Object.isExist(_unit)then local position=_unit:getPosition() local point=position.p~=nil and position.p or _unit:GetPoint() if point then local coord=COORDINATE:NewFromVec3(point) coord.Heading=0 local munit=UNIT:Find(_unit) if munit then coord.Heading=munit:GetHeading()or 0 end return coord end end end end BASE:E({"Cannot GetCoordinate",Group=self,Alive=self:IsAlive()}) end function GROUP:GetRandomVec3(Radius) local FirstUnit=self:GetUnit(1) if FirstUnit then local FirstUnitRandomPointVec3=FirstUnit:GetRandomVec3(Radius) return FirstUnitRandomPointVec3 end BASE:E({"Cannot GetRandomVec3",Group=self,Alive=self:IsAlive()}) return nil end function GROUP:GetHeading() local GroupSize=self:GetSize() local HeadingAccumulator=0 local n=0 local Units=self:GetUnits() if GroupSize then for _,unit in pairs(Units)do 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() 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() 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() local template=_DATABASE:GetGroupTemplate(GroupName) if template then return UTILS.DeepCopy(template) end return nil 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.InitCoord=coordinate return self end function GROUP:InitRadioCommsOnOff(switch) if switch==true or switch==nil then self.InitRespawnRadio=true else self.InitRespawnRadio=false end return self end function GROUP:InitRadioFrequency(frequency) self.InitRespawnFreq=frequency return self end function GROUP:InitRadioModulation(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) 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 local function TransFormRoute(Template,OldPos,NewPos) if Template.route and Template.route.points then for _,_point in ipairs(Template.route.points)do _point.x=_point.x-OldPos.x+NewPos.x _point.y=_point.y-OldPos.y+NewPos.y end end return Template end if self:IsAlive()then local OldPos=self:GetVec2() 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 local NewPos={x=Vec3.x,y=Vec3.z} if Reset==true then for UnitID,UnitData in pairs(self:GetUnits())do local GroupUnit=UnitData if GroupUnit:IsAlive()then local GroupUnitVec3=GroupUnit:GetVec3() if Zone then if self.InitRespawnRandomizePositionZone then GroupUnitVec3=Zone:GetRandomVec3() else if self.InitRespawnRandomizePositionInner and self.InitRespawnRandomizePositionOuter then GroupUnitVec3=COORDINATE:NewFromVec3(From):GetRandomVec3InRadius(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 end end Template=TransFormRoute(Template,OldPos,NewPos) elseif Reset==false then for UnitID,TemplateUnitData in pairs(Template.units)do 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=COORDINATE: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 end Template=TransFormRoute(Template,OldPos,NewPos) 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:ScheduleOnce(0.1,_DATABASE.Spawn,_DATABASE,Template) self:ResetEvents() return self end function GROUP:Teleport(Coordinate) self:InitZone(Coordinate) return self:Respawn(nil,false) end function GROUP:RespawnAtCurrentAirbase(SpawnTemplate,Takeoff,Uncontrolled) if self and self:IsAlive()then local airbase=self:GetCoordinate():GetClosestAirbase() if airbase then 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) 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() return UTILS.DeepCopy(_DATABASE.Templates.Groups[self.GroupName].Template) end function GROUP:GetTaskRoute() if _DATABASE.Templates.Groups[self.GroupName].Template and _DATABASE.Templates.Groups[self.GroupName].Template.route and _DATABASE.Templates.Groups[self.GroupName].Template.route.points then return UTILS.DeepCopy(_DATABASE.Templates.Groups[self.GroupName].Template.route.points) else return{} end end function GROUP:CopyRoute(Begin,End,Randomize,Radius) local Points={} local GroupName=string.match(self:GetName(),".*#") if GroupName then GroupName=GroupName:sub(1,-2) else GroupName=self:GetName() end 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]=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 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() local DCSGroup=self:GetDCSObject() if DCSGroup then local DCSUnit=DCSGroup:getUnit(1) if DCSUnit then local GroupInAir=DCSGroup:getUnit(1):inAir() return GroupInAir end end return nil end function GROUP:IsAirborne(AllUnits) 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("APC") 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")or self:HasAttribute("Tanks") local aaa=self:HasAttribute("AAA")and(not self:HasAttribute("SAM elements")) local ewr=self:HasAttribute("EWR") local ifv=self:HasAttribute("IFV") local sam=self:HasAttribute("SAM elements")or self:HasAttribute("Optical Tracker") 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 fighter then attribute=GROUP.Attribute.AIR_FIGHTER elseif bomber then attribute=GROUP.Attribute.AIR_BOMBER elseif awacs then attribute=GROUP.Attribute.AIR_AWACS elseif transportplane then attribute=GROUP.Attribute.AIR_TRANSPORTPLANE elseif tanker then attribute=GROUP.Attribute.AIR_TANKER elseif attackhelicopter then attribute=GROUP.Attribute.AIR_ATTACKHELO elseif transporthelo then attribute=GROUP.Attribute.AIR_TRANSPORTHELO elseif uav then attribute=GROUP.Attribute.AIR_UAV elseif ewr then attribute=GROUP.Attribute.GROUND_EWR elseif sam then attribute=GROUP.Attribute.GROUND_SAM elseif aaa then attribute=GROUP.Attribute.GROUND_AAA elseif artillery then attribute=GROUP.Attribute.GROUND_ARTILLERY elseif tank then attribute=GROUP.Attribute.GROUND_TANK elseif ifv then attribute=GROUP.Attribute.GROUND_IFV elseif apc then attribute=GROUP.Attribute.GROUND_APC elseif infantry then attribute=GROUP.Attribute.GROUND_INFANTRY 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) 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} 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()or{})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 or{})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 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) local switch=switch or false local DCSUnit=self:GetDCSObject() if DCSUnit then DCSUnit:enableEmission(switch) end return self end function GROUP:SetCommandInvisible(switch) return self:CommandSetInvisible(switch) end function GROUP:CommandSetInvisible(switch) if switch==nil then switch=false end local SetInvisible={id='SetInvisible',params={value=switch}} self:SetCommand(SetInvisible) return self end function GROUP:SetCommandImmortal(switch) return self:CommandSetImmortal(switch) end function GROUP:CommandSetImmortal(switch) if switch==nil then switch=false end local SetImmortal={id='SetImmortal',params={value=switch}} self:SetCommand(SetImmortal) return self end function GROUP:GetSkill() local unit=self:GetUnit(1) local name=unit:GetName() local skill=_DATABASE.Templates.Units[name].Template.skill or"Random" return skill end function GROUP:GetHighestThreat() local units=self:GetUnits() if units then local threat=nil;local maxtl=0 for _,_unit in pairs(units or{})do local unit=_unit if unit and unit:IsAlive()then local tl=unit:GetThreatLevel() if tl>maxtl then maxtl=tl threat=unit end end end return threat,maxtl end return nil,nil end function GROUP:GetCustomCallSign(ShortCallsign,Keepnumber,CallsignTranslations,CustomFunction,...) self:T("GetCustomCallSign") local callsign="Ghost 1" if self:IsAlive()then local IsPlayer=self:IsPlayer() local shortcallsign=self:GetCallsign()or"unknown91" local callsignroot=string.match(shortcallsign,'(%a+)')or"Ghost" local groupname=self:GetName() local callnumber=string.match(shortcallsign,"(%d+)$")or"91" local callnumbermajor=string.char(string.byte(callnumber,1)) local callnumberminor=string.char(string.byte(callnumber,2)) local personalized=false local playername=shortcallsign if IsPlayer then playername=self:GetPlayerName()end self:T2("GetCustomCallSign outcome = "..playername) if CustomFunction and IsPlayer then local arguments=arg or{} local callsign=CustomFunction(groupname,playername,unpack(arguments)) return callsign end if CallsignTranslations and CallsignTranslations[callsignroot]then callsignroot=CallsignTranslations[callsignroot] elseif IsPlayer and string.find(groupname,"#")then if Keepnumber then shortcallsign=string.match(groupname,"#(.+)")or"Ghost 111" else shortcallsign=string.match(groupname,"#%s*([%a]+)")or"Ghost" end personalized=true elseif IsPlayer and string.find(playername,"|")then shortcallsign=string.match(playername,"|%s*([%a]+)")or string.match(self:GetPlayerName(),"|%s*([%d]+)")or"Ghost" personalized=true end if personalized then shortcallsign=string.gsub(shortcallsign,"^%s*","") shortcallsign=string.gsub(shortcallsign,"%s*$","") if Keepnumber then return shortcallsign elseif ShortCallsign then callsign=shortcallsign.." "..callnumbermajor else callsign=shortcallsign.." "..callnumbermajor.." "..callnumberminor end return callsign end if ShortCallsign then callsign=callsignroot.." "..callnumbermajor else callsign=callsignroot.." "..callnumbermajor.." "..callnumberminor end end return callsign end function GROUP:SetAsRecoveryTanker(CarrierGroup,Speed,ToKIAS,Altitude,Delay,LastWaypoint) local speed=ToKIAS==true and UTILS.KnotsToAltKIAS(Speed,Altitude)or Speed speed=UTILS.KnotsToMps(speed) local alt=UTILS.FeetToMeters(Altitude) local delay=Delay or 1 local task=self:TaskRecoveryTanker(CarrierGroup,speed,alt,LastWaypoint) self:SetTask(task,delay) local tankertask=self:EnRouteTaskTanker() self:PushTask(tankertask,delay+2) return self end function GROUP:GetGroupSTN() local tSTN={} local units=self:GetUnits() local gname=self:GetName() gname=string.gsub(gname,"(#%d+)$","") local report=REPORT:New() report:Add("Link16 S/TN Report") report:Add("Group: "..gname) report:Add("==================") for _,_unit in pairs(units)do local unit=_unit if unit and unit:IsAlive()then local STN,VCL,VCN,Lead=unit:GetSTN() local name=unit:GetName() tSTN[name]={ STN=STN, VCL=VCL, VCN=VCN, Lead=Lead, } local lead=Lead==true and"(*)"or"" report:Add(string.format("| %s%s %s %s",tostring(VCL),tostring(VCN),tostring(STN),lead)) end end report:Add("==================") local text=report:Text() return tSTN,text end function GROUP:IsSAM() local issam=false local units=self:GetUnits() for _,_unit in pairs(units or{})do local unit=_unit if unit:IsSAM()then issam=true break end end return issam end function GROUP:IsAAA() local isAAA=false local units=self:GetUnits() for _,_unit in pairs(units or{})do local unit=_unit if unit:IsAAA()then isAAA=true break end end return isAAA end UNIT={ ClassName="UNIT", UnitName=nil, GroupName=nil, DCSUnit=nil, } function UNIT:Register(UnitName) local self=BASE:Inherit(self,CONTROLLABLE:New(UnitName)) self.UnitName=UnitName local unit=Unit.getByName(self.UnitName) if unit then local group=unit:getGroup() if group then self.GroupName=group:getName() self.groupId=group:getID() end self.DCSUnit=unit end 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:FindByMatching(Pattern) local GroupFound=nil for name,group in pairs(_DATABASE.UNITS)do if string.match(name,Pattern)then GroupFound=group break end end return GroupFound end function UNIT:FindAllByMatching(Pattern) local GroupsFound={} for name,group in pairs(_DATABASE.UNITS)do if string.match(name,Pattern)then GroupsFound[#GroupsFound+1]=group end end return GroupsFound end function UNIT:Name() return self.UnitName end function UNIT:GetDCSObject() if(not self.LastCallDCSObject)or(self.LastCallDCSObject and timer.getTime()-self.LastCallDCSObject>1)or(self.DCSObject==nil)or(self.DCSObject:isExist()==false)then local DCSUnit=Unit.getByName(self.UnitName) if DCSUnit then self.LastCallDCSObject=timer.getTime() self.DCSObject=DCSUnit return DCSUnit else self.DCSObject=nil self.LastCallDCSObject=nil end else return self.DCSObject end return nil end function UNIT:GetAltitude(FromGround) local DCSUnit=self:GetDCSObject() if DCSUnit then local altitude=0 local point=DCSUnit:getPoint() altitude=point.y if FromGround then local land=land.getHeight({x=point.x,y=point.z})or 0 altitude=altitude-land end return altitude end return nil end function UNIT:ReSpawnAt(Coordinate,Heading) local SpawnGroupTemplate=UTILS.DeepCopy(_DATABASE:GetGroupTemplateFromUnitName(self:Name())) local SpawnGroup=self:GetGroup() if SpawnGroup then local Vec3=SpawnGroup:GetVec3() SpawnGroupTemplate.x=Coordinate.x SpawnGroupTemplate.y=Coordinate.z for UnitID,UnitData in pairs(SpawnGroup:GetUnits()or{})do local GroupUnit=UnitData 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 end end end for UnitTemplateID,UnitTemplateData in pairs(SpawnGroupTemplate.units)do SpawnGroupTemplate.units[UnitTemplateID].unitId=nil if UnitTemplateData.name==self:Name()then SpawnGroupTemplate.units[UnitTemplateID].alt=Coordinate.y SpawnGroupTemplate.units[UnitTemplateID].x=Coordinate.x SpawnGroupTemplate.units[UnitTemplateID].y=Coordinate.z SpawnGroupTemplate.units[UnitTemplateID].heading=Heading else 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 SpawnGroupTemplate.units[UnitTemplateID].delete=true end end end end local i=1 while i<=#SpawnGroupTemplate.units do local UnitTemplateData=SpawnGroupTemplate.units[i] if UnitTemplateData.delete then table.remove(SpawnGroupTemplate.units,i) else i=i+1 end end SpawnGroupTemplate.groupId=nil _DATABASE:Spawn(SpawnGroupTemplate) end function UNIT:IsActive() local DCSUnit=self:GetDCSObject() if DCSUnit then local UnitIsActive=DCSUnit:isActive() return UnitIsActive end return nil end function UNIT:IsExist() local DCSUnit=self:GetDCSObject() if DCSUnit then local exists=DCSUnit:isExist() return exists end return nil end function UNIT:IsAlive() local DCSUnit=self:GetDCSObject() if DCSUnit and DCSUnit:isExist()then local UnitIsAlive=DCSUnit:isActive() return UnitIsAlive end return nil end function UNIT:IsDead() return not self:IsAlive() end function UNIT:GetCallsign() local DCSUnit=self:GetDCSObject() if DCSUnit then local UnitCallSign=DCSUnit:getCallsign() if UnitCallSign==""then UnitCallSign=DCSUnit:getName() end return UnitCallSign end return nil end function UNIT:IsPlayer() local group=self:GetGroup() if not group then return false end local template=group:GetTemplate() if(template==nil)or(template.units==nil)then local DCSObject=self:GetDCSObject() if DCSObject then if DCSObject:getPlayerName()~=nil then return true else return false end else return false end end local units=template.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() 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:GetNatoReportingName() local typename=self:GetTypeName() return UTILS.GetReportingName(typename) end function UNIT:GetNumber() local DCSUnit=self:GetDCSObject() if DCSUnit then local UnitNumber=DCSUnit:getNumber() return UnitNumber end return nil end function UNIT:GetSpeedMax() local Desc=self:GetDesc() if Desc then local SpeedMax=Desc.speedMax or 0 return SpeedMax*3.6 end return 0 end function UNIT:GetRange() 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() 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() 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"or typename=="KC130J"then system=1 elseif typename=="KC135BDA"then system=1 elseif typename=="KC135MPRS"then system=1 elseif typename=="S-3B Tanker"then system=1 elseif typename=="KC_10_Extender"then system=1 elseif typename=="KC_10_Extender_D"then system=0 end end return tanker,system end function UNIT:IsAmmoSupply() local typename=self:GetTypeName() if typename=="M 818"then return true elseif typename=="Ural-375"then return true elseif typename=="ZIL-135"then return true end return false end function UNIT:IsFuelSupply() local typename=self:GetTypeName() if typename=="M978 HEMTT Tanker"then return true elseif typename=="ATMZ-5"then return true elseif typename=="ATMZ-10"then return true elseif typename=="ATZ-5"then return true end return false end function UNIT:GetGroup() local UnitGroup=GROUP:FindByName(self.GroupName) if UnitGroup then return UnitGroup else local DCSUnit=self:GetDCSObject() if DCSUnit then local grp=DCSUnit:getGroup() if grp then local UnitGroup=GROUP:FindByName(grp:getName()) return UnitGroup end end end return nil end function UNIT:GetPrefix() local DCSUnit=self:GetDCSObject() if DCSUnit then local UnitPrefix=string.match(self.UnitName,".*#"):sub(1,-2) return UnitPrefix end return nil end function UNIT:GetAmmo() local DCSUnit=self:GetDCSObject() if DCSUnit then local UnitAmmo=DCSUnit:getAmmo() return UnitAmmo end return nil end function UNIT:SetUnitInternalCargo(mass) local DCSUnit=self:GetDCSObject() if DCSUnit then trigger.action.setUnitInternalCargo(DCSUnit:getName(),mass) end return self end function UNIT:GetAmmunition() local nammo=0 local nshells=0 local nrockets=0 local nmissiles=0 local nbombs=0 local narti=0 local nAPshells=0 local nHEshells=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 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 if ammotable[w].desc.warhead and ammotable[w].desc.warhead.explosiveMass and ammotable[w].desc.warhead.explosiveMass>0 then narti=narti+Nammo end if ammotable[w].desc.typeName and string.find(ammotable[w].desc.typeName,"_AP",1,true)then nAPshells=nAPshells+Nammo end if ammotable[w].desc.typeName and string.find(ammotable[w].desc.typeName,"_HE",1,true)then nHEshells=nHEshells+Nammo end 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 elseif MissileCategory==Weapon.MissileCategory.SAM then nmissiles=nmissiles+Nammo elseif MissileCategory==Weapon.MissileCategory.CRUISE then nmissiles=nmissiles+Nammo end end end end nammo=nshells+nrockets+nmissiles+nbombs return nammo,nshells,nrockets,nbombs,nmissiles,narti,nAPshells,nHEshells end function UNIT:HasAPShells() local _,_,_,_,_,_,shells=self:GetAmmunition() if shells>0 then return true else return false end end function UNIT:GetAPShells() local _,_,_,_,_,_,shells=self:GetAmmunition() return shells or 0 end function UNIT:GetHEShells() local _,_,_,_,_,_,_,shells=self:GetAmmunition() return shells or 0 end function UNIT:HasHEShells() local _,_,_,_,_,_,_,shells=self:GetAmmunition() if shells>0 then return true else return false end end function UNIT:HasArtiShells() local _,_,_,_,_,shells=self:GetAmmunition() if shells>0 then return true else return false end end function UNIT:GetArtiShells() local _,_,_,_,_,shells=self:GetAmmunition() return shells or 0 end function UNIT:GetSensors() local DCSUnit=self:GetDCSObject() if DCSUnit then local UnitSensors=DCSUnit:getSensors() return UnitSensors end return nil end function UNIT:HasSensors(...) local DCSUnit=self:GetDCSObject() if DCSUnit then local HasSensors=DCSUnit:hasSensors(unpack(arg)) return HasSensors end return nil end function UNIT:HasSEAD() 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 or UnitSEADAttributes["Optical Tracker"]and UnitSEADAttributes["Optical Tracker"]==true then HasSEAD=true end return HasSEAD end return nil end function UNIT:GetRadar() local DCSUnit=self:GetDCSObject() if DCSUnit then local UnitRadarOn,UnitRadarObject=DCSUnit:getRadar() return UnitRadarOn,UnitRadarObject end return nil,nil end function UNIT:GetFuel() local DCSUnit=self:GetDCSObject() if DCSUnit then local UnitFuel=DCSUnit:getFuel() return UnitFuel end return nil end function UNIT:GetUnits() 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() local DCSUnit=self:GetDCSObject() if DCSUnit and DCSUnit:isExist()then local UnitLife=DCSUnit:getLife() return UnitLife end return-1 end function UNIT:GetLife0() local DCSUnit=self:GetDCSObject() if DCSUnit then local UnitLife0=DCSUnit:getLife0() return UnitLife0 end return 0 end function UNIT:GetLifeRelative() if self and self:IsAlive()then local life0=self:GetLife0() local lifeN=self:GetLife() return lifeN/life0 end return-1 end function UNIT:GetDamageRelative() if self and self:IsAlive()then return 1-self:GetLifeRelative() end return 1 end function UNIT:GetDrawArgumentValue(AnimationArgument) local DCSUnit=self:GetDCSObject() if DCSUnit then local value=DCSUnit:getDrawArgumentValue(AnimationArgument or 0) return value end return 0 end function UNIT:GetUnitCategory() local DCSUnit=self:GetDCSObject() if DCSUnit then return DCSUnit:getDesc().category end return nil end function UNIT:GetCategoryName() 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 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={ [1]="Unarmed", [2]="Infantry", [3]="Old Tanks & APCs", [4]="Tanks & IFVs without ATGM", [5]="Tanks & IFV with ATGM", [6]="Modern Tanks", [7]="AAA", [8]="IR Guided SAMs", [9]="SR SAMs", [10]="MR SAMs", [11]="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"]or Attributes["EWR"]then ThreatLevel=1 end ThreatText=ThreatLevels[ThreatLevel+1] end if self:IsAir()then local ThreatLevels={ [1]="Unarmed", [2]="Tanker", [3]="AWACS", [4]="Transport Helicopter", [5]="UAV", [6]="Bomber", [7]="Strategic Bomber", [8]="Attack Helicopter", [9]="Battleplane", [10]="Multirole Fighter", [11]="Fighter" } if Attributes["Fighters"]then ThreatLevel=10 elseif Attributes["Multirole fighters"]then ThreatLevel=9 elseif Attributes["Interceptors"]then ThreatLevel=9 elseif Attributes["Battleplanes"]then ThreatLevel=8 elseif Attributes["Battle airplanes"]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={ [1]="Unarmed ship", [2]="Light armed ships", [3]="Corvettes", [4]="", [5]="Frigates", [6]="", [7]="Cruiser", [8]="", [9]="Destroyer", [10]="", [11]="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) 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 return true else return false end end return nil end function UNIT:IsFriendly(FriendlyCoalition) local DCSUnit=self:GetDCSObject() if DCSUnit then local UnitCoalition=DCSUnit:getCoalition() local IsFriendlyResult=(UnitCoalition==FriendlyCoalition) return IsFriendlyResult end return nil end function UNIT:IsShip() local DCSUnit=self:GetDCSObject() if DCSUnit then local UnitDescriptor=DCSUnit:getDesc() local IsShipResult=(UnitDescriptor.category==Unit.Category.SHIP) return IsShipResult end return nil end function UNIT:InAir(NoHeloCheck) 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 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) local switch=switch or false local DCSUnit=self:GetDCSObject() if DCSUnit then DCSUnit:enableEmission(switch) end return self end function UNIT:GetSkill() local name=self.UnitName local skill="Random" if _DATABASE.Templates.Units[name]and _DATABASE.Templates.Units[name].Template and _DATABASE.Templates.Units[name].Template.skill then skill=_DATABASE.Templates.Units[name].Template.skill or"Random" end return skill end function UNIT:GetSTN() local STN=nil local VCL=nil local VCN=nil local FGL=false local template=self:GetTemplate() if template then if template.AddPropAircraft then if template.AddPropAircraft.STN_L16 then STN=template.AddPropAircraft.STN_L16 elseif template.AddPropAircraft.SADL_TN then STN=template.AddPropAircraft.SADL_TN end VCN=template.AddPropAircraft.VoiceCallsignNumber VCL=template.AddPropAircraft.VoiceCallsignLabel end if template.datalinks and template.datalinks.Link16 and template.datalinks.Link16.settings then FGL=template.datalinks.Link16.settings.flightLead end if template.datalinks and template.datalinks.SADL and template.datalinks.SADL.settings then FGL=template.datalinks.SADL.settings.flightLead end end return STN,VCL,VCN,FGL end do function UNIT:SetAIOnOff(AIOnOff) local DCSUnit=self:GetDCSObject() if DCSUnit then local DCSController=DCSUnit:getController() if DCSController then DCSController:setOnOff(AIOnOff) return self end end return nil end function UNIT:SetAIOn() return self:SetAIOnOff(true) end function UNIT:SetAIOff() return self:SetAIOnOff(false) end end function UNIT:IsSAM() if self:HasSEAD()and self:IsGround()and(not self:HasAttribute("Mobile AAA"))then return true end return false end function UNIT:IsEWR() if self:IsGround()then local DCSUnit=self:GetDCSObject() if DCSUnit then local attrs=DCSUnit:getDesc().attributes return attrs["EWR"]==true end end return false end function UNIT:IsAAA() local unit=self local desc=unit:GetDesc()or{} local attr=desc.attributes or{} if unit:HasSEAD()then return false end if attr["AAA"]or attr["SAM related"]then return true end return false 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:FindByPlayerName(Name) local foundclient=nil _DATABASE:ForEachClient( function(client) if client:GetPlayerName()==Name then foundclient=client end end ) return foundclient 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:CountPlayers() return#self.Players or 0 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:T2({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 function CLIENT:GetUCID() local PID=NET.GetPlayerIDByName(nil,self:GetPlayerName()) return net.get_player_info(tonumber(PID),'ucid') end function CLIENT:GetPlayerInfo(Attribute) local PID=NET.GetPlayerIDByName(nil,self:GetPlayerName()) if PID then return net.get_player_info(tonumber(PID),Attribute) else return nil end end STATIC={ ClassName="STATIC", } function STATIC:Register(StaticName) local self=BASE:Inherit(self,POSITIONABLE:New(StaticName)) self.StaticName=StaticName local DCSStatic=StaticObject.getByName(self.StaticName) if DCSStatic then local Life0=DCSStatic:getLife()or 1 self.Life0=Life0 else self:E(string.format("Static object %s does not exist!",tostring(self.StaticName))) end return self end function STATIC:GetLife0() return self.Life0 or 1 end function STATIC:GetLife() local DCSStatic=StaticObject.getByName(self.StaticName) if DCSStatic then return DCSStatic:getLife()or 1 end return nil 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 function STATIC:FindByMatching(Pattern) local GroupFound=nil for name,static in pairs(_DATABASE.STATICS)do if string.match(name,Pattern)then GroupFound=static break end end return GroupFound end function STATIC:FindAllByMatching(Pattern) local GroupsFound={} for name,static in pairs(_DATABASE.STATICS)do if string.match(name,Pattern)then GroupsFound[#GroupsFound+1]=static end end return GroupsFound end function STATIC:GetStaticStorage() local name=self:GetName() local storage=STORAGE:NewFromStaticCargo(name) return storage end function STATIC:GetCargoWeight() local DCSObject=StaticObject.getByName(self.StaticName) local mass=-1 if DCSObject then mass=DCSObject:getCargoWeight()or 0 local masstxt=DCSObject:getCargoDisplayName()or"none" end return mass end AIRBASE={ ClassName="AIRBASE", CategoryName={ [Airbase.Category.AIRDROME]="Airdrome", [Airbase.Category.HELIPAD]="Helipad", [Airbase.Category.SHIP]="Ship", }, activerwyno=nil, } AIRBASE.Caucasus={ ["Anapa_Vityazevo"]="Anapa-Vityazevo", ["Batumi"]="Batumi", ["Beslan"]="Beslan", ["Gelendzhik"]="Gelendzhik", ["Gudauta"]="Gudauta", ["Kobuleti"]="Kobuleti", ["Krasnodar_Center"]="Krasnodar-Center", ["Krasnodar_Pashkovsky"]="Krasnodar-Pashkovsky", ["Krymsk"]="Krymsk", ["Kutaisi"]="Kutaisi", ["Maykop_Khanskaya"]="Maykop-Khanskaya", ["Mineralnye_Vody"]="Mineralnye Vody", ["Mozdok"]="Mozdok", ["Nalchik"]="Nalchik", ["Novorossiysk"]="Novorossiysk", ["Senaki_Kolkhi"]="Senaki-Kolkhi", ["Sochi_Adler"]="Sochi-Adler", ["Soganlug"]="Soganlug", ["Sukhumi_Babushara"]="Sukhumi-Babushara", ["Tbilisi_Lochini"]="Tbilisi-Lochini", ["Vaziani"]="Vaziani", } AIRBASE.Nevada={ ["Beatty"]="Beatty", ["Boulder_City"]="Boulder City", ["Creech"]="Creech", ["Echo_Bay"]="Echo Bay", ["Groom_Lake"]="Groom Lake", ["Henderson_Executive"]="Henderson Executive", ["Jean"]="Jean", ["Laughlin"]="Laughlin", ["Lincoln_County"]="Lincoln County", ["McCarran_International"]="McCarran International", ["Mesquite"]="Mesquite", ["Mina"]="Mina", ["Nellis"]="Nellis", ["North_Las_Vegas"]="North Las Vegas", ["Pahute_Mesa"]="Pahute Mesa", ["Tonopah"]="Tonopah", ["Tonopah_Test_Range"]="Tonopah Test Range", } AIRBASE.Normandy={ ["Abbeville_Drucat"]="Abbeville Drucat", ["Amiens_Glisy"]="Amiens-Glisy", ["Argentan"]="Argentan", ["Avranches_Le_Val_Saint_Pere"]="Avranches Le Val-Saint-Pere", ["Azeville"]="Azeville", ["Barville"]="Barville", ["Bazenville"]="Bazenville", ["Beaumont_le_Roger"]="Beaumont-le-Roger", ["Beauvais_Tille"]="Beauvais-Tille", ["Beny_sur_Mer"]="Beny-sur-Mer", ["Bernay_Saint_Martin"]="Bernay Saint Martin", ["Beuzeville"]="Beuzeville", ["Biggin_Hill"]="Biggin Hill", ["Biniville"]="Biniville", ["Broglie"]="Broglie", ["Brucheville"]="Brucheville", ["Cardonville"]="Cardonville", ["Carpiquet"]="Carpiquet", ["Chailey"]="Chailey", ["Chippelle"]="Chippelle", ["Conches"]="Conches", ["Cormeilles_en_Vexin"]="Cormeilles-en-Vexin", ["Creil"]="Creil", ["Cretteville"]="Cretteville", ["Cricqueville_en_Bessin"]="Cricqueville-en-Bessin", ["Deanland"]="Deanland", ["Deauville"]="Deauville", ["Detling"]="Detling", ["Deux_Jumeaux"]="Deux Jumeaux", ["Dinan_Trelivan"]="Dinan-Trelivan", ["Dunkirk_Mardyck"]="Dunkirk-Mardyck", ["Essay"]="Essay", ["Evreux"]="Evreux", ["Farnborough"]="Farnborough", ["Fecamp_Benouville"]="Fecamp-Benouville", ["Flers"]="Flers", ["Ford"]="Ford", ["Friston"]="Friston", ["Funtington"]="Funtington", ["Goulet"]="Goulet", ["Gravesend"]="Gravesend", ["Guyancourt"]="Guyancourt", ["Hauterive"]="Hauterive", ["Heathrow"]="Heathrow", ["High_Halden"]="High Halden", ["Kenley"]="Kenley", ["Lantheuil"]="Lantheuil", ["Le_Molay"]="Le Molay", ["Lessay"]="Lessay", ["Lignerolles"]="Lignerolles", ["Longues_sur_Mer"]="Longues-sur-Mer", ["Lonrai"]="Lonrai", ["Lymington"]="Lymington", ["Lympne"]="Lympne", ["Manston"]="Manston", ["Maupertus"]="Maupertus", ["Meautis"]="Meautis", ["Merville_Calonne"]="Merville Calonne", ["Needs_Oar_Point"]="Needs Oar Point", ["Odiham"]="Odiham", ["Orly"]="Orly", ["Picauville"]="Picauville", ["Poix"]="Poix", ["Ronai"]="Ronai", ["Rouen_Boos"]="Rouen-Boos", ["Rucqueville"]="Rucqueville", ["Saint_Andre_de_lEure"]="Saint-Andre-de-lEure", ["Saint_Aubin"]="Saint-Aubin", ["Saint_Omer_Wizernes"]="Saint-Omer Wizernes", ["Saint_Pierre_du_Mont"]="Saint Pierre du Mont", ["Sainte_Croix_sur_Mer"]="Sainte-Croix-sur-Mer", ["Sainte_Laurent_sur_Mer"]="Sainte-Laurent-sur-Mer", ["Sommervieu"]="Sommervieu", ["Stoney_Cross"]="Stoney Cross", ["Tangmere"]="Tangmere", ["Triqueville"]="Triqueville", ["Villacoublay"]="Villacoublay", ["Vrigny"]="Vrigny", ["West_Malling"]="West Malling", ["Eastchurch"]="Eastchurch", ["Headcorn"]="Headcorn", ["Hawkinge"]="Hawkinge", } AIRBASE.PersianGulf={ ["Abu_Dhabi_Intl"]="Abu Dhabi Intl", ["Abu_Musa_Island"]="Abu Musa Island", ["Al_Ain_Intl"]="Al Ain Intl", ["Al_Bateen"]="Al-Bateen", ["Al_Dhafra_AFB"]="Al Dhafra AFB", ["Al_Maktoum_Intl"]="Al Maktoum Intl", ["Al_Minhad_AFB"]="Al Minhad AFB", ["Bandar_Abbas_Intl"]="Bandar Abbas Intl", ["Bandar_Lengeh"]="Bandar Lengeh", ["Bandar_e_Jask"]="Bandar-e-Jask", ["Dubai_Intl"]="Dubai Intl", ["Fujairah_Intl"]="Fujairah Intl", ["Havadarya"]="Havadarya", ["Jiroft"]="Jiroft", ["Kerman"]="Kerman", ["Khasab"]="Khasab", ["Kish_Intl"]="Kish Intl", ["Lar"]="Lar", ["Lavan_Island"]="Lavan Island", ["Liwa_AFB"]="Liwa AFB", ["Qeshm_Island"]="Qeshm Island", ["Quasoura_airport"]="Quasoura_airport", ["Ras_Al_Khaimah_Intl"]="Ras Al Khaimah Intl", ["Sas_Al_Nakheel"]="Sas Al Nakheel", ["Sharjah_Intl"]="Sharjah Intl", ["Shiraz_Intl"]="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", ["Biggin_Hill"]="Biggin Hill", ["Detling"]="Detling", ["Dunkirk_Mardyck"]="Dunkirk Mardyck", ["Eastchurch"]="Eastchurch", ["Hawkinge"]="Hawkinge", ["Headcorn"]="Headcorn", ["High_Halden"]="High Halden", ["Lympne"]="Lympne", ["Manston"]="Manston", ["Merville_Calonne"]="Merville Calonne", ["Saint_Omer_Longuenesse"]="Saint Omer Longuenesse", } AIRBASE.Syria={ ["Abu_al_Duhur"]="Abu al-Duhur", ["Adana_Sakirpasa"]="Adana Sakirpasa", ["Akrotiri"]="Akrotiri", ["Al_Dumayr"]="Al-Dumayr", ["Al_Qusayr"]="Al Qusayr", ["Aleppo"]="Aleppo", ["Amman"]="Amman", ["An_Nasiriyah"]="An Nasiriyah", ["At_Tanf"]="At Tanf", ["Bassel_Al_Assad"]="Bassel Al-Assad", ["Beirut_Rafic_Hariri"]="Beirut-Rafic Hariri", ["Damascus"]="Damascus", ["Deir_ez_Zor"]="Deir ez-Zor", ["Ercan"]="Ercan", ["Eyn_Shemer"]="Eyn Shemer", ["Gaziantep"]="Gaziantep", ["Gazipasa"]="Gazipasa", ["Gecitkale"]="Gecitkale", ["H"]="H", ["H3"]="H3", ["H3_Northwest"]="H3 Northwest", ["H3_Southwest"]="H3 Southwest", ["H4"]="H4", ["Haifa"]="Haifa", ["Hama"]="Hama", ["Hatay"]="Hatay", ["Herzliya"]="Herzliya", ["Incirlik"]="Incirlik", ["Jirah"]="Jirah", ["Khalkhalah"]="Khalkhalah", ["Kharab_Ishk"]="Kharab Ishk", ["King_Abdullah_II"]="King Abdullah II", ["King_Hussein_Air_College"]="King Hussein Air College", ["Kingsfield"]="Kingsfield", ["Kiryat_Shmona"]="Kiryat Shmona", ["Kuweires"]="Kuweires", ["Lakatamia"]="Lakatamia", ["Larnaca"]="Larnaca", ["Marj_Ruhayyil"]="Marj Ruhayyil", ["Marj_as_Sultan_North"]="Marj as Sultan North", ["Marj_as_Sultan_South"]="Marj as Sultan South", ["Megiddo"]="Megiddo", ["Mezzeh"]="Mezzeh", ["Minakh"]="Minakh", ["Muwaffaq_Salti"]="Muwaffaq Salti", ["Naqoura"]="Naqoura", ["Nicosia"]="Nicosia", ["Palmyra"]="Palmyra", ["Paphos"]="Paphos", ["Pinarbashi"]="Pinarbashi", ["Prince_Hassan"]="Prince Hassan", ["Qabr_as_Sitt"]="Qabr as Sitt", ["Ramat_David"]="Ramat David", ["Rayak"]="Rayak", ["Rene_Mouawad"]="Rene Mouawad", ["Rosh_Pina"]="Rosh Pina", ["Ruwayshid"]="Ruwayshid", ["Sanliurfa"]="Sanliurfa", ["Sayqal"]="Sayqal", ["Shayrat"]="Shayrat", ["Tabqa"]="Tabqa", ["Taftanaz"]="Taftanaz", ["Tal_Siman"]="Tal Siman", ["Tha_lah"]="Tha'lah", ["Tiyas"]="Tiyas", ["Wujah_Al_Hajar"]="Wujah Al Hajar", ["Ben_Gurion"]="Ben Gurion", ["Hatzor"]="Hatzor", ["Palmashim"]="Palmashim", ["Tel_Nof"]="Tel Nof", } AIRBASE.MarianaIslands={ ["Andersen_AFB"]="Andersen AFB", ["Antonio_B_Won_Pat_Intl"]="Antonio B. Won Pat Intl", ["North_West_Field"]="North West Field", ["Olf_Orote"]="Olf Orote", ["Pagan_Airstrip"]="Pagan Airstrip", ["Rota_Intl"]="Rota Intl", ["Saipan_Intl"]="Saipan Intl", ["Tinian_Intl"]="Tinian Intl", } AIRBASE.SouthAtlantic={ ["Almirante_Schroeders"]="Almirante Schroeders", ["Comandante_Luis_Piedrabuena"]="Comandante Luis Piedrabuena", ["Cullen"]="Cullen", ["El_Calafate"]="El Calafate", ["Franco_Bianco"]="Franco Bianco", ["Gobernador_Gregores"]="Gobernador Gregores", ["Goose_Green"]="Goose Green", ["Gull_Point"]="Gull Point", ["Hipico_Flying_Club"]="Hipico Flying Club", ["Mount_Pleasant"]="Mount Pleasant", ["O_Higgins"]="O'Higgins", ["Pampa_Guanaco"]="Pampa Guanaco", ["Port_Stanley"]="Port Stanley", ["Porvenir"]="Porvenir", ["Puerto_Natales"]="Puerto Natales", ["Puerto_Santa_Cruz"]="Puerto Santa Cruz", ["Puerto_Williams"]="Puerto Williams", ["Punta_Arenas"]="Punta Arenas", ["Rio_Chico"]="Rio Chico", ["Rio_Gallegos"]="Rio Gallegos", ["Rio_Grande"]="Rio Grande", ["Rio_Turbio"]="Rio Turbio", ["San_Carlos_FOB"]="San Carlos FOB", ["San_Julian"]="San Julian", ["Tolhuin"]="Tolhuin", ["Ushuaia"]="Ushuaia", ["Ushuaia_Helo_Port"]="Ushuaia Helo Port", } AIRBASE.Sinai={ ["Abu_Rudeis"]="Abu Rudeis", ["Abu_Suwayr"]="Abu Suwayr", ["Al_Bahr_al_Ahmar"]="Al Bahr al Ahmar", ["Al_Ismailiyah"]="Al Ismailiyah", ["Al_Khatatbah"]="Al Khatatbah", ["Al_Mansurah"]="Al Mansurah", ["Al_Rahmaniyah_Air_Base"]="Al Rahmaniyah Air Base", ["As_Salihiyah"]="As Salihiyah", ["AzZaqaziq"]="AzZaqaziq", ["Baluza"]="Baluza", ["Ben_Gurion"]="Ben-Gurion", ["Beni_Suef"]="Beni Suef", ["Bilbeis_Air_Base"]="Bilbeis Air Base", ["Bir_Hasanah"]="Bir Hasanah", ["Birma_Air_Base"]="Birma Air Base", ["Borj_El_Arab_International_Airport"]="Borj El Arab International Airport", ["Cairo_International_Airport"]="Cairo International Airport", ["Cairo_West"]="Cairo West", ["Difarsuwar_Airfield"]="Difarsuwar Airfield", ["El_Arish"]="El Arish", ["El_Gora"]="El Gora", ["El_Minya"]="El Minya", ["Fayed"]="Fayed", ["Gebel_El_Basur_Air_Base"]="Gebel El Basur Air Base", ["Hatzerim"]="Hatzerim", ["Hatzor"]="Hatzor", ["Hurghada_International_Airport"]="Hurghada International Airport", ["Inshas_Airbase"]="Inshas Airbase", ["Jiyanklis_Air_Base"]="Jiyanklis Air Base", ["Kedem"]="Kedem", ["Kibrit_Air_Base"]="Kibrit Air Base", ["Kom_Awshim"]="Kom Awshim", ["Melez"]="Melez", ["Nevatim"]="Nevatim", ["Ovda"]="Ovda", ["Palmachim"]="Palmachim", ["Quwaysina"]="Quwaysina", ["Ramon_Airbase"]="Ramon Airbase", ["Ramon_International_Airport"]="Ramon International Airport", ["Sde_Dov"]="Sde Dov", ["Sharm_El_Sheikh_International_Airport"]="Sharm El Sheikh International Airport", ["St_Catherine"]="St Catherine", ["Tel_Nof"]="Tel Nof", ["Wadi_Abu_Rish"]="Wadi Abu Rish", ["Wadi_al_Jandali"]="Wadi al Jandali", } AIRBASE.Kola={ ["Banak"]="Banak", ["Bodo"]="Bodo", ["Ivalo"]="Ivalo", ["Jokkmokk"]="Jokkmokk", ["Kalixfors"]="Kalixfors", ["Kallax"]="Kallax", ["Kemi_Tornio"]="Kemi Tornio", ["Kirkenes"]="Kirkenes", ["Kiruna"]="Kiruna", ["Kuusamo"]="Kuusamo", ["Monchegorsk"]="Monchegorsk", ["Murmansk_International"]="Murmansk International", ["Olenya"]="Olenya", ["Rovaniemi"]="Rovaniemi", ["Severomorsk_1"]="Severomorsk-1", ["Severomorsk_3"]="Severomorsk-3", ["Vidsel"]="Vidsel", ["Vuojarvi"]="Vuojarvi", ["Andoya"]="Andoya", ["Alakourtti"]="Alakourtti", ["Kittila"]="Kittila", ["Bardufoss"]="Bardufoss", } AIRBASE.Afghanistan={ ["Bagram"]="Bagram", ["Bamyan"]="Bamyan", ["Bost"]="Bost", ["Camp_Bastion"]="Camp Bastion", ["Camp_Bastion_Heliport"]="Camp Bastion Heliport", ["Chaghcharan"]="Chaghcharan", ["Dwyer"]="Dwyer", ["Farah"]="Farah", ["Gardez"]="Gardez", ["Ghazni_Heliport"]="Ghazni Heliport", ["Herat"]="Herat", ["Jalalabad"]="Jalalabad", ["Kabul"]="Kabul", ["Kandahar"]="Kandahar", ["Kandahar_Heliport"]="Kandahar Heliport", ["Khost"]="Khost", ["Khost_Heliport"]="Khost Heliport", ["Maymana_Zahiraddin_Faryabi"]="Maymana Zahiraddin Faryabi", ["Nimroz"]="Nimroz", ["Qala_i_Naw"]="Qala i Naw", ["Sharana"]="Sharana", ["Shindand"]="Shindand", ["Shindand_Heliport"]="Shindand Heliport", ["Tarinkot"]="Tarinkot", ["Urgoon_Heliport"]="Urgoon Heliport", } AIRBASE.Iraq={ ["Baghdad_International_Airport"]="Baghdad International Airport", ["Sulaimaniyah_International_Airport"]="Sulaimaniyah International Airport", ["Al_Sahra_Airport"]="Al-Sahra Airport", ["Erbil_International_Airport"]="Erbil International Airport", ["Al_Taji_Airport"]="Al-Taji Airport", ["Al_Asad_Airbase"]="Al-Asad Airbase", ["Al_Salam_Airbase"]="Al-Salam Airbase", ["Balad_Airbase"]="Balad Airbase", ["Kirkuk_International_Airport"]="Kirkuk International Airport", ["Bashur_Airport"]="Bashur Airport", ["Al_Taquddum_Airport"]="Al-Taquddum Airport", ["Qayyarah_Airfield_West"]="Qayyarah Airfield West", ["K1_Base"]="K1 Base", } AIRBASE.TerminalType={ Runway=16, HelicopterOnly=40, Shelter=68, OpenMed=72, SmallSizeFighter=100, OpenBig=104, OpenMedOrBig=176, HelicopterUsable=216, FighterAircraft=244, FighterAircraftSmall=344, } AIRBASE.SpotStatus={ FREE="Free", OCCUPIED="Occupied", RESERVED="Reserved", } 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 or self.descriptors.typeName=="FARP_SINGLE_01"then self.isHelipad=true self.category=Airbase.Category.HELIPAD elseif self.category==Airbase.Category.SHIP then self.isShip=true if self.descriptors.typeName=="Oil rig"or self.descriptors.typeName=="Ga"then self.isHelipad=true self.isShip=false self.category=Airbase.Category.HELIPAD _DATABASE:AddStatic(AirbaseName) end else self:E("ERROR: Unknown airbase category!") end self:_InitRunways() local Nrunways=#self.runways if Nrunways>0 then self:SetActiveRunway() end self:_InitParkingSpots() if self.category==Airbase.Category.AIRDROME and(Nrunways==0 or self.NparkingTotal==self.NparkingTerminal[AIRBASE.TerminalType.HelicopterOnly])then self.category=Airbase.Category.HELIPAD self.isAirdrome=true self.isHelipad=true end local vec2=self:GetVec2() self:GetCoordinate() self.storage=_DATABASE:AddStorage(AirbaseName) 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 self:T2(string.format("Registered airbase %s",tostring(self.AirbaseName))) return self end function AIRBASE:_GetCategory() local name=self.AirbaseName local static=StaticObject.getByName(name) local airbase=Airbase.getByName(name) local unit=Unit.getByName(name) local text=string.format("\n=====================================================") text=text..string.format("\nAirbase %s:",name) if static then local oc,uc=static:getCategory() local ex=static:getCategoryEx() text=text..string.format("\nSTATIC: oc=%d, uc=%d, ex=%d",oc,uc,ex) text=text..string.format("\n--------------------------------------------------") end if unit then local oc,uc=unit:getCategory() local ex=unit:getCategoryEx() text=text..string.format("\nUNIT: oc=%d, uc=%d, ex=%d",oc,uc,ex) text=text..string.format("\n--------------------------------------------------") end if airbase then local oc,uc=airbase:getCategory() local ex=airbase:getCategoryEx() text=text..string.format("\nAIRBASE: oc=%d, uc=%d, ex=%d",oc,uc,ex) text=text..string.format("\n--------------------------------------------------") text=text..UTILS.PrintTableToLog(airbase:getDesc(),nil,true) end text=text..string.format("\n=====================================================") env.info(text) 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:GetWarehouse() local warehouse=nil local airbase=self:GetDCSObject() if airbase and Airbase.getWarehouse then warehouse=airbase:getWarehouse() end return warehouse end function AIRBASE:GetStorage() return self.storage end function AIRBASE:SetAutoCapture(Switch) local airbase=self:GetDCSObject() if airbase then airbase:autoCapture(Switch) end return self end function AIRBASE:SetAutoCaptureON() self:SetAutoCapture(true) return self end function AIRBASE:SetAutoCaptureOFF() self:SetAutoCapture(false) return self end function AIRBASE:IsAutoCapture() local airbase=self:GetDCSObject() local auto=nil if airbase then auto=airbase:autoCaptureIsOn() end return auto end function AIRBASE:SetCoalition(Coal) local airbase=self:GetDCSObject() if airbase then airbase:setCoalition(Coal) end return self 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:SetRadioSilentMode(Silent) local airbase=self:GetDCSObject() if airbase then airbase:setRadioSilentMode(Silent) end return self end function AIRBASE:GetRadioSilentMode() local silent=nil local airbase=self:GetDCSObject() if airbase then silent=airbase:getRadioSilentMode() end return silent 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 local function isClient(coord) local clients=_DATABASE.CLIENTS for clientname,_client in pairs(clients)do local client=_client if client and client.SpawnCoord then local dist=client.SpawnCoord:Get2DDistance(coord) if dist<2 then return true,clientname end end end return false,nil 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 park.ClientSpot,park.ClientName=isClient(park.Coordinate) park.AirbaseName=self.AirbaseName self.NparkingTotal=self.NparkingTotal+1 for _,terminalType in pairs(AIRBASE.TerminalType)do if self._CheckTerminalType(park.TerminalType,terminalType)then self.NparkingTerminal[terminalType]=self.NparkingTerminal[terminalType]+1 end end self.parkingByID[park.TerminalID]=park table.insert(self.parking,park) end self.NparkingTotal=self.NparkingTotal-self.NparkingTerminal[AIRBASE.TerminalType.Runway] 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 spot.AirbaseName=self.AirbaseName 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)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 spot.AirbaseName=self.AirbaseName 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 terminal 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=nil local _aircraftsize=23 local ax=23 local ay=7 local az=17 if group and group.ClassName=="GROUP"then aircraft=group:GetUnit(1) if aircraft then _aircraftsize,ax,ay,az=aircraft:GetObjectSize() end end local _nspots=nspots or group:GetSize() self:T(string.format("%s: Looking for %d parking spot(s) for aircraft of size %.1f m (x=%.1f,y=%.1f,z=%.1f) at terminal type %s.",airport,_nspots,_aircraftsize,ax,ay,az,tostring(terminaltype))) 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:T(string.format("%s: Parking spot id %d occupied.",airport,_termid)) else self:T(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:T(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 elseif termtype==AIRBASE.TerminalType.FighterAircraftSmall then if Term_Type==AIRBASE.TerminalType.OpenMed or Term_Type==AIRBASE.TerminalType.OpenBig or Term_Type==AIRBASE.TerminalType.Shelter or Term_Type==AIRBASE.TerminalType.SmallSizeFighter then match=true end end return match end function AIRBASE:GetRunways() return self.runways or{} end function AIRBASE:GetRunwayByName(Name) if Name==nil then return end if Name then for _,_runway in pairs(self.runways)do local runway=_runway local name=self:GetRunwayName(runway) if name==Name:upper()then return runway end end end self:E("ERROR: Could not find runway with name "..tostring(Name)) return nil end function AIRBASE:_InitRunways(IncludeInverse) if IncludeInverse==nil then IncludeInverse=true end local Runways={} local function _createRunway(name,course,width,length,center) local bearing=-1*course local heading=math.deg(bearing) local runway={} local namefromheading=math.floor(heading/10) if self.AirbaseName==AIRBASE.Syria.Beirut_Rafic_Hariri and math.abs(namefromheading-name)>1 then runway.name=string.format("%02d",tonumber(namefromheading)) else runway.name=string.format("%02d",tonumber(name)) end runway.magheading=tonumber(runway.name)*10 runway.heading=heading runway.width=width or 0 runway.length=length or 0 runway.center=COORDINATE:NewFromVec3(center) if runway.heading>360 then runway.heading=runway.heading-360 elseif runway.heading<0 then runway.heading=runway.heading+360 end if math.abs(runway.heading-runway.magheading)>60 then self:T(string.format("WARNING: Runway %s: heading=%.1f magheading=%.1f",runway.name,runway.heading,runway.magheading)) runway.heading=runway.heading-180 end if runway.heading>360 then runway.heading=runway.heading-360 elseif runway.heading<0 then runway.heading=runway.heading+360 end runway.position=runway.center:Translate(-runway.length/2,runway.heading) runway.endpoint=runway.center:Translate(runway.length/2,runway.heading) local init=runway.center:GetVec3() local width=runway.width/2 local L2=runway.length/2 local offset1={x=init.x+(math.cos(bearing+math.pi)*L2),y=init.z+(math.sin(bearing+math.pi)*L2)} local offset2={x=init.x-(math.cos(bearing+math.pi)*L2),y=init.z-(math.sin(bearing+math.pi)*L2)} local points={} points[1]={x=offset1.x+(math.cos(bearing+(math.pi/2))*width),y=offset1.y+(math.sin(bearing+(math.pi/2))*width)} points[2]={x=offset1.x+(math.cos(bearing-(math.pi/2))*width),y=offset1.y+(math.sin(bearing-(math.pi/2))*width)} points[3]={x=offset2.x+(math.cos(bearing-(math.pi/2))*width),y=offset2.y+(math.sin(bearing-(math.pi/2))*width)} points[4]={x=offset2.x+(math.cos(bearing+(math.pi/2))*width),y=offset2.y+(math.sin(bearing+(math.pi/2))*width)} runway.zone=ZONE_POLYGON_BASE:New(string.format("%s Runway %s",self.AirbaseName,runway.name),points) return runway end local airbase=self:GetDCSObject() if airbase then local runways=airbase:getRunways() self:T2(runways) if runways and#runways>0 then for _,rwy in pairs(runways)do self:T(rwy) local runway=_createRunway(rwy.Name,rwy.course,rwy.width,rwy.length,rwy.position) table.insert(Runways,runway) if IncludeInverse then local idx=tonumber(runway.name) local name2=tostring(idx-18) if idx<18 then name2=tostring(idx+18) end local runway=_createRunway(name2,rwy.course-math.pi,rwy.width,rwy.length,rwy.position) table.insert(Runways,runway) end end else self.runways={} return{} end end local rpairs={} for i,_ri in pairs(Runways)do local ri=_ri for j,_rj in pairs(Runways)do local rj=_rj if i0 end for i,j in pairs(rpairs)do local ri=Runways[i] local rj=Runways[j] local c0=ri.center local a=UTILS.VecTranslate(c0,1000,ri.heading) local b=UTILS.VecSubstract(rj.center,ri.center) b=UTILS.VecAdd(ri.center,b) local left=isLeft(c0,a,b) if left then ri.isLeft=false rj.isLeft=true else ri.isLeft=true rj.isLeft=false end end self.runways=Runways return Runways 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 or name==AIRBASE.MarianaIslands.Andersen_AFB 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(Name,PreferLeft) self:SetActiveRunwayTakeoff(Name,PreferLeft) self:SetActiveRunwayLanding(Name,PreferLeft) end function AIRBASE:SetActiveRunwayLanding(Name,PreferLeft) local runway=self:GetRunwayByName(Name) if not runway then runway=self:GetRunwayIntoWind(PreferLeft) end if runway then self:T(string.format("%s: Setting active runway for landing as %s",self.AirbaseName,self:GetRunwayName(runway))) else self:E("ERROR: Could not set the runway for landing!") end self.runwayLanding=runway return runway end function AIRBASE:GetActiveRunway() return self.runwayLanding,self.runwayTakeoff end function AIRBASE:GetActiveRunwayLanding() return self.runwayLanding end function AIRBASE:GetActiveRunwayTakeoff() return self.runwayTakeoff end function AIRBASE:SetActiveRunwayTakeoff(Name,PreferLeft) local runway=self:GetRunwayByName(Name) if not runway then runway=self:GetRunwayIntoWind(PreferLeft) end if runway then self:T(string.format("%s: Setting active runway for takeoff as %s",self.AirbaseName,self:GetRunwayName(runway))) else self:E("ERROR: Could not set the runway for takeoff!") end self.runwayTakeoff=runway return runway end function AIRBASE:GetRunwayIntoWind(PreferLeft) local runways=self:GetRunways() 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 if PreferLeft==nil or PreferLeft==runway.isLeft then 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 function AIRBASE:GetCategory() return self.category end function AIRBASE:GetCategoryName() return AIRBASE.CategoryName[self.category] end SCENERY={ ClassName="SCENERY", } function SCENERY:Register(SceneryName,SceneryObject) local self=BASE:Inherit(self,POSITIONABLE:New(SceneryName)) self.SceneryName=tostring(SceneryName) self.SceneryObject=SceneryObject if self.SceneryObject and self.SceneryObject.getLife then self.Life0=self.SceneryObject:getLife()or 0 else self.Life0=0 end self.Properties={} return self end function SCENERY:GetProperty(PropertyName) return self.Properties[PropertyName] end function SCENERY:HasProperty(PropertyName) return self.Properties[PropertyName]~=nil and true or false end function SCENERY:GetAllProperties() return self.Properties end function SCENERY:SetProperty(PropertyName,PropertyValue) self.Properties[PropertyName]=PropertyValue return self end function SCENERY:GetName() return self.SceneryName end function SCENERY:GetDCSObject() return self.SceneryObject end function SCENERY:GetLife() local life=0 if self.SceneryObject and self.SceneryObject.getLife then life=self.SceneryObject:getLife() if life>self.Life0 then self.Life0=math.floor(life*1.2) end end return life end function SCENERY:GetLife0() return self.Life0 or 0 end function SCENERY:IsAlive(Threshold) if not Threshold then return self:GetLife()>=1 and true or false else return self:GetRelativeLife()>Threshold and true or false end end function SCENERY:IsDead(Threshold) if not Threshold then return self:GetLife()<1 and true or false else return self:GetRelativeLife()<=Threshold and true or false end end function SCENERY:GetRelativeLife() local life=self:GetLife() local life0=self:GetLife0() if life==0 or life0==0 then return 0 end local rlife=math.floor((life/life0)*100) return rlife end function SCENERY:GetThreatLevel() return 0,"Scenery" end function SCENERY:FindByName(Name,Coordinate,Radius,Role) local radius=Radius or 100 local name=Name or"unknown" local scenery=nil local function SceneryScan(scoordinate,sradius,sname) if scoordinate~=nil then local Vec2=scoordinate:GetVec2() local scanzone=ZONE_RADIUS:New("Zone-"..sname,Vec2,sradius,true) scanzone:Scan({Object.Category.SCENERY}) local scanned=scanzone:GetScannedSceneryObjects() local rscenery=nil for _,_scenery in pairs(scanned)do local scenery=_scenery if tostring(scenery.SceneryName)==tostring(sname)then rscenery=scenery if Role then rscenery:SetProperty("ROLE",Role)end break end end return rscenery end return nil end if Coordinate then scenery=SceneryScan(Coordinate,radius,name) end return scenery end function SCENERY:FindByNameInZone(Name,Zone,Radius) local radius=Radius or 100 local name=Name or"unknown" if type(Zone)=="string"then Zone=ZONE:FindByName(Zone) end local coordinate=Zone:GetCoordinate() return self:FindByName(Name,coordinate,Radius,Zone:GetProperty("ROLE")) end function SCENERY:FindByZoneName(ZoneName) local zone=ZoneName if type(ZoneName)=="string"then zone=ZONE:FindByName(ZoneName) end local _id=zone:GetProperty('OBJECT ID') if not _id then BASE:E("**** Zone without object ID: "..ZoneName.." | Type: "..tostring(zone.ClassName)) if string.find(zone.ClassName,"POLYGON")then zone:Scan({Object.Category.SCENERY}) local scanned=zone:GetScannedSceneryObjects() for _,_scenery in(scanned)do local scenery=_scenery if scenery:IsAlive()then local role=zone:GetProperty("ROLE") if role then scenery:SetProperty("ROLE",role)end return scenery end end return nil else return self:FindByName(_id,zone:GetCoordinate(),nil,zone:GetProperty("ROLE")) end else return self:FindByName(_id,zone:GetCoordinate(),nil,zone:GetProperty("ROLE")) end end function SCENERY:FindAllByZoneName(ZoneName) local zone=ZoneName if type(ZoneName)=="string"then zone=ZONE:FindByName(ZoneName) end local _id=zone:GetProperty('OBJECT ID') if not _id then zone:Scan({Object.Category.SCENERY}) local scanned=zone:GetScannedSceneryObjects() if#scanned>0 then return scanned else return nil end else local obj=self:FindByName(_id,zone:GetCoordinate(),nil,zone:GetProperty("ROLE")) if obj then return{obj} else return nil end end end function SCENERY:Destroy() return self 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.1" function MARKER:New(Coordinate,Text) local self=BASE:Inherit(self,FSM:New()) self.coordinate=UTILS.DeepCopy(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:ReadWrite() self.readonly=false 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.tocoalition=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.tocoalition=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.tocoalition=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.tocoalition 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 local MarkID=EventData.MarkID self:T3(self.lid..string.format("Captured event MarkRemoved 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 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.text=tostring(EventData.MarkText) self:Changed(EventData) end 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 WEAPON={ ClassName="WEAPON", verbose=0, } WEAPON.version="0.1.0" function WEAPON:New(WeaponObject) if WeaponObject==nil then env.error("ERROR: Weapon object does NOT exist") return nil end local self=BASE:Inherit(self,POSITIONABLE:New("Weapon")) self.weapon=WeaponObject self.desc=WeaponObject:getDesc() self.category=self.desc.category if self:IsMissile()and self.desc.missileCategory then self.categoryMissile=self.desc.missileCategory if self.desc.guidance then self.guidance=self.desc.guidance end end self.typeName=WeaponObject:getTypeName()or"Unknown Type" self.name=WeaponObject:getName() self.coalition=WeaponObject:getCoalition() self.country=WeaponObject:getCountry() self.launcher=WeaponObject:getLauncher() self.launcherName="Unknown Launcher" if self.launcher then self.launcherName=self.launcher:getName() self.launcherUnit=UNIT:Find(self.launcher) end self.coordinate=COORDINATE:NewFromVec3(self.launcher:getPoint()) self.lid=string.format("[%s] %s | ",self.typeName,self.name) if self.launcherUnit then self.releaseHeading=self.launcherUnit:GetHeading() self.releaseAltitudeASL=self.launcherUnit:GetAltitude() self.releaseAltitudeAGL=self.launcherUnit:GetAltitude(true) self.releaseCoordinate=self.launcherUnit:GetCoordinate() self.releasePitch=self.launcherUnit:GetPitch() end self:SetTimeStepTrack() self:SetDistanceInterceptPoint() local text=string.format("Weapon v%s\nName=%s, TypeName=%s, Category=%s, Coalition=%d, Country=%d, Launcher=%s", self.version,self.name,self.typeName,self.category,self.coalition,self.country,self.launcherName) self:T(self.lid..text) self:T2(self.desc) return self end function WEAPON:SetVerbosity(VerbosityLevel) self.verbose=VerbosityLevel or 0 return self end function WEAPON:SetTimeStepTrack(TimeStep) self.dtTrack=TimeStep or 0.01 return self end function WEAPON:SetDistanceInterceptPoint(Distance) self.distIP=Distance or 50 return self end function WEAPON:SetMarkImpact(Switch) if Switch==false then self.impactMark=false else self.impactMark=true end return self end function WEAPON:SetSmokeImpact(Switch,SmokeColor) if Switch==false then self.impactSmoke=false else self.impactSmoke=true end self.impactSmokeColor=SmokeColor or SMOKECOLOR.Red return self end function WEAPON:SetFuncTrack(FuncTrack,...) self.trackFunc=FuncTrack self.trackArg=arg or{} return self end function WEAPON:SetFuncImpact(FuncImpact,...) self.impactFunc=FuncImpact self.impactArg=arg or{} return self end function WEAPON:GetLauncher() return self.launcherUnit end function WEAPON:GetTarget() local target=nil if self.weapon then local object=self.weapon:getTarget() if object then local category=Object.getCategory(object) local name=object:getName() if name then self:T(self.lid..string.format("Got Target Object %s, category=%d",name,category)) if category==Object.Category.UNIT then target=UNIT:FindByName(name) elseif category==Object.Category.STATIC then target=STATIC:FindByName(name,false) elseif category==Object.Category.SCENERY then self:E(self.lid..string.format("ERROR: Scenery target not implemented yet!")) else self:E(self.lid..string.format("ERROR: Object category=%d is not implemented yet!",category)) end end end end return target end function WEAPON:GetTargetDistance(ConversionFunction) local target=self:GetTarget() local distance=nil if target then local tv3=target:GetVec3() local wv3=self:GetVec3() if tv3 and wv3 then distance=UTILS.VecDist3D(tv3,wv3) if ConversionFunction then distance=ConversionFunction(distance) end end end return distance end function WEAPON:GetTargetName() local target=self:GetTarget() local name="None" if target then name=target:GetName() end return name end function WEAPON:GetVelocityVec3() local Vvec3=nil if self.weapon then Vvec3=self.weapon:getVelocity() end return Vvec3 end function WEAPON:GetSpeed(ConversionFunction) local speed=nil if self.weapon then local v=self:GetVelocityVec3() speed=UTILS.VecNorm(v) if ConversionFunction then speed=ConversionFunction(speed) end end return speed end function WEAPON:GetVec3() local vec3=nil if self.weapon then vec3=self.weapon:getPoint() end return vec3 end function WEAPON:GetVec2() local vec3=self:GetVec3() if vec3 then local vec2={x=vec3.x,y=vec3.z} return vec2 end return nil end function WEAPON:GetTypeName() return self.typeName end function WEAPON:GetCoalition() return self.coalition end function WEAPON:GetCountry() return self.country end function WEAPON:GetDCSObject() return self.weapon end function WEAPON:GetImpactVec3() return self.impactVec3 end function WEAPON:GetImpactCoordinate() return self.impactCoord end function WEAPON:GetReleaseHeading(AccountForMagneticInclination) AccountForMagneticInclination=AccountForMagneticInclination or true if AccountForMagneticInclination then return UTILS.ClampAngle(self.releaseHeading-UTILS.GetMagneticDeclination())else return UTILS.ClampAngle(self.releaseHeading)end end function WEAPON:GetReleaseAltitudeASL() return self.releaseAltitudeASL end function WEAPON:GetReleaseAltitudeAGL() return self.releaseAltitudeAGL end function WEAPON:GetReleaseCoordinate() return self.releaseCoordinate end function WEAPON:GetReleasePitch() return self.releasePitch end function WEAPON:GetImpactHeading(AccountForMagneticInclination) AccountForMagneticInclination=AccountForMagneticInclination or true if AccountForMagneticInclination then return UTILS.ClampAngle(self.impactHeading-UTILS.GetMagneticDeclination())else return self.impactHeading end end function WEAPON:InAir() local inAir=nil if self.weapon then inAir=self.weapon:inAir() end return inAir end function WEAPON:IsExist() local isExist=nil if self.weapon then isExist=self.weapon:isExist() end return isExist end function WEAPON:IsBomb() return self.category==Weapon.Category.BOMB end function WEAPON:IsMissile() return self.category==Weapon.Category.MISSILE end function WEAPON:IsRocket() return self.category==Weapon.Category.ROCKET end function WEAPON:IsShell() return self.category==Weapon.Category.SHELL end function WEAPON:IsTorpedo() return self.category==Weapon.Category.TORPEDO end function WEAPON:IsFoxOne() return self.guidance==Weapon.GuidanceType.RADAR_SEMI_ACTIVE end function WEAPON:IsFoxTwo() return self.guidance==Weapon.GuidanceType.IR end function WEAPON:IsFoxThree() return self.guidance==Weapon.GuidanceType.RADAR_ACTIVE end function WEAPON:Destroy(Delay) if Delay and Delay>0 then self:ScheduleOnce(Delay,WEAPON.Destroy,self,0) else if self.weapon then self:T(self.lid.."Destroying Weapon NOW!") self:StopTrack() self.weapon:destroy() end end return self end function WEAPON:StartTrack(Delay) Delay=math.max(Delay or 0.001,0.001) self:T(self.lid..string.format("Start tracking weapon in %.4f sec",Delay)) self.trackScheduleID=timer.scheduleFunction(WEAPON._TrackWeapon,self,timer.getTime()+Delay) return self end function WEAPON:StopTrack(Delay) if Delay and Delay>0 then self:ScheduleOnce(Delay,WEAPON.StopTrack,self,0) else if self.trackScheduleID then timer.removeFunction(self.trackScheduleID) end end return self end function WEAPON:_TrackWeapon(time) if self.verbose>=20 then self:I(self.lid..string.format("Tracking at T=%.5f",time)) end local status,pos3=pcall( function() local point=self.weapon:getPosition() return point end ) if status then self.pos3=pos3 self.vec3=UTILS.DeepCopy(self.pos3.p) self.coordinate:UpdateFromVec3(self.vec3) self.last_velocity=self.weapon:getVelocity() self.tracking=true if self.trackFunc then self.trackFunc(self,unpack(self.trackArg)) end if self.verbose>=5 then local vec2={x=self.vec3.x,y=self.vec3.z} local height=land.getHeight(vec2) local agl=self.vec3.y-height local ip=self:_GetIP(self.distIP) local d=0 if ip then d=UTILS.VecDist3D(self.vec3,ip) end self:I(self.lid..string.format("T=%.3f: Height=%.3f m AGL=%.3f m, dIP=%.3f",time,height,agl,d)) end else local ip=self:_GetIP(self.distIP) if self.verbose>=10 and ip then self:I(self.lid.."Got intercept point!") local coord=COORDINATE:NewFromVec3(ip) coord:MarkToAll("Intercept point") coord:SmokeBlue() local d=UTILS.VecDist3D(ip,self.vec3) self:I(self.lid..string.format("FF d(ip, vec3)=%.3f meters",d)) end self.impactVec3=ip or self.vec3 self.impactCoord=COORDINATE:NewFromVec3(self.vec3) self.impactHeading=UTILS.VecHdg(self.last_velocity) if self.impactMark then self.impactCoord:MarkToAll(string.format("Impact point of weapon %s\ntype=%s\nlauncher=%s",self.name,self.typeName,self.launcherName)) end if self.impactSmoke then self.impactCoord:Smoke(self.impactSmokeColor) end if self.impactFunc then self.impactFunc(self,unpack(self.impactArg or{})) end self.tracking=false end if self.tracking then if self.dtTrack and self.dtTrack>=0.00001 then return time+self.dtTrack else return nil end end return nil end function WEAPON:_GetIP(Distance) Distance=Distance or 50 local ip=nil if Distance>0 and self.pos3 then ip=land.getIP(self.pos3.p,self.pos3.x,Distance or 20) end return ip end do NET={ ClassName="NET", Version="0.1.4", BlockTime=600, BlockedPilots={}, BlockedUCIDs={}, BlockedSides={}, BlockedSlots={}, KnownPilots={}, BlockMessage=nil, UnblockMessage=nil, lid=nil, } function NET:New() local self=BASE:Inherit(self,FSM:New()) self.BlockTime=600 self.BlockedPilots={} self.KnownPilots={} self:SetBlockMessage() self:SetUnblockMessage() self.BlockedSides={} self.BlockedSides[1]=false self.BlockedSides[2]=false self:SetStartState("Stopped") self:AddTransition("Stopped","Run","Running") self:AddTransition("*","PlayerJoined","*") self:AddTransition("*","PlayerLeft","*") self:AddTransition("*","PlayerDied","*") self:AddTransition("*","PlayerEjected","*") self:AddTransition("*","PlayerBlocked","*") self:AddTransition("*","PlayerUnblocked","*") self:AddTransition("*","Status","*") self:AddTransition("*","Stop","Stopped") self.lid=string.format("NET %s | ",self.Version) self:Run() return self end function NET:IsAnyBlocked(UCID,Name,PlayerID,PlayerSide,PlayerSlot) self:T({UCID,Name,PlayerID,PlayerSide,PlayerSlot}) local blocked=false local TNow=timer.getTime() if UCID and self.BlockedUCIDs[UCID]and TNow3)then self:__PlayerJoined(1,client,name) self.KnownPilots[name]={ name=name, ucid=ucid, id=PlayerID, side=PlayerSide, slot=PlayerSlot, timestamp=TNow, } end return self end end if data.id==EVENTS.PlayerLeaveUnit and self.KnownPilots[name]then self:T(self.lid.."Pilot Leaving: "..name.." | UCID: "..ucid) self:__PlayerLeft(1,data.IniUnit,name) self.KnownPilots[name]=false return self end if data.id==EVENTS.Ejection and self.KnownPilots[name]then self:T(self.lid.."Pilot Ejecting: "..name.." | UCID: "..ucid) self:__PlayerEjected(1,data.IniUnit,name) self.KnownPilots[name]=false return self end if(data.id==EVENTS.PilotDead or data.id==EVENTS.SelfKillPilot or data.id==EVENTS.Crash)and self.KnownPilots[name]then self:T(self.lid.."Pilot Dead: "..name.." | UCID: "..ucid) self:__PlayerDied(1,data.IniUnit,name) self.KnownPilots[name]=false return self end end return self end function NET:BlockPlayer(Client,PlayerName,Seconds,Message) self:T({PlayerName,Seconds,Message}) local name=PlayerName if Client and(not PlayerName)then name=Client:GetPlayerName() elseif PlayerName then name=PlayerName else self:F(self.lid.."Block: No Client or PlayerName given or nothing found!") return self end local ucid=self:GetPlayerUCID(Client,name) local addon=Seconds or self.BlockTime self.BlockedPilots[name]=timer.getTime()+addon self.BlockedUCIDs[ucid]=timer.getTime()+addon local message=Message or self.BlockMessage if name then self:SendChatToPlayer(message,name) else self:SendChat(name..": "..message) end self:__PlayerBlocked(1,Client,name,Seconds) local PlayerID=self:GetPlayerIDByName(name) if PlayerID and tonumber(PlayerID)~=1 then local outcome=net.force_player_slot(tonumber(PlayerID),0,'') end return self end function NET:BlockPlayerSet(PlayerSet,Seconds,Message) self:T({PlayerSet.Set,Seconds,Message}) local addon=Seconds or self.BlockTime local message=Message or self.BlockMessage for _,_client in pairs(PlayerSet.Set)do local name=_client:GetPlayerName() self:BlockPlayer(_client,name,addon,message) end return self end function NET:UnblockPlayerSet(PlayerSet,Message) self:T({PlayerSet.Set,Seconds,Message}) local message=Message or self.UnblockMessage for _,_client in pairs(PlayerSet.Set)do local name=_client:GetPlayerName() self:UnblockPlayer(_client,name,message) end return self end function NET:BlockUCID(ucid,Seconds) self:T({ucid,Seconds}) local addon=Seconds or self.BlockTime self.BlockedUCIDs[ucid]=timer.getTime()+addon return self end function NET:UnblockUCID(ucid) self:T({ucid}) self.BlockedUCIDs[ucid]=nil return self end function NET:BlockSide(Side,Seconds) local addon=Seconds or self.BlockTime if Side==1 or Side==2 then self.BlockedSides[Side]=timer.getTime()+addon end return self end function NET:UnblockSide(Side,Seconds) local addon=Seconds or self.BlockTime if Side==1 or Side==2 then self.BlockedSides[Side]=false end return self end function NET:BlockSlot(Slot,Seconds) self:T({Slot,Seconds}) local addon=Seconds or self.BlockTime self.BlockedSlots[Slot]=timer.getTime()+addon return self end function NET:UnblockSlot(Slot) self:T({Slot}) self.BlockedSlots[Slot]=nil return self end function NET:UnblockPlayer(Client,PlayerName,Message) local name=PlayerName if Client then name=Client:GetPlayerName() elseif PlayerName then name=PlayerName else self:F(self.lid.."Unblock: No PlayerName given or not found!") return self end local ucid=self:GetPlayerUCID(Client,name) self.BlockedPilots[name]=nil self.BlockedUCIDs[ucid]=nil local message=Message or self.UnblockMessage if name then self:SendChatToPlayer(message,name) else self:SendChat(name..": "..message) end self:__PlayerUnblocked(1,Client,name) return self end function NET:SetBlockMessage(Text) self.BlockMessage=Text or"You are blocked from joining. Wait time is: "..self.BlockTime.." seconds!" return self end function NET:SetBlockTime(Seconds) self.BlockTime=Seconds or 600 return self end function NET:SetUnblockMessage(Text) self.UnblockMessage=Text or"You are unblocked now and can join again." return self end function NET:SendChat(Message,ToAll) if Message then net.send_chat(Message,ToAll) end return self end function NET:GetPlayerIDByName(Name) if not Name then return nil end local playerList=net.get_player_list() for i=1,#playerList do local playerName=net.get_name(i) if playerName==Name then return playerList[i] end end return nil end function NET:GetPlayerIDFromClient(Client) self:T("GetPlayerIDFromClient") self:T({Client=Client}) if Client then local name=Client:GetPlayerName() self:T({name=name}) local id=self:GetPlayerIDByName(name) return id else return nil end end function NET:SendChatToClient(Message,ToClient,FromClient) local PlayerId=self:GetPlayerIDFromClient(ToClient) local FromId=self:GetPlayerIDFromClient(FromClient) if Message and PlayerId and FromId then net.send_chat_to(Message,tonumber(PlayerId),tonumber(FromId)) elseif Message and PlayerId then net.send_chat_to(Message,tonumber(PlayerId)) end return self end function NET:SendChatToPlayer(Message,ToPlayer,FromPlayer) local PlayerId=self:GetPlayerIDByName(ToPlayer) local FromId=self:GetPlayerIDByName(FromPlayer) if Message and PlayerId and FromId then net.send_chat_to(Message,tonumber(PlayerId),tonumber(FromId)) elseif Message and PlayerId then net.send_chat_to(Message,tonumber(PlayerId)) end return self end function NET:GetPlayerList() local plist=nil plist=net.get_player_list() return plist end function NET:GetMyPlayerID() return net.get_my_player_id() end function NET:GetServerID() return net.get_server_id() end function NET:GetPlayerInfo(Client,Attribute) local PlayerID=self:GetPlayerIDFromClient(Client) if PlayerID then return net.get_player_info(tonumber(PlayerID),Attribute) else return nil end end function NET:GetPlayerUCID(Client,Name) local PlayerID=nil if Client then PlayerID=self:GetPlayerIDFromClient(Client) elseif Name then PlayerID=self:GetPlayerIDByName(Name) else self:E(self.lid.."Neither client nor name provided!") end local ucid=net.get_player_info(tonumber(PlayerID),'ucid') return ucid end function NET:Kick(Client,Message) local PlayerID=self:GetPlayerIDFromClient(Client) if PlayerID and tonumber(PlayerID)~=1 then return net.kick(tonumber(PlayerID),Message) else return false end end function NET:GetPlayerStatistic(Client,StatisticID) local PlayerID=self:GetPlayerIDFromClient(Client) local stats=StatisticID or 0 if stats>7 or stats<0 then stats=0 end if PlayerID then return net.get_stat(tonumber(PlayerID),stats) else return nil end end function NET:GetName(Client) local PlayerID=self:GetPlayerIDFromClient(Client) if PlayerID then return net.get_name(tonumber(PlayerID)) else return nil end end function NET:GetSlot(Client) self:T("NET.GetSlot") local PlayerID=self:GetPlayerIDFromClient(Client) self:T("NET.GetSlot PlayerID = "..tostring(PlayerID)) if PlayerID then local side,slot=net.get_slot(tonumber(PlayerID)) self:T("NET.GetSlot side, slot = "..tostring(side)..","..tostring(slot)) return side,slot else return nil,nil end end function NET:ForceSlot(Client,SideID,SlotID) local PlayerID=self:GetPlayerIDFromClient(Client) local SlotID=SlotID or Client:GetID() if PlayerID then return net.force_player_slot(tonumber(PlayerID),SideID,SlotID) else return false end end function NET:ReturnToSpectators(Client) local outcome=self:ForceSlot(Client,0) local sched=TIMER:New(Client.Destroy,Client,1):Start(1) return outcome end function NET.Lua2Json(Lua) return net.lua2json(Lua) end function NET.Json2Lua(Json) return net.json2lua(Json) end function NET:DoStringIn(State,DoString) return net.dostring_in(State,DoString) end function NET:Log(Message) net.log(Message) return self end function NET:GetKnownPilotData(Client,Name) local name=Name if Client and not Name then name=Client:GetPlayerName() end if name then return self.KnownPilots[name] else return nil end end function NET:onafterStatus(From,Event,To) self:T({From,Event,To}) local function HouseHold(tavolo) local TNow=timer.getTime() for _,entry in pairs(tavolo)do if type(entry)=="number"and entry>=TNow then entry=false end end end HouseHold(self.BlockedPilots) HouseHold(self.BlockedSides) HouseHold(self.BlockedSlots) HouseHold(self.BlockedUCIDs) if self:Is("Running")then self:__Status(-60) end return self end function NET:onafterRun(From,Event,To) self:T({From,Event,To}) self:HandleEvent(EVENTS.PlayerEnterUnit,self._EventHandler) self:HandleEvent(EVENTS.PlayerEnterAircraft,self._EventHandler) self:HandleEvent(EVENTS.PlayerLeaveUnit,self._EventHandler) self:HandleEvent(EVENTS.PilotDead,self._EventHandler) self:HandleEvent(EVENTS.Ejection,self._EventHandler) self:HandleEvent(EVENTS.Crash,self._EventHandler) self:HandleEvent(EVENTS.SelfKillPilot,self._EventHandler) self:__Status(-10) end function NET:onafterStop(From,Event,To) self:T({From,Event,To}) self:UnHandleEvent(EVENTS.PlayerEnterUnit) self:UnHandleEvent(EVENTS.PlayerEnterAircraft) self:UnHandleEvent(EVENTS.PlayerLeaveUnit) self:UnHandleEvent(EVENTS.PilotDead) self:UnHandleEvent(EVENTS.Ejection) self:UnHandleEvent(EVENTS.Crash) self:UnHandleEvent(EVENTS.SelfKillPilot) return self end end STORAGE={ ClassName="STORAGE", verbose=0, } STORAGE.Liquid={ JETFUEL=0, GASOLINE=1, MW50=2, DIESEL=3, } STORAGE.LiquidName={ GASOLINE="gasoline", DIESEL="diesel", MW50="methanol_mixture", JETFUEL="jet_fuel", } STORAGE.Type={ WEAPONS="weapons", LIQUIDS="liquids", AIRCRAFT="aircrafts", } STORAGE.version="0.1.5" function STORAGE:New(AirbaseName) local self=BASE:Inherit(self,BASE:New()) self.airbase=Airbase.getByName(AirbaseName) if Airbase.getWarehouse and self.airbase then self.warehouse=self.airbase:getWarehouse() end self.lid=string.format("STORAGE %s | ",AirbaseName) return self end function STORAGE:NewFromStaticCargo(StaticCargoName) local self=BASE:Inherit(self,BASE:New()) self.airbase=StaticObject.getByName(StaticCargoName) if Airbase.getWarehouse then self.warehouse=Warehouse.getCargoAsWarehouse(self.airbase) end self.lid=string.format("STORAGE %s | ",StaticCargoName) return self end function STORAGE:NewFromDynamicCargo(DynamicCargoName) local self=BASE:Inherit(self,BASE:New()) self.airbase=Unit.getByName(DynamicCargoName)or StaticObject.getByName(DynamicCargoName) if Airbase.getWarehouse then self.warehouse=Warehouse.getCargoAsWarehouse(self.airbase) end self.lid=string.format("STORAGE %s | ",DynamicCargoName) return self end function STORAGE:FindByName(AirbaseName) local storage=_DATABASE:FindStorage(AirbaseName) return storage end function STORAGE:SetVerbosity(VerbosityLevel) self.verbose=VerbosityLevel or 0 if self.verbose>1 then BASE:TraceOn() BASE:TraceClass("STORAGE") end return self end function STORAGE:AddItem(Name,Amount) self:T(self.lid..string.format("Adding %d items of %s",Amount,UTILS.OneLineSerialize(Name))) self.warehouse:addItem(Name,Amount) return self end function STORAGE:SetItem(Name,Amount) self:T(self.lid..string.format("Setting item %s to N=%d",UTILS.OneLineSerialize(Name),Amount)) self.warehouse:setItem(Name,Amount) return self end function STORAGE:GetItemAmount(Name) local N=self.warehouse:getItemCount(Name) return N end function STORAGE:RemoveItem(Name,Amount) self:T(self.lid..string.format("Removing N=%d of item %s",Amount,Name)) self.warehouse:removeItem(Name,Amount) return self end function STORAGE:AddLiquid(Type,Amount) self:T(self.lid..string.format("Adding %d liquids of %s",Amount,self:GetLiquidName(Type))) self.warehouse:addLiquid(Type,Amount) return self end function STORAGE:SetLiquid(Type,Amount) self:T(self.lid..string.format("Setting liquid %s to N=%d",self:GetLiquidName(Type),Amount)) self.warehouse:setLiquidAmount(Type,Amount) return self end function STORAGE:RemoveLiquid(Type,Amount) self:T(self.lid..string.format("Removing N=%d of liquid %s",Amount,self:GetLiquidName(Type))) self.warehouse:removeLiquid(Type,Amount) return self end function STORAGE:GetLiquidAmount(Type) local N=self.warehouse:getLiquidAmount(Type) return N end function STORAGE:GetLiquidName(Type) local name="Unknown" if Type==STORAGE.Liquid.JETFUEL then name="Jet fuel" elseif Type==STORAGE.Liquid.GASOLINE then name="Aircraft gasoline" elseif Type==STORAGE.Liquid.MW50 then name="MW 50" elseif Type==STORAGE.Liquid.DIESEL then name="Diesel" else self:E(self.lid..string.format("ERROR: Unknown liquid type %s",tostring(Type))) end return name end function STORAGE:AddAmount(Type,Amount) if type(Type)=="number"then self:AddLiquid(Type,Amount) else self:AddItem(Type,Amount) end return self end function STORAGE:RemoveAmount(Type,Amount) if type(Type)=="number"then self:RemoveLiquid(Type,Amount) else self:RemoveItem(Type,Amount) end return self end function STORAGE:SetAmount(Type,Amount) if type(Type)=="number"then self:SetLiquid(Type,Amount) else self:SetItem(Type,Amount) end return self end function STORAGE:GetAmount(Type) local N=0 if type(Type)=="number"then N=self:GetLiquidAmount(Type) else N=self:GetItemAmount(Type) end return N end function STORAGE:IsUnlimited(Type) local N=self:GetAmount(Type) local unlimited=false if N>0 then self:RemoveAmount(Type,1) local n=self:GetAmount(Type) unlimited=unlimited or n>2^29 or n==N if not unlimited then self:AddAmount(Type,1) end self:T(self.lid..string.format("Type=%s: unlimited=%s (N=%d n=%d)",tostring(Type),tostring(unlimited),N,n)) end return unlimited end function STORAGE:IsLimited(Type) local limited=not self:IsUnlimited(Type) return limited end function STORAGE:IsUnlimitedAircraft() local unlimited=self:IsUnlimited("A-10C") return unlimited end function STORAGE:IsUnlimitedLiquids() local unlimited=self:IsUnlimited(STORAGE.Liquid.DIESEL) return unlimited end function STORAGE:IsUnlimitedWeapons() local unlimited=self:IsUnlimited(ENUMS.Storage.weapons.bombs.Mk_82) return unlimited end function STORAGE:IsLimitedAircraft() local limited=self:IsLimited("A-10C") return limited end function STORAGE:IsLimitedLiquids() local limited=self:IsLimited(STORAGE.Liquid.DIESEL) return limited end function STORAGE:IsLimitedWeapons() local limited=self:IsLimited(ENUMS.Storage.weapons.bombs.Mk_82) return limited end function STORAGE:GetInventory(Item) local inventory=self.warehouse:getInventory(Item) return inventory.aircraft,inventory.liquids,inventory.weapon end function STORAGE:SaveToFile(Path,Filename) if not io then BASE:E("ERROR: io not desanitized. Can't save the files.") return false end if Path==nil and not lfs then BASE:E("WARNING: lfs not desanitized. File will be saved in DCS installation root directory rather than your given path.") end local ac,lq,wp=self:GetInventory() local DataAircraft="" local DataLiquids="" local DataWeapons="" if#lq>0 then DataLiquids=DataLiquids.."Liquids in Storage:\n" for key,amount in pairs(lq)do DataLiquids=DataLiquids..tostring(key).."="..tostring(amount).."\n" end UTILS.SaveToFile(Path,Filename.."_Liquids.csv",DataLiquids) if self.verbose and self.verbose>0 then self:I(self.lid.."Saving Liquids to "..tostring(Path).."\\"..tostring(Filename).."_Liquids.csv") end end if UTILS.TableLength(ac)>0 then DataAircraft=DataAircraft.."Aircraft in Storage:\n" for key,amount in pairs(ac)do DataAircraft=DataAircraft..tostring(key).."="..tostring(amount).."\n" end UTILS.SaveToFile(Path,Filename.."_Aircraft.csv",DataAircraft) if self.verbose and self.verbose>0 then self:I(self.lid.."Saving Aircraft to "..tostring(Path).."\\"..tostring(Filename).."_Aircraft.csv") end end if UTILS.TableLength(wp)>0 then DataWeapons=DataWeapons.."Weapons and Materiel in Storage:\n" for _,_category in pairs(ENUMS.Storage.weapons)do for _,_key in pairs(_category)do local amount=self:GetAmount(_key) if type(_key)=="table"then _key="{"..table.concat(_key,",").."}" end DataWeapons=DataWeapons..tostring(_key).."="..tostring(amount).."\n" end end for key,amount in pairs(ENUMS.Storage.weapons.Gazelle)do amount=self:GetItemAmount(ENUMS.Storage.weapons.Gazelle[key]) DataWeapons=DataWeapons.."ENUMS.Storage.weapons.Gazelle."..tostring(key).."="..tostring(amount).."\n" end for key,amount in pairs(ENUMS.Storage.weapons.CH47)do amount=self:GetItemAmount(ENUMS.Storage.weapons.CH47[key]) DataWeapons=DataWeapons.."ENUMS.Storage.weapons.CH47."..tostring(key).."="..tostring(amount).."\n" end for key,amount in pairs(ENUMS.Storage.weapons.UH1H)do amount=self:GetItemAmount(ENUMS.Storage.weapons.UH1H[key]) DataWeapons=DataWeapons.."ENUMS.Storage.weapons.UH1H."..tostring(key).."="..tostring(amount).."\n" end for key,amount in pairs(ENUMS.Storage.weapons.OH58)do amount=self:GetItemAmount(ENUMS.Storage.weapons.OH58[key]) DataWeapons=DataWeapons.."ENUMS.Storage.weapons.OH58."..tostring(key).."="..tostring(amount).."\n" end for key,amount in pairs(ENUMS.Storage.weapons.AH64D)do amount=self:GetItemAmount(ENUMS.Storage.weapons.AH64D[key]) DataWeapons=DataWeapons.."ENUMS.Storage.weapons.AH64D."..tostring(key).."="..tostring(amount).."\n" end UTILS.SaveToFile(Path,Filename.."_Weapons.csv",DataWeapons) if self.verbose and self.verbose>0 then self:I(self.lid.."Saving Weapons to "..tostring(Path).."\\"..tostring(Filename).."_Weapons.csv") end end return self end function STORAGE:LoadFromFile(Path,Filename) if not io then BASE:E("ERROR: io not desanitized. Can't read the files.") return false end if Path==nil and not lfs then BASE:E("WARNING: lfs not desanitized. File will be read from DCS installation root directory rather than your give path.") end if self:IsLimitedLiquids()then local Ok,Liquids=UTILS.LoadFromFile(Path,Filename.."_Liquids.csv") if Ok then if self.verbose and self.verbose>0 then self:I(self.lid.."Loading Liquids from "..tostring(Path).."\\"..tostring(Filename).."_Liquids.csv") end for _id,_line in pairs(Liquids)do if string.find(_line,"Storage")==nil then local tbl=UTILS.Split(_line,"=") local lqno=tonumber(tbl[1]) local lqam=tonumber(tbl[2]) self:SetLiquid(lqno,lqam) end end else self:E("File for Liquids could not be found: "..tostring(Path).."\\"..tostring(Filename"_Liquids.csv")) end end if self:IsLimitedAircraft()then local Ok,Aircraft=UTILS.LoadFromFile(Path,Filename.."_Aircraft.csv") if Ok then if self.verbose and self.verbose>0 then self:I(self.lid.."Loading Aircraft from "..tostring(Path).."\\"..tostring(Filename).."_Aircraft.csv") end for _id,_line in pairs(Aircraft)do if string.find(_line,"Storage")==nil then local tbl=UTILS.Split(_line,"=") local acname=tbl[1] local acnumber=tonumber(tbl[2]) self:SetAmount(acname,acnumber) end end else self:E("File for Aircraft could not be found: "..tostring(Path).."\\"..tostring(Filename"_Aircraft.csv")) end end if self:IsLimitedWeapons()then local Ok,Weapons=UTILS.LoadFromFile(Path,Filename.."_Weapons.csv") if Ok then if self.verbose and self.verbose>0 then self:I(self.lid.."Loading Weapons from "..tostring(Path).."\\"..tostring(Filename).."_Weapons.csv") end for _id,_line in pairs(Weapons)do if string.find(_line,"Storage")==nil then local tbl=UTILS.Split(_line,"=") local wpname=tbl[1] local wpnumber=tonumber(tbl[2]) if string.find(wpname,"{")==1 then wpname=string.gsub(wpname,"{","") wpname=string.gsub(wpname,"}","") local tbl=UTILS.Split(wpname,",") local wptbl={} for _id,_key in ipairs(tbl)do table.insert(wptbl,_id,_key) end self:SetAmount(wptbl,wpnumber) else self:SetAmount(wpname,wpnumber) end end end else self:E("File for Weapons could not be found: "..tostring(Path).."\\"..tostring(Filename"_Weapons.csv")) end end return self end function STORAGE:StartAutoSave(Path,Filename,Interval,LoadOnce) if LoadOnce~=false then self:LoadFromFile(Path,Filename) end local interval=Interval or 300 self.SaverTimer=TIMER:New(STORAGE.SaveToFile,self,Path,Filename) self.SaverTimer:Start(interval,interval) return self end function STORAGE:StopAutoSave() if self.SaverTimer and self.SaverTimer:IsRunning()then self.SaverTimer:Stop() self.SaverTimer=nil end return self end function STORAGE:FindSyriaHHelipadWarehouse(ZoneName) local findzone=ZONE:New(ZoneName) local base=world.getAirbases() for i=1,#base do local info={} info.callsign=Airbase.getCallsign(base[i]) info.id=Airbase.getID(base[i]) info.point=Airbase.getPoint(base[i]) info.coordinate=COORDINATE:NewFromVec3(info.point) info.DCSObject=base[i] if info.callsign=="H"and findzone:IsCoordinateInZone(info.coordinate)then info.warehouse=info.DCSObject:getWarehouse() info.Storage=STORAGE:New(info.callsign..info.id) info.Storage.airbase=info.DCSObject info.Storage.warehouse=info.warehouse return info.Storage end end end DYNAMICCARGO={ ClassName="DYNAMICCARGO", verbose=0, testing=false, Interval=10, } DYNAMICCARGO.Liquid={ JETFUEL=0, GASOLINE=1, MW50=2, DIESEL=3, } DYNAMICCARGO.LiquidName={ GASOLINE="gasoline", DIESEL="diesel", MW50="methanol_mixture", JETFUEL="jet_fuel", } DYNAMICCARGO.Type={ WEAPONS="weapons", LIQUIDS="liquids", AIRCRAFT="aircrafts", } DYNAMICCARGO.State={ NEW="NEW", LOADED="LOADED", UNLOADED="UNLOADED", REMOVED="REMOVED", } DYNAMICCARGO.AircraftTypes={ ["CH-47Fbl1"]="CH-47Fbl1", } DYNAMICCARGO.AircraftDimensions={ ["CH-47Fbl1"]={ ["width"]=4, ["height"]=6, ["length"]=11, ["ropelength"]=30, }, } DYNAMICCARGO.version="0.0.7" function DYNAMICCARGO:Register(CargoName) local self=BASE:Inherit(self,POSITIONABLE:New(CargoName)) self.StaticName=CargoName self.LastPosition=self:GetCoordinate() self.CargoState=DYNAMICCARGO.State.NEW self.Interval=DYNAMICCARGO.Interval or 10 local DCSObject=self:GetDCSObject() if DCSObject then local warehouse=STORAGE:NewFromDynamicCargo(CargoName) self.warehouse=warehouse end self.lid=string.format("DYNAMICCARGO %s",CargoName) self.Owner=string.match(CargoName,"^(.+)|%d%d:%d%d|PKG%d+")or"None" self.timer=TIMER:New(DYNAMICCARGO._UpdatePosition,self) self.timer:Start(self.Interval,self.Interval) if not _DYNAMICCARGO_HELOS then _DYNAMICCARGO_HELOS=SET_CLIENT:New():FilterAlive():FilterFunction(DYNAMICCARGO._FilterHeloTypes):FilterStart() end if self.testing then BASE:TraceOn() BASE:TraceClass("DYNAMICCARGO") end return self end function DYNAMICCARGO:GetDCSObject() local DCSStatic=StaticObject.getByName(self.StaticName)or Unit.getByName(self.StaticName) if DCSStatic then return DCSStatic end return nil end function DYNAMICCARGO:GetLastOwner() return self.Owner end function DYNAMICCARGO:IsNew() if self.CargoState and self.CargoState==DYNAMICCARGO.State.NEW then return true else return false end end function DYNAMICCARGO:IsLoaded() if self.CargoState and self.CargoState==DYNAMICCARGO.State.LOADED then return true else return false end end function DYNAMICCARGO:IsUnloaded() if self.CargoState and self.CargoState==DYNAMICCARGO.State.UNLOADED then return true else return false end end function DYNAMICCARGO:IsRemoved() if self.CargoState and self.CargoState==DYNAMICCARGO.State.REMOVED then return true else return false end end function DYNAMICCARGO:GetCratesNeeded() return 1 end function DYNAMICCARGO:WasDropped() return self.CargoState==DYNAMICCARGO.State.UNLOADED and true or false end function DYNAMICCARGO:GetType() return CTLD_CARGO.Enum.GCLOADABLE end function DYNAMICCARGO:GetLastPosition() return self.LastPosition end function DYNAMICCARGO:GetState() return self.CargoState end function DYNAMICCARGO:FindByName(Name) local storage=_DATABASE:FindDynamicCargo(Name) return storage end function DYNAMICCARGO:FindByMatching(Pattern) local GroupFound=nil for name,static in pairs(_DATABASE.DYNAMICCARGO)do if string.match(name,Pattern)then GroupFound=static break end end return GroupFound end function DYNAMICCARGO:FindAllByMatching(Pattern) local GroupsFound={} for name,static in pairs(_DATABASE.DYNAMICCARGO)do if string.match(name,Pattern)then GroupsFound[#GroupsFound+1]=static end end return GroupsFound end function DYNAMICCARGO:GetStorageObject() return self.warehouse end function DYNAMICCARGO:GetCargoWeight() local DCSObject=self:GetDCSObject() if DCSObject then local weight=DCSObject:getCargoWeight() return weight else return 0 end end function DYNAMICCARGO:GetCargoDisplayName() local DCSObject=self:GetDCSObject() if DCSObject then local weight=DCSObject:getCargoDisplayName() return weight else return self.StaticName end end function DYNAMICCARGO:_HeloHovering(Unit,ropelength) local DCSUnit=Unit:GetDCSObject() local hovering=false local Height=0 if DCSUnit then local UnitInAir=DCSUnit:inAir() local UnitCategory=DCSUnit:getDesc().category if UnitInAir==true and UnitCategory==1 then local VelocityVec3=DCSUnit:getVelocity() local Velocity=UTILS.VecNorm(VelocityVec3) local Coordinate=DCSUnit:getPoint() local LandHeight=land.getHeight({x=Coordinate.x,y=Coordinate.z}) Height=Coordinate.y-LandHeight if Velocity<1 and Height<=ropelength and Height>6 then hovering=true end end return hovering,Height end return false end function DYNAMICCARGO:_GetPossibleHeloNearby(pos,loading) local set=_DYNAMICCARGO_HELOS:GetAliveSet() local success=false local Helo=nil local Playername=nil for _,_helo in pairs(set or{})do local helo=_helo local name=helo:GetPlayerName()or _DATABASE:_FindPlayerNameByUnitName(helo:GetName())or"None" self:T(self.lid.." Checking: "..name) local hpos=helo:GetCoordinate() local typename=helo:GetTypeName() local dimensions=DYNAMICCARGO.AircraftDimensions[typename] local hovering,height=self:_HeloHovering(helo,dimensions.ropelength) local helolanded=not helo:InAir() self:T(self.lid.." InAir: AGL/Hovering: "..hpos.y-hpos:GetLandHeight().."/"..tostring(hovering)) if hpos and typename and dimensions then local delta2D=hpos:Get2DDistance(pos) local delta3D=hpos:Get3DDistance(pos) if self.testing then self:T(string.format("Cargo relative position: 2D %dm | 3D %dm",delta2D,delta3D)) self:T(string.format("Helo dimension: length %dm | width %dm | rope %dm",dimensions.length,dimensions.width,dimensions.ropelength)) self:T(string.format("Helo hovering: %s at %dm",tostring(hovering),height)) end if loading~=true and(delta2D>dimensions.length or delta2D>dimensions.width)and helolanded then success=true Helo=helo Playername=name end if loading~=true and delta3D>dimensions.ropelength then success=true Helo=helo Playername=name end if loading==true and((delta2D0.5 then if self.CargoState==DYNAMICCARGO.State.NEW or self.CargoState==DYNAMICCARGO.State.UNLOADED then local isloaded,client,playername=self:_GetPossibleHeloNearby(pos,true) self:T(self.lid.." moved! NEW -> LOADED by "..tostring(playername)) self.CargoState=DYNAMICCARGO.State.LOADED self.Owner=playername _DATABASE:CreateEventDynamicCargoLoaded(self) end elseif self.CargoState==DYNAMICCARGO.State.LOADED then local count=_DYNAMICCARGO_HELOS:CountAlive() local landheight=pos:GetLandHeight() local agl=pos.y-landheight agl=UTILS.Round(agl,2) self:T(self.lid.." AGL: "..agl or-1) local isunloaded=true local client local playername=self.Owner if count>0 then self:T(self.lid.." Possible alive helos: "..count or-1) isunloaded,client,playername=self:_GetPossibleHeloNearby(pos,false) if isunloaded then self:T(self.lid.." moved! LOADED -> UNLOADED by "..tostring(playername)) self.CargoState=DYNAMICCARGO.State.UNLOADED self.Owner=playername _DATABASE:CreateEventDynamicCargoUnloaded(self) end end end self.LastPosition=pos else if self.timer and self.timer:IsRunning()then self.timer:Stop()end self:T(self.lid.." dead! "..self.CargoState.."-> REMOVED") self.CargoState=DYNAMICCARGO.State.REMOVED _DATABASE:CreateEventDynamicCargoRemoved(self) end return self end function DYNAMICCARGO._FilterHeloTypes(client) if not client then return false end local typename=client:GetTypeName() local isinclude=DYNAMICCARGO.AircraftTypes[typename]~=nil and true or false return isinclude 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:T({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:T() 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:T({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:T({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:T({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:T({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:T({NearUnit=NearUnit}) local NearUnitCoalition=NearUnit:GetCoalition() local CargoCoalition=self:GetCoalition() if NearUnitCoalition==CargoCoalition then local Attributes=NearUnit:GetDesc() self:T({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:T({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:T({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:T() 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:T() 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:T() 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:T() 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:T() 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:T() 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:T() 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:T({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:T({"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:T({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:T({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:T({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:T({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:T({From,Event,To,CargoCarrier:GetName(),NearRadius=NearRadius}) self:T({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:T("Something is wrong") end end function CARGO_UNIT:onenterLoaded(From,Event,To,CargoCarrier) self:T({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:T({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:T({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:T({Coordinate=Coordinate}) end function CARGO_CRATE:IsNear(CargoCarrier,NearRadius) self:T({NearRadius=NearRadius}) return self:IsNear(CargoCarrier:GetCoordinate(),NearRadius) end function CARGO_CRATE:Respawn() self:T({"Respawning crate "..self:GetName()}) if self.CargoObject then self.CargoObject:ReSpawn() self:__Reset(-0.1) end end function CARGO_CRATE:onafterReset() self:T({"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:T({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:T({"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:T("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:T({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:T({"Regroup",GroupTemplate}) self.CargoObject=_DATABASE:Spawn(GroupTemplate) end end function CARGO_GROUP:OnEventCargoDead(EventData) self:T(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:T({"Cargo group destroyed"}) end end function CARGO_GROUP:onafterBoard(From,Event,To,CargoCarrier,NearRadius,...) self:T({CargoCarrier.UnitName,From,Event,To,NearRadius=NearRadius}) NearRadius=NearRadius or self.NearRadius self.CargoSet:ForEach( function(Cargo,...) self:T({"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:T("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:T({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:T({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:T("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:T({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:T({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={}, AutoSave=true, version="1.18.4" } 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,SavePath,AutoSave) 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:SetScoreIncrementOnHit(0) self:SetFratricide(self.ScaleDestroyPenalty*3) self.penaltyonfratricide=true self:SetCoalitionChangePenalty(self.ScaleDestroyPenalty) self.penaltyoncoalitionchange=true 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.AutoSavePath=SavePath self.AutoSave=AutoSave or true 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:AddScoreSetGroup(Set,Score) local set=Set:GetSetObjects() for _,_group in pairs(set)do if _group and _group:IsAlive()then self:AddScoreGroup(_group,Score) end end local function AddScore(group) self:AddScoreGroup(group,Score) end function Set:OnAfterAdded(From,Event,To,ObjectName,Object) AddScore(Object) 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:SetScoreIncrementOnHit(score) self.ScoreIncrementOnHit=score 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:SwitchFratricide(OnOff) self.penaltyonfratricide=OnOff return self end function SCORING:SwitchTreason(OnOff) self.penaltyoncoalitionchange=OnOff 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 and self.penaltyoncoalitionchange then self.Players[PlayerName].Penalty=self.Players[PlayerName].Penalty+self.CoalitionChangePenalty or 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). "..self.CoalitionChangePenalty.." penalty points added.", MESSAGE.Type.Information ):ToAll() self:ScoreCSV(PlayerName,"","COALITION_PENALTY",1,-1*self.CoalitionChangePenalty,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 if self.Players[PlayerName].Penalty>self.Fratricide*0.50 and self.penaltyonfratricide then if self.Players[PlayerName].PenaltyWarning<1 then MESSAGE:NewType(self.DisplayMessagePrefix.."Player '"..PlayerName.."': WARNING! If you continue to commit FRATRICIDE and have a PENALTY score higher than "..self.Fratricide..", you will be COURT MARTIALED and DISMISSED from this mission! \nYour total penalty is: "..self.Players[PlayerName].Penalty, MESSAGE.Type.Information) :ToAll() self.Players[PlayerName].PenaltyWarning=self.Players[PlayerName].PenaltyWarning+1 end end if self.Players[PlayerName].Penalty>self.Fratricide and self.penaltyonfratricide then MESSAGE:NewType(self.DisplayMessagePrefix.."Player '"..PlayerName.."' committed FRATRICIDE, he will be COURT MARTIALED and is DISMISSED from this mission!", MESSAGE.Type.Information) :ToAll() UnitData:GetGroup():Destroy() end 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 if Text then MESSAGE:NewType(self.DisplayMessagePrefix..Text, MESSAGE.Type.Information) :ToAll() end self:ScoreCSV(PlayerName,"","GOAL_"..string.upper(GoalTag),1,Score,nil) end end function SCORING:AddGoalScore(PlayerUnit,GoalTag,Text,Score) local PlayerName=PlayerUnit:GetPlayerName() self:T2({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 if Text then MESSAGE:NewType(self.DisplayMessagePrefix..Text, MESSAGE.Type.Information) :ToAll() end 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 if Text then MESSAGE:NewType(self.DisplayMessagePrefix..Mission:GetText().." : "..Text.." Score: "..Score, MESSAGE.Type.Information) :ToAll() end 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 if Text then MESSAGE:NewType(string.format("%s%s: %s! Player %s receives %d score!",self.DisplayMessagePrefix,Mission:GetText(),Text,PlayerName,Score),MESSAGE.Type.Information):ToAll() end 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 if Text then MESSAGE:NewType(self.DisplayMessagePrefix.."Player '"..PlayerName.."' has "..Text.." in "..Mission:GetText()..". "..Score.." mission score!", MESSAGE.Type.Information) :ToAll() end self:ScoreCSV(PlayerName,"","MISSION_"..MissionName:gsub(' ','_'),1,Score) end end end function SCORING:OnEventBirth(Event) if Event.IniUnit then Event.IniUnit.ThreatLevel,Event.IniUnit.ThreatType=Event.IniUnit:GetThreatLevel() if Event.IniObjectCategory==1 then local PlayerName=Event.IniUnit:GetPlayerName() Event.IniUnit.BirthTime=timer.getTime() if PlayerName then self:_AddPlayerFromUnit(Event.IniUnit) self.Players[PlayerName].PlayerKills=0 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 if PlayerHit.UNIT.ThreatType==nil then PlayerHit.ThreatLevel,PlayerHit.ThreatType=PlayerHit.UNIT:GetThreatLevel() if PlayerHit.ThreatType==nil or PlayerHit.ThreatType==""then PlayerHit.ThreatLevel=1 PlayerHit.ThreatType="Unknown" end else PlayerHit.ThreatLevel=PlayerHit.UNIT.ThreatLevel PlayerHit.ThreatType=PlayerHit.UNIT.ThreatType end 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 local Penalty=10 Player.Penalty=Player.Penalty+Penalty PlayerHit.Penalty=PlayerHit.Penalty+Penalty 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: -"..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: -"..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+self.ScoreIncrementOnHit PlayerHit.Score=PlayerHit.Score+self.ScoreIncrementOnHit 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 if PlayerHit.UNIT.ThreatType==nil then PlayerHit.ThreatLevel,PlayerHit.ThreatType=PlayerHit.UNIT:GetThreatLevel() if PlayerHit.ThreatType==nil then PlayerHit.ThreatLevel=1 PlayerHit.ThreatType="Unknown" end else PlayerHit.ThreatLevel=PlayerHit.UNIT.ThreatLevel PlayerHit.ThreatType=PlayerHit.UNIT.ThreatType end if timer.getTime()-PlayerHit.TimeStamp>1 then PlayerHit.TimeStamp=timer.getTime() local Score=0 if InitCoalition then if InitCoalition==TargetCoalition then local Penalty=10 Player.Penalty=Player.Penalty+Penalty PlayerHit.Penalty=PlayerHit.Penalty+Penalty PlayerHit.PenaltyHit=PlayerHit.PenaltyHit+1*self.ScaleDestroyPenalty MESSAGE :NewType(self.DisplayMessagePrefix.."Player '"..Event.WeaponPlayerName.."' hit friendly target ".. TargetUnitCategory.." ( "..TargetType.." ) ".. "Penalty: -"..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+self.ScoreIncrementOnHit PlayerHit.Score=PlayerHit.Score+self.ScoreIncrementOnHit PlayerHit.ScoreHit=PlayerHit.ScoreHit+1 MESSAGE:NewType(self.DisplayMessagePrefix.."Player '"..Event.WeaponPlayerName.."' hit enemy target "..TargetUnitCategory.." ( "..TargetType.." ) ".. "Score: "..PlayerHit.Score..". Score Total:"..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 and(TargetUnit.BirthTime==nil or Player.Hit[TargetCategory][TargetUnitName].TimeStamp>TargetUnit.BirthTime)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 self:OnKillPvP(PlayerName,TargetPlayerName,true) MESSAGE:NewType(self.DisplayMessagePrefix.."Player '"..PlayerName.."' destroyed friendly player '"..TargetPlayerName.."' "..TargetUnitCategory.." ( "..ThreatTypeTarget.." ) ".. "Penalty: -"..ThreatPenalty.." = "..Player.Score-Player.Penalty, MESSAGE.Type.Information) :ToAllIf(self:IfMessagesDestroy()and self:IfMessagesToAll()) :ToCoalitionIf(InitCoalition,self:IfMessagesDestroy()and self:IfMessagesToCoalition()) else self:OnKillPvE(PlayerName,TargetUnitName,true,TargetThreatLevel,Player.ThreatLevel,ThreatPenalty) MESSAGE:NewType(self.DisplayMessagePrefix.."Player '"..PlayerName.."' destroyed friendly target "..TargetUnitCategory.." ( "..ThreatTypeTarget.." ) ".. "Penalty: -"..ThreatPenalty.." = "..Player.Score-Player.Penalty, MESSAGE.Type.Information) :ToAllIf(self:IfMessagesDestroy()and self:IfMessagesToAll()) :ToCoalitionIf(InitCoalition,self:IfMessagesDestroy()and self:IfMessagesToCoalition()) end self:ScoreCSV(PlayerName,TargetPlayerName,"DESTROY_PENALTY",1,ThreatPenalty,InitUnitName,InitUnitCoalition,InitUnitCategory,InitUnitType,TargetUnitName,TargetUnitCoalition,TargetUnitCategory,TargetUnitType) Destroyed=true 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 if Player.PlayerKills~=nil then Player.PlayerKills=Player.PlayerKills+1 else Player.PlayerKills=1 end self:OnKillPvP(PlayerName,TargetPlayerName,false,TargetThreatLevel,Player.ThreatLevel,ThreatScore) MESSAGE:NewType(self.DisplayMessagePrefix.."Player '"..PlayerName.."' destroyed enemy player '"..TargetPlayerName.."' "..TargetUnitCategory.." ( "..ThreatTypeTarget.." ) ".. "Score: +"..ThreatScore.." = "..Player.Score-Player.Penalty, MESSAGE.Type.Information) :ToAllIf(self:IfMessagesDestroy()and self:IfMessagesToAll()) :ToCoalitionIf(InitCoalition,self:IfMessagesDestroy()and self:IfMessagesToCoalition()) else self:OnKillPvE(PlayerName,TargetUnitName,false,TargetThreatLevel,Player.ThreatLevel,ThreatScore) MESSAGE:NewType(self.DisplayMessagePrefix.."Player '"..PlayerName.."' destroyed enemy "..TargetUnitCategory.." ( "..ThreatTypeTarget.." ) ".. "Score: +"..ThreatScore.." = "..Player.Score-Player.Penalty, MESSAGE.Type.Information) :ToAllIf(self:IfMessagesDestroy()and self:IfMessagesToAll()) :ToCoalitionIf(InitCoalition,self:IfMessagesDestroy()and self:IfMessagesToCoalition()) end self:ScoreCSV(PlayerName,TargetPlayerName,"DESTROY_SCORE",1,ThreatScore,InitUnitName,InitUnitCoalition,InitUnitCategory,InitUnitType,TargetUnitName,TargetUnitCoalition,TargetUnitCategory,TargetUnitType) Destroyed=true 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()) self:ScoreCSV(PlayerName,"","DESTROY_SCORE",1,Score,InitUnitName,InitUnitCoalition,InitUnitCategory,InitUnitType,TargetUnitName,"","Scenery",TargetUnitType) Destroyed=true 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+PenaltyGoals+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+PenaltyGoals+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 local nHours=string.format("%02.f",math.floor(nSeconds/3600)); local nMins=string.format("%02.f",math.floor(nSeconds/60-(nHours*60))); local 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 and self.AutoSave==true then if ScoringCSV then self.ScoringCSV=ScoringCSV local path=self.AutoSavePath or lfs.writedir()..[[Logs\]] local fdir=path..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","PlayerUnitCoalition","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 and self.AutoSave 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 and self.AutoSave then self.CSVFile:close() end end function SCORING:SwitchAutoSave(OnOff) self.AutoSave=OnOff return self end function SCORING:OnKillPvP(PlayerName,TargetPlayerName,IsTeamKill,TargetThreatLevel,PlayerThreatLevel,Score) end function SCORING:OnKillPvE(PlayerName,TargetUnitName,IsTeamKill,TargetThreatLevel,PlayerThreatLevel,Score) 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, Padding=15, CallBack=nil, UseCallBack=false, debug=false, WeaponTrack=false, } SEAD.Harms={ ["AGM_88"]="AGM_88", ["AGM_122"]="AGM_122", ["AGM_84"]="AGM_84", ["AGM_45"]="AGM_45", ["AGM_65"]="AGM_65", ["ALARM"]="ALARM", ["LD-10"]="LD-10", ["X_58"]="X_58", ["X_28"]="X_28", ["X_25"]="X_25", ["X_31"]="X_31", ["Kh25"]="Kh25", ["BGM_109"]="BGM_109", ["AGM_154"]="AGM_154", ["HY-2"]="HY-2", ["ADM_141A"]="ADM_141A", } SEAD.HarmData={ ["AGM_88"]={150,3}, ["AGM_45"]={12,2}, ["AGM_65"]={16,0.9}, ["AGM_122"]={16.5,2.3}, ["AGM_84"]={280,0.8}, ["ALARM"]={45,2}, ["LD-10"]={60,4}, ["X_58"]={70,4}, ["X_28"]={80,2.5}, ["X_25"]={25,0.76}, ["X_31"]={150,3}, ["Kh25"]={25,0.8}, ["BGM_109"]={460,0.705}, ["AGM_154"]={130,0.61}, ["HY-2"]={90,1}, ["ADM_141A"]={126,0.6}, } function SEAD:New(SEADGroupPrefixes,Padding) local self=BASE:Inherit(self,FSM:New()) self:T(SEADGroupPrefixes) if type(SEADGroupPrefixes)=='table'then for SEADGroupPrefixID,SEADGroupPrefix in pairs(SEADGroupPrefixes)do self.SEADGroupPrefixes[SEADGroupPrefix]=SEADGroupPrefix end else self.SEADGroupPrefixes[SEADGroupPrefixes]=SEADGroupPrefixes end local padding=Padding or 10 if padding<10 then padding=10 end self.Padding=padding self.UseEmissionsOnOff=true self.debug=false self.CallBack=nil self.UseCallBack=false self:HandleEvent(EVENTS.Shot,self.HandleEventShot) self:SetStartState("Running") self:AddTransition("*","ManageEvasion","*") self:AddTransition("*","CalculateHitZone","*") self:I("*** SEAD - Started Version 0.4.9") return self end function SEAD:UpdateSet(SEADGroupPrefixes) self:T(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:T({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:SetPadding(Padding) self:T({Padding}) local padding=Padding or 10 if padding<10 then padding=10 end self.Padding=padding return self end function SEAD:SwitchEmissions(Switch) self:T({Switch}) self.UseEmissionsOnOff=Switch return self end function SEAD:AddCallBack(Object) self:T({Class=Object.ClassName}) self.CallBack=Object self.UseCallBack=true return self end function SEAD:_CheckHarms(WeaponName) self:T({WeaponName}) local hit=false local name="" for _,_name in pairs(SEAD.Harms)do if string.find(WeaponName,_name,1,true)then hit=true name=_name break end end return hit,name end function SEAD:_GetDistance(_point1,_point2) self:T("_GetDistance") if _point1 and _point2 then local distance1=_point1:Get2DDistance(_point2) local distance2=_point1:DistanceFromPointVec2(_point2) if distance1 and type(distance1)=="number"then return distance1 elseif distance2 and type(distance2)=="number"then return distance2 else self:E("*****Cannot calculate distance!") self:E({_point1,_point2}) return-1 end else self:E("******Cannot calculate distance!") self:E({_point1,_point2}) return-1 end end function SEAD:onafterCalculateHitZone(From,Event,To,SEADWeapon,pos0,height,SEADGroup,SEADWeaponName) self:T("**** Calculating hit zone for "..(SEADWeaponName or"None")) if SEADWeapon and SEADWeapon:isExist()then local position=SEADWeapon:getPosition() local mheight=height local wph=math.atan2(position.x.z,position.x.x) if wph<0 then wph=wph+2*math.pi end wph=math.deg(wph) local wpndata=SEAD.HarmData["AGM_88"] if string.find(SEADWeaponName,"154",1)then wpndata=SEAD.HarmData["AGM_154"] end local mveloc=math.floor(wpndata[2]*340.29) local c1=(2*mheight*9.81)/(mveloc^2) local c2=(mveloc^2)/9.81 local Ropt=c2*math.sqrt(c1+1) if height<=5000 then Ropt=Ropt*0.72 elseif height<=7500 then Ropt=Ropt*0.82 elseif height<=10000 then Ropt=Ropt*0.87 elseif height<=12500 then Ropt=Ropt*0.98 end for n=1,3 do local dist=Ropt-((n-1)*20000) local predpos=pos0:Translate(dist,wph) if predpos then local targetzone=ZONE_RADIUS:New("Target Zone",predpos:GetVec2(),20000) if self.debug then predpos:MarkToAll(string.format("height=%dm | heading=%d | velocity=%ddeg | Ropt=%dm",mheight,wph,mveloc,Ropt),false) targetzone:DrawZone(coalition.side.BLUE,{0,0,1},0.2,nil,nil,3,true) end local seadset=SET_GROUP:New():FilterPrefixes(self.SEADGroupPrefixes):FilterZones({targetzone}):FilterOnce() local tgtgrp=seadset:GetRandom() local _targetgroup=nil local _targetgroupname="none" local _targetskill="Random" if tgtgrp and tgtgrp:IsAlive()then _targetgroup=tgtgrp _targetgroupname=tgtgrp:GetName() _targetskill=tgtgrp:GetUnit(1):GetSkill() self:T("*** Found Target = ".._targetgroupname) self:ManageEvasion(_targetskill,_targetgroup,pos0,"AGM_88",SEADGroup,20) end end end end return self end function SEAD:onafterManageEvasion(From,Event,To,_targetskill,_targetgroup,SEADPlanePos,SEADWeaponName,SEADGroup,timeoffset,Weapon) local timeoffset=timeoffset or 0 if _targetskill=="Random"then local Skills={"Average","Good","High","Excellent"} _targetskill=Skills[math.random(1,4)] end if self.TargetSkill[_targetskill]then local _evade=math.random(1,100) if(_evade>self.TargetSkill[_targetskill].Evade)then self:T("*** SEAD - Evading") local _targetpos=_targetgroup:GetCoordinate() local _distance=self:_GetDistance(SEADPlanePos,_targetpos) local hit,data=self:_CheckHarms(SEADWeaponName) local wpnspeed=666 local reach=10 if hit then local wpndata=SEAD.HarmData[data] reach=wpndata[1]*1.1 local mach=wpndata[2] wpnspeed=math.floor(mach*340.29) if Weapon and Weapon:GetSpeed()>0 then wpnspeed=Weapon:GetSpeed() self:T(string.format("*** SEAD - Weapon Speed from WEAPON: %f m/s",wpnspeed)) end end local _tti=math.floor(_distance/wpnspeed)-timeoffset if _distance>0 then _distance=math.floor(_distance/1000) else _distance=0 end self:T(string.format("*** SEAD - target skill %s, distance %dkm, reach %dkm, tti %dsec",_targetskill,_distance,reach,_tti)) if reach>=_distance then self:T("*** SEAD - Shot in Reach") local function SuppressionStart(args) self:T(string.format("*** SEAD - %s Radar Off & Relocating",args[2])) local grp=args[1] local name=args[2] local attacker=args[3] if self.UseEmissionsOnOff then grp:EnableEmission(false) end grp:OptionAlarmStateGreen() grp:RelocateGroundRandomInRadius(20,300,false,false,"Diamond",true) if self.UseCallBack then local object=self.CallBack object:SeadSuppressionStart(grp,name,attacker) end end local function SuppressionStop(args) self:T(string.format("*** SEAD - %s Radar On",args[2])) local grp=args[1] local name=args[2] if self.UseEmissionsOnOff then grp:EnableEmission(true) end grp:OptionAlarmStateRed() grp:OptionEngageRange(self.EngagementRange) self.SuppressedGroups[name]=false if self.UseCallBack then local object=self.CallBack object:SeadSuppressionEnd(grp,name) end end local delay=math.random(self.TargetSkill[_targetskill].DelayOn[1],self.TargetSkill[_targetskill].DelayOn[2]) if delay>_tti then delay=delay/2 end if _tti>600 then delay=_tti-90 end local SuppressionStartTime=timer.getTime()+delay local SuppressionEndTime=timer.getTime()+delay+_tti+self.Padding+delay local _targetgroupname=_targetgroup:GetName() if not self.SuppressedGroups[_targetgroupname]then self:T(string.format("*** SEAD - %s | Parameters TTI %ds | Switch-Off in %ds",_targetgroupname,_tti,delay)) timer.scheduleFunction(SuppressionStart,{_targetgroup,_targetgroupname,SEADGroup},SuppressionStartTime) timer.scheduleFunction(SuppressionStop,{_targetgroup,_targetgroupname},SuppressionEndTime) self.SuppressedGroups[_targetgroupname]=true if self.UseCallBack then local object=self.CallBack object:SeadSuppressionPlanned(_targetgroup,_targetgroupname,SuppressionStartTime,SuppressionEndTime,SEADGroup) end end end end end return self end function SEAD:HandleEventShot(EventData) self:T({EventData.id}) local SEADWeapon=EventData.Weapon local SEADWeaponName=EventData.WeaponName or"None" if self:_CheckHarms(SEADWeaponName)then local SEADPlane=EventData.IniUnit if not SEADPlane then return self end local SEADGroup=EventData.IniGroup local SEADPlanePos=SEADPlane:GetCoordinate() local SEADUnit=EventData.IniDCSUnit local SEADUnitName=EventData.IniDCSUnitName local WeaponWrapper=WEAPON:New(EventData.Weapon) self:T("*** SEAD - Missile Launched = "..SEADWeaponName) self:T('*** SEAD - Weapon Match') if self.WeaponTrack==true then WeaponWrapper:SetFuncTrack(function(weapon)env.info(string.format("*** Weapon Speed: %d m/s",weapon:GetSpeed()or-1))end) WeaponWrapper:StartTrack(0.1) WeaponWrapper:StopTrack(30) end local _targetskill="Random" local _targetgroupname="none" local _target=EventData.Weapon:getTarget() if not _target or self.debug then self:E("***** SEAD - No target data for "..(SEADWeaponName or"None")) if string.find(SEADWeaponName,"AGM_88",1,true)or string.find(SEADWeaponName,"AGM_154",1,true)then self:T("**** Tracking AGM-88/154 with no target data.") local pos0=SEADPlane:GetCoordinate() local fheight=SEADPlane:GetHeight() self:__CalculateHitZone(20,SEADWeapon,pos0,fheight,SEADGroup,SEADWeaponName) end return self end local targetcat=Object.getCategory(_target) local _targetUnit=nil local _targetgroup=nil self:T(string.format("*** Targetcat = %d",targetcat)) if targetcat==Object.Category.UNIT then self:T("*** Target Category UNIT") _targetUnit=UNIT:Find(_target) if _targetUnit and _targetUnit:IsAlive()then _targetgroup=_targetUnit:GetGroup() _targetgroupname=_targetgroup:GetName() local _targetUnitName=_targetUnit:GetName() _targetUnit:GetSkill() _targetskill=_targetUnit:GetSkill() end elseif targetcat==Object.Category.STATIC then self:T("*** Target Category STATIC") local seadset=SET_GROUP:New():FilterPrefixes(self.SEADGroupPrefixes):FilterOnce() local targetpoint=_target:getPoint()or{x=0,y=0,z=0} local tgtcoord=COORDINATE:NewFromVec3(targetpoint) local tgtgrp=seadset:FindNearestGroupFromPointVec2(tgtcoord) if tgtgrp and tgtgrp:IsAlive()then _targetgroup=tgtgrp _targetgroupname=tgtgrp:GetName() _targetskill=tgtgrp:GetUnit(1):GetSkill() self:T("*** Found Target = ".._targetgroupname) end end local SEADGroupFound=false for SEADGroupPrefixID,SEADGroupPrefix in pairs(self.SEADGroupPrefixes)do self:T("Target = ".._targetgroupname.." | Prefix = "..SEADGroupPrefix) if string.find(_targetgroupname,SEADGroupPrefix,1,true)then SEADGroupFound=true self:T('*** SEAD - Group Match Found') break end end if SEADGroupFound==true then if string.find(SEADWeaponName,"ADM_141",1,true)then self:__ManageEvasion(2,_targetskill,_targetgroup,SEADPlanePos,SEADWeaponName,SEADGroup,2,WeaponWrapper) else self:ManageEvasion(_targetskill,_targetgroup,SEADPlanePos,SEADWeaponName,SEADGroup,0,WeaponWrapper) end end end return self 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,self.ReportTargetsSchedulerID=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 self.ReportTargetsScheduler:Remove(self.ReportTargetsSchedulerID) 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 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 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:T({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 if Airbase.ZoneBoundary then Airbase.ZoneBoundary=ZONE_POLYGON_BASE:New("Boundary "..AirbaseID,Airbase.ZoneBoundary) else Airbase.ZoneBoundary=_DATABASE:FindAirbase(AirbaseID):GetZone() end Airbase.ZoneRunways={} if Airbase.PointsRunways then for PointsRunwayID,PointsRunway in pairs(Airbase.PointsRunways)do Airbase.ZoneRunways[PointsRunwayID]=ZONE_POLYGON_BASE:New("Runway "..PointsRunwayID,PointsRunway) end 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:T(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:T(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_UNIVERSAL={ ClassName="ATC_GROUND_UNIVERSAL", Version="0.0.2", SetClient=nil, Airbases=nil, AirbaseList=nil, KickSpeed=nil, } function ATC_GROUND_UNIVERSAL:New(AirbaseList) local self=BASE:Inherit(self,BASE:New()) self:T({self.ClassName}) self.Airbases={} self.AirbaseList=AirbaseList if not self.AirbaseList then self.AirbaseList={} for _name,_base in pairs(_DATABASE.AIRBASES)do if _base and _base.isAirdrome==true then self.AirbaseList[_name]=_name self.Airbases[_name]={} end end else for _,_name in pairs(AirbaseList)do local airbase=_DATABASE:FindAirbase(_name) if airbase and(airbase.isAirdrome==true)then self.Airbases[_name]={} end end end self.SetClient=SET_CLIENT:New():FilterCategories("plane"):FilterStart() for AirbaseID,Airbase in pairs(self.Airbases)do if Airbase.ZoneBoundary then Airbase.ZoneBoundary=ZONE_POLYGON_BASE:New("Boundary "..AirbaseID,Airbase.ZoneBoundary) else Airbase.ZoneBoundary=_DATABASE:FindAirbase(AirbaseID):GetZone() end Airbase.ZoneRunways=AIRBASE:FindByName(AirbaseID):GetRunways() 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) self.KickSpeed=UTILS.KnotsToMps(10) self:SetMaximumKickSpeedMiph(30) return self end function ATC_GROUND_UNIVERSAL:SetAirbaseBoundaries(Airbase,Zone) self.Airbases[Airbase].ZoneBoundary=Zone return self end function ATC_GROUND_UNIVERSAL:SmokeRunways(SmokeColor) local SmokeColor=SmokeColor or SMOKECOLOR.Red for AirbaseID,Airbase in pairs(self.Airbases)do if Airbase.ZoneRunways then for _,_runwaydata in pairs(Airbase.ZoneRunways)do local runwaydata=_runwaydata runwaydata.zone:SmokeZone(SmokeColor) end end end return self end function ATC_GROUND_UNIVERSAL:DrawRunways(Color) local Color=Color or{1,0,0} for AirbaseID,Airbase in pairs(self.Airbases)do if Airbase.ZoneRunways then for _,_runwaydata in pairs(Airbase.ZoneRunways)do local runwaydata=_runwaydata runwaydata.zone:DrawZone(-1,Color) end end end return self end function ATC_GROUND_UNIVERSAL:DrawBoundaries(Color) local Color=Color or{1,0,0} for AirbaseID,Airbase in pairs(self.Airbases)do if Airbase.ZoneBoundary then Airbase.ZoneBoundary:DrawZone(-1,Color) end end return self end function ATC_GROUND_UNIVERSAL:SetKickSpeed(KickSpeed,Airbase) if not Airbase then self.KickSpeed=KickSpeed else self.Airbases[Airbase].KickSpeed=KickSpeed end return self end function ATC_GROUND_UNIVERSAL:SetKickSpeedKmph(KickSpeed,Airbase) self:SetKickSpeed(UTILS.KmphToMps(KickSpeed),Airbase) return self end function ATC_GROUND_UNIVERSAL:SetKickSpeedMiph(KickSpeedMiph,Airbase) self:SetKickSpeed(UTILS.MiphToMps(KickSpeedMiph),Airbase) return self end function ATC_GROUND_UNIVERSAL:SetMaximumKickSpeed(MaximumKickSpeed,Airbase) if not Airbase then self.MaximumKickSpeed=MaximumKickSpeed else self.Airbases[Airbase].MaximumKickSpeed=MaximumKickSpeed end return self end function ATC_GROUND_UNIVERSAL:SetMaximumKickSpeedKmph(MaximumKickSpeed,Airbase) self:SetMaximumKickSpeed(UTILS.KmphToMps(MaximumKickSpeed),Airbase) return self end function ATC_GROUND_UNIVERSAL:SetMaximumKickSpeedMiph(MaximumKickSpeedMiph,Airbase) self:SetMaximumKickSpeed(UTILS.MiphToMps(MaximumKickSpeedMiph),Airbase) return self end function ATC_GROUND_UNIVERSAL:_AirbaseMonitor() self:I("_AirbaseMonitor") self.SetClient:ForEachClient( function(Client) if Client:IsAlive()then local IsOnGround=Client:InAir()==false for AirbaseID,AirbaseMeta in pairs(self.Airbases)do self:T(AirbaseID,AirbaseMeta.KickSpeed) if AirbaseMeta.Monitor==true and Client:IsInZone(AirbaseMeta.ZoneBoundary)then local NotInRunwayZone=true if AirbaseMeta.ZoneRunways then for _,_runwaydata in pairs(AirbaseMeta.ZoneRunways)do local runwaydata=_runwaydata NotInRunwayZone=(Client:IsNotInZone(_runwaydata.zone)==true)and NotInRunwayZone or false end end if NotInRunwayZone then local Taxi=Client:GetState(self,"Taxi") if IsOnGround then self:T(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) Client:SetState(self,"Speeding",false) Client:SetState(self,"Warnings",0) end local Velocity=VELOCITY_POSITIONABLE:New(Client) local IsAboveRunway=Client:IsAboveRunway() self:T({IsAboveRunway,IsOnGround,Velocity:Get()}) if IsOnGround and not Taxi 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 Client:SetState(self,"Speeding",true) local SpeedingWarnings=Client:GetState(self,"Warnings") Client:SetState(self,"Warnings",SpeedingWarnings+1) Client:Message("Warning "..SpeedingWarnings.."/3! Airbase traffic rule violation! Slow down now! Your speed is ".. Velocity:ToString(),5,"ATC") 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 function ATC_GROUND_UNIVERSAL:Start(RepeatScanSeconds) RepeatScanSeconds=RepeatScanSeconds or 0.05 self.AirbaseMonitor=SCHEDULER:New(self,self._AirbaseMonitor,{self},0,RepeatScanSeconds) return self end ATC_GROUND_CAUCASUS={ ClassName="ATC_GROUND_CAUCASUS", } function ATC_GROUND_CAUCASUS:New(AirbaseNames) local self=BASE:Inherit(self,ATC_GROUND_UNIVERSAL:New(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,RepeatScanSeconds) end ATC_GROUND_NEVADA={ ClassName="ATC_GROUND_NEVADA", } function ATC_GROUND_NEVADA:New(AirbaseNames) local self=BASE:Inherit(self,ATC_GROUND_UNIVERSAL:New(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,RepeatScanSeconds) end ATC_GROUND_NORMANDY={ ClassName="ATC_GROUND_NORMANDY", } function ATC_GROUND_NORMANDY:New(AirbaseNames) local self=BASE:Inherit(self,ATC_GROUND_UNIVERSAL:New(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,RepeatScanSeconds) end ATC_GROUND_PERSIANGULF={ ClassName="ATC_GROUND_PERSIANGULF", } function ATC_GROUND_PERSIANGULF:New(AirbaseNames) local self=BASE:Inherit(self,ATC_GROUND_UNIVERSAL:New(AirbaseNames)) self:SetKickSpeedKmph(50) self:SetMaximumKickSpeedKmph(150) end function ATC_GROUND_PERSIANGULF:Start(RepeatScanSeconds) RepeatScanSeconds=RepeatScanSeconds or 0.05 self.AirbaseMonitor=SCHEDULER:New(self,self._AirbaseMonitor,{self},0,RepeatScanSeconds) end ATC_GROUND_MARIANAISLANDS={ ClassName="ATC_GROUND_MARIANAISLANDS", } function ATC_GROUND_MARIANAISLANDS:New(AirbaseNames) local self=BASE:Inherit(self,ATC_GROUND_UNIVERSAL:New(AirbaseNames)) self:SetKickSpeedKmph(50) self:SetMaximumKickSpeedKmph(150) return self end function ATC_GROUND_MARIANAISLANDS:Start(RepeatScanSeconds) RepeatScanSeconds=RepeatScanSeconds or 0.05 self.AirbaseMonitor=SCHEDULER:New(self,self._AirbaseMonitor,{self},0,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.15 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:T({DetectedObjects=self.DetectedObjects}) self.DetectionRun=self.DetectionRun+1 local HasDetectedObjects=false if Detection and Detection:IsAlive()then self:T({"DetectionGroup is Alive",Detection:GetName()}) local DetectionGroupName=Detection:GetName() local DetectionUnit=Detection:GetFirstUnitAlive() local DetectedUnits={} local DetectedTargets=DetectionUnit:GetDetectedTargets( self.DetectVisual, self.DetectOptical, self.DetectRadar, self.DetectIRST, self.DetectRWR, self.DetectDLINK ) for DetectionObjectID,Detection in pairs(DetectedTargets or{})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 or{})do local DetectedObject=DetectedObjectData.Object if DetectedObject:isExist()then local TargetIsDetected,TargetIsVisible,TargetKnowType,TargetKnowDistance,TargetLastTime,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()or{x=0,y=0,z=0} 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:IsVec2InZone(DetectedObjectVec2)==true then DetectionAccepted=false end end end if self.RadarBlur then MESSAGE:New("Radar Blur",10):ToLogIf(self.debug):ToAllIf(self.verbose) local minheight=self.RadarBlurMinHeight or 250 local thresheight=self.RadarBlurThresHeight or 90 local thresblur=self.RadarBlurThresBlur or 85 local dist=math.floor(Distance) if dist<=self.RadarBlurClosing then thresheight=(((dist*dist)/self.RadarBlurClosingSquare)*thresheight) thresblur=(((dist*dist)/self.RadarBlurClosingSquare)*thresblur) end local fheight=math.floor(math.random(1,10000)/100) local fblur=math.floor(math.random(1,10000)/100) local unit=UNIT:FindByName(DetectedObjectName) if unit and unit:IsAlive()then local AGL=unit:GetAltitude(true) MESSAGE:New("Unit "..DetectedObjectName.." is at "..math.floor(AGL).."m. Distance "..math.floor(Distance).."km.",10):ToLogIf(self.debug):ToAllIf(self.verbose) MESSAGE:New(string.format("fheight = %d/%d | fblur = %d/%d",fheight,thresheight,fblur,thresblur),10):ToLogIf(self.debug):ToAllIf(self.verbose) if fblur>thresblur then DetectionAccepted=false end if AGL<=minheight and fheight0 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 function DETECTION_BASE:SetRadarBlur(minheight,thresheight,thresblur,closing) self.RadarBlur=true self.RadarBlurMinHeight=minheight or 250 self.RadarBlurThresHeight=thresheight or 90 self.RadarBlurThresBlur=thresblur or 85 self.RadarBlurClosing=closing or 20 self.RadarBlurClosingSquare=self.RadarBlurClosing*self.RadarBlurClosing 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,nil,true):Text(", ") DetectedReport:Add(string.rep("-",40)) 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={} 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 TargetSetUnit==nil then return 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 if DCSdesc.box then self.aircraft.length=DCSdesc.box.max.x self.aircraft.height=DCSdesc.box.max.y self.aircraft.width=DCSdesc.box.max.z elseif DCStype=="Mirage-F1CE"then self.aircraft.length=16 self.aircraft.height=5 self.aircraft.width=9 elseif DCStype=="Saab340"then self.aircraft.length=19.73 self.aircraft.height=6.97 self.aircraft.width=21.44 end 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:FindByName(_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:FindByName(_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:FindByName(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:FindByName(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()and(group:GetCoordinate()or group:GetVec3())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()or group:GetVec3() local alt=1000 if coords then alt=coords.y or 1000 end 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={}, planned={}, 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.planned[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 self.planned[i]=self.planned[i]+1 SCHEDULER:New(nil,RATMANAGER._Spawn,{self,i},time) end end end function RATMANAGER:_Spawn(i) local rat=self.rat[i] rat:_SpawnWithRoute() self.planned[i]=self.planned[i]-1 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, planned=%d",self.name[i],n,self.planned[i]) 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 local a=alive[i]+self.planned[i] N[#N+1]=0 M[#M+1]=math.max(a,min[i]) P[#P+1]=math.max(min[i]-a,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]-self.planned[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, planned=%d, min=%d, mini=%d, maxi=%d, add=%d, sumN=%d, sumP=%d",j,alive[j],self.planned[i],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, planned=%d, min=%d, mini=%d, maxi=%d, add=%d\n",self.name[i],i,alive[i],self.planned[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, trackbombs=true, trackrockets=true, trackmissiles=true, defaultsmokebomb=true, autosave=false, instructorfreq=nil, instructor=nil, rangecontrolfreq=nil, rangecontrol=nil, soundpath="Range Soundfiles/", targetsheet=nil, targetpath=nil, targetprefix=nil, Coalition=nil, ceilingaltitude=20000, ceilingenabled=false, } RANGE.Defaults={ goodhitrange=25, strafemaxalt=914, dtBombtrack=0.005, Tmsg=30, ndisplayresult=10, rangeradius=5000, TdelaySmoke=3.0, boxlength=3000, boxwidth=300, goodpass=20, foulline=610 } RANGE.TargetType={ UNIT="Unit", STATIC="Static", COORD="Coordinate", SCENERY="Scenery" } 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.8.0" function RANGE:New(RangeName,Coalition) local self=BASE:Inherit(self,FSM:New()) self.rangename=RangeName or"Practice Range" self.Coalition=Coalition self.lid=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.lid..text) self:SetDefaultPlayerSmokeBomb() self:SetStartState("Stopped") self:AddTransition("Stopped","Start","Running") self:AddTransition("*","Status","*") self:AddTransition("*","Impact","*") self:AddTransition("*","RollingIn","*") self:AddTransition("*","StrafeResult","*") 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.lid..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.lid..text) self:HandleEvent(EVENTS.Birth) self:HandleEvent(EVENTS.Hit) self:HandleEvent(EVENTS.Shot) for _,_target in pairs(self.bombingTargets)do local target=_target if target.move and target.type==RANGE.TargetType.UNIT and target.speed>1 then target.target:PatrolZones({self.rangezone},target.speed*0.75,ENUMS.Formation.Vehicle.OffRoad) end end if self.rangecontrolfreq and not self.useSRS then self.rangecontrol=RADIOQUEUE:New(self.rangecontrolfreq,nil,self.rangename) self.rangecontrol.schedonce=true self.rangecontrol:SetDigit(0,self.Sound.RC0.filename,self.Sound.RC0.duration,self.soundpath) self.rangecontrol:SetDigit(1,self.Sound.RC1.filename,self.Sound.RC1.duration,self.soundpath) self.rangecontrol:SetDigit(2,self.Sound.RC2.filename,self.Sound.RC2.duration,self.soundpath) self.rangecontrol:SetDigit(3,self.Sound.RC3.filename,self.Sound.RC3.duration,self.soundpath) self.rangecontrol:SetDigit(4,self.Sound.RC4.filename,self.Sound.RC4.duration,self.soundpath) self.rangecontrol:SetDigit(5,self.Sound.RC5.filename,self.Sound.RC5.duration,self.soundpath) self.rangecontrol:SetDigit(6,self.Sound.RC6.filename,self.Sound.RC6.duration,self.soundpath) self.rangecontrol:SetDigit(7,self.Sound.RC7.filename,self.Sound.RC7.duration,self.soundpath) self.rangecontrol:SetDigit(8,self.Sound.RC8.filename,self.Sound.RC8.duration,self.soundpath) self.rangecontrol:SetDigit(9,self.Sound.RC9.filename,self.Sound.RC9.duration,self.soundpath) self.rangecontrol:SetSenderCoordinate(self.location) self.rangecontrol:SetSenderUnitName(self.rangecontrolrelayname) self.rangecontrol:Start(1,0.1) if self.instructorfreq and not self.useSRS then self.instructor=RADIOQUEUE:New(self.instructorfreq,nil,self.rangename) self.instructor.schedonce=true self.instructor:SetDigit(0,self.Sound.IR0.filename,self.Sound.IR0.duration,self.soundpath) self.instructor:SetDigit(1,self.Sound.IR1.filename,self.Sound.IR1.duration,self.soundpath) self.instructor:SetDigit(2,self.Sound.IR2.filename,self.Sound.IR2.duration,self.soundpath) self.instructor:SetDigit(3,self.Sound.IR3.filename,self.Sound.IR3.duration,self.soundpath) self.instructor:SetDigit(4,self.Sound.IR4.filename,self.Sound.IR4.duration,self.soundpath) self.instructor:SetDigit(5,self.Sound.IR5.filename,self.Sound.IR5.duration,self.soundpath) self.instructor:SetDigit(6,self.Sound.IR6.filename,self.Sound.IR6.duration,self.soundpath) self.instructor:SetDigit(7,self.Sound.IR7.filename,self.Sound.IR7.duration,self.soundpath) self.instructor:SetDigit(8,self.Sound.IR8.filename,self.Sound.IR8.duration,self.soundpath) self.instructor:SetDigit(9,self.Sound.IR9.filename,self.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(-10) end function RANGE:SetMenuRoot(menu) self.menuF10root=menu return self 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:SetTargetSheet(path,prefix) if io then self.targetsheet=true self.targetpath=path self.targetprefix=prefix else self:E(self.lid.."ERROR: io is not desanitized. Cannot save target sheet.") end return self end function RANGE:SetFunkManOn(Port,Host) self.funkmanSocket=SOCKET:New(Port,Host) 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) if zone and type(zone)=="string"then zone=ZONE:FindByName(zone) end self.rangezone=zone return self end function RANGE:SetRangeCeiling(altitude) self:T(self.lid.."SetRangeCeiling") if altitude and type(altitude)=="number"then self.ceilingaltitude=altitude else self:E(self.lid.."Altitude either not provided or is not a number, using default setting (20000).") self.ceilingaltitude=20000 end return self end function RANGE:EnableRangeCeiling(enabled) self:T(self.lid.."EnableRangeCeiling") if enabled and type(enabled)=="boolean"then self.ceilingenabled=enabled else self:E(self.lid.."Enabled either not provide or is not a boolean, using default setting (false).") self.ceilingenabled=false end 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:SetSRS(PathToSRS,Port,Coalition,Frequency,Modulation,Volume,PathToGoogleKey) if PathToSRS or MSRS.path then self.useSRS=true self.controlmsrs=MSRS:New(PathToSRS or MSRS.path,Frequency or 256,Modulation or radio.modulation.AM) self.controlmsrs:SetPort(Port or MSRS.port) self.controlmsrs:SetCoalition(Coalition or coalition.side.BLUE) self.controlmsrs:SetLabel("RANGEC") self.controlmsrs:SetVolume(Volume or 1.0) self.controlsrsQ=MSRSQUEUE:New("CONTROL") self.instructmsrs=MSRS:New(PathToSRS or MSRS.path,Frequency or 305,Modulation or radio.modulation.AM) self.instructmsrs:SetPort(Port or MSRS.port) self.instructmsrs:SetCoalition(Coalition or coalition.side.BLUE) self.instructmsrs:SetLabel("RANGEI") self.instructmsrs:SetVolume(Volume or 1.0) self.instructsrsQ=MSRSQUEUE:New("INSTRUCT") if PathToGoogleKey then self.controlmsrs:SetProviderOptionsGoogle(PathToGoogleKey,PathToGoogleKey) self.controlmsrs:SetProvider(MSRS.Provider.GOOGLE) self.instructmsrs:SetProviderOptionsGoogle(PathToGoogleKey,PathToGoogleKey) self.instructmsrs:SetProvider(MSRS.Provider.GOOGLE) end else self:E(self.lid..string.format("ERROR: No SRS path specified!")) end return self end function RANGE:SetSRSRangeControl(frequency,modulation,voice,culture,gender,relayunitname) if not self.instructmsrs then self:E(self.lid.."Use myrange:SetSRS() once first before using myrange:SetSRSRangeControl!") return self end self.rangecontrolfreq=frequency or 256 self.controlmsrs:SetFrequencies(self.rangecontrolfreq) self.controlmsrs:SetModulations(modulation or radio.modulation.AM) self.controlmsrs:SetVoice(voice) self.controlmsrs:SetCulture(culture or"en-US") self.controlmsrs:SetGender(gender or"female") self.rangecontrol=true if relayunitname then local unit=UNIT:FindByName(relayunitname) local Coordinate=unit:GetCoordinate() self.rangecontrolrelayname=relayunitname end return self end function RANGE:SetSRSRangeInstructor(frequency,modulation,voice,culture,gender,relayunitname) if not self.instructmsrs then self:E(self.lid.."Use myrange:SetSRS() once first before using myrange:SetSRSRangeInstructor!") return self end self.instructorfreq=frequency or 305 self.instructmsrs:SetFrequencies(self.instructorfreq) self.instructmsrs:SetModulations(modulation or radio.modulation.AM) self.instructmsrs:SetVoice(voice) self.instructmsrs:SetCulture(culture or"en-US") self.instructmsrs:SetGender(gender or"male") self.instructor=true if relayunitname then local unit=UNIT:FindByName(relayunitname) local Coordinate=unit:GetCoordinate() self.instructmsrs:SetCoordinate(Coordinate) self.instructorrelayname=relayunitname end 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:T2(self.lid..string.format("Setting sound files path to %s",self.soundpath)) return self end function RANGE:SetSoundfilesInfo(csvfile) local function getSound(filename) for key,_soundfile in pairs(self.Sound)do local soundfile=_soundfile if filename==soundfile.filename then return soundfile end end return nil end local data=UTILS.ReadCSV(csvfile) if data then for i,sound in pairs(data)do local soundfile=getSound(sound.filename..".ogg") if soundfile then soundfile.duration=tonumber(sound.duration) else self:E(string.format("ERROR: Could not get info for sound file %s",sound.filename)) end end else self:E(string.format("ERROR: Could not read sound csv file!")) end 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.lid..string.format("Adding STATIC object %s as strafe target #%d.",_name,_i)) unit=STATIC:FindByName(_name,false) elseif _isstatic==false then self:T(self.lid..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.lid..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.lid..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.lid..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.lid..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.lid..string.format("Adding unit bombing target %s with hit range %d.",name,goodhitrange,randommove)) self:AddBombingTargetUnit(_unit,goodhitrange,randommove) else self:E(self.lid..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:T(self.lid..string.format("Adding STATIC bombing target %s with good hit range %d. Random move = %s.",name,goodhitrange,tostring(randommove))) elseif _isstatic==false then self:T(self.lid..string.format("Adding UNIT bombing target %s with good hit range %d. Random move = %s.",name,goodhitrange,tostring(randommove))) else self:E(self.lid..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:AddBombingTargetScenery(scenery,goodhitrange) local name=scenery:GetName() goodhitrange=goodhitrange or RANGE.Defaults.goodhitrange if name then self:T(self.lid..string.format("Adding SCENERY bombing target %s with good hit range %d",name,goodhitrange)) else self:E(self.lid..string.format("ERROR! No bombing target with name %s could be found!",name)) end local target={} target.name=name target.target=scenery target.goodhitrange=goodhitrange target.move=false target.speed=0 target.coordinate=scenery:GetCoordinate() target.type=RANGE.TargetType.SCENERY table.insert(self.bombingTargets,target) return self end function RANGE:AddBombingTargetGroup(group,goodhitrange,randommove) self:F({group=group,goodhitrange=goodhitrange,randommove=randommove}) if group and type(group)=="string"then group=GROUP:FindByName(group) end 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.lid..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.lid..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.lid..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.lid..string.format("Foul line distance = %.1f m.",fouldist)) return fouldist end function RANGE:OnEventBirth(EventData) self:F({eventbirth=EventData}) if not EventData.IniPlayerName then return end local _unitName=EventData.IniUnitName local _unit,_playername=self:_GetPlayerUnitAndName(_unitName,EventData.IniPlayerName) self:T3(self.lid.."BIRTH: unit = "..tostring(EventData.IniUnitName)) self:T3(self.lid.."BIRTH: group = "..tostring(EventData.IniGroupName)) self:T3(self.lid.."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.lid..text) self.strafeStatus[_uid]=nil if self.Coalition then if EventData.IniCoalition==self.Coalition then self:ScheduleOnce(0.1,self._AddF10Commands,self,_unitName) end else self:ScheduleOnce(0.1,self._AddF10Commands,self,_unitName) end 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].unit=_unit 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.lid.."HIT: Ini unit = "..tostring(EventData.IniUnitName)) self:T3(self.lid.."HIT: Ini group = "..tostring(EventData.IniGroupName)) self:T3(self.lid.."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 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) if self.useSRS then local ttstext=string.format("%s, Invalid hit! You already passed foul line distance of %d meters for target %s.",self:_myname(_unitName),_d,targetname) self.controlsrsQ:NewTransmission(ttstext,nil,self.controlmsrs,nil,2) end self:_DisplayMessageToGroup(_unit,text) self:T2(self.lid..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._OnImpact(weapon,self,playerData,attackHdg,attackAlt,attackVel) if not playerData then return end local _closetTarget=nil local _distance=nil local _closeCoord=nil local _hitquality="POOR" local _callsign=self:_myname(playerData.unitname) local _playername=playerData.playername local _unit=playerData.unit local impactcoord=weapon:GetImpactCoordinate() local insidezone=self.rangezone:IsCoordinateInZone(impactcoord) if playerData and playerData.smokebombimpact and insidezone then if playerData and 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 bombtarget=_bombtarget 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<=1.53 then _hitquality="SHACK" elseif _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.command=SOCKET.DataType.BOMBRESULT result.name=_closetTarget.name or"unknown" result.distance=_distance result.radial=_closeCoord:HeadingTo(impactcoord) result.weapon=weapon:GetTypeName()or"unknown" result.quality=_hitquality result.player=playerData.playername result.time=timer.getAbsTime() result.clock=UTILS.SecondsToClock(result.time,true) result.midate=UTILS.GetDCSMissionDate() result.theatre=env.mission.theatre result.airframe=playerData.airframe result.roundsFired=0 result.roundsHit=0 result.roundsQuality="N/A" result.rangename=self.rangename result.attackHdg=attackHdg result.attackVel=attackVel result.attackAlt=attackAlt result.date=os and os.date()or"n/a" 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) if self.useSRS then local ttstext=string.format("%s, weapon impacted too far from nearest range target, mor than %.1f kilometer. No score!",_callsign,self.scorebombdistance/1000) self.controlsrsQ:NewTransmission(ttstext,nil,self.controlmsrs,nil,2) end self:_DisplayMessageToGroup(_unit,_message,nil,false) if self.rangecontrol then if self.useSRS then self.controlsrsQ:NewTransmission(_message,nil,self.controlmsrs,nil,1) else self.rangecontrol:NewTransmission(self.Sound.RCWeaponImpactedTooFar.filename,self.Sound.RCWeaponImpactedTooFar.duration,self.soundpath,nil,nil,_message,self.subduration) end end else self:T(self.lid.."Weapon impacted outside range zone.") end end function RANGE:OnEventShot(EventData) self:F({eventshot=EventData}) if EventData.Weapon==nil or EventData.IniDCSUnit==nil or EventData.IniPlayerName==nil then return end local weapon=WEAPON:New(EventData.weapon) local _track=(weapon:IsBomb()and self.trackbombs)or(weapon:IsRocket()and self.trackrockets)or(weapon:IsMissile()and self.trackmissiles) local _unitName=EventData.IniUnitName local _unit,_playername=self:_GetPlayerUnitAndName(_unitName,EventData.IniPlayerName) local dPR=self.BombtrackThreshold*2 if _unit and _playername then dPR=_unit:GetCoordinate():Get2DDistance(self.location) self:T(self.lid..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 and self.PlayerSettings[_playername]then local playerData=self.PlayerSettings[_playername] if not playerData then return end local attackHdg=_unit:GetHeading() local attackAlt=_unit:GetHeight() attackAlt=UTILS.MetersToFeet(attackAlt) local attackVel=_unit:GetVelocityKNOTS() self:T(self.lid..string.format("RANGE %s: Tracking %s - %s.",self.rangename,weapon:GetTypeName(),weapon:GetName())) weapon:SetFuncImpact(RANGE._OnImpact,self,playerData,attackHdg,attackAlt,attackVel) self:T(self.lid..string.format("Range %s, player %s: Tracking of weapon starts in 0.1 seconds.",self.rangename,_playername)) weapon:StartTrack(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:T(self.lid..text) end self:_CheckPlayers() self:__Status(-10) end function RANGE:onafterEnterRange(From,Event,To,player) if self.instructor and self.rangecontrol then if self.useSRS then local text=string.format("You entered the bombing range. For hit assessment, contact the range controller at %.3f MHz",self.rangecontrolfreq) local ttstext=string.format("You entered the bombing range. For hit assessment, contact the range controller at %.3f mega hertz.",self.rangecontrolfreq) local group=player.client:GetGroup() self.instructsrsQ:NewTransmission(ttstext,nil,self.instructmsrs,nil,1,{group},text,10) else local RF=UTILS.Split(string.format("%.3f",self.rangecontrolfreq),".") self.instructor:NewTransmission(self.Sound.IREnterRange.filename,self.Sound.IREnterRange.duration,self.soundpath) self.instructor:Number2Transmission(RF[1]) if tonumber(RF[2])>0 then self.instructor:NewTransmission(self.Sound.IRDecimal.filename,self.Sound.IRDecimal.duration,self.soundpath) self.instructor:Number2Transmission(RF[2]) end self.instructor:NewTransmission(self.Sound.IRMegaHertz.filename,self.Sound.IRMegaHertz.duration,self.soundpath) end end end function RANGE:onafterExitRange(From,Event,To,player) if self.instructor then if self.useSRS then local text="You left the bombing range zone. " local r=math.random(5) if r==1 then text=text.."Have a nice day!" elseif r==2 then text=text.."Take care and bye bye!" elseif r==3 then text=text.."Talk to you soon!" elseif r==4 then text=text.."See you in two weeks!" elseif r==5 then text=text.."!" end self.instructsrsQ:NewTransmission(text,nil,self.instructmsrs,nil,1,{player.client:GetGroup()},text,10) else self.instructor:NewTransmission(self.Sound.IRExitRange.filename,self.Sound.IRExitRange.duration,self.soundpath) end end end function RANGE:onafterImpact(From,Event,To,result,player) local targetname=nil if#self.bombingTargets>1 then targetname=result.name end local text=string.format("%s, impact %03d° for %d ft (%d m)",player.playername,result.radial,UTILS.MetersToFeet(result.distance),result.distance) if targetname then text=text..string.format(" from bulls of target %s.",targetname) else text=text.."." end text=text..string.format(" %s hit.",result.quality) if self.rangecontrol then if self.useSRS then local group=player.client:GetGroup() self.controlsrsQ:NewTransmission(text,nil,self.controlmsrs,nil,1,{group},text,10) else self.rangecontrol:NewTransmission(self.Sound.RCImpact.filename,self.Sound.RCImpact.duration,self.soundpath,nil,nil,text,self.subduration) self.rangecontrol:Number2Transmission(string.format("%03d",result.radial),nil,0.1) self.rangecontrol:NewTransmission(self.Sound.RCDegrees.filename,self.Sound.RCDegrees.duration,self.soundpath) self.rangecontrol:NewTransmission(self.Sound.RCFor.filename,self.Sound.RCFor.duration,self.soundpath) self.rangecontrol:Number2Transmission(string.format("%d",UTILS.MetersToFeet(result.distance))) self.rangecontrol:NewTransmission(self.Sound.RCFeet.filename,self.Sound.RCFeet.duration,self.soundpath) if result.quality=="POOR"then self.rangecontrol:NewTransmission(self.Sound.RCPoorHit.filename,self.Sound.RCPoorHit.duration,self.soundpath,nil,0.5) elseif result.quality=="INEFFECTIVE"then self.rangecontrol:NewTransmission(self.Sound.RCIneffectiveHit.filename,self.Sound.RCIneffectiveHit.duration,self.soundpath,nil,0.5) elseif result.quality=="GOOD"then self.rangecontrol:NewTransmission(self.Sound.RCGoodHit.filename,self.Sound.RCGoodHit.duration,self.soundpath,nil,0.5) elseif result.quality=="EXCELLENT"then self.rangecontrol:NewTransmission(self.Sound.RCExcellentHit.filename,self.Sound.RCExcellentHit.duration,self.soundpath,nil,0.5) end end end if player.unitname and not self.useSRS then local unit=UNIT:FindByName(player.unitname) self:_DisplayMessageToGroup(unit,text,nil,true) self:T(self.lid..text) end if self.autosave then self:Save() end if self.funkmanSocket then self.funkmanSocket:SendTable(result) end end function RANGE:onafterStrafeResult(From,Event,To,player,result) if self.funkmanSocket then self.funkmanSocket:SendTable(result) end end function RANGE:onbeforeSave(From,Event,To) if io and lfs then return true else self:E(self.lid..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:T(self.lid..string.format("Saving player results to file %s",tostring(filename))) else self:E(self.lid..string.format("ERROR: Could not save results to file %s",tostring(filename))) end end local path=self.targetpath or 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,true) local airframe=result.airframe local date=result.date or"n/a" 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.lid..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.lid..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=self.targetpath or 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:T(self.lid..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:_SaveTargetSheet(_playername,result) local function _savefile(filename,data) local f=io.open(filename,"wb") if f then f:write(data) f:close() else env.info("RANGEBOSS EDIT - could not save target sheet to file") end end local path=self.targetpath if lfs then path=path or lfs.writedir()..[[Logs\]] end local filename=nil for i=1,9999 do if self.targetprefix then filename=string.format("%s_%s-%04d.csv",self.targetprefix,result.airframe,i) else local name=UTILS.ReplaceIllegalCharacters(_playername,"_") filename=string.format("RANGERESULTS-%s_Targetsheet-%s-%04d.csv",self.rangename,name,i) end if path~=nil then filename=path.."\\"..filename end local _exists=UTILS.FileExists(filename) if not _exists then break end end local data="Name,Target,Rounds Fired,Rounds Hit,Rounds Quality,Airframe,Mission Time,OS Time\n" local target=result.name local airframe=result.airframe local roundsFired=result.roundsFired local roundsHit=result.roundsHit local strafeResult=result.roundsQuality local time=UTILS.SecondsToClock(result.time) local date="n/a" if os then date=os.date() end data=data..string.format("%s,%s,%d,%d,%s,%s,%s,%s",_playername,target,roundsFired,roundsHit,strafeResult,airframe,time,date) _savefile(filename,data) end function RANGE._DelayedSmoke(_args) _args.coord:Smoke(_args.color) end function RANGE:_DisplayMyStrafePitResults(_unitName) self:F(_unitName) local _unit,_playername,_multiplayer=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.roundsHit>b.roundsHit end table.sort(_results,_sort) local _bestMsg="" local _count=1 for _,_result in pairs(_results)do local result=_result _message=_message..string.format("\n[%d] Hits %d - %s - %s",_count,result.roundsHit,result.name,result.roundsQuality) if _bestMsg==""then _bestMsg=string.format("Hits %d - %s - %s",result.roundsHit,result.name,result.roundsQuality) 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,_multiplayer) end end function RANGE:_DisplayStrafePitResults(_unitName) self:F(_unitName) local _unit,_playername,_multiplayer=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.roundsHit>_best.roundsHit then _best=_result end end if _best~=nil then local text=string.format("%s: Hits %i - %s - %s",_playerName,_best.roundsHit,_best.name,_best.roundsQuality) table.insert(_playerResults,{msg=text,hits=_best.roundsHit}) 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,_multiplayer) end end function RANGE:_DisplayMyBombingResults(_unitName) self:F(_unitName) local _unit,_playername,_multiplayer=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,_multiplayer) end end function RANGE:_DisplayRangeWeather(_unitname) self:F(_unitname) local unit,playername,_multiplayer=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,_multiplayer) self:T2(self.lid..text) else self:T(self.lid..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 local unitalt=unit:GetAltitude(false) local unitaltinfeet=UTILS.MetersToFeet(unitalt) if unit:IsInZone(self.rangezone)and(not self.ceilingenabled or unitaltinfeet0 then accur=_result.hits/shots*100 if accur>100 then accur=100 end end local resulttext="" if _result.pastfoulline==true then resulttext="* INVALID - PASSED FOUL LINE *" _sound=self.Sound.RCPoorPass else if accur>=90 then resulttext="DEADEYE PASS" _sound=self.Sound.RCExcellentPass elseif accur>=75 then resulttext="EXCELLENT PASS" _sound=self.Sound.RCExcellentPass elseif accur>=50 then resulttext="GOOD PASS" _sound=self.Sound.RCGoodPass elseif accur>=25 then resulttext="INEFFECTIVE PASS" _sound=self.Sound.RCIneffectivePass else resulttext="POOR PASS" _sound=self.Sound.RCPoorPass end end local _text=string.format("%s, hits on target %s: %d",self:_myname(_unitName),_result.zone.name,_result.hits) local ttstext=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) ttstext=ttstext..string.format(". Total rounds fired %d. Accuracy %.1f percent.",shots,accur) end _text=_text..string.format("\n%s",resulttext) ttstext=ttstext..string.format(" %s",resulttext) self:_DisplayMessageToGroup(_unit,_text) local result={} result.command=SOCKET.DataType.STRAFERESULT result.player=_playername result.name=_result.zone.name or"unknown" result.time=timer.getAbsTime() result.clock=UTILS.SecondsToClock(result.time) result.midate=UTILS.GetDCSMissionDate() result.theatre=env.mission.theatre result.roundsFired=shots result.roundsHit=_result.hits result.roundsQuality=resulttext result.strafeAccuracy=accur result.rangename=self.rangename result.airframe=playerData.airframe result.invalid=_result.pastfoulline self:StrafeResult(playerData,result) if playerData and playerData.targeton and self.targetsheet then self:_SaveTargetSheet(_playername,result) end if self.rangecontrol then if self.useSRS then self.controlsrsQ:NewTransmission(ttstext,nil,self.controlmsrs,nil,1) else self.rangecontrol:NewTransmission(self.Sound.RCHitsOnTarget.filename,self.Sound.RCHitsOnTarget.duration,self.soundpath) self.rangecontrol:Number2Transmission(string.format("%d",_result.hits)) if shots and accur then self.rangecontrol:NewTransmission(self.Sound.RCTotalRoundsFired.filename,self.Sound.RCTotalRoundsFired.duration,self.soundpath,nil,0.2) self.rangecontrol:Number2Transmission(string.format("%d",shots),nil,0.2) self.rangecontrol:NewTransmission(self.Sound.RCAccuracy.filename,self.Sound.RCAccuracy.duration,self.soundpath,nil,0.2) self.rangecontrol:Number2Transmission(string.format("%d",UTILS.Round(accur,0))) self.rangecontrol:NewTransmission(self.Sound.RCPercent.filename,self.Sound.RCPercent.duration,self.soundpath) end self.rangecontrol:NewTransmission(_sound.filename,_sound.duration,self.soundpath,nil,0.5) end 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 target=_targetZone local zone=target.polygon local unitinzone=checkme(target.heading,zone) if unitinzone then local _ammo=self:_GetAmmo(_unitName) self.strafeStatus[_unitID]={hits=0,zone=target,time=1,ammo=_ammo,pastfoulline=false} local _msg=string.format("%s, rolling in on strafe pit %s.",self:_myname(_unitName),target.name) if self.rangecontrol then if self.useSRS then self.controlsrsQ:NewTransmission(_msg,nil,self.controlmsrs,nil,1) else self.rangecontrol:NewTransmission(self.Sound.RCRollingInOnStrafeTarget.filename,self.Sound.RCRollingInOnStrafeTarget.duration,self.soundpath) end end self:_DisplayMessageToGroup(_unit,_msg,10,true) self:RollingIn(playerData,target) 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 _rootMenu=nil if self.menuF10root then _rootMenu=self.menuF10root self:T2(self.lid..string.format("Creating F10 menu for group %s",group:GetName())) elseif RANGE.MenuF10Root then _rootMenu=RANGE.MenuF10Root else if RANGE.MenuF10[_gid]==nil then self:T2(self.lid..string.format("Creating F10 menu 'On the Range' for group %s",group:GetName())) else self:T2(self.lid..string.format("F10 menu 'On the Range' already EXISTS for group %s",group:GetName())) end _rootMenu=RANGE.MenuF10[_gid]or MENU_GROUP:New(group,"On the Range") end local _rangePath=MENU_GROUP:New(group,self.rangename,_rootMenu) local _infoPath=MENU_GROUP:New(group,"Range Info",_rangePath) local _markPath=MENU_GROUP:New(group,"Mark Targets",_rangePath) local _statsPath=MENU_GROUP:New(group,"Statistics",_rangePath) local _settingsPath=MENU_GROUP:New(group,"My Settings",_rangePath) local _mysmokePath=MENU_GROUP:New(group,"Smoke Color",_settingsPath) local _myflarePath=MENU_GROUP:New(group,"Flare Color",_settingsPath) local _MoMap=MENU_GROUP_COMMAND:New(group,"Mark On Map",_markPath,self._MarkTargetsOnMap,self,_unitName) local _IllRng=MENU_GROUP_COMMAND:New(group,"Illuminate Range",_markPath,self._IlluminateBombTargets,self,_unitName) local _SSpit=MENU_GROUP_COMMAND:New(group,"Smoke Strafe Pits",_markPath,self._SmokeStrafeTargetBoxes,self,_unitName) local _SStgts=MENU_GROUP_COMMAND:New(group,"Smoke Strafe Tgts",_markPath,self._SmokeStrafeTargets,self,_unitName) local _SBtgts=MENU_GROUP_COMMAND:New(group,"Smoke Bomb Tgts",_markPath,self._SmokeBombTargets,self,_unitName) local _AllSR=MENU_GROUP_COMMAND:New(group,"All Strafe Results",_statsPath,self._DisplayStrafePitResults,self,_unitName) local _AllBR=MENU_GROUP_COMMAND:New(group,"All Bombing Results",_statsPath,self._DisplayBombingResults,self,_unitName) local _MySR=MENU_GROUP_COMMAND:New(group,"My Strafe Results",_statsPath,self._DisplayMyStrafePitResults,self,_unitName) local _MyBR=MENU_GROUP_COMMAND:New(group,"My Bomb Results",_statsPath,self._DisplayMyBombingResults,self,_unitName) local _ResetST=MENU_GROUP_COMMAND:New(group,"Reset All Stats",_statsPath,self._ResetRangeStats,self,_unitName) local _BlueSM=MENU_GROUP_COMMAND:New(group,"Blue Smoke",_mysmokePath,self._playersmokecolor,self,_unitName,SMOKECOLOR.Blue) local _GrSM=MENU_GROUP_COMMAND:New(group,"Green Smoke",_mysmokePath,self._playersmokecolor,self,_unitName,SMOKECOLOR.Green) local _OrSM=MENU_GROUP_COMMAND:New(group,"Orange Smoke",_mysmokePath,self._playersmokecolor,self,_unitName,SMOKECOLOR.Orange) local _ReSM=MENU_GROUP_COMMAND:New(group,"Red Smoke",_mysmokePath,self._playersmokecolor,self,_unitName,SMOKECOLOR.Red) local _WhSm=MENU_GROUP_COMMAND:New(group,"White Smoke",_mysmokePath,self._playersmokecolor,self,_unitName,SMOKECOLOR.White) local _GrFl=MENU_GROUP_COMMAND:New(group,"Green Flares",_myflarePath,self._playerflarecolor,self,_unitName,FLARECOLOR.Green) local _ReFl=MENU_GROUP_COMMAND:New(group,"Red Flares",_myflarePath,self._playerflarecolor,self,_unitName,FLARECOLOR.Red) local _WhFl=MENU_GROUP_COMMAND:New(group,"White Flares",_myflarePath,self._playerflarecolor,self,_unitName,FLARECOLOR.White) local _YeFl=MENU_GROUP_COMMAND:New(group,"Yellow Flares",_myflarePath,self._playerflarecolor,self,_unitName,FLARECOLOR.Yellow) local _SmDe=MENU_GROUP_COMMAND:New(group,"Smoke Delay On/Off",_settingsPath,self._SmokeBombDelayOnOff,self,_unitName) local _SmIm=MENU_GROUP_COMMAND:New(group,"Smoke Impact On/Off",_settingsPath,self._SmokeBombImpactOnOff,self,_unitName) local _FlHi=MENU_GROUP_COMMAND:New(group,"Flare Hits On/Off",_settingsPath,self._FlareDirectHitsOnOff,self,_unitName) local _AlMeA=MENU_GROUP_COMMAND:New(group,"All Messages On/Off",_settingsPath,self._MessagesToPlayerOnOff,self,_unitName) local _TrpSh=MENU_GROUP_COMMAND:New(group,"Targetsheet On/Off",_settingsPath,self._TargetsheetOnOff,self,_unitName) local _WeIn=MENU_GROUP_COMMAND:New(group,"General Info",_infoPath,self._DisplayRangeInfo,self,_unitName) local _WeRe=MENU_GROUP_COMMAND:New(group,"Weather Report",_infoPath,self._DisplayRangeWeather,self,_unitName) local _BoTgtgs=MENU_GROUP_COMMAND:New(group,"Bombing Targets",_infoPath,self._DisplayBombTargets,self,_unitName) local _StrPits=MENU_GROUP_COMMAND:New(group,"Strafe Pits",_infoPath,self._DisplayStrafePits,self,_unitName):Refresh() end else self:E(self.lid.."Could not find group or group ID in AddF10Menu() function. Unit name: ".._unitName or"N/A") end else self:E(self.lid.."Player unit does not exist in AddF10Menu() function. Unit name: ".._unitName or"N/A") end end function RANGE:_GetBombTargetCoordinate(target) local coord=nil if target.type==RANGE.TargetType.UNIT then if target.target and target.target:IsAlive()then coord=target.target:GetCoordinate() target.coordinate=coord else coord=target.coordinate end elseif target.type==RANGE.TargetType.STATIC then coord=target.coordinate elseif target.type==RANGE.TargetType.COORD then coord=target.coordinate elseif target.type==RANGE.TargetType.SCENERY then coord=target.coordinate else self:E(self.lid.."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.lid..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.lid..text) else local text=string.format("Player %s has %d ammo of type %s",playername,Nammo,Tammo) self:T(self.lid..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,_togroup) 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 _grp=_unit:GetGroup() local _,playername=self:_GetPlayerUnitAndName(_unit:GetName()) local playermessage=self.PlayerSettings[playername].messages if _gid and(playermessage==true or display)and(not self.examinerexclusive)then if _togroup and _grp then local m=MESSAGE:New(_text,_time,nil,_clear):ToGroup(_grp) else local m=MESSAGE:New(_text,_time,nil,_clear):ToUnit(_unit) end end if self.examinergroupname~=nil then local _examinerid=GROUP:FindByName(self.examinergroupname) if _examinerid then local m=MESSAGE:New(_text,_time,nil,_clear):ToGroup(_examinerid) end end end end function RANGE:_SmokeBombImpactOnOff(unitname) self:F(unitname) local unit,playername,_multiplayer=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.PlayerSettings[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,_multiplayer=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.PlayerSettings[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,_multiplayer=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:_TargetsheetOnOff(_unitname) self:F2(_unitname) local unit,playername,_multiplayer=self:_GetPlayerUnitAndName(_unitname) if unit and playername then local playerData=self.PlayerSettings[playername] if playerData then local text="" if self.targetsheet then playerData.targeton=not playerData.targeton if playerData and playerData.targeton==true then text=string.format("Roger, your targetsheets are now SAVED.") else text=string.format("Affirm, your targetsheets are NOT SAVED.") end else text="Negative, target sheet data recorder is broken on this range." end self:_DisplayMessageToGroup(unit,text,5,false,false) end end end function RANGE:_FlareDirectHitsOnOff(unitname) self:F(unitname) local unit,playername,_multiplayer=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.lid..string.format("Adding DCS static to MOOSE database. Name = %s.",name)) _DATABASE:AddStatic(name) end return true else self:T3(self.lid..string.format("No static object with name %s exists.",name)) end if UNIT:FindByName(name)then return false else self:T3(self.lid..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,PlayerName) if _unitName~=nil then local multiplayer=false local DCSunit=Unit.getByName(_unitName) if DCSunit and DCSunit.getPlayerName then local playername=DCSunit:getPlayerName()or PlayerName or"None" local unit=UNIT:Find(DCSunit) self:T2({DCSunit=DCSunit,unit=unit,playername=playername}) if DCSunit and unit and playername then self:F2(playername) local grp=unit:GetGroup() if grp and grp:CountAliveUnits()>1 then multiplayer=true end return unit,playername,multiplayer end end end return nil,nil,nil end function RANGE:_myname(unitname) self:F2(unitname) local pname="Ghost 1 1" local unit=UNIT:FindByName(unitname) if unit and unit:IsAlive()then local grp=unit:GetGroup() if grp and grp:IsAlive()then pname=grp:GetCustomCallSign(true,true) end end return pname end do ZONE_GOAL={ ClassName="ZONE_GOAL", Goal=nil, SmokeTime=nil, SmokeScheduler=nil, SmokeColor=nil, SmokeZone=nil, } function ZONE_GOAL:New(Zone) BASE:I({Zone=Zone}) local self=BASE:Inherit(self,BASE:New()) if type(Zone)=="string"then self=BASE:Inherit(self,ZONE_POLYGON:NewFromGroupName(Zone)) else self=BASE:Inherit(self,ZONE_RADIUS:New(Zone:GetName(),Zone:GetVec2(),Zone:GetRadius())) self:F({Zone=Zone}) end 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, PreviousCoalition=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_COALITION!") 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.ClassName~="SCENERY"then 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 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={ ["LeFH_18-40-105"]={ displayname="FH LeFH-18 105mm", minrange=500, maxrange=10500, reloadtime=nil, }, ["M2A1-105"]={ displayname="FH M2A1 105mm", minrange=500, maxrange=11500, reloadtime=nil, }, ["Pak40"]={ displayname="FH Pak 40 75mm", minrange=500, maxrange=3000, reloadtime=nil, }, ["L118_Unit"]={ displayname="L118 Light Artillery Gun", minrange=500, maxrange=17500, reloadtime=nil, }, ["Smerch"]={ displayname="MLRS 9A52 Smerch CM 300mm", minrange=20000, maxrange=70000, reloadtime=2160, }, ["Smerch_HE"]={ displayname="MLRS 9A52 Smerch HE 300mm", minrange=20000, maxrange=70000, reloadtime=2160, }, ["Uragan_BM-27"]={ displayname="MLRS 9K57 Uragan BM-27 220mm", minrange=11500, maxrange=35800, reloadtime=840, }, ["Grad-URAL"]={ displayname="MLRS BM-21 Grad 122mm", minrange=5000, maxrange=19000, reloadtime=420, }, ["HL_B8M1"]={ displayname="MLRS HL with B8M1 80mm", minrange=500, maxrange=5000, reloadtime=nil, }, ["tt_B8M1"]={ displayname="MLRS LC with B8M1 80mm", minrange=500, maxrange=5000, reloadtime=nil, }, ["MLRS"]={ displayname="MLRS M270 227mm", minrange=10000, maxrange=32000, reloadtime=540, }, ["2B11 mortar"]={ displayname="Mortar 2B11 120mm", minrange=500, maxrange=7000, reloadtime=30, }, ["PLZ05"]={ displayname="PLZ-05", minrange=500, maxrange=23500, reloadtime=nil, }, ["SAU Gvozdika"]={ displayname="SPH 2S1 Gvozdika 122mm", minrange=300, maxrange=15000, reloadtime=nil, }, ["SAU Msta"]={ displayname="SPH 2S19 Msta 152mm", minrange=300, maxrange=23500, reloadtime=nil, }, ["SAU Akatsia"]={ displayname="SPH 2S3 Akatsia 152mm", minrange=300, maxrange=17000, reloadtime=nil, }, ["SpGH_Dana"]={ displayname="SPH Dana vz77 152mm", minrange=300, maxrange=18700, reloadtime=nil, }, ["M-109"]={ displayname="SPH M109 Paladin 155mm", minrange=300, maxrange=22000, reloadtime=nil, }, ["M12_GMC"]={ displayname="SPH M12 GMC 155mm", minrange=300, maxrange=18200, reloadtime=nil, }, ["Wespe124"]={ displayname="SPH Sd.Kfz.124 Wespe 105mm", minrange=300, maxrange=7000, reloadtime=nil, }, ["T155_Firtina"]={ displayname="SPH T155 Firtina 155mm", minrange=300, maxrange=41000, reloadtime=nil, }, ["SAU 2-C9"]={ displayname="SPM 2S9 Nona 120mm M", minrange=500, maxrange=7000, reloadtime=nil, }, } ARTY.version="1.3.3" 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>3.6 then self.ismobile=true else self.ismobile=false end self.dtTrack=0.2 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:SetTrackInterval(interval) self.dtTrack=interval or 0.2 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.Narty0=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.Type) 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 = %.1f km/h\n",self.SpeedMax) text=text..string.format("Speed default = %.1f 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,Narty=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",Narty) 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._FuncTrack(weapon,self,target) local _coord=weapon.coordinate local _dist=_coord:Get2DDistance(target.coord) local _destroyweapon=false self:T3(self.lid..string.format("ARTY %s weapon to target dist = %d m",self.groupname,_dist)) if target.weapontype==ARTY.WeaponType.IlluminationShells then if _dist0 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 weapon=WEAPON:New(EventData.weapon) weapon:SetTimeStepTrack(self.dtTrack) local target=UTILS.DeepCopy(self.currentTarget) weapon:SetFuncTrack(ARTY._FuncTrack,self,target) weapon:SetFuncImpact(ARTY._FuncImpact,self,target) weapon:StartTrack(2) end local _nammo,_nshells,_nrockets,_nmissiles,_narty=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,_narty,_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,narty=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,narty,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 if self:is("CombatReady")then self:OpenFire(_timedTarget) end elseif _normalTarget then if self:is("CombatReady")then self:OpenFire(_normalTarget) end 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,Narty=self:GetAmmo() local nfire=Narty local _type="shots" if target.weapontype==ARTY.WeaponType.Auto then nfire=Nammo _type="shots" elseif target.weapontype==ARTY.WeaponType.Cannon then nfire=Narty _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,narty=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) self:I("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 if group:HasTask()then group:ClearTasks() end group:OptionROEOpenFire() local vec2=coord:GetVec2() local fire=group:TaskFireAtPoint(vec2,radius,nshells,weapontype) group:SetTask(fire,1) 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 if group:HasTask()then group:ClearTasks() end group:OptionROEOpenFire() local targetgroup=GROUP:FindByName(target.name) local fire=group:TaskAttackGroup(targetgroup,weapontype,AI.Task.WeaponExpend.ONE,1) group:SetTask(fire,1) 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 nartyshells=0 local units=self.Controllable:GetUnits() if units==nil then return nammo,nshells,nrockets,nmissiles end for _,_unit in pairs(units)do local unit=_unit if unit 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 local _,_,_,_,_,shells=unit:GetAmmunition() nartyshells=nartyshells+shells 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,nartyshells 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 self:T(string.format("dt = %d WaitTime = %d | shots = %d TargetShells = %d",dt,self.WaitForShotTime,self.Nshots,self.currentTarget.nshells)) 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,_narty=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 _narty==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,Narty=self:GetAmmo() local nfire=Nammo if target.weapontype==ARTY.WeaponType.Auto then nfire=Nammo elseif target.weapontype==ARTY.WeaponType.Cannon then nfire=Narty 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, waypoints={}, } 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.4" 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("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() local group=Controllable local Waypoints=group:GetTemplateRoutePoints() group:Route(Waypoints,5) 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.Auto) 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:F3(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 ENUMS.Formation.Vehicle.OffRoad wait=wait or 30 local group=self.Controllable if group and group:IsAlive()then local ini=group:GetCoordinate() local dist=ini:Get2DDistance(fin) local heading=self:_Heading(ini,fin) local wp={} local tasks={} wp[1]=ini:WaypointGround(speed,formation) if self.Debug then local MarkerID=ini:MarkToAll(string.format("Waypoing %d of group %s (initial)",#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) wp[#wp+1]=fin:WaypointGround(speed,formation,TaskComboFin) if self.Debug then local MarkerID=fin:MarkToAll(string.format("Waypoing %d of group %s (final)",#wp,self.Controllable:GetName())) end group:Route(wp) 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(Fsm.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, reportplayername=false, } PSEUDOATC.id="PseudoATC | " PSEUDOATC.version="0.10.5" 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:I(PSEUDOATC.id.."Starting PseudoATC") 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) 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:SetReportPlayername() self.reportplayername=true return self 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:_OnBirth(EventData) self:F({EventData=EventData}) local _unitName=EventData.IniUnitName local _unit=EventData.IniUnit local _playername=EventData.IniPlayerName if _unit and _playername then self:PlayerEntered(_unit) end end function PSEUDOATC:_PlayerLeft(EventData) self:F({EventData=EventData}) local _unitName=EventData.IniUnitName local _unit=EventData.IniUnit local _playername=EventData.IniPlayerName if _unit and _playername then self:PlayerLeft(_unit) end end function PSEUDOATC:_PlayerLanded(EventData) self:F({EventData=EventData}) local _unitName=EventData.IniUnitName local _unit=EventData.IniUnit local _playername=EventData.IniPlayerName 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=EventData.IniUnit local _playername=EventData.IniPlayerName 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=unit:GetPlayerName()or"Ghost" local UnitName=unit:GetName()or"Ghostplane" local GroupName=group:GetName()or"Ghostgroup" if self.Debug then local text=string.format("Player %s in unit %s of group %s landed at %s.",PlayerName,UnitName,GroupName,place) self:T(PSEUDOATC.id..text) MESSAGE:New(text,30):ToAllIf(self.Debug) end 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 PlayerName=unit:GetPlayerName()or"Ghost" local UnitName=unit:GetName()or"Ghostplane" local GroupName=group:GetName()or"Ghostgroup" local CallSign=unit:GetCallsign()or"Ghost11" if self.Debug then local text=string.format("Player %s in unit %s of group %s took off at %s.",PlayerName,UnitName,GroupName,place) self:T(PSEUDOATC.id..text) MESSAGE:New(text,30):ToAllIf(self.Debug) end if place and self.chatty then local text=string.format("%s, %s, you are airborne. Have a safe trip!",place,CallSign) if self.reportplayername then text=string.format("%s, %s, you are airborne. Have a safe trip!",place,PlayerName) end MESSAGE:New(text,self.mdur):ToGroup(group) end 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]and self.group[GID].player and self.group[GID].player[UID]then local PlayerName=self.group[GID].player[UID].playername local CallSign=self.group[GID].player[UID].callsign local UnitName=self.group[GID].player[UID].unitname 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) if wp.name and wp.name~=""then name=string.format("Waypoint %s",wp.name) end 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.CelsiusToFahrenheit(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 PlayerName=self.group[GID].player[UID].playername 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 self.reportplayername then _text=string.format("%s, your altitude is %s AGL.",PlayerName,Hs) end 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,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 a=AIRBASE:FindByName(name) if a then local q=a:GetCoordinate() local d=q:Get2DDistance(pos) table.insert(self.group[GID].player[UID].airports,{distance=d,name=name}) end 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.runwaydestroyed=nil 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 if self.verbosity>2 then self:_PrintQueue(self.queue,"Queue waiting") self:_PrintQueue(self.pending,"Queue pending") end 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.requested=false asset.isReserved=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) local opsgroup=_DATABASE:GetOpsGroup(group:GetName()) if opsgroup then opsgroup:Despawn(0,true) opsgroup:__Stop(-0.01) else group:Destroy() end else local opsgroup=_DATABASE:GetOpsGroup(group:GetName()) if opsgroup then opsgroup:Stop() end 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-DCSdesc.box.min.x local y=DCSdesc.box.max.y-DCSdesc.box.min.y local z=DCSdesc.box.max.z-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 local weights={} 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 weights[_i]=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.weights=weights 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.requested=false asset.isReserved=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 descval="assetlist" if request.assetdesc==WAREHOUSE.Descriptor.ASSETLIST then else descval=tostring(request.assetdescval) end 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,descval,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 or Request.transporttype==WAREHOUSE.TransportType.ARMEDSHIP or Request.transporttype==WAREHOUSE.TransportType.WARSHIP 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 if spawngroup then self:__AssetSpawned(0.01,spawngroup,_assetitem,Request) 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.flightgroup and not asset.arrived then asset.arrived=true return false end 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() return self 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 return self 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 asset.spawngroupname=group:GetName() self:_DeleteStockItem(asset) 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) 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) if asset and request then local text=string.format("Asset %s from request id=%d is dead!",asset.templatename,request.uid) self:T(self.lid..text) local groupname=asset.spawngroupname local NoTriggerEvent=true if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then if request.cargogroupset then request.cargogroupset:Remove(groupname,NoTriggerEvent) self:T(self.lid..string.format("Removed selfpropelled cargo %s: ncargo=%d.",groupname,request.cargogroupset:Count())) else self:E(self.lid..string.format("ERROR: cargogroupset is nil for request ID=%s!",tostring(request.uid))) end else local istransport=not asset.iscargo if istransport==true then request.transportgroupset:Remove(groupname,NoTriggerEvent) self:T(self.lid..string.format("Removed transport %s: ntransport=%d",groupname,request.transportgroupset:Count())) elseif istransport==false then request.cargogroupset:Remove(groupname,NoTriggerEvent) self:T(self.lid..string.format("Removed transported cargo %s outside carrier: ncargo=%d",groupname,request.cargogroupset:Count())) else end end else self:E(self.lid.."ERROR: Asset and/or Request is nil in onafterAssetDead") end 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] if not asset.spawned then 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,Request.lateActivation) 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,Request.lateActivation) else _group=self:_SpawnAssetAircraft(_alias,asset,Request,nil,UnControlled,Request.lateActivation) end elseif asset.category==Group.Category.TRAIN then if self.rail then _group=self:_SpawnAssetGroundNaval(_alias,asset,Request,self.spawnzone,Request.lateActivation) end elseif asset.category==Group.Category.SHIP then _group=self:_SpawnAssetGroundNaval(_alias,asset,Request,self.portzone,Request.lateActivation) else self:E(self.lid.."ERROR: Unknown asset category!") end if _group then self:__AssetSpawned(0.01,_group,asset,Request) end end end end function WAREHOUSE:_SpawnAssetGroundNaval(alias,asset,request,spawnzone,lateactivated) 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.lateActivation=lateactivated 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) return group end return nil end function WAREHOUSE:_SpawnAssetAircraft(alias,asset,request,parking,uncontrolled,lateactivated) if asset and asset.category==Group.Category.AIRPLANE or asset.category==Group.Category.HELICOPTER then local template=self:_SpawnAssetPrepareTemplate(asset,alias) local _type=COORDINATE.WaypointType.TakeOffParking local _action=COORDINATE.WaypointAction.FromParkingArea if asset.takeoffType and asset.takeoffType==COORDINATE.WaypointType.TakeOffParkingHot then _type=COORDINATE.WaypointType.TakeOffParkingHot _action=COORDINATE.WaypointAction.FromParkingAreaHot uncontrolled=false end local airstart=asset.takeoffType and asset.takeoffType==COORDINATE.WaypointType.TurningPoint or false if airstart then _type=COORDINATE.WaypointType.TurningPoint _action=COORDINATE.WaypointAction.TurningPoint uncontrolled=false end if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then if request.toself then local coord=self.airbase:GetCoordinate() if airstart then coord:SetAltitude(math.random(1000,2000)) end local wp=coord:WaypointAir("RADIO",_type,_action,0,false,self.airbase,{},"Parking") template.route.points={wp} else template.route.points=self:_GetFlightplan(asset,self.airbase,request.warehouse.airbase) end else 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 and#parking<#template.units and not airstart 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 if airstart then unit.alt=math.random(1000,2000) end unit.parking_id=nil unit.parking=nil else local coord=nil local terminal=nil if airstart then coord=self.airbase:GetCoordinate():SetAltitude(math.random(1000,2000)) else coord=parking[i].Coordinate terminal=parking[i].TerminalID end if self.Debug and terminal then local text=string.format("Spawnplace unit %s terminal %d.",unit.name,terminal) coord:MarkToAll(text) env.info(text) 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 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()))) if self.flightcontrol then local fg=FLIGHTGROUP:New(aircraft) fg:SetReadyForTakeoff(true) else aircraft:StartUncontrolled(math.random(60)) end 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 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 if self:IsRunwayOperational()then self:RunwayDestroyed() else self.runwaydestroyed=timer.getAbsTime() end end end self:T2(self.lid..string.format("Warehouse %s captured event dead or crash or unit %s",self.alias,tostring(EventData.IniUnitName))) 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,EventData.IniGroup,request) end end end end end end function WAREHOUSE:_UnitDead(deadunit,deadgroup,request) local opsgroup=_DATABASE:FindOpsGroup(deadgroup) if opsgroup then return nil end local nalive=deadgroup:CountAliveUnits() local groupdead=false if nalive>0 then groupdead=false else groupdead=true end local asset=self:FindAssetInDB(deadgroup) local unitname=self:_GetNameWithOut(deadunit) local groupname=self:_GetNameWithOut(deadgroup) if groupdead then self:T(self.lid..string.format("Group %s (transport=%s) is dead!",groupname,tostring(self:_GroupIsTransport(deadgroup,request)))) if self.Debug then deadgroup:SmokeWhite() end self:AssetDead(asset,request) end local NoTriggerEvent=true if request.transporttype~=WAREHOUSE.TransportType.SELFPROPELLED then if not asset.iscargo 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 else self:E(self.lid..string.format("ERROR: Group %s is neither cargo nor transport!",deadgroup: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 coalition! 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 local asset=_assets[1] _assetattribute=_assets[1].attribute _assetcategory=_assets[1].category _assetairstart=_assets[1].takeoffType and _assets[1].takeoffType==COORDINATE.WaypointType.TurningPoint or false if _assetcategory==Group.Category.AIRPLANE or _assetcategory==Group.Category.HELICOPTER then if self.airbase and self.airbase:GetCoalition()==self:GetCoalition()then if self.airbase.storage then local nS=self.airbase.storage:GetAmount(asset.unittype) local nA=asset.nunits*request.nasset if nS NOT enough to spawn the requested %d asset units (%d groups)", self.alias,nS,asset.unittype,nA,request.nasset) self:_InfoMessage(text,5) return false end end if self:IsRunwayOperational()or _assetairstart then if _assetairstart then else 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 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 elseif _assetcategory==Group.Category.AIRPLANE or _assetcategory==Group.Category.HELICOPTER then 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 or _attribute==WAREHOUSE.Attribute.AIR_UAV 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 coords={} if not self.allowSpawnOnClientSpots then local clients=_DATABASE.CLIENTS for clientname,client in pairs(clients)do local template=_DATABASE:GetGroupTemplateFromUnitName(clientname) if template then 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 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() if unit and unit:IsAlive()then table.insert(obstacles,{coord=_coord,size=_size,name=_name,type="unit"}) end 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 if not _asset.spawned then local terminaltype=asset.terminalType or self:_GetTerminal(asset.attribute,self:GetAirbaseCategory()) parking[_asset.uid]={} for i=1,_asset.nunits do local assetname=_asset.spawngroupname.."-"..tostring(i) local gotit=false for _,_parkingspot in pairs(parkingdata)do local parkingspot=_parkingspot local valid=true if asset.parkingIDs then valid=self:_CheckParkingAsset(parkingspot,asset) else local validTerminal=AIRBASE._CheckTerminalType(parkingspot.TerminalType,terminaltype) local validParking=self:_CheckParkingValid(parkingspot) local validBWlist=airbase:_CheckParkingLists(parkingspot.TerminalID) valid=validTerminal and validParking and validBWlist end if valid 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 self:T3(self.lid..string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is NOT SAFE",assetname,_asset.uid,_termid,dist)) free=false problem=obstacle problem.dist=dist break else end end if free then table.insert(parking[_asset.uid],parkingspot) self:T(self.lid..string.format("Parking spot %d is free for asset %s [id=%d]!",_termid,assetname,_asset.uid)) table.insert(obstacles,{coord=_spot,size=_asset.size,name=assetname,type="asset"}) gotit=true break else if self.Debug then local coord=problem.coord if coord then local text=string.format("Obstacle %s [type=%s] blocking spot=%d! Size=%.1f m and distance=%.1f m.",problem.name,problem.type,_termid,problem.size,problem.dist) self:I(self.lid..text) coord:MarkToAll(text) end else self:T(self.lid..string.format("Parking spot %d is occupied or not big enough!",_termid)) end end else self:T2(self.lid..string.format("Terminal ID=%d: type=%s not supported",parkingspot.TerminalID,parkingspot.TerminalType)) end end if not gotit then self:I(self.lid..string.format("WARNING: No free parking spot for asset %s [id=%d]",assetname,_asset.uid)) return nil end end 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) if group then local groupname=group:GetName() local wid,aid,rid=self:_GetIDsFromGroupName(groupname) return wid,aid,rid else self:E("WARNING: Group not found in GetIDsFromGroup() function!") end end function WAREHOUSE:_GetIDsFromGroupName(groupname) 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 local wid,aid,rid=analyse(groupname) local asset=self:GetAssetByID(aid) if asset then wid=asset.wid rid=asset.rid end self:T3(self.lid..string.format("Group Name = %s",tostring(groupname))) self:T3(self.lid..string.format("Warehouse ID = %s",tostring(wid))) self:T3(self.lid..string.format("Asset ID = %s",tostring(aid))) self:T3(self.lid..string.format("Request ID = %s",tostring(rid))) return wid,aid,rid end 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 groupCat=group:GetCategory() local transportplane=group:HasAttribute("Transports")and group:HasAttribute("Planes")and groupCat==Group.Category.AIRPLANE 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("APC") local truck=group:HasAttribute("Trucks")and group:GetCategory()==Group.Category.GROUND local infantry=group:HasAttribute("Infantry") local ifv=group:HasAttribute("IFV") 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 ifv then attribute=WAREHOUSE.Attribute.GROUND_IFV 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) 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 self.Debug and 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={} local _type=COORDINATE.WaypointType.TakeOffParking local _action=COORDINATE.WaypointAction.FromParkingArea if asset.takeoffType and asset.takeoffType==COORDINATE.WaypointType.TakeOffParkingHot then _type=COORDINATE.WaypointType.TakeOffParkingHot _action=COORDINATE.WaypointAction.FromParkingAreaHot else end c[#c+1]=Pdeparture wp[#wp+1]=Pdeparture:WaypointAir("RADIO",_type,_action,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", verbose=0, 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.8.0" 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:SetEnableF10Menu() self.menudisabled=false return self end function FOX:SetVerbosity(VerbosityLevel) self.verbose=VerbosityLevel or 0 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) if self.verbose>=1 then self:I(self.lid..string.format("Missile trainer status %s: %s",clock,fsmstate)) end 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 if self.verbose>=2 then self:I(self.lid..text) end 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:GetSet())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._FuncTrack(weapon,self,missile) local missileCoord=missile.missileCoord:UpdateFromVec3(weapon.vec3) local missileVelocity=weapon:GetSpeed() self:GetMissileTarget(missile) local target=nil if missile.targetUnit then if missile.targetPlayer then if missile.targetPlayer.destroy==true then target=missile.targetUnit end else if self:_IsProtected(missile.targetUnit)then target=missile.targetUnit end end else local function _GetTarget(_unit) local unit=_unit local playerCoord=unit:GetCoordinate() local dist=missileCoord:Get3DDistance(playerCoord) if dist<=self.explosiondist then return unit end end local mindist=nil for _,_player in pairs(self.players)do local player=_player if player.unitname~=missile.shooterName then local playerCoord=player.unit:GetCoordinate() local dist=missileCoord:Get3DDistance(playerCoord) local Dshooter2player=playerCoord:Get3DDistance(missile.shotCoord) if(mindist==nil or dist=self.bigmissilemass end if destroymissile and self:_CheckCoordSafe(targetVec3)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)) weapon:Destroy() missile.active=false if self.Debug then missileCoord:SmokeRed() 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 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 weapon:SetTimeStepTrack(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)) weapon:SetTimeStepTrack(0.1) end end function FOX._FuncImpact(weapon,self,missile) if missile.targetPlayer then local player=missile.targetPlayer 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.")) weapon.tracking=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:T(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 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 or"unknown")) end else self:E(self.lid..string.format("ERROR: Player unit does not exist in AddF10Menu() function. Unit name: %s.",_unitName or"unknown")) 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 Vec2={x=coord.x,y=coord.z} local inzone=zone:IsVec2InZone(Vec2) 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 Vec2={x=coord.x,y=coord.z} local inzone=zone:IsVec2InZone(Vec2) 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={}, SAM_Table_Long={}, SAM_Table_Medium={}, SAM_Table_Short={}, SAM_Table_PointDef={}, lid="", Detection=nil, AWACS_Detection=nil, debug=false, checkradius=25000, grouping=5000, acceptrange=80000, detectinterval=30, engagerange=95, 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=25000, UseEmOnOff=false, TimeStamp=0, state2flag=false, SamStateTracker={}, DLink=false, DLTimeStamp=0, Padding=10, SuppressedGroups={}, automode=true, autoshorad=true, ShoradGroupSet=nil, checkforfriendlies=false, SmokeDecoy=false, SmokeDecoyColor=SMOKECOLOR.White, checkcounter=1, } MANTIS.AdvancedState={ GREEN=0, AMBER=1, RED=2, } MANTIS.SamType={ SHORT="Short", MEDIUM="Medium", LONG="Long", POINT="Point", } MANTIS.radiusscale={} MANTIS.radiusscale[MANTIS.SamType.LONG]=1.1 MANTIS.radiusscale[MANTIS.SamType.MEDIUM]=1.2 MANTIS.radiusscale[MANTIS.SamType.SHORT]=1.75 MANTIS.radiusscale[MANTIS.SamType.POINT]=3 MANTIS.SamData={ ["Hawk"]={Range=35,Blindspot=0,Height=12,Type="Medium",Radar="Hawk"}, ["NASAMS"]={Range=14,Blindspot=0,Height=7,Type="Short",Radar="NSAMS"}, ["Patriot"]={Range=99,Blindspot=0,Height=25,Type="Long",Radar="Patriot"}, ["Rapier"]={Range=10,Blindspot=0,Height=3,Type="Short",Radar="rapier"}, ["SA-2"]={Range=40,Blindspot=7,Height=25,Type="Medium",Radar="S_75M_Volhov"}, ["SA-3"]={Range=18,Blindspot=6,Height=18,Type="Short",Radar="5p73 s-125 ln"}, ["SA-5"]={Range=250,Blindspot=7,Height=40,Type="Long",Radar="5N62V"}, ["SA-6"]={Range=25,Blindspot=0,Height=8,Type="Medium",Radar="1S91"}, ["SA-10"]={Range=119,Blindspot=0,Height=18,Type="Long",Radar="S-300PS 4"}, ["SA-11"]={Range=35,Blindspot=0,Height=20,Type="Medium",Radar="SA-11"}, ["Roland"]={Range=5,Blindspot=0,Height=5,Type="Point",Radar="Roland"}, ["HQ-7"]={Range=12,Blindspot=0,Height=3,Type="Short",Radar="HQ-7"}, ["SA-9"]={Range=4,Blindspot=0,Height=3,Type="Point",Radar="Strela",Point="true"}, ["SA-8"]={Range=10,Blindspot=0,Height=5,Type="Short",Radar="Osa 9A33"}, ["SA-19"]={Range=8,Blindspot=0,Height=3,Type="Short",Radar="Tunguska"}, ["SA-15"]={Range=11,Blindspot=0,Height=6,Type="Point",Radar="Tor 9A331",Point="true"}, ["SA-13"]={Range=5,Blindspot=0,Height=3,Type="Point",Radar="Strela",Point="true"}, ["Avenger"]={Range=4,Blindspot=0,Height=3,Type="Short",Radar="Avenger"}, ["Chaparral"]={Range=8,Blindspot=0,Height=3,Type="Short",Radar="Chaparral"}, ["Linebacker"]={Range=4,Blindspot=0,Height=3,Type="Point",Radar="Linebacker",Point="true"}, ["Silkworm"]={Range=90,Blindspot=1,Height=0.2,Type="Long",Radar="Silkworm"}, ["SA-10B"]={Range=75,Blindspot=0,Height=18,Type="Medium",Radar="SA-10B"}, ["SA-17"]={Range=50,Blindspot=3,Height=30,Type="Medium",Radar="SA-17"}, ["SA-20A"]={Range=150,Blindspot=5,Height=27,Type="Long",Radar="S-300PMU1"}, ["SA-20B"]={Range=200,Blindspot=4,Height=27,Type="Long",Radar="S-300PMU2"}, ["HQ-2"]={Range=50,Blindspot=6,Height=35,Type="Medium",Radar="HQ_2_Guideline_LN"}, ["TAMIR IDFA"]={Range=20,Blindspot=0.6,Height=12.3,Type="Short",Radar="IRON_DOME_LN"}, ["STUNNER IDFA"]={Range=250,Blindspot=1,Height=45,Type="Long",Radar="DAVID_SLING_LN"}, } MANTIS.SamDataHDS={ ["SA-2 HDS"]={Range=56,Blindspot=7,Height=30,Type="Medium",Radar="V759"}, ["SA-3 HDS"]={Range=20,Blindspot=6,Height=30,Type="Short",Radar="V-601P"}, ["SA-10C HDS 2"]={Range=90,Blindspot=5,Height=25,Type="Long",Radar="5P85DE ln"}, ["SA-10C HDS 1"]={Range=90,Blindspot=5,Height=25,Type="Long",Radar="5P85CE ln"}, ["SA-12 HDS 2"]={Range=100,Blindspot=10,Height=25,Type="Long",Radar="S-300V 9A82 l"}, ["SA-12 HDS 1"]={Range=75,Blindspot=1,Height=25,Type="Long",Radar="S-300V 9A83 l"}, ["SA-23 HDS 2"]={Range=200,Blindspot=5,Height=37,Type="Long",Radar="S-300VM 9A82ME"}, ["SA-23 HDS 1"]={Range=100,Blindspot=1,Height=50,Type="Long",Radar="S-300VM 9A83ME"}, ["HQ-2 HDS"]={Range=50,Blindspot=6,Height=35,Type="Medium",Radar="HQ_2_Guideline_LN"}, } MANTIS.SamDataSMA={ ["RBS98M SMA"]={Range=20,Blindspot=0,Height=8,Type="Short",Radar="RBS-98"}, ["RBS70 SMA"]={Range=8,Blindspot=0,Height=5.5,Type="Short",Radar="RBS-70"}, ["RBS70M SMA"]={Range=8,Blindspot=0,Height=5.5,Type="Short",Radar="BV410_RBS70"}, ["RBS90 SMA"]={Range=8,Blindspot=0,Height=5.5,Type="Short",Radar="RBS-90"}, ["RBS90M SMA"]={Range=8,Blindspot=0,Height=5.5,Type="Short",Radar="BV410_RBS90"}, ["RBS103A SMA"]={Range=150,Blindspot=3,Height=24.5,Type="Long",Radar="LvS-103_Lavett103_Rb103A"}, ["RBS103B SMA"]={Range=35,Blindspot=0,Height=36,Type="Medium",Radar="LvS-103_Lavett103_Rb103B"}, ["RBS103AM SMA"]={Range=150,Blindspot=3,Height=24.5,Type="Long",Radar="LvS-103_Lavett103_HX_Rb103A"}, ["RBS103BM SMA"]={Range=35,Blindspot=0,Height=36,Type="Medium",Radar="LvS-103_Lavett103_HX_Rb103B"}, ["Lvkv9040M SMA"]={Range=4,Blindspot=0,Height=2.5,Type="Point",Radar="LvKv9040",Point="true"}, } MANTIS.SamDataCH={ ["2S38 CHM"]={Range=8,Blindspot=0.5,Height=6,Type="Short",Radar="2S38"}, ["PantsirS1 CHM"]={Range=20,Blindspot=1.2,Height=15,Type="Short",Radar="PantsirS1"}, ["PantsirS2 CHM"]={Range=30,Blindspot=1.2,Height=18,Type="Medium",Radar="PantsirS2"}, ["PGL-625 CHM"]={Range=10,Blindspot=0.5,Height=5,Type="Short",Radar="PGL_625"}, ["HQ-17A CHM"]={Range=20,Blindspot=1.5,Height=10,Type="Short",Radar="HQ17A"}, ["M903PAC2 CHM"]={Range=160,Blindspot=3,Height=24.5,Type="Long",Radar="MIM104_M903_PAC2"}, ["M903PAC3 CHM"]={Range=120,Blindspot=1,Height=40,Type="Long",Radar="MIM104_M903_PAC3"}, ["TorM2 CHM"]={Range=12,Blindspot=1,Height=10,Type="Short",Radar="TorM2"}, ["TorM2K CHM"]={Range=12,Blindspot=1,Height=10,Type="Short",Radar="TorM2K"}, ["TorM2M CHM"]={Range=16,Blindspot=1,Height=10,Type="Short",Radar="TorM2M"}, ["NASAMS3-AMRAAMER CHM"]={Range=50,Blindspot=2,Height=35.7,Type="Medium",Radar="CH_NASAMS3_LN_AMRAAM_ER"}, ["NASAMS3-AIM9X2 CHM"]={Range=20,Blindspot=0.2,Height=18,Type="Short",Radar="CH_NASAMS3_LN_AIM9X2"}, ["C-RAM CHM"]={Range=2,Blindspot=0,Height=2,Type="Point",Radar="CH_Centurion_C_RAM",Point="true"}, ["PGZ-09 CHM"]={Range=4,Blindspot=0,Height=3,Type="Point",Radar="CH_PGZ09",Point="true"}, ["S350-9M100 CHM"]={Range=15,Blindspot=1.5,Height=8,Type="Short",Radar="CH_S350_50P6_9M100"}, ["S350-9M96D CHM"]={Range=150,Blindspot=2.5,Height=30,Type="Long",Radar="CH_S350_50P6_9M96D"}, ["LAV-AD CHM"]={Range=8,Blindspot=0.2,Height=4.8,Type="Short",Radar="CH_LAVAD"}, ["HQ-22 CHM"]={Range=170,Blindspot=5,Height=27,Type="Long",Radar="CH_HQ22_LN"}, ["PGZ-95 CHM"]={Range=2,Blindspot=0,Height=2,Type="Point",Radar="CH_PGZ95",Point="true"}, ["LD-3000 CHM"]={Range=3,Blindspot=0,Height=3,Type="Point",Radar="CH_LD3000_stationary",Point="true"}, ["LD-3000M CHM"]={Range=3,Blindspot=0,Height=3,Type="Point",Radar="CH_LD3000",Point="true"}, ["FlaRakRad CHM"]={Range=8,Blindspot=1.5,Height=6,Type="Short",Radar="HQ17A"}, ["IRIS-T SLM CHM"]={Range=40,Blindspot=0.5,Height=20,Type="Medium",Radar="CH_IRIST_SLM"}, ["M903PAC2KAT1 CHM"]={Range=160,Blindspot=3,Height=24.5,Type="Long",Radar="CH_MIM104_M903_PAC2_KAT1"}, ["Skynex CHM"]={Range=3.5,Blindspot=0,Height=3.5,Type="Point",Radar="CH_SkynexHX",Point="true"}, ["Skyshield CHM"]={Range=3.5,Blindspot=0,Height=3.5,Type="Point",Radar="CH_Skyshield_Gun",Point="true"}, ["WieselOzelot CHM"]={Range=8,Blindspot=0.2,Height=4.8,Type="Short",Radar="CH_Wiesel2Ozelot"}, ["BukM3-9M317M CHM"]={Range=70,Blindspot=0.25,Height=35,Type="Medium",Radar="CH_BukM3_9A317M"}, ["BukM3-9M317MA CHM"]={Range=70,Blindspot=0.25,Height=35,Type="Medium",Radar="CH_BukM3_9A317MA"}, ["SkySabre CHM"]={Range=30,Blindspot=0.5,Height=10,Type="Medium",Radar="CH_SkySabreLN"}, ["Stormer CHM"]={Range=7.5,Blindspot=0.3,Height=7,Type="Short",Radar="CH_StormerHVM"}, ["THAAD CHM"]={Range=200,Blindspot=40,Height=150,Type="Long",Radar="CH_THAAD_M1120"}, ["USInfantryFIM92K CHM"]={Range=8,Blindspot=0.2,Height=4.8,Type="Short",Radar="CH_USInfantry_FIM92"}, ["RBS98M CHM"]={Range=20,Blindspot=0,Height=8,Type="Short",Radar="RBS-98"}, ["RBS70 CHM"]={Range=8,Blindspot=0,Height=5.5,Type="Short",Radar="RBS-70"}, ["RBS90 CHM"]={Range=8,Blindspot=0,Height=5.5,Type="Short",Radar="RBS-90"}, ["RBS103A CHM"]={Range=150,Blindspot=3,Height=24.5,Type="Long",Radar="LvS-103_Lavett103_Rb103A"}, ["RBS103B CHM"]={Range=35,Blindspot=0,Height=36,Type="Medium",Radar="LvS-103_Lavett103_Rb103B"}, ["RBS103AM CHM"]={Range=150,Blindspot=3,Height=24.5,Type="Long",Radar="LvS-103_Lavett103_HX_Rb103A"}, ["RBS103BM CHM"]={Range=35,Blindspot=0,Height=36,Type="Medium",Radar="LvS-103_Lavett103_HX_Rb103B"}, ["Lvkv9040M CHM"]={Range=4,Blindspot=0,Height=2.5,Type="Point",Radar="LvKv9040",Point="true"}, } do function MANTIS:New(name,samprefix,ewrprefix,hq,coalition,dynamic,awacs,EmOnOff,Padding,Zones) local self=BASE:Inherit(self,FSM:New()) 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=coalition or"red" self.SAM_Table={} self.SAM_Table_Long={} self.SAM_Table_Medium={} self.SAM_Table_Short={} self.SAM_Table_PointDef={} self.dynamic=dynamic or false self.checkradius=25000 self.grouping=5000 self.acceptrange=80000 self.detectinterval=30 self.engagerange=95 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=25000 self.TimeStamp=timer.getAbsTime() self.relointerval=math.random(1800,3600) self.state2flag=false self.SamStateTracker={} self.DLink=false self.Padding=Padding or 10 self.SuppressedGroups={} self.automode=true self.usezones=false self.AcceptZones={} self.RejectZones={} self.ConflictZones={} self.maxlongrange=1 self.maxmidrange=2 self.maxshortrange=2 self.maxpointdefrange=6 self.maxclassic=6 self.autoshorad=true self.ShoradGroupSet=SET_GROUP:New() self.FilterZones=Zones self.SkateZones=nil self.SkateNumber=3 self.shootandscoot=false self.SmokeDecoy=false self.SmokeDecoyColor=SMOKECOLOR.White self.UseEmOnOff=true if EmOnOff==false then self.UseEmOnOff=false end if type(awacs)=="string"then self.advAwacs=true else self.advAwacs=false end self.lid=string.format("MANTIS %s | ",self.name) if self.debug then BASE:TraceOnOff(true) BASE:TraceClass(self.ClassName) BASE:TraceLevel(1) end self.ewr_templates={} if type(samprefix)~="table"then self.SAM_Templates_Prefix={samprefix} end if type(ewrprefix)~="table"then self.EWR_Templates_Prefix={ewrprefix} end for _,_group in pairs(self.SAM_Templates_Prefix)do table.insert(self.ewr_templates,_group) end for _,_group in pairs(self.EWR_Templates_Prefix)do table.insert(self.ewr_templates,_group) end if self.advAwacs then table.insert(self.ewr_templates,awacs) end self:T({self.ewr_templates}) self.SAM_Group=SET_GROUP:New():FilterPrefixes(self.SAM_Templates_Prefix):FilterCoalitions(self.Coalition) self.EWR_Group=SET_GROUP:New():FilterPrefixes(self.ewr_templates):FilterCoalitions(self.Coalition) if self.FilterZones then self.SAM_Group:FilterZones(self.FilterZones) end if self.dynamic then self.SAM_Group:FilterStart() self.EWR_Group:FilterStart() else self.SAM_Group:FilterOnce() self.EWR_Group:FilterOnce() end if self.HQ_Template_CC then self.HQ_CC=GROUP:FindByName(self.HQ_Template_CC) end self.checkcounter=1 self.version="0.9.27" self:I(string.format("***** Starting MANTIS Version %s *****",self.version)) self:SetStartState("Stopped") self:AddTransition("Stopped","Start","Running") self:AddTransition("*","Status","*") self:AddTransition("*","Relocating","*") self:AddTransition("*","GreenState","*") self:AddTransition("*","RedState","*") self:AddTransition("*","AdvStateChange","*") self:AddTransition("*","ShoradActivated","*") self:AddTransition("*","SeadSuppressionStart","*") self:AddTransition("*","SeadSuppressionEnd","*") self:AddTransition("*","SeadSuppressionPlanned","*") self:AddTransition("*","Stop","Stopped") return self end function MANTIS:_GetSAMTable() self:T(self.lid.."GetSAMTable") return self.SAM_Table end function MANTIS:_SetSAMTable(table) self:T(self.lid.."SetSAMTable") self.SAM_Table=table return self end function MANTIS:SetEWRGrouping(radius) self:T(self.lid.."SetEWRGrouping") local radius=radius or 5000 self.grouping=radius return self end function MANTIS:AddScootZones(ZoneSet,Number,Random,Formation) self:T(self.lid.." AddScootZones") self.SkateZones=ZoneSet self.SkateNumber=Number or 3 self.shootandscoot=true self.ScootRandom=Random self.ScootFormation=Formation or"Cone" return self end function MANTIS:AddZones(AcceptZones,RejectZones,ConflictZones) self:T(self.lid.."AddZones") self.AcceptZones=AcceptZones or{} self.RejectZones=RejectZones or{} self.ConflictZones=ConflictZones or{} if#self.AcceptZones>0 or#self.RejectZones>0 or#self.ConflictZones>0 then self.usezones=true end return self end function MANTIS:SetEWRRange(radius) self:T(self.lid.."SetEWRRange") return self end function MANTIS:SetSAMRadius(radius) self:T(self.lid.."SetSAMRadius") local radius=radius or 25000 self.checkradius=radius return self end function MANTIS:SetSAMRange(range) self:T(self.lid.."SetSAMRange") local range=range or 95 if range<0 or range>100 then range=95 end self.engagerange=range return self end function MANTIS:SetSmokeDecoy(Onoff,Color) self.SmokeDecoy=Onoff self.SmokeDecoyColor=Color or SMOKECOLOR.White return self end function MANTIS:SetMaxActiveSAMs(Short,Mid,Long,Classic,Point) self:T(self.lid.."SetMaxActiveSAMs") self.maxclassic=Classic or 6 self.maxlongrange=Long or 1 self.maxmidrange=Mid or 2 self.maxshortrange=Short or 2 self.maxpointdefrange=Point or 6 return self end function MANTIS:SetNewSAMRangeWhileRunning(range) self:T(self.lid.."SetNewSAMRangeWhileRunning") local range=range or 95 if range<0 or range>100 then range=95 end self.engagerange=range self:_RefreshSAMTable() self.mysead.EngagementRange=range return self end function MANTIS:Debug(onoff) self:T(self.lid.."SetDebug") local onoff=onoff or false self.debug=onoff if onoff then BASE:TraceOn() BASE:TraceClass("MANTIS") BASE:TraceLevel(1) else BASE:TraceOff() end return self end function MANTIS:GetCommandCenter() self:T(self.lid.."GetCommandCenter") if self.HQ_CC then return self.HQ_CC else return nil end end function MANTIS:SetAwacs(prefix) self:T(self.lid.."SetAwacs") if prefix~=nil then if type(prefix)=="string"then self.AWACS_Prefix=prefix self.advAwacs=true end end return self end function MANTIS:SetAwacsRange(range) self:T(self.lid.."SetAwacsRange") local range=range or 250000 self.awacsrange=range return self end function MANTIS:SetCommandCenter(group) self:T(self.lid.."SetCommandCenter") 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 return self end function MANTIS:SetDetectInterval(interval) self:T(self.lid.."SetDetectInterval") local interval=interval or 30 self.detectinterval=interval return self end function MANTIS:SetAdvancedMode(onoff,ratio) self:T(self.lid.."SetAdvancedMode") 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() self:I(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() self:E(text) end return self end function MANTIS:SetUsingEmOnOff(switch) self:T(self.lid.."SetUsingEmOnOff") self.UseEmOnOff=switch or false return self end function MANTIS:SetUsingDLink(DLink) self:T(self.lid.."SetUsingDLink") self.DLink=true self.Detection=DLink self.DLTimeStamp=timer.getAbsTime() return self end function MANTIS:_CheckHQState() self:T(self.lid.."CheckHQState") local text=self.lid.." Checking HQ State" local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) if self.verbose then self:I(text)end if self.advanced then local hq=self.HQ_Template_CC local hqgrp=GROUP:FindByName(hq) if hqgrp then if hqgrp:IsAlive()then return true else return false end end end return self end function MANTIS:_CheckEWRState() self:T(self.lid.."CheckEWRState") local text=self.lid.." Checking EWR State" local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) if self.verbose then self:I(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 if nalive>0 then return true else return false end end return self end function MANTIS:_CheckAnyEWRAlive() self:T(self.lid.."_CheckAnyEWRAlive") local alive=false if self.EWR_Group:CountAlive()>0 then alive=true end if not alive and self.AWACS_Prefix then local awacs=GROUP:FindByName(self.AWACS_Prefix) if awacs and awacs:IsAlive()then alive=true end end return alive end function MANTIS:_CalcAdvState() self:T(self.lid.."CalcAdvState") local m=MESSAGE:New(self.lid.." Calculating Advanced State",10,"MANTIS"):ToAllIf(self.debug) if self.verbose then self:I(self.lid.." Calculating Advanced State")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) if self.debug or self.verbose then local text=self.lid..string.format(" Calculated OldState/NewState/Interval: %d / %d / %d",currstate,self.adv_state,newinterval) local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) if self.verbose then self:I(text)end end return newinterval,currstate end function MANTIS:SetAutoRelocate(hq,ewr) self:T(self.lid.."SetAutoRelocate") local hqrel=hq or false local ewrel=ewr or false if hqrel or ewrel then self.autorelocate=true self.autorelocateunits={HQ=hqrel,EWR=ewrel} end return self end function MANTIS:_RelocateGroups() self:T(self.lid.."RelocateGroups") local text=self.lid.." Relocating Groups" local m=MESSAGE:New(text,10,"MANTIS",true):ToAllIf(self.debug) if self.verbose then self:I(text)end if self.autorelocate then local HQGroup=self.HQ_CC if self.autorelocateunits.HQ and self.HQ_CC and HQGroup:IsAlive()then local _hqgrp=self.HQ_CC local text=self.lid.." Relocating HQ" _hqgrp:RelocateGroundRandomInRadius(20,500,true,true,nil,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:IsAlive()and _grp:IsGround()then local text=self.lid.." Relocating EWR ".._grp:GetName() local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) if self.verbose then self:I(text)end _grp:RelocateGroundRandomInRadius(20,500,true,true,nil,true) end end end end return self end function MANTIS:_CheckCoordinateInZones(coord) self:T(self.lid.."_CheckCoordinateInZones") local inzone=false if#self.AcceptZones>0 then for _,_zone in pairs(self.AcceptZones)do local zone=_zone if zone:IsCoordinateInZone(coord)then inzone=true self:T(self.lid.."Target coord in Accept Zone!") break end end end if#self.RejectZones>0 and inzone then for _,_zone in pairs(self.RejectZones)do local zone=_zone if zone:IsCoordinateInZone(coord)then inzone=false self:T(self.lid.."Target coord in Reject Zone!") break end end end if#self.ConflictZones>0 and not inzone then for _,_zone in pairs(self.ConflictZones)do local zone=_zone if zone:IsCoordinateInZone(coord)then inzone=true self:T(self.lid.."Target coord in Conflict Zone!") break end end end return inzone end function MANTIS:_PreFilterHeight(height) self:T(self.lid.."_PreFilterHeight") local set={} local dlink=self.Detection local detectedgroups=dlink:GetContactTable() for _,_contact in pairs(detectedgroups)do local contact=_contact local grp=contact.group if grp:IsAlive()then if grp:GetHeight(true)29)then self.DLink=false self.Detection=self:StartDetection() self:I(self.lid.."Intel DLink not running - switching back to single detection!") end end function MANTIS:onafterStart(From,Event,To) self:T({From,Event,To}) self:T(self.lid.."Starting MANTIS") self:SetSAMStartState() if not INTEL then self.Detection=self:StartDetection() else self.Detection=self:StartIntelDetection() end if self.autoshorad then self.Shorad=SHORAD:New(self.name.."-SHORAD","SHORAD",self.SAM_Group,self.ShoradActDistance,self.ShoradTime,self.coalition,self.UseEmOnOff) self.Shorad:SetDefenseLimits(80,95) self.ShoradLink=true self.Shorad.Groupset=self.ShoradGroupSet self.Shorad.debug=self.debug end if self.shootandscoot and self.SkateZones and self.Shorad then self.Shorad:AddScootZones(self.SkateZones,self.SkateNumber or 3,self.ScootRandom,self.ScootFormation) end self:__Status(-math.random(1,10)) return self end function MANTIS:onbeforeStatus(From,Event,To) self:T({From,Event,To}) if not self.state2flag then self:_Check(self.Detection,self.DLink) end local EWRAlive=self:_CheckAnyEWRAlive() local function FindSAMSRTR() for i=1,1000 do local randomsam=self.SAM_Group:GetRandom() if randomsam and randomsam:IsAlive()then if randomsam:IsSAM()then return randomsam end end end end if not EWRAlive then local randomsam=FindSAMSRTR() if randomsam and randomsam:IsAlive()then if self.UseEmOnOff then randomsam:EnableEmission(true) else randomsam:OptionAlarmStateRed() end local name=randomsam:GetName() if self.SamStateTracker[name]~="RED"then self:__RedState(1,randomsam) self.SamStateTracker[name]="RED" end end end if self.autorelocate then local relointerval=self.relointerval local thistime=timer.getAbsTime() local timepassed=thistime-self.TimeStamp local halfintv=math.floor(timepassed/relointerval) if halfintv>=1 then self.TimeStamp=timer.getAbsTime() self:_Relocate() self:__Relocating(1) end end if self.advanced then self:_CheckAdvState() end if self.DLink then self:_CheckDLinkState() end return self end function MANTIS:onafterStatus(From,Event,To) self:T({From,Event,To}) if self.debug and self.verbose then self:I(self.lid.."Status Report") for _name,_state in pairs(self.SamStateTracker)do self:I(string.format("Site %s\tStatus %s",_name,_state)) end end local interval=self.detectinterval*-1 self:__Status(interval) return self end function MANTIS:onafterStop(From,Event,To) self:T({From,Event,To}) return self end function MANTIS:onafterRelocating(From,Event,To) self:T({From,Event,To}) return self end function MANTIS:onafterGreenState(From,Event,To,Group) self:T({From,Event,To,Group:GetName()}) return self end function MANTIS:onafterRedState(From,Event,To,Group) self:T({From,Event,To,Group:GetName()}) return self end function MANTIS:onafterAdvStateChange(From,Event,To,Oldstate,Newstate,Interval) self:T({From,Event,To,Oldstate,Newstate,Interval}) return self end function MANTIS:onafterShoradActivated(From,Event,To,Name,Radius,Ontime) self:T({From,Event,To,Name,Radius,Ontime}) return self end function MANTIS:onafterSeadSuppressionStart(From,Event,To,Group,Name,Attacker) self:T({From,Event,To,Name}) self.SuppressedGroups[Name]=true if self.ShoradLink then local Shorad=self.Shorad local radius=self.checkradius local ontime=self.ShoradTime Shorad:WakeUpShorad(Name,radius,ontime,nil,true) self:__ShoradActivated(1,Name,radius,ontime) end return self end function MANTIS:onafterSeadSuppressionEnd(From,Event,To,Group,Name) self:T({From,Event,To,Name}) self.SuppressedGroups[Name]=false return self end function MANTIS:onafterSeadSuppressionPlanned(From,Event,To,Group,Name,SuppressionStartTime,SuppressionEndTime,Attacker) self:T({From,Event,To,Name}) return self end end SHORAD={ ClassName="SHORAD", name="MyShorad", debug=false, Prefixes="", Radius=20000, Groupset=nil, Samset=nil, Coalition=nil, ActiveTimer=600, ActiveGroups={}, lid="", DefendHarms=true, DefendMavs=true, DefenseLowProb=70, DefenseHighProb=90, UseEmOnOff=true, shootandscoot=false, SkateNumber=3, SkateZones=nil, minscootdist=100, maxscootdist=3000, scootrandomcoord=false, } do SHORAD.Harms={ ["AGM_88"]="AGM_88", ["AGM_122"]="AGM_122", ["AGM_84"]="AGM_84", ["AGM_45"]="AGM_45", ["ALARM"]="ALARM", ["LD-10"]="LD-10", ["X_58"]="X_58", ["X_28"]="X_28", ["X_25"]="X_25", ["X_31"]="X_31", ["Kh25"]="Kh25", ["HY-2"]="HY-2", ["ADM_141A"]="ADM_141A", } SHORAD.Mavs={ ["AGM"]="AGM", ["C-701"]="C-701", ["Kh25"]="Kh25", ["Kh29"]="Kh29", ["Kh31"]="Kh31", ["Kh66"]="Kh66", } function SHORAD:New(Name,ShoradPrefix,Samset,Radius,ActiveTimer,Coalition,UseEmOnOff) local self=BASE:Inherit(self,FSM:New()) self:T({Name,ShoradPrefix,Samset,Radius,ActiveTimer,Coalition}) local GroupSet=SET_GROUP:New():FilterPrefixes(ShoradPrefix):FilterCoalitions(Coalition):FilterCategoryGround():FilterStart() self.name=Name or"MyShorad" self.Prefixes=ShoradPrefix or"SAM SHORAD" self.Radius=Radius or 20000 self.Coalition=Coalition or"blue" self.Samset=Samset or GroupSet self.ActiveTimer=ActiveTimer or 600 self.ActiveGroups={} self.Groupset=GroupSet self.DefendHarms=true self.DefendMavs=true self.DefenseLowProb=70 self.DefenseHighProb=90 self.UseEmOnOff=true if UseEmOnOff==false then self.UseEmOnOff=UseEmOnOff end self:I("*** SHORAD - Started Version 0.3.4") self.lid=string.format("SHORAD %s | ",self.name) self:_InitState() self:HandleEvent(EVENTS.Shot,self.HandleEventShot) self:SetStartState("Running") self:AddTransition("*","WakeUpShorad","*") self:AddTransition("*","CalculateHitZone","*") self:AddTransition("*","ShootAndScoot","*") return self end function SHORAD:_InitState() self:T(self.lid.." _InitState") local table={} local set=self.Groupset self:T({set=set}) local aliveset=set:GetAliveSet() for _,_group in pairs(aliveset)do if self.UseEmOnOff then _group:EnableEmission(false) _group:OptionAlarmStateRed() else _group:OptionAlarmStateGreen() end _group:OptionDisperseOnAttack(30) end for i=1,100 do math.random() end return self end function SHORAD:AddScootZones(ZoneSet,Number,Random,Formation) self:T(self.lid.." AddScootZones") self.SkateZones=ZoneSet self.SkateNumber=Number or 3 self.shootandscoot=true self.scootrandomcoord=Random self.scootformation=Formation or"Cone" return self end function SHORAD:SwitchDebug(onoff) self:T({onoff}) if onoff then self:SwitchDebugOn() else self:SwitchDebugOff() end return self end function SHORAD:SwitchDebugOn() self.debug=true BASE:TraceOn() BASE:TraceClass("SHORAD") return self end function SHORAD:SwitchDebugOff() self.debug=false BASE:TraceOff() return self end function SHORAD:SwitchHARMDefense(onoff) self:T({onoff}) local onoff=onoff or true self.DefendHarms=onoff return self end function SHORAD:SwitchAGMDefense(onoff) self:T({onoff}) local onoff=onoff or true self.DefendMavs=onoff return self end function SHORAD:SetDefenseLimits(low,high) self:T({low,high}) local low=low or 70 local high=high or 90 if(low<0)or(low>100)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 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(TimeInterval) self.dTqueue=TimeInterval or 30 return self end function AIRBOSS:SetLSOCallInterval(TimeInterval) self.LSOdT=TimeInterval or 4 return self end function AIRBOSS:SetIntoWindLegacy(SwitchOn) if SwitchOn==nil then SwitchOn=true end self.intowindold=SwitchOn 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(MaxAltitude) self.initialmaxalt=UTILS.FeetToMeters(MaxAltitude 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(TimeInterval) self.dTstatus=TimeInterval 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) if self.carriertype==AIRBOSS.CarrierType.INVINCIBLE or self.carriertype==AIRBOSS.CarrierType.HERMES or self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS or self.carriertype==AIRBOSS.CarrierType.CANBERRA then self.gle._max=_max or 0.7 self.gle.High=High or 1.4 self.gle.HIGH=HIGH or 1.9 self.gle._min=_min or-0.5 self.gle.Low=Low or-1.2 self.gle.LOW=LOW or-1.5 else 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 end return self end function AIRBOSS:SetLineupErrorThresholds(_max,_min,Left,LeftMed,LEFT,Right,RightMed,RIGHT) if self.carriertype==AIRBOSS.CarrierType.INVINCIBLE or self.carriertype==AIRBOSS.CarrierType.HERMES or self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS or self.carriertype==AIRBOSS.CarrierType.CANBERRA then self.lue._max=_max or 1.8 self.lue._min=_min or-1.8 self.lue.Left=Left or-2.8 self.lue.LeftMed=LeftMed or-3.8 self.lue.LEFT=LEFT or-4.5 self.lue.Right=Right or 2.8 self.lue.RightMed=RightMed or 3.8 self.lue.RIGHT=RIGHT or 4.5 else 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 end 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(TimeInterval) self.dTbeacon=TimeInterval or(20*60) return self end function AIRBOSS:EnableSRS(PathToSRS,Port,Culture,Gender,Voice,GoogleCreds,Volume,AltBackend) local Frequency=self.AirbossRadio.frequency local Modulation=self.AirbossRadio.modulation self.SRS=MSRS:New(PathToSRS,Frequency,Modulation,AltBackend) self.SRS:SetCoalition(self:GetCoalition()) self.SRS:SetCoordinate(self:GetCoordinate()) self.SRS:SetCulture(Culture or"en-US") self.SRS:SetGender(Gender or"male") self.SRS:SetPath(PathToSRS) self.SRS:SetPort(Port or 5002) self.SRS:SetLabel(self.AirbossRadio.alias or"AIRBOSS") self.SRS:SetCoordinate(self.carrier:GetCoordinate()) self.SRS:SetVolume(Volume or 1) if GoogleCreds then self.SRS:SetGoogle(GoogleCreds) end if Voice then self.SRS:SetVoice(Voice) end self.SRS:SetVolume(Volume or 1.0) self.SRSQ=MSRSQUEUE:New("AIRBOSS") self.SRSQ:SetTransmitOnlyWithPlayers(true) if not self.PilotRadio then self:SetSRSPilotVoice() end return self end function AIRBOSS:SetLSORadio(Frequency,Modulation,Voice,Gender,Culture) 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" self.LSORadio.voice=Voice self.LSORadio.gender=Gender or"male" self.LSORadio.culture=Culture or"en-US" return self end function AIRBOSS:SetAirbossRadio(Frequency,Modulation,Voice,Gender,Culture) self.AirbossFreq=Frequency or self:_GetTowerFrequency()or 127.5 Modulation=Modulation or"AM" if type(Modulation)=="table"then self.AirbossModu=Modulation else if Modulation=="FM"then self.AirbossModu=radio.modulation.FM else self.AirbossModu=radio.modulation.AM end end self.AirbossRadio={} self.AirbossRadio.frequency=self.AirbossFreq self.AirbossRadio.modulation=self.AirbossModu self.AirbossRadio.alias="AIRBOSS" self.AirbossRadio.voice=Voice self.AirbossRadio.gender=Gender or"male" self.AirbossRadio.culture=Culture or"en-US" return self end function AIRBOSS:SetMarshalRadio(Frequency,Modulation,Voice,Gender,Culture) 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" self.MarshalRadio.voice=Voice self.MarshalRadio.gender=Gender or"male" self.MarshalRadio.culture=Culture or"en-US" 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:T(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:T(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:SetExtraVoiceOvers(status) self.xtVoiceOvers=status return self end function AIRBOSS:SetExtraVoiceOversAI(status) self.xtVoiceOversAI=status 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:SetFunkManOn(Port,Host) self.funkmanSocket=SOCKET:New(Port,Host) 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.RunwayTouch) 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() if require then self.magvar=pos:GetMagneticDeclination() end 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:T(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.RunwayTouch) 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=18.30 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.carrierparam.landingdist=self.carrierparam.sterndist+self.carrierparam.wire3 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 self.carrierparam.landingdist=self.carrierparam.sterndist+self.carrierparam.wire3 end function AIRBOSS:_InitForrestal() self:_InitNimitz() self.carrierparam.sterndist=-135.5 self.carrierparam.deckheight=20 self.carrierparam.totlength=315 self.carrierparam.totwidthport=45 self.carrierparam.totwidthstarboard=35 self.carrierparam.rwyangle=-9.1359 self.carrierparam.rwylength=212 self.carrierparam.rwywidth=25 self.carrierparam.wire1=44 self.carrierparam.wire2=54 self.carrierparam.wire3=64 self.carrierparam.wire4=74 self.carrierparam.landingdist=self.carrierparam.sterndist+self.carrierparam.wire3 end function AIRBOSS:_InitHermes() self:_InitStennis() self.carrierparam.sterndist=-105 self.carrierparam.deckheight=12 self.carrierparam.totlength=228.19 self.carrierparam.totwidthport=20.5 self.carrierparam.totwidthstarboard=24.5 self.carrierparam.rwyangle=0 self.carrierparam.rwylength=215 self.carrierparam.rwywidth=13 self.carrierparam.wire1=nil self.carrierparam.wire2=nil self.carrierparam.wire3=nil self.carrierparam.wire4=nil self.carrierparam.landingspot=69 self.carrierparam.landingdist=self.carrierparam.sterndist+self.carrierparam.landingspot 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:_InitInvincible() self:_InitStennis() self.carrierparam.sterndist=-105 self.carrierparam.deckheight=12 self.carrierparam.totlength=228.19 self.carrierparam.totwidthport=20.5 self.carrierparam.totwidthstarboard=24.5 self.carrierparam.rwyangle=0 self.carrierparam.rwylength=215 self.carrierparam.rwywidth=13 self.carrierparam.wire1=nil self.carrierparam.wire2=nil self.carrierparam.wire3=nil self.carrierparam.wire4=nil self.carrierparam.landingspot=69 self.carrierparam.landingdist=self.carrierparam.sterndist+self.carrierparam.landingspot 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:_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.carrierparam.landingspot=57 self.carrierparam.landingdist=self.carrierparam.sterndist+self.carrierparam.landingspot 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:_InitAmerica() self:_InitStennis() self.carrierparam.sterndist=-125 self.carrierparam.deckheight=20 self.carrierparam.totlength=257 self.carrierparam.totwidthport=11 self.carrierparam.totwidthstarboard=25 self.carrierparam.rwyangle=0 self.carrierparam.rwylength=240 self.carrierparam.rwywidth=15 self.carrierparam.wire1=nil self.carrierparam.wire2=nil self.carrierparam.wire3=nil self.carrierparam.wire4=nil self.carrierparam.landingspot=59 self.carrierparam.landingdist=self.carrierparam.sterndist+self.carrierparam.landingspot 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:_InitJcarlos() self:_InitStennis() self.carrierparam.sterndist=-125 self.carrierparam.deckheight=20 self.carrierparam.totlength=231 self.carrierparam.totwidthport=10 self.carrierparam.totwidthstarboard=22 self.carrierparam.rwyangle=0 self.carrierparam.rwylength=202 self.carrierparam.rwywidth=14 self.carrierparam.wire1=nil self.carrierparam.wire2=nil self.carrierparam.wire3=nil self.carrierparam.wire4=nil self.carrierparam.landingspot=89 self.carrierparam.landingdist=self.carrierparam.sterndist+self.carrierparam.landingspot 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:_InitCanberra() self:_InitJcarlos() 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.EXPECTSPOT5.duration=1.3 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.EXPECTSPOT5.duration=1.3 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}, EXPECTSPOT5={file="LSO-ExpectSpot5",suffix="ogg",loud=false,subtitle="Expect spot 5",duration=1.3,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}, GREYHOUND={file="PILOT-Greyhound",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 or playerData.actype==AIRBOSS.AircraftCarrier.RHINOE or playerData.actype==AIRBOSS.AircraftCarrier.RHINOF or playerData.actype==AIRBOSS.AircraftCarrier.GROWLER 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=16.0 aoa.Slow=13.5 aoa.OnSpeedMax=12.5 aoa.OnSpeed=10.0 aoa.OnSpeedMin=9.5 aoa.Fast=8.0 aoa.FAST=7.5 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 or playerData.actype==AIRBOSS.AircraftCarrier.RHINOE or playerData.actype==AIRBOSS.AircraftCarrier.RHINOF or playerData.actype==AIRBOSS.AircraftCarrier.GROWLER 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 goshawk=playerData.actype==AIRBOSS.AircraftCarrier.T45C 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 goshawk then dist=UTILS.NMToMeters(0.9) elseif harrier 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(312) 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) if self.xtVoiceOversAI then local leader=flight.group:GetUnits()[1] self:_CommencingCall(leader,flight.onboard) end 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 self:T2(self.lid..string.format("Known flight group %s of type %s in CCA.",groupname,actype)) if knownflight.ai and knownflight.flag==-100 and self.handleai then local putintomarshal=false local flight=_DATABASE:GetOpsGroup(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 if self.xtVoiceOversAI then local leader=flight.group:GetUnits()[1] self:_MarshallInboundCall(leader,flight.onboard) end 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,true):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.RHINOE or actype==AIRBOSS.AircraftCarrier.RHINOF or actype==AIRBOSS.AircraftCarrier.GROWLER 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 or flight.actype==AIRBOSS.AircraftCarrier.RHINOE or flight.actype==AIRBOSS.AircraftCarrier.RHINOF or flight.actype==AIRBOSS.AircraftCarrier.GROWLER then Speed=UTILS.KnotsToKmph(200) elseif flight.actype==AIRBOSS.AircraftCarrier.E2D or flight.actype==AIRBOSS.AircraftCarrier.C2A 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.INVINCIBLE or self.carriertype==AIRBOSS.CarrierType.HERMES or self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS or self.carriertype==AIRBOSS.CarrierType.CANBERRA 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(self.recoverywindow.SPEED) 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:T(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.hover=false playerData.stable=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:T(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 and(not EventData.IniObjectCategory==Object.Category.STATIC)then self:E(self.lid.."ERROR: EventData.IniUnit=nil in event BIRTH!") self:E(EventData) return end if EventData.IniObjectCategory~=Object.Category.UNIT then 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:OnEventRunwayTouch(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.INVINCIBLE or self.carriertype==AIRBOSS.CarrierType.HERMES or self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS or self.carriertype==AIRBOSS.CarrierType.CANBERRA 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.INVINCIBLE or self.carriertype~=AIRBOSS.CarrierType.HERMES or self.carriertype~=AIRBOSS.CarrierType.TARAWA or self.carriertype~=AIRBOSS.CarrierType.AMERICA or self.carriertype~=AIRBOSS.CarrierType.JCARLOS or self.carriertype~=AIRBOSS.CarrierType.CANBERRA 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 or playerData.actype==AIRBOSS.AircraftCarrier.RHINOE or playerData.actype==AIRBOSS.AircraftCarrier.RHINOF or playerData.actype==AIRBOSS.AircraftCarrier.GROWLER 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 and self.carriertype==AIRBOSS.CarrierType.JCARLOS then self:RadioTransmission(self.LSORadio,self.LSOCall.EXPECTSPOT5,nil,nil,nil,true) elseif playerData.actype==AIRBOSS.AircraftCarrier.AV8B and self.carriertype==AIRBOSS.CarrierType.CANBERRA then self:RadioTransmission(self.LSORadio,self.LSOCall.EXPECTSPOT5,nil,nil,nil,true) elseif 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=2.6 glMin=-2.2 luAbs=4.1 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 and playerData.actype~=AIRBOSS.AircraftCarrier.AV8B 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() local case=self.case self.sterncoord:UpdateFromCoordinate(self:GetCoordinate()) if self.carriertype==AIRBOSS.CarrierType.INVINCIBLE or self.carriertype==AIRBOSS.CarrierType.HERMES or self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS or self.carriertype==AIRBOSS.CarrierType.CANBERRA then if case==3 then self.sterncoord:Translate(self.carrierparam.sterndist,hdg,true,true):Translate(8,FB-90,true,true) elseif case==2 or case==1 then self.sterncoord:Translate(self.carrierparam.sterndist,hdg,true,true):Translate(8,FB-90,true,true) end elseif self.carriertype==AIRBOSS.CarrierType.STENNIS then self.sterncoord:Translate(self.carrierparam.sterndist,hdg,true,true):Translate(7,FB+90,true,true) elseif self.carriertype==AIRBOSS.CarrierType.FORRESTAL then self.sterncoord:Translate(self.carrierparam.sterndist,hdg,true,true):Translate(7.5,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:_GetWireFromDrawArg() local wireArgs={} wireArgs[1]=141 wireArgs[2]=142 wireArgs[3]=143 wireArgs[4]=144 for wire,drawArg in pairs(wireArgs)do local value=self.carrier:GetDrawArgumentValue(drawArg) if math.abs(value)>0.001 then return wire end end return 99 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 or playerData.actype==AIRBOSS.AircraftCarrier.RHINOE or playerData.actype==AIRBOSS.AircraftCarrier.RHINOF or playerData.actype==AIRBOSS.AircraftCarrier.GROWLER 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(-45,FB):Translate(15,FB+90) p[3]=S:Translate(-45,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.INVINCIBLE or self.carriertype==AIRBOSS.CarrierType.HERMES or self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS or self.carriertype==AIRBOSS.CarrierType.CANBERRA 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.INVINCIBLE or self.carriertype==AIRBOSS.CarrierType.HERMES or self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS or self.carriertype==AIRBOSS.CarrierType.CANBERRA 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:GetVec3()) 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) local case=self.case if self.carriertype==AIRBOSS.CarrierType.INVINCIBLE or self.carriertype==AIRBOSS.CarrierType.HERMES or self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS or self.carriertype==AIRBOSS.CarrierType.CANBERRA then if case==3 then self.landingcoord:UpdateFromCoordinate(self:_GetLandingSpotCoordinate()) self.landingcoord:SetAltitude(UTILS.FeetToMeters(120)) elseif case==2 or case==1 then self.landingcoord:UpdateFromCoordinate(self:_GetLandingSpotCoordinate()):Translate(35,FB-90,true,true) self.landingcoord:SetAltitude(UTILS.FeetToMeters(120)) end else if self.carrierparam.wire3 then self.landingcoord:Translate(self.carrierparam.wire3,FB,true,true) end self.landingcoord.y=self.landingcoord.y+2 end return self.landingcoord end function AIRBOSS:_GetLandingSpotCoordinate() self.landingspotcoord:UpdateFromCoordinate(self:_GetSternCoord()) local hdg=self:GetHeading() self.landingspotcoord:Translate(self.carrierparam.landingspot,hdg,true,true):SetAltitude(self.carrierparam.deckheight) 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 18) 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 18) 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(vdeck,magnetic,coord) if self.intowindold then return self:GetHeadingIntoWind_old(vdeck,magnetic,coord) else return self:GetHeadingIntoWind_new(vdeck,magnetic,coord) end end function AIRBOSS:GetHeadingIntoWind_old(vdeck,magnetic,coord) local function adjustDegreesForWindSpeed(windSpeed) local degreesAdjustment=0 if windSpeed>0 and windSpeed<3 then degreesAdjustment=30 elseif windSpeed>=3 and windSpeed<5 then degreesAdjustment=20 elseif windSpeed>=5 and windSpeed<8 then degreesAdjustment=8 elseif windSpeed>=8 and windSpeed<13 then degreesAdjustment=4 elseif windSpeed>=13 then degreesAdjustment=0 end return degreesAdjustment end local windfrom,vwind=self:GetWind(nil,nil,coord) local intowind=windfrom-self.carrierparam.rwyangle+adjustDegreesForWindSpeed(vwind) 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 local vtot=math.max(vdeck-UTILS.MpsToKnots(vwind),4) return intowind,vtot end function AIRBOSS:GetHeadingIntoWind_new(vdeck,magnetic,coord) local Offset=self.carrierparam.rwyangle or 0 local windfrom,vwind=self:GetWind(18,nil,coord) local Vmin=4 local Vmax=UTILS.KmphToKnots(self.carrier:GetSpeedMax()) if vwind<0.1 then local h=self:GetHeading(magnetic) return h,math.min(vdeck,Vmax) end vwind=UTILS.MpsToKnots(vwind) local windto=(windfrom+180)%360 local alpha=math.rad(-Offset) local C=math.sqrt(math.cos(alpha)^2/math.sin(alpha)^2+1) local vdeckMax=vwind+math.cos(alpha)*Vmax local vdeckMin=vwind+math.cos(alpha)*Vmin local v=0 local theta=0 if vdeck>vdeckMax then v=Vmax theta=math.asin(v/(vwind*C))-math.asin(-1/C) elseif vdeckvwind then theta=math.pi/2 v=math.sqrt(vdeck^2-vwind^2) else theta=math.asin(vdeck*math.sin(alpha)/vwind) v=vdeck*math.cos(alpha)-vwind*math.cos(theta) end local magvar=magnetic and self.magvar or 0 local intowind=(540+(windto-magvar+math.deg(theta)))%360 return intowind,v end function AIRBOSS:GetBRCintoWind(vdeck) return self:GetHeadingIntoWind(vdeck,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=76 then grade="SLOW V/STOL Groove" else grade="LIG" end if t>=16.4 and t<=16.6 then grade="_OK_" end if playerData.actype==AIRBOSS.AircraftCarrier.AV8B and(t>=60.0 and t<=65.0)then grade="_OK_ V/STOL" 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 vtol=playerData.actype==AIRBOSS.AircraftCarrier.AV8B 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 TgrooveVstolUnicorn=Tgroove and(Tgroove>=60.0 and Tgroove<=65.0)and playerData.actype==AIRBOSS.AircraftCarrier.AV8B or false local grade local points if N==0 and(TgrooveUnicorn or TgrooveVstolUnicorn or playerData.case==3)then grade="_OK_" points=5.0 G="Unicorn" else if vtol then local Gb=GXX.." "..GIM local N=nXX+nIM local nL=count(Gb,'_')/2 local nS=count(Gb,'%(') local nN=N-nS-nL local Gv=GIC.." "..GAR local Nv=nIC+nAR local nLv=count(Gv,'_')/2 local nSv=count(Gv,'%(') local nNv=Nv-nSv-nLv if nL>0 or nLv>1 then grade="--" points=2.0 elseif nN>0 or nNv>1 or nLv==1 then grade="(OK)" points=3.0 else grade="OK" points=4.0 end 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 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 elseif not playerData.hover and playerData.actype==AIRBOSS.AircraftCarrier.AV8B then if playerData.landed then grade="CUT" points=0.0 end 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 or playerData.actype==AIRBOSS.AircraftCarrier.RHINOE or playerData.actype==AIRBOSS.AircraftCarrier.RHINOF or playerData.actype==AIRBOSS.AircraftCarrier.GROWLER 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=UTILS.Round(UTILS.MpsToKnots(windondeck),1) mygrade.modex=playerData.onboard mygrade.airframe=playerData.actype mygrade.carriertype=self.carriertype mygrade.carriername=self.alias mygrade.carrierrwy=self.carrierparam.rwyangle mygrade.theatre=self.theatre mygrade.mitime=UTILS.SecondsToClock(timer.getAbsTime(),true) mygrade.midate=UTILS.GetDCSMissionDate() mygrade.osdate="n/a" if os then mygrade.osdate=os.date() end playerData.grade=mygrade 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 vdeck=UTILS.MpsToKnots(vdeck) local hiw,speedknots=self:GetHeadingIntoWind(vdeck) local vtot=UTILS.KnotsToMps(speedknots) local dist=vtot*time local distNM=UTILS.MetersToNM(dist) local hdg=self:GetHeading() local deltaH=self:_GetDeltaHeading(hdg,hiw) self:I(self.lid..string.format("Carrier steaming into the wind (%.1f kts). Heading=%03d-->%03d (Delta=%.1f), Speed=%.1f knots, Distance=%.1f NM, Time=%d sec", UTILS.MpsToKnots(vwind),hdg,hiw,deltaH,speedknots,distNM,speedknots,time)) 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(vdeck,false,Csoo) Ctiw=Csoo:Translate(dist,hsw) elseif deltaH<90 then Csoo=Cv:Translate(900,hdg):Translate(900,hiw) local hsw=self:GetHeadingIntoWind(vdeck,false,Csoo) Ctiw=Csoo:Translate(dist,hsw) elseif deltaH<135 then Csoo=Cv:Translate(1100,hdg-90):Translate(1000,hiw) local hsw=self:GetHeadingIntoWind(vdeck,false,Csoo) Ctiw=Csoo:Translate(dist,hsw) else Csoo=Cv:Translate(1200,hdg-90):Translate(1000,hiw) local hsw=self:GetHeadingIntoWind(vdeck,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 local vdeck=self.recoverywindow and self.recoverywindow.SPEED or 20 hdg=self:GetHeadingIntoWind(vdeck,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.C2A then nickname="Greyhound" 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.RHINOE or actype==AIRBOSS.AircraftCarrier.RHINOF then nickname="Rhino" elseif actype==AIRBOSS.AircraftCarrier.GROWLER then nickname="Growler" 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 template=group:GetTemplate() local numbers={} if template then local units=template.units 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) else if playeronly then return 101 else local units=group:GetUnits() for i,_unit in pairs(units)do local name=_unit:GetName() numbers[name]=100+i end end end 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.INVINCIBLE or self.carriertype==AIRBOSS.CarrierType.HERMES or self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS or self.carriertype==AIRBOSS.CarrierType.CANBERRA then return true else return false end end if self.carriertype==AIRBOSS.CarrierType.INVINCIBLE or self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS or self.carriertype==AIRBOSS.CarrierType.CANBERRA 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 and DCSunit.getPlayerName 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 if not self.SRS then 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 else if call.subtitle~=nil and string.len(call.subtitle)>1 then local frequency=self.MarshalRadio.frequency local modulation=self.MarshalRadio.modulation local voice=nil local gender=nil local culture=nil if radio.alias=="AIRBOSS"then frequency=self.AirbossRadio.frequency modulation=self.AirbossRadio.modulation voice=self.AirbossRadio.voice gender=self.AirbossRadio.gender culture=self.AirbossRadio.culture end if radio.alias=="MARSHAL"then voice=self.MarshalRadio.voice gender=self.MarshalRadio.gender culture=self.MarshalRadio.culture end if radio.alias=="LSO"then frequency=self.LSORadio.frequency modulation=self.LSORadio.modulation voice=self.LSORadio.voice gender=self.LSORadio.gender culture=self.LSORadio.culture end if pilotcall then voice=self.PilotRadio.voice gender=self.PilotRadio.gender culture=self.PilotRadio.culture radio.alias="PILOT" end if not radio.alias then frequency=self.AirbossRadio.frequency modulation=self.AirbossRadio.modulation radio.alias="AIRBOSS" end local volume=nil if loud then volume=1.0 end local text=call.subtitle self:T(self.lid..text) local srstext=self:_GetNiceSRSText(text) self.SRSQ:NewTransmission(srstext,call.duration,self.SRS,nil,0.1,nil,call.subtitle,call.subduration,frequency,modulation,gender,culture,voice,volume,radio.alias) end end end function AIRBOSS:SetSRSPilotVoice(Voice,Gender,Culture) self.PilotRadio={} self.PilotRadio.alias="PILOT" self.PilotRadio.voice=Voice or MSRS.Voices.Microsoft.David self.PilotRadio.gender=Gender or"male" self.PilotRadio.culture=Culture or"en-US" if(not Voice)and self.SRS and self.SRS:GetProvider()==MSRS.Provider.GOOGLE then self.PilotRadio.voice=MSRS.Voices.Google.Standard.en_US_Standard_J end return self 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:_GetNiceSRSText(text) text=string.gsub(text,"================================\n","") text=string.gsub(text,"||","parallel") text=string.gsub(text,"==","perpendicular") text=string.gsub(text,"BRC","Base recovery") text=string.gsub(text,"%((%a+)%)","Morse %1") text=string.gsub(text,"°C","° Celsius") text=string.gsub(text,"°"," degrees") text=string.gsub(text," FB "," Final bearing ") text=string.gsub(text," ops"," operations ") text=string.gsub(text," kts"," knots") text=string.gsub(text,"TACAN","Tackan") text=string.gsub(text,"ICLS","I.C.L.S.") text=string.gsub(text,"LSO","L.S.O.") text=string.gsub(text,"inHg","inches of Mercury") text=string.gsub(text,"QFE","Q.F.E.") text=string.gsub(text,"hPa","hecto pascal") text=string.gsub(text," NM"," nautical miles") text=string.gsub(text," ft"," feet") text=string.gsub(text,"A/C","aircraft") text=string.gsub(text,"(#[%a%d%p%s]+)\n","") text=string.gsub(text,"%.000"," dot zero") text=string.gsub(text,"00"," double zero") text=string.gsub(text," 0 "," zero ") text=string.gsub(text,"\n","; ") return text end function AIRBOSS:MessageToPlayer(playerData,message,sender,receiver,duration,clear,delay) self:T({sender,receiver,message}) 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 if not self.SRS then 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 else local frequency=self.MarshalRadio.frequency local modulation=self.MarshalRadio.modulation local voice=self.MarshalRadio.voice local gender=self.MarshalRadio.gender local culture=self.MarshalRadio.culture if not sender then sender="AIRBOSS"end if string.find(sender,"AIRBOSS")then frequency=self.AirbossRadio.frequency modulation=self.AirbossRadio.modulation voice=self.AirbossRadio.voice gender=self.AirbossRadio.gender culture=self.AirbossRadio.culture end if sender=="LSO"then frequency=self.LSORadio.frequency modulation=self.LSORadio.modulation voice=self.LSORadio.voice gender=self.LSORadio.gender culture=self.LSORadio.culture end self:T(self.lid..text) self:T({sender,frequency,modulation,voice}) local srstext=self:_GetNiceSRSText(text) self.SRSQ:NewTransmission(srstext,duration,self.SRS,nil,0.1,nil,nil,nil,frequency,modulation,gender,culture,voice,nil,sender) 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(tostring(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 if Sender==""then self:E(self.lid..string.format("ERROR: Sender unknown!")) return end local numbers=_split(tostring(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:_MarshallInboundCall(unit,modex) local vectorCarrier=self:GetCoordinate():GetDirectionVec3(unit:GetCoordinate()) local bearing=UTILS.Round(unit:GetCoordinate():GetAngleDegrees(vectorCarrier),0) local distance=UTILS.Round(UTILS.MetersToNM(unit:GetCoordinate():Get2DDistance(self:GetCoordinate())),0) local angels=UTILS.Round(UTILS.MetersToFeet(unit:GetHeight()/1000),0) local state=UTILS.Round(self:_GetFuelState(unit)/1000,1) local text=string.format("Marshal, %s, marking mom's %d for %d, angels %d, state %.1f",modex,bearing,distance,angels,state) self:T(self.lid..text) local FS=UTILS.Split(string.format("%.1f",state),".") local inboundcall=self:_NewRadioCall(self.MarshalCall.CLICK,unit.UnitName:upper(),text,self.Tmessage,nil,unit.UnitName:upper()) self:RadioTransmission(self.MarshalRadio,inboundcall) self:RadioTransmission(self.MarshalRadio,self.PilotCall.MARSHAL,nil,nil,nil,nil,true) self:_Number2Radio(self.MarshalRadio,modex,nil,nil,true) self:RadioTransmission(self.MarshalRadio,self.PilotCall.MARKINGMOMS,nil,nil,nil,nil,true) self:_Number2Radio(self.MarshalRadio,tostring(bearing),nil,nil,true) self:RadioTransmission(self.MarshalRadio,self.PilotCall.FOR,nil,nil,nil,nil,true) self:_Number2Radio(self.MarshalRadio,tostring(distance),nil,nil,true) self:RadioTransmission(self.MarshalRadio,self.PilotCall.ANGELS,nil,nil,nil,nil,true) self:_Number2Radio(self.MarshalRadio,tostring(angels),nil,nil,true) self:RadioTransmission(self.MarshalRadio,self.PilotCall.STATE,nil,nil,nil,nil,true) self:_Number2Radio(self.MarshalRadio,FS[1],nil,nil,true) self:RadioTransmission(self.MarshalRadio,self.PilotCall.POINT,nil,nil,nil,nil,true) self:_Number2Radio(self.MarshalRadio,FS[2],nil,nil,true) self:RadioTransmission(self.MarshalRadio,self.MarshalRadio.CLICK,nil,nil,nil,nil,true) end function AIRBOSS:_CommencingCall(unit,modex) local text=string.format("%s, commencing",modex) self:T(self.lid..text) local commencingCall=self:_NewRadioCall(self.MarshalCall.CLICK,unit.UnitName:upper(),text,self.Tmessage,nil,unit.UnitName:upper()) self:RadioTransmission(self.MarshalRadio,commencingCall) self:_Number2Radio(self.MarshalRadio,modex,nil,nil,true) self:RadioTransmission(self.MarshalRadio,self.PilotCall.COMMENCING,nil,nil,nil,nil,true) self:RadioTransmission(self.MarshalRadio,self.MarshalRadio.CLICK,nil,nil,nil,nil,true) end function AIRBOSS:_LSOCallAircraftBall(modex,nickname,fuelstate) local text=string.format("%s Ball, %.1f.",nickname,fuelstate) self:T(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:T(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:T(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:T(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:T(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:T(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:T(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:T(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:T(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(" BRC %03d°.",self:GetBRC()) elseif case==2 then text=text..string.format(" Marshal radial %03d°. BRC %03d°.",radial,self:GetBRC()) elseif case==3 then text=text..string.format(" Marshal radial %03d°. Final heading %03d°.",radial,self:GetFinalBearing(false)) 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:T(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 if self.xtVoiceOvers then self:_MarshallInboundCall(_unit,playerData.onboard) end 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,"AIRBOSS","") 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 if self.xtVoiceOvers then self:_CommencingCall(_unit,playerData.onboard) end 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.." (currently turning)" 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 turn 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.INVINCIBLE or self.carriertype==AIRBOSS.CarrierType.HERMES or self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS or self.carriertype==AIRBOSS.CarrierType.CANBERRA 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 turn 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 function AIRBOSS:onafterLSOGrade(From,Event,To,playerData,grade) if self.funkmanSocket then local trapsheet={};trapsheet.X={};trapsheet.Z={};trapsheet.AoA={};trapsheet.Alt={} for i=1,#playerData.trapsheet do local ts=playerData.trapsheet[i] table.insert(trapsheet.X,UTILS.Round(ts.X,1)) table.insert(trapsheet.Z,UTILS.Round(ts.Z,1)) table.insert(trapsheet.AoA,UTILS.Round(ts.AoA,2)) table.insert(trapsheet.Alt,UTILS.Round(ts.Alt,1)) end local result={} result.command=SOCKET.DataType.LSOGRADE result.name=playerData.name result.trapsheet=trapsheet result.airframe=grade.airframe result.mitime=grade.mitime result.midate=grade.midate result.wind=grade.wind result.carriertype=grade.carriertype result.carriername=grade.carriername result.carrierrwy=grade.carrierrwy result.landingdist=self.carrierparam.landingdist result.theatre=grade.theatre result.case=playerData.case result.Tgroove=grade.Tgroove result.wire=grade.wire result.grade=grade.grade result.points=grade.points result.details=grade.details self:T(self.lid.."Result onafterLSOGrade") self:T(result) self.funkmanSocket:SendTable(result) end 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, unlimitedfuel=false, } _RECOVERYTANKERID=0 RECOVERYTANKER.version="1.0.10" 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:SetUnlimitedFuel(OnOff) self.unlimitedfuel=OnOff 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,mode) self.TACANchannel=channel or 1 self.TACANmode=mode or"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) if self.unlimitedfuel then Spawn:OnSpawnGroup( function(grp) grp:CommandSetUnlimitedFuel(self.unlimitedfuel) end ) end 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) return self 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:T(self.lid..text) local Vec3=EventData.IniDCSUnit:getPoint() local coord=COORDINATE:NewFromVec3(Vec3) 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) return self 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 fuelheight+25 then dust=true visibility=math.min(visibility,dustdens) end else local fvis=world.weather.getFogVisibilityDistance() local fheight=world.weather.getFogThickness() if fvis>0 and fheight>height+25 then fog=true visibility=math.min(visibility,fvis) end end local VISIBILITY="" if self.metric then local reportedviz=UTILS.Round(visibility/1000) if reportedviz>10 then reportedviz=10 end VISIBILITY=string.format("%d",reportedviz) else local reportedviz=UTILS.Round(UTILS.MetersToSM(visibility)) 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("RainyPreset1")then clouddens=9 if temperature>5 then precepitation=1 else precepitation=3 end elseif cloudspreset:find("RainyPreset2")then clouddens=9 if temperature>5 then precepitation=1 else precepitation=3 end elseif cloudspreset:find("RainyPreset3")then clouddens=9 if temperature>5 then precepitation=1 else precepitation=3 end elseif cloudspreset:find("RainyPreset4")then clouddens=5 if temperature>5 then precepitation=1 else precepitation=3 end elseif cloudspreset:find("RainyPreset5")then clouddens=5 if temperature>5 then precepitation=1 else precepitation=3 end elseif cloudspreset:find("RainyPreset6")then clouddens=5 if temperature>5 then precepitation=1 else precepitation=3 end elseif cloudspreset:find("NEWRAINPRESET4")then clouddens=5 if temperature>5 then precepitation=1 else precepitation=3 end elseif cloudspreset:find("RainyPreset")then clouddens=9 if temperature>5 then precepitation=1 else precepitation=3 end elseif 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 else self:E(string.format("WARNING! Unknown weather preset: %s",tostring(cloudspreset))) 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=self.Sound.CloudsNotAvailable local CLOUDSsub=self.gettext:GetEntry("NOCLOUDINFO",self.locale) if static then if clouddens>=9 then CloudCover=self.Sound.CloudsOvercast CLOUDSsub=self.gettext:GetEntry("OVERCAST",self.locale) elseif clouddens>=7 then CloudCover=self.Sound.CloudsBroken CLOUDSsub=self.gettext:GetEntry("BROKEN",self.locale) elseif clouddens>=4 then CloudCover=self.Sound.CloudsScattered CLOUDSsub=self.gettext:GetEntry("SCATTERED",self.locale) elseif clouddens>=1 then CloudCover=self.Sound.CloudsFew CLOUDSsub=self.gettext:GetEntry("FEWCLOUDS",self.locale) else CLOUDBASE=nil CLOUDCEIL=nil CloudCover=self.Sound.CloudsNo CLOUDSsub=self.gettext:GetEntry("NOCLOUDS",self.locale) end end local subtitle="" subtitle=string.format("%s",self.airbasename) if(not self.ATISforFARPs)and self.airbasename:find("AFB")==nil and self.airbasename:find("Airport")==nil and self.airbasename:find("Airstrip")==nil and self.airbasename:find("airfield")==nil and self.airbasename:find("AB")==nil and self.airbasename:find("Field")==nil then subtitle=subtitle.." "..self.gettext:GetEntry("AIRPORT",self.locale) end if not self.useSRS then self.radioqueue:NewTransmission(string.format("%s.ogg",self.airbasename),3.0,self.soundpathAirports,nil,nil,subtitle,self.subduration) end local alltext=subtitle local information=self.gettext:GetEntry("INFORMATION",self.locale) subtitle=string.format("%s %s",information,NATO) local _INFORMATION=subtitle if not self.useSRS then self:Transmission(self.Sound.Information,0.5,subtitle) self.radioqueue:NewTransmission(string.format("%s.ogg",NATO),0.75,self.soundpathNato) end alltext=alltext..";\n"..subtitle subtitle=string.format("%s Zulu",ZULU) if not self.useSRS then self.radioqueue:Number2Transmission(ZULU,nil,0.5) self:Transmission(self.Sound.Zulu,0.2,subtitle) end alltext=alltext..";\n"..subtitle if not self.zulutimeonly then local sunrise=self.gettext:GetEntry("SUNRISEAT",self.locale) subtitle=string.format(sunrise,SUNRISE) if not self.useSRS and NorthPolar==false then self:Transmission(self.Sound.SunriseAt,0.5,subtitle) self.radioqueue:Number2Transmission(SUNRISE,nil,0.2) self:Transmission(self.Sound.TimeLocal,0.2) end alltext=alltext..";\n"..subtitle local sunset=self.gettext:GetEntry("SUNSETAT",self.locale) subtitle=string.format(sunset,SUNSET) if not self.useSRS and NorthPolar==false then self:Transmission(self.Sound.SunsetAt,0.5,subtitle) self.radioqueue:Number2Transmission(SUNSET,nil,0.5) self:Transmission(self.Sound.TimeLocal,0.2) end alltext=alltext..";\n"..subtitle end if self.useSRS then WINDFROM=string.gsub(WINDFROM,".","%1 ") end if self.metric then local windfrom=self.gettext:GetEntry("WINDFROMMS",self.locale) subtitle=string.format(windfrom,WINDFROM,WINDSPEED) else local windfrom=self.gettext:GetEntry("WINDFROMKNOTS",self.locale) subtitle=string.format(windfrom,WINDFROM,WINDSPEED) end if turbulence>0 then subtitle=subtitle..", "..self.gettext:GetEntry("GUSTING",self.locale) end local _WIND=subtitle if not self.useSRS then self:Transmission(self.Sound.WindFrom,1.0,subtitle) self.radioqueue:Number2Transmission(WINDFROM) self:Transmission(self.Sound.At,0.2) self.radioqueue:Number2Transmission(WINDSPEED) if self.metric then self:Transmission(self.Sound.MetersPerSecond,0.2) else self:Transmission(self.Sound.Knots,0.2) end if turbulence>0 then self:Transmission(self.Sound.Gusting,0.2) end end alltext=alltext..";\n"..subtitle if self.metric then local visi=self.gettext:GetEntry("VISIKM",self.locale) subtitle=string.format(visi,VISIBILITY) else local visi=self.gettext:GetEntry("VISISM",self.locale) subtitle=string.format(visi,VISIBILITY) end if not self.useSRS then self:Transmission(self.Sound.Visibilty,1.0,subtitle) self.radioqueue:Number2Transmission(VISIBILITY) if self.metric then self:Transmission(self.Sound.Kilometers,0.2) else self:Transmission(self.Sound.StatuteMiles,0.2) end end alltext=alltext..";\n"..subtitle subtitle="" local wp=false local wpsub="" if precepitation==1 then wp=true wpsub=wpsub.." "..self.gettext:GetEntry("RAIN",self.locale) elseif precepitation==2 then if wp then wpsub=wpsub.."," end wpsub=wpsub.." "..self.gettext:GetEntry("TSTORM",self.locale) wp=true elseif precepitation==3 then wpsub=wpsub.." "..self.gettext:GetEntry("SNOW",self.locale) wp=true elseif precepitation==4 then wpsub=wpsub.." "..self.gettext:GetEntry("SSTROM",self.locale) wp=true end if fog then if wp then wpsub=wpsub.."," end wpsub=wpsub.." "..self.gettext:GetEntry("FOG",self.locale) wp=true end if dust then if wp then wpsub=wpsub.."," end wpsub=wpsub.." "..self.gettext:GetEntry("DUST",self.locale) wp=true end if wp then local phenos=self.gettext:GetEntry("PHENOMENA",self.locale) subtitle=string.format("%s: %s",phenos,wpsub) if not self.useSRS then self:Transmission(self.Sound.WeatherPhenomena,1.0,subtitle) if precepitation==1 then self:Transmission(self.Sound.Rain,0.5) elseif precepitation==2 then self:Transmission(self.Sound.ThunderStorm,0.5) elseif precepitation==3 then self:Transmission(self.Sound.Snow,0.5) elseif precepitation==4 then self:Transmission(self.Sound.SnowStorm,0.5) end if fog then self:Transmission(self.Sound.Fog,0.5) end if dust then self:Transmission(self.Sound.Dust,0.5) end end alltext=alltext..";\n"..subtitle end if not self.useSRS then self:Transmission(CloudCover,1.0,CLOUDSsub) end if CLOUDBASE and static then local cbase=tostring(tonumber(CLOUDBASE1000)*1000+tonumber(CLOUDBASE0100)*100) local cceil=tostring(tonumber(CLOUDCEIL1000)*1000+tonumber(CLOUDCEIL0100)*100) if self.metric then local cloudbase=self.gettext:GetEntry("CLOUDBASEM",self.locale) subtitle=string.format(cloudbase,cbase,cceil) else local cloudbase=self.gettext:GetEntry("CLOUDBASEFT",self.locale) subtitle=string.format(cloudbase,cbase,cceil) end if not self.useSRS then self:Transmission(self.Sound.CloudBase,1.0,subtitle) if tonumber(CLOUDBASE1000)>0 then self.radioqueue:Number2Transmission(CLOUDBASE1000) self:Transmission(self.Sound.Thousand,0.1) end if tonumber(CLOUDBASE0100)>0 then self.radioqueue:Number2Transmission(CLOUDBASE0100) self:Transmission(self.Sound.Hundred,0.1) end self:Transmission(self.Sound.CloudCeiling,0.5) if tonumber(CLOUDCEIL1000)>0 then self.radioqueue:Number2Transmission(CLOUDCEIL1000) self:Transmission(self.Sound.Thousand,0.1) end if tonumber(CLOUDCEIL0100)>0 then self.radioqueue:Number2Transmission(CLOUDCEIL0100) self:Transmission(self.Sound.Hundred,0.1) end if self.metric then self:Transmission(self.Sound.Meters,0.1) else self:Transmission(self.Sound.Feet,0.1) end end end alltext=alltext..";\n"..subtitle subtitle="" local temptext=self.gettext:GetEntry("TEMPERATURE",self.locale) if self.TDegF then if temperature<0 then subtitle=string.format("%s -%s °F",temptext,TEMPERATURE) else subtitle=string.format("%s %s °F",temptext,TEMPERATURE) end else if temperature<0 then subtitle=string.format("%s -%s °C",temptext,TEMPERATURE) else subtitle=string.format("%s %s °C",temptext,TEMPERATURE) end end local _TEMPERATURE=subtitle if not self.useSRS then self:Transmission(self.Sound.Temperature,1.0,subtitle) if temperature<0 then self:Transmission(self.Sound.Minus,0.2) end self.radioqueue:Number2Transmission(TEMPERATURE) if self.TDegF then self:Transmission(self.Sound.DegreesFahrenheit,0.2) else self:Transmission(self.Sound.DegreesCelsius,0.2) end end alltext=alltext..";\n"..subtitle local dewtext=self.gettext:GetEntry("DEWPOINT",self.locale) if self.TDegF then if dewpoint<0 then subtitle=string.format("%s -%s °F",dewtext,DEWPOINT) else subtitle=string.format("%s %s °F",dewtext,DEWPOINT) end else if dewpoint<0 then subtitle=string.format("%s -%s °C",dewtext,DEWPOINT) else subtitle=string.format("%s %s °C",dewtext,DEWPOINT) end end local _DEWPOINT=subtitle if not self.useSRS then self:Transmission(self.Sound.DewPoint,1.0,subtitle) if dewpoint<0 then self:Transmission(self.Sound.Minus,0.2) end self.radioqueue:Number2Transmission(DEWPOINT) if self.TDegF then self:Transmission(self.Sound.DegreesFahrenheit,0.2) else self:Transmission(self.Sound.DegreesCelsius,0.2) end end alltext=alltext..";\n"..subtitle local altim=self.gettext:GetEntry("ALTIMETER",self.locale) if self.PmmHg then if self.qnhonly then subtitle=string.format("%s %s.%s mmHg",altim,QNH[1],QNH[2]) else subtitle=string.format("%s: QNH %s.%s, QFE %s.%s mmHg",altim,QNH[1],QNH[2],QFE[1],QFE[2]) end else if self.metric then if self.qnhonly then subtitle=string.format("%s %s.%s hPa",altim,QNH[1],QNH[2]) else subtitle=string.format("%s: QNH %s.%s, QFE %s.%s hPa",altim,QNH[1],QNH[2],QFE[1],QFE[2]) end else if self.qnhonly then subtitle=string.format("%s %s.%s inHg",altim,QNH[1],QNH[2]) else subtitle=string.format("%s: QNH %s.%s, QFE %s.%s inHg",altim,QNH[1],QNH[2],QFE[1],QFE[2]) end end end if self.ReportmBar and not self.metric then if self.qnhonly then subtitle=string.format("%s;\n%s %d hPa",subtitle,altim,mBarqnh) else subtitle=string.format("%s;\n%s: QNH %d, QFE %d hPa",subtitle,altim,mBarqnh,mBarqfe) end end local _ALTIMETER=subtitle if not self.useSRS then self:Transmission(self.Sound.Altimeter,1.0,subtitle) if not self.qnhonly then self:Transmission(self.Sound.QNH,0.5) end self.radioqueue:Number2Transmission(QNH[1]) if ATIS.ICAOPhraseology[UTILS.GetDCSMap()]then self:Transmission(self.Sound.Decimal,0.2) end self.radioqueue:Number2Transmission(QNH[2]) if not self.qnhonly then self:Transmission(self.Sound.QFE,0.75) self.radioqueue:Number2Transmission(QFE[1]) if ATIS.ICAOPhraseology[UTILS.GetDCSMap()]then self:Transmission(self.Sound.Decimal,0.2) end self.radioqueue:Number2Transmission(QFE[2]) end if self.PmmHg then self:Transmission(self.Sound.MillimetersOfMercury,0.1) else if self.metric then self:Transmission(self.Sound.HectoPascal,0.1) else self:Transmission(self.Sound.InchesOfMercury,0.1) end end end alltext=alltext..";\n"..subtitle local _RUNACT if not self.ATISforFARPs then local subtitle="" if runwayLanding and runwayLanding~=runwayTakeoff then local actrun=self.gettext:GetEntry("ACTIVELANDING",self.locale) subtitle=string.format("%s %s",actrun,runwayLanding) if rwyLandingLeft==true then subtitle=subtitle.." "..self.gettext:GetEntry("LEFT",self.locale) elseif rwyLandingLeft==false then subtitle=subtitle.." "..self.gettext:GetEntry("RIGHT",self.locale) end alltext=alltext..";\n"..subtitle if not self.useSRS then self:Transmission(self.Sound.ActiveRunwayArrival,1.0,subtitle) self.radioqueue:Number2Transmission(runwayLanding) if rwyLandingLeft==true then self:Transmission(self.Sound.Left,0.2) elseif rwyLandingLeft==false then self:Transmission(self.Sound.Right,0.2) end end end if runwayTakeoff then local actrun=self.gettext:GetEntry("ACTIVERUN",self.locale) subtitle=string.format("%s %s",actrun,runwayTakeoff) if rwyTakeoffLeft==true then subtitle=subtitle.." "..self.gettext:GetEntry("LEFT",self.locale) elseif rwyTakeoffLeft==false then subtitle=subtitle.." "..self.gettext:GetEntry("RIGHT",self.locale) end alltext=alltext..";\n"..subtitle if not self.useSRS then self:Transmission(self.Sound.ActiveRunwayDeparture,1.0,subtitle) self.radioqueue:Number2Transmission(runwayTakeoff) if rwyTakeoffLeft==true then self:Transmission(self.Sound.Left,0.2) elseif rwyTakeoffLeft==false then self:Transmission(self.Sound.Right,0.2) end end end _RUNACT=subtitle 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 rwyl=self.gettext:GetEntry("RWYLENGTH",self.locale) local meters=self.gettext:GetEntry("METERS",self.locale) local feet=self.gettext:GetEntry("FEET",self.locale) local subtitle=string.format("%s %d",rwyl,length) if self.metric then subtitle=subtitle.." "..meters else subtitle=subtitle.." "..feet end if not self.useSRS then self:Transmission(self.Sound.RunwayLength,1.0,subtitle) if tonumber(L1000)>0 then self.radioqueue:Number2Transmission(L1000) self:Transmission(self.Sound.Thousand,0.1) end if tonumber(L0100)>0 then self.radioqueue:Number2Transmission(L0100) self:Transmission(self.Sound.Hundred,0.1) end if self.metric then self:Transmission(self.Sound.Meters,0.1) else self:Transmission(self.Sound.Feet,0.1) end end alltext=alltext..";\n"..subtitle end end if self.elevation then local elev=self.gettext:GetEntry("ELEVATION",self.locale) local meters=self.gettext:GetEntry("METERS",self.locale) local feet=self.gettext:GetEntry("FEET",self.locale) 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("%s %d",elev,elevation) if self.metric then subtitle=subtitle.." "..meters else subtitle=subtitle.." "..feet end if not self.useSRS then self:Transmission(self.Sound.Elevation,1.0,subtitle) if tonumber(L1000)>0 then self.radioqueue:Number2Transmission(L1000) self:Transmission(self.Sound.Thousand,0.1) end if tonumber(L0100)>0 then self.radioqueue:Number2Transmission(L0100) self:Transmission(self.Sound.Hundred,0.1) end if self.metric then self:Transmission(self.Sound.Meters,0.1) else self:Transmission(self.Sound.Feet,0.1) end 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 local twrfrq=self.gettext:GetEntry("TOWERFREQ",self.locale) subtitle=string.format("%s %s",twrfrq,freqs) if not self.useSRS then self:Transmission(self.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(self.Sound.Decimal,0.2) self.radioqueue:Number2Transmission(f[2]) end self:Transmission(self.Sound.MegaHertz,0.2) end end alltext=alltext..";\n"..subtitle end local ils=self:GetNavPoint(self.ils,runwayLanding,rwyLandingLeft) if ils then local ilstxt=self.gettext:GetEntry("ILSFREQ",self.locale) subtitle=string.format("%s %.2f MHz",ilstxt,ils.frequency) if not self.useSRS then self:Transmission(self.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(self.Sound.Decimal,0.2) self.radioqueue:Number2Transmission(f[2]) end self:Transmission(self.Sound.MegaHertz,0.2) end alltext=alltext..";\n"..subtitle end local ndb=self:GetNavPoint(self.ndbouter,runwayLanding,rwyLandingLeft) if ndb then local ndbtxt=self.gettext:GetEntry("OUTERNDB",self.locale) subtitle=string.format("%s %.2f MHz",ndbtxt,ndb.frequency) if not self.useSRS then self:Transmission(self.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(self.Sound.Decimal,0.2) self.radioqueue:Number2Transmission(f[2]) end self:Transmission(self.Sound.MegaHertz,0.2) end alltext=alltext..";\n"..subtitle end local ndb=self:GetNavPoint(self.ndbinner,runwayLanding,rwyLandingLeft) if ndb then local ndbtxt=self.gettext:GetEntry("INNERNDB",self.locale) subtitle=string.format("%s %.2f MHz",ndbtxt,ndb.frequency) if not self.useSRS then self:Transmission(self.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(self.Sound.Decimal,0.2) self.radioqueue:Number2Transmission(f[2]) end self:Transmission(self.Sound.MegaHertz,0.2) end alltext=alltext..";\n"..subtitle end if self.vor then local vortxt=self.gettext:GetEntry("VORFREQ",self.locale) local vorttstxt=self.gettext:GetEntry("VORFREQTTS",self.locale) subtitle=string.format("%s %.2f MHz",vortxt,self.vor) if self.useSRS then subtitle=string.format("%s %.2f MHz",vorttstxt,self.vor) end if not self.useSRS then self:Transmission(self.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(self.Sound.Decimal,0.2) self.radioqueue:Number2Transmission(f[2]) end self:Transmission(self.Sound.MegaHertz,0.2) end alltext=alltext..";\n"..subtitle end if self.tacan then local tactxt=self.gettext:GetEntry("TACANCH",self.locale) subtitle=string.format(tactxt,self.tacan) if not self.useSRS then self:Transmission(self.Sound.TACANChannel,1.0,subtitle) self.radioqueue:Number2Transmission(tostring(self.tacan),nil,0.2) self.radioqueue:NewTransmission("Xray.ogg",0.75,self.soundpathNato,nil,0.2) end alltext=alltext..";\n"..subtitle end if self.rsbn then local rsbntxt=self.gettext:GetEntry("RSBNCH",self.locale) subtitle=string.format("%s %d",rsbntxt,self.rsbn) if not self.useSRS then self:Transmission(self.Sound.RSBNChannel,1.0,subtitle) self.radioqueue:Number2Transmission(tostring(self.rsbn),nil,0.2) end alltext=alltext..";\n"..subtitle end local ndb=self:GetNavPoint(self.prmg,runwayLanding,rwyLandingLeft) if ndb then local prmtxt=self.gettext:GetEntry("PRMGCH",self.locale) subtitle=string.format("%s %d",prmtxt,ndb.frequency) if not self.useSRS then self:Transmission(self.Sound.PRMGChannel,1.0,subtitle) self.radioqueue:Number2Transmission(tostring(ndb.frequency),nil,0.5) end alltext=alltext..";\n"..subtitle end if self.useSRS and self.AdditionalInformation then alltext=alltext..";\n"..self.AdditionalInformation end local advtxt=self.gettext:GetEntry("ADVISE",self.locale) subtitle=string.format("%s %s",advtxt,NATO) if not self.useSRS then self:Transmission(self.Sound.AdviceOnInitial,0.5,subtitle) self.radioqueue:NewTransmission(string.format("%s.ogg",NATO),0.75,self.soundpathNato) end 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({From,Event,To}) self:T(self.lid..string.format("Report:\n%s",Text)) if self.useSRS and self.msrs then local text=string.gsub(Text,"[\r\n]","") local statute=self.gettext:GetEntry("STATUTE",self.locale) local degc=self.gettext:GetEntry("DEGREES",self.locale) local degf=self.gettext:GetEntry("FAHRENHEIT",self.locale) local inhg=self.gettext:GetEntry("INCHHG",self.locale) local mmhg=self.gettext:GetEntry("MMHG",self.locale) local hpa=self.gettext:GetEntry("HECTO",self.locale) local emes=self.gettext:GetEntry("METERSPER",self.locale) local tacan=self.gettext:GetEntry("TACAN",self.locale) local farp=self.gettext:GetEntry("FARP",self.locale) local text=string.gsub(text,"SM",statute) text=string.gsub(text,"°C",degc) text=string.gsub(text,"°F",degf) text=string.gsub(text,"inHg",inhg) text=string.gsub(text,"mmHg",mmhg) text=string.gsub(text,"hPa",hpa) text=string.gsub(text,"m/s",emes) text=string.gsub(text,"TACAN",tacan) text=string.gsub(text,"FARP",farp) local delimiter=self.gettext:GetEntry("DELIMITER",self.locale) if string.lower(self.locale)~="en"then text=string.gsub(text,"(%d+)(%.)(%d+)","%1 "..delimiter.." %3") end local text=string.gsub(text,";"," . ") self:T("SRS TTS: "..text) local duration=MSRS.getSpeechTime(text,0.95) self.msrsQ:NewTransmission(text,duration,self.msrs,nil,2) self.SRSText=text end end function ATIS:OnEventBaseCaptured(EventData) if EventData and EventData.Place then local airbase=EventData.Place if EventData.PlaceName==self.airbasename then local NewCoalitionAirbase=airbase:GetCoalition() if self.useSRS and self.msrs and self.msrs.coalition~=NewCoalitionAirbase then self.msrs:SetCoalition(NewCoalitionAirbase) end end end end function ATIS:UpdateMarker(information,runact,wind,altimeter,temperature) if self.markerid then self.airbase:GetCoordinate():RemoveMark(self.markerid) end local text="" if type(self.frequency)=="table"then local frequency=table.concat(self.frequency,"/") local modulation=self.modulation if type(modulation)=="table"then modulation=table.concat(self.modulation,"/") end text=string.format("ATIS on %s %s, %s:\n",tostring(frequency),tostring(modulation),tostring(information)) else text=string.format("ATIS on %.3f %s, %s:\n",self.frequency,UTILS.GetModulationName(self.modulation),tostring(information)) end 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(Takeoff) local runway=nil if Takeoff then runway=self.airbase:GetActiveRunwayTakeoff() else runway=self.airbase:GetActiveRunwayLanding() end if runway then return runway.name,runway.isLeft else return nil,nil end 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 diff0 then for _,_cargo in pairs(crates)do local cgotype=_cargo:GetType() if _cargo:WasDropped()and cgotype~=CTLD_CARGO.Enum.STATIC then local ok=false local chalk=_cargo:GetMark() if chalk==nil then ok=true else local tag=chalk.tag or"none" local timestamp=chalk.timestamp or 0 local gone=timer.getAbsTime()-timestamp if gone>=self.marktimer then ok=true _cargo:WipeMark() end end if ok then local chalk={} chalk.tag="Engineers" chalk.timestamp=timer.getAbsTime() _cargo:AddMark(chalk) ind=ind+1 table.insert(ctable,ind,_cargo) end end end end if ind>0 then local crate=ctable[1] local static=crate:GetPositionable() local crate_pos=static:GetCoordinate() local gpos=group:GetCoordinate() local distance=self:_GetDistance(gpos,crate_pos) self:T(string.format("%s Distance to crate: %d",self.lid,distance)) if distance>30 and distance~=-1 and self:IsStatus("Searching")then group:RouteGroundTo(crate_pos,15,"Line abreast",1) self.currwpt=crate_pos self:Move() elseif distance<=30 and distance~=-1 then self:Arrive() end else self:T(self.lid.."No crates in reach!") end return self end function CTLD_ENGINEERING:Move() self:T(self.lid.."Move") self:SetStatus("Moving") local group=self.Group local tgtpos=self.currwpt local gpos=group:GetCoordinate() local distance=self:_GetDistance(gpos,tgtpos) self:T(string.format("%s Distance remaining: %d",self.lid,distance)) if distance<=30 and distance~=-1 then self:Arrive() end return self end function CTLD_ENGINEERING:Arrive() self:T(self.lid.."Arrive") self:SetStatus("Arrived") self.currwpt=nil local Grp=self.Group Grp:RouteStop() return self end function CTLD_ENGINEERING:_GetDistance(_point1,_point2) self:T(self.lid.." _GetDistance") if _point1 and _point2 then local distance1=_point1:Get2DDistance(_point2) local distance2=_point1:DistanceFromPointVec2(_point2) if distance1 and type(distance1)=="number"then return distance1 elseif distance2 and type(distance2)=="number"then return distance2 else self:E("*****Cannot calculate distance!") self:E({_point1,_point2}) return-1 end else self:E("******Cannot calculate distance!") self:E({_point1,_point2}) return-1 end end end do CTLD={ ClassName="CTLD", verbose=0, lid="", coalition=1, coalitiontxt="blue", PilotGroups={}, CtldUnits={}, FreeVHFFrequencies={}, FreeUHFFrequencies={}, FreeFMFrequencies={}, CargoCounter=0, Cargo_Troops={}, Cargo_Crates={}, Loaded_Cargo={}, Spawned_Crates={}, Spawned_Cargo={}, CrateDistance=35, PackDistance=35, debug=false, wpZones={}, dropOffZones={}, pickupZones={}, DynamicCargo={}, ChinookTroopCircleRadius=5, TroopUnloadDistGround=5, TroopUnloadDistGroundHerc=25, TroopUnloadDistGroundHook=15, TroopUnloadDistHoverHook=5, TroopUnloadDistHover=1.5, UserSetGroup=nil, LoadedGroupsTable={}, keeploadtable=true, } CTLD.RadioModulation={ AM=0, FM=1, } CTLD.CargoZoneType={ LOAD="load", DROP="drop", MOVE="move", SHIP="ship", BEACON="beacon", } CTLD.UnitTypeCapabilities={ ["SA342Mistral"]={type="SA342Mistral",crates=false,troops=true,cratelimit=0,trooplimit=4,length=12,cargoweightlimit=400}, ["SA342L"]={type="SA342L",crates=false,troops=true,cratelimit=0,trooplimit=2,length=12,cargoweightlimit=400}, ["SA342M"]={type="SA342M",crates=false,troops=true,cratelimit=0,trooplimit=4,length=12,cargoweightlimit=400}, ["SA342Minigun"]={type="SA342Minigun",crates=false,troops=true,cratelimit=0,trooplimit=2,length=12,cargoweightlimit=400}, ["UH-1H"]={type="UH-1H",crates=true,troops=true,cratelimit=1,trooplimit=8,length=15,cargoweightlimit=700}, ["Mi-8MTV2"]={type="Mi-8MTV2",crates=true,troops=true,cratelimit=2,trooplimit=12,length=15,cargoweightlimit=3000}, ["Mi-8MT"]={type="Mi-8MT",crates=true,troops=true,cratelimit=2,trooplimit=12,length=15,cargoweightlimit=3000}, ["Ka-50"]={type="Ka-50",crates=false,troops=false,cratelimit=0,trooplimit=0,length=15,cargoweightlimit=0}, ["Ka-50_3"]={type="Ka-50_3",crates=false,troops=false,cratelimit=0,trooplimit=0,length=15,cargoweightlimit=0}, ["Mi-24P"]={type="Mi-24P",crates=true,troops=true,cratelimit=2,trooplimit=8,length=18,cargoweightlimit=700}, ["Mi-24V"]={type="Mi-24V",crates=true,troops=true,cratelimit=2,trooplimit=8,length=18,cargoweightlimit=700}, ["Hercules"]={type="Hercules",crates=true,troops=true,cratelimit=7,trooplimit=64,length=25,cargoweightlimit=19000}, ["UH-60L"]={type="UH-60L",crates=true,troops=true,cratelimit=2,trooplimit=20,length=16,cargoweightlimit=3500}, ["MH-60R"]={type="MH-60R",crates=true,troops=true,cratelimit=2,trooplimit=20,length=16,cargoweightlimit=3500}, ["SH-60B"]={type="SH-60B",crates=true,troops=true,cratelimit=2,trooplimit=20,length=16,cargoweightlimit=3500}, ["AH-64D_BLK_II"]={type="AH-64D_BLK_II",crates=false,troops=true,cratelimit=0,trooplimit=2,length=17,cargoweightlimit=200}, ["Bronco-OV-10A"]={type="Bronco-OV-10A",crates=false,troops=true,cratelimit=0,trooplimit=5,length=13,cargoweightlimit=1450}, ["OH-6A"]={type="OH-6A",crates=false,troops=true,cratelimit=0,trooplimit=4,length=7,cargoweightlimit=550}, ["OH58D"]={type="OH58D",crates=false,troops=false,cratelimit=0,trooplimit=0,length=14,cargoweightlimit=400}, ["CH-47Fbl1"]={type="CH-47Fbl1",crates=true,troops=true,cratelimit=4,trooplimit=31,length=20,cargoweightlimit=10800}, ["MosquitoFBMkVI"]={type="MosquitoFBMkVI",crates=true,troops=false,cratelimit=2,trooplimit=0,length=13,cargoweightlimit=1800}, } CTLD.FixedWingTypes={ ["Hercules"]="Hercules", ["Bronco"]="Bronco", ["Mosquito"]="Mosquito", } CTLD.version="1.1.31" function CTLD:New(Coalition,Prefixes,Alias) local self=BASE:Inherit(self,FSM:New()) BASE:T({Coalition,Prefixes,Alias}) if Coalition and type(Coalition)=="string"then if Coalition=="blue"then self.coalition=coalition.side.BLUE self.coalitiontxt=Coalition elseif Coalition=="red"then self.coalition=coalition.side.RED self.coalitiontxt=Coalition elseif Coalition=="neutral"then self.coalition=coalition.side.NEUTRAL self.coalitiontxt=Coalition else self:E("ERROR: Unknown coalition in CTLD!") end else self.coalition=Coalition self.coalitiontxt=string.lower(UTILS.GetCoalitionName(self.coalition)) end if Alias then self.alias=tostring(Alias) else self.alias="UNHCR" if self.coalition then if self.coalition==coalition.side.RED then self.alias="Red CTLD" elseif self.coalition==coalition.side.BLUE then self.alias="Blue CTLD" end end end self.lid=string.format("%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("*","TroopsPickedUp","*") self:AddTransition("*","TroopsExtracted","*") self:AddTransition("*","CratesPickedUp","*") self:AddTransition("*","TroopsDeployed","*") self:AddTransition("*","TroopsRTB","*") self:AddTransition("*","CratesDropped","*") self:AddTransition("*","CratesBuild","*") self:AddTransition("*","CratesRepaired","*") self:AddTransition("*","CratesBuildStarted","*") self:AddTransition("*","CratesRepairStarted","*") self:AddTransition("*","HelicopterLost","*") self:AddTransition("*","Load","*") self:AddTransition("*","Loaded","*") self:AddTransition("*","Save","*") self:AddTransition("*","Stop","Stopped") self.PilotGroups={} self.CtldUnits={} self.FreeVHFFrequencies={} self.FreeUHFFrequencies={} self.FreeFMFrequencies={} self.UsedVHFFrequencies={} self.UsedUHFFrequencies={} self.UsedFMFrequencies={} self.RadioSound="beacon.ogg" self.RadioSoundFC3="beacon.ogg" self.RadioPath="l10n/DEFAULT/" self.pickupZones={} self.dropOffZones={} self.wpZones={} self.shipZones={} self.droppedBeacons={} self.droppedbeaconref={} self.droppedbeacontimeout=600 self.useprecisecoordloads=true self.Cargo_Crates={} self.Cargo_Troops={} self.Cargo_Statics={} self.Loaded_Cargo={} self.Spawned_Crates={} self.Spawned_Cargo={} self.MenusDone={} self.DroppedTroops={} self.DroppedCrates={} self.CargoCounter=0 self.CrateCounter=0 self.TroopCounter=0 self.Engineers=0 self.EngineersInField={} self.EngineerSearch=2000 self.nobuildmenu=false self.CrateDistance=35 self.PackDistance=35 self.ExtractFactor=3.33 self.prefixes=Prefixes or{"Cargoheli"} self.useprefix=true self.maximumHoverHeight=15 self.minimumHoverHeight=4 self.forcehoverload=true self.hoverautoloading=true self.dropcratesanywhere=false self.dropAsCargoCrate=false self.smokedistance=2000 self.movetroopstowpzone=true self.movetroopsdistance=5000 self.troopdropzoneradius=100 self.enableHercules=false self.enableFixedWing=false self.FixedMinAngels=165 self.FixedMaxAngels=2000 self.FixedMaxSpeed=77 self.suppressmessages=false self.repairtime=300 self.buildtime=300 self.placeCratesAhead=false self.cratecountry=country.id.GERMANY self.pilotmustopendoors=false if self.coalition==coalition.side.RED then self.cratecountry=country.id.RUSSIA end self.enableLoadSave=false self.filepath=nil self.saveinterval=600 self.eventoninject=true self.keeploadtable=true self.LoadedGroupsTable={} self.usesubcats=false self.subcats={} self.subcatsTroop={} self.showstockinmenuitems=false self.nobuildinloadzones=true self.movecratesbeforebuild=true self.surfacetypes={land.SurfaceType.LAND,land.SurfaceType.ROAD,land.SurfaceType.RUNWAY,land.SurfaceType.SHALLOW_WATER} self.enableChinookGCLoading=true self.ChinookTroopCircleRadius=5 self.UserSetGroup=nil local AliaS=string.gsub(self.alias," ","_") self.filename=string.format("CTLD_%s_Persist.csv",AliaS) self.allowcratepickupagain=true self.enableslingload=false self.basetype="container_cargo" self.SmokeColor=SMOKECOLOR.Red self.FlareColor=FLARECOLOR.Red for i=1,100 do math.random() end self:_GenerateVHFrequencies() self:_GenerateUHFrequencies() self:_GenerateFMFrequencies() return self end function CTLD:_GetUnitCapabilities(Unit) self:T(self.lid.." _GetUnitCapabilities") local _unit=Unit local unittype=_unit:GetTypeName() local capabilities=self.UnitTypeCapabilities[unittype] if not capabilities or capabilities=={}then capabilities={} capabilities.troops=false capabilities.crates=false capabilities.cratelimit=0 capabilities.trooplimit=0 capabilities.type="generic" capabilities.length=20 capabilities.cargoweightlimit=0 end return capabilities end function CTLD:_GenerateUHFrequencies() self:T(self.lid.." _GenerateUHFrequencies") self.FreeUHFFrequencies={} self.FreeUHFFrequencies=UTILS.GenerateUHFrequencies(243,320) return self end function CTLD:_GenerateFMFrequencies() self:T(self.lid.." _GenerateFMrequencies") self.FreeFMFrequencies={} self.FreeFMFrequencies=UTILS.GenerateFMFrequencies() return self end function CTLD:_GenerateVHFrequencies() self:T(self.lid.." _GenerateVHFrequencies") self.FreeVHFFrequencies={} self.UsedVHFFrequencies={} self.FreeVHFFrequencies=UTILS.GenerateVHFrequencies() return self end function CTLD:SetTroopDropZoneRadius(Radius) self:T(self.lid.." SetTroopDropZoneRadius") local tradius=Radius or 100 if tradius<25 then tradius=25 end self.troopdropzoneradius=tradius return self end function CTLD:AddPlayerTask(PlayerTask) self:T(self.lid.." AddPlayerTask") if not self.PlayerTaskQueue then self.PlayerTaskQueue=FIFO:New() end self.PlayerTaskQueue:Push(PlayerTask,PlayerTask.PlayerTaskNr) return self end function CTLD:_EventHandler(EventData) self:T(string.format("%s Event = %d",self.lid,EventData.id)) local event=EventData if event.id==EVENTS.PlayerEnterAircraft or event.id==EVENTS.PlayerEnterUnit then local _coalition=event.IniCoalition if _coalition~=self.coalition then return end local unitname=event.IniUnitName or"none" self.MenusDone[unitname]=nil local _unit=event.IniUnit local _group=event.IniGroup if _unit:IsHelicopter()or _group:IsHelicopter()then local unitname=event.IniUnitName or"none" self.Loaded_Cargo[unitname]=nil self:_RefreshF10Menus() end if self:IsFixedWing(_unit)and self.enableFixedWing then local unitname=event.IniUnitName or"none" self.Loaded_Cargo[unitname]=nil self:_RefreshF10Menus() end return elseif event.id==EVENTS.Land or event.id==EVENTS.Takeoff then local unitname=event.IniUnitName if self.CtldUnits[unitname]then local _group=event.IniGroup local _unit=event.IniUnit self:_RefreshLoadCratesMenu(_group,_unit) end elseif event.id==EVENTS.PlayerLeaveUnit or event.id==EVENTS.UnitLost then local unitname=event.IniUnitName or"none" if self.CtldUnits[unitname]then local lostcargo=UTILS.DeepCopy(self.Loaded_Cargo[unitname]or{}) self:__HelicopterLost(1,unitname,lostcargo) end self.CtldUnits[unitname]=nil self.Loaded_Cargo[unitname]=nil self.MenusDone[unitname]=nil elseif event.id==EVENTS.NewDynamicCargo then self:T(self.lid.."GC New Event "..event.IniDynamicCargoName) self.DynamicCargo[event.IniDynamicCargoName]=event.IniDynamicCargo elseif event.id==EVENTS.DynamicCargoLoaded then self:T(self.lid.."GC Loaded Event "..event.IniDynamicCargoName) local dcargo=event.IniDynamicCargo local client=CLIENT:FindByPlayerName(dcargo.Owner) if client and client:IsAlive()then local unitname=client:GetName()or"none" local loaded={} if self.Loaded_Cargo[unitname]then loaded=self.Loaded_Cargo[unitname] else loaded={} loaded.Troopsloaded=0 loaded.Cratesloaded=0 loaded.Cargo={} end loaded.Cratesloaded=loaded.Cratesloaded+1 table.insert(loaded.Cargo,dcargo) self.Loaded_Cargo[unitname]=nil self.Loaded_Cargo[unitname]=loaded local Group=client:GetGroup() self:_SendMessage(string.format("Crate %s loaded by ground crew!",event.IniDynamicCargoName),10,false,Group) self:__CratesPickedUp(1,Group,client,dcargo) end elseif event.id==EVENTS.DynamicCargoUnloaded then self:T(self.lid.."GC Unload Event "..event.IniDynamicCargoName) local dcargo=event.IniDynamicCargo local client=CLIENT:FindByPlayerName(dcargo.Owner) if client and client:IsAlive()then local unitname=client:GetName()or"none" local loaded={} if self.Loaded_Cargo[unitname]then loaded=self.Loaded_Cargo[unitname] loaded.Cratesloaded=loaded.Cratesloaded-1 if loaded.Cratesloaded<0 then loaded.Cratesloaded=0 end local Loaded={} for _,_item in pairs(loaded.Cargo or{})do self:T(self.lid.."UNLOAD checking: ".._item:GetName()) self:T(self.lid.."UNLOAD state: "..tostring(_item:WasDropped())) if _item and _item:GetType()==CTLD_CARGO.Enum.GCLOADABLE and event.IniDynamicCargoName and event.IniDynamicCargoName~=_item:GetName()and not _item:WasDropped()then table.insert(Loaded,_item) else table.insert(Loaded,_item) end end loaded.Cargo=nil loaded.Cargo=Loaded self.Loaded_Cargo[unitname]=nil self.Loaded_Cargo[unitname]=loaded else loaded={} loaded.Troopsloaded=0 loaded.Cratesloaded=0 loaded.Cargo={} self.Loaded_Cargo[unitname]=loaded end local Group=client:GetGroup() self:_SendMessage(string.format("Crate %s unloaded by ground crew!",event.IniDynamicCargoName),10,false,Group) self:__CratesDropped(1,Group,client,{dcargo}) end elseif event.id==EVENTS.DynamicCargoRemoved then self:T(self.lid.."GC Remove Event "..event.IniDynamicCargoName) self.DynamicCargo[event.IniDynamicCargoName]=nil end return self end function CTLD:_SendMessage(Text,Time,Clearscreen,Group) self:T(self.lid.." _SendMessage") if not self.suppressmessages then local m=MESSAGE:New(Text,Time,"CTLD",Clearscreen):ToGroup(Group) end return self end function CTLD:_FindTroopsCargoObject(Name) self:T(self.lid.." _FindTroopsCargoObject") local cargo=nil for _,_cargo in pairs(self.Cargo_Troops)do local cargo=_cargo if cargo.Name==Name then return cargo end end return nil end function CTLD:_FindCratesCargoObject(Name) self:T(self.lid.." _FindCratesCargoObject") local cargo=nil for _,_cargo in pairs(self.Cargo_Crates)do local cargo=_cargo if cargo.Name==Name then return cargo end end return nil end function CTLD:AddAllowedFixedWingType(typename) if type(typename)=="string"then self.FixedWingTypes[typename]=typename elseif typename and typename.ClassName and typename:IsInstanceOf("UNIT")then local TypeName=typename:GetTypeName()or"none" self.FixedWingTypes[TypeName]=TypeName else self:E(self.lid.."No valid typename or no UNIT handed!") end return self end function CTLD:PreloadTroops(Unit,Troopname) self:T(self.lid.." PreloadTroops") local name=Troopname or"Unknown" if Unit and Unit:IsAlive()then local cargo=self:_FindTroopsCargoObject(name) local group=Unit:GetGroup() if cargo then self:_LoadTroops(group,Unit,cargo,true) else self:E(self.lid.." Troops preload - Cargo Object "..name.." not found!") end end return self end function CTLD:_PreloadCrates(Group,Unit,Cargo,NumberOfCrates) local group=Group local unit=Unit local unitname=unit:GetName() local unittype=unit:GetTypeName() local capabilities=self:_GetUnitCapabilities(Unit) local cancrates=capabilities.crates local cratelimit=capabilities.cratelimit if not cancrates then self:_SendMessage("Sorry this chopper cannot carry crates!",10,false,Group) return self else local numberonboard=0 local massonboard=0 local loaded={} if self.Loaded_Cargo[unitname]then loaded=self.Loaded_Cargo[unitname] numberonboard=loaded.Cratesloaded or 0 massonboard=self:_GetUnitCargoMass(Unit) else loaded={} loaded.Troopsloaded=0 loaded.Cratesloaded=0 loaded.Cargo={} end local crate=Cargo local numbercrates=NumberOfCrates or crate:GetCratesNeeded() for i=1,numbercrates do loaded.Cratesloaded=loaded.Cratesloaded+1 crate:SetHasMoved(true) crate:SetWasDropped(false) table.insert(loaded.Cargo,crate) crate.Positionable=nil self:_SendMessage(string.format("Crate ID %d for %s loaded!",crate:GetID(),crate:GetName()),10,false,Group) self.Loaded_Cargo[unitname]=loaded self:_UpdateUnitCargoMass(Unit) end end return self end function CTLD:PreloadCrates(Unit,Cratesname,NumberOfCrates) self:T(self.lid.." PreloadCrates") local name=Cratesname or"Unknown" if Unit and Unit:IsAlive()then local cargo=self:_FindCratesCargoObject(name) local group=Unit:GetGroup() if cargo then self:_PreloadCrates(group,Unit,cargo,NumberOfCrates) else self:E(self.lid.." Crates preload - Cargo Object "..name.." not found!") end end return self end function CTLD:_LoadTroops(Group,Unit,Cargotype,Inject) self:T(self.lid.." _LoadTroops") local instock=Cargotype:GetStock() local cgoname=Cargotype:GetName() local cgotype=Cargotype:GetType() local cgonetmass=Cargotype:GetNetMass() local maxloadable=self:_GetMaxLoadableMass(Unit) if type(instock)=="number"and tonumber(instock)<=0 and tonumber(instock)~=-1 and not Inject then self:_SendMessage(string.format("Sorry, all %s are gone!",cgoname),10,false,Group) return self end local grounded=not self:IsUnitInAir(Unit) local hoverload=self:CanHoverLoad(Unit) local inzone,zonename,zone,distance=self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) if not inzone then inzone,zonename,zone,distance=self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP) end if not Inject then if not inzone then self:_SendMessage("You are not close enough to a logistics zone!",10,false,Group) if not self.debug then return self end elseif not grounded and not hoverload then self:_SendMessage("You need to land or hover in position to load!",10,false,Group) if not self.debug then return self end elseif self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName())then self:_SendMessage("You need to open the door(s) to load troops!",10,false,Group) if not self.debug then return self end end end local group=Group local unit=Unit local unitname=unit:GetName() local cargotype=Cargotype local cratename=cargotype:GetName() local unittype=unit:GetTypeName() local capabilities=self:_GetUnitCapabilities(Unit) local cantroops=capabilities.troops local trooplimit=capabilities.trooplimit local troopsize=cargotype:GetCratesNeeded() local numberonboard=0 local loaded={} if self.Loaded_Cargo[unitname]then loaded=self.Loaded_Cargo[unitname] numberonboard=loaded.Troopsloaded or 0 else loaded={} loaded.Troopsloaded=0 loaded.Cratesloaded=0 loaded.Cargo={} end if troopsize+numberonboard>trooplimit then self:_SendMessage("Sorry, we\'re crammed already!",10,false,Group) return elseif maxloadableself.EngineerSearch then self:_SendMessage("No unit close enough to repair!",10,false,Group) return nil,nil end local groupname=nearestGroup:GetName() local function matchstring(String,Table) local match=false String=string.gsub(String,"-"," ") if type(Table)=="table"then for _,_name in pairs(Table)do _name=string.gsub(_name,"-"," ") if string.find(String,_name)then match=true break end end else if type(String)=="string"then Table=string.gsub(Table,"-"," ") if string.find(String,Table)then match=true end end end return match end local Cargotype=nil for k,v in pairs(self.Cargo_Crates)do if matchstring(groupname,v.Templates)and matchstring(groupname,Repairtype)then Cargotype=v break end end if Cargotype==nil then return nil,nil else return nearestGroup,Cargotype end end function CTLD:_RepairObjectFromCrates(Group,Unit,Crates,Build,Number,Engineering) self:T(self.lid.." _RepairObjectFromCrates") local build=Build local Repairtype=build.Template local NearestGroup,CargoType=self:_FindRepairNearby(Group,Unit,Repairtype) if NearestGroup~=nil then if self.repairtime<2 then self.repairtime=30 end if not Engineering then self:_SendMessage(string.format("Repair started using %s taking %d secs",build.Name,self.repairtime),10,false,Group) end local name=CargoType:GetName() local required=CargoType:GetCratesNeeded() local template=CargoType:GetTemplates() local ctype=CargoType:GetType() local object={} object.Name=CargoType:GetName() object.Required=required object.Found=required object.Template=template object.CanBuild=true object.Type=ctype self:_CleanUpCrates(Crates,Build,Number) local desttimer=TIMER:New(function()NearestGroup:Destroy(false)end,self) desttimer:Start(self.repairtime-1) local buildtimer=TIMER:New(self._BuildObjectFromCrates,self,Group,Unit,object,true,NearestGroup:GetCoordinate()) buildtimer:Start(self.repairtime) self:__CratesRepairStarted(1,Group,Unit) else if not Engineering then self:_SendMessage("Can't repair this unit with "..build.Name,10,false,Group) else self:T("Can't repair this unit with "..build.Name) end end return self end function CTLD:_ExtractTroops(Group,Unit) self:T(self.lid.." _ExtractTroops") local grounded=not self:IsUnitInAir(Unit) local hoverload=self:CanHoverLoad(Unit) local hassecondaries=false if not grounded and not hoverload then self:_SendMessage("You need to land or hover in position to load!",10,false,Group) if not self.debug then return self end end if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName())then self:_SendMessage("You need to open the door(s) to extract troops!",10,false,Group) if not self.debug then return self end end local unit=Unit local unitname=unit:GetName() local unittype=unit:GetTypeName() local capabilities=self:_GetUnitCapabilities(Unit) local cantroops=capabilities.troops local trooplimit=capabilities.trooplimit local unitcoord=unit:GetCoordinate() local nearestGroup=nil local nearestGroupIndex=-1 local nearestDistance=10000000 local maxdistance=0 local nearestList={} local distancekeys={} local extractdistance=self.CrateDistance*self.ExtractFactor for k,v in pairs(self.DroppedTroops)do local distance=self:_GetDistance(v:GetCoordinate(),unitcoord) local TNow=timer.getTime() local vtime=v.ExtractTime or TNow-310 if distance<=extractdistance and distance~=-1 and(TNow-vtime>300)then nearestGroup=v nearestGroupIndex=k nearestDistance=distance if math.floor(distance)>maxdistance then maxdistance=math.floor(distance)end if nearestList[math.floor(distance)]then distance=maxdistance+1 maxdistance=distance end table.insert(nearestList,math.floor(distance),v) distancekeys[#distancekeys+1]=math.floor(distance) end end if nearestGroup==nil or nearestDistance>extractdistance then self:_SendMessage("No units close enough to extract!",10,false,Group) return self end table.sort(distancekeys) local secondarygroups={} for i=1,#distancekeys do local nearestGroup=nearestList[distancekeys[i]] local groupType=string.match(nearestGroup:GetName(),"(.+)-(.+)$") local Cargotype=nil for k,v in pairs(self.Cargo_Troops)do local comparison="" if type(v.Templates)=="string"then comparison=v.Templates else comparison=v.Templates[1]end if comparison==groupType then Cargotype=v break end end if Cargotype==nil then self:_SendMessage("Can't onboard "..groupType,10,false,Group) else local troopsize=Cargotype:GetCratesNeeded() local numberonboard=0 local loaded={} if self.Loaded_Cargo[unitname]then loaded=self.Loaded_Cargo[unitname] numberonboard=loaded.Troopsloaded or 0 else loaded={} loaded.Troopsloaded=0 loaded.Cratesloaded=0 loaded.Cargo={} end if troopsize+numberonboard>trooplimit then self:_SendMessage("Sorry, we\'re crammed already!",10,false,Group) nearestGroup.ExtractTime=0 else self.CargoCounter=self.CargoCounter+1 nearestGroup.ExtractTime=timer.getTime() local loadcargotype=CTLD_CARGO:New(self.CargoCounter,Cargotype.Name,Cargotype.Templates,Cargotype.CargoType,true,true,Cargotype.CratesNeeded,nil,nil,Cargotype.PerCrateMass) self:T({cargotype=loadcargotype}) local running=math.floor(nearestDistance/4)+20 loaded.Troopsloaded=loaded.Troopsloaded+troopsize table.insert(loaded.Cargo,loadcargotype) self.Loaded_Cargo[unitname]=loaded self:ScheduleOnce(running,self._SendMessage,self,string.format("%s boarded!",Cargotype.Name),10,false,Group) self:_SendMessage(string.format("%s boarding!",Cargotype.Name),10,false,Group) self:_RefreshDropTroopsMenu(Group,Unit) self:_UpdateUnitCargoMass(Unit) local groupname=nearestGroup:GetName() self:__TroopsExtracted(running,Group,Unit,nearestGroup,groupname) local coord=Unit:GetCoordinate()or Group:GetCoordinate() local Point if coord then local heading=unit:GetHeading()or 0 local Angle=math.floor((heading+160)%360) Point=coord:Translate(8,Angle):GetVec2() if Point then nearestGroup:RouteToVec2(Point,5) end end hassecondaries=false if type(Cargotype.Templates)=="table"and Cargotype.Templates[2]then for _,_key in pairs(Cargotype.Templates)do table.insert(secondarygroups,_key) hassecondaries=true end end local destroytimer=math.random(10,20) nearestGroup:Destroy(false,destroytimer) end end end if hassecondaries==true then for _,_name in pairs(secondarygroups)do for _,_group in pairs(nearestList)do if _group and _group:IsAlive()then local groupname=string.match(_group:GetName(),"(.+)-(.+)$") if _name==groupname then _group:Destroy(false,15) end end end end end self:CleanDroppedTroops() return self end function CTLD:_GetCrates(Group,Unit,Cargo,number,drop,pack) self:T(self.lid.." _GetCrates") if not drop and not pack then local cgoname=Cargo:GetName() local instock=Cargo:GetStock() if type(instock)=="number"and tonumber(instock)<=0 and tonumber(instock)~=-1 then self:_SendMessage(string.format("Sorry, we ran out of %s",cgoname),10,false,Group) return self end end local inzone=false local drop=drop or false local ship=nil local width=20 local distance=nil local zone=nil if not drop and not pack then inzone=self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) if not inzone then inzone,ship,zone,distance,width=self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP) end elseif drop and not pack then if self.dropcratesanywhere then inzone=true else inzone=self:IsUnitInZone(Unit,CTLD.CargoZoneType.DROP) end elseif pack and not drop then inzone=true end if not inzone then self:_SendMessage("You are not close enough to a logistics zone!",10,false,Group) if not self.debug then return self end end local location=Cargo:GetLocation() if location then local unitcoord=Unit:GetCoordinate()or Group:GetCoordinate() if unitcoord then if not location:IsCoordinateInZone(unitcoord)then self:_SendMessage("The requested cargo is not available in this zone!",10,false,Group) if not self.debug then return self end end end end local capabilities=self:_GetUnitCapabilities(Unit) local canloadcratesno=capabilities.cratelimit local loaddist=self.CrateDistance or 35 local nearcrates,numbernearby=self:_FindCratesNearby(Group,Unit,loaddist,true,true) if numbernearby>=canloadcratesno and not drop then self:_SendMessage("There are enough crates nearby already! Take care of those first!",10,false,Group) return self end local IsHerc=self:IsFixedWing(Unit) local IsHook=self:IsHook(Unit) local cargotype=Cargo local number=number or cargotype:GetCratesNeeded() local cratesneeded=cargotype:GetCratesNeeded() local cratename=cargotype:GetName() local cratetemplate="Container" local cgotype=cargotype:GetType() local cgomass=cargotype:GetMass() local isstatic=false if cgotype==CTLD_CARGO.Enum.STATIC then cratetemplate=cargotype:GetTemplates() isstatic=true end local position=Unit:GetCoordinate() local heading=Unit:GetHeading()+1 local height=Unit:GetHeight() local droppedcargo={} local cratedistance=0 local rheading=0 local angleOffNose=0 local addon=0 if IsHerc or IsHook then addon=180 end heading=(heading+addon)%360 local row=1 local column=1 local initialdist=IsHerc and 16 or(capabilities.length+2) local startpos=position:Translate(initialdist,heading) if self.placeCratesAhead==true then cratedistance=initialdist end local cratecoord=nil for i=1,number do local cratealias=string.format("%s-%s-%d",cratename,cratetemplate,math.random(1,100000)) if not self.placeCratesAhead or drop==true then cratedistance=(i-1)*2.5+capabilities.length if cratedistance>self.CrateDistance then cratedistance=self.CrateDistance end rheading=UTILS.RandomGaussian(0,30,-90,90,100) rheading=math.fmod((heading+rheading),360) cratecoord=position:Translate(cratedistance,rheading) else cratedistance=(row-1)*6 rheading=90 row=row+1 cratecoord=startpos:Translate(cratedistance,rheading) if row>4 then row=1 startpos:Translate(6,heading,nil,true) end end self.CrateCounter=self.CrateCounter+1 local CCat,CType,CShape=Cargo:GetStaticTypeAndShape() local basetype=CType or self.basetype or"container_cargo" CCat=CCat or"Cargos" if isstatic then basetype=cratetemplate end if type(ship)=="string"then self:T("Spawning on ship "..ship) local Ship=UNIT:FindByName(ship) local shipcoord=Ship:GetCoordinate() local unitcoord=Unit:GetCoordinate() local dist=shipcoord:Get2DDistance(unitcoord) dist=dist-(20+math.random(1,10)) local width=width/2 local Offy=math.random(-width,width) local spawnstatic=SPAWNSTATIC:NewFromType(basetype,CCat,self.cratecountry) :InitCargoMass(cgomass) :InitCargo(self.enableslingload) :InitLinkToUnit(Ship,dist,Offy,0) if CShape then spawnstatic:InitShape(CShape) end if isstatic then local map=cargotype:GetStaticResourceMap() spawnstatic.TemplateStaticUnit.resourcePayload=map end self.Spawned_Crates[self.CrateCounter]=spawnstatic:Spawn(270,cratealias) else local spawnstatic=SPAWNSTATIC:NewFromType(basetype,CCat,self.cratecountry) :InitCoordinate(cratecoord) :InitCargoMass(cgomass) :InitCargo(self.enableslingload) if CShape then spawnstatic:InitShape(CShape) end if isstatic then local map=cargotype:GetStaticResourceMap() spawnstatic.TemplateStaticUnit.resourcePayload=map end self.Spawned_Crates[self.CrateCounter]=spawnstatic:Spawn(270,cratealias) end local templ=cargotype:GetTemplates() local sorte=cargotype:GetType() local subcat=cargotype.Subcategory self.CargoCounter=self.CargoCounter+1 local realcargo=nil if drop then realcargo=CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,true,false,cratesneeded,self.Spawned_Crates[self.CrateCounter],true,cargotype.PerCrateMass,nil,subcat) local map=cargotype:GetStaticResourceMap() realcargo:SetStaticResourceMap(map) local CCat,CType,CShape=cargotype:GetStaticTypeAndShape() realcargo:SetStaticTypeAndShape(CCat,CType,CShape) if cargotype.TypeNames then realcargo.TypeNames=UTILS.DeepCopy(cargotype.TypeNames) end table.insert(droppedcargo,realcargo) else realcargo=CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,false,false,cratesneeded,self.Spawned_Crates[self.CrateCounter],false,cargotype.PerCrateMass,nil,subcat) local map=cargotype:GetStaticResourceMap() realcargo:SetStaticResourceMap(map) if cargotype.TypeNames then realcargo.TypeNames=UTILS.DeepCopy(cargotype.TypeNames) end end local CCat,CType,CShape=cargotype:GetStaticTypeAndShape() realcargo:SetStaticTypeAndShape(CCat,CType,CShape) table.insert(self.Spawned_Cargo,realcargo) end if not(drop or pack)then Cargo:RemoveStock() end local text=string.format("Crates for %s have been positioned near you!",cratename) if drop then text=string.format("Crates for %s have been dropped!",cratename) self:__CratesDropped(1,Group,Unit,droppedcargo) end self:_SendMessage(text,10,false,Group) self:_RefreshLoadCratesMenu(Group,Unit) return self end function CTLD:InjectStatics(Zone,Cargo,RandomCoord,FromLoad) self:T(self.lid.." InjectStatics") local cratecoord=Zone:GetCoordinate() if RandomCoord then cratecoord=Zone:GetRandomCoordinate(5,20) end local surface=cratecoord:GetSurfaceType() if surface==land.SurfaceType.WATER then return self end local cargotype=Cargo local cratesneeded=cargotype:GetCratesNeeded() local cratetemplate="Container" local cratename=cargotype:GetName() local cgotype=cargotype:GetType() local cgomass=cargotype:GetMass() local cratenumber=cargotype:GetCratesNeeded()or 1 if FromLoad==true then cratenumber=1 end for i=1,cratenumber do local cratealias=string.format("%s-%s-%d",cratename,cratetemplate,math.random(1,100000)) local isstatic=false if cgotype==CTLD_CARGO.Enum.STATIC then cratetemplate=cargotype:GetTemplates() isstatic=true end local CCat,CType,CShape=cargotype:GetStaticTypeAndShape() local basetype=CType or self.basetype or"container_cargo" CCat=CCat or"Cargos" if isstatic then basetype=cratetemplate end self.CrateCounter=self.CrateCounter+1 local spawnstatic=SPAWNSTATIC:NewFromType(basetype,CCat,self.cratecountry) :InitCargoMass(cgomass) :InitCargo(self.enableslingload) :InitCoordinate(cratecoord) if CShape then spawnstatic:InitShape(CShape) end if isstatic then local map=cargotype:GetStaticResourceMap() spawnstatic.TemplateStaticUnit.resourcePayload=map end self.Spawned_Crates[self.CrateCounter]=spawnstatic:Spawn(270,cratealias) local templ=cargotype:GetTemplates() local sorte=cargotype:GetType() cargotype.Positionable=self.Spawned_Crates[self.CrateCounter] table.insert(self.Spawned_Cargo,cargotype) end return self end function CTLD:InjectStaticFromTemplate(Zone,Template,Mass) self:T(self.lid.." InjectStaticFromTemplate") local cargotype=self:GetStaticsCargoFromTemplate(Template,Mass) self:InjectStatics(Zone,cargotype,true,true) return self end function CTLD:_ListCratesNearby(_group,_unit) self:T(self.lid.." _ListCratesNearby") local finddist=self.CrateDistance or 35 local crates,number,loadedbygc,indexgc=self:_FindCratesNearby(_group,_unit,finddist,true,true) if number>0 or indexgc>0 then local text=REPORT:New("Crates Found Nearby:") text:Add("------------------------------------------------------------") for _,_entry in pairs(crates)do local entry=_entry local name=entry:GetName() local dropped=entry:WasDropped() if dropped then text:Add(string.format("Dropped crate for %s, %dkg",name,entry.PerCrateMass)) else text:Add(string.format("Crate for %s, %dkg",name,entry.PerCrateMass)) end end if text:GetCount()==1 then text:Add(" N O N E") end text:Add("------------------------------------------------------------") if indexgc>0 then text:Add("Probably ground crew loadable (F8)") for _,_entry in pairs(loadedbygc)do local entry=_entry local name=entry:GetName() local dropped=entry:WasDropped() if dropped then text:Add(string.format("Dropped crate for %s, %dkg",name,entry.PerCrateMass)) else text:Add(string.format("Crate for %s, %dkg",name,entry.PerCrateMass)) end end end self:_SendMessage(text:Text(),30,true,_group) else self:_SendMessage(string.format("No (loadable) crates within %d meters!",finddist),10,false,_group) end return self end function CTLD:_RemoveCratesNearby(_group,_unit) self:T(self.lid.." _RemoveCratesNearby") local finddist=self.CrateDistance or 35 local crates,number=self:_FindCratesNearby(_group,_unit,finddist,true,true) if number>0 then local removedIDs={} local text=REPORT:New("Removing Crates Found Nearby:") text:Add("------------------------------------------------------------") for _,_entry in pairs(crates)do local entry=_entry local name=entry:GetName()or"none" text:Add(string.format("Crate for %s, %dkg removed",name,entry.PerCrateMass)) if entry:GetPositionable()then entry:GetPositionable():Destroy(false) end table.insert(removedIDs,entry:GetID()) end if text:GetCount()==1 then text:Add(" N O N E") end text:Add("------------------------------------------------------------") self:_SendMessage(text:Text(),30,true,_group) self:_CleanupTrackedCrates(removedIDs) self:_RefreshLoadCratesMenu(_group,_unit) else self:_SendMessage(string.format("No (loadable) crates within %d meters!",finddist),10,false,_group) end return self end function CTLD:_GetDistance(_point1,_point2) self:T(self.lid.." _GetDistance") if _point1 and _point2 then local distance1=_point1:Get2DDistance(_point2) local distance2=_point1:DistanceFromPointVec2(_point2) if distance1 and type(distance1)=="number"then return distance1 elseif distance2 and type(distance2)=="number"then return distance2 else self:E("*****Cannot calculate distance!") self:E({_point1,_point2}) return-1 end else self:E("******Cannot calculate distance!") self:E({_point1,_point2}) return-1 end end function CTLD:_FindCratesNearby(_group,_unit,_dist,_ignoreweight,ignoretype) self:T(self.lid.." _FindCratesNearby") local finddist=_dist local location=_group:GetCoordinate() local existingcrates=self.Spawned_Cargo local index=0 local indexg=0 local found={} local LoadedbyGC={} local loadedmass=0 local unittype="none" local capabilities={} local maxloadable=2000 local IsHook=self:IsHook(_unit) if not _ignoreweight then maxloadable=self:_GetMaxLoadableMass(_unit) end self:T2(self.lid.." Max loadable mass: "..maxloadable) for _,_cargoobject in pairs(existingcrates)do local cargo=_cargoobject local static=cargo:GetPositionable() local weight=cargo:GetMass() local staticid=cargo:GetID() self:T2(self.lid.." Found cargo mass: "..weight) if static and static:IsAlive()then local restricthooktononstatics=self.enableChinookGCLoading and IsHook local cargoisstatic=cargo:GetType()==CTLD_CARGO.Enum.STATIC and true or false local restricted=cargoisstatic and restricthooktononstatics local staticpos=static:GetCoordinate() local cando=cargo:UnitCanCarry(_unit) if ignoretype==true then cando=true end local distance=self:_GetDistance(location,staticpos) self:T(self.lid..string.format("Dist %dm/%dm | weight %dkg | maxloadable %dkg",distance,finddist,weight,maxloadable)) if distance<=finddist and(weight<=maxloadable or _ignoreweight)and restricted==false and cando==true then index=index+1 table.insert(found,staticid,cargo) maxloadable=maxloadable-weight end end end return found,index,LoadedbyGC,indexg end function CTLD:_LoadCratesNearby(Group,Unit) self:T(self.lid.." _LoadCratesNearby") local group=Group local unit=Unit local unitname=unit:GetName() local unittype=unit:GetTypeName() local capabilities=self:_GetUnitCapabilities(Unit) local cancrates=capabilities.crates local cratelimit=capabilities.cratelimit local grounded=not self:IsUnitInAir(Unit) local canhoverload=self:CanHoverLoad(Unit) if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName())then self:_SendMessage("You need to open the door(s) to load cargo!",10,false,Group) if not self.debug then return self end end if not cancrates then self:_SendMessage("Sorry this chopper cannot carry crates!",10,false,Group) elseif self.forcehoverload and not canhoverload then self:_SendMessage("Hover over the crates to pick them up!",10,false,Group) elseif not grounded and not canhoverload then self:_SendMessage("Land or hover over the crates to pick them up!",10,false,Group) else local numberonboard=0 local loaded={} if self.Loaded_Cargo[unitname]then loaded=self.Loaded_Cargo[unitname] numberonboard=loaded.Cratesloaded or 0 else loaded={} loaded.Troopsloaded=0 loaded.Cratesloaded=0 loaded.Cargo={} end local finddist=self.CrateDistance or 35 local nearcrates,number=self:_FindCratesNearby(Group,Unit,finddist,false,false) self:T(self.lid.." Crates found: "..number) if number==0 and self.hoverautoloading then return self elseif number==0 then self:_SendMessage("Sorry, no loadable crates nearby or max cargo weight reached!",10,false,Group) return self elseif numberonboard==cratelimit then self:_SendMessage("Sorry, we are fully loaded!",10,false,Group) return self else local capacity=cratelimit-numberonboard local crateidsloaded={} local crateMap={} for _,cObj in pairs(nearcrates)do if not cObj:HasMoved()or self.allowcratepickupagain then local cName=cObj:GetName()or"Unknown" crateMap[cName]=crateMap[cName]or{} table.insert(crateMap[cName],cObj) end end for cName,crateList in pairs(crateMap)do if capacity<=0 then break end table.sort(crateList,function(a,b)return a:GetID()>b:GetID()end) local needed=crateList[1]:GetCratesNeeded()or 1 local totalFound=#crateList local loadedHere=0 while loaded.Cratesloaded0 then local fullSets=math.floor(loadedHere/needed) local leftover=loadedHere%needed if needed>1 then if fullSets>0 and leftover==0 then self:_SendMessage(string.format("Loaded %d %s.",fullSets,cName),10,false,Group) elseif fullSets>0 and leftover>0 then self:_SendMessage(string.format("Loaded %d %s(s), with %d leftover crate(s).",fullSets,cName,leftover),10,false,Group) else self:_SendMessage(string.format("Loaded only %d/%d crate(s) of %s.",loadedHere,needed,cName),15,false,Group) end else self:_SendMessage(string.format("Loaded %d %s(s).",loadedHere,cName),10,false,Group) end end end self.Loaded_Cargo[unitname]=loaded self:_UpdateUnitCargoMass(Unit) self:_RefreshDropCratesMenu(Group,Unit) self:_RefreshLoadCratesMenu(Group,Unit) self:_CleanupTrackedCrates(crateidsloaded) end end return self end function CTLD:_CleanupTrackedCrates(crateIdsToRemove) local existingcrates=self.Spawned_Cargo local newexcrates={} for _,_crate in pairs(existingcrates)do local excrate=_crate local ID=excrate:GetID() local keep=true for _,_ID in pairs(crateIdsToRemove)do if ID==_ID then keep=false end end local static=_crate:GetPositionable() if not static or not static:IsAlive()then keep=false end if keep then table.insert(newexcrates,_crate) end end self.Spawned_Cargo=nil self.Spawned_Cargo=newexcrates return self end function CTLD:_GetUnitCargoMass(Unit) self:T(self.lid.." _GetUnitCargoMass") if not Unit then return 0 end local unitname=Unit:GetName() local loadedcargo=self.Loaded_Cargo[unitname]or{} local loadedmass=0 if self.Loaded_Cargo[unitname]then local cargotable=loadedcargo.Cargo or{} for _,_cargo in pairs(cargotable)do local cargo=_cargo local type=cargo:GetType() if(type==CTLD_CARGO.Enum.TROOPS or type==CTLD_CARGO.Enum.ENGINEERS)and not cargo:WasDropped()then loadedmass=loadedmass+(cargo.PerCrateMass*cargo:GetCratesNeeded()) end if type~=CTLD_CARGO.Enum.TROOPS and type~=CTLD_CARGO.Enum.ENGINEERS and type~=CTLD_CARGO.Enum.GCLOADABLE and not cargo:WasDropped()then loadedmass=loadedmass+cargo.PerCrateMass end if type==CTLD_CARGO.Enum.GCLOADABLE then local mass=cargo:GetCargoWeight() loadedmass=loadedmass+mass end end end return loadedmass end function CTLD:_GetMaxLoadableMass(Unit) self:T(self.lid.." _GetMaxLoadableMass") if not Unit then return 0 end local loadable=0 local loadedmass=self:_GetUnitCargoMass(Unit) local capabilities=self:_GetUnitCapabilities(Unit) local maxmass=capabilities.cargoweightlimit or 2000 loadable=maxmass-loadedmass return loadable end function CTLD:_UpdateUnitCargoMass(Unit) self:T(self.lid.." _UpdateUnitCargoMass") local calculatedMass=self:_GetUnitCargoMass(Unit) Unit:SetUnitInternalCargo(calculatedMass) return self end function CTLD:_ListCargo(Group,Unit) self:T(self.lid.." _ListCargo") local unitname=Unit:GetName() local unittype=Unit:GetTypeName() local capabilities=self:_GetUnitCapabilities(Unit) local trooplimit=capabilities.trooplimit local cratelimit=capabilities.cratelimit local loadedcargo=self.Loaded_Cargo[unitname]or{} local loadedmass=self:_GetUnitCargoMass(Unit) local maxloadable=self:_GetMaxLoadableMass(Unit) local finddist=self.CrateDistance or 35 if self.Loaded_Cargo[unitname]then local no_troops=loadedcargo.Troopsloaded or 0 local no_crates=loadedcargo.Cratesloaded or 0 local cargotable=loadedcargo.Cargo or{} local report=REPORT:New("Transport Checkout Sheet") report:Add("------------------------------------------------------------") report:Add(string.format("Troops: %d(%d), Crates: %d(%d)",no_troops,trooplimit,no_crates,cratelimit)) report:Add("------------------------------------------------------------") report:Add(" -- TROOPS --") for _,_cargo in pairs(cargotable)do local cargo=_cargo local type=cargo:GetType() if(type==CTLD_CARGO.Enum.TROOPS or type==CTLD_CARGO.Enum.ENGINEERS)and(not cargo:WasDropped()or self.allowcratepickupagain)then report:Add(string.format("Troop: %s size %d",cargo:GetName(),cargo:GetCratesNeeded())) end end if report:GetCount()==4 then report:Add(" N O N E") end report:Add("------------------------------------------------------------") report:Add(" -- CRATES --") local cratecount=0 local accumCrates={} for _,_cargo in pairs(cargotable or{})do local cargo=_cargo local type=cargo:GetType() if(type~=CTLD_CARGO.Enum.TROOPS and type~=CTLD_CARGO.Enum.ENGINEERS and type~=CTLD_CARGO.Enum.GCLOADABLE)and(not cargo:WasDropped()or self.allowcratepickupagain)then local cName=cargo:GetName() local needed=cargo:GetCratesNeeded()or 1 accumCrates[cName]=accumCrates[cName]or{count=0,needed=needed} accumCrates[cName].count=accumCrates[cName].count+1 end if type==CTLD_CARGO.Enum.GCLOADABLE and not cargo:WasDropped()then report:Add(string.format("GC loaded Crate: %s size 1",cargo:GetName())) cratecount=cratecount+1 end end for cName,data in pairs(accumCrates)do cratecount=cratecount+data.count report:Add(string.format("Crate: %s %d/%d",cName,data.count,data.needed)) end if cratecount==0 then report:Add(" N O N E") end report:Add("------------------------------------------------------------") report:Add("Total Mass: "..loadedmass.." kg. Loadable: "..maxloadable.." kg.") local text=report:Text() self:_SendMessage(text,30,true,Group) else self:_SendMessage(string.format("Nothing loaded!\nTroop limit: %d | Crate limit %d | Weight limit %d kgs",trooplimit,cratelimit,maxloadable),10,false,Group) end return self end function CTLD:_ListInventory(Group,Unit) self:T(self.lid.." _ListInventory") local unitname=Unit:GetName() local unittype=Unit:GetTypeName() local cgotypes=self.Cargo_Crates local trptypes=self.Cargo_Troops local stctypes=self.Cargo_Statics local function countcargo(cgotable) local counter=0 for _,_cgo in pairs(cgotable)do counter=counter+1 end return counter end local crateno=countcargo(cgotypes) local troopno=countcargo(trptypes) local staticno=countcargo(stctypes) if(crateno>0 or troopno>0 or staticno>0)then local report=REPORT:New("Inventory Sheet") report:Add("------------------------------------------------------------") report:Add(string.format("Troops: %d, Cratetypes: %d",troopno,crateno+staticno)) report:Add("------------------------------------------------------------") report:Add(" -- TROOPS --") for _,_cargo in pairs(trptypes)do local cargo=_cargo local type=cargo:GetType() if(type==CTLD_CARGO.Enum.TROOPS or type==CTLD_CARGO.Enum.ENGINEERS)and not cargo:WasDropped()then local stockn=cargo:GetStock() local stock="none" if stockn==-1 then stock="unlimited" elseif stockn>0 then stock=tostring(stockn) end report:Add(string.format("Unit: %s | Soldiers: %d | Stock: %s",cargo:GetName(),cargo:GetCratesNeeded(),stock)) end end if report:GetCount()==4 then report:Add(" N O N E") end report:Add("------------------------------------------------------------") report:Add(" -- CRATES --") local cratecount=0 for _,_cargo in pairs(cgotypes)do local cargo=_cargo local type=cargo:GetType() if(type~=CTLD_CARGO.Enum.TROOPS and type~=CTLD_CARGO.Enum.ENGINEERS)and not cargo:WasDropped()then local stockn=cargo:GetStock() local stock="none" if stockn==-1 then stock="unlimited" elseif stockn>0 then stock=tostring(stockn) end report:Add(string.format("Type: %s | Crates per Set: %d | Stock: %s",cargo:GetName(),cargo:GetCratesNeeded(),stock)) cratecount=cratecount+1 end end for _,_cargo in pairs(stctypes)do local cargo=_cargo local type=cargo:GetType() if(type==CTLD_CARGO.Enum.STATIC)and not cargo:WasDropped()then local stockn=cargo:GetStock() local stock="none" if stockn==-1 then stock="unlimited" elseif stockn>0 then stock=tostring(stockn) end report:Add(string.format("Type: %s | Stock: %s",cargo:GetName(),stock)) cratecount=cratecount+1 end end if cratecount==0 then report:Add(" N O N E") end local text=report:Text() self:_SendMessage(text,30,true,Group) else self:_SendMessage(string.format("Nothing in stock!"),10,false,Group) end return self end function CTLD:IsFixedWing(Unit) local typename=Unit:GetTypeName()or"none" for _,_name in pairs(self.FixedWingTypes or{})do if typename==_name or string.find(typename,_name,1,true)then return true end end return false end function CTLD:IsHook(Unit) if Unit and string.find(Unit:GetTypeName(),"CH.47")then return true else return false end end function CTLD:_GetUnitPositions(Coordinate,Radius,Heading,Template) local Positions={} local template=_DATABASE:GetGroupTemplate(Template) local numbertroops=#template.units local slightshift=math.abs(math.random(1,500)/100) local newcenter=Coordinate:Translate(Radius+slightshift,((Heading+270+math.random(1,10))%360)) for i=1,360,math.floor(360/numbertroops)do local phead=((Heading+270+i)%360) local post=newcenter:Translate(Radius,phead) local pos1=post:GetVec2() local p1t={ x=pos1.x, y=pos1.y, heading=phead, } table.insert(Positions,p1t) end return Positions end function CTLD:_UnloadTroops(Group,Unit) self:T(self.lid.." _UnloadTroops") local droppingatbase=false local canunload=true if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName())then self:_SendMessage("You need to open the door(s) to unload troops!",10,false,Group) if not self.debug then return self end end local inzone,zonename,zone,distance=self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) if not inzone then inzone,zonename,zone,distance=self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP) end if inzone then droppingatbase=true end local hoverunload=self:IsCorrectHover(Unit) local IsHerc=self:IsFixedWing(Unit) local IsHook=self:IsHook(Unit) if IsHerc and(not IsHook)then hoverunload=self:IsCorrectFlightParameters(Unit) end local grounded=not self:IsUnitInAir(Unit) local unitname=Unit:GetName() if self.Loaded_Cargo[unitname]and(grounded or hoverunload)then if not droppingatbase or self.debug then local loadedcargo=self.Loaded_Cargo[unitname]or{} local cargotable=loadedcargo.Cargo for _,_cargo in pairs(cargotable)do local cargo=_cargo local type=cargo:GetType() if(type==CTLD_CARGO.Enum.TROOPS or type==CTLD_CARGO.Enum.ENGINEERS)and not cargo:WasDropped()then local name=cargo:GetName()or"none" local temptable=cargo:GetTemplates()or{} local position=Group:GetCoordinate() local zoneradius=self.troopdropzoneradius or 100 local factor=1 if IsHerc then factor=cargo:GetCratesNeeded()or 1 zoneradius=Unit:GetVelocityMPS()or 100 end local zone=ZONE_GROUP:New(string.format("Unload zone-%s",unitname),Group,zoneradius*factor) local randomcoord=zone:GetRandomCoordinate(10,30*factor) local heading=Group:GetHeading()or 0 if hoverunload or grounded then randomcoord=Group:GetCoordinate() local Angle=(heading+270)%360 if IsHerc or IsHook then Angle=(heading+180)%360 end local offset=hoverunload and self.TroopUnloadDistHover or self.TroopUnloadDistGround if IsHerc then offset=self.TroopUnloadDistGroundHerc or 25 end if IsHook then offset=self.TroopUnloadDistGroundHook or 15 if hoverunload and self.TroopUnloadDistHoverHook then offset=self.TroopUnloadDistHoverHook or 5 end end randomcoord:Translate(offset,Angle,nil,true) end local tempcount=0 local ishook=self:IsHook(Unit) if ishook then tempcount=self.ChinookTroopCircleRadius or 5 end for _,_template in pairs(temptable)do self.TroopCounter=self.TroopCounter+1 tempcount=tempcount+1 local alias=string.format("%s-%d",_template,math.random(1,100000)) local rad=2.5+(tempcount*2) local Positions=self:_GetUnitPositions(randomcoord,rad,heading,_template) self.DroppedTroops[self.TroopCounter]=SPAWN:NewWithAlias(_template,alias) :InitDelayOff() :InitSetUnitAbsolutePositions(Positions) :OnSpawnGroup(function(grp)grp.spawntime=timer.getTime()end) :SpawnFromVec2(randomcoord:GetVec2()) self:__TroopsDeployed(1,Group,Unit,self.DroppedTroops[self.TroopCounter],type) end cargo:SetWasDropped(true) if type==CTLD_CARGO.Enum.ENGINEERS then self.Engineers=self.Engineers+1 local grpname=self.DroppedTroops[self.TroopCounter]:GetName() self.EngineersInField[self.Engineers]=CTLD_ENGINEERING:New(name,grpname) self:_SendMessage(string.format("Dropped Engineers %s into action!",name),10,false,Group) else self:_SendMessage(string.format("Dropped Troops %s into action!",name),10,false,Group) end end end else self:_SendMessage("Troops have returned to base!",10,false,Group) self:__TroopsRTB(1,Group,Unit,zonename,zone) end local loaded={} loaded.Troopsloaded=0 loaded.Cratesloaded=0 loaded.Cargo={} local loadedcargo=self.Loaded_Cargo[unitname]or{} local cargotable=loadedcargo.Cargo or{} for _,_cargo in pairs(cargotable)do local cargo=_cargo local type=cargo:GetType() local dropped=cargo:WasDropped() if type~=CTLD_CARGO.Enum.TROOPS and type~=CTLD_CARGO.Enum.ENGINEERS and not dropped then table.insert(loaded.Cargo,_cargo) loaded.Cratesloaded=loaded.Cratesloaded+1 else if(type==CTLD_CARGO.Enum.TROOPS or type==CTLD_CARGO.Enum.ENGINEERS)and droppingatbase then local name=cargo:GetName() local gentroops=self.Cargo_Troops for _id,_troop in pairs(gentroops)do if _troop.Name==name then local stock=_troop:GetStock() if stock and tonumber(stock)>=0 then _troop:AddStock()end end end end end end self.Loaded_Cargo[unitname]=nil self.Loaded_Cargo[unitname]=loaded self:_RefreshDropTroopsMenu(Group,Unit) self:_UpdateUnitCargoMass(Unit) else if IsHerc then self:_SendMessage("Nothing loaded or not within airdrop parameters!",10,false,Group) else self:_SendMessage("Nothing loaded or not hovering within parameters!",10,false,Group) end end return self end function CTLD:_UnloadCrates(Group,Unit) self:T(self.lid.." _UnloadCrates") if not self.dropcratesanywhere then local inzone,zonename,zone,distance=self:IsUnitInZone(Unit,CTLD.CargoZoneType.DROP) if not inzone then self:_SendMessage("You are not close enough to a drop zone!",10,false,Group) if not self.debug then return self end end end if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName())then self:_SendMessage("You need to open the door(s) to drop cargo!",10,false,Group) if not self.debug then return self end end local hoverunload=self:IsCorrectHover(Unit) local IsHerc=self:IsFixedWing(Unit) local IsHook=self:IsHook(Unit) if IsHerc and(not IsHook)then hoverunload=self:IsCorrectFlightParameters(Unit) end local grounded=not self:IsUnitInAir(Unit) local unitname=Unit:GetName() if self.Loaded_Cargo[unitname]and(grounded or hoverunload)then local loadedcargo=self.Loaded_Cargo[unitname]or{} local cargotable=loadedcargo.Cargo for _,_cargo in pairs(cargotable)do local cargo=_cargo local type=cargo:GetType() if type~=CTLD_CARGO.Enum.TROOPS and type~=CTLD_CARGO.Enum.ENGINEERS and type~=CTLD_CARGO.Enum.GCLOADABLE and(not cargo:WasDropped()or self.allowcratepickupagain)then self:_GetCrates(Group,Unit,cargo,1,true) cargo:SetWasDropped(true) cargo:SetHasMoved(true) end end local loaded={} loaded.Troopsloaded=0 loaded.Cratesloaded=0 loaded.Cargo={} for _,_cargo in pairs(cargotable)do local cargo=_cargo local type=cargo:GetType() local size=cargo:GetCratesNeeded() if type==CTLD_CARGO.Enum.TROOPS or type==CTLD_CARGO.Enum.ENGINEERS then table.insert(loaded.Cargo,_cargo) loaded.Troopsloaded=loaded.Troopsloaded+size end if type==CTLD_CARGO.Enum.GCLOADABLE and not cargo:WasDropped()then table.insert(loaded.Cargo,_cargo) loaded.Cratesloaded=loaded.Cratesloaded+size end end self.Loaded_Cargo[unitname]=nil self.Loaded_Cargo[unitname]=loaded self:_UpdateUnitCargoMass(Unit) self:_RefreshDropCratesMenu(Group,Unit) else if IsHerc then self:_SendMessage("Nothing loaded or not within airdrop parameters!",10,false,Group) else self:_SendMessage("Nothing loaded or not hovering within parameters!",10,false,Group) end end return self end function CTLD:_BuildCrates(Group,Unit,Engineering) self:T(self.lid.." _BuildCrates") if self:IsFixedWing(Unit)and self.enableFixedWing and not Engineering then local speed=Unit:GetVelocityKMH() if speed>1 then self:_SendMessage("You need to land / stop to build something, Pilot!",10,false,Group) return self end end if not Engineering and self.nobuildinloadzones then local inloadzone=self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) if inloadzone then self:_SendMessage("You cannot build in a loading area, Pilot!",10,false,Group) return self end end local finddist=self.CrateDistance or 35 local crates,number=self:_FindCratesNearby(Group,Unit,finddist,true,true) local buildables={} local foundbuilds=false local canbuild=false if number>0 then for _,_crate in pairs(crates)do local Crate=_crate if(Crate:WasDropped()or not self.movecratesbeforebuild)and not Crate:IsRepair()and not Crate:IsStatic()then local name=Crate:GetName() local required=Crate:GetCratesNeeded() local template=Crate:GetTemplates() local ctype=Crate:GetType() local ccoord=Crate:GetPositionable():GetCoordinate() if not buildables[name]then local object={} object.Name=name object.Required=required object.Found=1 object.Template=template object.CanBuild=false object.Type=ctype object.Coord=ccoord:GetVec2() buildables[name]=object foundbuilds=true else buildables[name].Found=buildables[name].Found+1 foundbuilds=true end if buildables[name].Found>=buildables[name].Required then buildables[name].CanBuild=true canbuild=true end self:T({buildables=buildables}) end end local report=REPORT:New("Checklist Buildable Crates") report:Add("------------------------------------------------------------") for _,_build in pairs(buildables)do local build=_build local name=build.Name local needed=build.Required local found=build.Found local txtok="NO" if build.CanBuild then txtok="YES" end local text=string.format("Type: %s | Required %d | Found %d | Can Build %s",name,needed,found,txtok) report:Add(text) end if not foundbuilds then report:Add(" --- None found! ---") if self.movecratesbeforebuild then report:Add("*** Crates need to be moved before building!") end end report:Add("------------------------------------------------------------") local text=report:Text() if not Engineering then self:_SendMessage(text,30,true,Group) else self:T(text) end if canbuild then for _,_build in pairs(buildables)do local build=_build if build.CanBuild then self:_CleanUpCrates(crates,build,number) if self.buildtime and self.buildtime>0 then local buildtimer=TIMER:New(self._BuildObjectFromCrates,self,Group,Unit,build,false,Group:GetCoordinate()) buildtimer:Start(self.buildtime) self:_SendMessage(string.format("Build started, ready in %d seconds!",self.buildtime),15,false,Group) self:__CratesBuildStarted(1,Group,Unit) else self:_BuildObjectFromCrates(Group,Unit,build) end end end end else if not Engineering then self:_SendMessage(string.format("No crates within %d meters!",finddist),10,false,Group)end end return self end function CTLD:_PackCratesNearby(Group,Unit) self:T(self.lid.." _PackCratesNearby") local location=Group:GetCoordinate() local nearestGroups=SET_GROUP:New():FilterCoalitions("blue"):FilterZones({ZONE_RADIUS:New("TempZone",location:GetVec2(),self.PackDistance,false)}):FilterOnce() for _,_Group in pairs(nearestGroups.Set)do for _,_Template in pairs(_DATABASE.Templates.Groups)do if(string.match(_Group:GetName(),_Template.GroupName))then for _,_entry in pairs(self.Cargo_Crates)do if(_entry.Templates[1]==_Template.GroupName)then _Group:Destroy() self:_GetCrates(Group,Unit,_entry,nil,false,true) self:_RefreshLoadCratesMenu(Group,Unit) return self end end end end end return self end function CTLD:_RepairCrates(Group,Unit,Engineering) self:T(self.lid.." _RepairCrates") local finddist=self.CrateDistance or 35 local crates,number=self:_FindCratesNearby(Group,Unit,finddist,true,true) local buildables={} local foundbuilds=false local canbuild=false if number>0 then for _,_crate in pairs(crates)do local Crate=_crate if Crate:WasDropped()and Crate:IsRepair()and not Crate:IsStatic()then local name=Crate:GetName() local required=Crate:GetCratesNeeded() local template=Crate:GetTemplates() local ctype=Crate:GetType() if not buildables[name]then local object={} object.Name=name object.Required=required object.Found=1 object.Template=template object.CanBuild=false object.Type=ctype buildables[name]=object foundbuilds=true else buildables[name].Found=buildables[name].Found+1 foundbuilds=true end if buildables[name].Found>=buildables[name].Required then buildables[name].CanBuild=true canbuild=true end self:T({repair=buildables}) end end local report=REPORT:New("Checklist Repairs") report:Add("------------------------------------------------------------") for _,_build in pairs(buildables)do local build=_build local name=build.Name local needed=build.Required local found=build.Found local txtok="NO" if build.CanBuild then txtok="YES" end local text=string.format("Type: %s | Required %d | Found %d | Can Repair %s",name,needed,found,txtok) report:Add(text) end if not foundbuilds then report:Add(" --- None Found ---")end report:Add("------------------------------------------------------------") local text=report:Text() if not Engineering then self:_SendMessage(text,30,true,Group) else self:T(text) end if canbuild then for _,_build in pairs(buildables)do local build=_build if build.CanBuild then self:_RepairObjectFromCrates(Group,Unit,crates,build,number,Engineering) end end end else if not Engineering then self:_SendMessage(string.format("No crates within %d meters!",finddist),10,false,Group)end end return self end function CTLD:_BuildObjectFromCrates(Group,Unit,Build,Repair,RepairLocation) self:T(self.lid.." _BuildObjectFromCrates") if Group and Group:IsAlive()or(RepairLocation and not Repair)then local name=Build.Name local ctype=Build.Type local canmove=false if ctype==CTLD_CARGO.Enum.VEHICLE then canmove=true end if ctype==CTLD_CARGO.Enum.STATIC then return self end local temptable=Build.Template or{} if type(temptable)=="string"then temptable={temptable} end local zone=nil if RepairLocation and not Repair then zone=ZONE_RADIUS:New(string.format("Build zone-%d",math.random(1,10000)),RepairLocation:GetVec2(),100) else zone=ZONE_GROUP:New(string.format("Unload zone-%d",math.random(1,10000)),Group,100) end local randomcoord=Build.Coord or zone:GetRandomCoordinate(35):GetVec2() if Repair then randomcoord=RepairLocation:GetVec2() end for _,_template in pairs(temptable)do self.TroopCounter=self.TroopCounter+1 local alias=string.format("%s-%d",_template,math.random(1,100000)) if canmove then self.DroppedTroops[self.TroopCounter]=SPAWN:NewWithAlias(_template,alias) :InitDelayOff() :OnSpawnGroup(function(grp)grp.spawntime=timer.getTime()end) :SpawnFromVec2(randomcoord) else self.DroppedTroops[self.TroopCounter]=SPAWN:NewWithAlias(_template,alias) :InitDelayOff() :OnSpawnGroup(function(grp)grp.spawntime=timer.getTime()end) :SpawnFromVec2(randomcoord) end if Repair then self:__CratesRepaired(1,Group,Unit,self.DroppedTroops[self.TroopCounter]) else self:__CratesBuild(1,Group,Unit,self.DroppedTroops[self.TroopCounter]) end end else self:T(self.lid.."Group KIA while building!") end return self end function CTLD:_MoveGroupToZone(Group) self:T(self.lid.." _MoveGroupToZone") local groupname=Group:GetName()or"none" local groupcoord=Group:GetCoordinate() local outcome,name,zone,distance=self:IsUnitInZone(Group,CTLD.CargoZoneType.MOVE) if(distance<=self.movetroopsdistance)and outcome==true and zone~=nil then local groupname=Group:GetName() local zonecoord=zone:GetRandomCoordinate(20,125) local coordinate=zonecoord:GetVec2() Group:SetAIOn() Group:OptionAlarmStateAuto() Group:OptionDisperseOnAttack(30) Group:OptionROEOpenFirePossible() Group:RouteToVec2(coordinate,5) end return self end function CTLD:_CleanUpCrates(Crates,Build,Number) self:T(self.lid.." _CleanUpCrates") local build=Build local existingcrates=self.Spawned_Cargo local newexcrates={} local numberdest=Build.Required local nametype=Build.Name local found=0 local rounds=Number local destIDs={} for _,_crate in pairs(Crates)do local nowcrate=_crate local name=nowcrate:GetName() local thisID=nowcrate:GetID() if name==nametype then table.insert(destIDs,thisID) found=found+1 nowcrate:GetPositionable():Destroy(false) nowcrate.Positionable=nil nowcrate.HasBeenDropped=false end if found==numberdest then break end end self:_CleanupTrackedCrates(destIDs) return self end function CTLD:_RefreshF10Menus() self:T(self.lid.." _RefreshF10Menus") local PlayerSet=self.PilotGroups local PlayerTable=PlayerSet:GetSetObjects() local _UnitList={} for _,groupObj in pairs(PlayerTable)do local firstUnit=groupObj:GetFirstUnitAlive() if firstUnit then if firstUnit:IsPlayer()then if firstUnit:IsHelicopter()or(self.enableFixedWing and self:IsFixedWing(firstUnit))then local _unit=firstUnit:GetName() _UnitList[_unit]=_unit end end end end self.CtldUnits=_UnitList if self.usesubcats then for _id,_cargo in pairs(self.Cargo_Crates)do local entry=_cargo if not self.subcats[entry.Subcategory]then self.subcats[entry.Subcategory]=entry.Subcategory end end for _id,_cargo in pairs(self.Cargo_Statics)do local entry=_cargo if not self.subcats[entry.Subcategory]then self.subcats[entry.Subcategory]=entry.Subcategory end end for _id,_cargo in pairs(self.Cargo_Troops)do local entry=_cargo if not self.subcatsTroop[entry.Subcategory]then self.subcatsTroop[entry.Subcategory]=entry.Subcategory end end end local menucount=0 local menus={} for _,_unitName in pairs(self.CtldUnits)do if(not self.MenusDone[_unitName])or(self.showstockinmenuitems==true)then local _unit=UNIT:FindByName(_unitName) if _unit and _unit:IsAlive()then local _group=_unit:GetGroup() if _group then local capabilities=self:_GetUnitCapabilities(_unit) local cantroops=capabilities.troops local cancrates=capabilities.crates local unittype=_unit:GetTypeName() local isHook=self:IsHook(_unit) local nohookswitch=true if _group.CTLDTopmenu then _group.CTLDTopmenu:Remove() _group.CTLDTopmenu=nil end local toptroops=nil local topcrates=nil local topmenu=MENU_GROUP:New(_group,"CTLD",nil) _group.CTLDTopmenu=topmenu if cantroops then local toptroops=MENU_GROUP:New(_group,"Manage Troops",topmenu) local troopsmenu=MENU_GROUP:New(_group,"Load troops",toptroops) _group.MyTopTroopsMenu=toptroops if self.usesubcats then local subcatmenus={} for catName,_ in pairs(self.subcatsTroop)do subcatmenus[catName]=MENU_GROUP:New(_group,catName,troopsmenu) end for _,cargoObj in pairs(self.Cargo_Troops)do if not cargoObj.DontShowInMenu then local stock=cargoObj:GetStock() local menutext=cargoObj.Name if(stock>=0)and(self.showstockinmenuitems==true)then menutext=menutext.." ["..stock.."]"end MENU_GROUP_COMMAND:New(_group,menutext,subcatmenus[cargoObj.Subcategory],self._LoadTroops,self,_group,_unit,cargoObj) end end else for _,cargoObj in pairs(self.Cargo_Troops)do if not cargoObj.DontShowInMenu then local stock=cargoObj:GetStock() local menutext=cargoObj.Name if(stock>=0)and(self.showstockinmenuitems==true)then menutext=menutext.." ["..stock.."]"end MENU_GROUP_COMMAND:New(_group,menutext,troopsmenu,self._LoadTroops,self,_group,_unit,cargoObj) end end end local dropTroopsMenu=MENU_GROUP:New(_group,"Drop Troops",toptroops):Refresh() MENU_GROUP_COMMAND:New(_group,"Drop ALL troops",dropTroopsMenu,self._UnloadTroops,self,_group,_unit):Refresh() MENU_GROUP_COMMAND:New(_group,"Extract troops",toptroops,self._ExtractTroops,self,_group,_unit):Refresh() local uName=_unit:GetName() local loadedData=self.Loaded_Cargo[uName] if loadedData and loadedData.Cargo then for i,cargoObj in ipairs(loadedData.Cargo)do if cargoObj and(cargoObj:GetType()==CTLD_CARGO.Enum.TROOPS or cargoObj:GetType()==CTLD_CARGO.Enum.ENGINEERS)and not cargoObj:WasDropped()then local name=cargoObj:GetName()or"Unknown" local needed=cargoObj:GetCratesNeeded()or 1 local cID=cargoObj:GetID() local line=string.format("Drop: %s",name,needed,cID) MENU_GROUP_COMMAND:New(_group,line,dropTroopsMenu,self._UnloadSingleTroopByID,self,_group,_unit,cID):Refresh() end end end end if cancrates then local topcrates=MENU_GROUP:New(_group,"Manage Crates",topmenu) _group.MyTopCratesMenu=topcrates local cratesmenu=MENU_GROUP:New(_group,"Get Crates",topcrates) if self.usesubcats then local subcatmenus={} for catName,_ in pairs(self.subcats)do subcatmenus[catName]=MENU_GROUP:New(_group,catName,cratesmenu) end for _,cargoObj in pairs(self.Cargo_Crates)do if not cargoObj.DontShowInMenu then local txt=string.format("Crate %s (%dkg)",cargoObj.Name,cargoObj.PerCrateMass or 0) if cargoObj.Location then txt=txt.."[R]"end local stock=cargoObj:GetStock() if stock>=0 and self.showstockinmenuitems then txt=txt.."["..stock.."]"end MENU_GROUP_COMMAND:New(_group,txt,subcatmenus[cargoObj.Subcategory],self._GetCrates,self,_group,_unit,cargoObj) end end for _,cargoObj in pairs(self.Cargo_Statics)do if not cargoObj.DontShowInMenu then local txt=string.format("Crate %s (%dkg)",cargoObj.Name,cargoObj.PerCrateMass or 0) if cargoObj.Location then txt=txt.."[R]"end local stock=cargoObj:GetStock() if stock>=0 and self.showstockinmenuitems then txt=txt.."["..stock.."]"end MENU_GROUP_COMMAND:New(_group,txt,subcatmenus[cargoObj.Subcategory],self._GetCrates,self,_group,_unit,cargoObj) end end else for _,cargoObj in pairs(self.Cargo_Crates)do if not cargoObj.DontShowInMenu then local txt=string.format("Crate %s (%dkg)",cargoObj.Name,cargoObj.PerCrateMass or 0) if cargoObj.Location then txt=txt.."[R]"end local stock=cargoObj:GetStock() if stock>=0 and self.showstockinmenuitems then txt=txt.."["..stock.."]"end MENU_GROUP_COMMAND:New(_group,txt,cratesmenu,self._GetCrates,self,_group,_unit,cargoObj) end end for _,cargoObj in pairs(self.Cargo_Statics)do if not cargoObj.DontShowInMenu then local txt=string.format("Crate %s (%dkg)",cargoObj.Name,cargoObj.PerCrateMass or 0) if cargoObj.Location then txt=txt.."[R]"end local stock=cargoObj:GetStock() if stock>=0 and self.showstockinmenuitems then txt=txt.."["..stock.."]"end MENU_GROUP_COMMAND:New(_group,txt,cratesmenu,self._GetCrates,self,_group,_unit,cargoObj) end end end local loadCratesMenu=MENU_GROUP:New(_group,"Load Crates",topcrates) _group.MyLoadCratesMenu=loadCratesMenu MENU_GROUP_COMMAND:New(_group,"Load ALL",loadCratesMenu,self._LoadCratesNearby,self,_group,_unit) MENU_GROUP_COMMAND:New(_group,"Show loadable crates",loadCratesMenu,self._RefreshLoadCratesMenu,self,_group,_unit) local dropCratesMenu=MENU_GROUP:New(_group,"Drop Crates",topcrates) topcrates.DropCratesMenu=dropCratesMenu if not self.nobuildmenu then MENU_GROUP_COMMAND:New(_group,"Build crates",topcrates,self._BuildCrates,self,_group,_unit) MENU_GROUP_COMMAND:New(_group,"Repair",topcrates,self._RepairCrates,self,_group,_unit):Refresh() end local removecratesmenu=MENU_GROUP:New(_group,"Remove crates",topcrates) MENU_GROUP_COMMAND:New(_group,"Remove crates nearby",removecratesmenu,self._RemoveCratesNearby,self,_group,_unit) MENU_GROUP_COMMAND:New(_group,"Pack crates",topcrates,self._PackCratesNearby,self,_group,_unit) MENU_GROUP_COMMAND:New(_group,"List crates nearby",topcrates,self._ListCratesNearby,self,_group,_unit) local uName=_unit:GetName() local loadedData=self.Loaded_Cargo[uName] if loadedData and loadedData.Cargo then local cargoByName={} for _,cgo in pairs(loadedData.Cargo)do if cgo and(not cgo:WasDropped())then local cname=cgo:GetName() local cneeded=cgo:GetCratesNeeded() cargoByName[cname]=cargoByName[cname]or{count=0,needed=cneeded} cargoByName[cname].count=cargoByName[cname].count+1 end end for name,info in pairs(cargoByName)do local line=string.format("Drop %s (%d/%d)",name,info.count,info.needed) MENU_GROUP_COMMAND:New(_group,line,dropCratesMenu,self._UnloadSingleCrateSet,self,_group,_unit,name) end end end MENU_GROUP_COMMAND:New(_group,"List boarded cargo",topmenu,self._ListCargo,self,_group,_unit) MENU_GROUP_COMMAND:New(_group,"Inventory",topmenu,self._ListInventory,self,_group,_unit) MENU_GROUP_COMMAND:New(_group,"List active zone beacons",topmenu,self._ListRadioBeacons,self,_group,_unit) local smoketopmenu=MENU_GROUP:New(_group,"Smokes, Flares, Beacons",topmenu) MENU_GROUP_COMMAND:New(_group,"Smoke zones nearby",smoketopmenu,self.SmokeZoneNearBy,self,_unit,false) local smokeself=MENU_GROUP:New(_group,"Drop smoke now",smoketopmenu) MENU_GROUP_COMMAND:New(_group,"Red smoke",smokeself,self.SmokePositionNow,self,_unit,false,SMOKECOLOR.Red) MENU_GROUP_COMMAND:New(_group,"Blue smoke",smokeself,self.SmokePositionNow,self,_unit,false,SMOKECOLOR.Blue) MENU_GROUP_COMMAND:New(_group,"Green smoke",smokeself,self.SmokePositionNow,self,_unit,false,SMOKECOLOR.Green) MENU_GROUP_COMMAND:New(_group,"Orange smoke",smokeself,self.SmokePositionNow,self,_unit,false,SMOKECOLOR.Orange) MENU_GROUP_COMMAND:New(_group,"White smoke",smokeself,self.SmokePositionNow,self,_unit,false,SMOKECOLOR.White) MENU_GROUP_COMMAND:New(_group,"Flare zones nearby",smoketopmenu,self.SmokeZoneNearBy,self,_unit,true) MENU_GROUP_COMMAND:New(_group,"Fire flare now",smoketopmenu,self.SmokePositionNow,self,_unit,true) MENU_GROUP_COMMAND:New(_group,"Drop beacon now",smoketopmenu,self.DropBeaconNow,self,_unit):Refresh() if self:IsFixedWing(_unit)then MENU_GROUP_COMMAND:New(_group,"Show flight parameters",topmenu,self._ShowFlightParams,self,_group,_unit):Refresh() else MENU_GROUP_COMMAND:New(_group,"Show hover parameters",topmenu,self._ShowHoverParams,self,_group,_unit):Refresh() end self.MenusDone[_unitName]=true self:_RefreshLoadCratesMenu(_group,_unit) self:_RefreshDropCratesMenu(_group,_unit) end end else self:T(self.lid.." Menus already done for this group!") end end return self end function CTLD:_RefreshLoadCratesMenu(Group,Unit) if not Group.MyLoadCratesMenu then return end Group.MyLoadCratesMenu:RemoveSubMenus() local d=self.CrateDistance or 35 local nearby,n=self:_FindCratesNearby(Group,Unit,d,true,true) if n==0 then MENU_GROUP_COMMAND:New(Group,"No crates found! Rescan?",Group.MyLoadCratesMenu,function()self:_RefreshLoadCratesMenu(Group,Unit)end) return end MENU_GROUP_COMMAND:New(Group,"Load ALL",Group.MyLoadCratesMenu,self._LoadCratesNearby,self,Group,Unit) local cargoByName={} for _,crate in pairs(nearby)do local cName=crate:GetName() cargoByName[cName]=cargoByName[cName]or{} table.insert(cargoByName[cName],crate) end for cName,cList in pairs(cargoByName)do local needed=cList[1]:GetCratesNeeded()or 1 local found=#cList local line if found>=needed then line=string.format("Load %s",cName) else MENU_GROUP_COMMAND:New(Group,"Rescan?",Group.MyLoadCratesMenu,function()self:_RefreshLoadCratesMenu(Group,Unit)end) line=string.format("Load %s (%d/%d)",cName,found,needed) end MENU_GROUP_COMMAND:New(Group,line,Group.MyLoadCratesMenu,self._LoadSingleCrateSet,self,Group,Unit,cName) end end function CTLD:_LoadSingleCrateSet(Group,Unit,cargoName) self:T(self.lid.." _LoadSingleCrateSet cargoName="..(cargoName or"nil")) local grounded=not self:IsUnitInAir(Unit) local hover=self:CanHoverLoad(Unit) if not grounded and not hover then self:_SendMessage("You must land or hover to load crates!",10,false,Group) return self end if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName())then self:_SendMessage("You need to open the door(s) to load cargo!",10,false,Group) return self end local finddist=self.CrateDistance or 35 local cratesNearby,number=self:_FindCratesNearby(Group,Unit,finddist,false,false) if number==0 then self:_SendMessage("No crates found in range!",10,false,Group) return self end local matchingCrates={} local needed=nil for _,crateObj in pairs(cratesNearby)do if crateObj:GetName()==cargoName then needed=needed or crateObj:GetCratesNeeded() table.insert(matchingCrates,crateObj) end end if not needed then self:_SendMessage(string.format("No \"%s\" crates found in range!",cargoName),10,false,Group) return self end local found=#matchingCrates local unitName=Unit:GetName() local loadedData=self.Loaded_Cargo[unitName]or{Troopsloaded=0,Cratesloaded=0,Cargo={}} local capabilities=self:_GetUnitCapabilities(Unit) local capacity=capabilities.cratelimit or 0 if loadedData.Cratesloaded>=capacity then self:_SendMessage("No more capacity to load crates!",10,false,Group) return self end local spaceLeft=capacity-loadedData.Cratesloaded local toLoad=math.min(found,needed,spaceLeft) if toLoad<1 then self:_SendMessage("Cannot load crates: either none found or no capacity left.",10,false,Group) return self end local crateIDsLoaded={} for i=1,toLoad do local crate=matchingCrates[i] crate:SetHasMoved(true) crate:SetWasDropped(false) table.insert(loadedData.Cargo,crate) loadedData.Cratesloaded=loadedData.Cratesloaded+1 local stObj=crate:GetPositionable() if stObj and stObj:IsAlive()then stObj:Destroy(false) end table.insert(crateIDsLoaded,crate:GetID()) end self.Loaded_Cargo[unitName]=loadedData self:_UpdateUnitCargoMass(Unit) local newSpawned={} for _,cObj in ipairs(self.Spawned_Cargo)do local keep=true for i=1,toLoad do if matchingCrates[i]and cObj:GetID()==matchingCrates[i]:GetID()then keep=false break end end if keep then table.insert(newSpawned,cObj) end end self.Spawned_Cargo=newSpawned local loadedHere=toLoad if loadedHere=capacity then self:_SendMessage(string.format("Loaded only %d/%d crate(s) of %s. Cargo limit is now reached!",loadedHere,needed,cargoName),10,false,Group) else local fullSets=math.floor(loadedHere/needed) local leftover=loadedHere%needed if needed>1 then if fullSets>0 and leftover==0 then self:_SendMessage(string.format("Loaded %d %s.",fullSets,cargoName),10,false,Group) elseif fullSets>0 and leftover>0 then self:_SendMessage(string.format("Loaded %d %s(s), with %d leftover crate(s).",fullSets,cargoName,leftover),10,false,Group) else self:_SendMessage(string.format("Loaded only %d/%d crate(s) of %s.",loadedHere,needed,cargoName),15,false,Group) end else self:_SendMessage(string.format("Loaded %d %s(s).",loadedHere,cargoName),10,false,Group) end end self:_RefreshLoadCratesMenu(Group,Unit) self:_RefreshDropCratesMenu(Group,Unit) return self end function CTLD:_UnloadSingleCrateSet(Group,Unit,setIndex) self:T(self.lid.." _UnloadSingleCrateSet") if not self.dropcratesanywhere then local inzone,zoneName,zone,distance=self:IsUnitInZone(Unit,CTLD.CargoZoneType.DROP) if not inzone then self:_SendMessage("You are not close enough to a drop zone!",10,false,Group) if not self.debug then return self end end end if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName())then self:_SendMessage("You need to open the door(s) to drop cargo!",10,false,Group) if not self.debug then return self end end local unitName=Unit:GetName() if not self.CrateGroupList or not self.CrateGroupList[unitName]then self:_SendMessage("No crate groups found for this unit!",10,false,Group) if not self.debug then return self end return self end local chunk=self.CrateGroupList[unitName][setIndex] if not chunk then self:_SendMessage("No crate set found or index invalid!",10,false,Group) if not self.debug then return self end return self end if#chunk==0 then self:_SendMessage("No crate found in that set!",10,false,Group) if not self.debug then return self end return self end local grounded=not self:IsUnitInAir(Unit) local hoverunload=self:IsCorrectHover(Unit) local isHerc=self:IsFixedWing(Unit) local isHook=self:IsHook(Unit) if isHerc and not isHook then hoverunload=self:IsCorrectFlightParameters(Unit) end if not grounded and not hoverunload then if isHerc then self:_SendMessage("Nothing loaded or not within airdrop parameters!",10,false,Group) else self:_SendMessage("Nothing loaded or not hovering within parameters!",10,false,Group) end if not self.debug then return self end return self end local crateObj=chunk[1] if not crateObj then self:_SendMessage("No crate found in that set!",10,false,Group) if not self.debug then return self end return self end local needed=crateObj:GetCratesNeeded()or 1 self:_GetCrates(Group,Unit,crateObj,#chunk,true) for _,cObj in ipairs(chunk)do cObj:SetWasDropped(true) cObj:SetHasMoved(true) end local loadedData=self.Loaded_Cargo[unitName] if loadedData and loadedData.Cargo then local newList={} local newCratesCount=0 for _,cObj in ipairs(loadedData.Cargo)do if not cObj:WasDropped()then table.insert(newList,cObj) local ct=cObj:GetType() if ct~=CTLD_CARGO.Enum.TROOPS and ct~=CTLD_CARGO.Enum.ENGINEERS then newCratesCount=newCratesCount+1 end end end loadedData.Cargo=newList loadedData.Cratesloaded=newCratesCount self.Loaded_Cargo[unitName]=loadedData end self:_UpdateUnitCargoMass(Unit) self:_RefreshDropCratesMenu(Group,Unit) self:_RefreshLoadCratesMenu(Group,Unit) return self end function CTLD:_RefreshDropCratesMenu(Group,Unit) if not Group.CTLDTopmenu then return end local topCrates=Group.MyTopCratesMenu if not topCrates then return end if topCrates.DropCratesMenu then topCrates.DropCratesMenu:RemoveSubMenus() else topCrates.DropCratesMenu=MENU_GROUP:New(Group,"Drop Crates",topCrates) end local dropCratesMenu=topCrates.DropCratesMenu local loadedData=self.Loaded_Cargo[Unit:GetName()] if not loadedData or not loadedData.Cargo then MENU_GROUP_COMMAND:New(Group,"No crates to drop!",dropCratesMenu,function()end) return end local cargoByName={} local dropableCrates=0 for _,cObj in ipairs(loadedData.Cargo)do if cObj and not cObj:WasDropped()then local cType=cObj:GetType() if cType~=CTLD_CARGO.Enum.TROOPS and cType~=CTLD_CARGO.Enum.ENGINEERS and cType~=CTLD_CARGO.Enum.GCLOADABLE then local name=cObj:GetName()or"Unknown" cargoByName[name]=cargoByName[name]or{} table.insert(cargoByName[name],cObj) dropableCrates=dropableCrates+1 end end end if dropableCrates==0 then MENU_GROUP_COMMAND:New(Group,"No crates to drop!",dropCratesMenu,function()end) return end MENU_GROUP_COMMAND:New(Group,"Drop ALL crates",dropCratesMenu,self._UnloadCrates,self,Group,Unit) self.CrateGroupList=self.CrateGroupList or{} self.CrateGroupList[Unit:GetName()]={} local lineIndex=1 for cName,list in pairs(cargoByName)do local needed=list[1]:GetCratesNeeded()or 1 table.sort(list,function(a,b)return a:GetID()=needed then local chunk={} for n=i,i+needed-1 do table.insert(chunk,list[n]) end local label=string.format("%d. %s",lineIndex,cName) table.insert(self.CrateGroupList[Unit:GetName()],chunk) local setIndex=#self.CrateGroupList[Unit:GetName()] MENU_GROUP_COMMAND:New(Group,label,dropCratesMenu,self._UnloadSingleCrateSet,self,Group,Unit,setIndex) i=i+needed else local chunk={} for n=i,#list do table.insert(chunk,list[n]) end local label=string.format("%d. %s %d/%d",lineIndex,cName,left,needed) table.insert(self.CrateGroupList[Unit:GetName()],chunk) local setIndex=#self.CrateGroupList[Unit:GetName()] MENU_GROUP_COMMAND:New(Group,label,dropCratesMenu,self._UnloadSingleCrateSet,self,Group,Unit,setIndex) i=#list+1 end lineIndex=lineIndex+1 end end end function CTLD:_UnloadSingleTroopByID(Group,Unit,chunkID) self:T(self.lid.." _UnloadSingleTroopByID chunkID="..tostring(chunkID)) local droppingatbase=false local inzone,zonename,zone,distance=self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) if not inzone then inzone,zonename,zone,distance=self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP) end if inzone then droppingatbase=true end if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName())then self:_SendMessage("You need to open the door(s) to unload troops!",10,false,Group) if not self.debug then return self end end local hoverunload=self:IsCorrectHover(Unit) local isHerc=self:IsFixedWing(Unit) local isHook=self:IsHook(Unit) if isHerc and not isHook then hoverunload=self:IsCorrectFlightParameters(Unit) end local grounded=not self:IsUnitInAir(Unit) local unitName=Unit:GetName() if self.Loaded_Cargo[unitName]and(grounded or hoverunload)then if not droppingatbase or self.debug then if not self.TroopsIDToChunk or not self.TroopsIDToChunk[chunkID]then self:_SendMessage(string.format("No troop cargo chunk found for ID %d!",chunkID),10,false,Group) if not self.debug then return self end return self end local chunk=self.TroopsIDToChunk[chunkID] if not chunk or#chunk==0 then self:_SendMessage(string.format("Troop chunk is empty for ID %d!",chunkID),10,false,Group) if not self.debug then return self end return self end local foundCargo=chunk[1] if not foundCargo then self:_SendMessage(string.format("No troop cargo at chunk %d!",chunkID),10,false,Group) if not self.debug then return self end return self end local cType=foundCargo:GetType() local name=foundCargo:GetName()or"none" local tmpl=foundCargo:GetTemplates()or{} local zoneradius=self.troopdropzoneradius or 100 local factor=1 if isHerc then factor=foundCargo:GetCratesNeeded()or 1 zoneradius=Unit:GetVelocityMPS()or 100 end local zone=ZONE_GROUP:New(string.format("Unload zone-%s",unitName),Group,zoneradius*factor) local randomcoord=zone:GetRandomCoordinate(10,30*factor) local heading=Group:GetHeading()or 0 if grounded or hoverunload then randomcoord=Group:GetCoordinate() local Angle=(heading+270)%360 if isHerc or isHook then Angle=(heading+180)%360 end local offset=hoverunload and self.TroopUnloadDistHover or self.TroopUnloadDistGround if isHerc then offset=self.TroopUnloadDistGroundHerc or 25 end if isHook then offset=self.TroopUnloadDistGroundHook or 15 if hoverunload and self.TroopUnloadDistHoverHook then offset=self.TroopUnloadDistHoverHook or 5 end end randomcoord:Translate(offset,Angle,nil,true) end local tempcount=0 if isHook then tempcount=self.ChinookTroopCircleRadius or 5 end for _,_template in pairs(tmpl)do self.TroopCounter=self.TroopCounter+1 tempcount=tempcount+1 local alias=string.format("%s-%d",_template,math.random(1,100000)) local rad=2.5+(tempcount*2) local Positions=self:_GetUnitPositions(randomcoord,rad,heading,_template) self.DroppedTroops[self.TroopCounter]=SPAWN:NewWithAlias(_template,alias) :InitDelayOff() :InitSetUnitAbsolutePositions(Positions) :OnSpawnGroup(function(grp)grp.spawntime=timer.getTime()end) :SpawnFromVec2(randomcoord:GetVec2()) self:__TroopsDeployed(1,Group,Unit,self.DroppedTroops[self.TroopCounter],cType) end foundCargo:SetWasDropped(true) if cType==CTLD_CARGO.Enum.ENGINEERS then self.Engineers=self.Engineers+1 self:_SendMessage(string.format("Dropped Engineers %s into action!",name),10,false,Group) else self:_SendMessage(string.format("Dropped Troops %s into action!",name),10,false,Group) end table.remove(chunk,1) if#chunk==0 then self.TroopsIDToChunk[chunkID]=nil end else self:_SendMessage("Troops have returned to base!",10,false,Group) self:__TroopsRTB(1,Group,Unit,zonename,zone) if self.TroopsIDToChunk and self.TroopsIDToChunk[chunkID]then local chunk=self.TroopsIDToChunk[chunkID] if#chunk>0 then local firstObj=chunk[1] local cName=firstObj:GetName() local gentroops=self.Cargo_Troops for _id,_troop in pairs(gentroops)do if _troop.Name==cName then local st=_troop:GetStock() if st and tonumber(st)>=0 then _troop:AddStock() end end end firstObj:SetWasDropped(true) table.remove(chunk,1) if#chunk==0 then self.TroopsIDToChunk[chunkID]=nil end end end end local cargoList=self.Loaded_Cargo[unitName].Cargo for i=#cargoList,1,-1 do if cargoList[i]:WasDropped()then table.remove(cargoList,i) end end local troopsLoaded=0 local cratesLoaded=0 for _,cargo in ipairs(cargoList)do local cT=cargo:GetType() if cT==CTLD_CARGO.Enum.TROOPS or cT==CTLD_CARGO.Enum.ENGINEERS then troopsLoaded=troopsLoaded+1 else cratesLoaded=cratesLoaded+1 end end self.Loaded_Cargo[unitName].Troopsloaded=troopsLoaded self.Loaded_Cargo[unitName].Cratesloaded=cratesLoaded self:_RefreshDropTroopsMenu(Group,Unit) else local isHerc=self:IsFixedWing(Unit) if isHerc then self:_SendMessage("Nothing loaded or not within airdrop parameters!",10,false,Group) else self:_SendMessage("Nothing loaded or not hovering within parameters!",10,false,Group) end end return self end function CTLD:_RefreshDropTroopsMenu(Group,Unit) local theGroup=Group local theUnit=Unit if not theGroup.CTLDTopmenu then return end local topTroops=theGroup.MyTopTroopsMenu if not topTroops then return end if topTroops.DropTroopsMenu then topTroops.DropTroopsMenu:Remove() end local dropTroopsMenu=MENU_GROUP:New(theGroup,"Drop Troops",topTroops) topTroops.DropTroopsMenu=dropTroopsMenu MENU_GROUP_COMMAND:New(theGroup,"Drop ALL troops",dropTroopsMenu,self._UnloadTroops,self,theGroup,theUnit) local loadedData=self.Loaded_Cargo[theUnit:GetName()] if not loadedData or not loadedData.Cargo then return end local troopsByName={} for _,cargoObj in ipairs(loadedData.Cargo)do if cargoObj and(cargoObj:GetType()==CTLD_CARGO.Enum.TROOPS or cargoObj:GetType()==CTLD_CARGO.Enum.ENGINEERS) and not cargoObj:WasDropped() then local name=cargoObj:GetName()or"Unknown" troopsByName[name]=troopsByName[name]or{} table.insert(troopsByName[name],cargoObj) end end self.TroopsIDToChunk=self.TroopsIDToChunk or{} for tName,objList in pairs(troopsByName)do table.sort(objList,function(a,b)return a:GetID()timeout then local name=beacon.name self.droppedbeaconref[name]=nil _beacon=nil else table.insert(livebeacontable,beacon) end end self.droppedBeacons=nil self.droppedBeacons=livebeacontable return self end function CTLD:_ListRadioBeacons(Group,Unit) self:T(self.lid.." _ListRadioBeacons") local report=REPORT:New("Active Zone Beacons") report:Add("------------------------------------------------------------") local zones={[1]=self.pickupZones,[2]=self.wpZones,[3]=self.dropOffZones,[4]=self.shipZones,[5]=self.droppedBeacons} for i=1,5 do for index,cargozone in pairs(zones[i])do local czone=cargozone if czone.active and czone.hasbeacon then local FMbeacon=czone.fmbeacon local VHFbeacon=czone.vhfbeacon local UHFbeacon=czone.uhfbeacon local Name=czone.name local FM=FMbeacon.frequency local VHF=VHFbeacon.frequency*1000 local UHF=UHFbeacon.frequency report:AddIndent(string.format(" %s | FM %s Mhz | VHF %s KHz | UHF %s Mhz ",Name,FM,VHF,UHF),"|") end end end if report:GetCount()==1 then report:Add(" N O N E") end report:Add("------------------------------------------------------------") self:_SendMessage(report:Text(),30,true,Group) return self end function CTLD:_AddRadioBeacon(Name,Sound,Mhz,Modulation,IsShip,IsDropped) self:T(self.lid.." _AddRadioBeacon") local Zone=nil if IsShip then Zone=UNIT:FindByName(Name) elseif IsDropped then Zone=self.droppedbeaconref[Name] else Zone=ZONE:FindByName(Name) if not Zone then Zone=AIRBASE:FindByName(Name):GetZone() end end local Sound=Sound or"beacon.ogg" if Zone then if IsDropped then local ZoneCoord=Zone local ZoneVec3=ZoneCoord:GetVec3()or{x=0,y=0,z=0} local Frequency=Mhz*1000000 local Sound=self.RadioPath..Sound trigger.action.radioTransmission(Sound,ZoneVec3,Modulation,false,Frequency,1000,Name..math.random(1,10000)) self:T2(string.format("Beacon added | Name = %s | Sound = %s | Vec3 = %d %d %d | Freq = %f | Modulation = %d (0=AM/1=FM)",Name,Sound,ZoneVec3.x,ZoneVec3.y,ZoneVec3.z,Mhz,Modulation)) else local ZoneCoord=Zone:GetCoordinate() local ZoneVec3=ZoneCoord:GetVec3()or{x=0,y=0,z=0} local Frequency=Mhz*1000000 local Sound=self.RadioPath..Sound trigger.action.radioTransmission(Sound,ZoneVec3,Modulation,false,Frequency,1000,Name..math.random(1,10000)) self:T2(string.format("Beacon added | Name = %s | Sound = %s | Vec3 = {x=%d, y=%d, z=%d} | Freq = %f | Modulation = %d (0=AM/1=FM)",Name,Sound,ZoneVec3.x,ZoneVec3.y,ZoneVec3.z,Mhz,Modulation)) end else self:E(self.lid.."***** _AddRadioBeacon: Zone does not exist: "..Name) end return self end function CTLD:SetSoundfilesFolder(FolderPath) self:T(self.lid.." SetSoundfilesFolder") if FolderPath then local lastchar=string.sub(FolderPath,-1) if lastchar~="/"then FolderPath=FolderPath.."/" end end self.RadioPath=FolderPath self:I(self.lid..string.format("Setting sound files folder to: %s",self.RadioPath)) return self end function CTLD:_RefreshRadioBeacons() self:T(self.lid.." _RefreshRadioBeacons") local zones={[1]=self.pickupZones,[2]=self.wpZones,[3]=self.dropOffZones,[4]=self.shipZones,[5]=self.droppedBeacons} for i=1,5 do local IsShip=false if i==4 then IsShip=true end local IsDropped=false if i==5 then IsDropped=true end for index,cargozone in pairs(zones[i])do local czone=cargozone local Sound=self.RadioSound local Silent=self.RadioSoundFC3 or self.RadioSound if czone.active and czone.hasbeacon then local FMbeacon=czone.fmbeacon local VHFbeacon=czone.vhfbeacon local UHFbeacon=czone.uhfbeacon local Name=czone.name local FM=FMbeacon.frequency local VHF=VHFbeacon.frequency local UHF=UHFbeacon.frequency self:_AddRadioBeacon(Name,Sound,FM,CTLD.RadioModulation.FM,IsShip,IsDropped) self:_AddRadioBeacon(Name,Sound,VHF,CTLD.RadioModulation.AM,IsShip,IsDropped) self:_AddRadioBeacon(Name,Silent,UHF,CTLD.RadioModulation.AM,IsShip,IsDropped) end end end return self end function CTLD:IsUnitInZone(Unit,Zonetype) self:T(self.lid.." IsUnitInZone") self:T(Zonetype) local unitname=Unit:GetName() local zonetable={} local outcome=false if Zonetype==CTLD.CargoZoneType.LOAD then zonetable=self.pickupZones elseif Zonetype==CTLD.CargoZoneType.DROP then zonetable=self.dropOffZones elseif Zonetype==CTLD.CargoZoneType.SHIP then zonetable=self.shipZones else zonetable=self.wpZones end local zonecoord=nil local colorret=nil local maxdist=1000000 local zoneret=nil local zonewret=nil local zonenameret=nil local unitcoord=Unit:GetCoordinate() local unitVec2=unitcoord:GetVec2() for _,_cargozone in pairs(zonetable)do local czone=_cargozone local zonename=czone.name local active=czone.active local color=czone.color local zone=nil local zoneradius=100 local zonewidth=20 if Zonetype==CTLD.CargoZoneType.SHIP then self:T("Checking Type Ship: "..zonename) local ZoneUNIT=UNIT:FindByName(zonename) zonecoord=ZoneUNIT:GetCoordinate() zoneradius=czone.shiplength zonewidth=czone.shipwidth zone=ZONE_UNIT:New(ZoneUNIT:GetName(),ZoneUNIT,zoneradius/2) elseif ZONE:FindByName(zonename)then zone=ZONE:FindByName(zonename) self:T("Checking Zone: "..zonename) zonecoord=zone:GetCoordinate() zonewidth=zoneradius elseif AIRBASE:FindByName(zonename)then zone=AIRBASE:FindByName(zonename):GetZone() self:T("Checking Zone: "..zonename) zonecoord=zone:GetCoordinate() zoneradius=2000 zonewidth=zoneradius end local distance=self:_GetDistance(zonecoord,unitcoord) self:T("Distance Zone: "..distance) if(zone:IsVec2InZone(unitVec2)or Zonetype==CTLD.CargoZoneType.MOVE)and active==true and maxdist>distance then outcome=true maxdist=distance zoneret=zone zonenameret=zonename zonewret=zonewidth colorret=color end end if Zonetype==CTLD.CargoZoneType.SHIP then return outcome,zonenameret,zoneret,maxdist,zonewret else return outcome,zonenameret,zoneret,maxdist end end function CTLD:SmokePositionNow(Unit,Flare,SmokeColor) self:T(self.lid.." SmokePositionNow") local Smokecolor=self.SmokeColor or SMOKECOLOR.Red if SmokeColor then Smokecolor=SmokeColor end local FlareColor=self.FlareColor or FLARECOLOR.Red local unitcoord=Unit:GetCoordinate() local Group=Unit:GetGroup() if Flare then unitcoord:Flare(FlareColor,90) else local height=unitcoord:GetLandHeight()+2 unitcoord.y=height unitcoord:Smoke(Smokecolor) end return self end function CTLD:SmokeZoneNearBy(Unit,Flare) self:T(self.lid.." SmokeZoneNearBy") local unitcoord=Unit:GetCoordinate() local Group=Unit:GetGroup() local smokedistance=self.smokedistance local smoked=false local zones={[1]=self.pickupZones,[2]=self.wpZones,[3]=self.dropOffZones,[4]=self.shipZones} for i=1,4 do for index,cargozone in pairs(zones[i])do local CZone=cargozone local zonename=CZone.name local zone=nil if i==4 then zone=UNIT:FindByName(zonename) else zone=ZONE:FindByName(zonename) if not zone then zone=AIRBASE:FindByName(zonename):GetZone() end end local zonecoord=zone:GetCoordinate() local active=CZone.active local color=CZone.color local distance=self:_GetDistance(zonecoord,unitcoord) if distance=minh)then outcome=true end end return outcome end function CTLD:IsCorrectFlightParameters(Unit) self:T(self.lid.." IsCorrectFlightParameters") local outcome=false if self:IsUnitInAir(Unit)then local uspeed=Unit:GetVelocityMPS() local uheight=Unit:GetHeight() local ucoord=Unit:GetCoordinate() if not ucoord then return false end local gheight=ucoord:GetLandHeight() local aheight=uheight-gheight local minh=self.FixedMinAngels local maxh=self.FixedMaxAngels local maxspeed=self.FixedMaxSpeed local kmspeed=uspeed*3.6 local knspeed=kmspeed/1.86 self:T(string.format("%s Unit parameters: at %dm AGL with %dmps | %dkph | %dkn",self.lid,aheight,uspeed,kmspeed,knspeed)) if(aheight<=maxh)and(aheight>=minh)and(uspeed<=maxspeed)then outcome=true end end return outcome end function CTLD:_ShowHoverParams(Group,Unit) local inhover=self:IsCorrectHover(Unit) local htxt="true" if not inhover then htxt="false"end local text="" if _SETTINGS:IsMetric()then text=string.format("Hover parameters (autoload/drop):\n - Min height %dm \n - Max height %dm \n - Max speed 2mps \n - In parameter: %s",self.minimumHoverHeight,self.maximumHoverHeight,htxt) else local minheight=UTILS.MetersToFeet(self.minimumHoverHeight) local maxheight=UTILS.MetersToFeet(self.maximumHoverHeight) text=string.format("Hover parameters (autoload/drop):\n - Min height %dft \n - Max height %dft \n - Max speed 6ftps \n - In parameter: %s",minheight,maxheight,htxt) end self:_SendMessage(text,10,false,Group) return self end function CTLD:_ShowFlightParams(Group,Unit) local inhover=self:IsCorrectFlightParameters(Unit) local htxt="true" if not inhover then htxt="false"end local text="" if _SETTINGS:IsImperial()then local minheight=UTILS.MetersToFeet(self.FixedMinAngels) local maxheight=UTILS.MetersToFeet(self.FixedMaxAngels) text=string.format("Flight parameters (airdrop):\n - Min height %dft \n - Max height %dft \n - In parameter: %s",minheight,maxheight,htxt) else local minheight=self.FixedMinAngels local maxheight=self.FixedMaxAngels text=string.format("Flight parameters (airdrop):\n - Min height %dm \n - Max height %dm \n - In parameter: %s",minheight,maxheight,htxt) end self:_SendMessage(text,10,false,Group) return self end function CTLD:CanHoverLoad(Unit) self:T(self.lid.." CanHoverLoad") if self:IsFixedWing(Unit)then return false end local outcome=self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD)and self:IsCorrectHover(Unit) if not outcome then outcome=self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP) end return outcome end function CTLD:IsUnitInAir(Unit) local minheight=self.minimumHoverHeight if self.enableFixedWing and self:IsFixedWing(Unit)then minheight=5.1 end local uheight=Unit:GetHeight() local ucoord=Unit:GetCoordinate() if not ucoord then return false end local gheight=ucoord:GetLandHeight() local aheight=uheight-gheight if aheight>=minheight then return true else return false end end function CTLD:AutoHoverLoad(Unit) self:T(self.lid.." AutoHoverLoad") local unittype=Unit:GetTypeName() local unitname=Unit:GetName() local Group=Unit:GetGroup() local capabilities=self:_GetUnitCapabilities(Unit) local cancrates=capabilities.crates local cratelimit=capabilities.cratelimit if cancrates then local numberonboard=0 local loaded={} if self.Loaded_Cargo[unitname]then loaded=self.Loaded_Cargo[unitname] numberonboard=loaded.Cratesloaded or 0 end local load=cratelimit-numberonboard local canload=self:CanHoverLoad(Unit) if canload and load>0 then self:_LoadCratesNearby(Group,Unit) end end return self end function CTLD:CheckAutoHoverload() if self.hoverautoloading then for _,_pilot in pairs(self.CtldUnits)do local Unit=UNIT:FindByName(_pilot) if self:CanHoverLoad(Unit)then self:AutoHoverLoad(Unit)end end end return self end function CTLD:CleanDroppedTroops() local troops=self.DroppedTroops local newtable={} for _index,_group in pairs(troops)do self:T({_group.ClassName}) if _group and _group.ClassName=="GROUP"then if _group:IsAlive()then newtable[_index]=_group end end end self.DroppedTroops=newtable local engineers=self.EngineersInField local engtable={} for _index,_group in pairs(engineers)do self:T({_group.ClassName}) if _group and _group:IsNotStatus("Stopped")then engtable[_index]=_group end end self.EngineersInField=engtable return self end function CTLD:_CountStockPlusInHeloPlusAliveGroups(Restock,Threshold) local Troopstable={} for _id,_cargo in pairs(self.Cargo_Crates)do local generic=_cargo local genname=generic:GetName() if generic and generic:GetStock0()>0 and not Troopstable[genname]then Troopstable[genname]={ Stock0=generic:GetStock0(), Stock=generic:GetStock(), StockR=generic:GetRelativeStock(), Infield=0, Inhelo=0, CratesInfield=0, Sum=generic:GetStock(), } if Restock==true then Troopstable[genname].GenericCargo=generic end end end for _id,_cargo in pairs(self.Cargo_Troops)do local generic=_cargo local genname=generic:GetName() if generic and generic:GetStock0()>0 and not Troopstable[genname]then Troopstable[genname]={ Stock0=generic:GetStock0(), Stock=generic:GetStock(), StockR=generic:GetRelativeStock(), Infield=0, Inhelo=0, CratesInfield=0, Sum=generic:GetStock(), } if Restock==true then Troopstable[genname].GenericCargo=generic end end end for _index,_group in pairs(self.DroppedTroops)do if _group and _group:IsAlive()then self:T("Looking at ".._group:GetName().." in the field") local generic=self:GetGenericCargoObjectFromGroupName(_group:GetName()) if generic then local genname=generic:GetName() self:T("Found Generic "..genname.." in the field. Adding.") if generic:GetStock0()>0 then Troopstable[genname].Infield=Troopstable[genname].Infield+1 Troopstable[genname].Sum=Troopstable[genname].Infield+Troopstable[genname].Stock+Troopstable[genname].Inhelo end else self:E(self.lid.."Group without Cargo Generic: ".._group:GetName()) end end end for _unitname,_loaded in pairs(self.Loaded_Cargo)do local _unit=UNIT:FindByName(_unitname) if _unit and _unit:IsAlive()then local unitname=_unit:GetName() local loadedcargo=self.Loaded_Cargo[unitname].Cargo or{} for _,_cgo in pairs(loadedcargo)do local cargo=_cgo local type=cargo.CargoType local gname=cargo.Name local gcargo=self:_FindCratesCargoObject(gname)or self:_FindTroopsCargoObject(gname) self:T("Looking at "..gname.." in the helo - type = "..type) if(type==CTLD_CARGO.Enum.TROOPS or type==CTLD_CARGO.Enum.ENGINEERS or type==CTLD_CARGO.Enum.VEHICLE or type==CTLD_CARGO.Enum.FOB)then if gcargo and gcargo:GetStock0()>0 then self:T("Adding "..gname.." in the helo - type = "..type) if(type==CTLD_CARGO.Enum.TROOPS or type==CTLD_CARGO.Enum.ENGINEERS)then Troopstable[gname].Inhelo=Troopstable[gname].Inhelo+1 end if(type==CTLD_CARGO.Enum.VEHICLE or type==CTLD_CARGO.Enum.FOB)then local counting=gcargo.CratesNeeded local added=1 if counting>1 then added=added/counting end Troopstable[gname].Inhelo=Troopstable[gname].Inhelo+added end Troopstable[gname].Sum=Troopstable[gname].Infield+Troopstable[gname].Stock+Troopstable[gname].Inhelo+Troopstable[gname].CratesInfield end end end end end if self.Spawned_Cargo then for i=#self.Spawned_Cargo,1,-1 do local cargo=self.Spawned_Cargo[i] if cargo and cargo:GetPositionable()and cargo:GetPositionable():IsAlive()then local genname=cargo:GetName() local gcargo=self:_FindCratesCargoObject(genname) if Troopstable[genname]and gcargo and gcargo:GetStock0()>0 then local needed=gcargo.CratesNeeded or 1 local added=1 if needed>1 then added=added/needed end Troopstable[genname].CratesInfield=Troopstable[genname].CratesInfield+added Troopstable[genname].Sum=Troopstable[genname].Infield+Troopstable[genname].Stock +Troopstable[genname].Inhelo+Troopstable[genname].CratesInfield end end end for i=#self.Spawned_Cargo,1,-1 do local cargo=self.Spawned_Cargo[i] if cargo and cargo:GetPositionable()and cargo:GetPositionable():IsAlive()then local genname=cargo:GetName() if Troopstable[genname]then if Troopstable[genname].Inhelo==0 and Troopstable[genname].CratesInfield<1 then Troopstable[genname].CratesInfield=0 Troopstable[genname].Sum=Troopstable[genname].Stock cargo:GetPositionable():Destroy(false) table.remove(self.Spawned_Cargo,i) local leftover=Troopstable[genname].Stock0-(Troopstable[genname].Infield+Troopstable[genname].Inhelo+Troopstable[genname].CratesInfield) if leftover0 then local text=string.format("%s Pilots %d | Live Crates %d |\nCargo Counter %d | Troop Counter %d",self.lid,pilots,boxes,cc,tc) local m=MESSAGE:New(text,10,"CTLD"):ToAll() if self.verbose>0 then self:I(self.lid.."Cargo and Troops in Stock:") for _,_troop in pairs(self.Cargo_Crates)do local name=_troop:GetName() local stock=_troop:GetStock() self:I(string.format("-- %s \t\t\t %d",name,stock)) end for _,_troop in pairs(self.Cargo_Statics)do local name=_troop:GetName() local stock=_troop:GetStock() self:I(string.format("-- %s \t\t\t %d",name,stock)) end for _,_troop in pairs(self.Cargo_Troops)do local name=_troop:GetName() local stock=_troop:GetStock() self:I(string.format("-- %s \t\t %d",name,stock)) end end end self:__Status(-30) return self end function CTLD:onafterStop(From,Event,To) self:T({From,Event,To}) self:UnHandleEvent(EVENTS.PlayerEnterAircraft) self:UnHandleEvent(EVENTS.PlayerEnterUnit) self:UnHandleEvent(EVENTS.PlayerLeaveUnit) self:UnHandleEvent(EVENTS.UnitLost) self:UnHandleEvent(EVENTS.Shot) return self end function CTLD:onbeforeTroopsPickedUp(From,Event,To,Group,Unit,Cargo) self:T({From,Event,To}) return self end function CTLD:onbeforeCratesPickedUp(From,Event,To,Group,Unit,Cargo) self:T({From,Event,To}) return self end function CTLD:onbeforeTroopsExtracted(From,Event,To,Group,Unit,Troops,Groupname) self:T({From,Event,To}) if Unit and Unit:IsPlayer()and self.PlayerTaskQueue then local playername=Unit:GetPlayerName() self.PlayerTaskQueue:ForEach( function(Task) local task=Task local subtype=task:GetSubType() if Event==subtype and not task:IsDone()then local targetzone=task.Target:GetObject() local okaygroup=string.find(Groupname,task:GetProperty("ExtractName"),1,true) if targetzone and targetzone.ClassName and string.match(targetzone.ClassName,"ZONE")and okaygroup then if task.Clients:HasUniqueID(playername)then task:__Success(-1) end end end end ) end return self end function CTLD:onbeforeTroopsDeployed(From,Event,To,Group,Unit,Troops) self:T({From,Event,To}) if Unit and Unit:IsPlayer()and self.PlayerTaskQueue then local playername=Unit:GetPlayerName() local dropcoord=Troops:GetCoordinate()or COORDINATE:New(0,0,0) local dropvec2=dropcoord:GetVec2() self.PlayerTaskQueue:ForEach( function(Task) local task=Task local subtype=task:GetSubType() if Event==subtype and not task:IsDone()then local targetzone=task.Target:GetObject() if targetzone and targetzone.ClassName and string.match(targetzone.ClassName,"ZONE")and targetzone:IsVec2InZone(dropvec2)then if task.Clients:HasUniqueID(playername)then task:__Success(-1) end end end end ) end return self end function CTLD:onafterTroopsDeployed(From,Event,To,Group,Unit,Troops,Type) self:T({From,Event,To}) if self.movetroopstowpzone and Type~=CTLD_CARGO.Enum.ENGINEERS then self:_MoveGroupToZone(Troops) end return self end function CTLD:onbeforeCratesDropped(From,Event,To,Group,Unit,Cargotable) self:T({From,Event,To}) return self end function CTLD:onbeforeCratesBuild(From,Event,To,Group,Unit,Vehicle) self:T({From,Event,To}) if Unit and Unit:IsPlayer()and self.PlayerTaskQueue then local playername=Unit:GetPlayerName() local dropcoord=Vehicle:GetCoordinate()or COORDINATE:New(0,0,0) local dropvec2=dropcoord:GetVec2() self.PlayerTaskQueue:ForEach( function(Task) local task=Task local subtype=task:GetSubType() if Event==subtype and not task:IsDone()then local targetzone=task.Target:GetObject() if targetzone and targetzone.ClassName and string.match(targetzone.ClassName,"ZONE")and targetzone:IsVec2InZone(dropvec2)then if task.Clients:HasUniqueID(playername)then task:__Success(-1) end end end end ) end return self end function CTLD:onafterCratesBuild(From,Event,To,Group,Unit,Vehicle) self:T({From,Event,To}) if self.movetroopstowpzone then self:_MoveGroupToZone(Vehicle) end return self end function CTLD:onbeforeTroopsRTB(From,Event,To,Group,Unit,ZoneName,ZoneObject) self:T({From,Event,To}) return self end function CTLD:onbeforeSave(From,Event,To,path,filename) self:T({From,Event,To,path,filename}) if not self.enableLoadSave then return self end if not io then self:E(self.lid.."ERROR: io not desanitized. Can't save current state.") return false end if path==nil and not lfs then self:E(self.lid.."WARNING: lfs not desanitized. State will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.") end return true end function CTLD:onafterSave(From,Event,To,path,filename) self:T({From,Event,To,path,filename}) if not self.enableLoadSave then return self end local function _savefile(filename,data) local f=assert(io.open(filename,"wb")) f:write(data) f:close() end if lfs then path=self.filepath or lfs.writedir() end filename=filename or self.filename if path~=nil then filename=path.."\\"..filename end local grouptable=self.DroppedTroops local cgovehic=self.Cargo_Crates local cgotable=self.Cargo_Troops local stcstable=self.Spawned_Cargo local statics=nil local statics={} self:T(self.lid.."Building Statics Table for Saving") for _,_cargo in pairs(stcstable)do local cargo=_cargo local object=cargo:GetPositionable() if object and object:IsAlive()and(cargo:WasDropped()or not cargo:HasMoved())then statics[#statics+1]=cargo end end local function FindCargoType(name,table) local match=false local cargo=nil name=string.gsub(name,"-"," ") for _ind,_cargo in pairs(table)do local thiscargo=_cargo local template=thiscargo:GetTemplates() if type(template)=="string"then template={template} end for _,_name in pairs(template)do _name=string.gsub(_name,"-"," ") if string.find(name,_name)and _cargo:GetType()~=CTLD_CARGO.Enum.REPAIR then match=true cargo=thiscargo end end if match then break end end return match,cargo end local data="Group,x,y,z,CargoName,CargoTemplates,CargoType,CratesNeeded,CrateMass,Structure,StaticCategory,StaticType,StaticShape,SpawnTime\n" local n=0 for _,_grp in pairs(grouptable)do local group=_grp if group and group:IsAlive()then local name=group:GetName() local template=name if string.find(template,"#")then template=string.gsub(name,"#(%d+)$","") end local template=string.gsub(name,"-(%d+)$","") local match,cargo=FindCargoType(template,cgotable) if not match then match,cargo=FindCargoType(template,cgovehic) end if match then n=n+1 local cargo=cargo local cgoname=cargo.Name local cgotemp=cargo.Templates local cgotype=cargo.CargoType local cgoneed=cargo.CratesNeeded local cgomass=cargo.PerCrateMass local scat,stype,sshape=cargo:GetStaticTypeAndShape() local structure=UTILS.GetCountPerTypeName(group) local strucdata="" for typen,anzahl in pairs(structure)do strucdata=strucdata..typen.."=="..anzahl..";" end local spawntime=group.spawntime or timer.getTime()+n if type(cgotemp)=="table"then local templates="{" for _,_tmpl in pairs(cgotemp)do templates=templates.._tmpl..";" end templates=templates.."}" cgotemp=templates end local location=group:GetVec3() local txt=string.format("%s,%d,%d,%d,%s,%s,%s,%d,%d,%s,%s,%s,%s,%f\n" ,template,location.x,location.y,location.z,cgoname,cgotemp,cgotype,cgoneed,cgomass,strucdata,scat,stype,sshape or"none",spawntime) data=data..txt end end end for _,_cgo in pairs(statics)do local object=_cgo local cgoname=object.Name local cgotemp=object.Templates if type(cgotemp)=="table"then local templates="{" for _,_tmpl in pairs(cgotemp)do templates=templates.._tmpl..";" end templates=templates.."}" cgotemp=templates end local cgotype=object.CargoType local cgoneed=object.CratesNeeded local cgomass=object.PerCrateMass local crateobj=object.Positionable local location=crateobj:GetVec3() local scat,stype,sshape=object:GetStaticTypeAndShape() local txt=string.format("%s,%d,%d,%d,%s,%s,%s,%d,%d,'none',%s,%s,%s\n" ,"STATIC",location.x,location.y,location.z,cgoname,cgotemp,cgotype,cgoneed,cgomass,scat,stype,sshape or"none") data=data..txt end _savefile(filename,data) if self.enableLoadSave then local interval=self.saveinterval local filename=self.filename local filepath=self.filepath self:__Save(interval,filepath,filename) end return self end function CTLD:onbeforeLoad(From,Event,To,path,filename) self:T({From,Event,To,path,filename}) if not self.enableLoadSave then return self end 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 self.filename path=path or self.filepath if not io then self:E(self.lid.."WARNING: io not desanitized. Cannot load file.") return false end if path==nil and not lfs then self:E(self.lid.."WARNING: lfs not desanitized. State will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.") end if lfs then path=path or lfs.writedir() end 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: State file %s might not exist.",filename)) return false end end function CTLD:onafterLoad(From,Event,To,path,filename) self:T({From,Event,To,path,filename}) if not self.enableLoadSave then return self end local function _loadfile(filename) local f=assert(io.open(filename,"rb")) local data=f:read("*all") f:close() return data end filename=filename or self.filename path=path or self.filepath if lfs then path=path or lfs.writedir() end if path~=nil then filename=path.."\\"..filename end local text=string.format("Loading CTLD state from file %s",filename) MESSAGE:New(text,10):ToAllIf(self.Debug) self:I(self.lid..text) local file=assert(io.open(filename,"rb")) local loadeddata={} for line in file:lines()do loadeddata[#loadeddata+1]=line end file:close() table.remove(loadeddata,1) local n=0 for _id,_entry in pairs(loadeddata)do local dataset=UTILS.Split(_entry,",") local groupname=dataset[1] local vec2={} vec2.x=tonumber(dataset[2]) vec2.y=tonumber(dataset[4]) local cargoname=dataset[5] local cargotemplates=dataset[6] local cargotype=dataset[7] local size=tonumber(dataset[8]) local mass=tonumber(dataset[9]) local StaticCategory=dataset[11] local StaticType=dataset[12] local StaticShape=dataset[13] n=n+1 local timestamp=tonumber(dataset[14])or(timer.getTime()+n) self:T2("TimeStamp = "..timestamp) if type(groupname)=="string"and groupname~="STATIC"then cargotemplates=string.gsub(cargotemplates,"{","") cargotemplates=string.gsub(cargotemplates,"}","") cargotemplates=UTILS.Split(cargotemplates,";") local structure=nil if dataset[10]and dataset[10]~="none"then structure=dataset[10] structure=string.gsub(structure,",","") end local dropzone=ZONE_RADIUS:New("DropZone",vec2,20) if cargotype==CTLD_CARGO.Enum.VEHICLE or cargotype==CTLD_CARGO.Enum.FOB then local injectvehicle=CTLD_CARGO:New(nil,cargoname,cargotemplates,cargotype,true,true,size,nil,true,mass) injectvehicle:SetStaticTypeAndShape(StaticCategory,StaticType,StaticShape) self:InjectVehicles(dropzone,injectvehicle,self.surfacetypes,self.useprecisecoordloads,structure,timestamp) elseif cargotype==CTLD_CARGO.Enum.TROOPS or cargotype==CTLD_CARGO.Enum.ENGINEERS then local injecttroops=CTLD_CARGO:New(nil,cargoname,cargotemplates,cargotype,true,true,size,nil,true,mass) self:InjectTroops(dropzone,injecttroops,self.surfacetypes,self.useprecisecoordloads,structure,timestamp) end elseif(type(groupname)=="string"and groupname=="STATIC")or cargotype==CTLD_CARGO.Enum.REPAIR then local dropzone=ZONE_RADIUS:New("DropZone",vec2,20) local injectstatic=nil if cargotype==CTLD_CARGO.Enum.VEHICLE or cargotype==CTLD_CARGO.Enum.FOB then cargotemplates=string.gsub(cargotemplates,"{","") cargotemplates=string.gsub(cargotemplates,"}","") cargotemplates=UTILS.Split(cargotemplates,";") injectstatic=CTLD_CARGO:New(nil,cargoname,cargotemplates,cargotype,true,true,size,nil,true,mass) injectstatic:SetStaticTypeAndShape(StaticCategory,StaticType,StaticShape) elseif cargotype==CTLD_CARGO.Enum.STATIC or cargotype==CTLD_CARGO.Enum.REPAIR then injectstatic=CTLD_CARGO:New(nil,cargoname,cargotemplates,cargotype,true,true,size,nil,true,mass) injectstatic:SetStaticTypeAndShape(StaticCategory,StaticType,StaticShape) local map=cargotype:GetStaticResourceMap() injectstatic:SetStaticResourceMap(map) end if injectstatic then self:InjectStatics(dropzone,injectstatic,false,true) end end end if self.keeploadtable then self:__Loaded(1,self.LoadedGroupsTable) end return self end end do CTLD_HERCULES={ ClassName="CTLD_HERCULES", lid="", Name="", Version="0.0.3", } CTLD_HERCULES.Types={ ["ATGM M1045 HMMWV TOW Air [7183lb]"]={['name']="M1045 HMMWV TOW",['container']=true}, ["ATGM M1045 HMMWV TOW Skid [7073lb]"]={['name']="M1045 HMMWV TOW",['container']=false}, ["APC M1043 HMMWV Armament Air [7023lb]"]={['name']="M1043 HMMWV Armament",['container']=true}, ["APC M1043 HMMWV Armament Skid [6912lb]"]={['name']="M1043 HMMWV Armament",['container']=false}, ["SAM Avenger M1097 Air [7200lb]"]={['name']="M1097 Avenger",['container']=true}, ["SAM Avenger M1097 Skid [7090lb]"]={['name']="M1097 Avenger",['container']=false}, ["APC Cobra Air [10912lb]"]={['name']="Cobra",['container']=true}, ["APC Cobra Skid [10802lb]"]={['name']="Cobra",['container']=false}, ["APC M113 Air [21624lb]"]={['name']="M-113",['container']=true}, ["APC M113 Skid [21494lb]"]={['name']="M-113",['container']=false}, ["Tanker M978 HEMTT [34000lb]"]={['name']="M978 HEMTT Tanker",['container']=false}, ["HEMTT TFFT [34400lb]"]={['name']="HEMTT TFFT",['container']=false}, ["SPG M1128 Stryker MGS [33036lb]"]={['name']="M1128 Stryker MGS",['container']=false}, ["AAA Vulcan M163 Air [21666lb]"]={['name']="Vulcan",['container']=true}, ["AAA Vulcan M163 Skid [21577lb]"]={['name']="Vulcan",['container']=false}, ["APC M1126 Stryker ICV [29542lb]"]={['name']="M1126 Stryker ICV",['container']=false}, ["ATGM M1134 Stryker [30337lb]"]={['name']="M1134 Stryker ATGM",['container']=false}, ["APC LAV-25 Air [22520lb]"]={['name']="LAV-25",['container']=true}, ["APC LAV-25 Skid [22514lb]"]={['name']="LAV-25",['container']=false}, ["M1025 HMMWV Air [6160lb]"]={['name']="Hummer",['container']=true}, ["M1025 HMMWV Skid [6050lb]"]={['name']="Hummer",['container']=false}, ["IFV M2A2 Bradley [34720lb]"]={['name']="M-2 Bradley",['container']=false}, ["IFV MCV-80 [34720lb]"]={['name']="MCV-80",['container']=false}, ["IFV BMP-1 [23232lb]"]={['name']="BMP-1",['container']=false}, ["IFV BMP-2 [25168lb]"]={['name']="BMP-2",['container']=false}, ["IFV BMP-3 [32912lb]"]={['name']="BMP-3",['container']=false}, ["ARV BRDM-2 Air [12320lb]"]={['name']="BRDM-2",['container']=true}, ["ARV BRDM-2 Skid [12210lb]"]={['name']="BRDM-2",['container']=false}, ["APC BTR-80 Air [23936lb]"]={['name']="BTR-80",['container']=true}, ["APC BTR-80 Skid [23826lb]"]={['name']="BTR-80",['container']=false}, ["APC BTR-82A Air [24998lb]"]={['name']="BTR-82A",['container']=true}, ["APC BTR-82A Skid [24888lb]"]={['name']="BTR-82A",['container']=false}, ["SAM ROLAND ADS [34720lb]"]={['name']="Roland Radar",['container']=false}, ["SAM ROLAND LN [34720b]"]={['name']="Roland ADS",['container']=false}, ["SAM SA-13 STRELA [21624lb]"]={['name']="Strela-10M3",['container']=false}, ["AAA ZSU-23-4 Shilka [32912lb]"]={['name']="ZSU-23-4 Shilka",['container']=false}, ["SAM SA-19 Tunguska 2S6 [34720lb]"]={['name']="2S6 Tunguska",['container']=false}, ["Transport UAZ-469 Air [3747lb]"]={['name']="UAZ-469",['container']=true}, ["Transport UAZ-469 Skid [3630lb]"]={['name']="UAZ-469",['container']=false}, ["AAA GEPARD [34720lb]"]={['name']="Gepard",['container']=false}, ["SAM CHAPARRAL Air [21624lb]"]={['name']="M48 Chaparral",['container']=true}, ["SAM CHAPARRAL Skid [21516lb]"]={['name']="M48 Chaparral",['container']=false}, ["SAM LINEBACKER [34720lb]"]={['name']="M6 Linebacker",['container']=false}, ["Transport URAL-375 [14815lb]"]={['name']="Ural-375",['container']=false}, ["Transport M818 [16000lb]"]={['name']="M 818",['container']=false}, ["IFV MARDER [34720lb]"]={['name']="Marder",['container']=false}, ["Transport Tigr Air [15900lb]"]={['name']="Tigr_233036",['container']=true}, ["Transport Tigr Skid [15730lb]"]={['name']="Tigr_233036",['container']=false}, ["IFV TPZ FUCH [33440lb]"]={['name']="TPZ",['container']=false}, ["IFV BMD-1 Air [18040lb]"]={['name']="BMD-1",['container']=true}, ["IFV BMD-1 Skid [17930lb]"]={['name']="BMD-1",['container']=false}, ["IFV BTR-D Air [18040lb]"]={['name']="BTR_D",['container']=true}, ["IFV BTR-D Skid [17930lb]"]={['name']="BTR_D",['container']=false}, ["EWR SBORKA Air [21624lb]"]={['name']="Dog Ear radar",['container']=true}, ["EWR SBORKA Skid [21624lb]"]={['name']="Dog Ear radar",['container']=false}, ["ART 2S9 NONA Air [19140lb]"]={['name']="SAU 2-C9",['container']=true}, ["ART 2S9 NONA Skid [19030lb]"]={['name']="SAU 2-C9",['container']=false}, ["ART GVOZDIKA [34720lb]"]={['name']="SAU Gvozdika",['container']=false}, ["APC MTLB Air [26400lb]"]={['name']="MTLB",['container']=true}, ["APC MTLB Skid [26290lb]"]={['name']="MTLB",['container']=false}, } function CTLD_HERCULES:New(Coalition,Alias,CtldObject) local self=BASE:Inherit(self,FSM:New()) if Coalition and type(Coalition)=="string"then if Coalition=="blue"then self.coalition=coalition.side.BLUE self.coalitiontxt=Coalition elseif Coalition=="red"then self.coalition=coalition.side.RED self.coalitiontxt=Coalition elseif Coalition=="neutral"then self.coalition=coalition.side.NEUTRAL self.coalitiontxt=Coalition else self:E("ERROR: Unknown coalition in CTLD!") end else self.coalition=Coalition self.coalitiontxt=string.lower(UTILS.GetCoalitionName(self.coalition)) end if Alias then self.alias=tostring(Alias) else self.alias="UNHCR" if self.coalition then if self.coalition==coalition.side.RED then self.alias="Red CTLD Hercules" elseif self.coalition==coalition.side.BLUE then self.alias="Blue CTLD Hercules" end end end self.lid=string.format("%s (%s) | ",self.alias,self.coalitiontxt) self.infantrytemplate="Infantry" self.CTLD=CtldObject self.verbose=true self.j=0 self.carrierGroups={} self.Cargo={} self.ParatrooperCount={} self.ObjectTracker={} self.lid=string.format("%s (%s) | ",self.alias,self.coalition and UTILS.GetCoalitionName(self.coalition)or"unknown") self:HandleEvent(EVENTS.Shot,self._HandleShot) self:I(self.lid.."Started") self:CheckTemplates() return self end function CTLD_HERCULES:CheckTemplates() self:T(self.lid..'CheckTemplates') self.Types["Paratroopers 10"]={ name=self.infantrytemplate, container=false, available=false, } local missing={} local nomissing=0 local found={} local nofound=0 for _index,_tab in pairs(self.Types)do local outcometxt="MISSING" if _DATABASE.Templates.Groups[_tab.name]then outcometxt="OK" self.Types[_index].available=true found[_tab.name]=true else self.Types[_index].available=false missing[_tab.name]=true end if self.verbose then self:I(string.format(self.lid.."Checking template for %s (%s) ... %s",_index,_tab.name,outcometxt)) end end for _,_name in pairs(found)do nofound=nofound+1 end for _,_name in pairs(missing)do nomissing=nomissing+1 end self:I(string.format(self.lid.."Template Check Summary: Found %d, Missing %d, Total %d",nofound,nomissing,nofound+nomissing)) return self end function CTLD_HERCULES:Soldier_SpawnGroup(Cargo_Drop_initiator,Cargo_Drop_Position,Cargo_Type_name,CargoHeading,Cargo_Country,GroupSpacing) self:T(self.lid..'Soldier_SpawnGroup') self:T(Cargo_Drop_Position) local InjectTroopsType=CTLD_CARGO:New(nil,self.infantrytemplate,{self.infantrytemplate},CTLD_CARGO.Enum.TROOPS,true,true,10,nil,false,80) local position=Cargo_Drop_Position:GetVec2() local dropzone=ZONE_RADIUS:New("Infantry "..math.random(1,10000),position,100) self.CTLD:InjectTroops(dropzone,InjectTroopsType) return self end function CTLD_HERCULES:Cargo_SpawnGroup(Cargo_Drop_initiator,Cargo_Drop_Position,Cargo_Type_name,CargoHeading,Cargo_Country) self:T(self.lid.."Cargo_SpawnGroup") self:T(Cargo_Type_name) if Cargo_Type_name~='Container red 1'then local InjectVehicleType=CTLD_CARGO:New(nil,Cargo_Type_name,{Cargo_Type_name},CTLD_CARGO.Enum.VEHICLE,true,true,1,nil,false,1000) local position=Cargo_Drop_Position:GetVec2() local dropzone=ZONE_RADIUS:New("Vehicle "..math.random(1,10000),position,100) self.CTLD:InjectVehicles(dropzone,InjectVehicleType) end return self end function CTLD_HERCULES:Cargo_SpawnStatic(Cargo_Drop_initiator,Cargo_Drop_Position,Cargo_Type_name,CargoHeading,dead,Cargo_Country) self:T(self.lid.."Cargo_SpawnStatic") self:T("Static "..Cargo_Type_name.." Dead "..tostring(dead)) local position=Cargo_Drop_Position:GetVec2() local Zone=ZONE_RADIUS:New("Cargo Static "..math.random(1,10000),position,100) if not dead then local injectstatic=CTLD_CARGO:New(nil,"Cargo Static Group "..math.random(1,10000),"iso_container",CTLD_CARGO.Enum.STATIC,true,false,1,nil,true,4500,1) self.CTLD:InjectStatics(Zone,injectstatic,true,true) end return self end function CTLD_HERCULES:Cargo_SpawnDroppedAsCargo(_name,_pos) local theCargo=self.CTLD:_FindCratesCargoObject(_name) if theCargo then self.CTLD.CrateCounter=self.CTLD.CrateCounter+1 local CCat,CType,CShape=theCargo:GetStaticTypeAndShape() local basetype=CType or self.CTLD.basetype or"container_cargo" CCat=CCat or"Cargos" local theStatic=SPAWNSTATIC:NewFromType(basetype,CCat,self.cratecountry) :InitCargoMass(theCargo.PerCrateMass) :InitCargo(self.CTLD.enableslingload) :InitCoordinate(_pos) if CShape then theStatic:InitShape(CShape) end theStatic:Spawn(270,_name.."-Container-"..math.random(1,100000)) self.CTLD.Spawned_Crates[self.CTLD.CrateCounter]=theStatic local newCargo=CTLD_CARGO:New(self.CTLD.CargoCounter,theCargo.Name,theCargo.Templates,theCargo.CargoType,true,false,theCargo.CratesNeeded,self.CTLD.Spawned_Crates[self.CTLD.CrateCounter],true,theCargo.PerCrateMass,nil,theCargo.Subcategory) local map=theCargo:GetStaticResourceMap() newCargo:SetStaticResourceMap(map) table.insert(self.CTLD.Spawned_Cargo,newCargo) newCargo:SetWasDropped(true) newCargo:SetHasMoved(true) end return self end function CTLD_HERCULES:Cargo_SpawnObjects(Cargo_Drop_initiator,Cargo_Drop_Direction,Cargo_Content_position,Cargo_Type_name,Cargo_over_water,Container_Enclosed,ParatrooperGroupSpawn,offload_cargo,all_cargo_survive_to_the_ground,all_cargo_gets_destroyed,destroy_cargo_dropped_without_parachute,Cargo_Country) self:T(self.lid..'Cargo_SpawnObjects') local CargoHeading=self.CargoHeading if offload_cargo==true or ParatrooperGroupSpawn==true then if ParatrooperGroupSpawn==true then self:Soldier_SpawnGroup(Cargo_Drop_initiator,Cargo_Content_position,Cargo_Type_name,CargoHeading,Cargo_Country,10) else self:Cargo_SpawnGroup(Cargo_Drop_initiator,Cargo_Content_position,Cargo_Type_name,CargoHeading,Cargo_Country) end else if all_cargo_gets_destroyed==true or Cargo_over_water==true then else if all_cargo_survive_to_the_ground==true then if ParatrooperGroupSpawn==true then self:Cargo_SpawnStatic(Cargo_Drop_initiator,Cargo_Content_position,Cargo_Type_name,CargoHeading,true,Cargo_Country) else self:Cargo_SpawnGroup(Cargo_Drop_initiator,Cargo_Content_position,Cargo_Type_name,CargoHeading,Cargo_Country) end if Container_Enclosed==true then if ParatrooperGroupSpawn==false then self:Cargo_SpawnStatic(Cargo_Drop_initiator,Cargo_Content_position,"Hercules_Container_Parachute_Static",CargoHeading,false,Cargo_Country) end end end if destroy_cargo_dropped_without_parachute==true then if Container_Enclosed==true then if ParatrooperGroupSpawn==true then self:Soldier_SpawnGroup(Cargo_Drop_initiator,Cargo_Content_position,Cargo_Type_name,CargoHeading,Cargo_Country,0) else if self.CTLD.dropAsCargoCrate then self:Cargo_SpawnDroppedAsCargo(Cargo_Type_name,Cargo_Content_position) else self:Cargo_SpawnGroup(Cargo_Drop_initiator,Cargo_Content_position,Cargo_Type_name,CargoHeading,Cargo_Country) self:Cargo_SpawnStatic(Cargo_Drop_initiator,Cargo_Content_position,"Hercules_Container_Parachute_Static",CargoHeading,false,Cargo_Country) end end else self:Cargo_SpawnStatic(Cargo_Drop_initiator,Cargo_Content_position,Cargo_Type_name,CargoHeading,true,Cargo_Country) end end end end return self end function CTLD_HERCULES:Calculate_Object_Height_AGL(group) self:T(self.lid.."Calculate_Object_Height_AGL") if group.ClassName and group.ClassName=="GROUP"then local gcoord=group:GetCoordinate() local height=group:GetHeight() local lheight=gcoord:GetLandHeight() self:T(self.lid.."Height "..height-lheight) return height-lheight else if group:isExist()then local dcsposition=group:getPosition().p local dcsvec2={x=dcsposition.x,y=dcsposition.z} local height=math.floor(group:getPosition().p.y-land.getHeight(dcsvec2)) self.ObjectTracker[group.id_]=dcsposition self:T(self.lid.."Height "..height) return height else return 0 end end end function CTLD_HERCULES:Check_SurfaceType(object) self:T(self.lid.."Check_SurfaceType") if object:isExist()then return land.getSurfaceType({x=object:getPosition().p.x,y=object:getPosition().p.z}) else return 1 end end function CTLD_HERCULES:Cargo_Track(cargo,initiator) self:T(self.lid.."Cargo_Track") local Cargo_Drop_initiator=initiator if cargo.Cargo_Contents~=nil then if self:Calculate_Object_Height_AGL(cargo.Cargo_Contents)<10 then if self:Check_SurfaceType(cargo.Cargo_Contents)==2 or self:Check_SurfaceType(cargo.Cargo_Contents)==3 then cargo.Cargo_over_water=true end local dcsvec3=self.ObjectTracker[cargo.Cargo_Contents.id_]or initiator:GetVec3() self:T("SPAWNPOSITION: ") self:T({dcsvec3}) local Vec2={ x=dcsvec3.x, y=dcsvec3.z, } local vec3=COORDINATE:NewFromVec2(Vec2) self.ObjectTracker[cargo.Cargo_Contents.id_]=nil self:Cargo_SpawnObjects(Cargo_Drop_initiator,cargo.Cargo_Drop_Direction,vec3,cargo.Cargo_Type_name,cargo.Cargo_over_water,cargo.Container_Enclosed,cargo.ParatrooperGroupSpawn,cargo.offload_cargo,cargo.all_cargo_survive_to_the_ground,cargo.all_cargo_gets_destroyed,cargo.destroy_cargo_dropped_without_parachute,cargo.Cargo_Country) if cargo.Cargo_Contents:isExist()then cargo.Cargo_Contents:destroy() end cargo.scheduleFunctionID:Stop() cargo={} end end return self end function CTLD_HERCULES:Calculate_Cargo_Drop_initiator_NorthCorrection(point) self:T(self.lid.."Calculate_Cargo_Drop_initiator_NorthCorrection") 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 function CTLD_HERCULES:Calculate_Cargo_Drop_initiator_Heading(Cargo_Drop_initiator) self:T(self.lid.."Calculate_Cargo_Drop_initiator_Heading") local Heading=Cargo_Drop_initiator:GetHeading() Heading=Heading+self:Calculate_Cargo_Drop_initiator_NorthCorrection(Cargo_Drop_initiator:GetVec3()) if Heading<0 then Heading=Heading+(2*math.pi) end return Heading+0.06 end function CTLD_HERCULES:Cargo_Initialize(Initiator,Cargo_Contents,Cargo_Type_name,Container_Enclosed,SoldierGroup,ParatrooperGroupSpawnInit) self:T(self.lid.."Cargo_Initialize") local Cargo_Drop_initiator=Initiator:GetName() if Cargo_Drop_initiator~=nil then if ParatrooperGroupSpawnInit==true then self:T("Paratrooper Drop") if not self.ParatrooperCount[Cargo_Drop_initiator]then self.ParatrooperCount[Cargo_Drop_initiator]=1 else self.ParatrooperCount[Cargo_Drop_initiator]=self.ParatrooperCount[Cargo_Drop_initiator]+1 end local Paratroopers=self.ParatrooperCount[Cargo_Drop_initiator] self:T("Paratrooper Drop Number "..self.ParatrooperCount[Cargo_Drop_initiator]) local SpawnParas=false if math.fmod(Paratroopers,10)==0 then SpawnParas=true end self.j=self.j+1 self.Cargo[self.j]={} self.Cargo[self.j].Cargo_Drop_Direction=self:Calculate_Cargo_Drop_initiator_Heading(Initiator) self.Cargo[self.j].Cargo_Contents=Cargo_Contents self.Cargo[self.j].Cargo_Type_name=Cargo_Type_name self.Cargo[self.j].Container_Enclosed=Container_Enclosed self.Cargo[self.j].ParatrooperGroupSpawn=SpawnParas self.Cargo[self.j].Cargo_Country=Initiator:GetCountry() if self:Calculate_Object_Height_AGL(Initiator)<5.0 then self.Cargo[self.j].offload_cargo=true elseif self:Calculate_Object_Height_AGL(Initiator)<10.0 then self.Cargo[self.j].all_cargo_survive_to_the_ground=true elseif self:Calculate_Object_Height_AGL(Initiator)<100.0 then self.Cargo[self.j].all_cargo_gets_destroyed=true else self.Cargo[self.j].all_cargo_gets_destroyed=false end local timer=TIMER:New(self.Cargo_Track,self,self.Cargo[self.j],Initiator) self.Cargo[self.j].scheduleFunctionID=timer timer:Start(1,1,600) else self.j=self.j+1 self.Cargo[self.j]={} self.Cargo[self.j].Cargo_Drop_Direction=self:Calculate_Cargo_Drop_initiator_Heading(Initiator) self.Cargo[self.j].Cargo_Contents=Cargo_Contents self.Cargo[self.j].Cargo_Type_name=Cargo_Type_name self.Cargo[self.j].Container_Enclosed=Container_Enclosed self.Cargo[self.j].ParatrooperGroupSpawn=false self.Cargo[self.j].Cargo_Country=Initiator:GetCountry() if self:Calculate_Object_Height_AGL(Initiator)<5.0 then self.Cargo[self.j].offload_cargo=true elseif self:Calculate_Object_Height_AGL(Initiator)<10.0 then self.Cargo[self.j].all_cargo_survive_to_the_ground=true elseif self:Calculate_Object_Height_AGL(Initiator)<100.0 then self.Cargo[self.j].all_cargo_gets_destroyed=true else self.Cargo[self.j].destroy_cargo_dropped_without_parachute=true end local timer=TIMER:New(self.Cargo_Track,self,self.Cargo[self.j],Initiator) self.Cargo[self.j].scheduleFunctionID=timer timer:Start(1,1,600) end end return self end function CTLD_HERCULES:SetType(key,cargoType,cargoNum) self:T(self.lid.."SetType") self.carrierGroups[key]['cargoType']=cargoType self.carrierGroups[key]['cargoNum']=cargoNum return self end function CTLD_HERCULES:_HandleShot(Cargo_Drop_Event) self:T(self.lid.."Shot Event ID:"..Cargo_Drop_Event.id) if Cargo_Drop_Event.id==EVENTS.Shot then local GT_Name="" local SoldierGroup=false local ParatrooperGroupSpawnInit=false local GT_DisplayName=Weapon.getDesc(Cargo_Drop_Event.weapon).typeName:sub(15,-1) self:T(string.format("%sCargo_Drop_Event: %s",self.lid,Weapon.getDesc(Cargo_Drop_Event.weapon).typeName)) if(GT_DisplayName=="Squad 30 x Soldier [7950lb]")then self:Cargo_Initialize(Cargo_Drop_Event.IniGroup,Cargo_Drop_Event.weapon,"Soldier M4 GRG",false,true,true) end if self.Types[GT_DisplayName]then local GT_Name=self.Types[GT_DisplayName]['name'] local Cargo_Container_Enclosed=self.Types[GT_DisplayName]['container'] self:Cargo_Initialize(Cargo_Drop_Event.IniGroup,Cargo_Drop_Event.weapon,GT_Name,Cargo_Container_Enclosed) end end return self end function CTLD_HERCULES:_HandleBirth(event) self:T(self.lid.."Birth Event ID:"..event.id) return self end end CSAR={ ClassName="CSAR", verbose=0, lid="", coalition=1, coalitiontxt="blue", FreeVHFFrequencies={}, UsedVHFFrequencies={}, takenOff={}, csarUnits={}, downedPilots={}, landedStatus={}, addedTo={}, woundedGroups={}, inTransitGroups={}, smokeMarkers={}, heliVisibleMessage={}, heliCloseMessage={}, max_units=6, hoverStatus={}, pilotDisabled={}, pilotLives={}, useprefix=true, csarPrefix={}, template=nil, mash={}, smokecolor=4, rescues=0, rescuedpilots=0, limitmaxdownedpilots=true, maxdownedpilots=10, allheligroupset=nil, topmenuname="CSAR", ADFRadioPwr=1000, PilotWeight=80, CreateRadioBeacons=true, UserSetGroup=nil, AllowIRStrobe=false, IRStrobeRuntime=300, } CSAR.AircraftType={} CSAR.AircraftType["SA342Mistral"]=2 CSAR.AircraftType["SA342Minigun"]=2 CSAR.AircraftType["SA342L"]=4 CSAR.AircraftType["SA342M"]=4 CSAR.AircraftType["UH-1H"]=8 CSAR.AircraftType["Mi-8MTV2"]=12 CSAR.AircraftType["Mi-8MT"]=12 CSAR.AircraftType["Mi-24P"]=8 CSAR.AircraftType["Mi-24V"]=8 CSAR.AircraftType["Bell-47"]=2 CSAR.AircraftType["UH-60L"]=10 CSAR.AircraftType["AH-64D_BLK_II"]=2 CSAR.AircraftType["Bronco-OV-10A"]=2 CSAR.AircraftType["MH-60R"]=10 CSAR.AircraftType["OH-6A"]=2 CSAR.AircraftType["OH58D"]=2 CSAR.AircraftType["CH-47Fbl1"]=31 CSAR.version="1.0.30" function CSAR:New(Coalition,Template,Alias) local self=BASE:Inherit(self,FSM:New()) BASE:T({Coalition,Template,Alias}) if Coalition and type(Coalition)=="string"then if Coalition=="blue"then self.coalition=coalition.side.BLUE self.coalitiontxt=Coalition elseif Coalition=="red"then self.coalition=coalition.side.RED self.coalitiontxt=Coalition elseif Coalition=="neutral"then self.coalition=coalition.side.NEUTRAL self.coalitiontxt=Coalition else self:E("ERROR: Unknown coalition in CSAR!") end else self.coalition=Coalition self.coalitiontxt=string.lower(UTILS.GetCoalitionName(self.coalition)) end if Alias then self.alias=tostring(Alias) else self.alias="Red Cross" if self.coalition then if self.coalition==coalition.side.RED then self.alias="IFRC" elseif self.coalition==coalition.side.BLUE then self.alias="CSAR" end end end self.lid=string.format("%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("*","PilotDown","*") self:AddTransition("*","Approach","*") self:AddTransition("*","Landed","*") self:AddTransition("*","Boarded","*") self:AddTransition("*","Returning","*") self:AddTransition("*","Rescued","*") self:AddTransition("*","KIA","*") self:AddTransition("*","Load","*") self:AddTransition("*","Save","*") self:AddTransition("*","Stop","Stopped") self.addedTo={} self.allheligroupset={} self.csarUnits={} self.FreeVHFFrequencies={} self.heliVisibleMessage={} self.heliCloseMessage={} self.hoverStatus={} self.inTransitGroups={} self.landedStatus={} self.lastCrash={} self.takenOff={} self.smokeMarkers={} self.UsedVHFFrequencies={} self.woundedGroups={} self.downedPilots={} self.downedpilotcounter=1 self.rescues=0 self.rescuedpilots=0 self.csarOncrash=false self.allowDownedPilotCAcontrol=false self.enableForAI=false self.smokecolor=4 self.coordtype=2 self.immortalcrew=true self.invisiblecrew=false self.messageTime=15 self.pilotRuntoExtractPoint=true self.loadDistance=75 self.extractDistance=500 self.loadtimemax=135 self.radioSound="beacon.ogg" self.beaconRefresher=29 self.allowFARPRescue=true self.FARPRescueDistance=1000 self.max_units=6 self.useprefix=true self.csarPrefix={"helicargo","MEDEVAC"} self.template=Template or"generic" self.mashprefix={"MASH"} self.autosmoke=false self.autosmokedistance=2000 self.limitmaxdownedpilots=true self.maxdownedpilots=25 self:_GenerateVHFrequencies() self.approachdist_far=5000 self.approachdist_near=3000 self.pilotmustopendoors=false self.suppressmessages=false self.rescuehoverheight=20 self.rescuehoverdistance=10 self.countryblue=country.id.USA self.countryred=country.id.RUSSIA self.countryneutral=country.id.UN_PEACEKEEPERS self.csarUsePara=false self.wetfeettemplate=nil self.usewetfeet=false self.allowbronco=false self.ADFRadioPwr=1000 self.PilotWeight=80 self.UserSetGroup=nil self.useSRS=false self.SRSPath="E:\\Program Files\\DCS-SimpleRadio-Standalone" self.SRSchannel=300 self.SRSModulation=radio.modulation.AM self.SRSport=5002 self.SRSCulture="en-GB" self.SRSVoice=MSRS.Voices.Google.Standard.en_GB_Standard_B self.SRSGPathToCredentials=nil self.SRSVolume=1.0 self.SRSGender="male" self.CSARVoice=MSRS.Voices.Google.Standard.en_US_Standard_A self.CSARVoiceMS=MSRS.Voices.Microsoft.Hedda self.coordinate=nil local AliaS=string.gsub(self.alias," ","_") self.filename=string.format("CSAR_%s_Persist.csv",AliaS) self.enableLoadSave=false self.filepath=nil self.saveinterval=600 return self end function CSAR:_CreateDownedPilotTrack(Group,Groupname,Side,OriginalUnit,Description,Typename,Frequency,Playername,Wetfeet,BeaconName) self:T({"_CreateDownedPilotTrack",Groupname,Side,OriginalUnit,Description,Typename,Frequency,Playername}) local DownedPilot={} DownedPilot.desc=Description or"" DownedPilot.frequency=Frequency or 0 DownedPilot.index=self.downedpilotcounter DownedPilot.name=Groupname or Playername or"" DownedPilot.originalUnit=OriginalUnit or"" DownedPilot.player=Playername or"" DownedPilot.side=Side or 0 DownedPilot.typename=Typename or"" DownedPilot.group=Group DownedPilot.timestamp=0 DownedPilot.alive=true DownedPilot.wetfeet=Wetfeet or false DownedPilot.BeaconName=BeaconName local PilotTable=self.downedPilots local counter=self.downedpilotcounter PilotTable[counter]={} PilotTable[counter]=DownedPilot self:T({Table=PilotTable}) self.downedPilots=PilotTable self.downedpilotcounter=self.downedpilotcounter+1 return self end function CSAR:_PilotsOnboard(_heliName) self:T(self.lid.." _PilotsOnboard") local count=0 if self.inTransitGroups[_heliName]then for _,_group in pairs(self.inTransitGroups[_heliName])do count=count+1 end end return count end function CSAR:_DoubleEjection(_unitname) if self.lastCrash[_unitname]then local _time=self.lastCrash[_unitname] if timer.getTime()-_time<10 then self:E(self.lid.."Caught double ejection!") return true end end self.lastCrash[_unitname]=timer.getTime() return false end function CSAR:AddPlayerTask(PlayerTask) self:T(self.lid.." AddPlayerTask") if not self.PlayerTaskQueue then self.PlayerTaskQueue=FIFO:New() end self.PlayerTaskQueue:Push(PlayerTask,PlayerTask.PlayerTaskNr) return self end function CSAR:_SpawnPilotInField(country,point,frequency,wetfeet) self:T({country,point,frequency,tostring(wetfeet)}) local freq=frequency or 1000 local freq=freq/1000 for i=1,10 do math.random(i,10000) end if point:IsSurfaceTypeWater()or wetfeet then point.y=0 end local template=self.template if self.usewetfeet and wetfeet then template=self.wetfeettemplate end local alias=string.format("Pilot %.2fkHz-%d",freq,math.random(1,99)) local coalition=self.coalition local pilotcacontrol=self.allowDownedPilotCAcontrol local _spawnedGroup=SPAWN :NewWithAlias(template,alias) :InitCoalition(coalition) :InitCountry(country) :InitDelayOff() :SpawnFromCoordinate(point) return _spawnedGroup,alias end function CSAR:_AddSpecialOptions(group) self:T(self.lid.." _AddSpecialOptions") self:T({group}) local immortalcrew=self.immortalcrew local invisiblecrew=self.invisiblecrew if immortalcrew then local _setImmortal={ id='SetImmortal', params={ value=true } } group:SetCommand(_setImmortal) end if invisiblecrew then local _setInvisible={ id='SetInvisible', params={ value=true } } group:SetCommand(_setInvisible) end group:OptionAlarmStateGreen() group:OptionROEHoldFire() return self end function CSAR:_AddCsar(_coalition,_country,_point,_typeName,_unitName,_playerName,_freq,noMessage,_description,forcedesc) self:T(self.lid.." _AddCsar") self:T({_coalition,_country,_point,_typeName,_unitName,_playerName,_freq,noMessage,_description}) local template=self.template local wetfeet=false local surface=_point:GetSurfaceType() if surface==land.SurfaceType.WATER then wetfeet=true end if not _freq then _freq=self:_GenerateADFFrequency() if not _freq then _freq=333000 end end local _spawnedGroup,_alias=self:_SpawnPilotInField(_country,_point,_freq,wetfeet) local _typeName=_typeName or"Pilot" if not noMessage then if _freq~=0 then self:_DisplayToAllSAR("MAYDAY MAYDAY! ".._typeName.." is down. ",self.coalition,self.messageTime) else self:_DisplayToAllSAR("Troops In Contact. ".._typeName.." requests CASEVAC. ",self.coalition,self.messageTime) end end local BeaconName if _playerName then BeaconName=_playerName..math.random(1,10000) elseif _unitName then BeaconName=_unitName..math.random(1,10000) else BeaconName="Ghost-1-1"..math.random(1,10000) end if(_freq and _freq~=0)then self:_AddBeaconToGroup(_spawnedGroup,_freq,BeaconName) end self:_AddSpecialOptions(_spawnedGroup) local _text=_description if not forcedesc then if _playerName~=nil then if _freq~=0 then _text="Pilot ".._playerName else _text="TIC - ".._playerName end elseif _unitName~=nil then if _freq~=0 then _text="AI Pilot of ".._unitName else _text="TIC - ".._unitName end end end self:T({_spawnedGroup,_alias}) local _GroupName=_spawnedGroup:GetName()or _alias self:_CreateDownedPilotTrack(_spawnedGroup,_GroupName,_coalition,_unitName,_text,_typeName,_freq,_playerName,wetfeet,BeaconName) self:_InitSARForPilot(_spawnedGroup,_unitName,_freq,noMessage,_playerName) return _spawnedGroup,_alias end function CSAR:_SpawnCsarAtZone(_zone,_coalition,_description,_randomPoint,_nomessage,unitname,typename,forcedesc) self:T(self.lid.." _SpawnCsarAtZone") local freq=self:_GenerateADFFrequency() local _triggerZone=nil if type(_zone)=="string"then _triggerZone=ZONE:New(_zone) elseif type(_zone)=="table"and _zone.ClassName then if string.find(_zone.ClassName,"ZONE",1)then _triggerZone=_zone end end if _triggerZone==nil then self:E(self.lid.."ERROR: Can\'t find zone called ".._zone,10) return end local _description=_description or"PoW" local unitname=unitname or"Old Rusty" local typename=typename or"Phantom II" local pos={} if _randomPoint then local _pos=_triggerZone:GetRandomPointVec3() pos=COORDINATE:NewFromVec3(_pos) else pos=_triggerZone:GetCoordinate() end local _country=0 if _coalition==coalition.side.BLUE then _country=self.countryblue elseif _coalition==coalition.side.RED then _country=self.countryred else _country=self.countryneutral end self:_AddCsar(_coalition,_country,pos,typename,unitname,_description,freq,_nomessage,_description,forcedesc) return self end function CSAR:SpawnCSARAtZone(Zone,Coalition,Description,RandomPoint,Nomessage,Unitname,Typename,Forcedesc) self:_SpawnCsarAtZone(Zone,Coalition,Description,RandomPoint,Nomessage,Unitname,Typename,Forcedesc) return self end function CSAR:_SpawnCASEVAC(_Point,_coalition,_description,_nomessage,unitname,typename,forcedesc) self:T(self.lid.." _SpawnCASEVAC") local _description=_description or"CASEVAC" local unitname=unitname or"CASEVAC" local typename=typename or"Ground Commander" local pos={} pos=_Point local _country=0 if _coalition==coalition.side.BLUE then _country=self.countryblue elseif _coalition==coalition.side.RED then _country=self.countryred else _country=self.countryneutral end self:_AddCsar(_coalition,_country,pos,typename,unitname,_description,0,_nomessage,_description,forcedesc) return self end function CSAR:SpawnCASEVAC(Point,Coalition,Description,Nomessage,Unitname,Typename,Forcedesc) self:_SpawnCASEVAC(Point,Coalition,Description,Nomessage,Unitname,Typename,Forcedesc) return self end function CSAR:_EventHandler(EventData) self:T(self.lid.." _EventHandler") self:T({Event=EventData.id}) local _event=EventData if self.enableForAI==false and _event.IniPlayerName==nil then return self end if _event==nil or _event.initiator==nil then return self elseif _event.id==EVENTS.Takeoff then self:T(self.lid.." Event unit - Takeoff") local _coalition=_event.IniCoalition if _coalition~=self.coalition then return self end if _event.IniGroupName then self.takenOff[_event.IniUnitName]=true end return self elseif _event.id==EVENTS.PlayerEnterAircraft or _event.id==EVENTS.PlayerEnterUnit then self:T(self.lid.." Event unit - Player Enter") local _coalition=_event.IniCoalition self:T("Coalition = "..UTILS.GetCoalitionName(_coalition)) if _coalition~=self.coalition then return self end if _event.IniPlayerName then self.takenOff[_event.IniPlayerName]=nil end self:T("Taken Off: "..tostring(_event.IniUnit:InAir(true))) if _event.IniUnit:InAir(true)then self.takenOff[_event.IniPlayerName]=true end local _unit=_event.IniUnit local _group=_event.IniGroup local function IsBronco(Group) local grp=Group local typename=grp:GetTypeName() self:T(typename) if typename=="Bronco-OV-10A"then return true end return false end if _unit:IsHelicopter()or _group:IsHelicopter()or IsBronco(_group)then self:_AddMedevacMenuItem() end return self elseif(_event.id==EVENTS.PilotDead and self.csarOncrash==false)then self:T(self.lid.." Event unit - Pilot Dead") local _unit=_event.IniUnit local _unitname=_event.IniUnitName local _group=_event.IniGroup if _unit==nil then return self end local _coalition=_event.IniCoalition if _coalition~=self.coalition then return self end if self.takenOff[_event.IniUnitName]==true or _group:IsAirborne()then if self:_DoubleEjection(_unitname)then return self end else self:T(self.lid.." Pilot has not taken off, ignore") end return self elseif _event.id==EVENTS.PilotDead or _event.id==EVENTS.Ejection then if _event.id==EVENTS.PilotDead and self.csarOncrash==false then return self end self:T(self.lid.." Event unit - Pilot Ejected") local _unit=_event.IniUnit local _unitname=_event.IniUnitName local _group=_event.IniGroup self:T({_unit.UnitName,_unitname,_group.GroupName}) if _unit==nil then self:T("Unit NIL!") return self end local _coalition=_group:GetCoalition() if _coalition~=self.coalition then self:T("Wrong coalition! Coalition = "..UTILS.GetCoalitionName(_coalition)) return self end self:T("Airborne: "..tostring(_group:IsAirborne())) self:T("Taken Off: "..tostring(self.takenOff[_event.IniUnitName])) if not self.takenOff[_event.IniUnitName]and not _group:IsAirborne()then self:T(self.lid.." Pilot has not taken off, ignore") end if self:_DoubleEjection(_unitname)then self:T("Double Ejection!") return self end if self.limitmaxdownedpilots and self:_ReachedPilotLimit()then self:T("Maxed Downed Pilot!") return self end local wetfeet=false local initdcscoord=nil local initcoord=nil if _event.id==EVENTS.Ejection then initdcscoord=_event.TgtDCSUnit:getPoint() initcoord=COORDINATE:NewFromVec3(initdcscoord) self:T({initdcscoord}) else initdcscoord=_event.IniDCSUnit:getPoint() initcoord=COORDINATE:NewFromVec3(initdcscoord) self:T({initdcscoord}) end local surface=initcoord:GetSurfaceType() if surface==land.SurfaceType.WATER then self:T("Wet feet!") wetfeet=true end if self.csarUsePara==false or(self.csarUsePara and wetfeet)then local _freq=self:_GenerateADFFrequency() self:_AddCsar(_coalition,_unit:GetCountry(),initcoord,_unit:GetTypeName(),_unit:GetName(),_event.IniPlayerName,_freq,false,"none") return self end elseif _event.id==EVENTS.Land then self:T(self.lid.." Landing") if _event.IniUnitName then self.takenOff[_event.IniUnitName]=nil end if self.allowFARPRescue then local _unit=_event.IniUnit if _unit==nil then self:T(self.lid.." Unit nil on landing") return self end local _coalition=_event.IniGroup:GetCoalition() if _coalition~=self.coalition then self:T(self.lid.." Wrong coalition") return self end self.takenOff[_event.IniUnitName]=nil local _place=_event.Place if _place==nil then self:T(self.lid.." Landing Place Nil") return self end if self.inTransitGroups[_event.IniUnitName]==nil then return self end if _place:GetCoalition()==self.coalition or _place:GetCoalition()==coalition.side.NEUTRAL then self:__Landed(2,_event.IniUnitName,_place) self:_ScheduledSARFlight(_event.IniUnitName,_event.IniGroupName,true,true) else self:T(string.format("Airfield %d, Unit %d",_place:GetCoalition(),_unit:GetCoalition())) end end return self end if(_event.id==EVENTS.LandingAfterEjection and self.csarUsePara==true)then self:T("LANDING_AFTER_EJECTION") local _LandingPos=COORDINATE:NewFromVec3(_event.initiator:getPosition().p) local _unitname="Aircraft" local _typename="Ejected Pilot" local _country=_event.initiator:getCountry() local _coalition=coalition.getCountryCoalition(_country) self:T("Country = ".._country.." Coalition = ".._coalition) if _coalition==self.coalition then local _freq=self:_GenerateADFFrequency() self:I({coalition=_coalition,country=_country,coord=_LandingPos,name=_unitname,player=_event.IniPlayerName,freq=_freq}) self:_AddCsar(_coalition,_country,_LandingPos,nil,_unitname,_event.IniPlayerName,_freq,false,"none") Unit.destroy(_event.initiator) end end return self end function CSAR:_InitSARForPilot(_downedGroup,_GroupName,_freq,_nomessage,_playername) self:T(self.lid.." _InitSARForPilot") local _leader=_downedGroup:GetUnit(1) local _groupName=_GroupName local _freqk=_freq/1000 local _coordinatesText=self:_GetPositionOfWounded(_downedGroup) local _leadername=_leader:GetName() if not _nomessage then if _freq~=0 then local _text=string.format("%s requests SAR at %s, beacon at %.2f KHz",_groupName,_coordinatesText,_freqk) if self.coordtype~=2 then self:_DisplayToAllSAR(_text,self.coalition,self.messageTime) else self:_DisplayToAllSAR(_text,self.coalition,self.messageTime,false,true) local coordtext=UTILS.MGRSStringToSRSFriendly(_coordinatesText,true) local _text=string.format("%s requests SAR at %s, beacon at %.2f kilo hertz",_groupName,coordtext,_freqk) self:_DisplayToAllSAR(_text,self.coalition,self.messageTime,true,false) end else local _text=string.format("Pickup Zone at %s.",_coordinatesText) if self.coordtype~=2 then self:_DisplayToAllSAR(_text,self.coalition,self.messageTime) else self:_DisplayToAllSAR(_text,self.coalition,self.messageTime,false,true) local coordtext=UTILS.MGRSStringToSRSFriendly(_coordinatesText,true) local _text=string.format("Pickup Zone at %s.",coordtext) self:_DisplayToAllSAR(_text,self.coalition,self.messageTime,true,false) end end end for _,_heliName in pairs(self.csarUnits)do self:_CheckWoundedGroupStatus(_heliName,_groupName) end self:__PilotDown(2,_downedGroup,_freqk,_groupName,_coordinatesText,_playername) return self end function CSAR:_CheckNameInDownedPilots(name) local PilotTable=self.downedPilots local found=false local table=nil for _,_pilot in pairs(PilotTable)do if _pilot.name==name and _pilot.alive==true then found=true table=_pilot break end end return found,table end function CSAR:_RemoveNameFromDownedPilots(name,force) local PilotTable=self.downedPilots local found=false for _index,_pilot in pairs(PilotTable)do if _pilot.name==name then self.downedPilots[_index].alive=false end end return found end function CSAR:SetCallSignOptions(ShortCallsign,Keepnumber,CallsignTranslations) if not ShortCallsign or ShortCallsign==false then self.ShortCallsign=false else self.ShortCallsign=true end self.Keepnumber=Keepnumber or false self.CallsignTranslations=CallsignTranslations return self end function CSAR:_GetCustomCallSign(UnitName) local callsign=UnitName local unit=UNIT:FindByName(UnitName) if unit and unit:IsAlive()then local group=unit:GetGroup() callsign=group:GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations) end return callsign end function CSAR:_CheckWoundedGroupStatus(heliname,woundedgroupname) self:T(self.lid.." _CheckWoundedGroupStatus") local _heliName=heliname local _woundedGroupName=woundedgroupname self:T({Heli=_heliName,Downed=_woundedGroupName}) local _found,_downedpilot=self:_CheckNameInDownedPilots(_woundedGroupName) if not _found then self:T("...not found in list!") return end local _woundedGroup=_downedpilot.group if _woundedGroup~=nil and _woundedGroup:IsAlive()then local _heliUnit=self:_GetSARHeli(_heliName) local _lookupKeyHeli=_heliName.."_".._woundedGroupName if _heliUnit==nil then self.heliVisibleMessage[_lookupKeyHeli]=nil self.heliCloseMessage[_lookupKeyHeli]=nil self.landedStatus[_lookupKeyHeli]=nil self:T("...heliunit nil!") return end local _heliCoord=_heliUnit:GetCoordinate() local _leaderCoord=_woundedGroup:GetCoordinate() local _distance=self:_GetDistance(_heliCoord,_leaderCoord) if(self.autosmoke==true)and(_distance0 then if self:_CheckCloseWoundedGroup(_distance,_heliUnit,_heliName,_woundedGroup,_woundedGroupName)==true then _downedpilot.timestamp=timer.getAbsTime() self:__Approach(-5,heliname,woundedgroupname) end elseif _distance>=self.approachdist_near and _distance_lastSmoke then local _smokecolor=self.smokecolor local _smokecoord=_woundedLeader:GetCoordinate():Translate(6,math.random(1,360)) _smokecoord:Smoke(_smokecolor) self.smokeMarkers[_woundedGroupName]=timer.getTime()+300 end return self end function CSAR:_PickupUnit(_heliUnit,_pilotName,_woundedGroup,_woundedGroupName) self:T(self.lid.." _PickupUnit") local _heliName=_heliUnit:GetName() local _groups=self.inTransitGroups[_heliName] local _unitsInHelicopter=self:_PilotsOnboard(_heliName) if not _groups then self.inTransitGroups[_heliName]={} _groups=self.inTransitGroups[_heliName] end local _maxUnits=self.AircraftType[_heliUnit:GetTypeName()] if _maxUnits==nil then _maxUnits=self.max_units end if _unitsInHelicopter+1>_maxUnits then self:_DisplayMessageToSAR(_heliUnit,string.format("%s, %s. We\'re already crammed with %d guys! Sorry!",_pilotName,self:_GetCustomCallSign(_heliName),_unitsInHelicopter,_unitsInHelicopter),self.messageTime,false,false,true) return self end local found,downedgrouptable=self:_CheckNameInDownedPilots(_woundedGroupName) local grouptable=downedgrouptable self.inTransitGroups[_heliName][_woundedGroupName]= { originalUnit=grouptable.originalUnit, woundedGroup=_woundedGroupName, side=self.coalition, desc=grouptable.desc, player=grouptable.player, } _woundedGroup:Destroy(false) self:_RemoveNameFromDownedPilots(_woundedGroupName,true) self:_DisplayMessageToSAR(_heliUnit,string.format("%s: %s I\'m in! Get to the MASH ASAP! ",self:_GetCustomCallSign(_heliName),_pilotName),self.messageTime,true,true) self:_UpdateUnitCargoMass(_heliName) self:__Boarded(5,_heliName,_woundedGroupName,grouptable.desc) return self end function CSAR:_UpdateUnitCargoMass(_heliName) self:T(self.lid.." _UpdateUnitCargoMass") local calculatedMass=self:_PilotsOnboard(_heliName)*(self.PilotWeight or 80) local Unit=UNIT:FindByName(_heliName) if Unit then Unit:SetUnitInternalCargo(calculatedMass) end return self end function CSAR:_OrderGroupToMoveToPoint(_leader,_destination) self:T(self.lid.." _OrderGroupToMoveToPoint") local group=_leader local coordinate=_destination:GetVec2() group:SetAIOn() group:RouteToVec2(coordinate,5) return self end function CSAR:_IsLoadingDoorOpen(unit_name) self:T(self.lid.." _IsLoadingDoorOpen") return UTILS.IsLoadingDoorOpen(unit_name) end function CSAR:_CheckCloseWoundedGroup(_distance,_heliUnit,_heliName,_woundedGroup,_woundedGroupName) self:T(self.lid.." _CheckCloseWoundedGroup") local _woundedLeader=_woundedGroup local _lookupKeyHeli=_heliUnit:GetName().."_".._woundedGroupName local _found,_pilotable=self:_CheckNameInDownedPilots(_woundedGroupName) local _pilotName=_pilotable.desc local _reset=true if(_distance<500)then self:T(self.lid.."[Pickup Debug] Helo closer than 500m: ".._lookupKeyHeli) if self.heliCloseMessage[_lookupKeyHeli]==nil then if self.autosmoke==true then self:_DisplayMessageToSAR(_heliUnit,string.format("%s: %s. You\'re close now! Land or hover at the smoke.",self:_GetCustomCallSign(_heliName),_pilotName),self.messageTime,false,true) else self:_DisplayMessageToSAR(_heliUnit,string.format("%s: %s. You\'re close now! Land in a safe place, I will go there ",self:_GetCustomCallSign(_heliName),_pilotName),self.messageTime,false,true) end self.heliCloseMessage[_lookupKeyHeli]=true end self:T(self.lid.."[Pickup Debug] Checking landed vs Hover for ".._lookupKeyHeli) if not _heliUnit:InAir()then self:T(self.lid.."[Pickup Debug] Helo landed: ".._lookupKeyHeli) if self.pilotRuntoExtractPoint==true then if(_distance0 then self:T(self.lid.."[Pickup Debug] Helo hovering not long enough ".._lookupKeyHeli) self:_DisplayMessageToSAR(_heliUnit,"Hovering above ".._pilotName..". \n\nHold hover for ".._time.." seconds to winch them up. \n\nIf the countdown stops you\'re too far away!",self.messageTime,true) else self:T(self.lid.."[Pickup Debug] Helo hovering long enough - door check ".._lookupKeyHeli) if self.pilotmustopendoors and(self:_IsLoadingDoorOpen(_heliName)==false)then self:_DisplayMessageToSAR(_heliUnit,"Open the door to let me in!",self.messageTime,true,true) self:T(self.lid.."[Pickup Debug] Door closed, try again next loop ".._lookupKeyHeli) return false else self.hoverStatus[_lookupKeyHeli]=nil self:_PickupUnit(_heliUnit,_pilotName,_woundedGroup,_woundedGroupName) self:T(self.lid.."[Pickup Debug] Pilot picked up ".._lookupKeyHeli) return true end end _reset=false else self:T(self.lid.."[Pickup Debug] Helo hovering too high ".._lookupKeyHeli) self:_DisplayMessageToSAR(_heliUnit,"Too high to winch ".._pilotName.." \nReduce height and hover for 10 seconds!",self.messageTime,true,true) self:T(self.lid.."[Pickup Debug] Hovering too high, try again next loop ".._lookupKeyHeli) return false end end end end end if _reset then self.hoverStatus[_lookupKeyHeli]=nil end if _distance<500 then return true else return false end end function CSAR:_ScheduledSARFlight(heliname,groupname,isairport,noreschedule) self:T(self.lid.." _ScheduledSARFlight") self:T({heliname,groupname}) local _heliUnit=self:_GetSARHeli(heliname) local _woundedGroupName=groupname if(_heliUnit==nil)then self.inTransitGroups[heliname]=nil return end if self.inTransitGroups[heliname]==nil or self.inTransitGroups[heliname][_woundedGroupName]==nil then return end local _dist=self:_GetClosestMASH(_heliUnit) if _dist==-1 then self:T(self.lid.."[Drop off debug] Check distance to MASH for "..heliname.." Distance can not be determined!") return end self:T(self.lid.."[Drop off debug] Check distance to MASH for "..heliname.." Distance km: "..math.floor(_dist/1000)) if(_distsmokedist then smokedist=self.approachdist_far end if _closest~=nil and _closest.pilot~=nil and _closest.distance>0 and _closest.distance0 and _closest.distance0 and _closest.distance12 then clock=clock-12 end end return clock end function CSAR:_AddBeaconToGroup(_group,_freq,_name) self:T(self.lid.." _AddBeaconToGroup") if self.CreateRadioBeacons==false then return end local _group=_group if _group==nil then for _i,_current in ipairs(self.UsedVHFFrequencies)do if _current==_freq then table.insert(self.FreeVHFFrequencies,_freq) table.remove(self.UsedVHFFrequencies,_i) end end return end if _group:IsAlive()then local _radioUnit=_group:GetUnit(1) if _radioUnit then local name=_radioUnit:GetName() local Frequency=_freq local name=_radioUnit:GetName() local Sound="l10n/DEFAULT/"..self.radioSound local vec3=_radioUnit:GetVec3()or _radioUnit:GetPositionVec3()or{x=0,y=0,z=0} trigger.action.radioTransmission(Sound,vec3,0,false,Frequency,self.ADFRadioPwr or 1000,_name) end end return self end function CSAR:_RefreshRadioBeacons() self:T(self.lid.." _RefreshRadioBeacons") if self.CreateRadioBeacons==false then return end if self:_CountActiveDownedPilots()>0 then local PilotTable=self.downedPilots for _,_pilot in pairs(PilotTable)do self:T({_pilot.name}) local pilot=_pilot local group=pilot.group local frequency=pilot.frequency or 0 local bname=pilot.BeaconName or pilot.name..math.random(1,100000) trigger.action.stopRadioTransmission(bname) if group and group:IsAlive()and frequency>0 then self:_AddBeaconToGroup(group,frequency,bname) end end end return self end function CSAR:_CountActiveDownedPilots() self:T(self.lid.." _CountActiveDownedPilots") local PilotsInFieldN=0 for _,_unitName in pairs(self.downedPilots)do self:T({_unitName.desc}) if _unitName.alive==true then PilotsInFieldN=PilotsInFieldN+1 end end return PilotsInFieldN end function CSAR:_ReachedPilotLimit() self:T(self.lid.." _ReachedPilotLimit") local limit=self.maxdownedpilots local islimited=self.limitmaxdownedpilots local count=self:_CountActiveDownedPilots() if islimited and(count>=limit)then return true else return false end end function CSAR:SetOwnSetPilotGroups(Set) self.UserSetGroup=Set return self end function CSAR:onafterStart(From,Event,To) self:T({From,Event,To}) self:I(self.lid.."Started ("..self.version..")") self:HandleEvent(EVENTS.Takeoff,self._EventHandler) self:HandleEvent(EVENTS.Land,self._EventHandler) self:HandleEvent(EVENTS.Ejection,self._EventHandler) self:HandleEvent(EVENTS.LandingAfterEjection,self._EventHandler) self:HandleEvent(EVENTS.PlayerEnterAircraft,self._EventHandler) self:HandleEvent(EVENTS.PlayerEnterUnit,self._EventHandler) self:HandleEvent(EVENTS.PilotDead,self._EventHandler) if self.UserSetGroup then self.allheligroupset=self.UserSetGroup elseif self.allowbronco then local prefixes=self.csarPrefix or{} self.allheligroupset=SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(prefixes):FilterStart() elseif self.useprefix then local prefixes=self.csarPrefix or{} self.allheligroupset=SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(prefixes):FilterCategoryHelicopter():FilterStart() else self.allheligroupset=SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterCategoryHelicopter():FilterStart() end self.mash=SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(self.mashprefix):FilterStart() local staticmashes=SET_STATIC:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(self.mashprefix):FilterOnce() local zonemashes=SET_ZONE:New():FilterPrefixes(self.mashprefix):FilterOnce() if staticmashes:Count()>0 then for _,_mash in pairs(staticmashes.Set)do self.mash:AddObject(_mash) end end if zonemashes:Count()>0 then for _,_mash in pairs(zonemashes.Set)do self.mash:AddObject(_mash) end end if not self.coordinate then local csarhq=self.mash:GetRandom() if csarhq then self.coordinate=csarhq:GetCoordinate() end end if self.wetfeettemplate then self.usewetfeet=true end if self.useSRS then local path=self.SRSPath local modulation=self.SRSModulation local channel=self.SRSchannel self.msrs=MSRS:New(path,channel,modulation) self.msrs:SetPort(self.SRSport) self.msrs:SetLabel("CSAR") self.msrs:SetCulture(self.SRSCulture) self.msrs:SetCoalition(self.coalition) self.msrs:SetVoice(self.SRSVoice) self.msrs:SetGender(self.SRSGender) if self.SRSGPathToCredentials then self.msrs:SetProviderOptionsGoogle(self.SRSGPathToCredentials,self.SRSGPathToCredentials) self.msrs:SetProvider(MSRS.Provider.GOOGLE) end self.msrs:SetVolume(self.SRSVolume) self.msrs:SetLabel("CSAR") self.SRSQueue=MSRSQUEUE:New("CSAR") end self:__Status(-10) if self.enableLoadSave then local interval=self.saveinterval local filename=self.filename local filepath=self.filepath self:__Save(interval,filepath,filename) end return self end function CSAR:_CheckDownedPilotTable() local pilots=self.downedPilots local npilots={} for _ind,_entry in pairs(pilots)do local _group=_entry.group if _group:IsAlive()then npilots[_ind]=_entry else if _entry.alive then self:__KIA(1,_entry.desc) end end end self.downedPilots=npilots return self end function CSAR:onbeforeStatus(From,Event,To) self:T({From,Event,To}) self:_AddMedevacMenuItem() if not self.BeaconTimer or(self.BeaconTimer and not self.BeaconTimer:IsRunning())then self.BeaconTimer=TIMER:New(self._RefreshRadioBeacons,self) self.BeaconTimer:Start(2,self.beaconRefresher) end self:_CheckDownedPilotTable() for _,_sar in pairs(self.csarUnits)do local PilotTable=self.downedPilots for _,_entry in pairs(PilotTable)do if _entry.alive then local entry=_entry local name=entry.name local timestamp=entry.timestamp or 0 local now=timer.getAbsTime() if now-timestamp>17 then self:_CheckWoundedGroupStatus(_sar,name) end end end end return self end function CSAR:onafterStatus(From,Event,To) self:T({From,Event,To}) local NumberOfSARPilots=0 for _,_unitName in pairs(self.csarUnits)do NumberOfSARPilots=NumberOfSARPilots+1 end local PilotsInFieldN=self:_CountActiveDownedPilots() local PilotsBoarded=0 for _,_unitName in pairs(self.inTransitGroups)do for _,_units in pairs(_unitName)do PilotsBoarded=PilotsBoarded+1 end end if self.verbose>0 then local text=string.format("%s Active SAR: %d | Downed Pilots in field: %d (max %d) | Pilots boarded: %d | Landings: %d | Pilots rescued: %d", self.lid,NumberOfSARPilots,PilotsInFieldN,self.maxdownedpilots,PilotsBoarded,self.rescues,self.rescuedpilots) self:T(text) if self.verbose<2 then self:I(text) elseif self.verbose>1 then self:I(text) local m=MESSAGE:New(text,"10","Status",true):ToCoalition(self.coalition) end end self:__Status(-20) return self end function CSAR:onafterStop(From,Event,To) self:T({From,Event,To}) self:UnHandleEvent(EVENTS.Takeoff) self:UnHandleEvent(EVENTS.Land) self:UnHandleEvent(EVENTS.Ejection) self:UnHandleEvent(EVENTS.LandingAfterEjection) self:UnHandleEvent(EVENTS.PlayerEnterUnit) self:UnHandleEvent(EVENTS.PlayerEnterAircraft) self:UnHandleEvent(EVENTS.PilotDead) self:T(self.lid.."Stopped.") return self end function CSAR:onbeforeApproach(From,Event,To,Heliname,Woundedgroupname) self:T({From,Event,To,Heliname,Woundedgroupname}) self:_CheckWoundedGroupStatus(Heliname,Woundedgroupname) return self end function CSAR:onbeforeBoarded(From,Event,To,Heliname,Woundedgroupname) self:T({From,Event,To,Heliname,Woundedgroupname}) self:_ScheduledSARFlight(Heliname,Woundedgroupname) local Unit=UNIT:FindByName(Heliname) if Unit and Unit:IsPlayer()and self.PlayerTaskQueue then local playername=Unit:GetPlayerName() local dropcoord=Unit:GetCoordinate()or COORDINATE:New(0,0,0) local dropvec2=dropcoord:GetVec2() self.PlayerTaskQueue:ForEach( function(Task) local task=Task local subtype=task:GetSubType() if Event==subtype and not task:IsDone()then local targetzone=task.Target:GetObject() if(targetzone and targetzone.ClassName and string.match(targetzone.ClassName,"ZONE")and targetzone:IsVec2InZone(dropvec2)) or(string.find(task.CSARPilotName,Woundedgroupname))then if task.Clients:HasUniqueID(playername)then task:__Success(-1) end end end end ) end return self end function CSAR:onbeforeReturning(From,Event,To,Heliname,Woundedgroupname,IsAirPort) self:T({From,Event,To,Heliname,Woundedgroupname}) return self end function CSAR:onbeforeRescued(From,Event,To,HeliUnit,HeliName,PilotsSaved) self:T({From,Event,To,HeliName,HeliUnit}) self.rescues=self.rescues+1 self.rescuedpilots=self.rescuedpilots+PilotsSaved local Unit=HeliUnit or UNIT:FindByName(HeliName) if Unit and Unit:IsPlayer()and self.PlayerTaskQueue then local playername=Unit:GetPlayerName() self.PlayerTaskQueue:ForEach( function(Task) local task=Task local subtype=task:GetSubType() if Event==subtype and not task:IsDone()then if task.Clients:HasUniqueID(playername)then task:__Success(-1) end end end ) end return self end function CSAR:onbeforePilotDown(From,Event,To,Group,Frequency,Leadername,CoordinatesText,Playername) self:T({From,Event,To,Group,Frequency,Leadername,CoordinatesText,tostring(Playername)}) return self end function CSAR:onbeforeLanded(From,Event,To,HeliName,Airbase) self:T({From,Event,To,HeliName,Airbase}) return self end function CSAR:onbeforeSave(From,Event,To,path,filename) self:T({From,Event,To,path,filename}) if not self.enableLoadSave then return self end if not io then self:E(self.lid.."ERROR: io not desanitized. Can't save current state.") return false end if path==nil and not lfs then self:E(self.lid.."WARNING: lfs not desanitized. State will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.") end return true end function CSAR:onafterSave(From,Event,To,path,filename) self:T({From,Event,To,path,filename}) if not self.enableLoadSave then return self end local function _savefile(filename,data) local f=assert(io.open(filename,"wb")) f:write(data) f:close() end if lfs then path=self.filepath or lfs.writedir() end filename=filename or self.filename if path~=nil then filename=path.."\\"..filename end local pilots=self.downedPilots local data="playerName,x,y,z,coalition,country,description,typeName,unitName,freq\n" local n=0 for _,_grp in pairs(pilots)do local DownedPilot=_grp if DownedPilot and DownedPilot.alive then local playerName=DownedPilot.player local group=DownedPilot.group local coalition=group:GetCoalition() local country=group:GetCountry() local description=DownedPilot.desc local typeName=DownedPilot.typename local freq=DownedPilot.frequency local location=group:GetVec3() local unitName=DownedPilot.originalUnit local txt=string.format("%s,%d,%d,%d,%s,%s,%s,%s,%s,%d\n",playerName,location.x,location.y,location.z,coalition,country,description,typeName,unitName,freq) self:I(self.lid.."Saving to CSAR File: "..txt) data=data..txt end end _savefile(filename,data) if self.enableLoadSave then local interval=self.saveinterval local filename=self.filename local filepath=self.filepath self:__Save(interval,filepath,filename) end return self end function CSAR:onbeforeLoad(From,Event,To,path,filename) self:T({From,Event,To,path,filename}) if not self.enableLoadSave then return self end 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 self.filename path=path or self.filepath if not io then self:E(self.lid.."WARNING: io not desanitized. Cannot load file.") return false end if path==nil and not lfs then self:E(self.lid.."WARNING: lfs not desanitized. State will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.") end if lfs then path=path or lfs.writedir() end 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: State file %s might not exist.",filename)) return false end end function CSAR:onafterLoad(From,Event,To,path,filename) self:T({From,Event,To,path,filename}) if not self.enableLoadSave then return self end local function _loadfile(filename) local f=assert(io.open(filename,"rb")) local data=f:read("*all") f:close() return data end filename=filename or self.filename path=path or self.filepath if lfs then path=path or lfs.writedir() end if path~=nil then filename=path.."\\"..filename end local text=string.format("Loading CSAR state from file %s",filename) MESSAGE:New(text,10):ToAllIf(self.Debug) self:I(self.lid..text) local file=assert(io.open(filename,"rb")) local loadeddata={} for line in file:lines()do loadeddata[#loadeddata+1]=line end file:close() table.remove(loadeddata,1) for _id,_entry in pairs(loadeddata)do local dataset=UTILS.Split(_entry,",") local playerName=dataset[1] local vec3={} vec3.x=tonumber(dataset[2]) vec3.y=tonumber(dataset[3]) vec3.z=tonumber(dataset[4]) local point=COORDINATE:NewFromVec3(vec3) local coalition=tonumber(dataset[5]) local country=tonumber(dataset[6]) local description=dataset[7] local typeName=dataset[8] local unitName=dataset[9] local freq=tonumber(dataset[10]) self:_AddCsar(coalition,country,point,typeName,unitName,playerName,freq,nil,description,nil) 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=COORDINATE:New(AIGroup:GetVec2().x,0,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 self.RTBSpeedMaxFactor=0.6 self.RTBSpeedMinFactor=0.5 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:T(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:T(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:SetRTBSpeedFactors(MinFactor,MaxFactor) self.RTBSpeedMaxFactor=MaxFactor or 0.6 self.RTBSpeedMinFactor=MinFactor or 0.5 return self end function AI_AIR:onafterRTB(AIGroup,From,Event,To) self:F({AIGroup,From,Event,To}) if AIGroup and AIGroup:IsAlive()then self:T("Group "..AIGroup:GetName().." ... RTB! ( "..self:GetState().." )") self:ClearTargetDistance() AIGroup:OptionProhibitAfterburner(true) local EngageRoute={} local FromCoord=AIGroup:GetCoordinate() if not FromCoord then return end local ToTargetCoord=self.HomeAirbase:GetCoordinate() local ToTargetVec3=ToTargetCoord:GetVec3() ToTargetVec3.y=ToTargetCoord:GetLandHeight()+3000 local ToTargetCoord2=COORDINATE:NewFromVec3(ToTargetVec3) if not self.RTBMinSpeed or not self.RTBMaxSpeed then local RTBSpeedMax=AIGroup:GetSpeedMax() local RTBSpeedMaxFactor=self.RTBSpeedMaxFactor or 0.6 local RTBSpeedMinFactor=self.RTBSpeedMinFactor or 0.5 self:SetRTBSpeed(RTBSpeedMax*RTBSpeedMinFactor,RTBSpeedMax*RTBSpeedMaxFactor) end local RTBSpeed=math.random(self.RTBMinSpeed,self.RTBMaxSpeed) local Distance=FromCoord:Get2DDistance(ToTargetCoord2) local ToAirbaseCoord=ToTargetCoord2 if Distance<5000 then self:T("RTB and near the airbase!") self:Home() return end if not AIGroup:InAir()==true then self:T("Not anymore in the air, considered Home.") self:Home() return end local FromRTBRoutePoint=FromCoord:WaypointAir( self.PatrolAltType, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, RTBSpeed, true ) local ToRTBRoutePoint=ToAirbaseCoord:WaypointAir( self.PatrolAltType, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.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:T("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:T("Group "..self.Controllable:GetName().." ... Holding! ( "..self:GetState().." )") if AIGroup and AIGroup:IsAlive()then local Coordinate=AIGroup:GetCoordinate() if Coordinate==nil then return end local OrbitTask=AIGroup:TaskOrbitCircle(math.random(self.PatrolFloorAltitude,self.PatrolCeilingAltitude),self.PatrolMinSpeed,Coordinate) 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:T({"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:T("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,COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.TurningPoint,ToRefuelSpeed,true) local ToRefuelRoutePoint=Tanker:GetCoordinate():WaypointAir(self.PatrolAltType,COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.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",COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.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,COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.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:T(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:T({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:GetRandomSurely():GetPointVec3() if TargetCoord==nil then self:Return() return end 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:__Engage(0.1,AttackSetUnit) else local EngageRoute={} local AttackTasks={} local FromWP=DefenderCoord:WaypointAir(self.PatrolAltType or"RADIO",COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.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",COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.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:T(DefenderGroupName..": No targets found -> Going RTB") self:Return() end end function AI_AIR_ENGAGE.___Engage(AIGroup,Fsm,AttackSetUnit) Fsm:T(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:GetRandomSurely():GetPointVec3() if not TargetCoord then self:Return() return end 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",COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.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",COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.TurningPoint,EngageSpeed,true) EngageRoute[#EngageRoute+1]=ToWP if TargetDistance<=EngageDistance*9 then local AttackUnitTasks=self:CreateAttackUnitTasks(AttackSetUnit,DefenderGroup,EngageAltitude) if#AttackUnitTasks==0 then self:T(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:T(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:T(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:T("Captured "..AirbaseName) for SquadronName,Squadron in pairs(self.DefenderSquadrons)do if Squadron.AirbaseName==AirbaseName then Squadron.ResourceCount=-999 Squadron.Captured=true self:T("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:QuerySquadron(Squadron) local Squadron=self:GetSquadron(Squadron) if Squadron.ResourceCount then self:T2(string.format("%s = %s",Squadron.Name,Squadron.ResourceCount)) return Squadron.ResourceCount end self:F({Squadron=Squadron.Name,SquadronResourceCount=Squadron.ResourceCount}) return nil 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:T({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:T({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 if AIFriendly then local classname=AIFriendly.ClassName or"No Class Name" local unitname=AIFriendly.IdentifiableName or"No Unit Name" end local Friendly=nil if AIFriendly and AIFriendly:IsAlive()then Friendly=AIFriendly:GetGroup() end 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 function AI_A2A_DISPATCHER:AddToSquadron(Squadron,Amount) local Squadron=self:GetSquadron(Squadron) if Squadron.ResourceCount then Squadron.ResourceCount=Squadron.ResourceCount+Amount end self:T({Squadron=Squadron.Name,SquadronResourceCount=Squadron.ResourceCount}) end function AI_A2A_DISPATCHER:RemoveFromSquadron(Squadron,Amount) local Squadron=self:GetSquadron(Squadron) if Squadron.ResourceCount then Squadron.ResourceCount=Squadron.ResourceCount-Amount end self:T({Squadron=Squadron.Name,SquadronResourceCount=Squadron.ResourceCount}) 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:T({Airbases=AirbaseNames}) self:T("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:T({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:T({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:T("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:T({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:T({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:T({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:T({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:T({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:T({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:T({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:T({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:GetRandomSurely() if FirstUnit then local Coordinate=FirstUnit:GetCoordinate() if self.SetSendPlayerMessages then Dispatcher:MessageToPlayers(Squadron,DefenderName..", on route to ground target at "..Coordinate:ToStringA2G(DefenderGroup),DefenderGroup) end else return 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 DetectedThreatLevel=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 DetectedThreatLevel0 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 DetectedThreatLevel=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 DetectedThreatLevel0 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 function AI_A2G_DISPATCHER:AddToSquadron(Squadron,Amount) local Squadron=self:GetSquadron(Squadron) if Squadron.ResourceCount then Squadron.ResourceCount=Squadron.ResourceCount+Amount end self:T({Squadron=Squadron.Name,SquadronResourceCount=Squadron.ResourceCount}) end function AI_A2G_DISPATCHER:RemoveFromSquadron(Squadron,Amount) local Squadron=self:GetSquadron(Squadron) if Squadron.ResourceCount then Squadron.ResourceCount=Squadron.ResourceCount-Amount end self:T({Squadron=Squadron.Name,SquadronResourceCount=Squadron.ResourceCount}) 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 local life=self.Controllable:GetLife()or 0 if self.Controllable:IsAlive()and life>1 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() if not CurrentVec2 then return end local CurrentAltitude=self.Controllable:GetAltitude() local CurrentPointVec3=COORDINATE:New(CurrentVec2.x,CurrentAltitude,CurrentVec2.y) local ToPatrolZoneSpeed=self.PatrolMaxSpeed local CurrentRoutePoint=CurrentPointVec3:WaypointAir( self.PatrolAltType, COORDINATE.WaypointType.TakeOffParking, COORDINATE.WaypointAction.FromParkingArea, ToPatrolZoneSpeed, true ) PatrolRoute[#PatrolRoute+1]=CurrentRoutePoint else self:T("In the air, finding route path within PatrolZone") local CurrentVec2=self.Controllable:GetVec2() if not CurrentVec2 then return end local CurrentAltitude=self.Controllable:GetAltitude() local CurrentPointVec3=COORDINATE:New(CurrentVec2.x,CurrentAltitude,CurrentVec2.y) local ToPatrolZoneSpeed=self.PatrolMaxSpeed local CurrentRoutePoint=CurrentPointVec3:WaypointAir( self.PatrolAltType, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.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=COORDINATE:New(ToTargetVec2.x,ToTargetAltitude,ToTargetVec2.y) local ToTargetRoutePoint=ToTargetPointVec3:WaypointAir( self.PatrolAltType, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.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() if not CurrentVec2 then return self end local CurrentAltitude=self.Controllable:GetAltitude() local CurrentPointVec3=COORDINATE:New(CurrentVec2.x,CurrentAltitude,CurrentVec2.y) local ToEngageZoneSpeed=self.PatrolMaxSpeed local CurrentRoutePoint=CurrentPointVec3:WaypointAir( self.PatrolAltType, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.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=COORDINATE:New(ToTargetVec2.x,ToTargetAltitude,ToTargetVec2.y) local ToPatrolRoutePoint=ToTargetPointVec3:WaypointAir( self.PatrolAltType, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.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:GetAltitude() local CurrentPointVec3=COORDINATE:New(CurrentVec2.x,CurrentAltitude,CurrentVec2.y) local ToEngageZoneSpeed=self.PatrolMaxSpeed local CurrentRoutePoint=CurrentPointVec3:WaypointAir( self.PatrolAltType, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.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=COORDINATE:New(ToTargetVec2.x,self.EngageAltitude,ToTargetVec2.y) local ToTargetRoutePoint=ToTargetPointVec3:WaypointAir( self.PatrolAltType, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.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:GetAltitude() local CurrentPointVec3=COORDINATE:New(CurrentVec2.x,CurrentAltitude,CurrentVec2.y) local ToEngageZoneSpeed=self.PatrolMaxSpeed local CurrentRoutePoint=CurrentPointVec3:WaypointAir( self.PatrolAltType, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.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=COORDINATE:New(ToTargetVec2.x,self.EngageAltitude,ToTargetVec2.y) local ToTargetRoutePoint=ToTargetPointVec3:WaypointAir( self.PatrolAltType, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.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=COORDINATE: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=COORDINATE: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=COORDINATE: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.Menu.HoldAtEscortPosition=self.Menu.HoldAtEscortPosition or{} self.Menu.HoldAtLeaderPosition=self.Menu.HoldAtLeaderPosition or{} self.Menu.Flare=self.Menu.Flare or{} self.Menu.Smoke=self.Menu.Smoke or{} self.Menu.Targets=self.Menu.Targets or{} self.Menu.ROE=self.Menu.ROE or{} self.Menu.ROT=self.Menu.ROT or{} 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 or{})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 or{})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 or{})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 or{})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 or{})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 or{})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 or{})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 or{})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 or{})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 or{}or{})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 or{})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 or{})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 or{})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 or{})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:T({EscortAirbase=self.EscortAirbase}) self:T({PlayerGroupName=PlayerGroupName}) self:T({PlayerGroup=PlayerGroup}) self:T({FirstGroup=self.CarrierSet:GetFirst()}) self:T({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:T({EscortAirbase=self.EscortAirbase}) self:T({PlayerGroupName=PlayerGroupName}) self:T({PlayerGroup=PlayerGroup}) self:T({FirstGroup=self.CarrierSet:GetFirst()}) self:T({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:T({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","Unloaded") self:AddTransition("*","Load","*") self:AddTransition("*","Reload","*") self:AddTransition("*","Board","*") self:AddTransition("*","Loaded","Loaded") self:AddTransition("Loaded","PickedUp","Loaded") self:AddTransition("Loaded","Deploy","*") self:AddTransition("*","Unload","*") self:AddTransition("*","Unboard","*") self:AddTransition("*","Unloaded","Unloaded") self:AddTransition("Unloaded","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()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()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:T({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("*","Deployed","*") self:AddTransition("*","PickedUp","*") 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","Unloaded") self:AddTransition("*","Landed","*") self:AddTransition("*","Load","*") self:AddTransition("*","Loaded","Loaded") self:AddTransition("Loaded","PickedUp","Loaded") self:AddTransition("Loaded","Deploy","*") self:AddTransition("*","Queue","*") self:AddTransition("*","Orbit","*") self:AddTransition("*","Destroyed","*") self:AddTransition("*","Unload","*") self:AddTransition("*","Unloaded","Unloaded") self:AddTransition("Unloaded","Deployed","Unloaded") self:AddTransition("*","Home","*") 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) self.landingspeed=15 self.landingheight=5.5 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:SetLandingSpeedAndHeight(speed,height) local _speed=speed or 15 local _height=height or 5.5 self.landingheight=_height self.landingspeed=_speed 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:T({Helicopter:GetName(),Height=Helicopter:GetHeight(true),Velocity=Helicopter:GetVelocityKMH()}) if self.RoutePickup==true then if Helicopter:GetHeight(true)<=self.landingheight then self:Load(self.PickupZone) self.RoutePickup=false end end if self.RouteDeploy==true then if Helicopter:GetHeight(true)<=self.landingheight 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", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.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",COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.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",COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.TurningPoint,_speed,true) local CoordinateTo=Coordinate local landheight=CoordinateTo:GetLandHeight() CoordinateTo.y=landheight+50 local WaypointTo=CoordinateTo:WaypointAir("RADIO",COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.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",COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.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",COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.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",COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.TurningPoint,Speed,true) Route[#Route+1]=WaypointFrom local CoordinateTo=Coordinate local landheight=CoordinateTo:GetLandHeight() CoordinateTo.y=landheight+Height local WaypointTo=CoordinateTo:WaypointAir("RADIO",COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.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(COORDINATE.WaypointAltType.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:T(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(40,12) self:SetDeployRadius(40,12) self:SetPickupHeight(500,200) self:SetDeployHeight(500,200) return self end function AI_CARGO_DISPATCHER_HELICOPTER:AICargo(Helicopter,CargoSet) local dispatcher=AI_CARGO_HELICOPTER:New(Helicopter,CargoSet) dispatcher:SetLandingSpeedAndHeight(27,6) return dispatcher 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 SHAPE_BASE={ ClassName="SHAPE_BASE", Name="", CenterVec2=nil, Points={}, Coords={}, MarkIDs={}, ColorString="", ColorRGBA={} } function SHAPE_BASE:New() local self=BASE:Inherit(self,BASE:New()) return self end function SHAPE_BASE:FindOnMap(shape_name) local self=BASE:Inherit(self,BASE:New()) local found=false for _,layer in pairs(env.mission.drawings.layers)do for _,object in pairs(layer["objects"])do if object["name"]==shape_name then self.Name=object["name"] self.CenterVec2={x=object["mapX"],y=object["mapY"]} self.ColorString=object["colorString"] self.ColorRGBA=UTILS.HexToRGBA(self.ColorString) found=true end end end if not found then self:E("Can't find a shape with name "..shape_name) end return self end function SHAPE_BASE:GetAllShapes(filter) filter=filter or"" local return_shapes={} for _,layer in pairs(env.mission.drawings.layers)do for _,object in pairs(layer["objects"])do if string.contains(object["name"],filter)then table.add(return_shapes,object) end end end return return_shapes end function SHAPE_BASE:Offset(new_vec2) local offset_vec2=UTILS.Vec2Subtract(new_vec2,self.CenterVec2) self.CenterVec2=new_vec2 if self.ClassName=="POLYGON"then for _,point in pairs(self.Points)do point.x=point.x+offset_vec2.x point.y=point.y+offset_vec2.y end end end function SHAPE_BASE:GetName() return self.Name end function SHAPE_BASE:GetColorString() return self.ColorString end function SHAPE_BASE:GetColorRGBA() return self.ColorRGBA end function SHAPE_BASE:GetColorRed() return self.ColorRGBA.R end function SHAPE_BASE:GetColorGreen() return self.ColorRGBA.G end function SHAPE_BASE:GetColorBlue() return self.ColorRGBA.B end function SHAPE_BASE:GetColorAlpha() return self.ColorRGBA.A end function SHAPE_BASE:GetCenterVec2() return self.CenterVec2 end function SHAPE_BASE:GetCenterCoordinate() return COORDINATE:NewFromVec2(self.CenterVec2) end function SHAPE_BASE:GetCoordinate() return self:GetCenterCoordinate() end function SHAPE_BASE:ContainsPoint(_) self:E("This needs to be set in the derived class") end function SHAPE_BASE:ContainsUnit(unit_name) local unit=UNIT:FindByName(unit_name) if unit==nil or not unit:IsAlive()then return false end if self:ContainsPoint(unit:GetVec2())then return true end return false end function SHAPE_BASE:ContainsAnyOfGroup(group_name) local group=GROUP:FindByName(group_name) if group==nil or not group:IsAlive()then return false end for _,unit in pairs(group:GetUnits())do if self:ContainsPoint(unit:GetVec2())then return true end end return false end function SHAPE_BASE:ContainsAllOfGroup(group_name) local group=GROUP:FindByName(group_name) if group==nil or not group:IsAlive()then return false end for _,unit in pairs(group:GetUnits())do if not self:ContainsPoint(unit:GetVec2())then return false end end return true end CIRCLE={ ClassName="CIRCLE", Radius=nil, } function CIRCLE:FindOnMap(shape_name) local self=BASE:Inherit(self,SHAPE_BASE:FindOnMap(shape_name)) for _,layer in pairs(env.mission.drawings.layers)do for _,object in pairs(layer["objects"])do if string.find(object["name"],shape_name,1,true)then if object["polygonMode"]=="circle"then self.Radius=object["radius"] end end end end return self end function CIRCLE:Find(shape_name) return _DATABASE:FindShape(shape_name) end function CIRCLE:New(vec2,radius) local self=BASE:Inherit(self,SHAPE_BASE:New()) self.CenterVec2=vec2 self.Radius=radius return self end function CIRCLE:GetRadius() return self.Radius end function CIRCLE:ContainsPoint(point) if((point.x-self.CenterVec2.x)^2+(point.y-self.CenterVec2.y)^2)^0.5<=self.Radius then return true end return false end function CIRCLE:PointInSector(point,sector_start,sector_end,center,radius) center=center or self.CenterVec2 radius=radius or self.Radius local function are_clockwise(v1,v2) return-v1.x*v2.y+v1.y*v2.x>0 end local function is_in_radius(rp) return rp.x*rp.x+rp.y*rp.y<=radius^2 end local rel_pt={ x=point.x-center.x, y=point.y-center.y } local rel_sector_start={ x=sector_start.x-center.x, y=sector_start.y-center.y, } local rel_sector_end={ x=sector_end.x-center.x, y=sector_end.y-center.y, } return not are_clockwise(rel_sector_start,rel_pt)and are_clockwise(rel_sector_end,rel_pt)and is_in_radius(rel_pt,radius) end function CIRCLE:UnitInSector(unit_name,sector_start,sector_end,center,radius) center=center or self.CenterVec2 radius=radius or self.Radius if self:PointInSector(UNIT:FindByName(unit_name):GetVec2(),sector_start,sector_end,center,radius)then return true end return false end function CIRCLE:AnyOfGroupInSector(group_name,sector_start,sector_end,center,radius) center=center or self.CenterVec2 radius=radius or self.Radius for _,unit in pairs(GROUP:FindByName(group_name):GetUnits())do if self:PointInSector(unit:GetVec2(),sector_start,sector_end,center,radius)then return true end end return false end function CIRCLE:AllOfGroupInSector(group_name,sector_start,sector_end,center,radius) center=center or self.CenterVec2 radius=radius or self.Radius for _,unit in pairs(GROUP:FindByName(group_name):GetUnits())do if not self:PointInSector(unit:GetVec2(),sector_start,sector_end,center,radius)then return false end end return true end function CIRCLE:UnitInRadius(unit_name,center,radius) center=center or self.CenterVec2 radius=radius or self.Radius if UTILS.IsInRadius(center,UNIT:FindByName(unit_name):GetVec2(),radius)then return true end return false end function CIRCLE:AnyOfGroupInRadius(group_name,center,radius) center=center or self.CenterVec2 radius=radius or self.Radius for _,unit in pairs(GROUP:FindByName(group_name):GetUnits())do if UTILS.IsInRadius(center,unit:GetVec2(),radius)then return true end end return false end function CIRCLE:AllOfGroupInRadius(group_name,center,radius) center=center or self.CenterVec2 radius=radius or self.Radius for _,unit in pairs(GROUP:FindByName(group_name):GetUnits())do if not UTILS.IsInRadius(center,unit:GetVec2(),radius)then return false end end return true end function CIRCLE:GetRandomVec2() local angle=math.random()*2*math.pi local rx=math.random(0,self.Radius)*math.cos(angle)+self.CenterVec2.x local ry=math.random(0,self.Radius)*math.sin(angle)+self.CenterVec2.y return{x=rx,y=ry} end function CIRCLE:GetRandomVec2OnBorder() local angle=math.random()*2*math.pi local rx=self.Radius*math.cos(angle)+self.CenterVec2.x local ry=self.Radius*math.sin(angle)+self.CenterVec2.y return{x=rx,y=ry} end function CIRCLE:GetBoundingBox() local min_x=self.CenterVec2.x-self.Radius local min_y=self.CenterVec2.y-self.Radius local max_x=self.CenterVec2.x+self.Radius local max_y=self.CenterVec2.y+self.Radius return{ {x=min_x,y=min_x},{x=max_x,y=min_y},{x=max_x,y=max_y},{x=min_x,y=max_y} } end CUBE={ ClassName="CUBE", Points={}, Coords={} } function CUBE:New(p1,p2,p3,p4,p5,p6,p7,p8) local self=BASE:Inherit(self,SHAPE_BASE) self.Points={p1,p2,p3,p4,p5,p6,p7,p8} for _,point in spairs(self.Points)do table.insert(self.Coords,COORDINATE:NewFromVec3(point)) end return self end function CUBE:GetCenter() local center={x=0,y=0,z=0} for _,point in pairs(self.Points)do center.x=center.x+point.x center.y=center.y+point.y center.z=center.z+point.z end center.x=center.x/8 center.y=center.y/8 center.z=center.z/8 return center end function CUBE:ContainsPoint(point,cube_points) cube_points=cube_points or self.Points local min_x,min_y,min_z=math.huge,math.huge,math.huge local max_x,max_y,max_z=-math.huge,-math.huge,-math.huge for _,p in ipairs(cube_points)do if p.xmax_x then max_x=p.x end if p.y>max_y then max_y=p.y end if p.z>max_z then max_z=p.z end end return point.x>=min_x and point.x<=max_x and point.y>=min_y and point.y<=max_y and point.z>=min_z and point.z<=max_z end LINE={ ClassName="LINE", Points={}, Coords={}, } function LINE:FindOnMap(line_name) local self=BASE:Inherit(self,SHAPE_BASE:FindOnMap(line_name)) for _,layer in pairs(env.mission.drawings.layers)do for _,object in pairs(layer["objects"])do if object["name"]==line_name then if object["primitiveType"]=="Line"then for _,point in UTILS.spairs(object["points"])do local p={x=object["mapX"]+point["x"], y=object["mapY"]+point["y"]} local coord=COORDINATE:NewFromVec2(p) table.insert(self.Points,p) table.insert(self.Coords,coord) end end end end end self:I(#self.Points) if#self.Points==0 then return nil end self.MarkIDs={} return self end function LINE:Find(shape_name) return _DATABASE:FindShape(shape_name) end function LINE:New(...) local self=BASE:Inherit(self,SHAPE_BASE:New()) self.Points={...} self:I(self.Points) for _,point in UTILS.spairs(self.Points)do table.insert(self.Coords,COORDINATE:NewFromVec2(point)) end return self end function LINE:NewFromCircle(center_point,radius,angle_degrees) local self=BASE:Inherit(self,SHAPE_BASE:New()) self.CenterVec2=center_point local angleRadians=math.rad(angle_degrees) local point1={ x=center_point.x+radius*math.cos(angleRadians), y=center_point.y+radius*math.sin(angleRadians) } local point2={ x=center_point.x+radius*math.cos(angleRadians+math.pi), y=center_point.y+radius*math.sin(angleRadians+math.pi) } for _,point in pairs{point1,point2}do table.insert(self.Points,point) table.insert(self.Coords,COORDINATE:NewFromVec2(point)) end return self end function LINE:Coordinates() return self.Coords end function LINE:GetStartCoordinate() return self.Coords[1] end function LINE:GetEndCoordinate() return self.Coords[#self.Coords] end function LINE:GetStartPoint() return self.Points[1] end function LINE:GetEndPoint() return self.Points[#self.Points] end function LINE:GetLength() local total_length=0 for i=1,#self.Points-1 do local x1,y1=self.Points[i]["x"],self.Points[i]["y"] local x2,y2=self.Points[i+1]["x"],self.Points[i+1]["y"] local segment_length=math.sqrt((x2-x1)^2+(y2-y1)^2) total_length=total_length+segment_length end return total_length end function LINE:GetRandomPoint(points) points=points or self.Points local rand=math.random() local random_x=points[1].x+rand*(points[2].x-points[1].x) local random_y=points[1].y+rand*(points[2].y-points[1].y) return{x=random_x,y=random_y} end function LINE:GetHeading(points) points=points or self.Points local angle=math.atan2(points[2].y-points[1].y,points[2].x-points[1].x) angle=math.deg(angle) if angle<0 then angle=angle+360 end return angle end function LINE:GetIndividualParts() local parts={} if#self.Points==2 then parts={self} end for i=1,#self.Points-1 do local p1=self.Points[i] local p2=self.Points[i%#self.Points+1] table.add(parts,LINE:New(p1,p2)) end return parts end function LINE:GetPointsInbetween(amount,start_point,end_point) start_point=start_point or self:GetStartPoint() end_point=end_point or self:GetEndPoint() if amount==0 then return{start_point,end_point}end amount=amount+1 local points={} local difference={x=end_point.x-start_point.x,y=end_point.y-start_point.y} local divided={x=difference.x/amount,y=difference.y/amount} for j=0,amount do local part_pos={x=divided.x*j,y=divided.y*j} local point={x=start_point.x+part_pos.x,y=start_point.y+part_pos.y} table.insert(points,point) end return points end function LINE:GetCoordinatesInBetween(amount,start_point,end_point) local coords={} for _,pt in pairs(self:GetPointsInbetween(amount,start_point,end_point))do table.add(coords,COORDINATE:NewFromVec2(pt)) end return coords end function LINE:GetRandomPoint(start_point,end_point) start_point=start_point or self:GetStartPoint() end_point=end_point or self:GetEndPoint() local fraction=math.random() local difference={x=end_point.x-start_point.x,y=end_point.y-start_point.y} local part_pos={x=difference.x*fraction,y=difference.y*fraction} local random_point={x=start_point.x+part_pos.x,y=start_point.y+part_pos.y} return random_point end function LINE:GetRandomCoordinate(start_point,end_point) start_point=start_point or self:GetStartPoint() end_point=end_point or self:GetEndPoint() return COORDINATE:NewFromVec2(self:GetRandomPoint(start_point,end_point)) end function LINE:GetPointsBetweenAsSineWave(amount,start_point,end_point,frequency,phase,amplitude) amount=amount or 20 start_point=start_point or self:GetStartPoint() end_point=end_point or self:GetEndPoint() frequency=frequency or 1 phase=phase or 0 amplitude=amplitude or 100 local points={} local function sine_wave(x) return amplitude*math.sin(2*math.pi*frequency*(x-start_point.x)+phase) end local x=start_point.x local step=(end_point.x-start_point.x)/20 for _=1,amount do local y=sine_wave(x) x=x+step table.add(points,{x=x,y=y}) end return points end function LINE:GetBoundingBox() local min_x,min_y,max_x,max_y=self.Points[1].x,self.Points[1].y,self.Points[2].x,self.Points[2].y for i=2,#self.Points do local x,y=self.Points[i].x,self.Points[i].y if xmax_x then max_x=x end if y>max_y then max_y=y end end return{ {x=min_x,y=min_x},{x=max_x,y=min_y},{x=max_x,y=max_y},{x=min_x,y=max_y} } end function LINE:Draw() for i=1,#self.Coords-1 do local c1=self.Coords[i] local c2=self.Coords[i%#self.Coords+1] table.add(self.MarkIDs,c1:LineToAll(c2)) end end function LINE:RemoveDraw() for _,mark_id in pairs(self.MarkIDs)do UTILS.RemoveMark(mark_id) end end OVAL={ ClassName="OVAL", MajorAxis=nil, MinorAxis=nil, Angle=0, DrawPoly=nil } function OVAL:FindOnMap(shape_name) local self=BASE:Inherit(self,SHAPE_BASE:FindOnMap(shape_name)) for _,layer in pairs(env.mission.drawings.layers)do for _,object in pairs(layer["objects"])do if string.find(object["name"],shape_name,1,true)then if object["polygonMode"]=="oval"then self.CenterVec2={x=object["mapX"],y=object["mapY"]} self.MajorAxis=object["r1"] self.MinorAxis=object["r2"] self.Angle=object["angle"] end end end end return self end function OVAL:Find(shape_name) return _DATABASE:FindShape(shape_name) end function OVAL:New(vec2,major_axis,minor_axis,angle) local self=BASE:Inherit(self,SHAPE_BASE:New()) self.CenterVec2=vec2 self.MajorAxis=major_axis self.MinorAxis=minor_axis self.Angle=angle or 0 return self end function OVAL:GetMajorAxis() return self.MajorAxis end function OVAL:GetMinorAxis() return self.MinorAxis end function OVAL:GetAngle() return self.Angle end function OVAL:SetMajorAxis(value) self.MajorAxis=value end function OVAL:SetMinorAxis(value) self.MinorAxis=value end function OVAL:SetAngle(value) self.Angle=value end function OVAL:ContainsPoint(point) local cos,sin=math.cos,math.sin local dx=point.x-self.CenterVec2.x local dy=point.y-self.CenterVec2.y local rx=dx*cos(self.Angle)+dy*sin(self.Angle) local ry=-dx*sin(self.Angle)+dy*cos(self.Angle) return rx*rx/(self.MajorAxis*self.MajorAxis)+ry*ry/(self.MinorAxis*self.MinorAxis)<=1 end function OVAL:GetRandomVec2() local theta=math.rad(self.Angle) local random_point=math.sqrt(math.random()) local phi=math.random()*2*math.pi local x_c=random_point*math.cos(phi) local y_c=random_point*math.sin(phi) local x_e=x_c*self.MajorAxis local y_e=y_c*self.MinorAxis local rx=(x_e*math.cos(theta)-y_e*math.sin(theta))+self.CenterVec2.x local ry=(x_e*math.sin(theta)+y_e*math.cos(theta))+self.CenterVec2.y return{x=rx,y=ry} end function OVAL:GetBoundingBox() local min_x=self.CenterVec2.x-self.MajorAxis local min_y=self.CenterVec2.y-self.MinorAxis local max_x=self.CenterVec2.x+self.MajorAxis local max_y=self.CenterVec2.y+self.MinorAxis return{ {x=min_x,y=min_x},{x=max_x,y=min_y},{x=max_x,y=max_y},{x=min_x,y=max_y} } end function OVAL:Draw() self.DrawPoly=POLYGON:NewFromPoints(self:PointsOnEdge(20)) self.DrawPoly:Draw(true) end function OVAL:RemoveDraw() self.DrawPoly:RemoveDraw() end function OVAL:PointsOnEdge(num_points) num_points=num_points or 20 local points={} local dtheta=2*math.pi/num_points for i=0,num_points-1 do local theta=i*dtheta local x=self.CenterVec2.x+self.MajorAxis*math.cos(theta)*math.cos(self.Angle)-self.MinorAxis*math.sin(theta)*math.sin(self.Angle) local y=self.CenterVec2.y+self.MajorAxis*math.cos(theta)*math.sin(self.Angle)+self.MinorAxis*math.sin(theta)*math.cos(self.Angle) table.insert(points,{x=x,y=y}) end return points end POLYGON={ ClassName="POLYGON", Points={}, Coords={}, Triangles={}, SurfaceArea=0, TriangleMarkIDs={}, OutlineMarkIDs={}, Angle=nil, Heading=nil } function POLYGON:FindOnMap(shape_name) local self=BASE:Inherit(self,SHAPE_BASE:FindOnMap(shape_name)) for _,layer in pairs(env.mission.drawings.layers)do for _,object in pairs(layer["objects"])do if object["name"]==shape_name then if(object["primitiveType"]=="Line"and object["closed"]==true)or(object["polygonMode"]=="free")then for _,point in UTILS.spairs(object["points"])do local p={x=object["mapX"]+point["x"], y=object["mapY"]+point["y"]} local coord=COORDINATE:NewFromVec2(p) self.Points[#self.Points+1]=p self.Coords[#self.Coords+1]=coord end elseif object["polygonMode"]=="rect"then local angle=object["angle"] local half_width=object["width"]/2 local half_height=object["height"]/2 local p1=UTILS.RotatePointAroundPivot({x=self.CenterVec2.x-half_height,y=self.CenterVec2.y+half_width},self.CenterVec2,angle) local p2=UTILS.RotatePointAroundPivot({x=self.CenterVec2.x+half_height,y=self.CenterVec2.y+half_width},self.CenterVec2,angle) local p3=UTILS.RotatePointAroundPivot({x=self.CenterVec2.x+half_height,y=self.CenterVec2.y-half_width},self.CenterVec2,angle) local p4=UTILS.RotatePointAroundPivot({x=self.CenterVec2.x-half_height,y=self.CenterVec2.y-half_width},self.CenterVec2,angle) self.Points={p1,p2,p3,p4} for _,point in pairs(self.Points)do self.Coords[#self.Coords+1]=COORDINATE:NewFromVec2(point) end elseif object["polygonMode"]=="arrow"then for _,point in UTILS.spairs(object["points"])do local p={x=object["mapX"]+point["x"], y=object["mapY"]+point["y"]} local coord=COORDINATE:NewFromVec2(p) self.Points[#self.Points+1]=p self.Coords[#self.Coords+1]=coord end self.Angle=object["angle"] self.Heading=UTILS.ClampAngle(self.Angle+90) end end end end if#self.Points==0 then return nil end self.CenterVec2=self:GetCentroid() self.Triangles=self:Triangulate() self.SurfaceArea=self:__CalculateSurfaceArea() self.TriangleMarkIDs={} self.OutlineMarkIDs={} return self end function POLYGON:FromZone(zone_name) for _,zone in pairs(env.mission.triggers.zones)do if zone["name"]==zone_name then return POLYGON:New(unpack(zone["verticies"]or{})) end end end function POLYGON:Find(shape_name) return _DATABASE:FindShape(shape_name) end function POLYGON:New(...) local self=BASE:Inherit(self,SHAPE_BASE:New()) self.Points={...} self.Coords={} for _,point in UTILS.spairs(self.Points)do table.insert(self.Coords,COORDINATE:NewFromVec2(point)) end self.Triangles=self:Triangulate() self.SurfaceArea=self:__CalculateSurfaceArea() return self end function POLYGON:GetCentroid() local function sum(t) local total=0 for _,value in pairs(t)do total=total+value end return total end local x_values={} local y_values={} local length=table.length(self.Points) for _,point in pairs(self.Points)do table.insert(x_values,point.x) table.insert(y_values,point.y) end local x=sum(x_values)/length local y=sum(y_values)/length return{ ["x"]=x, ["y"]=y } end function POLYGON:GetCoordinates() return self.Coords end function POLYGON:GetStartCoordinate() return self.Coords[1] end function POLYGON:GetEndCoordinate() return self.Coords[#self.Coords] end function POLYGON:GetStartPoint() return self.Points[1] end function POLYGON:GetEndPoint() return self.Points[#self.Points] end function POLYGON:GetPoints() return self.Points end function POLYGON:GetSurfaceArea() return self.SurfaceArea end function POLYGON:GetBoundingBox() local min_x,min_y,max_x,max_y=self.Points[1].x,self.Points[1].y,self.Points[1].x,self.Points[1].y for i=2,#self.Points do local x,y=self.Points[i].x,self.Points[i].y if xmax_x then max_x=x end if y>max_y then max_y=y end end return{ {x=min_x,y=min_x},{x=max_x,y=min_y},{x=max_x,y=max_y},{x=min_x,y=max_y} } end function POLYGON:Triangulate(points) points=points or self.Points local triangles={} local function get_orientation(shape_points) local sum=0 for i=1,#shape_points do local j=i%#shape_points+1 sum=sum+(shape_points[j].x-shape_points[i].x)*(shape_points[j].y+shape_points[i].y) end return sum>=0 and"clockwise"or"counter-clockwise" end local function ensure_clockwise(shape_points) local orientation=get_orientation(shape_points) if orientation=="counter-clockwise"then local reversed={} for i=#shape_points,1,-1 do table.insert(reversed,shape_points[i]) end return reversed end return shape_points end local function is_clockwise(p1,p2,p3) local cross_product=(p2.x-p1.x)*(p3.y-p1.y)-(p2.y-p1.y)*(p3.x-p1.x) return cross_product<0 end local function divide_recursively(shape_points) if#shape_points==3 then table.insert(triangles,TRIANGLE:New(shape_points[1],shape_points[2],shape_points[3])) elseif#shape_points>3 then for i,p1 in ipairs(shape_points)do local p2=shape_points[(i%#shape_points)+1] local p3=shape_points[(i+1)%#shape_points+1] local triangle=TRIANGLE:New(p1,p2,p3) local is_ear=true if not is_clockwise(p1,p2,p3)then is_ear=false else for _,point in ipairs(shape_points)do if point~=p1 and point~=p2 and point~=p3 and triangle:ContainsPoint(point)then is_ear=false break end end end if is_ear then local is_valid_triangle=true for _,point in ipairs(points)do if point~=p1 and point~=p2 and point~=p3 and triangle:ContainsPoint(point)then is_valid_triangle=false break end end if is_valid_triangle then table.insert(triangles,triangle) local remaining_points={} for j,point in ipairs(shape_points)do if point~=p2 then table.insert(remaining_points,point) end end divide_recursively(remaining_points) break end end end end end points=ensure_clockwise(points) divide_recursively(points) return triangles end function POLYGON:CovarianceMatrix() local cx,cy=self:GetCentroid() local covXX,covYY,covXY=0,0,0 for _,p in ipairs(self.points)do covXX=covXX+(p.x-cx)^2 covYY=covYY+(p.y-cy)^2 covXY=covXY+(p.x-cx)*(p.y-cy) end covXX=covXX/(#self.points-1) covYY=covYY/(#self.points-1) covXY=covXY/(#self.points-1) return covXX,covYY,covXY end function POLYGON:Direction() local covXX,covYY,covXY=self:CovarianceMatrix() local theta=0.5*math.atan2(2*covXY,covXX-covYY) return math.cos(theta),math.sin(theta) end function POLYGON:GetRandomVec2() local weights={} for _,triangle in pairs(self.Triangles)do weights[triangle]=triangle.SurfaceArea/self.SurfaceArea end local random_weight=math.random() local accumulated_weight=0 for triangle,weight in pairs(weights)do accumulated_weight=accumulated_weight+weight if accumulated_weight>=random_weight then return triangle:GetRandomVec2() end end end function POLYGON:GetRandomNonWeightedVec2() return self.Triangles[math.random(1,#self.Triangles)]:GetRandomVec2() end function POLYGON:ContainsPoint(point,polygon_points) local x=point.x local y=point.y polygon_points=polygon_points or self.Points local counter=0 local num_points=#polygon_points for current_index=1,num_points do local next_index=(current_index%num_points)+1 local current_x,current_y=polygon_points[current_index].x,polygon_points[current_index].y local next_x,next_y=polygon_points[next_index].x,polygon_points[next_index].y if((current_y>y)~=(next_y>y))and(x<(next_x-current_x)*(y-current_y)/(next_y-current_y)+current_x)then counter=counter+1 end end return counter%2==1 end function POLYGON:Draw(include_inner_triangles) include_inner_triangles=include_inner_triangles or false for i=1,#self.Coords do local c1=self.Coords[i] local c2=self.Coords[i%#self.Coords+1] table.add(self.OutlineMarkIDs,c1:LineToAll(c2)) end if include_inner_triangles then for _,triangle in ipairs(self.Triangles)do triangle:Draw() end end end function POLYGON:RemoveDraw() for _,triangle in pairs(self.Triangles)do triangle:RemoveDraw() end for _,mark_id in pairs(self.OutlineMarkIDs)do UTILS.RemoveMark(mark_id) end end function POLYGON:__CalculateSurfaceArea() local area=0 for _,triangle in pairs(self.Triangles)do area=area+triangle.SurfaceArea end return area end TRIANGLE={ ClassName="TRIANGLE", Points={}, Coords={}, SurfaceArea=0 } function TRIANGLE:New(p1,p2,p3) local self=BASE:Inherit(self,SHAPE_BASE:New()) self.Points={p1,p2,p3} local center_x=(p1.x+p2.x+p3.x)/3 local center_y=(p1.y+p2.y+p3.y)/3 self.CenterVec2={x=center_x,y=center_y} for _,pt in pairs({p1,p2,p3})do table.add(self.Coords,COORDINATE:NewFromVec2(pt)) end self.SurfaceArea=math.abs((p2.x-p1.x)*(p3.y-p1.y)-(p3.x-p1.x)*(p2.y-p1.y))*0.5 self.MarkIDs={} return self end function TRIANGLE:ContainsPoint(pt,points) points=points or self.Points local function sign(p1,p2,p3) return(p1.x-p3.x)*(p2.y-p3.y)-(p2.x-p3.x)*(p1.y-p3.y) end local d1=sign(pt,self.Points[1],self.Points[2]) local d2=sign(pt,self.Points[2],self.Points[3]) local d3=sign(pt,self.Points[3],self.Points[1]) local has_neg=(d1<0)or(d2<0)or(d3<0) local has_pos=(d1>0)or(d2>0)or(d3>0) return not(has_neg and has_pos) end function TRIANGLE:GetRandomVec2(points) points=points or self.Points local pt={math.random(),math.random()} table.sort(pt) local s=pt[1] local t=pt[2]-pt[1] local u=1-pt[2] return{x=s*points[1].x+t*points[2].x+u*points[3].x, y=s*points[1].y+t*points[2].y+u*points[3].y} end function TRIANGLE:Draw() for i=1,#self.Coords do local c1=self.Coords[i] local c2=self.Coords[i%#self.Coords+1] table.add(self.MarkIDs,c1:LineToAll(c2)) end end function TRIANGLE:RemoveDraw() for _,mark_id in pairs(self.MarkIDs)do UTILS.RemoveMark(mark_id) 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 function USERSOUND:ToUnit(Unit,Delay) Delay=Delay or 0 if Delay>0 then SCHEDULER:New(nil,USERSOUND.ToUnit,{self,Unit},Delay) else trigger.action.outSoundForUnit(Unit:GetID(),self.UserSoundFileName) end return self end function USERSOUND:ToClient(Client,Delay) Delay=Delay or 0 if Delay>0 then SCHEDULER:New(nil,USERSOUND.ToClient,{self,Client},Delay) else trigger.action.outSoundForUnit(Client:GetID(),self.UserSoundFileName) end return self end end do SOUNDBASE={ ClassName="SOUNDBASE", } function SOUNDBASE:New() local self=BASE:Inherit(self,BASE:New()) return self end function SOUNDBASE:GetSpeechTime(length,speed,isGoogle) local maxRateRatio=3 speed=speed or 1.0 isGoogle=isGoogle or false local speedFactor=1.0 if isGoogle then speedFactor=speed else if speed~=0 then speedFactor=math.abs(speed)*(maxRateRatio-1)/10+1 end if speed<0 then speedFactor=1/speedFactor end end local wpm=math.ceil(100*speedFactor) local cps=math.floor((wpm*5)/60) if type(length)=="string"then length=string.len(length) end return math.ceil(length/cps) end end do SOUNDFILE={ ClassName="SOUNDFILE", filename=nil, path="l10n/DEFAULT/", duration=3, subtitle=nil, subduration=0, useSRS=false, } function SOUNDFILE:New(FileName,Path,Duration,UseSrs) local self=BASE:Inherit(self,BASE:New()) self:F({FileName,Path,Duration,UseSrs}) self:SetFileName(FileName) self:SetPlayWithSRS(UseSrs or false) self:SetPath(Path) self:SetDuration(Duration) return self end function SOUNDFILE:SetPath(Path) self:F({Path}) if not Path then if self.useSRS then self.path=lfs.tempdir().."Mission\\l10n\\DEFAULT" else self.path="l10n/DEFAULT/" end else self.path=Path end local nmax=1000;local n=1 while(self.path:sub(-1)=="/"or self.path:sub(-1)==[[\]])and n<=nmax do self.path=self.path:sub(1,#self.path-1) n=n+1 end self.path=self.path.."/" self:T("self.path="..self.path) return self end function SOUNDFILE:GetPath() local path=self.path or"l10n/DEFAULT/" return path end function SOUNDFILE:SetFileName(FileName) self.filename=FileName or"Hello World.mp3" return self end function SOUNDFILE:GetFileName() return self.filename end function SOUNDFILE:SetDuration(Duration) if Duration and type(Duration)=="string"then Duration=tonumber(Duration) end self.duration=Duration or 3 return self end function SOUNDFILE:GetDuration() return self.duration or 3 end function SOUNDFILE:GetName() local path=self:GetPath() local filename=self:GetFileName() local name=string.format("%s%s",path,filename) return name end function SOUNDFILE:SetPlayWithSRS(Switch) self:F({Switch}) if Switch==true or Switch==nil then self.useSRS=true else self.useSRS=false end self:T("self.useSRS="..tostring(self.useSRS)) return self end end do SOUNDTEXT={ ClassName="SOUNDTEXT", } function SOUNDTEXT:New(Text,Duration) local self=BASE:Inherit(self,BASE:New()) self:SetText(Text) self:SetDuration(Duration or MSRS.getSpeechTime(Text)) self:T(string.format("New SOUNDTEXT: text=%s, duration=%.1f sec",self.text,self.duration)) return self end function SOUNDTEXT:SetText(Text) self.text=Text or"Hello World!" return self end function SOUNDTEXT:SetDuration(Duration) self.duration=Duration or 3 return self end function SOUNDTEXT:SetGender(Gender) self.gender=Gender or"female" return self end function SOUNDTEXT:SetCulture(Culture) self.culture=Culture or"en-GB" return self end function SOUNDTEXT:SetVoice(VoiceName) self.voice=VoiceName return self end end RADIO={ ClassName="RADIO", FileName="", Frequency=0, Modulation=radio.modulation.AM, Subtitle="", SubtitleDuration=0, Power=100, Loop=false, alias=nil, moduhasbeenset=false, } 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 self.Frequency=Frequency self.HertzFrequency=Frequency*1000000 if self.Positionable.ClassName=="UNIT"or self.Positionable.ClassName=="GROUP"then local commandSetFrequency={ id="SetFrequency", params={ frequency=self.HertzFrequency, modulation=self.Modulation, } } self:T2(commandSetFrequency) self.Positionable:SetCommand(commandSetFrequency) end return self end self:E({"Frequency is not a number. 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 if self.moduhasbeenset==false and Modulation==radio.modulation.FM then self:SetFrequency(self.Frequency) end self.moduhasbeenset=true 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 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(self.delay) else self.RQid=self.scheduler:Schedule(nil,RADIOQUEUE._CheckRadioQueue,{self},self.delay,self.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:SetSRS(PathToSRS,Port) local path=PathToSRS or MSRS.path local port=Port or MSRS.port self.msrs=MSRS:New(path,self.frequency/1000000,self.modulation) self.msrs:SetPort(port) 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..string.format("ERROR: Duration specified is NOT a number but type=%s. Filename=%s, duration=%s",type(duration),tostring(filename),tostring(duration))) 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 transmission end function RADIOQUEUE:AddSoundFile(soundfile,tstart,interval) local transmission=self:NewTransmission(soundfile:GetFileName(),soundfile.duration,soundfile:GetPath(),tstart,interval,soundfile.subtitle,soundfile.subduration) transmission.soundfile=soundfile return self end function RADIOQUEUE:AddSoundText(soundtext,tstart,interval) local transmission=self:NewTransmission("SoundText.ogg",soundtext.duration,nil,tstart,interval,soundtext.subtitle,soundtext.subduration) transmission.soundtext=soundtext return self end function RADIOQUEUE:Number2Transmission(number,delay,interval) local numbers=UTILS.GetCharacters(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) self:T("Broadcast") if((transmission.soundfile and transmission.soundfile.useSRS)or transmission.soundtext)and self.msrs then self:_BroadcastSRS(transmission) return end 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())) local commandFrequency={ id="SetFrequency", params={ frequency=self.frequency, modulation=self.modulation, }} sender:SetCommand(commandFrequency) self.senderinit=true 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 else self:E("ERROR: Could not get vec3 to determine transmission origin! Did you specify a sender and is it still alive?") end end end function RADIOQUEUE:_BroadcastSRS(transmission) if transmission.soundfile and transmission.soundfile.useSRS then self.msrs:PlaySoundFile(transmission.soundfile) elseif transmission.soundtext then self.msrs:PlaySoundText(transmission.soundtext) 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",0.51}, ["защитник"]={"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 MSRS={ ClassName="MSRS", lid=nil, port=5002, name="MSRS", backend="srsexe", frequencies={}, modulations={}, coalition=0, gender="female", culture=nil, voice=nil, volume=1, speed=1, coordinate=nil, provider="win", Label="ROBOT", ConfigFileName="Moose_MSRS.lua", ConfigFilePath="Config\\", ConfigLoaded=false, poptions={}, UsePowerShell=false, } MSRS.version="0.3.3" MSRS.Voices={ Amazon={ Generative={ en_AU={ Olivia="Olivia", }, en_GB={ Amy="Amy", }, en_US={ Danielle="Danielle", Joanna="Joanna", Ruth="Ruth", Stephen="Stephen", }, fr_FR={ ["Léa"]="Léa", ["Rémi"]="Rémi", }, de_DE={ Vicki="Vicki", Daniel="Daniel", }, it_IT={ Bianca="Bianca", Adriano="Adriano", }, es_ES={ Lucia="Lucia", Sergio="Sergio", }, }, LongForm={ en_US={ Danielle="Danielle", Gregory="Gregory", Ivy="Ivy", Ruth="Ruth", Patrick="Patrick", }, es_ES={ Alba="Alba", ["Raúl"]="Raúl", }, }, Neural={ en_AU={ Olivia="Olivia", }, en_GB={ Amy="Amy", Emma="Emma", Brian="Brian", Arthur="Arthur", }, en_US={ Danielle="Danielle", Gregory="Gregory", Ivy="Ivy", Joanna="Joanna", Kendra="Kendra", Kimberly="Kimberly", Salli="Salli", Joey="Joey", Kevin="Kevin", Ruth="Ruth", Stephen="Stephen", }, fr_FR={ ["Léa"]="Léa", ["Rémi"]="Rémi", }, de_DE={ Vicki="Vicki", Daniel="Daniel", }, it_IT={ Bianca="Bianca", Adriano="Adriano", }, es_ES={ Lucia="Lucia", Sergio="Sergio", }, }, Standard={ en_AU={ Nicole="Nicole", Russel="Russel", }, en_GB={ Amy="Amy", Emma="Emma", Brian="Brian", }, en_IN={ Aditi="Aditi", Raveena="Raveena", }, en_US={ Ivy="Ivy", Joanna="Joanna", Kendra="Kendra", Kimberly="Kimberly", Salli="Salli", Joey="Joey", Kevin="Kevin", }, fr_FR={ Celine="Celine", ["Léa"]="Léa", Mathieu="Mathieu", }, de_DE={ Marlene="Marlene", Vicki="Vicki", Hans="Hans", }, it_IT={ Carla="Carla", Bianca="Bianca", Giorgio="Giorgio", }, es_ES={ Conchita="Conchita", Lucia="Lucia", Enrique="Enrique", }, }, }, Microsoft={ ["Hedda"]="Microsoft Hedda Desktop", ["Hazel"]="Microsoft Hazel Desktop", ["David"]="Microsoft David Desktop", ["Zira"]="Microsoft Zira Desktop", ["Hortense"]="Microsoft Hortense Desktop", ["de_DE_Hedda"]="Microsoft Hedda Desktop", ["en_GB_Hazel"]="Microsoft Hazel Desktop", ["en_US_David"]="Microsoft David Desktop", ["en_US_Zira"]="Microsoft Zira Desktop", ["fr_FR_Hortense"]="Microsoft Hortense Desktop", }, MicrosoftGRPC={ ["Hazel"]="Hazel", ["George"]="George", ["Susan"]="Susan", ["David"]="David", ["Zira"]="Zira", ["Mark"]="Mark", ["James"]="James", ["Catherine"]="Catherine", ["Richard"]="Richard", ["Linda"]="Linda", ["Ravi"]="Ravi", ["Heera"]="Heera", ["Sean"]="Sean", ["en_GB_Hazel"]="Hazel", ["en_GB_George"]="George", ["en_GB_Susan"]="Susan", ["en_US_David"]="David", ["en_US_Zira"]="Zira", ["en_US_Mark"]="Mark", ["en_AU_James"]="James", ["en_AU_Catherine"]="Catherine", ["en_CA_Richard"]="Richard", ["en_CA_Linda"]="Linda", ["en_IN_Ravi"]="Ravi", ["en_IN_Heera"]="Heera", ["en_IR_Sean"]="Sean", }, Google={ Standard={ ["en_AU_Standard_A"]='en-AU-Standard-A', ["en_AU_Standard_B"]='en-AU-Standard-B', ["en_AU_Standard_C"]='en-AU-Standard-C', ["en_AU_Standard_D"]='en-AU-Standard-D', ["en_IN_Standard_A"]='en-IN-Standard-A', ["en_IN_Standard_B"]='en-IN-Standard-B', ["en_IN_Standard_C"]='en-IN-Standard-C', ["en_IN_Standard_D"]='en-IN-Standard-D', ["en_GB_Standard_A"]='en-GB-Standard-N', ["en_GB_Standard_B"]='en-GB-Standard-O', ["en_GB_Standard_C"]='en-GB-Standard-N', ["en_GB_Standard_D"]='en-GB-Standard-O', ["en_GB_Standard_F"]='en-GB-Standard-N', ["en_GB_Standard_O"]='en-GB-Standard-O', ["en_GB_Standard_N"]='en-GB-Standard-N', ["en_US_Standard_A"]='en-US-Standard-A', ["en_US_Standard_B"]='en-US-Standard-B', ["en_US_Standard_C"]='en-US-Standard-C', ["en_US_Standard_D"]='en-US-Standard-D', ["en_US_Standard_E"]='en-US-Standard-E', ["en_US_Standard_F"]='en-US-Standard-F', ["en_US_Standard_G"]='en-US-Standard-G', ["en_US_Standard_H"]='en-US-Standard-H', ["en_US_Standard_I"]='en-US-Standard-I', ["en_US_Standard_J"]='en-US-Standard-J', ["fr_FR_Standard_A"]="fr-FR-Standard-F", ["fr_FR_Standard_B"]="fr-FR-Standard-G", ["fr_FR_Standard_C"]="fr-FR-Standard-F", ["fr_FR_Standard_D"]="fr-FR-Standard-G", ["fr_FR_Standard_E"]="fr-FR-Standard-F", ["fr_FR_Standard_G"]="fr-FR-Standard-G", ["fr_FR_Standard_F"]="fr-FR-Standard-F", ["de_DE_Standard_A"]="de-DE-Standard-G", ["de_DE_Standard_B"]="de-DE-Standard-H", ["de_DE_Standard_C"]="de-DE-Standard-G", ["de_DE_Standard_D"]="de-DE-Standard-H", ["de_DE_Standard_E"]="de-DE-Standard-H", ["de_DE_Standard_F"]="de-DE-Standard-G", ["de_DE_Standard_H"]="de-DE-Standard-H", ["de_DE_Standard_G"]="de-DE-Standard-G", ["es_ES_Standard_A"]="es-ES-Standard-E", ["es_ES_Standard_B"]="es-ES-Standard-F", ["es_ES_Standard_C"]="es-ES-Standard-E", ["es_ES_Standard_D"]="es-ES-Standard-F", ["es_ES_Standard_E"]="es-ES-Standard-E", ["es_ES_Standard_F"]="es-ES-Standard-F", ["it_IT_Standard_A"]="it-IT-Standard-E", ["it_IT_Standard_B"]="it-IT-Standard-E", ["it_IT_Standard_C"]="it-IT-Standard-F", ["it_IT_Standard_D"]="it-IT-Standard-F", ["it_IT_Standard_E"]="it-IT-Standard-E", ["it_IT_Standard_F"]="it-IT-Standard-F", }, Wavenet={ ["en_AU_Wavenet_A"]='en-AU-Wavenet-A', ["en_AU_Wavenet_B"]='en-AU-Wavenet-B', ["en_AU_Wavenet_C"]='en-AU-Wavenet-C', ["en_AU_Wavenet_D"]='en-AU-Wavenet-D', ["en_IN_Wavenet_A"]='en-IN-Wavenet-A', ["en_IN_Wavenet_B"]='en-IN-Wavenet-B', ["en_IN_Wavenet_C"]='en-IN-Wavenet-C', ["en_IN_Wavenet_D"]='en-IN-Wavenet-D', ["en_GB_Wavenet_A"]='en-GB-Wavenet-N', ["en_GB_Wavenet_B"]='en-GB-Wavenet-O', ["en_GB_Wavenet_C"]='en-GB-Wavenet-N', ["en_GB_Wavenet_D"]='en-GB-Wavenet-O', ["en_GB_Wavenet_F"]='en-GB-Wavenet-N', ["en_GB_Wavenet_O"]='en-GB-Wavenet-O', ["en_GB_Wavenet_N"]='en-GB-Wavenet-N', ["en_US_Wavenet_A"]='en-US-Wavenet-N', ["en_US_Wavenet_B"]='en-US-Wavenet-B', ["en_US_Wavenet_C"]='en-US-Wavenet-C', ["en_US_Wavenet_D"]='en-US-Wavenet-D', ["en_US_Wavenet_E"]='en-US-Wavenet-E', ["en_US_Wavenet_F"]='en-US-Wavenet-F', ["en_US_Wavenet_G"]='en-US-Wavenet-G', ["en_US_Wavenet_H"]='en-US-Wavenet-H', ["en_US_Wavenet_I"]='en-US-Wavenet-I', ["en_US_Wavenet_J"]='en-US-Wavenet-J', ["fr_FR_Wavenet_A"]="fr-FR-Wavenet-F", ["fr_FR_Wavenet_B"]="fr-FR-Wavenet-G", ["fr_FR_Wavenet_C"]="fr-FR-Wavenet-F", ["fr_FR_Wavenet_D"]="fr-FR-Wavenet-G", ["fr_FR_Wavenet_E"]="fr-FR-Wavenet-F", ["fr_FR_Wavenet_G"]="fr-FR-Wavenet-G", ["fr_FR_Wavenet_F"]="fr-FR-Wavenet-F", ["de_DE_Wavenet_A"]="de-DE-Wavenet-G", ["de_DE_Wavenet_B"]="de-DE-Wavenet-H", ["de_DE_Wavenet_C"]="de-DE-Wavenet-G", ["de_DE_Wavenet_D"]="de-DE-Wavenet-H", ["de_DE_Wavenet_E"]="de-DE-Wavenet-H", ["de_DE_Wavenet_F"]="de-DE-Wavenet-G", ["de_DE_Wavenet_H"]="de-DE-Wavenet-H", ["de_DE_Wavenet_G"]="de-DE-Wavenet-G", ["es_ES_Wavenet_B"]="es-ES-Wavenet-E", ["es_ES_Wavenet_C"]="es-ES-Wavenet-F", ["es_ES_Wavenet_D"]="es-ES-Wavenet-E", ["es_ES_Wavenet_E"]="es-ES-Wavenet-E", ["es_ES_Wavenet_F"]="es-ES-Wavenet-F", ["it_IT_Wavenet_A"]="it-IT-Wavenet-E", ["it_IT_Wavenet_B"]="it-IT-Wavenet-E", ["it_IT_Wavenet_C"]="it-IT-Wavenet-F", ["it_IT_Wavenet_D"]="it-IT-Wavenet-F", ["it_IT_Wavenet_E"]="it-IT-Wavenet-E", ["it_IT_Wavenet_F"]="it-IT-Wavenet-F", }, }, } MSRS.Backend={ SRSEXE="srsexe", GRPC="grpc", } MSRS.Provider={ WINDOWS="win", GOOGLE="gcloud", AZURE="azure", AMAZON="aws", } function MSRS.uuid() local random=math.random local template='yxxx-xxxxxxxxxxxx' return string.gsub(template,'[xy]',function(c) local v=(c=='x')and random(0,0xf)or random(8,0xb) return string.format('%x',v) end) end function MSRS:New(Path,Frequency,Modulation,Backend) local self=BASE:Inherit(self,BASE:New()) self:F({Path,Frequency,Modulation,Backend}) Frequency=Frequency or 143 Modulation=Modulation or radio.modulation.AM self.lid=string.format("%s-%s | ","unknown",self.version) if not self.ConfigLoaded then self:SetPath(Path) self:SetPort() self:SetFrequencies(Frequency) self:SetModulations(Modulation) self:SetGender() self:SetCoalition() self:SetLabel() self:SetVolume() self:SetBackend(Backend) else if Path then self:SetPath(Path) end if Frequency then self:SetFrequencies(Frequency) end if Modulation then self:SetModulations(Modulation) end if Backend then self:SetBackend(Backend) end end self.lid=string.format("%s-%s | ",self.name,self.version) if not io or not os then self:E(self.lid.."***** ERROR - io or os NOT desanitized! MSRS will not work!") end return self end function MSRS:SetBackend(Backend) self:F({Backend=Backend}) Backend=Backend or MSRS.Backend.SRSEXE local function Checker(back) local ok=false for _,_backend in pairs(MSRS.Backend)do if tostring(back)==_backend then ok=true end end return ok end if Checker(Backend)then self.backend=Backend else MESSAGE:New("ERROR: Backend "..tostring(Backend).." is not supported!",30,"MSRS",true):ToLog():ToAll() end return self end function MSRS:SetBackendGRPC() self:F() self:SetBackend(MSRS.Backend.GRPC) return self end function MSRS:SetBackendSRSEXE() self:F() self:SetBackend(MSRS.Backend.SRSEXE) return self end function MSRS.SetDefaultBackend(Backend) MSRS.backend=Backend or MSRS.Backend.SRSEXE end function MSRS.SetDefaultBackendGRPC() MSRS.backend=MSRS.Backend.GRPC end function MSRS:GetBackend() return self.backend end function MSRS:SetPath(Path) self:F({Path=Path}) self.path=Path or"C:\\Program Files\\DCS-SimpleRadio-Standalone" local n=1;local nmax=1000 while(self.path:sub(-1)=="/"or self.path:sub(-1)==[[\]])and n<=nmax do self.path=self.path:sub(1,#self.path-1) n=n+1 end self:F(string.format("SRS path=%s",self:GetPath())) return self end function MSRS:GetPath() return self.path end function MSRS:SetVolume(Volume) self:F({Volume=Volume}) local volume=Volume or 1 if volume>1 then volume=1 elseif volume<0 then volume=0 end self.volume=volume return self end function MSRS:GetVolume() return self.volume end function MSRS:SetLabel(Label) self:F({Label=Label}) self.Label=Label or"ROBOT" return self end function MSRS:GetLabel() return self.Label end function MSRS:SetPort(Port) self:F({Port=Port}) self.port=Port or 5002 self:T(string.format("SRS port=%s",self:GetPort())) return self end function MSRS:GetPort() return self.port end function MSRS:SetCoalition(Coalition) self:F({Coalition=Coalition}) self.coalition=Coalition or 0 return self end function MSRS:GetCoalition() return self.coalition end function MSRS:SetFrequencies(Frequencies) self:F(Frequencies) self.frequencies=UTILS.EnsureTable(Frequencies,false) return self end function MSRS:AddFrequencies(Frequencies) self:F(Frequencies) for _,_freq in pairs(UTILS.EnsureTable(Frequencies,false))do self:T(self.lid..string.format("Adding frequency %s",tostring(_freq))) table.insert(self.frequencies,_freq) end return self end function MSRS:GetFrequencies() return self.frequencies end function MSRS:SetModulations(Modulations) self:F(Modulations) self.modulations=UTILS.EnsureTable(Modulations,false) self:T(self.lid.."Modulations:") self:T(self.modulations) return self end function MSRS:AddModulations(Modulations) self:F(Modulations) for _,_mod in pairs(UTILS.EnsureTable(Modulations,false))do table.insert(self.modulations,_mod) end return self end function MSRS:GetModulations() return self.modulations end function MSRS:SetGender(Gender) self:F({Gender=Gender}) Gender=Gender or"female" self.gender=Gender:lower() self:T("Setting gender to "..tostring(self.gender)) return self end function MSRS:SetCulture(Culture) self:F({Culture=Culture}) self.culture=Culture return self end function MSRS:SetVoice(Voice) self:F({Voice=Voice}) self.voice=Voice return self end function MSRS:SetVoiceProvider(Voice,Provider) self:F({Voice=Voice,Provider=Provider}) self.poptions=self.poptions or{} self.poptions[Provider or self:GetProvider()].voice=Voice return self end function MSRS:SetVoiceWindows(Voice) self:F({Voice=Voice}) self:SetVoiceProvider(Voice or"Microsoft Hazel Desktop",MSRS.Provider.WINDOWS) return self end function MSRS:SetVoiceGoogle(Voice) self:F({Voice=Voice}) self:SetVoiceProvider(Voice or MSRS.Voices.Google.Standard.en_GB_Standard_A,MSRS.Provider.GOOGLE) return self end function MSRS:SetVoiceAzure(Voice) self:F({Voice=Voice}) self:SetVoiceProvider(Voice or"en-US-AriaNeural",MSRS.Provider.AZURE) return self end function MSRS:SetVoiceAmazon(Voice) self:F({Voice=Voice}) self:SetVoiceProvider(Voice or"Brian",MSRS.Provider.AMAZON) return self end function MSRS:GetVoice(Provider) Provider=Provider or self.provider if Provider and self.poptions[Provider]and self.poptions[Provider].voice then return self.poptions[Provider].voice else return self.voice end end function MSRS:SetCoordinate(Coordinate) self:F(Coordinate) self.coordinate=Coordinate return self end function MSRS:SetGoogle(PathToCredentials) self:F({PathToCredentials=PathToCredentials}) if PathToCredentials then self.provider=MSRS.Provider.GOOGLE self:SetProviderOptionsGoogle(PathToCredentials,PathToCredentials) end return self end function MSRS:SetGoogleAPIKey(APIKey) self:F({APIKey=APIKey}) if APIKey then self.provider=MSRS.Provider.GOOGLE if self.poptions[MSRS.Provider.GOOGLE]then self.poptions[MSRS.Provider.GOOGLE].key=APIKey else self:SetProviderOptionsGoogle(nil,APIKey) end end return self end function MSRS:SetProvider(Provider) BASE:F({Provider=Provider}) if self then self.provider=Provider or MSRS.Provider.WINDOWS return self else MSRS.provider=Provider or MSRS.Provider.WINDOWS end return end function MSRS:GetProvider() return self.provider or MSRS.Provider.WINDOWS end function MSRS:SetProviderOptions(Provider,CredentialsFile,AccessKey,SecretKey,Region) BASE:F({Provider,CredentialsFile,AccessKey,SecretKey,Region}) local option=MSRS._CreateProviderOptions(Provider,CredentialsFile,AccessKey,SecretKey,Region) if self then self.poptions=self.poptions or{} self.poptions[Provider]=option else MSRS.poptions=MSRS.poptions or{} MSRS.poptions[Provider]=option end return option end function MSRS._CreateProviderOptions(Provider,CredentialsFile,AccessKey,SecretKey,Region) BASE:F({Provider,CredentialsFile,AccessKey,SecretKey,Region}) local option={} option.provider=Provider option.credentials=CredentialsFile option.key=AccessKey option.secret=SecretKey option.region=Region return option end function MSRS:SetProviderOptionsGoogle(CredentialsFile,AccessKey) self:F({CredentialsFile,AccessKey}) self:SetProviderOptions(MSRS.Provider.GOOGLE,CredentialsFile,AccessKey) return self end function MSRS:SetProviderOptionsAmazon(AccessKey,SecretKey,Region) self:F({AccessKey,SecretKey,Region}) self:SetProviderOptions(MSRS.Provider.AMAZON,nil,AccessKey,SecretKey,Region) return self end function MSRS:SetProviderOptionsAzure(AccessKey,Region) self:F({AccessKey,Region}) self:SetProviderOptions(MSRS.Provider.AZURE,nil,AccessKey,nil,Region) return self end function MSRS:GetProviderOptions(Provider) return self.poptions[Provider or self.provider]or{} end function MSRS:SetTTSProviderGoogle() self:F() self:SetProvider(MSRS.Provider.GOOGLE) return self end function MSRS:SetTTSProviderMicrosoft() self:F() self:SetProvider(MSRS.Provider.WINDOWS) return self end function MSRS:SetTTSProviderAzure() self:F() self:SetProvider(MSRS.Provider.AZURE) return self end function MSRS:SetTTSProviderAmazon() self:F() self:SetProvider(MSRS.Provider.AMAZON) return self end function MSRS:Help() self:F() local path=self:GetPath() local exe="DCS-SR-ExternalAudio.exe" local filename=os.getenv('TMP').."\\MSRS-help-"..MSRS.uuid()..".txt" local command=string.format("%s/%s --help > %s",path,exe,filename) os.execute(command) local f=assert(io.open(filename,"rb")) local data=f:read("*all") f:close() env.info("SRS help output:") env.info("======================================================================") env.info(data) env.info("======================================================================") return self end function MSRS:PlaySoundFile(Soundfile,Delay) self:F({Soundfile,Delay}) local soundfile=Soundfile:GetName() local exists=UTILS.FileExists(soundfile) if not exists then self:E("ERROR: MSRS sound file does not exist! File="..soundfile) return self end if Delay and Delay>0 then self:ScheduleOnce(Delay,MSRS.PlaySoundFile,self,Soundfile,0) else local command=self:_GetCommand() command=command..' --file="'..tostring(soundfile)..'"' command=string.gsub(command,"--ssml","-h") self:_ExecCommand(command) end return self end function MSRS:PlaySoundText(SoundText,Delay) self:F({SoundText,Delay}) if Delay and Delay>0 then self:ScheduleOnce(Delay,MSRS.PlaySoundText,self,SoundText,0) else if self.backend==MSRS.Backend.GRPC then self:_DCSgRPCtts(SoundText.text,nil,SoundText.gender,SoundText.culture,SoundText.voice,SoundText.volume,SoundText.label,SoundText.coordinate) else local command=self:_GetCommand(nil,nil,nil,SoundText.gender,SoundText.voice,SoundText.culture,SoundText.volume,SoundText.speed) command=command..string.format(" --text=\"%s\"",tostring(SoundText.text)) self:_ExecCommand(command) end end return self end function MSRS:PlayText(Text,Delay,Coordinate) self:F({Text,Delay,Coordinate}) if Delay and Delay>0 then self:ScheduleOnce(Delay,MSRS.PlayText,self,Text,nil,Coordinate) else if self.backend==MSRS.Backend.GRPC then self:T(self.lid.."Transmitting") self:_DCSgRPCtts(Text,nil,nil,nil,nil,nil,nil,Coordinate) else self:PlayTextExt(Text,Delay,nil,nil,nil,nil,nil,nil,nil,Coordinate) end end return self end function MSRS:PlayTextExt(Text,Delay,Frequencies,Modulations,Gender,Culture,Voice,Volume,Label,Coordinate) self:T({Text,Delay,Frequencies,Modulations,Gender,Culture,Voice,Volume,Label,Coordinate}) if Delay and Delay>0 then self:ScheduleOnce(Delay,self.PlayTextExt,self,Text,0,Frequencies,Modulations,Gender,Culture,Voice,Volume,Label,Coordinate) else Frequencies=Frequencies or self:GetFrequencies() Modulations=Modulations or self:GetModulations() if self.backend==MSRS.Backend.SRSEXE then local command=self:_GetCommand(UTILS.EnsureTable(Frequencies,false),UTILS.EnsureTable(Modulations,false),nil,Gender,Voice,Culture,Volume,nil,nil,Label,Coordinate) command=command..string.format(" --text=\"%s\"",tostring(Text)) self:_ExecCommand(command) elseif self.backend==MSRS.Backend.GRPC then self:_DCSgRPCtts(Text,Frequencies,Gender,Culture,Voice,Volume,Label,Coordinate) end end return self end function MSRS:PlayTextFile(TextFile,Delay) self:F({TextFile,Delay}) if Delay and Delay>0 then self:ScheduleOnce(Delay,MSRS.PlayTextFile,self,TextFile,0) else local exists=UTILS.FileExists(TextFile) if not exists then self:E("ERROR: MSRS Text file does not exist! File="..tostring(TextFile)) return self end local command=self:_GetCommand() command=command..string.format(" --textFile=\"%s\"",tostring(TextFile)) self:T(string.format("MSRS TextFile command=%s",command)) local l=string.len(command) self:T(string.format("Command length=%d",l)) self:_ExecCommand(command) end return self end function MSRS:_GetLatLongAlt(Coordinate) self:F({Coordinate=Coordinate}) local lat=0.0 local lon=0.0 local alt=0.0 if Coordinate then lat,lon,alt=coord.LOtoLL(Coordinate) end return lat,lon,math.floor(alt) end function MSRS:_GetCommand(freqs,modus,coal,gender,voice,culture,volume,speed,port,label,coordinate) self:F({freqs,modus,coal,gender,voice,culture,volume,speed,port,label,coordinate}) local path=self:GetPath() local exe="DCS-SR-ExternalAudio.exe" local fullPath=string.format("%s\\%s",path,exe) freqs=table.concat(freqs or self.frequencies,",") modus=table.concat(modus or self.modulations,",") coal=coal or self.coalition gender=gender or self.gender voice=voice or self:GetVoice(self.provider)or self.voice culture=culture or self.culture volume=volume or self.volume speed=speed or self.speed port=port or self.port label=label or self.Label coordinate=coordinate or self.coordinate modus=modus:gsub("0","AM") modus=modus:gsub("1","FM") local pwsh=string.format('Start-Process -WindowStyle Hidden -WorkingDirectory \"%s\" -FilePath \"%s\" -ArgumentList \'-f "%s" -m "%s" -c %s -p %s -n "%s" -v "%.1f"',path,exe,freqs,modus,coal,port,label,volume) local command=string.format('"%s\\%s" -f "%s" -m "%s" -c %s -p %s -n "%s" -v "%.1f"',path,exe,freqs,modus,coal,port,label,volume) if voice and self.UsePowerShell~=true then command=command..string.format(" --voice=\"%s\"",tostring(voice)) pwsh=pwsh..string.format(" --voice=\"%s\"",tostring(voice)) else if gender and gender~="female"then command=command..string.format(" -g %s",tostring(gender)) pwsh=pwsh..string.format(" -g %s",tostring(gender)) end if culture and culture~="en-GB"then command=command..string.format(" -l %s",tostring(culture)) pwsh=pwsh..string.format(" -l %s",tostring(culture)) end end if coordinate then local lat,lon,alt=self:_GetLatLongAlt(coordinate) command=command..string.format(" -L %.4f -O %.4f -A %d",lat,lon,alt) pwsh=pwsh..string.format(" -L %.4f -O %.4f -A %d",lat,lon,alt) end if self.provider==MSRS.Provider.GOOGLE then local pops=self:GetProviderOptions() command=command..string.format(' --ssml -G "%s"',pops.credentials) pwsh=pwsh..string.format(' --ssml -G "%s"',pops.credentials) elseif self.provider==MSRS.Provider.WINDOWS then else self:E("ERROR: SRS only supports WINDOWS and GOOGLE as TTS providers! Use DCS-gRPC backend for other providers such as AWS and Azure.") end if not UTILS.FileExists(fullPath)then self:E("ERROR: MSRS SRS executable does not exist! FullPath="..fullPath) command="CommandNotFound" end self:T("MSRS command from _GetCommand="..command) if self.UsePowerShell==true then return pwsh else return command end end function MSRS:_ExecCommand(command) self:T2({command=command}) if string.find(command,"CommandNotFound")then return 0 end local batContent=command.." && exit" local filename=os.getenv('TMP').."\\MSRS-"..MSRS.uuid()..".bat" if self.UsePowerShell==true then filename=os.getenv('TMP').."\\MSRS-"..MSRS.uuid()..".ps1" batContent=command.."\'" self:T({batContent=batContent}) end local script=io.open(filename,"w+") script:write(batContent) script:close() self:T("MSRS batch file created: "..filename) self:T("MSRS batch content: "..batContent) local res=nil if self.UsePowerShell~=true then local filenvbs=os.getenv('TMP').."\\MSRS-"..MSRS.uuid()..".vbs" local script=io.open(filenvbs,"w+") script:write(string.format('Dim WinScriptHost\n')) script:write(string.format('Set WinScriptHost = CreateObject("WScript.Shell")\n')) script:write(string.format('WinScriptHost.Run Chr(34) & "%s" & Chr(34), 0\n',filename)) script:write(string.format('Set WinScriptHost = Nothing')) script:close() self:T("MSRS vbs file created to start batch="..filenvbs) local runvbs=string.format('cscript.exe //Nologo //B "%s"',filenvbs) self:T("MSRS execute VBS command="..runvbs) res=os.execute(runvbs) timer.scheduleFunction(os.remove,filename,timer.getTime()+1) timer.scheduleFunction(os.remove,filenvbs,timer.getTime()+1) self:T("MSRS vbs and batch file removed") elseif self.UsePowerShell==true then local pwsh=string.format('start /min "" powershell.exe -ExecutionPolicy Unrestricted -WindowStyle Hidden -Command "%s"',filename) if string.len(pwsh)>255 then self:E("[MSRS] - pwsh string too long") end res=os.execute(pwsh) timer.scheduleFunction(os.remove,filename,timer.getTime()+1) else command=string.format('start /b "" "%s"',filename) self:T("MSRS execute command="..command) res=os.execute(command) timer.scheduleFunction(os.remove,filename,timer.getTime()+1) end return res end function MSRS:_DCSgRPCtts(Text,Frequencies,Gender,Culture,Voice,Volume,Label,Coordinate) self:T("MSRS_BACKEND_DCSGRPC:_DCSgRPCtts()") self:T({Text,Frequencies,Gender,Culture,Voice,Volume,Label,Coordinate}) local options={} local ssml=Text or'' Frequencies=UTILS.EnsureTable(Frequencies,true)or self:GetFrequencies() options.plaintext=Text options.srsClientName=Label or self.Label if self.coordinate then options.position={} options.position.lat,options.position.lon,options.position.alt=self:_GetLatLongAlt(self.coordinate) end options.coalition=UTILS.GetCoalitionName(self.coalition):lower() local provider=self.provider or MSRS.Provider.WINDOWS options.provider={} options.provider[provider]=self:GetProviderOptions(provider) Voice=Voice or self:GetVoice(self.provider)or self.voice if Voice then options.provider[provider].voice=Voice else local preTag,genderProp,langProp,postTag='','','','' local gender="" if self.gender then gender=string.format(' gender=\"%s\"',self.gender) end local language="" if self.culture then language=string.format(' language=\"%s\"',self.culture) end if self.gender or self.culture then ssml=string.format("%s",gender,language,Text) end end for _,freq in pairs(Frequencies)do self:T("Calling GRPC.tts with the following parameter:") self:T({ssml=ssml,freq=freq,options=options}) self:T(options.provider[provider]) GRPC.tts(ssml,freq*1e6,options) end end function MSRS:LoadConfigFile(Path,Filename) if lfs==nil then env.info("*****Note - lfs and os need to be desanitized for MSRS to work!") return false end local path=Path or lfs.writedir()..MSRS.ConfigFilePath local file=Filename or MSRS.ConfigFileName or"Moose_MSRS.lua" local pathandfile=path..file local filexsists=UTILS.FileExists(pathandfile) if filexsists and not MSRS.ConfigLoaded then env.info("FF reading config file") assert(loadfile(path..file))() if MSRS_Config then local Self=self or MSRS Self.path=MSRS_Config.Path or"C:\\Program Files\\DCS-SimpleRadio-Standalone" Self.port=MSRS_Config.Port or 5002 Self.backend=MSRS_Config.Backend or MSRS.Backend.SRSEXE Self.frequencies=MSRS_Config.Frequency or{127,243} Self.modulations=MSRS_Config.Modulation or{0,0} Self.coalition=MSRS_Config.Coalition or 0 if MSRS_Config.Coordinate then Self.coordinate=COORDINATE:New(MSRS_Config.Coordinate[1],MSRS_Config.Coordinate[2],MSRS_Config.Coordinate[3]) end Self.culture=MSRS_Config.Culture or"en-GB" Self.gender=MSRS_Config.Gender or"male" Self.Label=MSRS_Config.Label or"MSRS" Self.voice=MSRS_Config.Voice Self.provider=MSRS_Config.Provider or MSRS.Provider.WINDOWS for _,provider in pairs(MSRS.Provider)do if MSRS_Config[provider]then Self.poptions[provider]=MSRS_Config[provider] end end Self.ConfigLoaded=true end env.info("MSRS - Successfully loaded default configuration from disk!",false) end if not filexsists then env.info("MSRS - Cannot find default configuration file!",false) return false end return true end function MSRS.getSpeechTime(length,speed,isGoogle) local maxRateRatio=3 speed=speed or 1.0 isGoogle=isGoogle or false local speedFactor=1.0 if isGoogle then speedFactor=speed else if speed~=0 then speedFactor=math.abs(speed)*(maxRateRatio-1)/10+1 end if speed<0 then speedFactor=1/speedFactor end end local wpm=math.ceil(100*speedFactor) local cps=math.floor((wpm*5)/60) if type(length)=="string"then length=string.len(length) end return length/cps end MSRSQUEUE={ ClassName="MSRSQUEUE", Debugmode=nil, lid=nil, queue={}, alias=nil, dt=nil, Tlast=nil, checking=nil, } function MSRSQUEUE:New(alias) local self=BASE:Inherit(self,BASE:New()) self.alias=alias or"My Radio" self.dt=1.0 self.lid=string.format("MSRSQUEUE %s | ",self.alias) return self end function MSRSQUEUE:Clear() self:T(self.lid.."Clearing MSRSQUEUE") self.queue={} return self end function MSRSQUEUE:AddTransmission(transmission) transmission.isplaying=false transmission.Tstarted=nil table.insert(self.queue,transmission) if not self.checking then self:_CheckRadioQueue() end return self end function MSRSQUEUE:SetTransmitOnlyWithPlayers(Switch) self.TransmitOnlyWithPlayers=Switch if Switch==false or Switch==nil then if self.PlayerSet then self.PlayerSet:FilterStop() end self.PlayerSet=nil else self.PlayerSet=SET_CLIENT:New():FilterStart() end return self end function MSRSQUEUE:NewTransmission(text,duration,msrs,tstart,interval,subgroups,subtitle,subduration,frequency,modulation,gender,culture,voice,volume,label,coordinate) self:T({Text=text,Dur=duration,start=tstart,int=interval,sub=subgroups,subt=subtitle,sudb=subduration,F=frequency,M=modulation,G=gender,C=culture,V=voice,Vol=volume,L=label}) if self.TransmitOnlyWithPlayers then if self.PlayerSet and self.PlayerSet:CountAlive()==0 then return self end end if not text then self:E(self.lid.."ERROR: No text specified.") return nil end if type(text)~="string"then self:E(self.lid.."ERROR: Text specified is NOT a string.") return nil end local transmission={} transmission.text=text transmission.duration=duration or MSRS.getSpeechTime(text) transmission.msrs=msrs transmission.Tplay=tstart or timer.getAbsTime() transmission.subtitle=subtitle transmission.interval=interval or 0 transmission.frequency=frequency or msrs.frequencies transmission.modulation=modulation or msrs.modulations transmission.subgroups=subgroups if transmission.subtitle then transmission.subduration=subduration or transmission.duration else transmission.subduration=0 end transmission.gender=gender or msrs.gender transmission.culture=culture or msrs.culture transmission.voice=voice or msrs.voice transmission.volume=volume or msrs.volume transmission.label=label or msrs.Label transmission.coordinate=coordinate or msrs.coordinate self:AddTransmission(transmission) return transmission end function MSRSQUEUE:Broadcast(transmission) self:T(self.lid.."Broadcast") if transmission.frequency then transmission.msrs:PlayTextExt(transmission.text,nil,transmission.frequency,transmission.modulation,transmission.gender,transmission.culture,transmission.voice,transmission.volume,transmission.label,transmission.coordinate) else transmission.msrs:PlayText(transmission.text,nil,transmission.coordinate) end local function texttogroup(gid) trigger.action.outTextForGroup(gid,transmission.subtitle,transmission.subduration,true) end if transmission.subgroups and#transmission.subgroups>0 then for _,_group in pairs(transmission.subgroups)do local group=_group if group and group:IsAlive()then local gid=group:GetID() self:ScheduleOnce(4,texttogroup,gid) end end end end function MSRSQUEUE:CalcTransmisstionDuration() local Tnow=timer.getAbsTime() local T=0 for _,_transmission in pairs(self.queue)do local transmission=_transmission if transmission.isplaying then local dt=Tnow-transmission.Tstarted T=T+transmission.duration-dt else T=T+transmission.duration end end return T end function MSRSQUEUE:_CheckRadioQueue(delay) local N=#self.queue self:T2(self.lid..string.format("Check radio queue %s: delay=%.3f sec, N=%d, checking=%s",self.alias,delay or 0,N,tostring(self.checking))) if delay and delay>0 then self:ScheduleOnce(delay,MSRSQUEUE._CheckRadioQueue,self) self.checking=true else if N==0 then self:T(self.lid..string.format("Check radio queue %s empty ==> disable checking",self.alias)) self.checking=false return end local time=timer.getAbsTime() self.checking=true local dt=self.dt 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 dt=transmission.duration-(time-transmission.Tstarted) 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:T(self.lid..string.format("Broadcasting text=\"%s\" at T=%.3f",next.text,time)) self:Broadcast(next) next.isplaying=true next.Tstarted=time dt=next.duration end if remove then table.remove(self.queue,remove) N=N-1 if#self.queue==0 then self:T(self.lid..string.format("Check radio queue %s empty ==> disable checking",self.alias)) self.checking=false return end end self:_CheckRadioQueue(dt) end end MSRS.LoadConfigFile() 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:SetMessageDuration(10) 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) 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:T({"<== 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:CountAlive() 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:CountAlive()==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:CountAlive() 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:CountAlive() 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/Scripts/MissionScripting.lua and comment out the lines with sanitizeModule(''). Use at your own risk!)") end BASE.ServerName="Unknown" if lfs and loadfile then local serverfile=lfs.writedir()..'Config/serverSettings.lua' if UTILS.FileExists(serverfile)then loadfile(serverfile)() if cfg and cfg.name then BASE.ServerName=cfg.name end end BASE.ServerName=BASE.ServerName or"Unknown" BASE:I("Server Name: "..tostring(BASE.ServerName)) end BASE:TraceOnOff(false) env.info('*** MOOSE INCLUDE END *** ')