env.info('*** MOOSE GITHUB Commit Hash ID: 2026-05-14T10:46:22+02:00-696630fe4d7c53e25c3599abea51a147a4363f1a ***') 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, Decoys=8589934592, SmokeShell=17179869184, IlluminationShell=34359738368, MarkerShell=51539607552, MarkerWeapon=51539620864, SubmunitionDispenserShell=68719476736, ConventionalShell=206963736576, Auto=265214230526, AutoDCS=1073741822, AnyAG=2956984318, AnyAA=264241152, AnyUnguided=2952822768, AnyGuided=268402702, AnyShell=258503344128, } 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=265214230526, 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", Herc="C-130", Hercules="C-130J-30", 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", Intruder="A6E", 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={}, shells={}, gunmounts={}, droptanks={}, adapters={}, torpedoes={}, Gazelle={}, CH47={}, OH58={}, UH1H={}, AH64D={}, UH60L={}, } } ENUMS.Storage.weapons.containers.APK9='weapons.containers.APK-9' ENUMS.Storage.weapons.shells.KDA_35_FAPDS='weapons.shells.KDA_35_FAPDS' ENUMS.Storage.weapons.shells.BR_354N='weapons.shells.BR_354N' ENUMS.Storage.weapons.droptanks.HB_F4E_EXT_WingTank_EMPTY='weapons.droptanks.HB_F-4E_EXT_WingTank_EMPTY' ENUMS.Storage.weapons.nurs.HYDRA_70_M151_M433='weapons.nurs.HYDRA_70_M151_M433' ENUMS.Storage.weapons.shells.Rh202_20_HE='weapons.shells.Rh202_20_HE' ENUMS.Storage.weapons.bombs.GBU_38='weapons.bombs.GBU_38' ENUMS.Storage.weapons.containers._16c_hts_pod='weapons.containers.16c_hts_pod' ENUMS.Storage.weapons.missiles.AGM_65G='weapons.missiles.AGM_65G' ENUMS.Storage.weapons.gunmounts.NR30='weapons.gunmounts.NR-30' ENUMS.Storage.weapons.gunmounts.MB339_ANM3_L='weapons.gunmounts.{MB339_ANM3_L}' ENUMS.Storage.weapons.adapters.UB13='weapons.adapters.UB-13' ENUMS.Storage.weapons.shells.N37_37x155_HEI_T='weapons.shells.N37_37x155_HEI_T' ENUMS.Storage.weapons.bombs.AN_M30A1='weapons.bombs.AN_M30A1' ENUMS.Storage.weapons.adapters.APU601='weapons.adapters.APU-60-1' ENUMS.Storage.weapons.adapters.M2000C_AUF2='weapons.adapters.M-2000C_AUF2' ENUMS.Storage.weapons.shells.MG_20x82_API='weapons.shells.MG_20x82_API' ENUMS.Storage.weapons.adapters.Carrier_N1_EM_EF='weapons.adapters.Carrier_N-1_EM_EF' ENUMS.Storage.weapons.gunmounts.OH58D_M3P_L500='weapons.gunmounts.OH58D_M3P_L500' ENUMS.Storage.weapons.adapters.RB15pylon='weapons.adapters.RB15pylon' ENUMS.Storage.weapons.bombs.AGM_62='weapons.bombs.AGM_62' ENUMS.Storage.weapons.shells.GSH23_23_AP='weapons.shells.GSH23_23_AP' ENUMS.Storage.weapons.shells.Mauser7_92x57_S_m_K_Ub_m_Zerl='weapons.shells.Mauser7.92x57_S.m.K._Ub.m.Zerl.' ENUMS.Storage.weapons.shells.M2_12_7='weapons.shells.M2_12_7' ENUMS.Storage.weapons.shells.M230_HEDPM789='weapons.shells.M230_HEDP M789' ENUMS.Storage.weapons.adapters.UB_32A='weapons.adapters.UB_32A' ENUMS.Storage.weapons.shells.L23A1_APFSDS='weapons.shells.L23A1_APFSDS' ENUMS.Storage.weapons.shells.DEFA553_30HE='weapons.shells.DEFA553_30HE' ENUMS.Storage.weapons.bombs.LYSBOMB11087='weapons.bombs.LYSBOMB 11087' ENUMS.Storage.weapons.shells.KS19_100HE='weapons.shells.KS19_100HE' ENUMS.Storage.weapons.droptanks.M2KC_RPL_522_EMPTY='weapons.droptanks.M2KC_RPL_522_EMPTY' ENUMS.Storage.weapons.droptanks.M2KC_08_RPL541='weapons.droptanks.M2KC_08_RPL541' ENUMS.Storage.weapons.shells._50Browning_API_M8_Corsair='weapons.shells.50Browning_API_M8_Corsair' ENUMS.Storage.weapons.adapters.OH58D_M260='weapons.adapters.OH58D_M260' ENUMS.Storage.weapons.missiles.Rb05A='weapons.missiles.Rb 05A' ENUMS.Storage.weapons.adapters.HB_F14_EXT_SHOULDER_PHX_L='weapons.adapters.HB_F14_EXT_SHOULDER_PHX_L' ENUMS.Storage.weapons.shells.MG_13x64_HEI_T='weapons.shells.MG_13x64_HEI_T' ENUMS.Storage.weapons.droptanks.M2KC_08_RPL541_EMPTY='weapons.droptanks.M2KC_08_RPL541_EMPTY' ENUMS.Storage.weapons.missiles.HQ16='weapons.missiles.HQ-16' ENUMS.Storage.weapons.nurs.SMERCH_9M55F='weapons.nurs.SMERCH_9M55F' ENUMS.Storage.weapons.nurs.M26='weapons.nurs.M26' ENUMS.Storage.weapons.shells._2A38_30_AP='weapons.shells.2A38_30_AP' ENUMS.Storage.weapons.missiles.LS_6='weapons.missiles.LS_6' ENUMS.Storage.weapons.containers.EclairM_60='weapons.containers.{EclairM_60}' ENUMS.Storage.weapons.bombs.FAB_100='weapons.bombs.FAB_100' ENUMS.Storage.weapons.missiles.MALUTKA='weapons.missiles.MALUTKA' ENUMS.Storage.weapons.containers.HB_ALE_40_0_120='weapons.containers.HB_ALE_40_0_120' ENUMS.Storage.weapons.bombs.M485_FLARE='weapons.bombs.M485_FLARE' ENUMS.Storage.weapons.nurs.AGR_20_M282_unguided='weapons.nurs.AGR_20_M282_unguided' ENUMS.Storage.weapons.droptanks.F15E_Drop_Tank='weapons.droptanks.F-15E_Drop_Tank' ENUMS.Storage.weapons.shells._20mm_M70LD_SAPHEI='weapons.shells.20mm_M70LD_SAPHEI' ENUMS.Storage.weapons.adapters.CHAP_Mi28N_ataka='weapons.adapters.CHAP_Mi28N_ataka' ENUMS.Storage.weapons.bombs.GBU_30='weapons.bombs.GBU_30' ENUMS.Storage.weapons.bombs.AGM_62_I='weapons.bombs.AGM_62_I' ENUMS.Storage.weapons.bombs.BETAB500S='weapons.bombs.BETAB-500S' ENUMS.Storage.weapons.shells.ZTZ_125_HE='weapons.shells.ZTZ_125_HE' ENUMS.Storage.weapons.shells.HP30_30_AP='weapons.shells.HP30_30_AP' ENUMS.Storage.weapons.missiles.SM_6='weapons.missiles.SM_6' ENUMS.Storage.weapons.gunmounts.MINIGUN='weapons.gunmounts.MINIGUN' ENUMS.Storage.weapons.bombs.CBU_87='weapons.bombs.CBU_87' ENUMS.Storage.weapons.adapters.B8V20A='weapons.adapters.B-8V20A' ENUMS.Storage.weapons.containers.AN_ASQ_228='weapons.containers.AN_ASQ_228' ENUMS.Storage.weapons.missiles.Sea_Dart='weapons.missiles.Sea_Dart' ENUMS.Storage.weapons.adapters.apu13mt='weapons.adapters.apu-13mt' ENUMS.Storage.weapons.adapters.HB_ORD_Missile_Well_Adapter='weapons.adapters.HB_ORD_Missile_Well_Adapter' ENUMS.Storage.weapons.shells.AK176_76='weapons.shells.AK176_76' ENUMS.Storage.weapons.missiles.X_29T='weapons.missiles.X_29T' ENUMS.Storage.weapons.nurs.HYDRA_70_MK61='weapons.nurs.HYDRA_70_MK61' ENUMS.Storage.weapons.shells.M393A3_105_HE='weapons.shells.M393A3_105_HE' ENUMS.Storage.weapons.bombs.AN_M57='weapons.bombs.AN_M57' ENUMS.Storage.weapons.missiles.AIM_7='weapons.missiles.AIM_7' ENUMS.Storage.weapons.gunmounts.GIAT_M621_SAPHEI='weapons.gunmounts.{GIAT_M621_SAPHEI}' ENUMS.Storage.weapons.containers.MATRAPHIMAT='weapons.containers.MATRA-PHIMAT' ENUMS.Storage.weapons.shells.M61_20_AP='weapons.shells.M61_20_AP' ENUMS.Storage.weapons.droptanks.droptank_150_gal='weapons.droptanks.droptank_150_gal' ENUMS.Storage.weapons.missiles.SA48H6E2='weapons.missiles.SA48H6E2' ENUMS.Storage.weapons.nurs.HVARUSNMk28Mod4='weapons.nurs.HVAR USN Mk28 Mod4' ENUMS.Storage.weapons.adapters.KMGU2='weapons.adapters.KMGU-2' ENUMS.Storage.weapons.missiles.C_701T='weapons.missiles.C_701T' ENUMS.Storage.weapons.shells.DM53_120_AP='weapons.shells.DM53_120_AP' ENUMS.Storage.weapons.adapters._9M114PYLON_EMPTY='weapons.adapters.9M114-PYLON_EMPTY' ENUMS.Storage.weapons.missiles.P_500='weapons.missiles.P_500' ENUMS.Storage.weapons.bombs.S_8OM_FLARE='weapons.bombs.S_8OM_FLARE' ENUMS.Storage.weapons.adapters.LAU115C2_LAU127='weapons.adapters.LAU-115C+2_LAU127' ENUMS.Storage.weapons.shells.M256_120_HE='weapons.shells.M256_120_HE' ENUMS.Storage.weapons.shells._7_62x51tr='weapons.shells.7_62x51tr' ENUMS.Storage.weapons.adapters.adapter_gdj_kd63='weapons.adapters.adapter_gdj_kd63' ENUMS.Storage.weapons.missiles.CM802AKG='weapons.missiles.CM-802AKG' ENUMS.Storage.weapons.missiles.C_802AK='weapons.missiles.C_802AK' ENUMS.Storage.weapons.bombs.GBU_39='weapons.bombs.GBU_39' ENUMS.Storage.weapons.bombs.BETAB500M='weapons.bombs.BETAB-500M' ENUMS.Storage.weapons.adapters.LAU117='weapons.adapters.LAU-117' ENUMS.Storage.weapons.missiles.BK90_MJ1='weapons.missiles.BK90_MJ1' ENUMS.Storage.weapons.missiles.R60='weapons.missiles.R-60' ENUMS.Storage.weapons.shells.PJ26_76_PFHE='weapons.shells.PJ26_76_PFHE' ENUMS.Storage.weapons.nurs.AGR_20_M151_unguided='weapons.nurs.AGR_20_M151_unguided' ENUMS.Storage.weapons.shells.HEDPM430='weapons.shells.HEDPM430' ENUMS.Storage.weapons.shells.GSH_23_HE='weapons.shells.GSH_23_HE' ENUMS.Storage.weapons.gunmounts.CC420_GUN_POD='weapons.gunmounts.{CC420_GUN_POD}' ENUMS.Storage.weapons.shells.Hispano_Mk_II_SAPI='weapons.shells.Hispano_Mk_II_SAP/I' ENUMS.Storage.weapons.adapters.Spitfire_pilon2L='weapons.adapters.Spitfire_pilon2L' ENUMS.Storage.weapons.bombs.RBK_500SOAB='weapons.bombs.RBK_500SOAB' ENUMS.Storage.weapons.bombs.M_117='weapons.bombs.M_117' ENUMS.Storage.weapons.missiles.SPIKE_ER2='weapons.missiles.SPIKE_ER2' ENUMS.Storage.weapons.bombs.BDU_45LGB='weapons.bombs.BDU_45LGB' ENUMS.Storage.weapons.missiles.AGM_65H='weapons.missiles.AGM_65H' ENUMS.Storage.weapons.adapters.adapter_df4b='weapons.adapters.adapter_df4b' ENUMS.Storage.weapons.nurs.SNEB_TYPE252_F1B='weapons.nurs.SNEB_TYPE252_F1B' ENUMS.Storage.weapons.droptanks._800LTank='weapons.droptanks.800L Tank' ENUMS.Storage.weapons.missiles.X_31A='weapons.missiles.X_31A' ENUMS.Storage.weapons.containers.LANTIRNF14TARGET='weapons.containers.LANTIRN-F14-TARGET' ENUMS.Storage.weapons.bombs.CBU_52B='weapons.bombs.CBU_52B' ENUMS.Storage.weapons.adapters.b52mbd_mk84='weapons.adapters.b52-mbd_mk84' ENUMS.Storage.weapons.adapters.J11A_twinpylon_l='weapons.adapters.J-11A_twinpylon_l' ENUMS.Storage.weapons.gunmounts.MB339_DEFA553_R='weapons.gunmounts.{MB339_DEFA553_R}' ENUMS.Storage.weapons.containers.BARAX='weapons.containers.BARAX' ENUMS.Storage.weapons.shells.DEFA554_30_HE='weapons.shells.DEFA554_30_HE' ENUMS.Storage.weapons.droptanks.i16_eft='weapons.droptanks.i16_eft' ENUMS.Storage.weapons.bombs.BLU3B_GROUP='weapons.bombs.BLU-3B_GROUP' ENUMS.Storage.weapons.missiles.Sea_Cat='weapons.missiles.Sea_Cat' ENUMS.Storage.weapons.adapters.aero3b='weapons.adapters.aero-3b' ENUMS.Storage.weapons.nurs.SNEB_TYPE251_F1B='weapons.nurs.SNEB_TYPE251_F1B' ENUMS.Storage.weapons.missiles.FIM_92C='weapons.missiles.FIM_92C' ENUMS.Storage.weapons.missiles.SM_2ER='weapons.missiles.SM_2ER' ENUMS.Storage.weapons.missiles.AGM_114K='weapons.missiles.AGM_114K' ENUMS.Storage.weapons.bombs.AB_250_2_SD_10A='weapons.bombs.AB_250_2_SD_10A' ENUMS.Storage.weapons.missiles.X_65='weapons.missiles.X_65' ENUMS.Storage.weapons.bombs.British_GP_500LB_Bomb_Mk4='weapons.bombs.British_GP_500LB_Bomb_Mk4' ENUMS.Storage.weapons.shells._50Browning_Ball_M2='weapons.shells.50Browning_Ball_M2' ENUMS.Storage.weapons.containers.HB_F14_EXT_TARPS='weapons.containers.HB_F14_EXT_TARPS' ENUMS.Storage.weapons.gunmounts.PKT_7_62='weapons.gunmounts.PKT_7_62' ENUMS.Storage.weapons.shells._50Browning_I_M1='weapons.shells.50Browning_I_M1' ENUMS.Storage.weapons.shells.British303_Ball_Mk8='weapons.shells.British303_Ball_Mk8' ENUMS.Storage.weapons.adapters.F4PILON='weapons.adapters.F4-PILON' ENUMS.Storage.weapons.missiles.P_77='weapons.missiles.P_77' ENUMS.Storage.weapons.missiles.SA9M338K='weapons.missiles.SA9M338K' ENUMS.Storage.weapons.shells.ZTZ_7_62='weapons.shells.ZTZ_7_62' ENUMS.Storage.weapons.shells.Mauser7_92x57_B='weapons.shells.Mauser7.92x57_B.' ENUMS.Storage.weapons.missiles.X_28='weapons.missiles.X_28' ENUMS.Storage.weapons.missiles.KD_20='weapons.missiles.KD_20' ENUMS.Storage.weapons.missiles.TGM_65G='weapons.missiles.TGM_65G' ENUMS.Storage.weapons.adapters.mbd4='weapons.adapters.mbd-4' ENUMS.Storage.weapons.shells.Mauser7_92x57_S_m_K_='weapons.shells.Mauser7.92x57_S.m.K.' ENUMS.Storage.weapons.missiles.M39A1='weapons.missiles.M39A1' ENUMS.Storage.weapons.adapters.m559='weapons.adapters.m559' ENUMS.Storage.weapons.missiles.AGM_12B='weapons.missiles.AGM_12B' ENUMS.Storage.weapons.shells.M39_20_HEI='weapons.shells.M39_20_HEI' ENUMS.Storage.weapons.bombs.British_GP_500LB_Bomb_Mk4_Short='weapons.bombs.British_GP_500LB_Bomb_Mk4_Short' ENUMS.Storage.weapons.missiles.Rb15F='weapons.missiles.Rb 15F' ENUMS.Storage.weapons.missiles.AIM_120C='weapons.missiles.AIM_120C' ENUMS.Storage.weapons.shells.Mauser7_92x57_SmK_Lspurweiss="weapons.shells.Mauser7.92x57_S.m.K._L'spur(weiss)" ENUMS.Storage.weapons.nurs.S_5KP='weapons.nurs.S_5KP' ENUMS.Storage.weapons.bombs.GBU_31_V_4B='weapons.bombs.GBU_31_V_4B' ENUMS.Storage.weapons.missiles.HQ7B='weapons.missiles.HQ-7B' ENUMS.Storage.weapons.bombs.ODAB500PM='weapons.bombs.ODAB-500PM' ENUMS.Storage.weapons.bombs.BAP100='weapons.bombs.BAP-100' ENUMS.Storage.weapons.shells.MK_108_MGsch='weapons.shells.MK_108_MGsch' ENUMS.Storage.weapons.bombs.British_MC_500LB_Bomb_Mk1_Short='weapons.bombs.British_MC_500LB_Bomb_Mk1_Short' ENUMS.Storage.weapons.adapters.BRU42_LS_LAU131='weapons.adapters.BRU-42_LS_(LAU-131)' ENUMS.Storage.weapons.containers.SHPIL='weapons.containers.SHPIL' ENUMS.Storage.weapons.torpedoes.Mark_46='weapons.torpedoes.Mark_46' ENUMS.Storage.weapons.bombs.SAB_100MN='weapons.bombs.SAB_100MN' ENUMS.Storage.weapons.missiles.SA3M9M='weapons.missiles.SA3M9M' ENUMS.Storage.weapons.adapters.LAU61='weapons.adapters.LAU-61' ENUMS.Storage.weapons.adapters.mer2='weapons.adapters.mer2' ENUMS.Storage.weapons.shells.ship_Bofors_40mm_HE='weapons.shells.ship_Bofors_40mm_HE' ENUMS.Storage.weapons.nurs.S24A='weapons.nurs.S-24A' ENUMS.Storage.weapons.shells.GSh_30_2K_AP_Tr='weapons.shells.GSh_30_2K_AP_Tr' ENUMS.Storage.weapons.missiles.AIM7F='weapons.missiles.AIM-7F' ENUMS.Storage.weapons.shells.M383='weapons.shells.M383' ENUMS.Storage.weapons.nurs.HYDRA_70_M257='weapons.nurs.HYDRA_70_M257' ENUMS.Storage.weapons.droptanks.PTB_580G_F1='weapons.droptanks.PTB_580G_F1' ENUMS.Storage.weapons.gunmounts.C101DEFA553='weapons.gunmounts.{C-101-DEFA553}' ENUMS.Storage.weapons.missiles.MICA_R='weapons.missiles.MICA_R' ENUMS.Storage.weapons.shells.M53_APT_RED='weapons.shells.M53_APT_RED' ENUMS.Storage.weapons.missiles.AIM9P5='weapons.missiles.AIM-9P5' ENUMS.Storage.weapons.adapters._306M2='weapons.adapters.30-6-M2' ENUMS.Storage.weapons.shells._75mm_AA_JAP='weapons.shells.75mm_AA_JAP' ENUMS.Storage.weapons.nurs.TinyTim='weapons.nurs.Tiny Tim' ENUMS.Storage.weapons.missiles.X_22='weapons.missiles.X_22' ENUMS.Storage.weapons.nurs.S25O='weapons.nurs.S-25-O' ENUMS.Storage.weapons.missiles.X_101='weapons.missiles.X_101' ENUMS.Storage.weapons.missiles.AIM_54A_Mk47='weapons.missiles.AIM_54A_Mk47' ENUMS.Storage.weapons.containers.ECM_POD_L_175V='weapons.containers.{ECM_POD_L_175V}' ENUMS.Storage.weapons.shells._2A28_73='weapons.shells.2A28_73' ENUMS.Storage.weapons.shells.GAU8_30_AP='weapons.shells.GAU8_30_AP' ENUMS.Storage.weapons.shells.British303_Ball_Mk1c='weapons.shells.British303_Ball_Mk1c' ENUMS.Storage.weapons.missiles.AIM_9='weapons.missiles.AIM_9' ENUMS.Storage.weapons.missiles.SD10='weapons.missiles.SD-10' 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.gunmounts.defa_553='weapons.gunmounts.defa_553' ENUMS.Storage.weapons.nurs.BRM1_90MM_UG='weapons.nurs.BRM1_90MM_UG' ENUMS.Storage.weapons.gunmounts.CH47_STBD_M60D='weapons.gunmounts.{CH47_STBD_M60D}' ENUMS.Storage.weapons.adapters.LAU10='weapons.adapters.LAU-10' ENUMS.Storage.weapons.shells.L31_120mm_HESH='weapons.shells.L31_120mm_HESH' ENUMS.Storage.weapons.gunmounts.CH47_AFT_M60D='weapons.gunmounts.{CH47_AFT_M60D}' ENUMS.Storage.weapons.shells._20mm_M53_API='weapons.shells.20mm_M53_API' ENUMS.Storage.weapons.adapters.HB_F14_EXT_LAU7='weapons.adapters.HB_F14_EXT_LAU-7' ENUMS.Storage.weapons.shells.CHAP_76_PFHE='weapons.shells.CHAP_76_PFHE' ENUMS.Storage.weapons.bombs.KAB_500KrOD='weapons.bombs.KAB_500KrOD' ENUMS.Storage.weapons.adapters.PU_9S846_STRELEC='weapons.adapters.PU_9S846_STRELEC' ENUMS.Storage.weapons.containers.EclairM_51='weapons.containers.{EclairM_51}' ENUMS.Storage.weapons.containers.HB_ORD_Pave_Spike='weapons.containers.HB_ORD_Pave_Spike' ENUMS.Storage.weapons.shells.MINGR55_NO_TRC='weapons.shells.MINGR55_NO_TRC' ENUMS.Storage.weapons.nurs.PG_9V='weapons.nurs.PG_9V' ENUMS.Storage.weapons.gunmounts.M61A1='weapons.gunmounts.M-61A1' ENUMS.Storage.weapons.nurs.PG_16V='weapons.nurs.PG_16V' ENUMS.Storage.weapons.shells.British303_G_Mk4='weapons.shells.British303_G_Mk4' ENUMS.Storage.weapons.missiles.SA5B55='weapons.missiles.SA5B55' ENUMS.Storage.weapons.adapters.b52_CSRL_ALCM='weapons.adapters.b-52_CSRL_ALCM' ENUMS.Storage.weapons.adapters._9M114PILON='weapons.adapters.9M114-PILON' ENUMS.Storage.weapons.shells._50Browning_APIT_M20_Corsair='weapons.shells.50Browning_APIT_M20_Corsair' ENUMS.Storage.weapons.shells.PJ87_100_PFHE='weapons.shells.PJ87_100_PFHE' ENUMS.Storage.weapons.bombs._2503='weapons.bombs.250-3' ENUMS.Storage.weapons.shells._2A42_30_AP='weapons.shells.2A42_30_AP' ENUMS.Storage.weapons.shells._37mm_Type_100_JAP='weapons.shells.37mm_Type_100_JAP' ENUMS.Storage.weapons.droptanks.oiltank='weapons.droptanks.oiltank' ENUMS.Storage.weapons.droptanks.AV8BNA_AERO1D='weapons.droptanks.AV8BNA_AERO1D' ENUMS.Storage.weapons.containers.smoke_pod='weapons.containers.smoke_pod' ENUMS.Storage.weapons.missiles.AGM_12A='weapons.missiles.AGM_12A' ENUMS.Storage.weapons.missiles.MICA_T='weapons.missiles.MICA_T' ENUMS.Storage.weapons.droptanks._1100LTankEmpty='weapons.droptanks.1100L Tank Empty' ENUMS.Storage.weapons.adapters.CHAP_Mi28N_igla='weapons.adapters.CHAP_Mi28N_igla' ENUMS.Storage.weapons.bombs.GBU_15_V_1_B='weapons.bombs.GBU_15_V_1_B' ENUMS.Storage.weapons.missiles.Rb24='weapons.missiles.Rb 24' ENUMS.Storage.weapons.missiles.RB75='weapons.missiles.RB75' ENUMS.Storage.weapons.shells.M2_12_7_T='weapons.shells.M2_12_7_T' ENUMS.Storage.weapons.shells._2A42_30_HE='weapons.shells.2A42_30_HE' ENUMS.Storage.weapons.containers.HVAR_rocket='weapons.containers.HVAR_rocket' ENUMS.Storage.weapons.gunmounts.GIAT_M621_APHE='weapons.gunmounts.{GIAT_M621_APHE}' ENUMS.Storage.weapons.nurs.SNEB_TYPE253_F1B='weapons.nurs.SNEB_TYPE253_F1B' ENUMS.Storage.weapons.shells.M230_HEIM799='weapons.shells.M230_HEI M799' ENUMS.Storage.weapons.containers.HB_F14_EXT_BRU34='weapons.containers.HB_F14_EXT_BRU34' ENUMS.Storage.weapons.shells.Sprgr_34_L48='weapons.shells.Sprgr_34_L48' ENUMS.Storage.weapons.shells._7_62x39='weapons.shells.7_62x39' ENUMS.Storage.weapons.containers.LANTIRN='weapons.containers.LANTIRN' ENUMS.Storage.weapons.shells.GSH23_23_HE='weapons.shells.GSH23_23_HE' ENUMS.Storage.weapons.bombs.KAB_1500Kr='weapons.bombs.KAB_1500Kr' ENUMS.Storage.weapons.bombs.British_MC_250LB_Bomb_Mk1='weapons.bombs.British_MC_250LB_Bomb_Mk1' ENUMS.Storage.weapons.gunmounts.ANM3='weapons.gunmounts.{AN-M3}' ENUMS.Storage.weapons.droptanks.Spitfire_tank_1='weapons.droptanks.Spitfire_tank_1' ENUMS.Storage.weapons.missiles.AGM_78B='weapons.missiles.AGM_78B' ENUMS.Storage.weapons.adapters.hj12launchertube='weapons.adapters.hj12-launcher-tube' ENUMS.Storage.weapons.shells.M242_25_HE_M792='weapons.shells.M242_25_HE_M792' ENUMS.Storage.weapons.shells.M46='weapons.shells.M46' ENUMS.Storage.weapons.droptanks.HB_F14_EXT_DROPTANK_EMPTY='weapons.droptanks.HB_F14_EXT_DROPTANK_EMPTY' ENUMS.Storage.weapons.missiles.AIM_120='weapons.missiles.AIM_120' ENUMS.Storage.weapons.missiles.Rb15FforA_I__='weapons.missiles.Rb 15F (for A.I.)' ENUMS.Storage.weapons.gunmounts.NR23='weapons.gunmounts.NR-23' ENUMS.Storage.weapons.missiles.Vikhr_M='weapons.missiles.Vikhr_M' ENUMS.Storage.weapons.bombs.Mk_83='weapons.bombs.Mk_83' ENUMS.Storage.weapons.adapters._9m114pilon='weapons.adapters.9m114-pilon' ENUMS.Storage.weapons.bombs.RBK_500U_OAB_2_5RT='weapons.bombs.RBK_500U_OAB_2_5RT' ENUMS.Storage.weapons.shells._50Browning_AP_M2_Corsair='weapons.shells.50Browning_AP_M2_Corsair' ENUMS.Storage.weapons.shells.British303_G_Mk2='weapons.shells.British303_G_Mk2' ENUMS.Storage.weapons.gunmounts.UPK_23_25='weapons.gunmounts.UPK_23_25' ENUMS.Storage.weapons.shells.M242_25_AP_M919='weapons.shells.M242_25_AP_M919' ENUMS.Storage.weapons.bombs.Type_200A='weapons.bombs.Type_200A' ENUMS.Storage.weapons.containers.SORBCIJA_L='weapons.containers.SORBCIJA_L' ENUMS.Storage.weapons.shells.M322_120_AP='weapons.shells.M322_120_AP' ENUMS.Storage.weapons.missiles.AGR_20A='weapons.missiles.AGR_20A' ENUMS.Storage.weapons.missiles._9M723='weapons.missiles.9M723' ENUMS.Storage.weapons.bombs.BKF_AO2_5RT='weapons.bombs.BKF_AO2_5RT' ENUMS.Storage.weapons.missiles.P_33E='weapons.missiles.P_33E' ENUMS.Storage.weapons.adapters.b52mbd_m117='weapons.adapters.b52-mbd_m117' 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.adapters.OH58D_HRACK_R='weapons.adapters.OH58D_HRACK_R' ENUMS.Storage.weapons.missiles.AGM_78A='weapons.missiles.AGM_78A' ENUMS.Storage.weapons.bombs.FAB_100SV='weapons.bombs.FAB_100SV' ENUMS.Storage.weapons.adapters.F4E_dual_LAU7='weapons.adapters.F4E_dual_LAU7' ENUMS.Storage.weapons.shells.CHAP_76_HE_T='weapons.shells.CHAP_76_HE_T' ENUMS.Storage.weapons.adapters.HB_F14_EXT_SPARROW_PYLON='weapons.adapters.HB_F14_EXT_SPARROW_PYLON' ENUMS.Storage.weapons.missiles.P_27PE='weapons.missiles.P_27PE' ENUMS.Storage.weapons.shells._2A38_30_HE='weapons.shells.2A38_30_HE' ENUMS.Storage.weapons.nurs.WGr21='weapons.nurs.WGr21' ENUMS.Storage.weapons.droptanks.HB_F4E_EXT_WingTank_R_EMPTY='weapons.droptanks.HB_F-4E_EXT_WingTank_R_EMPTY' ENUMS.Storage.weapons.shells.British303_G_Mk5='weapons.shells.British303_G_Mk5' ENUMS.Storage.weapons.gunmounts.SUU_23_POD='weapons.gunmounts.{SUU_23_POD}' ENUMS.Storage.weapons.bombs.British_GP_250LB_Bomb_Mk1='weapons.bombs.British_GP_250LB_Bomb_Mk1' ENUMS.Storage.weapons.bombs.RBK_500U_BETAB_M='weapons.bombs.RBK_500U_BETAB_M' ENUMS.Storage.weapons.gunmounts.SA342_M134_SIDE_R='weapons.gunmounts.{SA342_M134_SIDE_R}' ENUMS.Storage.weapons.adapters.LAU_127='weapons.adapters.LAU_127' ENUMS.Storage.weapons.missiles.TGM_65D='weapons.missiles.TGM_65D' ENUMS.Storage.weapons.shells.ZTZ_125_AP='weapons.shells.ZTZ_125_AP' ENUMS.Storage.weapons.missiles.P_73='weapons.missiles.P_73' ENUMS.Storage.weapons.gunmounts.M134_R='weapons.gunmounts.M134_R' ENUMS.Storage.weapons.shells.M39_20_TP='weapons.shells.M39_20_TP' ENUMS.Storage.weapons.shells.GAU8_30_HE='weapons.shells.GAU8_30_HE' 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.gunmounts.OH_58_BRAUNING='weapons.gunmounts.OH_58_BRAUNING' ENUMS.Storage.weapons.shells.KDA_35_AP='weapons.shells.KDA_35_AP' ENUMS.Storage.weapons.shells.CL3143_120_AP='weapons.shells.CL3143_120_AP' ENUMS.Storage.weapons.shells.M61_20_TP_T='weapons.shells.M61_20_TP_T' 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.containers.US_M10_SMOKE_TANK_BLUE='weapons.containers.{US_M10_SMOKE_TANK_BLUE}' ENUMS.Storage.weapons.missiles.GAR8='weapons.missiles.GAR-8' ENUMS.Storage.weapons.missiles.ASM_N_2='weapons.missiles.ASM_N_2' ENUMS.Storage.weapons.bombs.BR_250='weapons.bombs.BR_250' ENUMS.Storage.weapons.containers.F18FLIRPOD='weapons.containers.F-18-FLIR-POD' ENUMS.Storage.weapons.containers.EclairM_42='weapons.containers.{EclairM_42}' ENUMS.Storage.weapons.missiles.P_27P='weapons.missiles.P_27P' ENUMS.Storage.weapons.bombs.British_SAP_500LB_Bomb_Mk5='weapons.bombs.British_SAP_500LB_Bomb_Mk5' ENUMS.Storage.weapons.shells.NR23_23x115_API='weapons.shells.NR23_23x115_API' ENUMS.Storage.weapons.shells.KPVT_14_5='weapons.shells.KPVT_14_5' ENUMS.Storage.weapons.gunmounts.M60_SIDE_L='weapons.gunmounts.M60_SIDE_L' ENUMS.Storage.weapons.nurs.S5M1_HEFRAG_FFAR='weapons.nurs.S5M1_HEFRAG_FFAR' ENUMS.Storage.weapons.bombs.SAB_100_FLARE='weapons.bombs.SAB_100_FLARE' ENUMS.Storage.weapons.adapters.HB_F4E_BRU42='weapons.adapters.HB_F-4E_BRU-42' ENUMS.Storage.weapons.containers.F15E_AAQ33_XR_ATPSE='weapons.containers.F-15E_AAQ-33_XR_ATP-SE' ENUMS.Storage.weapons.adapters.MBD267='weapons.adapters.MBD-2-67' ENUMS.Storage.weapons.adapters.OH58D_SRACK_R='weapons.adapters.OH58D_SRACK_R' ENUMS.Storage.weapons.adapters.UB16='weapons.adapters.UB-16' ENUMS.Storage.weapons.nurs.HYDRA_70_WTU1B='weapons.nurs.HYDRA_70_WTU1B' ENUMS.Storage.weapons.shells.L14A2_30_APDS='weapons.shells.L14A2_30_APDS' ENUMS.Storage.weapons.missiles.SVIR='weapons.missiles.SVIR' ENUMS.Storage.weapons.adapters.APU170='weapons.adapters.APU-170' ENUMS.Storage.weapons.missiles.AIM9E='weapons.missiles.AIM-9E' ENUMS.Storage.weapons.adapters.adapter_df4a='weapons.adapters.adapter_df4a' ENUMS.Storage.weapons.missiles.Super_530F='weapons.missiles.Super_530F' ENUMS.Storage.weapons.adapters.Rocket_Launcher_4_5inch='weapons.adapters.Rocket_Launcher_4_5inch' ENUMS.Storage.weapons.adapters.JF17_PF12_twin='weapons.adapters.JF-17_PF12_twin' ENUMS.Storage.weapons.adapters.CLB_30='weapons.adapters.CLB_30' ENUMS.Storage.weapons.gunmounts.KORD_12_7_MI24_R='weapons.gunmounts.KORD_12_7_MI24_R' ENUMS.Storage.weapons.gunmounts.KORD_12_7_MI24_L='weapons.gunmounts.KORD_12_7_MI24_L' ENUMS.Storage.weapons.adapters._9M120_pylon='weapons.adapters.9M120_pylon' ENUMS.Storage.weapons.missiles.REFLEX='weapons.missiles.REFLEX' ENUMS.Storage.weapons.adapters.oro57k_edm='weapons.adapters.oro-57k.edm' ENUMS.Storage.weapons.bombs.GBU_31_V_2B='weapons.bombs.GBU_31_V_2B' ENUMS.Storage.weapons.missiles.P_40T='weapons.missiles.P_40T' ENUMS.Storage.weapons.adapters.suu25='weapons.adapters.suu-25' ENUMS.Storage.weapons.missiles.GB6='weapons.missiles.GB-6' ENUMS.Storage.weapons.missiles.DWS39_MJ1='weapons.missiles.DWS39_MJ1' ENUMS.Storage.weapons.bombs.FAB_250='weapons.bombs.FAB_250' ENUMS.Storage.weapons.bombs.SD_500_A='weapons.bombs.SD_500_A' ENUMS.Storage.weapons.gunmounts.M60_SIDE_R='weapons.gunmounts.M60_SIDE_R' ENUMS.Storage.weapons.shells.L21A1_30_HE='weapons.shells.L21A1_30_HE' ENUMS.Storage.weapons.missiles.KD_63='weapons.missiles.KD_63' ENUMS.Storage.weapons.gunmounts.GAU_12_Equalizer_HE='weapons.gunmounts.{GAU_12_Equalizer_HE}' ENUMS.Storage.weapons.bombs.ROCKEYE='weapons.bombs.ROCKEYE' ENUMS.Storage.weapons.shells.GSh_30_2K_HE='weapons.shells.GSh_30_2K_HE' ENUMS.Storage.weapons.adapters.LAU3='weapons.adapters.LAU-3' ENUMS.Storage.weapons.shells.M39_20_API='weapons.shells.M39_20_API' ENUMS.Storage.weapons.nurs.HVAR='weapons.nurs.HVAR' ENUMS.Storage.weapons.adapters.F15E_LAU117='weapons.adapters.F-15E_LAU-117' ENUMS.Storage.weapons.adapters.SA342_LAU_HOT3_2x='weapons.adapters.SA342_LAU_HOT3_2x' ENUMS.Storage.weapons.droptanks.PTB800MIG21='weapons.droptanks.PTB-800-MIG21' ENUMS.Storage.weapons.missiles.AGM_114='weapons.missiles.AGM_114' ENUMS.Storage.weapons.shells._2A46M_125_AP='weapons.shells.2A46M_125_AP' ENUMS.Storage.weapons.droptanks.MB339_TT320_L='weapons.droptanks.MB339_TT320_L' ENUMS.Storage.weapons.shells.M61_20_HEIT_RED='weapons.shells.M61_20_HEIT_RED' ENUMS.Storage.weapons.shells.KS19_100AP='weapons.shells.KS19_100AP' ENUMS.Storage.weapons.containers.KINGAL='weapons.containers.KINGAL' ENUMS.Storage.weapons.nurs.RS82='weapons.nurs.RS-82' ENUMS.Storage.weapons.missiles.HOT2='weapons.missiles.HOT2' ENUMS.Storage.weapons.adapters.Schloss_500XIIC='weapons.adapters.Schloss_500XIIC' ENUMS.Storage.weapons.droptanks.fueltank450='weapons.droptanks.fueltank450' ENUMS.Storage.weapons.missiles.X_59M='weapons.missiles.X_59M' ENUMS.Storage.weapons.droptanks.PTB450='weapons.droptanks.PTB-450' ENUMS.Storage.weapons.containers.SPS141='weapons.containers.SPS-141' ENUMS.Storage.weapons.adapters.mbd3='weapons.adapters.mbd-3' ENUMS.Storage.weapons.bombs.OFAB100120TU='weapons.bombs.OFAB-100-120TU' ENUMS.Storage.weapons.shells._20mm_M56_HEI='weapons.shells.20mm_M56_HEI' 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.shells.M61='weapons.shells.M61' ENUMS.Storage.weapons.missiles.PL12='weapons.missiles.PL-12' ENUMS.Storage.weapons.missiles.R3R='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.GBU_10='weapons.bombs.GBU_10' ENUMS.Storage.weapons.adapters.b52mbd_agm86='weapons.adapters.b52-mbd_agm86' ENUMS.Storage.weapons.adapters.Spitfire_pilon2R='weapons.adapters.Spitfire_pilon2R' ENUMS.Storage.weapons.adapters.apu602_R='weapons.adapters.apu-60-2_R' ENUMS.Storage.weapons.shells._50Browning_APIT_M20='weapons.shells.50Browning_APIT_M20' ENUMS.Storage.weapons.bombs.FAB_50='weapons.bombs.FAB_50' ENUMS.Storage.weapons.shells._2A46M_125_HE='weapons.shells.2A46M_125_HE' ENUMS.Storage.weapons.containers.sa342_dipole_antenna='weapons.containers.sa342_dipole_antenna' ENUMS.Storage.weapons.shells._50Browning_T_M1='weapons.shells.50Browning_T_M1' ENUMS.Storage.weapons.bombs.OFAB100Jupiter='weapons.bombs.OFAB-100 Jupiter' ENUMS.Storage.weapons.adapters.MER5E='weapons.adapters.MER-5E' ENUMS.Storage.weapons.shells.NR30_30x155_APT='weapons.shells.NR30_30x155_APT' ENUMS.Storage.weapons.containers.ALQ184='weapons.containers.ALQ-184' ENUMS.Storage.weapons.missiles.AGM_45B='weapons.missiles.AGM_45B' ENUMS.Storage.weapons.containers.SKY_SHADOW='weapons.containers.SKY_SHADOW' ENUMS.Storage.weapons.gunmounts.FN_HMP400_200='weapons.gunmounts.{FN_HMP400_200}' ENUMS.Storage.weapons.containers.US_M10_SMOKE_TANK_GREEN='weapons.containers.{US_M10_SMOKE_TANK_GREEN}' ENUMS.Storage.weapons.bombs.BLU3_GROUP='weapons.bombs.BLU-3_GROUP' ENUMS.Storage.weapons.adapters.gdjiv1='weapons.adapters.gdj-iv1' ENUMS.Storage.weapons.missiles.SPIKE_ER='weapons.missiles.SPIKE_ER' ENUMS.Storage.weapons.shells.AK630_30_AP='weapons.shells.AK630_30_AP' ENUMS.Storage.weapons.missiles.AGM_65L='weapons.missiles.AGM_65L' ENUMS.Storage.weapons.gunmounts.MG_151_20='weapons.gunmounts.MG_151_20' ENUMS.Storage.weapons.droptanks.PTB490MIG21='weapons.droptanks.PTB-490-MIG21' ENUMS.Storage.weapons.shells.MG_20x82_HEI_T='weapons.shells.MG_20x82_HEI_T' ENUMS.Storage.weapons.adapters._143M2='weapons.adapters.14-3-M2' ENUMS.Storage.weapons.adapters.OH58D_Gorgona='weapons.adapters.OH-58D_Gorgona' ENUMS.Storage.weapons.missiles.Rb_04='weapons.missiles.Rb_04' ENUMS.Storage.weapons.nurs.C_8CM_RD='weapons.nurs.C_8CM_RD' ENUMS.Storage.weapons.missiles.AKD10='weapons.missiles.AKD-10' ENUMS.Storage.weapons.missiles.X_29L='weapons.missiles.X_29L' ENUMS.Storage.weapons.containers.F14LANTIRNTP='weapons.containers.{F14-LANTIRN-TP}' ENUMS.Storage.weapons.adapters.apu6='weapons.adapters.apu-6' ENUMS.Storage.weapons.bombs.AO_2_5RT='weapons.bombs.AO_2_5RT' ENUMS.Storage.weapons.shells.L23_120_AP='weapons.shells.L23_120_AP' ENUMS.Storage.weapons.missiles.AIM9L='weapons.missiles.AIM-9L' ENUMS.Storage.weapons.containers.ALQ131='weapons.containers.ALQ-131' ENUMS.Storage.weapons.shells._25mm_AA_JAP='weapons.shells.25mm_AA_JAP' ENUMS.Storage.weapons.nurs.C_8='weapons.nurs.C_8' ENUMS.Storage.weapons.missiles.YJ83='weapons.missiles.YJ-83' ENUMS.Storage.weapons.shells.MK_108_HEI='weapons.shells.MK_108_HEI' ENUMS.Storage.weapons.droptanks.PTB400_MIG19='weapons.droptanks.PTB400_MIG19' ENUMS.Storage.weapons.adapters.BRU42_LS='weapons.adapters.BRU-42_LS' ENUMS.Storage.weapons.adapters.M299_AGM114='weapons.adapters.M299_AGM114' ENUMS.Storage.weapons.containers.US_M10_SMOKE_TANK_ORANGE='weapons.containers.{US_M10_SMOKE_TANK_ORANGE}' ENUMS.Storage.weapons.adapters.B1B_Conventional_Rotary_Launcher='weapons.adapters.B-1B_Conventional_Rotary_Launcher' ENUMS.Storage.weapons.nurs.S_5M='weapons.nurs.S_5M' ENUMS.Storage.weapons.shells.MG_13x64_I_T='weapons.shells.MG_13x64_I_T' ENUMS.Storage.weapons.bombs.CBU_99='weapons.bombs.CBU_99' ENUMS.Storage.weapons.bombs.LUU_2B='weapons.bombs.LUU_2B' ENUMS.Storage.weapons.containers.aaq28LEFTlitening='weapons.containers.aaq-28LEFT litening' ENUMS.Storage.weapons.containers.F4PILON='weapons.containers.F4-PILON' 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.adapters.LAU105='weapons.adapters.LAU-105' ENUMS.Storage.weapons.nurs.FFARMk1HE='weapons.nurs.FFAR Mk1 HE' ENUMS.Storage.weapons.shells.M39_20_TP_T='weapons.shells.M39_20_TP_T' ENUMS.Storage.weapons.containers.FAS='weapons.containers.FAS' ENUMS.Storage.weapons.missiles.X_31P='weapons.missiles.X_31P' ENUMS.Storage.weapons.missiles.R13M='weapons.missiles.R-13M' ENUMS.Storage.weapons.missiles.AGM_154B='weapons.missiles.AGM_154B' ENUMS.Storage.weapons.bombs.BAT120='weapons.bombs.BAT-120' ENUMS.Storage.weapons.shells.OF_350='weapons.shells.OF_350' ENUMS.Storage.weapons.adapters.BRU42_LS_LAU68='weapons.adapters.BRU-42_LS_(LAU-68)' ENUMS.Storage.weapons.shells.M134_7_62_T='weapons.shells.M134_7_62_T' ENUMS.Storage.weapons.shells.DM33_120_AP='weapons.shells.DM33_120_AP' ENUMS.Storage.weapons.shells.M256_120_HE_L55='weapons.shells.M256_120_HE_L55' ENUMS.Storage.weapons.shells.Hispano_Mk_II_Mk_Z_Ball='weapons.shells.Hispano_Mk_II_Mk_Z_Ball' ENUMS.Storage.weapons.containers.aispodt50_r='weapons.containers.ais-pod-t50_r' ENUMS.Storage.weapons.bombs.GBU_11='weapons.bombs.GBU_11' ENUMS.Storage.weapons.gunmounts.m3_browning='weapons.gunmounts.m3_browning' ENUMS.Storage.weapons.containers.ah64d_radar='weapons.containers.ah-64d_radar' ENUMS.Storage.weapons.shells.YakB_12_7='weapons.shells.YakB_12_7' ENUMS.Storage.weapons.nurs.HYDRA_70_M151='weapons.nurs.HYDRA_70_M151' ENUMS.Storage.weapons.droptanks.fueltank200='weapons.droptanks.fueltank200' ENUMS.Storage.weapons.adapters.ER4_Rack='weapons.adapters.ER4_Rack' 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.containers.SORBCIJA_R='weapons.containers.SORBCIJA_R' ENUMS.Storage.weapons.missiles.R13M1='weapons.missiles.R-13M1' ENUMS.Storage.weapons.missiles.ALARM='weapons.missiles.ALARM' ENUMS.Storage.weapons.gunmounts.AKAN_NO_TRC='weapons.gunmounts.{AKAN_NO_TRC}' ENUMS.Storage.weapons.missiles.RS2US='weapons.missiles.RS2US' ENUMS.Storage.weapons.shells.M230_30='weapons.shells.M230_30' ENUMS.Storage.weapons.bombs.BLG66_EG='weapons.bombs.BLG66_EG' ENUMS.Storage.weapons.bombs.FAB_500='weapons.bombs.FAB_500' ENUMS.Storage.weapons.adapters.lau118a='weapons.adapters.lau-118a' ENUMS.Storage.weapons.missiles.BGM_109B='weapons.missiles.BGM_109B' ENUMS.Storage.weapons.missiles.LD10='weapons.missiles.LD-10' ENUMS.Storage.weapons.shells._120_EXPL_F1_120mm_HE='weapons.shells.120_EXPL_F1_120mm_HE' ENUMS.Storage.weapons.missiles.ROLAND_R='weapons.missiles.ROLAND_R' ENUMS.Storage.weapons.droptanks.PTB300_MIG15='weapons.droptanks.PTB300_MIG15' ENUMS.Storage.weapons.missiles.SPIKE_ERA='weapons.missiles.SPIKE_ERA' ENUMS.Storage.weapons.adapters.b52_suu67='weapons.adapters.b-52_suu67' ENUMS.Storage.weapons.shells.VOG17='weapons.shells.VOG17' ENUMS.Storage.weapons.adapters.JF17_GDJII19L='weapons.adapters.JF-17_GDJ-II19L' ENUMS.Storage.weapons.containers.F4U1D_SMOKE_WHITE='weapons.containers.{F4U1D_SMOKE_WHITE}' ENUMS.Storage.weapons.shells.GSh_30_2K_AP='weapons.shells.GSh_30_2K_AP' ENUMS.Storage.weapons.shells.M61_20_PGU30='weapons.shells.M61_20_PGU30' ENUMS.Storage.weapons.nurs.HYDRA_70_M274='weapons.nurs.HYDRA_70_M274' ENUMS.Storage.weapons.bombs.Mk_84='weapons.bombs.Mk_84' ENUMS.Storage.weapons.bombs.BDU_50LD='weapons.bombs.BDU_50LD' ENUMS.Storage.weapons.gunmounts.A20_TopTurret_M2_R='weapons.gunmounts.A20_TopTurret_M2_R' ENUMS.Storage.weapons.gunmounts.MG_131='weapons.gunmounts.MG_131' ENUMS.Storage.weapons.adapters.AUF2_RACK='weapons.adapters.AUF2_RACK' ENUMS.Storage.weapons.missiles.Mistral='weapons.missiles.Mistral' ENUMS.Storage.weapons.bombs.LUU_2BB='weapons.bombs.LUU_2BB' ENUMS.Storage.weapons.adapters.JF17_GDJII19R='weapons.adapters.JF-17_GDJ-II19R' ENUMS.Storage.weapons.shells.PGU32_SAPHEI_T='weapons.shells.PGU32_SAPHEI_T' ENUMS.Storage.weapons.adapters.F15E_LAU88='weapons.adapters.F-15E_LAU-88' ENUMS.Storage.weapons.missiles.AGM_154='weapons.missiles.AGM_154' ENUMS.Storage.weapons.gunmounts.A20_TopTurret_M2_L='weapons.gunmounts.A20_TopTurret_M2_L' ENUMS.Storage.weapons.missiles.TOW2='weapons.missiles.TOW2' ENUMS.Storage.weapons.shells.British303_B_Mk6z='weapons.shells.British303_B_Mk6z' ENUMS.Storage.weapons.bombs.P50T='weapons.bombs.P-50T' ENUMS.Storage.weapons.shells._5_56x45_NOtr='weapons.shells.5_56x45_NOtr' ENUMS.Storage.weapons.missiles.SA9M333='weapons.missiles.SA9M333' ENUMS.Storage.weapons.nurs.HYDRA_70_M259='weapons.nurs.HYDRA_70_M259' ENUMS.Storage.weapons.shells._50Browning_API_M8='weapons.shells.50Browning_API_M8' ENUMS.Storage.weapons.missiles.AGM_84E='weapons.missiles.AGM_84E' ENUMS.Storage.weapons.droptanks.FuelTank_350L='weapons.droptanks.FuelTank_350L' ENUMS.Storage.weapons.adapters._9k121='weapons.adapters.9k121' ENUMS.Storage.weapons.missiles.KD_63B='weapons.missiles.KD_63B' ENUMS.Storage.weapons.droptanks.FuelTank_150L='weapons.droptanks.FuelTank_150L' ENUMS.Storage.weapons.shells._5_45x39='weapons.shells.5_45x39' ENUMS.Storage.weapons.missiles.AIM_54C_Mk60='weapons.missiles.AIM_54C_Mk60' ENUMS.Storage.weapons.missiles.CATM_9M='weapons.missiles.CATM_9M' ENUMS.Storage.weapons.droptanks.Drop_Tank_300_Liter='weapons.droptanks.Drop_Tank_300_Liter' ENUMS.Storage.weapons.gunmounts.GUV_VOG='weapons.gunmounts.GUV_VOG' ENUMS.Storage.weapons.bombs.Mk_83AIR='weapons.bombs.Mk_83AIR' ENUMS.Storage.weapons.adapters.MAK79_VAR_4='weapons.adapters.MAK-79_VAR_4' ENUMS.Storage.weapons.shells.M39_20_HEI_T='weapons.shells.M39_20_HEI_T' 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.shells.CHAP_125_3BM69_APFSDS_T='weapons.shells.CHAP_125_3BM69_APFSDS_T' ENUMS.Storage.weapons.adapters.BRU_42A='weapons.adapters.BRU_42A' ENUMS.Storage.weapons.missiles.TGM_65H='weapons.missiles.TGM_65H' ENUMS.Storage.weapons.bombs.GBU_27='weapons.bombs.GBU_27' ENUMS.Storage.weapons.adapters.APU1240='weapons.adapters.APU-12-40' ENUMS.Storage.weapons.droptanks.F4U1D_Drop_Tank_Aux='weapons.droptanks.F4U-1D_Drop_Tank_Aux' ENUMS.Storage.weapons.shells.DM12_L55_120mm_HEAT_MP_T='weapons.shells.DM12_L55_120mm_HEAT_MP_T' ENUMS.Storage.weapons.shells.British303_O_Mk1='weapons.shells.British303_O_Mk1' ENUMS.Storage.weapons.missiles.HBAIM7E2='weapons.missiles.HB-AIM-7E-2' ENUMS.Storage.weapons.containers.Spear='weapons.containers.Spear' ENUMS.Storage.weapons.bombs.BetAB_500='weapons.bombs.BetAB_500' ENUMS.Storage.weapons.adapters.HB_F14_EXT_BRU34='weapons.adapters.HB_F14_EXT_BRU34' ENUMS.Storage.weapons.missiles.Rb24J='weapons.missiles.Rb 24J' ENUMS.Storage.weapons.shells.M256_120_AP='weapons.shells.M256_120_AP' ENUMS.Storage.weapons.bombs.SAMP250HD='weapons.bombs.SAMP250HD' ENUMS.Storage.weapons.containers.alq184long='weapons.containers.alq-184long' ENUMS.Storage.weapons.shells.UOF412_100HE='weapons.shells.UOF412_100HE' ENUMS.Storage.weapons.bombs.Mk_83CT='weapons.bombs.Mk_83CT' ENUMS.Storage.weapons.nurs.SNEB_TYPE254_F1B_YELLOW='weapons.nurs.SNEB_TYPE254_F1B_YELLOW' 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.gunmounts.HispanoMkII='weapons.gunmounts.HispanoMkII' ENUMS.Storage.weapons.missiles.C_701IR='weapons.missiles.C_701IR' ENUMS.Storage.weapons.missiles.P_9M133='weapons.missiles.P_9M133' ENUMS.Storage.weapons.containers.HB_ALE_40_0_0='weapons.containers.HB_ALE_40_0_0' ENUMS.Storage.weapons.missiles.KONKURS='weapons.missiles.KONKURS' ENUMS.Storage.weapons.bombs.HB_F4E_GBU15V1='weapons.bombs.HB_F4E_GBU15V1' ENUMS.Storage.weapons.bombs.SC_50='weapons.bombs.SC_50' ENUMS.Storage.weapons.bombs.AN_M66='weapons.bombs.AN_M66' ENUMS.Storage.weapons.adapters.UB32='weapons.adapters.UB-32' ENUMS.Storage.weapons.adapters.HB_ORD_LAU88='weapons.adapters.HB_ORD_LAU-88' ENUMS.Storage.weapons.bombs.RBK_250='weapons.bombs.RBK_250' ENUMS.Storage.weapons.shells._6_5mm_Type_91_JAP='weapons.shells.6_5mm_Type_91_JAP' ENUMS.Storage.weapons.gunmounts.ADEN_GUNPOD='weapons.gunmounts.{ADEN_GUNPOD}' ENUMS.Storage.weapons.bombs.MK106='weapons.bombs.MK106' ENUMS.Storage.weapons.bombs.RBK_250S='weapons.bombs.RBK_250S' ENUMS.Storage.weapons.shells.M61_20_PGU28='weapons.shells.M61_20_PGU28' ENUMS.Storage.weapons.gunmounts.OH58D_M3P='weapons.gunmounts.OH58D_M3P' ENUMS.Storage.weapons.containers.EclairM_15='weapons.containers.{EclairM_15}' ENUMS.Storage.weapons.containers.EclairM_33='weapons.containers.{EclairM_33}' ENUMS.Storage.weapons.shells.NR30_30x155_APHE='weapons.shells.NR30_30x155_APHE' ENUMS.Storage.weapons.gunmounts.GAU_12_Equalizer='weapons.gunmounts.{GAU_12_Equalizer}' ENUMS.Storage.weapons.bombs.IAB500='weapons.bombs.IAB-500' ENUMS.Storage.weapons.bombs.OH58D_Green_Smoke_Grenade='weapons.bombs.OH58D_Green_Smoke_Grenade' ENUMS.Storage.weapons.adapters.ptab2_5ko_block1='weapons.adapters.ptab-2_5ko_block1' ENUMS.Storage.weapons.shells._7_7mm_Type_97_JAP='weapons.shells.7_7mm_Type_97_JAP' ENUMS.Storage.weapons.missiles.R_530F_IR='weapons.missiles.R_530F_IR' ENUMS.Storage.weapons.bombs.FAB250M54='weapons.bombs.FAB-250M54' ENUMS.Storage.weapons.missiles.RIM_116A='weapons.missiles.RIM_116A' ENUMS.Storage.weapons.shells.PINK_PROJECTILE='weapons.shells.PINK_PROJECTILE' ENUMS.Storage.weapons.shells.CHAP_76_HESH_T='weapons.shells.CHAP_76_HESH_T' 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.Sea_Eagle='weapons.missiles.Sea_Eagle' ENUMS.Storage.weapons.shells.PKT_7_62='weapons.shells.PKT_7_62' ENUMS.Storage.weapons.missiles.PL5EII='weapons.missiles.PL-5EII' ENUMS.Storage.weapons.bombs.GBU_16='weapons.bombs.GBU_16' ENUMS.Storage.weapons.shells.OFL_120F2_AP='weapons.shells.OFL_120F2_AP' ENUMS.Storage.weapons.missiles.AIM_54A_Mk60='weapons.missiles.AIM_54A_Mk60' ENUMS.Storage.weapons.bombs.CBU_97='weapons.bombs.CBU_97' ENUMS.Storage.weapons.adapters.XM158='weapons.adapters.XM158' ENUMS.Storage.weapons.containers.M2KC_AGF='weapons.containers.{M2KC_AGF}' ENUMS.Storage.weapons.adapters.CBLS200='weapons.adapters.CBLS-200' ENUMS.Storage.weapons.containers.SPRD99='weapons.containers.SPRD-99' ENUMS.Storage.weapons.missiles.DWS39_MJ1_MJ2='weapons.missiles.DWS39_MJ1_MJ2' ENUMS.Storage.weapons.bombs.BDU_33='weapons.bombs.BDU_33' ENUMS.Storage.weapons.missiles.TOW='weapons.missiles.TOW' ENUMS.Storage.weapons.gunmounts.OH58D_M3P_L400='weapons.gunmounts.OH58D_M3P_L400' ENUMS.Storage.weapons.bombs.KAB_1500LG='weapons.bombs.KAB_1500LG' ENUMS.Storage.weapons.shells.MK45_127mm_AP_Essex='weapons.shells.MK45_127mm_AP_Essex' ENUMS.Storage.weapons.shells.M61_20_HE_gr='weapons.shells.M61_20_HE_gr' ENUMS.Storage.weapons.missiles.BRM1_90MM='weapons.missiles.BRM-1_90MM' ENUMS.Storage.weapons.missiles.Ataka_9M120F='weapons.missiles.Ataka_9M120F' ENUMS.Storage.weapons.adapters.lau88='weapons.adapters.lau-88' ENUMS.Storage.weapons.missiles.Sea_Wolf='weapons.missiles.Sea_Wolf' ENUMS.Storage.weapons.shells.M61_20_PGU27='weapons.shells.M61_20_PGU27' ENUMS.Storage.weapons.missiles.CM400AKG='weapons.missiles.CM-400AKG' ENUMS.Storage.weapons.containers.F15E_AAQ14_LANTIRN='weapons.containers.F-15E_AAQ-14_LANTIRN' ENUMS.Storage.weapons.containers.wmd7='weapons.containers.wmd7' ENUMS.Storage.weapons.missiles.AIM7E2='weapons.missiles.AIM-7E-2' ENUMS.Storage.weapons.shells.Utes_12_7x108='weapons.shells.Utes_12_7x108' ENUMS.Storage.weapons.containers.HB_ORD_Pave_Spike_Fast='weapons.containers.HB_ORD_Pave_Spike_Fast' ENUMS.Storage.weapons.adapters.MAK79_VAR_2='weapons.adapters.MAK-79_VAR_2' ENUMS.Storage.weapons.missiles.AGM_65D='weapons.missiles.AGM_65D' ENUMS.Storage.weapons.missiles.AGM_86='weapons.missiles.AGM_86' ENUMS.Storage.weapons.shells.British303_G_Mk3='weapons.shells.British303_G_Mk3' ENUMS.Storage.weapons.shells.M61_20_AP_gr='weapons.shells.M61_20_AP_gr' ENUMS.Storage.weapons.adapters.UB_32A_24='weapons.adapters.UB_32A_24' ENUMS.Storage.weapons.containers.F15E_AAQ28_LITENING='weapons.containers.F-15E_AAQ-28_LITENING' ENUMS.Storage.weapons.bombs.OH58D_Blue_Smoke_Grenade='weapons.bombs.OH58D_Blue_Smoke_Grenade' ENUMS.Storage.weapons.bombs.KAB_500Kr='weapons.bombs.KAB_500Kr' ENUMS.Storage.weapons.containers.SPS141100='weapons.containers.SPS-141-100' ENUMS.Storage.weapons.missiles.AIM9JULI='weapons.missiles.AIM-9JULI' ENUMS.Storage.weapons.droptanks.MB339_TT500_R='weapons.droptanks.MB339_TT500_R' ENUMS.Storage.weapons.adapters.towpilon='weapons.adapters.tow-pilon' ENUMS.Storage.weapons.nurs.SNEB_TYPE254_H1_YELLOW='weapons.nurs.SNEB_TYPE254_H1_YELLOW' ENUMS.Storage.weapons.missiles.M30='weapons.missiles.M30' ENUMS.Storage.weapons.bombs.Durandal='weapons.bombs.Durandal' ENUMS.Storage.weapons.adapters.apu7='weapons.adapters.apu-7' ENUMS.Storage.weapons.nurs.C_8CM_VT='weapons.nurs.C_8CM_VT' ENUMS.Storage.weapons.containers.aispodt50='weapons.containers.ais-pod-t50' ENUMS.Storage.weapons.shells.M485_155_IL='weapons.shells.M485_155_IL' ENUMS.Storage.weapons.bombs.RN24='weapons.bombs.RN-24' ENUMS.Storage.weapons.shells._2A64_152='weapons.shells.2A64_152' ENUMS.Storage.weapons.containers._='weapons.containers.' ENUMS.Storage.weapons.shells.M61_20_HE='weapons.shells.M61_20_HE' ENUMS.Storage.weapons.gunmounts.CPG_M4='weapons.gunmounts.CPG_M4' ENUMS.Storage.weapons.shells._57mm_Type_90_JAP='weapons.shells.57mm_Type_90_JAP' ENUMS.Storage.weapons.missiles.ADM_141A='weapons.missiles.ADM_141A' ENUMS.Storage.weapons.containers.KBpod='weapons.containers.KBpod' ENUMS.Storage.weapons.shells.DEFA554_30_HE_TRACERS='weapons.shells.DEFA554_30_HE_TRACERS' ENUMS.Storage.weapons.missiles.SA_IRIS_T_SL='weapons.missiles.SA_IRIS_T_SL' ENUMS.Storage.weapons.missiles.R55='weapons.missiles.R-55' ENUMS.Storage.weapons.adapters.BRU42_HS='weapons.adapters.BRU-42_HS' ENUMS.Storage.weapons.shells.Hispano_Mk_II_MKIIZ_AP='weapons.shells.Hispano_Mk_II_MKIIZ_AP' ENUMS.Storage.weapons.missiles.SA2V755='weapons.missiles.SA2V755' ENUMS.Storage.weapons.missiles.PL8B='weapons.missiles.PL-8B' ENUMS.Storage.weapons.droptanks.Mosquito_Drop_Tank_100gal='weapons.droptanks.Mosquito_Drop_Tank_100gal' ENUMS.Storage.weapons.shells.MG_13x64_HE='weapons.shells.MG_13x64_HE' ENUMS.Storage.weapons.shells.Hispano_Mk_II_Tracer_G='weapons.shells.Hispano_Mk_II_Tracer_G' ENUMS.Storage.weapons.nurs.SNEB_TYPE253_H1='weapons.nurs.SNEB_TYPE253_H1' ENUMS.Storage.weapons.nurs.ARAKM70BAPPX='weapons.nurs.ARAKM70BAPPX' ENUMS.Storage.weapons.adapters.TER9A='weapons.adapters.TER-9A' ENUMS.Storage.weapons.missiles._9M317='weapons.missiles.9M317' ENUMS.Storage.weapons.adapters.LAU115C='weapons.adapters.LAU-115C' ENUMS.Storage.weapons.gunmounts.M134_L='weapons.gunmounts.M134_L' ENUMS.Storage.weapons.shells._20mm_M220_Tracer='weapons.shells.20mm_M220_Tracer' ENUMS.Storage.weapons.containers.EclairM_06='weapons.containers.{EclairM_06}' ENUMS.Storage.weapons.bombs.RBK_500AO='weapons.bombs.RBK_500AO' ENUMS.Storage.weapons.shells.Bofors_40mm_Essex='weapons.shells.Bofors_40mm_Essex' ENUMS.Storage.weapons.containers.MB339_Vinten='weapons.containers.MB339_Vinten' ENUMS.Storage.weapons.nurs.ARAKM70BHE='weapons.nurs.ARAKM70BHE' ENUMS.Storage.weapons.bombs.FAB250M62='weapons.bombs.FAB-250-M62' ENUMS.Storage.weapons.missiles.Rb04E='weapons.missiles.Rb 04E' ENUMS.Storage.weapons.droptanks.PTB400_MIG15='weapons.droptanks.PTB400_MIG15' ENUMS.Storage.weapons.bombs.PTAB_2_5KO='weapons.bombs.PTAB_2_5KO' ENUMS.Storage.weapons.adapters.M2000C_LRF4_edm='weapons.adapters.M-2000C_LRF4.edm' ENUMS.Storage.weapons.missiles.AIM_9X='weapons.missiles.AIM_9X' ENUMS.Storage.weapons.shells.MG_13x64_I='weapons.shells.MG_13x64_I' ENUMS.Storage.weapons.bombs.GBU_8_B='weapons.bombs.GBU_8_B' ENUMS.Storage.weapons.missiles.SA9M31='weapons.missiles.SA9M31' ENUMS.Storage.weapons.containers.rightSeat='weapons.containers.rightSeat' ENUMS.Storage.weapons.shells.Pzgr_3940='weapons.shells.Pzgr_39/40' ENUMS.Storage.weapons.shells._2A60_120='weapons.shells.2A60_120' ENUMS.Storage.weapons.bombs.GBU_17='weapons.bombs.GBU_17' ENUMS.Storage.weapons.missiles.HHQ9='weapons.missiles.HHQ-9' ENUMS.Storage.weapons.bombs.Mk_84AIR_GP='weapons.bombs.Mk_84AIR_GP' ENUMS.Storage.weapons.bombs.RBK_250_275_AO_1SCH='weapons.bombs.RBK_250_275_AO_1SCH' ENUMS.Storage.weapons.missiles.AIM7E='weapons.missiles.AIM-7E' 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.shells.MK_108_MGsch_T='weapons.shells.MK_108_MGsch_T' ENUMS.Storage.weapons.missiles.GB6HE='weapons.missiles.GB-6-HE' ENUMS.Storage.weapons.nurs.SNEB_TYPE254_H1_RED='weapons.nurs.SNEB_TYPE254_H1_RED' ENUMS.Storage.weapons.shells.HP30_30_HE='weapons.shells.HP30_30_HE' ENUMS.Storage.weapons.bombs.RN28='weapons.bombs.RN-28' ENUMS.Storage.weapons.shells.L31A7_HESH='weapons.shells.L31A7_HESH' ENUMS.Storage.weapons.shells.GSH301_30_AP='weapons.shells.GSH301_30_AP' ENUMS.Storage.weapons.bombs.RBK_500U='weapons.bombs.RBK_500U' ENUMS.Storage.weapons.droptanks.HB_F4E_EXT_Center_Fuel_Tank='weapons.droptanks.HB_F-4E_EXT_Center_Fuel_Tank' ENUMS.Storage.weapons.containers.F15E_AAQ13_LANTIRN='weapons.containers.F-15E_AAQ-13_LANTIRN' ENUMS.Storage.weapons.droptanks._800LTankEmpty='weapons.droptanks.800L Tank Empty' ENUMS.Storage.weapons.missiles.MIM_104='weapons.missiles.MIM_104' ENUMS.Storage.weapons.shells.MG_13x64_API='weapons.shells.MG_13x64_API' ENUMS.Storage.weapons.shells.M61_20_TP='weapons.shells.M61_20_TP' ENUMS.Storage.weapons.bombs.LUU_19='weapons.bombs.LUU_19' ENUMS.Storage.weapons.shells.M55A2_TP_RED='weapons.shells.M55A2_TP_RED' ENUMS.Storage.weapons.adapters.su27twinpylon='weapons.adapters.su-27-twinpylon' ENUMS.Storage.weapons.nurs.M26HE='weapons.nurs.M26HE' ENUMS.Storage.weapons.bombs.BLU4B_GROUP='weapons.bombs.BLU-4B_GROUP' ENUMS.Storage.weapons.nurs.HYDRA_70_M156='weapons.nurs.HYDRA_70_M156' ENUMS.Storage.weapons.shells.DM23_105_AP='weapons.shells.DM23_105_AP' ENUMS.Storage.weapons.missiles.SM_1='weapons.missiles.SM_1' ENUMS.Storage.weapons.missiles.OH58D_FIM_92='weapons.missiles.OH58D_FIM_92' ENUMS.Storage.weapons.containers.F18LDTPOD='weapons.containers.F-18-LDT-POD' ENUMS.Storage.weapons.missiles.AGM_65K='weapons.missiles.AGM_65K' ENUMS.Storage.weapons.shells.Bofors_40mm_HE='weapons.shells.Bofors_40mm_HE' ENUMS.Storage.weapons.missiles.HAWK_RAKETA='weapons.missiles.HAWK_RAKETA' ENUMS.Storage.weapons.shells._7_62x54='weapons.shells.7_62x54' ENUMS.Storage.weapons.shells.DM12_120mm_HEAT_MP_T='weapons.shells.DM12_120mm_HEAT_MP_T' ENUMS.Storage.weapons.bombs.AN_M64='weapons.bombs.AN_M64' ENUMS.Storage.weapons.containers.rearCargoSeats='weapons.containers.rearCargoSeats' ENUMS.Storage.weapons.bombs.AN_M65='weapons.bombs.AN_M65' ENUMS.Storage.weapons.missiles.Rb74='weapons.missiles.Rb 74' ENUMS.Storage.weapons.shells.DEFA553_30AP='weapons.shells.DEFA553_30AP' ENUMS.Storage.weapons.nurs.S5M='weapons.nurs.S-5M' ENUMS.Storage.weapons.gunmounts.M134_SIDE_R='weapons.gunmounts.M134_SIDE_R' ENUMS.Storage.weapons.missiles.HJ12='weapons.missiles.HJ-12' ENUMS.Storage.weapons.shells.PLZ_155_HE='weapons.shells.PLZ_155_HE' ENUMS.Storage.weapons.adapters.BRU_33A='weapons.adapters.BRU_33A' ENUMS.Storage.weapons.nurs.ARAKM70BAP='weapons.nurs.ARAKM70BAP' ENUMS.Storage.weapons.missiles.MMagicII='weapons.missiles.MMagicII' ENUMS.Storage.weapons.nurs.HYDRA_70_M282='weapons.nurs.HYDRA_70_M282' ENUMS.Storage.weapons.nurs.ARF8M3HEI='weapons.nurs.ARF8M3HEI' ENUMS.Storage.weapons.shells._76mm_AA_JAP='weapons.shells.76mm_AA_JAP' ENUMS.Storage.weapons.missiles.Igla_1E='weapons.missiles.Igla_1E' ENUMS.Storage.weapons.nurs.SMERCH_9M55K='weapons.nurs.SMERCH_9M55K' ENUMS.Storage.weapons.nurs.C_24='weapons.nurs.C_24' ENUMS.Storage.weapons.shells.GSH301_30_HE='weapons.shells.GSH301_30_HE' ENUMS.Storage.weapons.nurs.SNEB_TYPE256_H1='weapons.nurs.SNEB_TYPE256_H1' ENUMS.Storage.weapons.adapters.T45_PMBR='weapons.adapters.T45_PMBR' ENUMS.Storage.weapons.containers.EclairM_24='weapons.containers.{EclairM_24}' ENUMS.Storage.weapons.droptanks.MB339_TT500_L='weapons.droptanks.MB339_TT500_L' ENUMS.Storage.weapons.bombs.BKF_PTAB2_5KO='weapons.bombs.BKF_PTAB2_5KO' ENUMS.Storage.weapons.shells.Br303='weapons.shells.Br303' ENUMS.Storage.weapons.shells.DANA_152='weapons.shells.DANA_152' ENUMS.Storage.weapons.nurs.S5MO_HEFRAG_FFAR='weapons.nurs.S5MO_HEFRAG_FFAR' ENUMS.Storage.weapons.missiles.AIM9P3='weapons.missiles.AIM-9P3' ENUMS.Storage.weapons.gunmounts.GAU_12='weapons.gunmounts.GAU_12' ENUMS.Storage.weapons.shells.MK45_127='weapons.shells.MK45_127' ENUMS.Storage.weapons.nurs.C_8CM_GN='weapons.nurs.C_8CM_GN' ENUMS.Storage.weapons.nurs.C_13='weapons.nurs.C_13' ENUMS.Storage.weapons.gunmounts.OH58D_M3P_L300='weapons.gunmounts.OH58D_M3P_L300' ENUMS.Storage.weapons.missiles.AGM_65A='weapons.missiles.AGM_65A' ENUMS.Storage.weapons.containers.AV8BNA_ALQ164='weapons.containers.AV8BNA_ALQ164' ENUMS.Storage.weapons.bombs.OH58D_Red_Smoke_Grenade='weapons.bombs.OH58D_Red_Smoke_Grenade' ENUMS.Storage.weapons.bombs.FAB_1500='weapons.bombs.FAB_1500' ENUMS.Storage.weapons.shells.M230_TPM788='weapons.shells.M230_TP M788' ENUMS.Storage.weapons.containers.leftSeat='weapons.containers.leftSeat' ENUMS.Storage.weapons.missiles.Kormoran='weapons.missiles.Kormoran' ENUMS.Storage.weapons.adapters.boz100='weapons.adapters.boz-100' ENUMS.Storage.weapons.nurs.HYDRA_70_MK1='weapons.nurs.HYDRA_70_MK1' ENUMS.Storage.weapons.shells.Utes_12_7x108_T='weapons.shells.Utes_12_7x108_T' ENUMS.Storage.weapons.missiles.AGM_154A='weapons.missiles.AGM_154A' ENUMS.Storage.weapons.adapters.MBD3LAU68='weapons.adapters.MBD-3-LAU-68' ENUMS.Storage.weapons.nurs.C_8CM_WH='weapons.nurs.C_8CM_WH' ENUMS.Storage.weapons.missiles.MatraSuper530D='weapons.missiles.Matra Super 530D' ENUMS.Storage.weapons.bombs.BDU_50HD='weapons.bombs.BDU_50HD' ENUMS.Storage.weapons.adapters.M2000c_BAP_Rack='weapons.adapters.M-2000c_BAP_Rack' ENUMS.Storage.weapons.shells._7_92x57sS='weapons.shells.7_92x57sS' ENUMS.Storage.weapons.shells.M20_50_aero_APIT='weapons.shells.M20_50_aero_APIT' ENUMS.Storage.weapons.bombs.KAB_500='weapons.bombs.KAB_500' ENUMS.Storage.weapons.gunmounts.AKAN='weapons.gunmounts.{AKAN}' ENUMS.Storage.weapons.shells._20MM_M242_HEIT='weapons.shells.20MM_M242_HEI-T' ENUMS.Storage.weapons.gunmounts.GAU_12_Equalizer_AP='weapons.gunmounts.{GAU_12_Equalizer_AP}' ENUMS.Storage.weapons.gunmounts.FN_HMP400='weapons.gunmounts.{FN_HMP400}' ENUMS.Storage.weapons.containers.dlpod_akg='weapons.containers.dlpod_akg' ENUMS.Storage.weapons.droptanks.PTB600_MIG15='weapons.droptanks.PTB600_MIG15' ENUMS.Storage.weapons.adapters.apu602_L='weapons.adapters.apu-60-2_L' ENUMS.Storage.weapons.missiles.SeaSparrow='weapons.missiles.SeaSparrow' ENUMS.Storage.weapons.droptanks._='weapons.droptanks.' ENUMS.Storage.weapons.adapters.lau117='weapons.adapters.lau-117' ENUMS.Storage.weapons.shells.M197_20='weapons.shells.M197_20' ENUMS.Storage.weapons.shells.Br303_tr='weapons.shells.Br303_tr' ENUMS.Storage.weapons.adapters.MBD3LAU61='weapons.adapters.MBD-3-LAU-61' ENUMS.Storage.weapons.bombs.British_SAP_250LB_Bomb_Mk5='weapons.bombs.British_SAP_250LB_Bomb_Mk5' ENUMS.Storage.weapons.adapters.apu68m3='weapons.adapters.apu-68m3' ENUMS.Storage.weapons.shells.British303_Ball_Mk6='weapons.shells.British303_Ball_Mk6' ENUMS.Storage.weapons.shells._7_62x54_NOTRACER='weapons.shells.7_62x54_NOTRACER' ENUMS.Storage.weapons.nurs.SNEB_TYPE250_F1B='weapons.nurs.SNEB_TYPE250_F1B' ENUMS.Storage.weapons.shells.ZTZ_14_5='weapons.shells.ZTZ_14_5' ENUMS.Storage.weapons.bombs.CBU_105='weapons.bombs.CBU_105' ENUMS.Storage.weapons.droptanks.FW190_FuelTank='weapons.droptanks.FW-190_Fuel-Tank' ENUMS.Storage.weapons.missiles.X_58='weapons.missiles.X_58' ENUMS.Storage.weapons.bombs.LYSBOMB11089='weapons.bombs.LYSBOMB 11089' ENUMS.Storage.weapons.containers.PAVETACK='weapons.containers.PAVETACK' ENUMS.Storage.weapons.bombs.GBU_24='weapons.bombs.GBU_24' ENUMS.Storage.weapons.gunmounts.FN_HMP400_100='weapons.gunmounts.{FN_HMP400_100}' ENUMS.Storage.weapons.missiles.AIM7MH='weapons.missiles.AIM-7MH' ENUMS.Storage.weapons.adapters.rb05pylon='weapons.adapters.rb05pylon' ENUMS.Storage.weapons.shells.DEFA553_30APIT='weapons.shells.DEFA553_30APIT' ENUMS.Storage.weapons.shells.Flak18_Sprgr_39='weapons.shells.Flak18_Sprgr_39' ENUMS.Storage.weapons.missiles.X_35='weapons.missiles.X_35' ENUMS.Storage.weapons.bombs.BL_755='weapons.bombs.BL_755' ENUMS.Storage.weapons.containers.ETHER='weapons.containers.ETHER' ENUMS.Storage.weapons.droptanks.F4U1D_Drop_Tank_Mk5='weapons.droptanks.F4U-1D_Drop_Tank_Mk5' ENUMS.Storage.weapons.containers.CE2_SMOKE_WHITE='weapons.containers.{CE2_SMOKE_WHITE}' ENUMS.Storage.weapons.bombs.Mk_82Y='weapons.bombs.Mk_82Y' ENUMS.Storage.weapons.bombs.British_MC_500LB_Bomb_Mk2='weapons.bombs.British_MC_500LB_Bomb_Mk2' ENUMS.Storage.weapons.adapters.HB_ORD_SUU_7='weapons.adapters.HB_ORD_SUU_7' ENUMS.Storage.weapons.shells.MK75_76='weapons.shells.MK75_76' ENUMS.Storage.weapons.shells.M68_105_AP='weapons.shells.M68_105_AP' ENUMS.Storage.weapons.missiles.SA57E6='weapons.missiles.SA57E6' ENUMS.Storage.weapons.missiles.AGM_86C='weapons.missiles.AGM_86C' ENUMS.Storage.weapons.missiles.P_24T='weapons.missiles.P_24T' ENUMS.Storage.weapons.adapters.OH58D_HRACK_L='weapons.adapters.OH58D_HRACK_L' ENUMS.Storage.weapons.gunmounts.MK_108='weapons.gunmounts.MK_108' ENUMS.Storage.weapons.adapters.APU68='weapons.adapters.APU-68' ENUMS.Storage.weapons.shells.British303_G_Mk6z='weapons.shells.British303_G_Mk6z' ENUMS.Storage.weapons.containers.aispodt50_l='weapons.containers.ais-pod-t50_l' ENUMS.Storage.weapons.gunmounts.N37='weapons.gunmounts.N-37' ENUMS.Storage.weapons.missiles.X_555='weapons.missiles.X_555' ENUMS.Storage.weapons.bombs.FAB500M54='weapons.bombs.FAB-500M54' ENUMS.Storage.weapons.containers.AN_AAQ_33='weapons.containers.AN_AAQ_33' ENUMS.Storage.weapons.containers.M2KC_AAF='weapons.containers.{M2KC_AAF}' ENUMS.Storage.weapons.shells.NR23_23x115_HEI_T='weapons.shells.NR23_23x115_HEI_T' ENUMS.Storage.weapons.shells.KPVT_14_5_T='weapons.shells.KPVT_14_5_T' ENUMS.Storage.weapons.shells.M56A3_HE_RED='weapons.shells.M56A3_HE_RED' ENUMS.Storage.weapons.bombs.FAB500SL='weapons.bombs.FAB-500SL' ENUMS.Storage.weapons.bombs.KAB_500S='weapons.bombs.KAB_500S' ENUMS.Storage.weapons.bombs.SAMP400LD='weapons.bombs.SAMP400LD' ENUMS.Storage.weapons.bombs.BDU_45B='weapons.bombs.BDU_45B' ENUMS.Storage.weapons.adapters.APU73='weapons.adapters.APU-73' ENUMS.Storage.weapons.missiles._9M723_HE='weapons.missiles.9M723_HE' ENUMS.Storage.weapons.bombs.GBU_15_V_31_B='weapons.bombs.GBU_15_V_31_B' ENUMS.Storage.weapons.adapters.CHAP_Tu95MS_rotary_launcher='weapons.adapters.CHAP_Tu95MS_rotary_launcher' ENUMS.Storage.weapons.droptanks.HB_F4E_EXT_WingTank='weapons.droptanks.HB_F-4E_EXT_WingTank' 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.nurs.C_25='weapons.nurs.C_25' ENUMS.Storage.weapons.adapters.MAK79_VAR_3='weapons.adapters.MAK-79_VAR_3' ENUMS.Storage.weapons.adapters._9m120='weapons.adapters.9m120' ENUMS.Storage.weapons.shells.GSh_30_2K_HE_Tr='weapons.shells.GSh_30_2K_HE_Tr' ENUMS.Storage.weapons.adapters.hf20_pod='weapons.adapters.hf20_pod' ENUMS.Storage.weapons.missiles.AGM_122='weapons.missiles.AGM_122' ENUMS.Storage.weapons.missiles.P_60='weapons.missiles.P_60' ENUMS.Storage.weapons.shells.K307_155HE='weapons.shells.K307_155HE' ENUMS.Storage.weapons.shells.A222_130='weapons.shells.A222_130' ENUMS.Storage.weapons.nurs.Zuni_127='weapons.nurs.Zuni_127' ENUMS.Storage.weapons.missiles.AIM9J='weapons.missiles.AIM-9J' ENUMS.Storage.weapons.shells.BK_27='weapons.shells.BK_27' ENUMS.Storage.weapons.adapters.M272_AGM114='weapons.adapters.M272_AGM114' ENUMS.Storage.weapons.shells.M242_25_AP_M791='weapons.shells.M242_25_AP_M791' ENUMS.Storage.weapons.adapters.HB_F14_EXT_SHOULDER_PHX_R='weapons.adapters.HB_F14_EXT_SHOULDER_PHX_R' ENUMS.Storage.weapons.gunmounts.OH58D_M3P_L100='weapons.gunmounts.OH58D_M3P_L100' ENUMS.Storage.weapons.bombs.BetAB_500ShP='weapons.bombs.BetAB_500ShP' 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.Ataka_9M220='weapons.missiles.Ataka_9M220' ENUMS.Storage.weapons.adapters.rb04pylon='weapons.adapters.rb04pylon' 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.adapters.M2000C_LRF4='weapons.adapters.M-2000C_LRF4' ENUMS.Storage.weapons.shells.HESH_105='weapons.shells.HESH_105' ENUMS.Storage.weapons.gunmounts.CH47_PORT_M240H='weapons.gunmounts.{CH47_PORT_M240H}' ENUMS.Storage.weapons.containers.SMOKE_WHITE='weapons.containers.{SMOKE_WHITE}' ENUMS.Storage.weapons.bombs.British_GP_250LB_Bomb_Mk4='weapons.bombs.British_GP_250LB_Bomb_Mk4' ENUMS.Storage.weapons.gunmounts.GIAT_M621_HEAP='weapons.gunmounts.{GIAT_M621_HEAP}' ENUMS.Storage.weapons.nurs.ARF8M3TPSM='weapons.nurs.ARF8M3TPSM' ENUMS.Storage.weapons.nurs.M8rocket='weapons.nurs.M8rocket' ENUMS.Storage.weapons.missiles.X_25MR='weapons.missiles.X_25MR' ENUMS.Storage.weapons.droptanks.fueltank230='weapons.droptanks.fueltank230' ENUMS.Storage.weapons.droptanks.PTB490CMIG21='weapons.droptanks.PTB-490C-MIG21' ENUMS.Storage.weapons.droptanks.M2KC_02_RPL541='weapons.droptanks.M2KC_02_RPL541' ENUMS.Storage.weapons.nurs.SNEB_TYPE254_F1B_GREEN='weapons.nurs.SNEB_TYPE254_F1B_GREEN' ENUMS.Storage.weapons.adapters.mbd='weapons.adapters.mbd' ENUMS.Storage.weapons.droptanks.HB_F4E_EXT_Center_Fuel_Tank_EMPTY='weapons.droptanks.HB_F-4E_EXT_Center_Fuel_Tank_EMPTY' ENUMS.Storage.weapons.missiles.AGM_84D='weapons.missiles.AGM_84D' ENUMS.Storage.weapons.bombs.M257_FLARE='weapons.bombs.M257_FLARE' ENUMS.Storage.weapons.missiles.AGM_84A='weapons.missiles.AGM_84A' ENUMS.Storage.weapons.gunmounts.GIAT_M621_HE='weapons.gunmounts.{GIAT_M621_HE}' ENUMS.Storage.weapons.missiles.AIM_54C_Mk47='weapons.missiles.AIM_54C_Mk47' ENUMS.Storage.weapons.containers.MPS410='weapons.containers.MPS-410' ENUMS.Storage.weapons.missiles.HY2='weapons.missiles.HY-2' ENUMS.Storage.weapons.bombs.Mk_81='weapons.bombs.Mk_81' ENUMS.Storage.weapons.shells.Oerlikon_20mm_Essex='weapons.shells.Oerlikon_20mm_Essex' ENUMS.Storage.weapons.adapters.PylonM71='weapons.adapters.PylonM71' ENUMS.Storage.weapons.droptanks._1100LTank='weapons.droptanks.1100L Tank' ENUMS.Storage.weapons.bombs.BAP_100='weapons.bombs.BAP_100' ENUMS.Storage.weapons.gunmounts.PK3='weapons.gunmounts.{PK-3}' ENUMS.Storage.weapons.adapters.b52_CRL_mod1='weapons.adapters.b-52_CRL_mod1' ENUMS.Storage.weapons.adapters._9m120m='weapons.adapters.9m120m' ENUMS.Storage.weapons.droptanks.M2KC_02_RPL541_EMPTY='weapons.droptanks.M2KC_02_RPL541_EMPTY' ENUMS.Storage.weapons.bombs.BR_500='weapons.bombs.BR_500' ENUMS.Storage.weapons.adapters._9K114_Shturm='weapons.adapters.9K114_Shturm' ENUMS.Storage.weapons.adapters.f4pilon='weapons.adapters.f4-pilon' ENUMS.Storage.weapons.gunmounts.SPPU_22='weapons.gunmounts.SPPU_22' ENUMS.Storage.weapons.gunmounts.GSh232taildefense='weapons.gunmounts.GSh-23-2 tail defense' ENUMS.Storage.weapons.bombs.FAB250M54TU='weapons.bombs.FAB-250M54TU' ENUMS.Storage.weapons.nurs.HYDRA_70_M229='weapons.nurs.HYDRA_70_M229' ENUMS.Storage.weapons.shells.M185_155='weapons.shells.M185_155' ENUMS.Storage.weapons.adapters.sa342_ATAM_Tube_2x='weapons.adapters.sa342_ATAM_Tube_2x' ENUMS.Storage.weapons.shells._53UBR281U='weapons.shells.53-UBR-281U' ENUMS.Storage.weapons.nurs.SNEB_TYPE259E_H1='weapons.nurs.SNEB_TYPE259E_H1' ENUMS.Storage.weapons.bombs.SAB_250_200='weapons.bombs.SAB_250_200' ENUMS.Storage.weapons.missiles.ADM_141B='weapons.missiles.ADM_141B' ENUMS.Storage.weapons.adapters.kmgu2='weapons.adapters.kmgu-2' ENUMS.Storage.weapons.adapters.B20='weapons.adapters.B-20' ENUMS.Storage.weapons.containers.U22A='weapons.containers.U22A' ENUMS.Storage.weapons.shells.M246_20_HE_gr='weapons.shells.M246_20_HE_gr' ENUMS.Storage.weapons.nurs.SNEB_TYPE251_H1='weapons.nurs.SNEB_TYPE251_H1' ENUMS.Storage.weapons.missiles.Kh25MP_PRGS1VP='weapons.missiles.Kh25MP_PRGS1VP' ENUMS.Storage.weapons.adapters.BR21Gerat='weapons.adapters.BR21-Gerat' ENUMS.Storage.weapons.adapters.apu13u2='weapons.adapters.apu-13u-2' ENUMS.Storage.weapons.shells.Hispano_Mk_II_MKI_HEI='weapons.shells.Hispano_Mk_II_MKI_HE/I' ENUMS.Storage.weapons.nurs.FFAR_Mk61='weapons.nurs.FFAR_Mk61' ENUMS.Storage.weapons.containers.BRD4250='weapons.containers.BRD-4-250' ENUMS.Storage.weapons.containers.HB_ORD_MER='weapons.containers.HB_ORD_MER' ENUMS.Storage.weapons.droptanks.PTB_1200_F1='weapons.droptanks.PTB_1200_F1' ENUMS.Storage.weapons.bombs.British_GP_500LB_Bomb_Mk1='weapons.bombs.British_GP_500LB_Bomb_Mk1' ENUMS.Storage.weapons.missiles.AGM_119='weapons.missiles.AGM_119' ENUMS.Storage.weapons.shells._3BM59_125_AP='weapons.shells.3BM59_125_AP' ENUMS.Storage.weapons.shells.M2_50_aero_AP='weapons.shells.M2_50_aero_AP' ENUMS.Storage.weapons.adapters.LAU68='weapons.adapters.LAU-68' ENUMS.Storage.weapons.nurs.C_5='weapons.nurs.C_5' ENUMS.Storage.weapons.nurs.S24B='weapons.nurs.S-24B' ENUMS.Storage.weapons.adapters.HB_F4E_LAU34='weapons.adapters.HB_F-4E_LAU-34' ENUMS.Storage.weapons.shells._2A18_122='weapons.shells.2A18_122' ENUMS.Storage.weapons.missiles.SCUD_RAKETA='weapons.missiles.SCUD_RAKETA' ENUMS.Storage.weapons.adapters.b52_HSAB='weapons.adapters.b-52_HSAB' ENUMS.Storage.weapons.nurs.R4M='weapons.nurs.R4M' ENUMS.Storage.weapons.bombs.LYSBOMB11086='weapons.bombs.LYSBOMB 11086' ENUMS.Storage.weapons.bombs.SD_250_Stg='weapons.bombs.SD_250_Stg' 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.bombs.AB_500_1_SD_10A='weapons.bombs.AB_500_1_SD_10A' ENUMS.Storage.weapons.containers.Eclair='weapons.containers.{Eclair}' ENUMS.Storage.weapons.gunmounts.MB339_ANM3_R='weapons.gunmounts.{MB339_ANM3_R}' ENUMS.Storage.weapons.shells.N37_37x155_API_T='weapons.shells.N37_37x155_API_T' ENUMS.Storage.weapons.shells.MG_20x82_MGsch='weapons.shells.MG_20x82_MGsch' ENUMS.Storage.weapons.containers.fullCargoSeats='weapons.containers.fullCargoSeats' ENUMS.Storage.weapons.shells.British303_G_Mk1='weapons.shells.British303_G_Mk1' ENUMS.Storage.weapons.adapters.LR25='weapons.adapters.LR-25' ENUMS.Storage.weapons.gunmounts.CH47_PORT_M134D='weapons.gunmounts.{CH47_PORT_M134D}' ENUMS.Storage.weapons.containers.MIG21_SMOKE_RED='weapons.containers.{MIG21_SMOKE_RED}' ENUMS.Storage.weapons.shells.M53_AP_RED='weapons.shells.M53_AP_RED' ENUMS.Storage.weapons.gunmounts.GIAT_M621_AP='weapons.gunmounts.{GIAT_M621_AP}' ENUMS.Storage.weapons.nurs.C_8CM='weapons.nurs.C_8CM' ENUMS.Storage.weapons.missiles.P_40R='weapons.missiles.P_40R' ENUMS.Storage.weapons.missiles.YJ12='weapons.missiles.YJ-12' ENUMS.Storage.weapons.missiles.CM_802AKG='weapons.missiles.CM_802AKG' ENUMS.Storage.weapons.missiles.SA9M38M1='weapons.missiles.SA9M38M1' ENUMS.Storage.weapons.droptanks.AV8BNA_AERO1D_EMPTY='weapons.droptanks.AV8BNA_AERO1D_EMPTY' ENUMS.Storage.weapons.bombs.British_GP_250LB_Bomb_Mk5='weapons.bombs.British_GP_250LB_Bomb_Mk5' ENUMS.Storage.weapons.shells.British303_Ball_Mk7='weapons.shells.British303_Ball_Mk7' ENUMS.Storage.weapons.bombs.GBU_43='weapons.bombs.GBU_43' ENUMS.Storage.weapons.missiles.AGM_88='weapons.missiles.AGM_88' ENUMS.Storage.weapons.droptanks.droptank_110_gal='weapons.droptanks.droptank_110_gal' ENUMS.Storage.weapons.missiles.GB6SFW='weapons.missiles.GB-6-SFW' ENUMS.Storage.weapons.bombs.SAB_250_FLARE='weapons.bombs.SAB_250_FLARE' ENUMS.Storage.weapons.adapters.ao2_5rt_block1='weapons.adapters.ao-2_5rt_block1' ENUMS.Storage.weapons.shells._7_62x51='weapons.shells.7_62x51' ENUMS.Storage.weapons.gunmounts.AKAN_NO_TRC='weapons.gunmounts.AKAN_NO_TRC' ENUMS.Storage.weapons.bombs.LYSBOMB11088='weapons.bombs.LYSBOMB 11088' ENUMS.Storage.weapons.shells.PKT_7_62_T='weapons.shells.PKT_7_62_T' ENUMS.Storage.weapons.gunmounts.BrowningM2='weapons.gunmounts.BrowningM2' ENUMS.Storage.weapons.containers.MIG21_SMOKE_WHITE='weapons.containers.{MIG21_SMOKE_WHITE}' ENUMS.Storage.weapons.adapters.BRU_57='weapons.adapters.BRU_57' ENUMS.Storage.weapons.bombs.MK_82AIR='weapons.bombs.MK_82AIR' ENUMS.Storage.weapons.missiles.R_550='weapons.missiles.R_550' ENUMS.Storage.weapons.shells.Hispano_Mk_II_APT='weapons.shells.Hispano_Mk_II_AP/T' ENUMS.Storage.weapons.adapters.MAK79_VAR_1='weapons.adapters.MAK-79_VAR_1' ENUMS.Storage.weapons.nurs.GRAD_9M22U='weapons.nurs.GRAD_9M22U' ENUMS.Storage.weapons.gunmounts.CH47_AFT_M240H='weapons.gunmounts.{CH47_AFT_M240H}' ENUMS.Storage.weapons.shells.GSH_23_AP='weapons.shells.GSH_23_AP' ENUMS.Storage.weapons.missiles.SA9M33='weapons.missiles.SA9M33' ENUMS.Storage.weapons.missiles.YJ83K='weapons.missiles.YJ-83K' ENUMS.Storage.weapons.shells.MG_13x64_APT='weapons.shells.MG_13x64_APT' ENUMS.Storage.weapons.missiles.AIM7P='weapons.missiles.AIM-7P' ENUMS.Storage.weapons.shells._50Browning_Ball_M2_Corsair='weapons.shells.50Browning_Ball_M2_Corsair' ENUMS.Storage.weapons.nurs.C_8CM_BU='weapons.nurs.C_8CM_BU' ENUMS.Storage.weapons.bombs.OH58D_Yellow_Smoke_Grenade='weapons.bombs.OH58D_Yellow_Smoke_Grenade' ENUMS.Storage.weapons.bombs.GBU_32_V_2B='weapons.bombs.GBU_32_V_2B' ENUMS.Storage.weapons.nurs.SNEB_TYPE257_F1B='weapons.nurs.SNEB_TYPE257_F1B' ENUMS.Storage.weapons.missiles.Rb04EforA_I__='weapons.missiles.Rb 04E (for A.I.)' ENUMS.Storage.weapons.containers.MB339_SMOKEPOD='weapons.containers.MB339_SMOKE-POD' ENUMS.Storage.weapons.containers.HB_F14_EXT_LAU7='weapons.containers.HB_F14_EXT_LAU-7' ENUMS.Storage.weapons.missiles.P_27T='weapons.missiles.P_27T' ENUMS.Storage.weapons.adapters.B1B_28store_Conventional_Bomb_Module='weapons.adapters.B-1B_28-store_Conventional_Bomb_Module' ENUMS.Storage.weapons.shells.GAU8_30_TP='weapons.shells.GAU8_30_TP' ENUMS.Storage.weapons.droptanks.LNS_VIG_XTANK='weapons.droptanks.LNS_VIG_XTANK' ENUMS.Storage.weapons.shells._2A7_23_HE='weapons.shells.2A7_23_HE' ENUMS.Storage.weapons.shells.MINGR55='weapons.shells.MINGR55' ENUMS.Storage.weapons.gunmounts.M230='weapons.gunmounts.M230' ENUMS.Storage.weapons.shells.MK45_127mm_Essex='weapons.shells.MK45_127mm_Essex' ENUMS.Storage.weapons.droptanks.F15E_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.torpedoes.LTF_5B='weapons.torpedoes.LTF_5B' ENUMS.Storage.weapons.adapters.HB_F4E_LAU117='weapons.adapters.HB_F4E_LAU117' ENUMS.Storage.weapons.containers.HB_ORD_Missile_Well_Adapter='weapons.containers.HB_ORD_Missile_Well_Adapter' ENUMS.Storage.weapons.bombs.SC_500_J='weapons.bombs.SC_500_J' ENUMS.Storage.weapons.adapters.AKU58='weapons.adapters.AKU-58' ENUMS.Storage.weapons.missiles.PL8A='weapons.missiles.PL-8A' ENUMS.Storage.weapons.gunmounts.MB339_DEFA553_L='weapons.gunmounts.{MB339_DEFA553_L}' ENUMS.Storage.weapons.adapters.UB1657UMP='weapons.adapters.UB-16-57UMP' ENUMS.Storage.weapons.droptanks.fuel_tank_230='weapons.droptanks.fuel_tank_230' ENUMS.Storage.weapons.nurs.SNEB_TYPE257_H1='weapons.nurs.SNEB_TYPE257_H1' ENUMS.Storage.weapons.missiles.RB75B='weapons.missiles.RB75B' ENUMS.Storage.weapons.shells.NR30_30x155_HEI_T='weapons.shells.NR30_30x155_HEI_T' ENUMS.Storage.weapons.adapters.apu68um3='weapons.adapters.apu-68um3' ENUMS.Storage.weapons.missiles.R_550_M1='weapons.missiles.R_550_M1' ENUMS.Storage.weapons.adapters.OH58D_SRACK_L='weapons.adapters.OH58D_SRACK_L' ENUMS.Storage.weapons.gunmounts.M61='weapons.gunmounts.M-61' ENUMS.Storage.weapons.missiles.X_41='weapons.missiles.X_41' ENUMS.Storage.weapons.gunmounts.GSH_23='weapons.gunmounts.GSH_23' ENUMS.Storage.weapons.missiles.R_530F_EM='weapons.missiles.R_530F_EM' ENUMS.Storage.weapons.containers.kg600='weapons.containers.kg600' ENUMS.Storage.weapons.missiles.M31='weapons.missiles.M31' ENUMS.Storage.weapons.bombs._2502='weapons.bombs.250-2' ENUMS.Storage.weapons.gunmounts.M134_SIDE_L='weapons.gunmounts.M134_SIDE_L' ENUMS.Storage.weapons.missiles.AGM_65B='weapons.missiles.AGM_65B' ENUMS.Storage.weapons.adapters.BRD4250='weapons.adapters.BRD-4-250' ENUMS.Storage.weapons.bombs.SAMP250LD='weapons.bombs.SAMP250LD' ENUMS.Storage.weapons.containers.AAQ28_LITENING='weapons.containers.AAQ-28_LITENING' ENUMS.Storage.weapons.droptanks.Mosquito_Drop_Tank_50gal='weapons.droptanks.Mosquito_Drop_Tank_50gal' ENUMS.Storage.weapons.shells._7_92x57_Smkl='weapons.shells.7_92x57_Smkl' ENUMS.Storage.weapons.shells.M68_105_HE='weapons.shells.M68_105_HE' ENUMS.Storage.weapons.containers.SPRD_99Twin='weapons.containers.SPRD_99Twin' ENUMS.Storage.weapons.missiles.HBAIM7E='weapons.missiles.HB-AIM-7E' ENUMS.Storage.weapons.shells.M2_12_7_TG='weapons.shells.M2_12_7_TG' ENUMS.Storage.weapons.torpedoes.YU6='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.shells.M256_120_AP_L55='weapons.shells.M256_120_AP_L55' ENUMS.Storage.weapons.gunmounts.GUV_YakB_GSHP='weapons.gunmounts.GUV_YakB_GSHP' ENUMS.Storage.weapons.bombs.GBU_29='weapons.bombs.GBU_29' ENUMS.Storage.weapons.gunmounts.GIAT_M261='weapons.gunmounts.GIAT_M261' ENUMS.Storage.weapons.missiles.R3S='weapons.missiles.R-3S' ENUMS.Storage.weapons.adapters.LAU131='weapons.adapters.LAU-131' ENUMS.Storage.weapons.gunmounts.KORD_12_7='weapons.gunmounts.KORD_12_7' ENUMS.Storage.weapons.shells.M230_ADEMDEFA='weapons.shells.M230_ADEM/DEFA' ENUMS.Storage.weapons.missiles.SM_2='weapons.missiles.SM_2' ENUMS.Storage.weapons.gunmounts.CH47_STBD_M134D='weapons.gunmounts.{CH47_STBD_M134D}' ENUMS.Storage.weapons.missiles.P_27TE='weapons.missiles.P_27TE' ENUMS.Storage.weapons.missiles.X_25ML='weapons.missiles.X_25ML' ENUMS.Storage.weapons.containers.lau105='weapons.containers.lau-105' ENUMS.Storage.weapons.droptanks.FPU_8A='weapons.droptanks.FPU_8A' ENUMS.Storage.weapons.bombs.BLG66='weapons.bombs.BLG66' ENUMS.Storage.weapons.shells._2A33_152='weapons.shells.2A33_152' ENUMS.Storage.weapons.nurs.MO_10104M='weapons.nurs.MO_10104M' ENUMS.Storage.weapons.shells.M825A1_155_SM='weapons.shells.M825A1_155_SM' ENUMS.Storage.weapons.gunmounts.AKAN='weapons.gunmounts.AKAN' ENUMS.Storage.weapons.gunmounts.Browning303MkII='weapons.gunmounts.Browning303MkII' ENUMS.Storage.weapons.nurs.URAGAN_9M27F='weapons.nurs.URAGAN_9M27F' ENUMS.Storage.weapons.nurs.FFARMk5HEAT='weapons.nurs.FFAR Mk5 HEAT' ENUMS.Storage.weapons.nurs.ARF8M3API='weapons.nurs.ARF8M3API' ENUMS.Storage.weapons.shells._3UBM11_100mm_AP='weapons.shells.3UBM11_100mm_AP' ENUMS.Storage.weapons.containers.ASO2='weapons.containers.ASO-2' ENUMS.Storage.weapons.shells.DEFA552_30='weapons.shells.DEFA552_30' ENUMS.Storage.weapons.gunmounts.DEFA_553='weapons.gunmounts.DEFA_553' ENUMS.Storage.weapons.missiles.AGM_45A='weapons.missiles.AGM_45A' ENUMS.Storage.weapons.missiles.Super_530D='weapons.missiles.Super_530D' ENUMS.Storage.weapons.adapters.mbd3u668='weapons.adapters.mbd3-u6-68' ENUMS.Storage.weapons.adapters.BRU_55='weapons.adapters.BRU_55' ENUMS.Storage.weapons.adapters.SA342_Telson8='weapons.adapters.SA342_Telson8' ENUMS.Storage.weapons.adapters.c25pu='weapons.adapters.c-25pu' ENUMS.Storage.weapons.bombs.FAB500TA='weapons.bombs.FAB-500TA' ENUMS.Storage.weapons.bombs.SAMP125LD='weapons.bombs.SAMP125LD' ENUMS.Storage.weapons.bombs.LYSBOMB_CANDLE='weapons.bombs.LYSBOMB_CANDLE' ENUMS.Storage.weapons.missiles.AGM_65F='weapons.missiles.AGM_65F' ENUMS.Storage.weapons.shells._50Browning_AP_M2='weapons.shells.50Browning_AP_M2' ENUMS.Storage.weapons.shells._5_56x45='weapons.shells.5_56x45' ENUMS.Storage.weapons.adapters.MatraF1Rocket='weapons.adapters.Matra-F1-Rocket' ENUMS.Storage.weapons.missiles.LS_6_500='weapons.missiles.LS_6_500' ENUMS.Storage.weapons.missiles.SA9M311='weapons.missiles.SA9M311' ENUMS.Storage.weapons.shells.AK100_100='weapons.shells.AK100_100' ENUMS.Storage.weapons.bombs.BLG66_BELOUGA='weapons.bombs.BLG66_BELOUGA' ENUMS.Storage.weapons.bombs.BIN_200='weapons.bombs.BIN_200' ENUMS.Storage.weapons.containers.pl5eii='weapons.containers.pl5eii' ENUMS.Storage.weapons.gunmounts.CH47_STBD_M240H='weapons.gunmounts.{CH47_STBD_M240H}' ENUMS.Storage.weapons.droptanks.Spitfire_slipper_tank='weapons.droptanks.Spitfire_slipper_tank' ENUMS.Storage.weapons.missiles.HOT3_MBDA='weapons.missiles.HOT3_MBDA' ENUMS.Storage.weapons.shells._53UOR281U='weapons.shells.53-UOR-281U' ENUMS.Storage.weapons.bombs.BDU_50LGB='weapons.bombs.BDU_50LGB' ENUMS.Storage.weapons.gunmounts.SHKAS_GUN='weapons.gunmounts.SHKAS_GUN' ENUMS.Storage.weapons.shells.British303_W_Mk1z='weapons.shells.British303_W_Mk1z' ENUMS.Storage.weapons.adapters.J11A_twinpylon_r='weapons.adapters.J-11A_twinpylon_r' ENUMS.Storage.weapons.missiles.P_700='weapons.missiles.P_700' ENUMS.Storage.weapons.missiles.SA5V28='weapons.missiles.SA5V28' ENUMS.Storage.weapons.missiles.MIM_72G='weapons.missiles.MIM_72G' ENUMS.Storage.weapons.adapters.CLB_4='weapons.adapters.CLB_4' ENUMS.Storage.weapons.droptanks.PTB_200_F86F35='weapons.droptanks.PTB_200_F86F35' ENUMS.Storage.weapons.shells.Mauser7_92x57_SmK_Lspurgelb="weapons.shells.Mauser7.92x57_S.m.K._L'spur(gelb)" ENUMS.Storage.weapons.droptanks.PTB_1500_MIG29A='weapons.droptanks.PTB_1500_MIG29A' ENUMS.Storage.weapons.bombs.GBU_31='weapons.bombs.GBU_31' ENUMS.Storage.weapons.missiles.Kh66_Grom='weapons.missiles.Kh-66_Grom' ENUMS.Storage.weapons.containers.HB_ALE_40_15_90='weapons.containers.HB_ALE_40_15_90' ENUMS.Storage.weapons.containers.U22='weapons.containers.U22' ENUMS.Storage.weapons.adapters.Spitfire_pilon1='weapons.adapters.Spitfire_pilon1' ENUMS.Storage.weapons.bombs.OH58D_Violet_Smoke_Grenade='weapons.bombs.OH58D_Violet_Smoke_Grenade' ENUMS.Storage.weapons.adapters.adapter_gdj_yj83k='weapons.adapters.adapter_gdj_yj83k' ENUMS.Storage.weapons.adapters.M299='weapons.adapters.M299' ENUMS.Storage.weapons.adapters.HB_ORD_MER='weapons.adapters.HB_ORD_MER' ENUMS.Storage.weapons.shells.Mauser7_92x57_SmKH='weapons.shells.Mauser7.92x57_S.m.K.H.' ENUMS.Storage.weapons.gunmounts.HMP400='weapons.gunmounts.HMP400' ENUMS.Storage.weapons.containers.F15E_AXQ14_DATALINK='weapons.containers.F-15E_AXQ-14_DATALINK' ENUMS.Storage.weapons.adapters._9m114_pylon2='weapons.adapters.9m114_pylon2' ENUMS.Storage.weapons.bombs.BEER_BOMB='weapons.bombs.BEER_BOMB' ENUMS.Storage.weapons.nurs.C_8OFP2='weapons.nurs.C_8OFP2' ENUMS.Storage.weapons.nurs.SNEB_TYPE254_F1B_RED='weapons.nurs.SNEB_TYPE254_F1B_RED' ENUMS.Storage.weapons.nurs.C_8OM='weapons.nurs.C_8OM' ENUMS.Storage.weapons.shells.M61_20_HE_INVIS='weapons.shells.M61_20_HE_INVIS' ENUMS.Storage.weapons.droptanks.HB_F4E_EXT_WingTank_R='weapons.droptanks.HB_F-4E_EXT_WingTank_R' ENUMS.Storage.weapons.missiles.CATM_65K='weapons.missiles.CATM_65K' ENUMS.Storage.weapons.nurs.FFARM156WP='weapons.nurs.FFAR M156 WP' ENUMS.Storage.weapons.bombs.MK_82SNAKEYE='weapons.bombs.MK_82SNAKEYE' ENUMS.Storage.weapons.shells.Rh202_20_AP='weapons.shells.Rh202_20_AP' ENUMS.Storage.weapons.adapters.M261='weapons.adapters.M261' ENUMS.Storage.weapons.bombs.KAB_1500T='weapons.bombs.KAB_1500T' ENUMS.Storage.weapons.shells.M339_120mm_HEAT_MP_T='weapons.shells.M339_120mm_HEAT_MP_T' ENUMS.Storage.weapons.shells.UOF_17_100HE='weapons.shells.UOF_17_100HE' ENUMS.Storage.weapons.nurs.SNEB_TYPE259E_F1B='weapons.nurs.SNEB_TYPE259E_F1B' ENUMS.Storage.weapons.shells._5_45x39_NOtr='weapons.shells.5_45x39_NOtr' ENUMS.Storage.weapons.gunmounts.CH47_AFT_M3M='weapons.gunmounts.{CH47_AFT_M3M}' ENUMS.Storage.weapons.containers.Fantasm='weapons.containers.Fantasm' ENUMS.Storage.weapons.missiles.AGM_12C_ED='weapons.missiles.AGM_12C_ED' ENUMS.Storage.weapons.droptanks.PTB760_MIG19='weapons.droptanks.PTB760_MIG19' ENUMS.Storage.weapons.missiles.SA9M330='weapons.missiles.SA9M330' ENUMS.Storage.weapons.missiles.BK90_MJ1_MJ2='weapons.missiles.BK90_MJ1_MJ2' ENUMS.Storage.weapons.containers.HB_F14_EXT_AN_APQ167='weapons.containers.HB_F14_EXT_AN_APQ-167' ENUMS.Storage.weapons.containers.MB339_TravelPod='weapons.containers.MB339_TravelPod' ENUMS.Storage.weapons.gunmounts.OH58D_M3P_L200='weapons.gunmounts.OH58D_M3P_L200' ENUMS.Storage.weapons.adapters.B1B_10store_Conventional_Bomb_Module='weapons.adapters.B-1B_10-store_Conventional_Bomb_Module' ENUMS.Storage.weapons.shells.GSH23_23_HE_T='weapons.shells.GSH23_23_HE_T' ENUMS.Storage.weapons.containers.TANGAZH='weapons.containers.TANGAZH' 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.gunmounts.CH47_PORT_M60D='weapons.gunmounts.{CH47_PORT_M60D}' ENUMS.Storage.weapons.missiles.M48='weapons.missiles.M48' ENUMS.Storage.weapons.shells.MAUZER30_30='weapons.shells.MAUZER30_30' ENUMS.Storage.weapons.adapters.tu22m3mbd='weapons.adapters.tu-22m3-mbd' ENUMS.Storage.weapons.gunmounts.DEFA554='weapons.gunmounts.DEFA 554' ENUMS.Storage.weapons.droptanks.F4U1D_Drop_Tank_Mk6='weapons.droptanks.F4U-1D_Drop_Tank_Mk6' ENUMS.Storage.weapons.containers.US_M10_SMOKE_TANK_YELLOW='weapons.containers.{US_M10_SMOKE_TANK_YELLOW}' ENUMS.Storage.weapons.bombs.HEBOMB='weapons.bombs.HEBOMB' ENUMS.Storage.weapons.nurs.Rkt_901_HE='weapons.nurs.Rkt_90-1_HE' ENUMS.Storage.weapons.adapters.HB_F14_EXT_BRU42='weapons.adapters.HB_F14_EXT_BRU42' ENUMS.Storage.weapons.missiles.YJ62='weapons.missiles.YJ-62' ENUMS.Storage.weapons.shells.KDA_35_HE='weapons.shells.KDA_35_HE' ENUMS.Storage.weapons.shells._2A7_23_AP='weapons.shells.2A7_23_AP' ENUMS.Storage.weapons.adapters.SA342_LAU_HOT3_1x='weapons.adapters.SA342_LAU_HOT3_1x' ENUMS.Storage.weapons.missiles.P_9M117='weapons.missiles.P_9M117' ENUMS.Storage.weapons.adapters.MBD267U='weapons.adapters.MBD-2-67U' ENUMS.Storage.weapons.shells.AK630_30_HE='weapons.shells.AK630_30_HE' ENUMS.Storage.weapons.bombs.British_GP_500LB_Bomb_Mk5='weapons.bombs.British_GP_500LB_Bomb_Mk5' ENUMS.Storage.weapons.bombs.LUU_2AB='weapons.bombs.LUU_2AB' ENUMS.Storage.weapons.missiles.BK90_MJ2='weapons.missiles.BK90_MJ2' ENUMS.Storage.weapons.shells.British303_B_Mk4z='weapons.shells.British303_B_Mk4z' ENUMS.Storage.weapons.adapters.BRU_41A='weapons.adapters.BRU_41A' ENUMS.Storage.weapons.bombs.BDU_45='weapons.bombs.BDU_45' ENUMS.Storage.weapons.adapters.b20='weapons.adapters.b-20' ENUMS.Storage.weapons.missiles.Rapier='weapons.missiles.Rapier' ENUMS.Storage.weapons.missiles.P_24R='weapons.missiles.P_24R' ENUMS.Storage.weapons.missiles.AGM_84S='weapons.missiles.AGM_84S' ENUMS.Storage.weapons.adapters.C25PU='weapons.adapters.C-25PU' ENUMS.Storage.weapons.containers.BOZ100='weapons.containers.BOZ-100' ENUMS.Storage.weapons.missiles.AGM_65E='weapons.missiles.AGM_65E' ENUMS.Storage.weapons.adapters._9M120_pylon2='weapons.adapters.9M120_pylon2' ENUMS.Storage.weapons.shells.YakB_12_7_T='weapons.shells.YakB_12_7_T' ENUMS.Storage.weapons.containers.IRDeflector='weapons.containers.IRDeflector' ENUMS.Storage.weapons.missiles.AIM9P='weapons.missiles.AIM-9P' ENUMS.Storage.weapons.missiles.SA5B27='weapons.missiles.SA5B27' ENUMS.Storage.weapons.bombs.SC_500_L2='weapons.bombs.SC_500_L2' ENUMS.Storage.weapons.containers.HB_F14_EXT_ECA='weapons.containers.HB_F14_EXT_ECA' ENUMS.Storage.weapons.bombs.SAMP400HD='weapons.bombs.SAMP400HD' ENUMS.Storage.weapons.adapters.ARAKM70B='weapons.adapters.ARAKM70B' ENUMS.Storage.weapons.adapters.M260='weapons.adapters.M260' ENUMS.Storage.weapons.shells.Mauser7_92x57_PmK='weapons.shells.Mauser7.92x57_P.m.K.' ENUMS.Storage.weapons.missiles.RB75T='weapons.missiles.RB75T' ENUMS.Storage.weapons.missiles.YJ82='weapons.missiles.YJ-82' ENUMS.Storage.weapons.bombs.FAB500M54TU='weapons.bombs.FAB-500M54TU' ENUMS.Storage.weapons.bombs.OH58D_White_Smoke_Grenade='weapons.bombs.OH58D_White_Smoke_Grenade' ENUMS.Storage.weapons.missiles.X_29TE='weapons.missiles.X_29TE' 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.adapters.lau105='weapons.adapters.lau-105' ENUMS.Storage.weapons.containers.US_M10_SMOKE_TANK_WHITE='weapons.containers.{US_M10_SMOKE_TANK_WHITE}' ENUMS.Storage.weapons.bombs.Mk_82='weapons.bombs.Mk_82' ENUMS.Storage.weapons.adapters.BRU42_LS_SUU25='weapons.adapters.BRU-42_LS_(SUU-25)' ENUMS.Storage.weapons.missiles.Aster_30_Blk_1='weapons.missiles.Aster 30 Blk 1' ENUMS.Storage.weapons.missiles.Aster_30_Blk_1NT='weapons.missiles.Aster 30 Blk 1NT' ENUMS.Storage.weapons.missiles.Aster_30_Blk_2='weapons.missiles.Aster 30 Blk 2' ENUMS.Storage.weapons.missiles.SA9M83M='weapons.missiles.SA9M83M' ENUMS.Storage.weapons.gunmounts.C130_M4_Rifle='weapons.gunmounts.C130_M4_Rifle' ENUMS.Storage.weapons.gunmounts.C130_M18_Sidearm_='weapons.gunmounts.{C130-M18-Sidearm}' ENUMS.Storage.weapons.gunmounts.C130_Cargo_Bay_M4='weapons.gunmounts.{C130-Cargo-Bay-M4}' ENUMS.Storage.weapons.gunmounts.C130_M18_Sidearm='weapons.gunmounts.C130_M18_Sidearm' ENUMS.Storage.weapons.droptanks.C130J_Ext_Tank_R='weapons.droptanks.C130J_Ext_Tank_R' ENUMS.Storage.weapons.droptanks.C130J_Ext_Tank_L='weapons.droptanks.C130J_Ext_Tank_L' ENUMS.Storage.weapons.missiles.SAHQ2='weapons.missiles.SAHQ2' ENUMS.Storage.weapons.missiles.Strela_2='weapons.missiles.Strela-2' ENUMS.Storage.weapons.missiles.Strela_2M='weapons.missiles.Strela-2M' ENUMS.Storage.weapons.missiles.Strela_3='weapons.missiles.Strela-3' ENUMS.Storage.weapons.missiles.SA9M83='weapons.missiles.SA9M83' ENUMS.Storage.weapons.missiles.SAV601P='weapons.missiles.SAV601P' ENUMS.Storage.weapons.missiles.SA2V759='weapons.missiles.SA2V759' ENUMS.Storage.weapons.missiles.SA9M317='weapons.missiles.SA9M317' ENUMS.Storage.weapons.missiles.SA9M82M='weapons.missiles.SA9M82M' ENUMS.Storage.weapons.missiles.SA9M82='weapons.missiles.SA9M82' ENUMS.Storage.weapons.missiles.Igla_S='weapons.missiles.Igla_S' ENUMS.Storage.weapons.gunmounts.AKAN_NO_TRC='weapons.gunmounts.{AKAN_NO_TRC}' ENUMS.Storage.weapons.gunmounts.AKAN='weapons.gunmounts.{AKAN}' ENUMS.Storage.weapons.shells.M882_9x19='weapons.shells.9x19_m882' ENUMS.Storage.weapons.droptanks.fuel_tank_370gal="weapons.droptanks.fuel_tank_370gal" ENUMS.Storage.weapons.droptanks.fuel_tank_300gal="weapons.droptanks.fuel_tank_300gal" ENUMS.Storage.weapons.adapters.HB_F_4E_ORD_LAU_77='weapons.adapters.HB_F-4E_ORD_LAU_77' ENUMS.Storage.weapons.adapters.hb_a_6e_lau7_adu299='weapons.adapters.hb_a-6e_lau7_adu299' ENUMS.Storage.weapons.adapters.CHAP_AIM92LN='weapons.adapters.CHAP_AIM92LN' ENUMS.Storage.weapons.adapters.CHAP_HOT3LN='weapons.adapters.CHAP_HOT3LN' ENUMS.Storage.weapons.adapters.CHAP_FZ225='weapons.adapters.CHAP_FZ225' ENUMS.Storage.weapons.bombs.AH6_SMOKE_BLUE='weapons.bombs.AH6_SMOKE_BLUE' ENUMS.Storage.weapons.bombs.AH6_SMOKE_GREEN='weapons.bombs.AH6_SMOKE_GREEN' ENUMS.Storage.weapons.bombs.AH6_SMOKE_RED='weapons.bombs.AH6_SMOKE_RED' ENUMS.Storage.weapons.bombs.AH6_SMOKE_YELLOW='weapons.bombs.AH6_SMOKE_YELLOW' ENUMS.Storage.weapons.bombs.AO_25SL='weapons.bombs.AO_25SL' ENUMS.Storage.weapons.missiles.HB_AGM_78='weapons.missiles.HB_AGM_78' ENUMS.Storage.weapons.missiles.V_1='weapons.missiles.V-1' ENUMS.Storage.weapons.shells.Oerlikon_20mm_HE='weapons.shells.Oerlikon_20mm_HE' ENUMS.Storage.weapons.shells.RM_15cm_HE='weapons.shells.RM_15cm_HE' ENUMS.Storage.weapons.shells.HE_T_MkII_40mm='weapons.shells.HE_T_MkII_40mm' ENUMS.Storage.weapons.shells.APCBC='weapons.shells.APCBC' ENUMS.Storage.weapons.shells.Sprgr_43_L71='weapons.shells.Sprgr_43_L71' ENUMS.Storage.weapons.shells.Mk_20_HE_shell='weapons.shells.Mk_20_HE_shell' ENUMS.Storage.weapons.shells.Pzgr_39_42='weapons.shells.Pzgr_39/42' ENUMS.Storage.weapons.shells.M1_37mm_37AP_T='weapons.shells.M1_37mm_37AP-T' ENUMS.Storage.weapons.shells.Sprgr_38='weapons.shells.Sprgr_38' ENUMS.Storage.weapons.shells.Sprgr_39='weapons.shells.Sprgr_39' ENUMS.Storage.weapons.shells.Pzgr_39_43='weapons.shells.Pzgr_39/43' ENUMS.Storage.weapons.shells.Pzgr_39_5cm='weapons.shells.Pzgr_39_5cm' ENUMS.Storage.weapons.shells.AP_2A20_115mm='weapons.shells.2A20_115mm_AP' ENUMS.Storage.weapons.shells.AP_T_MkI_40mm='weapons.shells.AP_T_MkI_40mm' ENUMS.Storage.weapons.shells.AP_37x263='weapons.shells.37x263_AP' ENUMS.Storage.weapons.shells.AP_20x138B='weapons.shells.20x138B_AP' ENUMS.Storage.weapons.shells.Besa7_92x57T='weapons.shells.Besa7_92x57T' ENUMS.Storage.weapons.shells.Sprgr_34_L70='weapons.shells.Sprgr_34_L70' ENUMS.Storage.weapons.shells.QF94_AA_HE='weapons.shells.QF94_AA_HE' ENUMS.Storage.weapons.shells.AH_6762x51mm_M62='weapons.shells.AH-6 7.62x51mm M62' ENUMS.Storage.weapons.shells.AH_6762x51mm_M80='weapons.shells.AH-6 7.62x51mm M80' ENUMS.Storage.weapons.shells.AH_6_762x51mm_M61='weapons.shells.AH-6 7.62x51mm M61' ENUMS.Storage.weapons.shells.leFH18_105HE='weapons.shells.leFH18_105HE' ENUMS.Storage.weapons.shells.M63_37HE='weapons.shells.M63_37HE' ENUMS.Storage.weapons.shells.QF95_206R_fixed='weapons.shells.QF95_206R_fixed' ENUMS.Storage.weapons.shells.UBR_365_85AP='weapons.shells.UBR_365_85AP' ENUMS.Storage.weapons.shells.M101='weapons.shells.M101' ENUMS.Storage.weapons.shells.HE_M1_Shell='weapons.shells.HE_M1_Shell' ENUMS.Storage.weapons.shells.UO_365K_85HE='weapons.shells.UO_365K_85HE' ENUMS.Storage.weapons.shells.Flak41_Sprgr_39='weapons.shells.Flak41_Sprgr_39' ENUMS.Storage.weapons.shells.M1_37mm_HE_T='weapons.shells.M1_37mm_HE-T' ENUMS.Storage.weapons.shells.QF17_HE='weapons.shells.QF17_HE' ENUMS.Storage.weapons.shells.Pzgr_39='weapons.shells.Pzgr_39' ENUMS.Storage.weapons.shells.Besa7_92x57='weapons.shells.Besa7_92x57' ENUMS.Storage.weapons.shells.I_Gr_33='weapons.shells.I_Gr_33' ENUMS.Storage.weapons.shells.M62_APC='weapons.shells.M62_APC' ENUMS.Storage.weapons.shells.Mk_12_HE_shell='weapons.shells.Mk_12_HE_shell' ENUMS.Storage.weapons.shells.M51_37AP='weapons.shells.M51_37AP' ENUMS.Storage.weapons.shells.M42A1_HE='weapons.shells.M42A1_HE' ENUMS.Storage.weapons.shells.HE_20x138B='weapons.shells.20x138B_HE' ENUMS.Storage.weapons.shells.HE_37x263='weapons.shells.37x263_HE' ENUMS.Storage.weapons.shells.HE_2A20_115mm='weapons.shells.2A20_115mm_HE' ENUMS.Storage.weapons.shells.AP_20x99R='weapons.shells.20x99R_AP' ENUMS.Storage.weapons.shells.IranFAC_DShK_API_T='weapons.shells.IranFAC_DShK_API_T' ENUMS.Storage.weapons.shells.IranFAC_DShK_API='weapons.shells.IranFAC_DShK_API' ENUMS.Storage.weapons.shells.HE_T_20x99R='weapons.shells.20x99R_HE_T' ENUMS.Storage.weapons.gunmounts.B17_TailTurret_M2_L='weapons.gunmounts.B17_TailTurret_M2_L' ENUMS.Storage.weapons.gunmounts.AH6_M134L='weapons.gunmounts.{AH6_M134L}' ENUMS.Storage.weapons.gunmounts.B17_Left_Nose_M2='weapons.gunmounts.B17_Left_Nose_M2' ENUMS.Storage.weapons.gunmounts.Ju88_Turret_Top_Right_MG_81='weapons.gunmounts.Ju88_Turret_Top_Right_MG_81' ENUMS.Storage.weapons.gunmounts.Ju88_Turret_Bottom_MG_81_L='weapons.gunmounts.Ju88_Turret_Bottom_MG_81_L' ENUMS.Storage.weapons.gunmounts.B17_ChinTurret_M2_R='weapons.gunmounts.B17_ChinTurret_M2_R' ENUMS.Storage.weapons.gunmounts.B17_Waist_Right_M2='weapons.gunmounts.B17_Waist_Right_M2' ENUMS.Storage.weapons.gunmounts.AH_6_Door_Gun='weapons.gunmounts.AH-6_Door_Gun' ENUMS.Storage.weapons.gunmounts.B17_BallTurret_M2_L='weapons.gunmounts.B17_BallTurret_M2_L' ENUMS.Storage.weapons.gunmounts.B17_BallTurret_M2_R='weapons.gunmounts.B17_BallTurret_M2_R' ENUMS.Storage.weapons.gunmounts.B17_TopTurret_M2_R='weapons.gunmounts.B17_TopTurret_M2_R' ENUMS.Storage.weapons.gunmounts.B17_Right_Nose_M2='weapons.gunmounts.B17_Right_Nose_M2' ENUMS.Storage.weapons.gunmounts.Ju88_Turret_Bottom_MG_81_R='weapons.gunmounts.Ju88_Turret_Bottom_MG_81_R' ENUMS.Storage.weapons.gunmounts.B17_TopTurret_M2_L='weapons.gunmounts.B17_TopTurret_M2_L' ENUMS.Storage.weapons.gunmounts.B17_Waist_Left_M2='weapons.gunmounts.B17_Waist_Left_M2' ENUMS.Storage.weapons.gunmounts.Ju88_Turret_ahead_MG_81='weapons.gunmounts.Ju88_Turret_ahead_MG_81' ENUMS.Storage.weapons.gunmounts.AH6_M134R='weapons.gunmounts.{AH6_M134R}' ENUMS.Storage.weapons.gunmounts.Ju88_Turret_Top_Left_MG_81='weapons.gunmounts.Ju88_Turret_Top_Left_MG_81' ENUMS.Storage.weapons.gunmounts.B17_TailTurret_M2_R='weapons.gunmounts.B17_TailTurret_M2_R' ENUMS.Storage.weapons.gunmounts.AKAN_NO_TRC='weapons.gunmounts.AKAN_NO_TRC' ENUMS.Storage.weapons.gunmounts.AKAN='weapons.gunmounts.AKAN' ENUMS.Storage.weapons.gunmounts.B17_ChinTurret_M2_L='weapons.gunmounts.B17_ChinTurret_M2_L' ENUMS.Storage.weapons.gunmounts.AKAN='weapons.gunmounts.AKAN' ENUMS.Storage.weapons.gunmounts.AKAN_NO_TRC='weapons.gunmounts.AKAN_NO_TRC' ENUMS.Storage.weapons.gunmounts.AH_6_Door='weapons.gunmounts.{AH-6_Door}' ENUMS.Storage.weapons.gunmounts.AH_6_FN_HMP400='weapons.gunmounts.{AH-6_FN_HMP400}' ENUMS.Storage.weapons.gunmounts.AH_6_M134L='weapons.gunmounts.AH-6_M134L' ENUMS.Storage.weapons.gunmounts.AH_6_M134R='weapons.gunmounts.AH-6_M134R' ENUMS.Storage.weapons.gunmounts.AH_6_HMP400='weapons.gunmounts.AH-6_HMP400' ENUMS.Storage.weapons.gunmounts.AKAN_NO_TRC='weapons.gunmounts.{AKAN_NO_TRC}' ENUMS.Storage.weapons.gunmounts.AKAN='weapons.gunmounts.{AKAN}' ENUMS.Storage.weapons.gunmounts.CHAP_HMP400LC='weapons.gunmounts.{CHAP_HMP400LC}' ENUMS.Storage.weapons.gunmounts.HMP400LC='weapons.gunmounts.HMP400LC' ENUMS.Storage.weapons.gunmounts.SHVAK_GUN='weapons.gunmounts.SHVAK_GUN' ENUMS.Storage.weapons.droptanks.PTB_800='weapons.droptanks.PTB-800' ENUMS.Storage.weapons.droptanks.PTB_275='weapons.droptanks.PTB-275' ENUMS.Storage.weapons.droptanks.HB_A6E_AERO1D_EMPTY='weapons.droptanks.HB_A6E_AERO1D_EMPTY' ENUMS.Storage.weapons.droptanks.Drop_tank_75gal='weapons.droptanks.Drop tank 75gal' ENUMS.Storage.weapons.droptanks.S_3_PTB='weapons.droptanks.S-3-PTB' ENUMS.Storage.weapons.droptanks.PTB_3000='weapons.droptanks.PTB-3000' ENUMS.Storage.weapons.droptanks.HB_A6E_D704='weapons.droptanks.HB_A6E_D704' ENUMS.Storage.weapons.droptanks.PTB_1150_29='weapons.droptanks.PTB-1150-29' ENUMS.Storage.weapons.droptanks.f_18c_ptb='weapons.droptanks.f-18c-ptb' ENUMS.Storage.weapons.droptanks.F4_BAK_L='weapons.droptanks.F4-BAK-L' ENUMS.Storage.weapons.droptanks.HB_A6E_AERO1D='weapons.droptanks.HB_A6E_AERO1D' ENUMS.Storage.weapons.droptanks.IAFS_ComboPak_100='weapons.droptanks.{IAFS_ComboPak_100}' ENUMS.Storage.weapons.droptanks.PTB_2000='weapons.droptanks.PTB-2000' ENUMS.Storage.weapons.droptanks.M2000_PTB='weapons.droptanks.M2000-PTB' ENUMS.Storage.weapons.droptanks.PTB_150='weapons.droptanks.PTB-150' ENUMS.Storage.weapons.droptanks.PTB_1150='weapons.droptanks.PTB-1150' ENUMS.Storage.weapons.droptanks.fuel_tank_300gal='weapons.droptanks.fuel_tank_300gal' ENUMS.Storage.weapons.droptanks.MIG_25_PTB='weapons.droptanks.MIG-25-PTB' ENUMS.Storage.weapons.droptanks.PTB_1500='weapons.droptanks.PTB-1500' ENUMS.Storage.weapons.droptanks.MIG_23_PTB='weapons.droptanks.MIG-23-PTB' ENUMS.Storage.weapons.droptanks.FT600='weapons.droptanks.FT600' ENUMS.Storage.weapons.droptanks.F15_PTB='weapons.droptanks.F15-PTB' ENUMS.Storage.weapons.droptanks.ah6_auxtank='weapons.droptanks.ah6_auxtank' ENUMS.Storage.weapons.droptanks.T_PTB='weapons.droptanks.T-PTB' ENUMS.Storage.weapons.droptanks.fuel_tank_370gal='weapons.droptanks.fuel_tank_370gal' ENUMS.Storage.weapons.droptanks.F4_BAK_C='weapons.droptanks.F4-BAK-C' ENUMS.Storage.weapons.droptanks.F16_PTB_N2='weapons.droptanks.F-16-PTB-N2' ENUMS.Storage.weapons.droptanks.PTB_800='weapons.droptanks.PTB-800' ENUMS.Storage.weapons.droptanks.PTB_275='weapons.droptanks.PTB-275' ENUMS.Storage.weapons.droptanks.T_PTB='weapons.droptanks.T-PTB' ENUMS.Storage.weapons.droptanks.F16_PTB_N2='weapons.droptanks.F-16-PTB-N2' ENUMS.Storage.weapons.droptanks.F4_BAK_C='weapons.droptanks.F4-BAK-C' ENUMS.Storage.weapons.droptanks.PTB_1150_29='weapons.droptanks.PTB-1150-29' ENUMS.Storage.weapons.droptanks.PTB_1150='weapons.droptanks.PTB-1150' ENUMS.Storage.weapons.droptanks.fuel_tank_300gal='weapons.droptanks.fuel_tank_300gal' ENUMS.Storage.weapons.droptanks.MIG_25_PTB='weapons.droptanks.MIG-25-PTB' ENUMS.Storage.weapons.droptanks.PTB_1500='weapons.droptanks.PTB-1500' ENUMS.Storage.weapons.droptanks.FT600='weapons.droptanks.FT600' ENUMS.Storage.weapons.droptanks.Drop_tank_75gal='weapons.droptanks.Drop tank 75gal' ENUMS.Storage.weapons.droptanks.F15_PTB='weapons.droptanks.F15-PTB' ENUMS.Storage.weapons.droptanks.M2000_PTB='weapons.droptanks.M2000-PTB' ENUMS.Storage.weapons.droptanks.PTB_150='weapons.droptanks.PTB-150' ENUMS.Storage.weapons.droptanks.F4_BAK_L='weapons.droptanks.F4-BAK-L' ENUMS.Storage.weapons.droptanks.PTB_3000='weapons.droptanks.PTB-3000' ENUMS.Storage.weapons.droptanks.PTB_2000='weapons.droptanks.PTB-2000' ENUMS.Storage.weapons.droptanks.S_3_PTB='weapons.droptanks.S-3-PTB' ENUMS.Storage.weapons.droptanks.fuel_tank_370gal='weapons.droptanks.fuel_tank_370gal' ENUMS.Storage.weapons.droptanks.MIG_23_PTB='weapons.droptanks.MIG-23-PTB' ENUMS.Storage.weapons.droptanks.f_18c_ptb='weapons.droptanks.f-18c-ptb' ENUMS.Storage.weapons.droptanks.CHAP_TigerUHT_fueltank='weapons.droptanks.CHAP_TigerUHT_fueltank' ENUMS.Storage.weapons.containers.FN_HMP400_100='weapons.containers.{FN_HMP400_100}' ENUMS.Storage.weapons.containers.AN_M3='weapons.containers.{AN-M3}' ENUMS.Storage.weapons.containers.OH58D_M3P_L500='weapons.containers.OH58D_M3P_L500' ENUMS.Storage.weapons.containers.GIAT_M621_HE='weapons.containers.{GIAT_M621_HE}' ENUMS.Storage.weapons.containers.KORD_12_7_MI24_L='weapons.containers.KORD_12_7_MI24_L' ENUMS.Storage.weapons.containers.CH47_STBD_M240H='weapons.containers.{CH47_STBD_M240H}' ENUMS.Storage.weapons.containers.CH47_AFT_M60D='weapons.containers.{CH47_AFT_M60D}' ENUMS.Storage.weapons.containers.UPK_23_250_MiG_21='weapons.containers.{UPK-23-250 MiG-21}' ENUMS.Storage.weapons.containers.CH47_STBD_M134D='weapons.containers.{CH47_STBD_M134D}' ENUMS.Storage.weapons.containers.GAU_12_Equalizer='weapons.containers.{GAU_12_Equalizer}' ENUMS.Storage.weapons.containers.PKT_7_62='weapons.containers.PKT_7_62' ENUMS.Storage.weapons.containers.CH47_AFT_M240H='weapons.containers.{CH47_AFT_M240H}' ENUMS.Storage.weapons.containers.GIAT_M621_SAPHEI='weapons.containers.{GIAT_M621_SAPHEI}' ENUMS.Storage.weapons.containers.ADEN_GUNPOD='weapons.containers.{ADEN_GUNPOD}' ENUMS.Storage.weapons.containers.GAU_12_Equalizer_HE='weapons.containers.{GAU_12_Equalizer_HE}' ENUMS.Storage.weapons.containers.M60_SIDE_L='weapons.containers.M60_SIDE_L' ENUMS.Storage.weapons.containers.KORD_12_7_MI24_R='weapons.containers.KORD_12_7_MI24_R' ENUMS.Storage.weapons.containers.GIAT_M621_APHE='weapons.containers.{GIAT_M621_APHE}' ENUMS.Storage.weapons.containers.MB339_ANM3_L='weapons.containers.{MB339_ANM3_L}' ENUMS.Storage.weapons.containers.FN_HMP400='weapons.containers.{FN_HMP400}' ENUMS.Storage.weapons.containers.AH_6_Door='weapons.containers.{AH-6_Door}' ENUMS.Storage.weapons.containers.MB339_DEFA553_L='weapons.containers.{MB339_DEFA553_L}' ENUMS.Storage.weapons.containers.MB339_ANM3_R='weapons.containers.{MB339_ANM3_R}' ENUMS.Storage.weapons.containers.PK_3='weapons.containers.{PK-3}' ENUMS.Storage.weapons.containers.GUV_VOG='weapons.containers.GUV_VOG' ENUMS.Storage.weapons.containers.SA342_M134_SIDE_R='weapons.containers.{SA342_M134_SIDE_R}' ENUMS.Storage.weapons.containers.OH58D_M3P_L100='weapons.containers.OH58D_M3P_L100' ENUMS.Storage.weapons.containers.MXU_648='weapons.containers.MXU-648' ENUMS.Storage.weapons.containers.OH58D_M3P_L400='weapons.containers.OH58D_M3P_L400' ENUMS.Storage.weapons.containers.FN_HMP400_200='weapons.containers.{FN_HMP400_200}' ENUMS.Storage.weapons.containers.GIAT_M621_HE='weapons.containers.{GIAT_M621_HE}' ENUMS.Storage.weapons.containers.M134_L='weapons.containers.M134_L' ENUMS.Storage.weapons.containers.OH58D_M3P_L200='weapons.containers.OH58D_M3P_L200' ENUMS.Storage.weapons.containers.GIAT_M621_AP='weapons.containers.{GIAT_M621_AP}' ENUMS.Storage.weapons.containers.CH47_STBD_M134D='weapons.containers.{CH47_STBD_M134D}' ENUMS.Storage.weapons.containers.PKT_7_62='weapons.containers.PKT_7_62' ENUMS.Storage.weapons.containers.OH58D_M3P_L300='weapons.containers.OH58D_M3P_L300' ENUMS.Storage.weapons.containers.GIAT_M621_SAPHEI='weapons.containers.{GIAT_M621_SAPHEI}' ENUMS.Storage.weapons.containers.M60_SIDE_L='weapons.containers.M60_SIDE_L' ENUMS.Storage.weapons.containers.CH47_PORT_M134D='weapons.containers.{CH47_PORT_M134D}' ENUMS.Storage.weapons.containers.CH47_PORT_M60D='weapons.containers.{CH47_PORT_M60D}' ENUMS.Storage.weapons.containers.ADEN_GUNPOD='weapons.containers.{ADEN_GUNPOD}' ENUMS.Storage.weapons.containers.C_101_DEFA553='weapons.containers.{C-101-DEFA553}' ENUMS.Storage.weapons.containers.MB339_ANM3_R='weapons.containers.{MB339_ANM3_R}' ENUMS.Storage.weapons.containers.CH47_AFT_M60D='weapons.containers.{CH47_AFT_M60D}' ENUMS.Storage.weapons.containers.KORD_12_7='weapons.containers.KORD_12_7' ENUMS.Storage.weapons.containers.GIAT_M621_HEAP='weapons.containers.{GIAT_M621_HEAP}' ENUMS.Storage.weapons.containers.CH47_AFT_M3M='weapons.containers.{CH47_AFT_M3M}' ENUMS.Storage.weapons.containers.GUV_YakB_GSHP='weapons.containers.GUV_YakB_GSHP' ENUMS.Storage.weapons.containers.R_73U='weapons.containers.R-73U' ENUMS.Storage.weapons.containers.GUV_VOG='weapons.containers.GUV_VOG' ENUMS.Storage.weapons.containers.KORD_12_7_MI24_L='weapons.containers.KORD_12_7_MI24_L' ENUMS.Storage.weapons.containers.OH58D_M3P_L100='weapons.containers.OH58D_M3P_L100' ENUMS.Storage.weapons.containers.OH58D_M3P_L400='weapons.containers.OH58D_M3P_L400' ENUMS.Storage.weapons.containers.CH47_STBD_M240H='weapons.containers.{CH47_STBD_M240H}' ENUMS.Storage.weapons.containers.CH47_PORT_M240H='weapons.containers.{CH47_PORT_M240H}' ENUMS.Storage.weapons.containers.CC420_GUN_POD='weapons.containers.{CC420_GUN_POD}' ENUMS.Storage.weapons.containers.FN_HMP400_100='weapons.containers.{FN_HMP400_100}' ENUMS.Storage.weapons.containers.MB339_ANM3_L='weapons.containers.{MB339_ANM3_L}' ENUMS.Storage.weapons.containers.MB339_DEFA553_R='weapons.containers.{MB339_DEFA553_R}' ENUMS.Storage.weapons.containers.GAU_12_Equalizer_HE='weapons.containers.{GAU_12_Equalizer_HE}' ENUMS.Storage.weapons.containers.OH58D_M3P_L500='weapons.containers.OH58D_M3P_L500' ENUMS.Storage.weapons.containers.SUU_23_POD='weapons.containers.{SUU_23_POD}' ENUMS.Storage.weapons.containers.PK_3='weapons.containers.{PK-3}' ENUMS.Storage.weapons.containers.AKAN='weapons.containers.{AKAN}' ENUMS.Storage.weapons.containers.CH47_STBD_M60D='weapons.containers.{CH47_STBD_M60D}' ENUMS.Storage.weapons.containers.SA342_M134_SIDE_R='weapons.containers.{SA342_M134_SIDE_R}' ENUMS.Storage.weapons.containers.M60_SIDE_R='weapons.containers.M60_SIDE_R' ENUMS.Storage.weapons.containers.GAU_12_Equalizer_AP='weapons.containers.{GAU_12_Equalizer_AP}' ENUMS.Storage.weapons.containers.GAU_12_Equalizer='weapons.containers.{GAU_12_Equalizer}' ENUMS.Storage.weapons.containers.KORD_12_7_MI24_R='weapons.containers.KORD_12_7_MI24_R' ENUMS.Storage.weapons.containers.M134_SIDE_R='weapons.containers.M134_SIDE_R' ENUMS.Storage.weapons.containers.AKAN_NO_TRC='weapons.containers.{AKAN_NO_TRC}' ENUMS.Storage.weapons.containers.oh_58_brauning='weapons.containers.oh-58-brauning' ENUMS.Storage.weapons.containers.MXU_648='weapons.containers.MXU-648' ENUMS.Storage.weapons.containers.M134_R='weapons.containers.M134_R' ENUMS.Storage.weapons.containers.AN_M3='weapons.containers.{AN-M3}' ENUMS.Storage.weapons.containers.GIAT_M621_APHE='weapons.containers.{GIAT_M621_APHE}' ENUMS.Storage.weapons.containers.AIM_9S='weapons.containers.AIM-9S' ENUMS.Storage.weapons.containers.CH47_AFT_M240H='weapons.containers.{CH47_AFT_M240H}' ENUMS.Storage.weapons.containers.FN_HMP400='weapons.containers.{FN_HMP400}' ENUMS.Storage.weapons.containers.M134_SIDE_L='weapons.containers.M134_SIDE_L' ENUMS.Storage.weapons.containers.MB339_DEFA553_L='weapons.containers.{MB339_DEFA553_L}' ENUMS.Storage.weapons.containers.MISC_1='weapons.containers.{05544F1A-C39C-466b-BC37-5BD1D52E57BB}' ENUMS.Storage.weapons.containers.MISC_2='weapons.containers.{E92CBFE5-C153-11d8-9897-000476191836}' ENUMS.Storage.weapons.containers.hvar_SmokeGenerator='weapons.containers.hvar_SmokeGenerator' ENUMS.Storage.weapons.containers.INV_SMOKE_RED='weapons.containers.{INV-SMOKE-RED}' ENUMS.Storage.weapons.containers.INV_SMOKE_YELLOW='weapons.containers.{INV-SMOKE-YELLOW}' ENUMS.Storage.weapons.containers.INV_SMOKE_BLUE='weapons.containers.{INV-SMOKE-BLUE}' ENUMS.Storage.weapons.containers.INV_SMOKE_GREEN='weapons.containers.{INV-SMOKE-GREEN}' ENUMS.Storage.weapons.containers.INV_SMOKE_WHITE='weapons.containers.{INV-SMOKE-WHITE}' ENUMS.Storage.weapons.containers.INV_SMOKE_ORANGE='weapons.containers.{INV-SMOKE-ORANGE}' ENUMS.Storage.weapons.containers.GAU_12_Equalizer_AP='weapons.containers.{GAU_12_Equalizer_AP}' ENUMS.Storage.weapons.containers.AH_6_Gunners='weapons.containers.{AH-6_Gunners}' ENUMS.Storage.weapons.containers.AH_6_FN_HMP400='weapons.containers.{AH-6_FN_HMP400}' ENUMS.Storage.weapons.containers.R_73U='weapons.containers.R-73U' ENUMS.Storage.weapons.containers.M60_SIDE_R='weapons.containers.M60_SIDE_R' ENUMS.Storage.weapons.containers.CH47_AFT_M3M='weapons.containers.{CH47_AFT_M3M}' ENUMS.Storage.weapons.containers.GUV_YakB_GSHP='weapons.containers.GUV_YakB_GSHP' ENUMS.Storage.weapons.containers.RKL609_L='weapons.containers.{RKL609_L}' ENUMS.Storage.weapons.containers.CH47_PORT_M134D='weapons.containers.{CH47_PORT_M134D}' ENUMS.Storage.weapons.containers.CH47_PORT_M60D='weapons.containers.{CH47_PORT_M60D}' ENUMS.Storage.weapons.containers.RKL609_R='weapons.containers.{RKL609_R}' ENUMS.Storage.weapons.containers.C_101_DEFA553='weapons.containers.{C-101-DEFA553}' ENUMS.Storage.weapons.containers.AH6_M134L='weapons.containers.{AH6_M134L}' ENUMS.Storage.weapons.containers.KORD_12_7='weapons.containers.KORD_12_7' ENUMS.Storage.weapons.containers.C130_M18_Sidearm='weapons.containers.{C130-M18-Sidearm}' ENUMS.Storage.weapons.containers.GIAT_M621_HEAP='weapons.containers.{GIAT_M621_HEAP}' ENUMS.Storage.weapons.containers.BRU_42_LS='weapons.containers.BRU-42_LS' ENUMS.Storage.weapons.containers.CH47_STBD_M60D='weapons.containers.{CH47_STBD_M60D}' ENUMS.Storage.weapons.containers.SUU_23_POD='weapons.containers.{SUU_23_POD}' ENUMS.Storage.weapons.containers.M134_SIDE_R='weapons.containers.M134_SIDE_R' ENUMS.Storage.weapons.containers.AKAN_NO_TRC='weapons.containers.{AKAN_NO_TRC}' ENUMS.Storage.weapons.containers.oh_58_brauning='weapons.containers.oh-58-brauning' ENUMS.Storage.weapons.containers.AH_6_DOORS='weapons.containers.{AH-6_DOORS}' ENUMS.Storage.weapons.containers.CC420_GUN_POD='weapons.containers.{CC420_GUN_POD}' ENUMS.Storage.weapons.containers.CH47_PORT_M240H='weapons.containers.{CH47_PORT_M240H}' ENUMS.Storage.weapons.containers.MB339_DEFA553_R='weapons.containers.{MB339_DEFA553_R}' ENUMS.Storage.weapons.containers.AIM_9S='weapons.containers.AIM-9S' ENUMS.Storage.weapons.containers.hvar_SmokeGenerator='weapons.containers.hvar_SmokeGenerator' ENUMS.Storage.weapons.containers.M134_L='weapons.containers.M134_L' ENUMS.Storage.weapons.containers.AKAN='weapons.containers.{AKAN}' ENUMS.Storage.weapons.containers.C130_Cargo_Bay_M4='weapons.containers.{C130-Cargo-Bay-M4}' ENUMS.Storage.weapons.containers.M134_SIDE_L='weapons.containers.M134_SIDE_L' ENUMS.Storage.weapons.containers.FN_HMP400_200='weapons.containers.{FN_HMP400_200}' ENUMS.Storage.weapons.containers.OH58D_M3P_L200='weapons.containers.OH58D_M3P_L200' ENUMS.Storage.weapons.containers.GIAT_M621_AP='weapons.containers.{GIAT_M621_AP}' ENUMS.Storage.weapons.containers.M134_R='weapons.containers.M134_R' ENUMS.Storage.weapons.containers.OH58D_M3P_L300='weapons.containers.OH58D_M3P_L300' ENUMS.Storage.weapons.containers.AH6_M134R='weapons.containers.{AH6_M134R}' ENUMS.Storage.weapons.containers.CHAP_HMP400LC='weapons.containers.{CHAP_HMP400LC}' ENUMS.Storage.weapons.missiles.C701_AShM='weapons.missiles.C701 AShM' ENUMS.Storage.weapons.missiles.IGLA_9M39='weapons.missiles.9M39_IGLA' ENUMS.Storage.weapons.missiles.CHAP_AIM92='weapons.missiles.CHAP_AIM92' ENUMS.Storage.weapons.missiles.AM39='weapons.missiles.AM39' ENUMS.Storage.weapons.missiles.Shahed136_LM='weapons.missiles.Shahed136_LM' ENUMS.Storage.weapons.torpedoes.G7A_T1='weapons.torpedoes.G7A_T1' ENUMS.Storage.weapons.gunmounts.UH60LGAU19='weapons.gunmounts.UH-60L GAU-19' ENUMS.Storage.weapons.gunmounts.UH60L_M134='weapons.gunmounts.UH60L_M134' ENUMS.Storage.weapons.gunmounts.UH60_M134='weapons.gunmounts.UH60_M134' ENUMS.Storage.weapons.adapters.uh60l_lwl12='weapons.adapters.uh60l_lwl12' ENUMS.Storage.weapons.droptanks.uh60l_iafts='weapons.droptanks.uh60l_iafts' ENUMS.Storage.weapons.gunmounts.UH60_GAU19_LEFT='weapons.gunmounts.{UH60_GAU19_LEFT}' ENUMS.Storage.weapons.gunmounts.UH60_GAU19_RIGHT='weapons.gunmounts.{UH60_GAU19_RIGHT}' ENUMS.Storage.weapons.gunmounts.UH60_M134_LEFT='weapons.gunmounts.{UH60_M134_LEFT}' ENUMS.Storage.weapons.gunmounts.UH60_M134_RIGHT='weapons.gunmounts.{UH60_M134_RIGHT}' ENUMS.Storage.weapons.gunmounts.UH60L_M134_GUNNER='weapons.gunmounts.{UH60L_M134_GUNNER}' ENUMS.Storage.weapons.gunmounts.UH60L_M60_GUNNER='weapons.gunmounts.{UH60L_M60_GUNNER}' ENUMS.Storage.weapons.gunmounts.UH60L_M2_GUNNER='weapons.gunmounts.{UH60L_M2_GUNNER}' ENUMS.Storage.weapons.gunmounts.UH60_M230_LEFT='weapons.gunmounts.{UH60_M230_LEFT}' ENUMS.Storage.weapons.gunmounts.UH60_M230_RIGHT='weapons.gunmounts.{UH60_M230_RIGHT}' ENUMS.Storage.weapons.containers.UH60_M134_RIGHT='weapons.containers.{UH60_M134_RIGHT}' ENUMS.Storage.weapons.containers.UH60_M134_LEFT='weapons.containers.{UH60_M134_LEFT}' ENUMS.Storage.weapons.containers.UH60_M230_LEFT='weapons.containers.{UH60_M230_LEFT}' ENUMS.Storage.weapons.containers.UH60L_M134_GUNNER='weapons.containers.{UH60L_M134_GUNNER}' ENUMS.Storage.weapons.containers.UH60_GAU19_LEFT='weapons.containers.{UH60_GAU19_LEFT}' ENUMS.Storage.weapons.containers.UH60L_M60_GUNNER='weapons.containers.{UH60L_M60_GUNNER}' ENUMS.Storage.weapons.containers.UH60_GAU19_RIGHT='weapons.containers.{UH60_GAU19_RIGHT}' ENUMS.Storage.weapons.containers.UH60_M230_RIGHT='weapons.containers.{UH60_M230_RIGHT}' ENUMS.Storage.weapons.containers.UH60L_M2_GUNNER='weapons.containers.{UH60L_M2_GUNNER}' 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"}, } ENUMS.FrequencyBand={ HF=0, VHF_LOW=1, VHF_HI=2, UHF=3, } ENUMS.ModulationType={ AM=0, FM=1, AMFM=2, DISCARD=-1, } SMOKECOLOR=trigger.smokeColor FLARECOLOR=trigger.flareColor BIGSMOKEPRESET={ SmallSmokeAndFire=1, MediumSmokeAndFire=2, LargeSmokeAndFire=3, HugeSmokeAndFire=4, SmallSmoke=5, MediumSmoke=6, LargeSmoke=7, HugeSmoke=8, } DCSMAP={ Caucasus="Caucasus", NTTR="Nevada", Normandy="Normandy", PersianGulf="PersianGulf", TheChannel="TheChannel", Syria="Syria", MarianaIslands="MarianaIslands", Falklands="Falklands", Sinai="SinaiMap", Kola="Kola", Afghanistan="Afghanistan", Iraq="Iraq", GermanyCW="GermanyCW", } 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, }, Intruder={ Raygun=4, Heartless=5, Viceroy=6, Cupcake=7, ["Flying Tiger"]=8, ["Flying Ace"]=9, Buckeye=10, Goldplate=11, Phoenix=12, Electron=13, Rustler=14, Vixen=15, Jackal=16, Milestone=17, Devil=18, }, } 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) local 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(t,indent,noprint,maxDepth,seen) maxDepth=maxDepth or 5 indent=indent or 0 seen=seen or{} if not t or type(t)~="table"then env.warning("No table passed!") return nil end if indent>maxDepth then local msg=string.rep(" ",indent).."\n" if not noprint then env.info(msg)end return msg end if seen[t]then local msg=string.rep(" ",indent).."\n" if not noprint then env.info(msg)end return msg end seen[t]=true local text="\n" for k,v in pairs(t)do local key=k if type(key)=="string"and key:find(" ",1,true)then key='"'..key..'"' else key=tostring(key) end if type(v)=="table"and next(v)~=nil then if not noprint then env.info(string.rep(" ",indent)..key.." = {") end text=text..string.rep(" ",indent)..key.." = {\n" text=text..UTILS.PrintTableToLog(v,indent+1,noprint,maxDepth,seen) text=text..string.rep(" ",indent).."},\n" if not noprint then env.info(string.rep(" ",indent).."},") end elseif type(v)=="function"then else local value if type(v)=="boolean"or type(v)=="number"then value=tostring(v) else value='"'..tostring(v)..'"' end if not noprint then env.info(string.rep(" ",indent)..key.." = "..value..",") end text=text..string.rep(" ",indent)..key.." = "..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.VecScale(v,s) return{x=v.x*s,y=v.y*s,z=v.z*s} 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.Vec3Substract(a,b) return{x=a.x-b.x,y=a.y-b.y,z=a.z-b.z} 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 or b.y)-(a.z or a.y) 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.Vec3Length(v) return math.sqrt(v.x*v.x+v.y*v.y+v.z*v.z) 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 elseif map==DCSMAP.GermanyCW then declination=0.1 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) if string.find(typename,"ka-50",1,true)then return"Shark" elseif string.find(typename,"a-50",1,true)then return"Mainstay" end 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 for name,value in pairs(CALLSIGN.Intruder)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 elseif theatre==DCSMAP.Iraq then return 3.0 elseif theatre==DCSMAP.GermanyCW then return 1.0 else BASE:E(string.format("ERROR: Unknown Map %s in UTILS.GMTToLocal function. Returning 0",tostring(theatre))) return 0 end end function UTILS.GetDayOfYear(Year,Month,Day) local floor=math.floor local n1=floor(275*Month/9) local n2=floor((Month+9)/12) local n3=(1+floor((Year-4*floor(Year/4)+2)/3)) return n1-(n2*n3)+Day-30 end function UTILS.GetSunRiseAndSet(DayOfYear,Latitude,Longitude,Rising,Tlocal) local zenith=90.83 local latitude=Latitude local longitude=Longitude local rising=Rising local n=DayOfYear Tlocal=Tlocal or 0 local rad=math.rad local deg=math.deg local floor=math.floor local frac=function(n)return n-floor(n)end local cos=function(d)return math.cos(rad(d))end local acos=function(d)return deg(math.acos(d))end local sin=function(d)return math.sin(rad(d))end local asin=function(d)return deg(math.asin(d))end local tan=function(d)return math.tan(rad(d))end local atan=function(d)return deg(math.atan(d))end local function fit_into_range(val,min,max) local range=max-min local count if val=max then count=floor((val-max)/range)+1 return val-count*range else return val end end local lng_hour=longitude/15 local t if rising then t=n+((6-lng_hour)/24) else t=n+((18-lng_hour)/24) end local M=(0.9856*t)-3.289 local L=fit_into_range(M+(1.916*sin(M))+(0.020*sin(2*M))+282.634,0,360) local RA=fit_into_range(atan(0.91764*tan(L)),0,360) local Lquadrant=floor(L/90)*90 local RAquadrant=floor(RA/90)*90 RA=RA+Lquadrant-RAquadrant RA=RA/15 local sinDec=0.39782*sin(L) local cosDec=cos(asin(sinDec)) local cosH=(cos(zenith)-(sinDec*sin(latitude)))/(cosDec*cos(latitude)) if rising and cosH>1 then return"N/R" elseif cosH<-1 then return"N/S" end local H if rising then H=360-acos(cosH) else H=acos(cosH) end H=H/15 local T=H+RA-(0.06571*t)-6.622 local UT=fit_into_range(T-lng_hour+Tlocal,0,24) return floor(UT)*60*60+frac(UT)*60*60 end function UTILS.GetSunrise(Day,Month,Year,Latitude,Longitude,Tlocal) local DayOfYear=UTILS.GetDayOfYear(Year,Month,Day) return UTILS.GetSunRiseAndSet(DayOfYear,Latitude,Longitude,true,Tlocal) end function UTILS.GetSunset(Day,Month,Year,Latitude,Longitude,Tlocal) local DayOfYear=UTILS.GetDayOfYear(Year,Month,Day) return UTILS.GetSunRiseAndSet(DayOfYear,Latitude,Longitude,false,Tlocal) end function UTILS.GetOSTime() if os then 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 type_name=="C-130J-30"and(unit:getDrawArgumentValue(86)==1)then BASE:T(unit_name.." rear doors are open") return true end if type_name=="C-130J-30"and(unit:getDrawArgumentValue(87)==1)then BASE:T(unit_name.." Side door(s) are open") return true end if type_name=="C-130J-30"and(unit:getDrawArgumentValue(88)==1)then BASE:T(unit_name.." Paratroop door(s) are open") 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=="UH-60L_DAP"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_DAP"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 local UnitDescriptor=unit:getDesc() local IsGroundResult=(UnitDescriptor.category==Unit.Category.GROUND_UNIT) return IsGroundResult 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,Resurrection,ResurrectPercentage,Healmin,Healmax) local healmin=Healmin or 25 local healmax=Healmax or 75 local resurrection=(Resurrection==true)and true or false local resurrectpercentage=ResurrectPercentage or 25 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,300,1,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 local resurectok=math.random(1,100) BASE:E(string.format("Load Group | Resurrection | Resurrect %s | Thresh %d | Random %d",tostring(resurrection),resurrectpercentage,resurectok)) if resurrection==true and(resurectoksize 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.AdjustHeading360(Heading) while Heading>=360 or Heading<0 do if Heading>=360 then Heading=Heading-360 elseif Heading<0 then Heading=Heading+360 end end return Heading 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,Backend) 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 and Backend~=nil and Backend~=MSRS.Backend.HOUND 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(timer.getTime()) 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.GenerateGridPoints(startVec2,n,spacingX,spacingY) local points={} local gridSize=math.ceil(math.sqrt(n)) local count=0 local n=n or 1 local spacingX=spacingX or 100 local spacingY=spacingY or 100 local startX=startVec2.x or 100 local startY=startVec2.y or 100 for row=0,gridSize-1 do for col=0,gridSize-1 do if count>=n then break end local point={ x=startX+(col*spacingX), y=startY+(row*spacingY) } table.insert(points,point) count=count+1 end if count>=n then break end end return points end function UTILS.SpawnFARPAndFunctionalStatics(Name,Coordinate,FARPType,Coalition,Country,CallSign,Frequency,Modulation,ADF,SpawnRadius,VehicleTemplate,Liquids,Equipment,Airframes,F10Text,DynamicSpawns,HotStart,NumberPads,SpacingX,SpacingY) local function PopulateStorage(Name,liquids,equip,airframes) local newWH=STORAGE:New(Name) if newWH then 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 if airframes and airframes>0 then for typename in pairs(CSAR.AircraftType)do newWH:SetItem(typename,airframes) end end end end 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 airframes=Airframes 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 NumberPads=NumberPads or 1 local SpacingX=SpacingX or 100 local SpacingY=SpacingY or 100 local FarpVec2=Coordinate:GetVec2() if NumberPads>1 then local Grid=UTILS.GenerateGridPoints(FarpVec2,NumberPads,SpacingX,SpacingY) local groupData={ ["visible"]=true, ["hidden"]=false, ["units"]={}, ["y"]=0, ["x"]=0, ["name"]=Name, } local unitData={ ["category"]="Heliports", ["type"]=STypeName, ["y"]=0, ["x"]=0, ["name"]=Name, ["heading"]=0, ["heliport_modulation"]=mod, ["heliport_frequency"]=freq, ["heliport_callsign_id"]=callsign, ["dead"]=false, ["shape_name"]=SShapeName, ["dynamicSpawn"]=DynamicSpawns, ["allowHotStart"]=HotStart, } for id,gridpoint in ipairs(Grid)do local UnitTemplate=UTILS.DeepCopy(unitData) UnitTemplate.x=gridpoint.x UnitTemplate.y=gridpoint.y if id>1 then UnitTemplate.name=Name.."-"..id end table.insert(groupData.units,UnitTemplate) if id==1 then groupData.x=gridpoint.x groupData.y=gridpoint.y end end local Static=coalition.addGroup(Country,-1,groupData) local Event={ id=EVENTS.Birth, time=timer.getTime(), initiator=Static } world.onEvent(Event) else local newfarp=SPAWNSTATIC:NewFromType(STypeName,"Heliports",Country) newfarp:InitShape(SShapeName) newfarp:InitFARP(callsign,freq,mod,DynamicSpawns,HotStart) local spawnedfarp=newfarp:SpawnFromCoordinate(farplocation,0,Name) table.insert(ReturnObjects,spawnedfarp) end PopulateStorage(Name,liquids,equip,airframes) 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 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 local MarkerID=nil if F10Text then local Color={0,0,1} if Coalition==coalition.side.RED then Color={1,0,0} elseif Coalition==coalition.side.NEUTRAL then Color={0,1,0} end local Alpha=0.75 local coordinate=Coordinate:Translate(600,0) MarkerID=coordinate:TextToAll(F10Text,Coalition,Color,1,{1,1,1},Alpha,14,true) end return ReturnObjects,ADFName,MarkerID end function UTILS.SpawnMASHStatics(Name,Coordinate,Country,ADF,Livery,DeployHelo,MASHRadio,MASHRadioModulation,MASHCallsign,Templates) local MASHTemplates={ [1]={category='Infantry',type='Soldier M4',shape_name='none',heading=0,x=0.000000,y=0.000000,}, [2]={category='Infantry',type='Soldier M4',shape_name='none',heading=0,x=0.313533,y=8.778935,}, [3]={category='Infantry',type='Soldier M4',shape_name='none',heading=0,x=16.303737,y=20.379671,}, [4]={category='Helicopters',type='CH-47Fbl1',shape_name='none',heading=0,x=-20.047735,y=-63.166179,livery_id="us army dark green",}, [5]={category='Infantry',type='Soldier M4',shape_name='none',heading=0,x=26.650339,y=20.066138,}, [6]={category='Heliports',type='FARP_SINGLE_01',shape_name='FARP_SINGLE_01',heading=0,x=-25.432292,y=9.077099,}, [7]={category='Heliports',type='FARP_SINGLE_01',shape_name='FARP_SINGLE_01',heading=0,x=-12.717421,y=-3.216114,}, [8]={category='Heliports',type='FARP_SINGLE_01',shape_name='FARP_SINGLE_01',heading=0,x=-25.439281,y=-3.216114,}, [9]={category='Heliports',type='FARP_SINGLE_01',shape_name='FARP_SINGLE_01',heading=0,x=-12.717421,y=9.155603,}, [10]={category='Fortifications',type='TACAN_beacon',shape_name='none',heading=0,x=-2.329847,y=-16.579903,}, [11]={category='Fortifications',type='FARP Fuel Depot',shape_name='GSM Rus',heading=0,x=2.222011,y=4.487030,}, [12]={category='Fortifications',type='APFC fuel',shape_name='M92_APFCfuel',heading=0,x=3.614927,y=0.367838,}, [13]={category='Fortifications',type='Camouflage03',shape_name='M92_Camouflage03',heading=0,x=21.544148,y=21.998879,}, [14]={category='Fortifications',type='Container_generator',shape_name='M92_Container_generator',heading=0,x=20.989192,y=37.314334,}, [15]={category='Fortifications',type='FireExtinguisher02',shape_name='M92_FireExtinguisher02',heading=0,x=3.988003,y=8.362333,}, [16]={category='Fortifications',type='FireExtinguisher02',shape_name='M92_FireExtinguisher02',heading=0,x=-3.953195,y=12.945844,}, [17]={category='Fortifications',type='Windsock',shape_name='H-Windsock_RW',heading=0,x=-18.944173,y=-33.042196,}, [18]={category='Fortifications',type='Tent04',shape_name='M92_Tent04',heading=0,x=21.220671,y=30.247529,}, } if Templates then MASHTemplates=Templates end local name=Name or"Florence Nightingale" local positionVec2 local positionVec3 local ReturnStatics={} local CountryID=Country or country.id.USA local livery="us army dark green" local MASHRadio=MASHRadio or 127.5 local MASHRadioModulation=MASHRadioModulation or radio.modulation.AM local MASHCallsign=MASHCallsign or CALLSIGN.FARP.Berlin if type(Coordinate)=="table"then if Coordinate:IsInstanceOf("COORDINATE")or Coordinate:IsInstanceOf("ZONE_BASE")then positionVec2=Coordinate:GetVec2() positionVec3=Coordinate:GetVec3() end else BASE:E("Spawn MASH - no ZONE or COORDINATE handed!") return end local BaseX=positionVec2.x local BaseY=positionVec2.y for id,object in pairs(MASHTemplates)do local NewName=string.format("%s#%3d",name,id) local vec2={x=BaseX+object.x,y=BaseY+object.y} local Coordinate=COORDINATE:NewFromVec2(vec2) local static=SPAWNSTATIC:NewFromType(object.type,object.category,CountryID) if object.shape_name and object.shape_name~="none"then static:InitShape(object.shape_name) end if object.category=="Helicopters"and DeployHelo==true then if object.livery_id~=nil then livery=object.livery_id end static:InitLivery(livery) local newstatic=static:SpawnFromCoordinate(Coordinate,object.heading,NewName) table.insert(ReturnStatics,newstatic) elseif object.category=="Heliports"then static:InitFARP(MASHCallsign,MASHRadio,MASHRadioModulation,false,false) local newstatic=static:SpawnFromCoordinate(Coordinate,object.heading,NewName) table.insert(ReturnStatics,newstatic) elseif object.category~="Helicopters"and object.category~="Heliports"then local newstatic=static:SpawnFromCoordinate(Coordinate,object.heading,NewName) table.insert(ReturnStatics,newstatic) end end local ADFName if ADF and type(ADF)=="number"then local ADFFreq=ADF*1000 local Sound="l10n/DEFAULT/beacon.ogg" ADFName=Name.." ADF "..tostring(ADF).."KHz" trigger.action.radioTransmission(Sound,positionVec3,0,true,ADFFreq,250,ADFName) end return ReturnStatics,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.Vec3toVec2(Vec3) if Vec3 and type(Vec3)=="table"then local Vec2={} Vec2.x=Vec3.x or 0 Vec2.y=Vec3.z or 0 return Vec2 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 function UTILS.GetEnvZone(name) for _,v in ipairs(env.mission.triggers.zones)do if v.name==name then return v end end end function UTILS.DoStringIn(State,DoString) return net.dostring_in(State,DoString) end function UTILS.ShowPictureToAll(FilePath,Duration,ClearView,StartDelay,HorizontalAlign,VerticalAlign,Size,SizeUnits) ClearView=ClearView or false StartDelay=StartDelay or 0 HorizontalAlign=HorizontalAlign or 1 VerticalAlign=VerticalAlign or 1 Size=Size or 100 SizeUnits=SizeUnits or 0 if ClearView then ClearView="true"else ClearView="false"end net.dostring_in("mission",string.format("a_out_picture(\"%s\", %d, %s, %d, \"%d\", \"%d\", %d, \"%d\")",FilePath,Duration or 10,ClearView,StartDelay,HorizontalAlign,VerticalAlign,Size,SizeUnits)) end function UTILS.ShowPictureToCoalition(Coalition,FilePath,Duration,ClearView,StartDelay,HorizontalAlign,VerticalAlign,Size,SizeUnits) ClearView=ClearView or false StartDelay=StartDelay or 0 HorizontalAlign=HorizontalAlign or 1 VerticalAlign=VerticalAlign or 1 Size=Size or 100 SizeUnits=SizeUnits or 0 if ClearView then ClearView="true"else ClearView="false"end local coalName=string.lower(UTILS.GetCoalitionName(Coalition)) net.dostring_in("mission",string.format("a_out_picture_s(\"%s\", \"%s\", %d, %s, %d, \"%d\", \"%d\", %d, \"%d\")",coalName,FilePath,Duration or 10,ClearView,StartDelay,HorizontalAlign,VerticalAlign,Size,SizeUnits)) end function UTILS.ShowPictureToCountry(Country,FilePath,Duration,ClearView,StartDelay,HorizontalAlign,VerticalAlign,Size,SizeUnits) ClearView=ClearView or false StartDelay=StartDelay or 0 HorizontalAlign=HorizontalAlign or 1 VerticalAlign=VerticalAlign or 1 Size=Size or 100 SizeUnits=SizeUnits or 0 if ClearView then ClearView="true"else ClearView="false"end net.dostring_in("mission",string.format("a_out_picture_c(%d, \"%s\", %d, %s, %d, \"%d\", \"%d\", %d, \"%d\")",Country,FilePath,Duration or 10,ClearView,StartDelay,HorizontalAlign,VerticalAlign,Size,SizeUnits)) end function UTILS.ShowPictureToGroup(Group,FilePath,Duration,ClearView,StartDelay,HorizontalAlign,VerticalAlign,Size,SizeUnits) ClearView=ClearView or false StartDelay=StartDelay or 0 HorizontalAlign=HorizontalAlign or 1 VerticalAlign=VerticalAlign or 1 Size=Size or 100 SizeUnits=SizeUnits or 0 if ClearView then ClearView="true"else ClearView="false"end net.dostring_in("mission",string.format("a_out_picture_g(%d, \"%s\", %d, %s, %d, \"%d\", \"%d\", %d, \"%d\")",Group:GetID(),FilePath,Duration or 10,ClearView,StartDelay,HorizontalAlign,VerticalAlign,Size,SizeUnits)) end function UTILS.ShowPictureToUnit(Unit,FilePath,Duration,ClearView,StartDelay,HorizontalAlign,VerticalAlign,Size,SizeUnits) ClearView=ClearView or false StartDelay=StartDelay or 0 HorizontalAlign=HorizontalAlign or 1 VerticalAlign=VerticalAlign or 1 Size=Size or 100 SizeUnits=SizeUnits or 0 if ClearView then ClearView="true"else ClearView="false"end net.dostring_in("mission",string.format("a_out_picture_u(%d, \"%s\", %d, %s, %d, \"%d\", \"%d\", %d, \"%d\")",Unit:GetID(),FilePath,Duration or 10,ClearView,StartDelay,HorizontalAlign,VerticalAlign,Size,SizeUnits)) end function UTILS.LoadMission(FileName) net.dostring_in("mission",string.format("a_load_mission(\"%s\")",FileName)) end function UTILS.SetMissionBriefing(Coalition,Text,Picture) Text=Text or"" Text=Text:gsub("\n","\\n") Picture=Picture or"" local coalName=string.lower(UTILS.GetCoalitionName(Coalition)) net.dostring_in("mission",string.format("a_set_briefing(\"%s\", \"%s\", \"%s\")",coalName,Picture,Text)) end function UTILS.ShowHelperGate(pos,heading) net.dostring_in("mission",string.format("a_show_helper_gate(%s, %s, %s, %f)",pos.x,pos.y,pos.z,math.rad(heading))) end function UTILS.ShowHelperGateForUnit(Unit,Flag) net.dostring_in("mission",string.format("a_show_route_gates_for_unit(%d, \"%d\")",Unit:GetID(),Flag)) end function UTILS.SetCarrierIlluminationMode(UnitID,Mode) net.dostring_in("mission",string.format("a_set_carrier_illumination_mode(%d, %d)",UnitID,Mode)) end function UTILS.ShellZone(name,power,count) local z=UTILS.GetEnvZone(name) if z then net.dostring_in("mission",string.format("a_shelling_zone(%d, %d, %d)",z.zoneId,power,count)) end end function UTILS.RemoveObjects(name,type) local z=UTILS.GetEnvZone(name) if z then net.dostring_in("mission",string.format("a_remove_scene_objects(%d, %d)",z.zoneId,type)) end end function UTILS.DestroyScenery(name,level) local z=UTILS.GetEnvZone(name) if z then net.dostring_in("mission",string.format("a_scenery_destruction_zone(%d, %d)",z.zoneId,level)) end end function UTILS.GetSimpleZones(Vec3,SearchRadius,PosRadius,NumPositions) return Disposition.getSimpleZones(Vec3,SearchRadius,PosRadius,NumPositions) end function UTILS.GetClearZonePositions(Zone,PosRadius,NumPositions) local radius=PosRadius or math.min(Zone:GetRadius()/10,200) local clearPositions=UTILS.GetSimpleZones(Zone:GetVec3(),Zone:GetRadius(),radius,NumPositions or 50) if clearPositions and#clearPositions>0 then local validZones={} for _,vec2 in pairs(clearPositions)do if Zone:IsVec2InZone(vec2)then table.insert(validZones,vec2) end end if#validZones>0 then return validZones,radius end end return nil end function UTILS.GetRandomClearZoneCoordinate(Zone,PosRadius,NumPositions) local clearPositions=UTILS.GetClearZonePositions(Zone,PosRadius,NumPositions) if clearPositions and#clearPositions>0 then local randomPosition,radius=clearPositions[math.random(1,#clearPositions)] return COORDINATE:NewFromVec2(randomPosition),radius end return nil end function UTILS.FindNearestPointOnCircle(Vec1,Radius,Vec2) local r=Radius local cx=Vec1.x or 1 local cy=Vec1.y or 1 local px=Vec2.x or 1 local py=Vec2.y or 1 local dx=px-cx local dy=py-cy local dist=math.sqrt(dx*dx+dy*dy) if dist==0 then return{x=cx+r,y=cy} end local norm_dx=dx/dist local norm_dy=dy/dist local qx=cx+r*norm_dx local qy=cy+r*norm_dy local shift_factor=1 qx=qx+shift_factor*norm_dx qy=qy+shift_factor*norm_dy return{x=qx,y=qy} end function UTILS.ValidateAndRepositionGroundUnits(Positions,Anchor,MaxRadius,Spacing) local units=Positions Anchor=Anchor or UTILS.GetCenterPoint(units) local gPos={x=Anchor.x,y=Anchor.z or Anchor.y} local maxRadius=0 local unitCount=0 for _,unit in pairs(units)do local pos={x=unit.x,y=unit.z or unit.y} local dist=UTILS.VecDist2D(pos,gPos) if dist>maxRadius then maxRadius=dist end unitCount=unitCount+1 end maxRadius=MaxRadius or math.max(maxRadius*2,10) local spacing=Spacing or math.max(maxRadius*0.05,5) if unitCount>0 and maxRadius>5 then local spots=UTILS.GetSimpleZones(UTILS.Vec2toVec3(gPos),maxRadius,spacing,1000) if spots and#spots>0 then local validSpots={} for _,spot in pairs(spots)do if land.getSurfaceType(spot)==land.SurfaceType.LAND then table.insert(validSpots,spot) end end spots=validSpots end local step=spacing for _,unit in pairs(units)do local pos={x=unit.x,y=unit.z or unit.y} local isOnLand=land.getSurfaceType(pos)==land.SurfaceType.LAND local isValid=false if spots and#spots>0 then local si=1 local sid=0 local closestDist=100000000 local closestSpot for _,spot in pairs(spots)do local dist=UTILS.VecDist2D(pos,spot) if dist=spacing then pos=closestSpot end isValid=true table.remove(spots,sid) end end if not isValid and not isOnLand then local h=UTILS.HdgTo(pos,gPos) local retries=0 while not isValid and retries<500 do local dist=UTILS.VecDist2D(pos,gPos) pos=UTILS.Vec2Translate(pos,step,h) local skip=false for _,unit2 in pairs(units)do if unit~=unit2 then local pos2={x=unit2.x,y=unit2.z or unit2.y} local dist2=UTILS.VecDist2D(pos,pos2) if dist2<12 then isValid=false skip=true break end end end if not skip and dist>step and land.getSurfaceType(pos)==land.SurfaceType.LAND then isValid=true break elseif dist<=step then break end retries=retries+1 end end if isValid then unit.x=pos.x if unit.z then unit.z=pos.y else unit.y=pos.y end end end end end function UTILS.ValidateAndRepositionStatic(Country,Category,Type,Position,ShapeName,MaxRadius) local coord=COORDINATE:NewFromVec2(Position) local st=SPAWNSTATIC:NewFromType(Type,Category,Country) if ShapeName then st:InitShape(ShapeName) end local sName="s-"..timer.getTime().."-"..math.random(1,10000) local tempStatic=st:SpawnFromCoordinate(coord,0,sName) if tempStatic then local sRadius=tempStatic:GetBoundingRadius(2)or 3 tempStatic:Destroy() sRadius=sRadius*0.5 MaxRadius=MaxRadius or math.max(sRadius*10,100) local positions=UTILS.GetSimpleZones(coord:GetVec3(),MaxRadius,sRadius,20) if positions and#positions>0 then local closestSpot local closestDist=math.huge for _,spot in pairs(positions)do if land.getSurfaceType(spot)==land.SurfaceType.LAND then local dist=UTILS.VecDist2D(Position,spot) if dist=sRadius then return closestSpot else return Position end end end end return nil end function UTILS.CreateAirbaseEnum() local function _savefile(filename,data) local file=lfs.writedir()..filename local f=io.open(file,"wb") if f then f:write(data) f:close() env.info(string.format("Saving to file %s",tostring(file))) else env.info(string.format("ERROR: Could not save results to file %s",tostring(file))) end end local airbases=world.getAirbases() local mapname=env.mission.theatre local myab={} for i,_airbase in pairs(airbases)do local airbase=_airbase local cat=airbase:getDesc().category if cat==Airbase.Category.AIRDROME then local name=airbase:getName() local key=name if name=="Airracing Lubeck"then key="Airracing_Luebeck" elseif name=="Bad Durkheim"then key="Bad_Duerkheim" elseif name=="Buchel"then key="Buechel" elseif name=="Buckeburg"then key="Bueckeburg" elseif name=="Dusseldorf"then key="Duesseldorf" elseif name=="Gutersloh"then key="Guetersloh" elseif name=="Kothen"then key="Koethen" elseif name=="Larz"then key="Laerz" elseif name=="Lubeck"then key="Luebeck" elseif name=="Luneburg"then key="Lueneburg" elseif name=="Norvenich"then key="Noervenich" elseif name=="Ober-Morlen"then key="Ober_Moerlen" elseif name=="Peenemunde"then key="Peenemuende" elseif name=="Pottschutthohe"then key="Pottschutthoehe" elseif name=="Schonefeld"then key="Schoenefeld" elseif name=="Weser Wumme"then key="Weser_Wuemme" elseif name=="Zollschen"then key="Zoellschen" elseif name=="Zweibrucken"then key="Zweibruecken" end key=key:gsub(" ","_") key=key:gsub("-","_") key=key:gsub("'","_") key=UTILS.ReplaceIllegalCharacters(key,"_") local entry={} entry.key=key entry.name=name table.insert(myab,entry) end end table.sort(myab,function(a,b)return a.namemaxDist then maxDist=dist end end return center,maxDist end function UTILS.GetMinimumBoundingCircle(points) if#points==0 then return nil,nil end local function distance(p1,p2) local dx=p2.x-p1.x local dy=p2.y-p1.y return math.sqrt(dx*dx+dy*dy) end local function circleFrom2Points(p1,p2) local center={ x=(p1.x+p2.x)/2, y=(p1.y+p2.y)/2 } local radius=distance(p1,p2)/2 return center,radius end local function circleFrom3Points(p1,p2,p3) local ax,ay=p1.x,p1.y local bx,by=p2.x,p2.y local cx,cy=p3.x,p3.y local d=2*(ax*(by-cy)+bx*(cy-ay)+cx*(ay-by)) if math.abs(d)<0.0001 then return circleFrom2Points(p1,p3) end local aSq=ax*ax+ay*ay local bSq=bx*bx+by*by local cSq=cx*cx+cy*cy local ux=(aSq*(by-cy)+bSq*(cy-ay)+cSq*(ay-by))/d local uy=(aSq*(cx-bx)+bSq*(ax-cx)+cSq*(bx-ax))/d local center={x=ux,y=uy} local radius=distance(center,p1) return center,radius end local function isInside(center,radius,point,tolerance) tolerance=tolerance or 0.0001 return distance(center,point)<=radius+tolerance end local function welzlHelper(pts,n,boundary) if n==0 or#boundary==3 then if#boundary==0 then return{x=0,y=0},0 elseif#boundary==1 then return{x=boundary[1].x,y=boundary[1].y},0 elseif#boundary==2 then return circleFrom2Points(boundary[1],boundary[2]) else return circleFrom3Points(boundary[1],boundary[2],boundary[3]) end end local p=pts[n] local center,radius=welzlHelper(pts,n-1,boundary) if isInside(center,radius,p)then return center,radius end local newBoundary={} for i=1,#boundary do newBoundary[i]=boundary[i] end table.insert(newBoundary,p) return welzlHelper(pts,n-1,newBoundary) end local pts={} for i,p in ipairs(points)do pts[i]={x=p.x,y=p.y} end for i=#pts,2,-1 do local j=math.random(1,i) pts[i],pts[j]=pts[j],pts[i] end return welzlHelper(pts,#pts,{}) end function UTILS.CalculateInterceptBearing(A1,V1,A2,V2_speed) local function berechne_bearing(richtung) local bearing=math.deg(math.atan2(richtung.x,richtung.z)) if bearing<0 then bearing=bearing+360 end return bearing end local function vec_normalize(v) local len=UTILS.Vec3Length(v) if len==0 then return{x=0,y=0,z=0}end return{x=v.x/len,y=v.y/len,z=v.z/len} end local rel_pos=UTILS.Vec3Substract(A1,A2) local distance=UTILS.Vec3Length(rel_pos) if distance==0 then return nil end local richtung_zu_f1=vec_normalize(rel_pos) local v1_normalisiert=vec_normalize(V1) local annaeherung=UTILS.VecDot(v1_normalisiert,richtung_zu_f1) if annaeherung<-0.95 then return nil end local rel_velocity=UTILS.VecSubstract(V1,{x=0,y=0,z=0}) local flucht_komponente=UTILS.VecDot(vec_normalize(rel_velocity),richtung_zu_f1) if flucht_komponente>0.95 then return nil end local v1=UTILS.Vec3Length(V1) local v2=V2_speed local a=UTILS.VecDot(V1,V1)-v2*v2 local b=2*UTILS.VecDot(rel_pos,V1) local c=UTILS.VecDot(rel_pos,rel_pos) local discriminant=b*b-4*a*c if discriminant<0 then return nil end local t1=(-b+math.sqrt(discriminant))/(2*a) local t2=(-b-math.sqrt(discriminant))/(2*a) local t=nil if t1>0 and t2>0 then t=math.min(t1,t2) elseif t1>0 then t=t1 elseif t2>0 then t=t2 else return nil end local treffpunkt=UTILS.VecAdd(A1,UTILS.VecScale(V1,t)) local richtung=UTILS.VecSubstract(treffpunkt,A2) local bearing=berechne_bearing(richtung) return UTILS.Round(bearing,0) 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) MaxDistance=MaxDistance or 2000 self:SetValidNeighbourFunction(ASTAR.DistMax,MaxDistance) return self end function ASTAR:SetValidNeighbourRoad(MaxDistance) MaxDistance=MaxDistance or 2000 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 or"") 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 and 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, CreateMarkCoordinateOnEvent=false, } world.event.S_EVENT_NEW_ZONE=world.event.S_EVENT_MAX+1000 world.event.S_EVENT_DELETE_ZONE=world.event.S_EVENT_MAX+1001 world.event.S_EVENT_NEW_ZONE_GOAL=world.event.S_EVENT_MAX+1002 world.event.S_EVENT_DELETE_ZONE_GOAL=world.event.S_EVENT_MAX+1003 world.event.S_EVENT_REMOVE_UNIT=world.event.S_EVENT_MAX+1004 world.event.S_EVENT_PLAYER_ENTER_AIRCRAFT=world.event.S_EVENT_MAX+1005 world.event.S_EVENT_NEW_DYNAMIC_CARGO=world.event.S_EVENT_MAX+1006 world.event.S_EVENT_DYNAMIC_CARGO_LOADED=world.event.S_EVENT_MAX+1007 world.event.S_EVENT_DYNAMIC_CARGO_UNLOADED=world.event.S_EVENT_MAX+1008 world.event.S_EVENT_DYNAMIC_CARGO_REMOVED=world.event.S_EVENT_MAX+1009 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, 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.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: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=STATIC:FindByName(Event.IniDCSUnitName,false) 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 and Event.IniDCSUnit.getName)and Event.IniDCSUnit:getName()or"Scenery no name "..math.random(1,20000) Event.IniUnitName=Event.IniDCSUnitName local ID=(Event.IniDCSUnit and Event.IniDCSUnit.getID)and Event.IniDCSUnit:getID()or Event.IniDCSUnitName Event.IniUnit=(_SCENERY~=nil)and _SCENERY[ID]or nil Event.IniCategory=(Event.IniDCSUnit and Event.IniDCSUnit.getDesc)and Event.IniDCSUnit:getDesc().category Event.IniTypeName=(Event.initiator and Event.initiator.isExist and Event.initiator:isExist()and Event.IniDCSUnit and Event.IniDCSUnit.getTypeName)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 local ID=(Event.TgtDCSUnit and Event.TgtDCSUnit.getID)and Event.TgtDCSUnit:getID()or Event.TgtDCSUnitName Event.TgtUnit=(_SCENERY~=nil)and _SCENERY[ID]or nil 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) if Event.Place then Event.PlaceName=Event.Place:GetName() end end end end if Event.idx then Event.MarkID=Event.idx Event.MarkVec3=Event.pos if self.CreateMarkCoordinateOnEvent==true then Event.MarkCoordinate=COORDINATE:NewFromVec3(Event.pos) end Event.MarkText=Event.text Event.MarkCoalition=Event.coalition Event.IniCoalition=Event.coalition Event.MarkGroupID=Event.groupID 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 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,Entry=Menu} end local function SortTable(k1,k2) if not k1 then if not k2 then return true else return false end elseif not k2 then if not k1 then return true else return false end else return(k1.Tag or 15)<=(k2.Tag or 15) end return false end table.sort(MenuTable,SortTable) for _,Menu in ipairs(MenuTable)do Menu.Entry:Refresh() end end return self end function MENU_GROUP:RemoveSubMenus(MenuStamp,MenuTag) for MenuText,Menu in pairs(self.Menus or{})do Menu:Remove(MenuStamp,MenuTag) end self.Menus=nil end function MENU_GROUP:Remove(MenuStamp,MenuTag) MENU_INDEX:PrepareGroup(self.Group) local Path=MENU_INDEX:ParentPath(self.ParentMenu,self.MenuText) local GroupMenu=MENU_INDEX:HasGroupMenu(self.Group,Path) if GroupMenu==self then self:RemoveSubMenus(MenuStamp,MenuTag) if not MenuStamp or self.MenuStamp~=MenuStamp then if(not MenuTag)or(MenuTag and self.MenuTag and MenuTag==self.MenuTag)then if self.MenuPath~=nil then self:F({Group=self.GroupID,Text=self.MenuText,Path=self.MenuPath}) missionCommands.removeItemForGroup(self.GroupID,self.MenuPath) end MENU_INDEX:ClearGroupMenu(self.Group,Path) self:ClearParentMenu(self.MenuText) return nil end end else BASE:E({"Cannot Remove MENU_GROUP",Path=Path,ParentMenu=self.ParentMenu,MenuText=self.MenuText,Group=self.Group}) return nil end return self end MENU_GROUP_COMMAND={ ClassName="MENU_GROUP_COMMAND" } function MENU_GROUP_COMMAND:New(Group,MenuText,ParentMenu,CommandMenuFunction,...) MENU_INDEX:PrepareGroup(Group) local Path=MENU_INDEX:ParentPath(ParentMenu,MenuText) local GroupMenu=MENU_INDEX:HasGroupMenu(Group,Path) if GroupMenu then GroupMenu:SetCommandMenuFunction(CommandMenuFunction) GroupMenu:SetCommandMenuArguments(arg) return GroupMenu else self=BASE:Inherit(self,MENU_COMMAND_BASE:New(MenuText,ParentMenu,CommandMenuFunction,arg)) MENU_INDEX:SetGroupMenu(Group,Path,self) self.Group=Group self.GroupID=Group:GetID() self.MenuPath=missionCommands.addCommandForGroup(self.GroupID,MenuText,self.MenuParentPath,self.MenuCallHandler) self:SetParentMenu(self.MenuText,self) return self end end function MENU_GROUP_COMMAND:Refresh() do missionCommands.removeItemForGroup(self.GroupID,self.MenuPath) missionCommands.addCommandForGroup(self.GroupID,self.MenuText,self.MenuParentPath,self.MenuCallHandler) end return self end function MENU_GROUP_COMMAND:Remove(MenuStamp,MenuTag) MENU_INDEX:PrepareGroup(self.Group) local Path=MENU_INDEX:ParentPath(self.ParentMenu,self.MenuText) local GroupMenu=MENU_INDEX:HasGroupMenu(self.Group,Path) if GroupMenu==self then if not MenuStamp or self.MenuStamp~=MenuStamp then if(not MenuTag)or(MenuTag and self.MenuTag and MenuTag==self.MenuTag)then if self.MenuPath~=nil then self:F({Group=self.GroupID,Text=self.MenuText,Path=self.MenuPath}) missionCommands.removeItemForGroup(self.GroupID,self.MenuPath) end MENU_INDEX:ClearGroupMenu(self.Group,Path) self:ClearParentMenu(self.MenuText) return nil end end else BASE:E({"Cannot Remove MENU_GROUP_COMMAND",Path=Path,ParentMenu=self.ParentMenu,MenuText=self.MenuText,Group=self.Group}) end return self end end do MENU_GROUP_DELAYED={ ClassName="MENU_GROUP_DELAYED" } function MENU_GROUP_DELAYED:New(Group,MenuText,ParentMenu) MENU_INDEX:PrepareGroup(Group) local Path=MENU_INDEX:ParentPath(ParentMenu,MenuText) local GroupMenu=MENU_INDEX:HasGroupMenu(Group,Path) if GroupMenu then return GroupMenu else self=BASE:Inherit(self,MENU_BASE:New(MenuText,ParentMenu)) MENU_INDEX:SetGroupMenu(Group,Path,self) self.Group=Group self.GroupID=Group:GetID() if self.MenuParentPath then self.MenuPath=UTILS.DeepCopy(self.MenuParentPath) else self.MenuPath={} end table.insert(self.MenuPath,self.MenuText) self:SetParentMenu(self.MenuText,self) return self end end function MENU_GROUP_DELAYED:Set() if not self.GroupID then return end do if not self.MenuSet then missionCommands.addSubMenuForGroup(self.GroupID,self.MenuText,self.MenuParentPath) self.MenuSet=true end for MenuText,Menu in pairs(self.Menus or{})do Menu:Set() end end end function MENU_GROUP_DELAYED:Refresh() do missionCommands.removeItemForGroup(self.GroupID,self.MenuPath) missionCommands.addSubMenuForGroup(self.GroupID,self.MenuText,self.MenuParentPath) for MenuText,Menu in pairs(self.Menus or{})do Menu:Refresh() end end return self end function MENU_GROUP_DELAYED:RemoveSubMenus(MenuStamp,MenuTag) for MenuText,Menu in pairs(self.Menus or{})do Menu:Remove(MenuStamp,MenuTag) end self.Menus=nil end function MENU_GROUP_DELAYED:Remove(MenuStamp,MenuTag) MENU_INDEX:PrepareGroup(self.Group) local Path=MENU_INDEX:ParentPath(self.ParentMenu,self.MenuText) local GroupMenu=MENU_INDEX:HasGroupMenu(self.Group,Path) if GroupMenu==self then self:RemoveSubMenus(MenuStamp,MenuTag) if not MenuStamp or self.MenuStamp~=MenuStamp then if(not MenuTag)or(MenuTag and self.MenuTag and MenuTag==self.MenuTag)then if self.MenuPath~=nil then self:F({Group=self.GroupID,Text=self.MenuText,Path=self.MenuPath}) missionCommands.removeItemForGroup(self.GroupID,self.MenuPath) end MENU_INDEX:ClearGroupMenu(self.Group,Path) self:ClearParentMenu(self.MenuText) return nil end end else BASE:E({"Cannot Remove MENU_GROUP_DELAYED",Path=Path,ParentMenu=self.ParentMenu,MenuText=self.MenuText,Group=self.Group}) return nil end return self end MENU_GROUP_COMMAND_DELAYED={ ClassName="MENU_GROUP_COMMAND_DELAYED" } function MENU_GROUP_COMMAND_DELAYED:New(Group,MenuText,ParentMenu,CommandMenuFunction,...) MENU_INDEX:PrepareGroup(Group) local Path=MENU_INDEX:ParentPath(ParentMenu,MenuText) local GroupMenu=MENU_INDEX:HasGroupMenu(Group,Path) if GroupMenu then GroupMenu:SetCommandMenuFunction(CommandMenuFunction) GroupMenu:SetCommandMenuArguments(arg) return GroupMenu else self=BASE:Inherit(self,MENU_COMMAND_BASE:New(MenuText,ParentMenu,CommandMenuFunction,arg)) MENU_INDEX:SetGroupMenu(Group,Path,self) self.Group=Group self.GroupID=Group:GetID() if self.MenuParentPath then self.MenuPath=UTILS.DeepCopy(self.MenuParentPath) else self.MenuPath={} end table.insert(self.MenuPath,self.MenuText) self:SetParentMenu(self.MenuText,self) return self end end function MENU_GROUP_COMMAND_DELAYED:Set() do if not self.MenuSet then self.MenuPath=missionCommands.addCommandForGroup(self.GroupID,self.MenuText,self.MenuParentPath,self.MenuCallHandler) self.MenuSet=true end end end function MENU_GROUP_COMMAND_DELAYED:Refresh() do missionCommands.removeItemForGroup(self.GroupID,self.MenuPath) missionCommands.addCommandForGroup(self.GroupID,self.MenuText,self.MenuParentPath,self.MenuCallHandler) end return self end function MENU_GROUP_COMMAND_DELAYED:Remove(MenuStamp,MenuTag) MENU_INDEX:PrepareGroup(self.Group) local Path=MENU_INDEX:ParentPath(self.ParentMenu,self.MenuText) local GroupMenu=MENU_INDEX:HasGroupMenu(self.Group,Path) if GroupMenu==self then if not MenuStamp or self.MenuStamp~=MenuStamp then if(not MenuTag)or(MenuTag and self.MenuTag and MenuTag==self.MenuTag)then if self.MenuPath~=nil then self:F({Group=self.GroupID,Text=self.MenuText,Path=self.MenuPath}) missionCommands.removeItemForGroup(self.GroupID,self.MenuPath) end MENU_INDEX:ClearGroupMenu(self.Group,Path) self:ClearParentMenu(self.MenuText) return nil end end else BASE:E({"Cannot Remove MENU_GROUP_COMMAND_DELAYED",Path=Path,ParentMenu=self.ParentMenu,MenuText=self.MenuText,Group=self.Group}) end return self end end ZONE_BASE={ ClassName="ZONE_BASE", ZoneName="", ZoneProbability=1, DrawID=nil, Color={}, ZoneID=nil, Properties={}, Surface=nil, Checktime=5, } function ZONE_BASE:New(ZoneName) local self=BASE:Inherit(self,FSM:New()) self.ZoneName=ZoneName return self end function ZONE_BASE:GetName() return self.ZoneName end function ZONE_BASE:SetName(ZoneName) self.ZoneName=ZoneName end function ZONE_BASE:IsVec2InZone(Vec2) return false end function ZONE_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_BASE:IsCoordinateInZone(Coordinate) if not Coordinate then return false end local InZone=self:IsVec2InZone(Coordinate:GetVec2()) return InZone end function ZONE_BASE:IsPointVec2InZone(Coordinate) local InZone=self:IsVec2InZone(Coordinate:GetVec2()) return InZone end function ZONE_BASE:IsPointVec3InZone(PointVec3) local InZone=self:IsPointVec2InZone(PointVec3) return InZone end function ZONE_BASE:GetVec2() return nil end function ZONE_BASE:GetPointVec2() local Vec2=self:GetVec2() local PointVec2=COORDINATE:NewFromVec2(Vec2) return PointVec2 end function ZONE_BASE:GetVec3(Height) Height=Height or 0 local Vec2=self:GetVec2() local Vec3={x=Vec2.x,y=Height and Height or land.getHeight(self:GetVec2()),z=Vec2.y} return Vec3 end function ZONE_BASE:GetPointVec3(Height) local Vec3=self:GetVec3(Height) local PointVec3=COORDINATE:NewFromVec3(Vec3) return PointVec3 end function ZONE_BASE:GetCoordinate(Height) local Vec3=self:GetVec3(Height) if self.Coordinate then self.Coordinate.x=Vec3.x self.Coordinate.y=Vec3.y self.Coordinate.z=Vec3.z else self.Coordinate=COORDINATE:NewFromVec3(Vec3) end return self.Coordinate end function ZONE_BASE:Get2DDistance(Coordinate) local a=self:GetVec2() local b={} if Coordinate.z then b.x=Coordinate.x b.y=Coordinate.z else b.x=Coordinate.x b.y=Coordinate.y end local dist=UTILS.VecDist2D(a,b) return dist end function ZONE_BASE:GetRandomVec2() return nil end function ZONE_BASE:GetRandomPointVec2() return nil end function ZONE_BASE:GetRandomPointVec3() return nil end function ZONE_BASE:GetBoundingSquare() return nil end function ZONE_BASE:GetSurfaceType() local coord=self:GetCoordinate() local surface=coord:GetSurfaceType() return surface end function ZONE_BASE:BoundZone() end function ZONE_BASE:SetDrawCoalition(Coalition) self.drawCoalition=Coalition or-1 return self end function ZONE_BASE:GetDrawCoalition() return self.drawCoalition or-1 end function ZONE_BASE:SetColor(RGBcolor,Alpha) RGBcolor=RGBcolor or{1,0,0} Alpha=Alpha or 0.15 self.Color={} self.Color[1]=RGBcolor[1] self.Color[2]=RGBcolor[2] self.Color[3]=RGBcolor[3] self.Color[4]=Alpha return self end function ZONE_BASE:GetColor() return self.Color or{1,0,0,0.15} end function ZONE_BASE:GetColorRGB() local rgb={} local Color=self:GetColor() rgb[1]=Color[1] rgb[2]=Color[2] rgb[3]=Color[3] return rgb end function ZONE_BASE:GetColorAlpha() local Color=self:GetColor() local alpha=Color[4] return alpha end function ZONE_BASE:SetFillColor(RGBcolor,Alpha) RGBcolor=RGBcolor or{1,0,0} Alpha=Alpha or 0.15 self.FillColor={} self.FillColor[1]=RGBcolor[1] self.FillColor[2]=RGBcolor[2] self.FillColor[3]=RGBcolor[3] self.FillColor[4]=Alpha return self end function ZONE_BASE:GetFillColor() return self.FillColor or{1,0,0,0.15} end function ZONE_BASE:GetFillColorRGB() local rgb={} local FillColor=self:GetFillColor() rgb[1]=FillColor[1] rgb[2]=FillColor[2] rgb[3]=FillColor[3] return rgb end function ZONE_BASE:GetFillColorAlpha() local FillColor=self:GetFillColor() local alpha=FillColor[4] return alpha end function ZONE_BASE:UndrawZone(Delay) if Delay and Delay>0 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:FindNearestCoordinateOnRadius(Outsidecoordinate) local Vec1=self:GetVec2() local Radius=self:GetRadius() local Vec2=Outsidecoordinate:GetVec2() local Point=UTILS.FindNearestPointOnCircle(Vec1,Radius,Vec2) local rc=COORDINATE:NewFromVec2(Point) return rc 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:SetPartlyInside(state) self.PartlyInside=state or not(state==false) 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 if self.PartlyInside and obj.ClassName=="GROUP"then inzone=obj:IsAnyInZone(self) else inzone=self:IsCoordinateInZone(obj:GetCoordinate()) end 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():SetAlt() local ZoneRadius=self:GetRadius() local SphereSearch={ id=world.VolumeType.SPHERE, params={ point=ZoneCoord:GetVec3(), radius=ZoneRadius, } } local function EvaluateZone(ZoneObject) if ZoneObject and self:IsVec3InZone(ZoneObject:getPoint())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 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(Coalition) self.SetUnit=self.SetUnit or SET_UNIT:New() self.SetUnit:Clear(false) self.SetUnit.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()) local FoundCoalition=FoundUnit and FoundUnit:GetCoalition()or nil local includeoncoalition=true if Coalition~=nil and FoundCoalition==Coalition then includeoncoalition=true else includeoncoalition=false end if Coalition==nil then includeoncoalition=true end if FoundUnit and includeoncoalition then self.SetUnit:AddUnit(FoundUnit) else local FoundStatic=STATIC:FindByName(UnitObject:getName(),false) if FoundStatic then self.SetUnit:AddUnit(FoundStatic) end end end end end return self.SetUnit end function ZONE_RADIUS:GetScannedSetGroup(Coalition) self.ScanSetGroup=self.ScanSetGroup or SET_GROUP:New() self.ScanSetGroup:Clear(false) 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()) local FoundCoalition=FoundUnit and FoundUnit:GetCoalition()or nil local includeoncoalition=true if Coalition~=nil and FoundCoalition==Coalition then includeoncoalition=true else includeoncoalition=false end if Coalition==nil then includeoncoalition=true end if FoundUnit and includeoncoalition 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:GetClearZonePositions(PosRadius,NumPositions) return UTILS.GetClearZonePositions(self,PosRadius,NumPositions) end function ZONE_RADIUS:GetRandomClearZoneCoordinate(PosRadius,NumPositions) return UTILS.GetRandomClearZoneCoordinate(self,PosRadius,NumPositions) end function ZONE_RADIUS:GetRandomVec2(inner,outer,surfacetypes) local Vec2=self:GetVec2() local _inner=inner or 0 local _outer=outer or self:GetRadius() math.random() math.random() math.random() 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:GetClearZonePositions(PosRadius,NumPositions) return UTILS.GetClearZonePositions(self,PosRadius,NumPositions) end function ZONE_POLYGON_BASE:GetRandomClearZoneCoordinate(PosRadius,NumPositions) return UTILS.GetRandomClearZoneCoordinate(self,PosRadius,NumPositions) 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() math.random() math.random() math.random() 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={}, 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.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 rotation=UTILS.ToRadian(objectData.angle or 0) local sinRot=math.sin(rotation) local cosRot=math.cos(rotation) local dx=h/2 local dy=w/2 local points={ {x=-dx*cosRot-(-dy*sinRot)+vec2.x,y=-dx*sinRot+(-dy*cosRot)+vec2.y}, {x=dx*cosRot-(-dy*sinRot)+vec2.x,y=dx*sinRot+(-dy*cosRot)+vec2.y}, {x=dx*cosRot-(dy*sinRot)+vec2.x,y=dx*sinRot+(dy*cosRot)+vec2.y}, {x=-dx*cosRot-(dy*sinRot)+vec2.x,y=-dx*sinRot+(dy*cosRot)+vec2.y}, } 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 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) if type(GroupName)~="string"or GroupName==""then return end 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:T("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:T("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:T("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:T("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:T("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' (%s) of group '%s'",tostring(PlayerName),tostring(Event.IniDCSUnitName),tostring(Event.IniTypeName),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.IniUnit and Event.IniUnit: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: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, FilterCoalitionNumbers={ [coalition.side.RED+1]="red", [coalition.side.BLUE+1]="blue", [coalition.side.NEUTRAL+1]="neutral", }, FilterMeta={ Coalitions={ ["red"]=coalition.side.RED, ["blue"]=coalition.side.BLUE, ["neutral"]=coalition.side.NEUTRAL, }, }, filterNoRegex=false, filterReplaceDash=true, } 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:FilterCoalitions(Coalitions,Clear) if Clear or(not self.Filter.Coalitions)then self.Filter.Coalitions={} end if type(Coalitions)~="table"then Coalitions={Coalitions}end for CoalitionID,Coalition in pairs(Coalitions)do local coalition=Coalition if type(Coalition)=="number"then coalition=self.FilterCoalitionNumbers[Coalition+1]or"unknown" end self.Filter.Coalitions[coalition]=coalition end return self end function SET_BASE:_Find(ObjectName) local ObjectFound=self.Set[ObjectName] return ObjectFound end function SET_BASE:_SearchPattern(Name,Pattern,NoRegex,ReplaceDash) NoRegex=NoRegex or self.filterNoRegex ReplaceDash=ReplaceDash or self.filterReplaceDash if ReplaceDash==true and NoRegex~=true then Pattern=Pattern:gsub("-","%%-") end local contain=string.find(Name,Pattern,1,NoRegex) return contain end function SET_BASE:FilterSetRegex(NoRegex,ReplaceDash) if NoRegex~=nil then self.filterNoRegex=NoRegex end self.filterReplaceDash=ReplaceDash or true return self 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 if Object==nil then return false end local name=(Object~=nil and Object.GetName)and Object:GetName()or"none" 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:IsInZone(Zone,Any) for ObjectName,Object in pairs(self.Set)do local object=Object local inzone=object:IsInZone(Zone) if inzone and Any then return true elseif not inzone then return false end end return true end function SET_BASE:Flush(MasterObject) local ObjectNames="" for ObjectName,Object in pairs(self.Set)do ObjectNames=ObjectNames..ObjectName..", " end return ObjectNames end function SET_BASE:GetAliveSet() local AliveSet={} for ObjectName,Object in pairs(self.Set)do if Object then if Object:IsAlive()then AliveSet[#AliveSet+1]=Object end end end return AliveSet or{} 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={} for GroupName,GroupObject in pairs(self.Set)do local GroupObject=GroupObject if GroupObject then if GroupObject:IsAlive()then AliveSet[GroupName]=GroupObject end end end return AliveSet 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 self:_SearchPattern(MUnit:GetName(),UnitPrefix,self.filterNoRegex,self.filterReplaceDash)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: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 self:_SearchPattern(MStatic:GetName(),StaticPrefix,self.filterNoRegex,self.filterReplaceDash)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 self:_SearchPattern(MZoneName,ZonePrefix,self.filterNoRegex,self.filterReplaceDash)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 self:_SearchPattern(MSceneryName,ZonePrefix,self.filterNoRegex,self.filterReplaceDash)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", 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 self:_SearchPattern(DCargo:GetName(),StaticPrefix,self.filterNoRegex,self.filterReplaceDash)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: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 self:_SearchPattern(cargo.Owner,PlayerName,self.filterNoRegex,self.filterReplaceDash)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:ScanObjectsSquare(sideLength,scanunits,scanstatics,scanscenery) self:F(string.format("Scanning cube volume (lower-left corner) with side length %.1f m.",sideLength)) local CornerVec3=self:GetVec3() local CenterY=CornerVec3.y local MinVec3={ x=CornerVec3.x, y=CenterY-(sideLength/2), z=CornerVec3.z } local MaxVec3={ x=CornerVec3.x+sideLength, y=CenterY+(sideLength/2), z=CornerVec3.z+sideLength } local BoxSearch={ id=world.VolumeType.BOX, params={ min=MinVec3, max=MaxVec3, } } if scanunits==nil then scanunits=true end if scanstatics==nil then scanstatics=true end if scanscenery==nil then scanscenery=false end local scanobjects={} if scanunits then table.insert(scanobjects,Object.Category.UNIT) end if scanstatics then table.insert(scanobjects,Object.Category.STATIC) end if scanscenery then table.insert(scanobjects,Object.Category.SCENERY) end local Units={} local Statics={} local Scenery={} local gotstatics=false local gotunits=false local gotscenery=false local function EvaluateZone(ZoneObject) if ZoneObject then local ObjectCategory=ZoneObject:getCategory() if(ObjectCategory==Object.Category.UNIT and ZoneObject:isExist())then table.insert(Units,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,BoxSearch,EvaluateZone) for _,unit in pairs(Units)do if not unit:isExist()then gotunits=false end end return gotunits,gotstatics,gotscenery,Units,Statics,Scenery 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:GetLandProfileVec3(Destination) return land.profile(self:GetVec3(),Destination) end function COORDINATE:GetLandProfileCoordinates(Destination) local points=self:GetLandProfileVec3(Destination:GetVec3()) local coords={} for _,point in ipairs(points)do table.insert(coords,COORDINATE:NewFromVec3(point)) end return coords 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 RoutePoint.airdromeId=airbase:IsAirdrome()and AirbaseID or nil 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:GetPathlineOnRoad(ToCoord,IncludeEndpoints,Railroad) local RoadType="roads" if Railroad==true then RoadType="railroads" end local path=land.findPathOnRoads(RoadType,self.x,self.z,ToCoord.x,ToCoord.z) if IncludeEndpoints then path=path or{} table.insert(path,1,self:GetVec2()) table.insert(path,ToCoord:GetVec2()) end local pathline=nil if path then pathline=PATHLINE:NewFromVec2Array(RoadType,path) end return pathline 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,Duration,Delay,Name,Offset,Direction,Distance) self:F2({SmokeColor,Name,Duration,Delay,Offset}) SmokeColor=SmokeColor or SMOKECOLOR.Green if Delay and Delay>0 then self:ScheduleOnce(Delay,COORDINATE.Smoke,self,SmokeColor,Duration,0,Name,Direction,Distance) else self.firename=Name or"Smoke-"..math.random(1,100000) if Offset or self.SmokeOffset then local Angle=Direction or self:GetSmokeOffsetDirection() local Distance=Distance or self:GetSmokeOffsetDistance() local newpos=self:Translate(Distance,Angle,true,false) local newvec3=newpos:GetVec3() trigger.action.smoke(newvec3,SmokeColor,self.firename) else trigger.action.smoke(self:GetVec3(),SmokeColor,self.firename) end if Duration and Duration>0 then self:ScheduleOnce(Duration,COORDINATE.StopSmoke,self,self.firename) end end return self end function COORDINATE:GetSmokeOffsetDirection() local direction=self.SmokeOffsetDirection or math.random(1,359) return direction end function COORDINATE:SetSmokeOffsetDirection(Direction) if self then self.SmokeOffsetDirection=Direction or math.random(1,359) return self else COORDINATE.SmokeOffsetDirection=Direction or math.random(1,359) end end function COORDINATE:GetSmokeOffsetDistance() local distance=self.SmokeOffsetDistance or math.random(10,20) return distance end function COORDINATE:SetSmokeOffsetDistance(Distance) if self then self.SmokeOffsetDistance=Distance or math.random(10,20) return self else COORDINATE.SmokeOffsetDistance=Distance or math.random(10,20) end end function COORDINATE:SwitchSmokeOffsetOn() if self then self.SmokeOffset=true return self else COORDINATE.SmokeOffset=true end end function COORDINATE:SwitchSmokeOffsetOff() if self then self.SmokeOffset=false return self else COORDINATE.SmokeOffset=false end end function COORDINATE:StopSmoke(name) self:StopBigSmokeAndFire(name) end function COORDINATE:SmokeGreen(Duration,Delay) self:Smoke(SMOKECOLOR.Green,Duration,Delay) return self end function COORDINATE:SmokeRed(Duration,Delay) self:Smoke(SMOKECOLOR.Red,Duration,Delay) return self end function COORDINATE:SmokeWhite(Duration,Delay) self:Smoke(SMOKECOLOR.White,Duration,Delay) return self end function COORDINATE:SmokeOrange(Duration,Delay) self:Smoke(SMOKECOLOR.Orange,Duration,Delay) return self end function COORDINATE:SmokeBlue(Duration,Delay) self:Smoke(SMOKECOLOR.Blue,Duration,Delay) return self end function COORDINATE:BigSmokeAndFire(Preset,Density,Duration,Delay,Name) self:F2({preset=Preset,density=Density}) Preset=Preset or BIGSMOKEPRESET.SmallSmokeAndFire Density=Density or 0.5 if Delay and Delay>0 then self:ScheduleOnce(Delay,COORDINATE.BigSmokeAndFire,self,Preset,Density,Duration,0,Name) else self.firename=Name or"Fire-"..math.random(1,10000) trigger.action.effectSmokeBig(self:GetVec3(),Preset,Density,self.firename) if Duration and Duration>0 then self:ScheduleOnce(Duration,COORDINATE.StopBigSmokeAndFire,self,self.firename) end end return self end function COORDINATE:StopBigSmokeAndFire(name) name=name or self.firename trigger.action.effectSmokeStop(name) end function COORDINATE:BigSmokeAndFireSmall(Density,Duration,Delay,Name) self:BigSmokeAndFire(BIGSMOKEPRESET.SmallSmokeAndFire,Density,Duration,Delay,Name) return self end function COORDINATE:BigSmokeAndFireMedium(Density,Duration,Delay,Name) self:BigSmokeAndFire(BIGSMOKEPRESET.MediumSmokeAndFire,Density,Duration,Delay,Name) return self end function COORDINATE:BigSmokeAndFireLarge(Density,Duration,Delay,Name) self:BigSmokeAndFire(BIGSMOKEPRESET.LargeSmokeAndFire,Density,Duration,Delay,Name) return self end function COORDINATE:BigSmokeAndFireHuge(Density,Duration,Delay,Name) self:BigSmokeAndFire(BIGSMOKEPRESET.HugeSmokeAndFire,Density,Duration,Delay,Name) return self end function COORDINATE:BigSmokeSmall(Density,Duration,Delay,Name) self:BigSmokeAndFire(BIGSMOKEPRESET.SmallSmoke,Density,Duration,Delay,Name) return self end function COORDINATE:BigSmokeMedium(Density,Duration,Delay,Name) self:BigSmokeAndFire(BIGSMOKEPRESET.MediumSmoke,Density,Duration,Delay,Name) return self end function COORDINATE:BigSmokeLarge(Density,Duration,Delay,Name) self:BigSmokeAndFire(BIGSMOKEPRESET.LargeSmoke,Density,Duration,Delay,Name) return self end function COORDINATE:BigSmokeHuge(Density,Duration,Delay,Name) self:BigSmokeAndFire(BIGSMOKEPRESET.HugeSmoke,Density,Duration,Delay,Name) return self 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 type(sunrise)=="string"or type(sunset)=="string"then if sunrise=="N/R"then return false end if sunset=="N/S"then return true end 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) if type(sunrise)=="string"or type(sunset)=="string"then if sunrise=="N/R"then return false end if sunset=="N/S"then return true end end 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:ToStringBearing(FromCoordinate,Settings,MagVar,Precision) local DirectionVec3=FromCoordinate:GetDirectionVec3(self) local AngleRadians=self:GetAngleRadians(DirectionVec3) return self:GetBearingText(AngleRadians,Precision,Settings,MagVar) 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 function COORDINATE:GetSimpleZones(SearchRadius,PosRadius,NumPositions) local clearPositions=UTILS.GetSimpleZones(self:GetVec3(),SearchRadius,PosRadius,NumPositions) if clearPositions and#clearPositions>0 then local coords={} for _,pos in pairs(clearPositions)do local coord=COORDINATE:NewFromVec2(pos) table.insert(coords,coord) end return coords end return nil 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:ToSet(Set,Settings) for _,_obj in pairs(Set:GetSetObjects()or{})do if _obj and _obj:IsAlive()then if _obj:IsInstanceOf("SET_GROUP")then self:ToGroup(_obj,Settings) elseif _obj:IsInstanceOf("SET_CLIENT")or _obj:IsInstanceOf("SET_UNIT")then self:ToUnit(_obj,Settings) end end end return self end function MESSAGE:ToSetIf(Set,Condition,Settings) if Set and Condition==true then self:ToSet(Set,Settings) end return self end function MESSAGE:ToGroup(Group,Settings) self:F(Group.GroupName) if Group and Group:IsAlive()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 and Unit:IsAlive()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,Provider,Speaker) _MESSAGESRS.PathToSRS=PathToSRS or MSRS.path or"C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio" _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 if Provider then _MESSAGESRS.MSRS:SetProvider(Provider) 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 if Speaker then _MESSAGESRS.MSRS:SetSpeakerPiper(Speaker)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:_ClearFSMEvent(EventName) if self._EventSchedules[EventName]then self.CallScheduler:Remove(self._EventSchedules[EventName]) self._EventSchedules[EventName]=nil 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:InitValidateAndRepositionGroundUnits(OnOff,MaxRadius,Spacing) self.SpawnValidateAndRepositionGroundUnits=OnOff self.SpawnValidateAndRepositionGroundUnitsRadius=MaxRadius self.SpawnValidateAndRepositionGroundUnitsSpacing=Spacing 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) if not SpawnZoneTable then return self end 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:InitCallSignRed(ID) self.SpawnInitCallSign=true self.SpawnInitCallSignID=ID or 100 self.SpawnInitCallSignRED=true 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 if self.SpawnRandomizePosition then PointVec3=COORDINATE:New(SpawnTemplate.x,SpawnTemplate.route.points[1].alt,SpawnTemplate.y) end 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 if self.SpawnValidateAndRepositionGroundUnits then local units=SpawnTemplate.units local gPos={x=SpawnTemplate.x,y=SpawnTemplate.y} UTILS.ValidateAndRepositionGroundUnits(units,gPos,self.SpawnValidateAndRepositionGroundUnitsRadius,self.SpawnValidateAndRepositionGroundUnitsSpacing) 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 local useexplicitspots=false 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 Parkingdata~=nil then nfree=#Parkingdata spots=Parkingdata useexplicitspots=true elseif autoparking and AirbaseCategory==Airbase.Category.HELIPAD and ishelo then if termtype==nil then spots=SpawnAirbase:FindFreeParkingSpotForAircraft(group,AIRBASE.TerminalType.HelicopterOnly,scanradius,scanunits,scanstatics,scanscenery,verysafe,nunits,nil) nfree=#spots if nfree=nunits then useexplicitspots=true else nfree=SpawnAirbase:GetFreeParkingSpotsNumber(termtype,true) spots=SpawnAirbase:GetFreeParkingSpotsTable(termtype,true) end elseif autoparking then nfree=SpawnAirbase:GetFreeParkingSpotsNumber(termtype,true) spots=SpawnAirbase:GetFreeParkingSpotsTable(termtype,true) 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 useexplicitspots and parkingspots[1]then PointVec3=parkingspots[1] 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 spawnonground=false 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 and not useexplicitspots 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 local useexplicitspots=false if Parkingdata~=nil then nfree=#Parkingdata spots=Parkingdata useexplicitspots=true elseif spawnonfarp and ishelo then if termtype==nil then spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup,AIRBASE.TerminalType.HelicopterOnly,scanradius,scanunits,scanstatics,scanscenery,verysafe,nunits,nil) nfree=#spots if nfree=nunits then useexplicitspots=true else nfree=SpawnAirbase:GetFreeParkingSpotsNumber(termtype,true) spots=SpawnAirbase:GetFreeParkingSpotsTable(termtype,true) end elseif 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.5", debug=false, Casesensitive=true, } function MARKEROPS_BASE:New(Tagname,Keywords) 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 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 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 coord=COORDINATE:NewFromVec3({y=Event.pos.y,x=Event.pos.x,z=Event.pos.z}) 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 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 coord=COORDINATE:NewFromVec3({y=Event.pos.y,x=Event.pos.x,z=Event.pos.z}) if self.debug then local coordtext=coord:ToStringLLDDM() local text=tostring(Event.text) local m=MESSAGE:New(string.format("Mark changed at %s with text: %s",coordtext,text),10,"Info",false):ToAll() end 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 self.Casesensitive==false then local type=string.lower(self.Tag) if string.find(string.lower(Eventtext),type,1,true)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 function MARKEROPS_BASE:SwitchCaseSensitiveOff() self.Casesensitive=false return self end function MARKEROPS_BASE:SwitchCaseSensitiveOn() self.Casesensitive=true return self 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.2.0" 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:GetLength(Project2D) local l=0 local np=#self.points for i=1,np-1 do local p1=self.points[i] local p2=self.points[i+1] if Project2D then l=l+UTILS.VecDist2D(p1.vec2,p2.vec2) else l=l+UTILS.VecDist3D(p1.vec3,p2.vec3) end end return l 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) 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 return self end function PATHLINE:DrawLine(Recipient,Color,LineType) Recipient=Recipient or-1 Color=Color or{1,0,0,1.0} LineType=LineType or 1 local ReadOnly=false local np=#self.points for i=1,np-1 do local p1=self.points[i] local p2=self.points[i+1] p1.lineID=UTILS.GetMarkID() trigger.action.lineToAll(Recipient,p1.lineID,p1.vec3,p2.vec3,Color,LineType,ReadOnly,"") end return self end function PATHLINE:UnDrawLine(Delay) local np=#self.points for _,_point in pairs(self.points)do local p=_point if p.lineID then UTILS.RemoveMark(p.lineID,Delay) end end return self 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 CLIENTMENU={ ClassName="CLIENTMENU", lid="", version="0.1.3", name=nil, path=nil, group=nil, client=nil, GroupID=nil, Children={}, Once=false, Generic=false, debug=false, Controller=nil, groupname=nil, active=false, } CLIENTMENU_ID=0 function CLIENTMENU:NewEntry(Client,Text,Parent,Function,...) local self=BASE:Inherit(self,BASE:New()) CLIENTMENU_ID=CLIENTMENU_ID+1 self.ID=CLIENTMENU_ID if Client then self.group=Client:GetGroup() self.client=Client self.GroupID=self.group:GetID() self.groupname=self.group:GetName()or"Unknown Groupname" else self.Generic=true end self.name=Text or"unknown entry" if Parent then if Parent:IsInstanceOf("MENU_BASE")then self.parentpath=Parent.MenuPath else self.parentpath=Parent:GetPath() Parent:AddChild(self) end end self.Parent=Parent self.Function=Function self.Functionargs=arg or{} table.insert(self.Functionargs,self.group) table.insert(self.Functionargs,self.client) if self.Functionargs and self.debug then self:T({"Functionargs",self.Functionargs}) end if not self.Generic and self.active==false then if Function~=nil then local ErrorHandler=function(errmsg) env.info("MOOSE Error in CLIENTMENU COMMAND function: "..errmsg) if BASE.Debug~=nil then env.info(BASE.Debug.traceback()) end return errmsg end self.CallHandler=function() local function MenuFunction() return self.Function(unpack(self.Functionargs)) end local Status,Result=xpcall(MenuFunction,ErrorHandler) if self.Once==true then self:Clear() end end self.path=missionCommands.addCommandForGroup(self.GroupID,Text,self.parentpath,self.CallHandler) self.active=true else self.path=missionCommands.addSubMenuForGroup(self.GroupID,Text,self.parentpath) self.active=true end else if self.parentpath then self.path=UTILS.DeepCopy(self.parentpath) else self.path={} end self.path[#self.path+1]=Text end self.UUID=table.concat(self.path,";") self:T({self.UUID}) self.Once=false self.lid=string.format("CLIENTMENU %s | %s | ",self.ID,self.name) self:T(self.lid.."Created") return self end function CLIENTMENU:CreateUUID(Parent,Text) local path={} if Parent and Parent.path then path=Parent.path end path[#path+1]=Text local UUID=table.concat(path,";") return UUID end function CLIENTMENU:SetController(Controller) self.Controller=Controller return self end function CLIENTMENU:SetOnce() self:T(self.lid.."SetOnce") self.Once=true return self end function CLIENTMENU:RemoveF10() self:T(self.lid.."RemoveF10") if self.GroupID then local function RemoveFunction() return missionCommands.removeItemForGroup(self.GroupID,self.path) end local status,err=pcall(RemoveFunction) if not status then self:I(string.format("**** Error Removing Menu Entry %s for %s!",tostring(self.name),self.groupname)) end self.active=false end return self end function CLIENTMENU:GetPath() self:T(self.lid.."GetPath") return self.path end function CLIENTMENU:GetUUID() self:T(self.lid.."GetUUID") return self.UUID end function CLIENTMENU:AddChild(Child) self:T(self.lid.."AddChild "..Child.ID) table.insert(self.Children,Child.ID,Child) return self end function CLIENTMENU:RemoveChild(Child) self:T(self.lid.."RemoveChild "..Child.ID) table.remove(self.Children,Child.ID) return self end function CLIENTMENU:RemoveSubEntries() self:T(self.lid.."RemoveSubEntries") self:T({self.Children}) for _id,_entry in pairs(self.Children)do self:T("Removing ".._id) if _entry then _entry:RemoveSubEntries() _entry:RemoveF10() if _entry.Parent then _entry.Parent:RemoveChild(self) end end end return self end function CLIENTMENU:Clear() self:T(self.lid.."Clear") for _id,_entry in pairs(self.Children)do if _entry then _entry:RemoveSubEntries() _entry=nil end end self:RemoveF10() if self.Parent then self.Parent:RemoveChild(self) end return self end CLIENTMENUMANAGER={ ClassName="CLIENTMENUMANAGER", lid="", version="0.1.7", name=nil, clientset=nil, menutree={}, flattree={}, playertree={}, entrycount=0, rootentries={}, debug=true, PlayerMenu={}, Coalition=nil, } function CLIENTMENUMANAGER:New(ClientSet,Alias,Coalition) local self=BASE:Inherit(self,BASE:New()) self.clientset=ClientSet self.PlayerMenu={} self.name=Alias or"Nightshift" self.Coalition=Coalition or coalition.side.BLUE self.lid=string.format("CLIENTMENUMANAGER %s | %s | ",self.version,self.name) if self.debug then self:I(self.lid.."Created") end return self end function CLIENTMENUMANAGER:_EventHandler(EventData,Retry) self:T(self.lid.."_EventHandler: "..EventData.id) if EventData.id==EVENTS.PlayerLeaveUnit or EventData.id==EVENTS.Ejection or EventData.id==EVENTS.Crash or EventData.id==EVENTS.PilotDead then self:T(self.lid.."Leave event for player: "..tostring(EventData.IniPlayerName)) local Client=_DATABASE:FindClient(EventData.IniUnitName) if Client then self:ResetMenu(Client) end elseif(EventData.id==EVENTS.PlayerEnterAircraft)and EventData.IniCoalition==self.Coalition then if EventData.IniPlayerName and EventData.IniGroup then if(not self.clientset:IsIncludeObject(_DATABASE:FindClient(EventData.IniUnitName)))then self:T(self.lid.."Client not in SET: "..EventData.IniPlayerName) if not Retry then self:ScheduleOnce(2,CLIENTMENUMANAGER._EventHandler,self,EventData,true) end return self end local player=_DATABASE:FindClient(EventData.IniUnitName) self:Propagate(player) end elseif EventData.id==EVENTS.PlayerEnterUnit then local grp=GROUP:FindByName(EventData.IniGroupName) if grp:IsGround()then self:T(string.format("Player %s entered GROUND unit %s!",EventData.IniPlayerName,EventData.IniUnitName)) local IsPlayer=EventData.IniDCSUnit:getPlayerName() if IsPlayer then local client=_DATABASE.CLIENTS[EventData.IniDCSUnitName] if not client then self:I(string.format("Player '%s' joined ground unit '%s' of group '%s'",tostring(EventData.IniPlayerName),tostring(EventData.IniDCSUnitName),tostring(EventData.IniDCSGroupName))) client=_DATABASE:AddClient(EventData.IniDCSUnitName) client:AddPlayer(EventData.IniPlayerName) if not _DATABASE.PLAYERS[EventData.IniPlayerName]then _DATABASE:AddPlayer(EventData.IniUnitName,EventData.IniPlayerName) end local Settings=SETTINGS:Set(EventData.IniPlayerName) Settings:SetPlayerMenu(EventData.IniUnit) end self:Propagate(client) end end end return self end function CLIENTMENUMANAGER:InitAutoPropagation() self:HandleEvent(EVENTS.PlayerLeaveUnit,self._EventHandler) self:HandleEvent(EVENTS.Ejection,self._EventHandler) self:HandleEvent(EVENTS.Crash,self._EventHandler) self:HandleEvent(EVENTS.PilotDead,self._EventHandler) self:HandleEvent(EVENTS.PlayerEnterAircraft,self._EventHandler) self:HandleEvent(EVENTS.PlayerEnterUnit,self._EventHandler) self:SetEventPriority(6) return self end function CLIENTMENUMANAGER:NewEntry(Text,Parent,Function,...) self:T(self.lid.."NewEntry "..Text or"None") self.entrycount=self.entrycount+1 local entry=CLIENTMENU:NewEntry(nil,Text,Parent,Function,unpack(arg)) if not Parent then self.rootentries[self.entrycount]=entry end local depth=#entry.path if not self.menutree[depth]then self.menutree[depth]={}end table.insert(self.menutree[depth],entry.UUID) self.flattree[entry.UUID]=entry return entry end function CLIENTMENUMANAGER:EntryUUIDExists(UUID) local exists=self.flattree[UUID]and true or false return exists end function CLIENTMENUMANAGER:FindEntryByUUID(UUID) self:T(self.lid.."FindEntryByUUID "..UUID or"None") local entry=nil for _gid,_entry in pairs(self.flattree)do local Entry=_entry if Entry and Entry.UUID==UUID then entry=Entry end end return entry end function CLIENTMENUMANAGER:FindUUIDsByText(Text,Parent) self:T(self.lid.."FindUUIDsByText "..Text or"None") local matches={} local entries={} local n=0 for _uuid,_entry in pairs(self.flattree)do local Entry=_entry if Parent then if Entry and string.find(Entry.name,Text,1,true)and string.find(Entry.UUID,Parent.UUID,1,true)then table.insert(matches,_uuid) table.insert(entries,Entry) n=n+1 end else if Entry and string.find(Entry.name,Text,1,true)then table.insert(matches,_uuid) table.insert(entries,Entry) n=n+1 end end end return matches,entries,n end function CLIENTMENUMANAGER:FindEntriesByText(Text,Parent) self:T(self.lid.."FindEntriesByText "..Text or"None") local matches,objects,number=self:FindUUIDsByText(Text,Parent) return objects,number end function CLIENTMENUMANAGER:FindUUIDsByParent(Parent) self:T(self.lid.."FindUUIDsByParent") local matches={} local entries={} local n=0 for _uuid,_entry in pairs(self.flattree)do local Entry=_entry if Parent then if Entry and string.find(Entry.UUID,Parent.UUID,1,true)then table.insert(matches,_uuid) table.insert(entries,Entry) n=n+1 end end end return matches,entries,n end function CLIENTMENUMANAGER:FindEntriesByParent(Parent) self:T(self.lid.."FindEntriesByParent") local matches,objects,number=self:FindUUIDsByParent(Parent) return objects,number end function CLIENTMENUMANAGER:ChangeEntryText(Entry,Text,Client) self:T(self.lid.."ChangeEntryText "..Text or"None") local newentry=CLIENTMENU:NewEntry(nil,Text,Entry.Parent,Entry.Function,unpack(Entry.Functionargs)) self:DeleteF10Entry(Entry,Client) self:DeleteGenericEntry(Entry) if not Entry.Parent then self.rootentries[self.entrycount]=newentry end local depth=#newentry.path if not self.menutree[depth]then self.menutree[depth]={}end table.insert(self.menutree[depth],newentry.UUID) self.flattree[newentry.UUID]=newentry self:AddEntry(newentry,Client) return self end function CLIENTMENUMANAGER:Propagate(Client) self:T(self.lid.."Propagate") local knownunits={} local Set=self.clientset.Set if Client then Set={Client} end self:ResetMenu(Client) for _,_client in pairs(Set)do local client=_client if client and client:IsAlive()then local playerunit=client:GetName() local playername=client:GetPlayerName()or"none" if not knownunits[playerunit]then knownunits[playerunit]=true else self:I("Player in multi seat unit: "..playername) break end if not self.playertree[playername]then self.playertree[playername]={} end for level,branch in pairs(self.menutree)do self:T("Building branch:"..level) for _,leaf in pairs(branch)do self:T("Building leaf:"..leaf) local entry=self:FindEntryByUUID(leaf) if entry then self:T("Found generic entry:"..entry.UUID) local parent=nil if entry.Parent and entry.Parent.UUID then parent=self.playertree[playername][entry.Parent.UUID]or self:FindEntryByUUID(entry.Parent.UUID) end self.playertree[playername][entry.UUID]=CLIENTMENU:NewEntry(client,entry.name,parent,entry.Function,unpack(entry.Functionargs)) self.playertree[playername][entry.UUID].Once=entry.Once else self:T("NO generic entry for:"..leaf) end end end end end return self end function CLIENTMENUMANAGER:AddEntry(Entry,Client) self:T(self.lid.."AddEntry") local Set=self.clientset.Set local knownunits={} if Client then Set={Client} end for _,_client in pairs(Set)do local client=_client if client and client:IsAlive()then local playername=client:GetPlayerName()or"None" local unitname=client:GetName() if not knownunits[unitname]then knownunits[unitname]=true else self:I("Player in multi seat unit: "..playername) break end if Entry then self:T("Adding generic entry:"..Entry.UUID) local parent=nil if not self.playertree[playername]then self.playertree[playername]={} end if Entry.Parent and Entry.Parent.UUID then parent=self.playertree[playername][Entry.Parent.UUID]or self:FindEntryByUUID(Entry.Parent.UUID) end self.playertree[playername][Entry.UUID]=CLIENTMENU:NewEntry(client,Entry.name,parent,Entry.Function,unpack(Entry.Functionargs)) self.playertree[playername][Entry.UUID].Once=Entry.Once else self:T("NO generic entry given") end end end return self end function CLIENTMENUMANAGER:ResetMenu(Client) self:T(self.lid.."ResetMenu") for _,_entry in pairs(self.rootentries)do if _entry then self:DeleteF10Entry(_entry,Client) end end return self end function CLIENTMENUMANAGER:ResetMenuComplete() self:T(self.lid.."ResetMenuComplete") for _,_entry in pairs(self.rootentries)do if _entry then self:DeleteF10Entry(_entry) end end self.playertree=nil self.playertree={} self.rootentries=nil self.rootentries={} self.menutree=nil self.menutree={} return self end function CLIENTMENUMANAGER:DeleteEntry(Entry,Client) self:T(self.lid.."DeleteEntry") return self:DeleteF10Entry(Entry,Client) end function CLIENTMENUMANAGER:DeleteF10Entry(Entry,Client) self:T(self.lid.."DeleteF10Entry") local Set=self.clientset.Set if Client then Set={Client} end for _,_client in pairs(Set)do if _client and _client:IsAlive()then local playername=_client:GetPlayerName() if self.playertree[playername]then local centry=self.playertree[playername][Entry.UUID] if centry then centry:Clear() end end end end return self end function CLIENTMENUMANAGER:DeleteGenericEntry(Entry) self:T(self.lid.."DeleteGenericEntry") if Entry.Children and#Entry.Children>0 then self:RemoveGenericSubEntries(Entry) end local depth=#Entry.path local uuid=Entry.UUID local tbl=UTILS.DeepCopy(self.menutree) if tbl[depth]then for i=depth,#tbl do for _id,_uuid in pairs(tbl[i])do self:T(_uuid) if string.find(_uuid,uuid,1,true)or _uuid==uuid then self.menutree[i][_id]=nil self.flattree[_uuid]=nil end end end end return self end function CLIENTMENUMANAGER:RemoveGenericSubEntries(Entry) self:T(self.lid.."RemoveGenericSubEntries") local depth=#Entry.path+1 local uuid=Entry.UUID local tbl=UTILS.DeepCopy(self.menutree) if tbl[depth]then for i=depth,#tbl do self:T("Level = "..i) for _id,_uuid in pairs(tbl[i])do self:T(_uuid) if string.find(_uuid,uuid,1,true)then self:T("Match for ".._uuid) self.menutree[i][_id]=nil self.flattree[_uuid]=nil end end end end return self end function CLIENTMENUMANAGER:RemoveF10SubEntries(Entry,Client) self:T(self.lid.."RemoveSubEntries") local Set=self.clientset.Set if Client then Set={Client} end for _,_client in pairs(Set)do if _client and _client:IsAlive()then local playername=_client:GetPlayerName() if self.playertree[playername]then local centry=self.playertree[playername][Entry.UUID] centry:RemoveSubEntries() end end end return self end VECTOR={ ClassName="VECTOR", verbose=0, } VECTOR.version="0.1.0" _VECTORID=0 VECTOR.__index=VECTOR function VECTOR:New(x,y,z) if z==nil then self=setmetatable({x=x or 0,y=0,z=y or 0},VECTOR) else self=setmetatable({x=x or 0,y=y or 0,z=z or 0},VECTOR) end _VECTORID=_VECTORID+1 self.uid=_VECTORID return self end function VECTOR:NewFromVec(Vec) local vector=VECTOR:New(Vec.x,Vec.y,Vec.z) return vector end function VECTOR:NewFromPolar(r,phi) local Phi=math.rad(phi) local x=r*math.cos(phi) local y=r*math.sin(phi) local v=VECTOR:New(x,y) return self end function VECTOR:NewFromSpherical(r,theta,phi) local sinPhi=math.sin(math.rad(phi)) local cosPhi=math.cos(math.rad(phi)) local sinTheta=math.sin(math.rad(theta)) local cosTheta=math.cos(math.rad(theta)) local x=r*sinTheta*cosPhi local y=r*sinTheta*sinPhi local z=r*cosTheta local v=VECTOR:New(x,y,z) return self end function VECTOR:NewDirectionalVector(a,b) local x=b.x-a.x local y local z if a.z and b.z then y=b.y-a.y z=b.z-a.z elseif b.z then y=b.y-0 z=b.z-a.y elseif a.z then y=0-a.y z=b.y-a.z else y=b.y-a.y z=nil end local c=VECTOR:New(x,y,z) return c end function VECTOR:NewFromLLDD(Latitude,Longitude,Altitude) local vec3=coord.LLtoLO(Latitude,Longitude) self=VECTOR:NewFromVec(vec3) if Altitude then self.y=Altitude end return self end function VECTOR:NewFromLLDMS(Latitude,Longitude,Altitude) local lat=UTILS.LLDMSstringToDD(Latitude) local lon=UTILS.LLDMSstringToDD(Longitude) self=VECTOR:NewFromLLDD(lat,lon,Altitude) return self end function VECTOR:GetVec2() local vec={x=self.x,y=self.z} return vec end function VECTOR:GetVec3(OnSurface) local x=self.x local y=OnSurface and land.getHeight({x=self.x,y=self.z})or self.y local z=self.z local vec={x=x,y=y,z=z} return vec end function VECTOR:GetCoordinate(OnSurface) local vec3=self:GetVec3(OnSurface) local coordinate=COORDINATE:NewFromVec3(vec3) return coordinate end function VECTOR:GetDistance(Vector,Only2D) local dx=self.x-Vector.x local dy=0 local dz=0 if Vector.z then if not Only2D then dy=self.y-Vector.y end dz=self.z-Vector.z else dy=0 dz=self.z-Vector.y end local dist=math.sqrt(dx*dx+dy*dy+dz*dz) return dist end function VECTOR:GetDirectionalVectorTo(a) local x=a.x-self.x local y=0 local z=nil if a.z then y=a.y-self.y z=a.z-self.z else y=a.y-self.z z=nil end local c=VECTOR:New(x,y,z) return c end function VECTOR:GetDirectionalVectorFrom(a) local x=self.x-a.x local y local z if a.z then y=self.y-a.y z=self.z-a.z else y=self.z-a.y z=nil end local c=VECTOR:New(x,y,z) return c end function VECTOR:GetLength() local l=math.sqrt(self.x*self.x+self.y*self.y+self.z*self.z) return l end function VECTOR:GetHeading(To360) local heading=math.atan2(self.z,self.x) heading=math.deg(heading) if To360==nil or To360==true then heading=UTILS.AdjustHeading360(heading) else if heading==360.0 then heading=0.0 end end return heading end function VECTOR:GetHeadingTo(Vector) local a=self:GetDirectionalVectorTo(Vector) local heading=math.deg(math.atan2(a.z,a.x)) heading=UTILS.AdjustHeading360(heading) return heading end function VECTOR:GetHeadingFrom(Vector) local a=self:GetDirectionalVectorFrom(Vector) local heading=math.deg(math.atan2(a.z,a.x)) heading=UTILS.AdjustHeading360(heading) return heading end function VECTOR:GetLatitudeLongitude() local vec3=self:GetVec3() local latitude,longitude,altitude=coord.LOtoLL(vec3) return latitude,longitude end function VECTOR:GetMGRS() local lat,long=self:GetLatitudeLongitude() local mgrs=coord.LLtoMGRS(lat,long) return mgrs end function VECTOR:GetHeadingDelta(Vector) local h1=self:GetHeading(false) local h2=Vector:GetHeading(false) local delta=h2-h1 return delta end function VECTOR:GetIntermediateVector(Vector,Fraction) local f=Fraction or 0.5 local vec=self:GetDirectionalVectorTo(Vector) local length=vec:GetLength() vec:SetLength(f*length) vec=self+vec return vec end function VECTOR:SetLength(Length) self:Normalize() local v=self*Length self:Replace(v) return self end function VECTOR:SetX(x) self.x=x or 0 return self end function VECTOR:SetY(y) if y==nil then y=self:GetSurfaceHeight() end self.y=y return self end function VECTOR:SetZ(z) self.z=z or 0 return self end function VECTOR:AddVec(Vec) self.x=self.x+Vec.x if Vec.z then self.y=self.y+Vec.y self.z=self.z+Vec.z else self.z=self.z+Vec.y end return self end function VECTOR:SubVec(Vec) self.x=self.x-Vec.x if Vec.z then self.y=self.y-Vec.y self.z=self.z-Vec.z else self.z=self.z-Vec.y end return self end function VECTOR:Dot(Vec) local dot=self.x*Vec.x if Vec.z then dot=dot+self.y*Vec.y+self.z*Vec.z else dot=dot+self.z*Vec.y end return dot end function VECTOR:Rot(Vec) local dot=self.x*Vec.x if Vec.z then dot=dot+self.y*Vec.y+self.z*Vec.z else dot=dot+self.z*Vec.y end return dot end function VECTOR:Copy() local c=VECTOR:New(self.x,self.y,self.z) return c end function VECTOR:Replace(Vector,Project2D) self.x=Vector.x if Vector.z then self.y=Vector.y self.z=Vector.z else if Project2D then self.y=0 end self.z=Vector.y end return self end function VECTOR:Normalize() local l=self:GetLength() if l~=0 then self:Replace(self/l) end return self end function VECTOR:Translate(Distance,Heading,Copy) Distance=Distance or 1000 local alpha=math.rad(Heading or 0) local vector=Copy and self:Copy()or self vector.x=Distance*math.cos(alpha)+vector.x vector.z=Distance*math.sin(alpha)+vector.z return vector end function VECTOR:Rotate2D(Angle,Copy) local phi=-math.rad(Angle or 0) local sinPhi=math.sin(phi) local cosPhi=math.cos(phi) local X=self.z local Y=self.x local z=X*cosPhi-Y*sinPhi local x=X*sinPhi+Y*cosPhi if Copy then local vector=VECTOR:New(x,self.y,z) return vector else self:SetX(x) self:SetZ(z) return self end end function VECTOR: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) local text="MGRS "..UTILS.tostringMGRS(MGRS,MGRS_Accuracy) return text end function VECTOR:GetSurfaceType() local vec2=self:GetVec2() local s=land.getSurfaceType(vec2) return s end function VECTOR:GetSurfaceTypeName() local vec2=self:GetVec2() local s=land.getSurfaceType(vec2) for name,id in land.SurfaceType()do if id==s then return name end end return"unknown" end function VECTOR:IsVisible(Vec) local vec1=self:GetVec3() local vec2={x=Vec.x,Vec.y,Vec.z} local los=land.isVisible(vec1,vec2) return los end function VECTOR:GetClosestRoad() local vec2=self:GetVec2() local x,y=land.getClosestPointOnRoads('roads',vec2.x,vec2.y) local road=nil if x and y then road=VECTOR:New(x,y) end return road end function VECTOR:GetClosestRailroad() local vec2=self:GetVec2() local x,y=land.getClosestPointOnRoads('railroads',vec2.x,vec2.y) local road=nil if x and y then road=VECTOR:New(x,y) end return road end function VECTOR:GetPathOnRoad(Vec) local vec1=self:GetVec2() local vec2=Vec:GetVec2() local vec2points=land.findPathOnRoads("roads",vec1.x,vec1.y,vec2.x,vec2.y) local path=nil if vec2points then path=PATHLINE:NewFromVec2Array("Road",vec2points) end return path end function VECTOR:GetProfile(Vec3) local vec3=self:GetVec3() local vec3s=land.profile(vec3,Vec3) local profile=nil if vec3s then profile=PATHLINE:NewFromVec3Array("Profile",vec3s) end return profile end function VECTOR:GetInterceptPoint(DirectionVector,Distance) local vec3=self:GetVec3() local ip3=land.getIP(vec3,DirectionVector,Distance or 1000) local ipvector=nil if ip3 then ipvector=VECTOR:New(ip3.x,ip3.y,ip3.z) end return ipvector end function VECTOR:GetSurfaceHeight() local vec2=self:GetVec2() local h=land.getHeight(vec2) return h end function VECTOR:GetSurfaceHeightAndDepth() local vec2=self:GetVec2() local h,d=land.getSurfaceHeightWithSeabed(vec2) return h,d end function VECTOR:GetWindVector(WithTurbulence) local vec3=self:GetVec3() local wind=nil if WithTurbulence then wind=atmosphere.getWindWithTurbulence(vec3) else wind=atmosphere.getWind(vec3) end local vector=VECTOR:New(wind) return vector end function VECTOR:GetTemperaturAndPressure() local vec3=self:GetVec3() local t,p=atmosphere.getTemperatureAndPressure(vec3) return t,p end function VECTOR:Smoke(Color,Duration) local vec3=self:GetVec3() Color=Color or 0 local name=string.format("Vector-Smoke-%d",self.uid) trigger.action.smoke(vec3,Color,name) if Duration and Duration>0 then self:StopSmoke(name,Duration) end return name end function VECTOR:SmokeAndFire(Preset,Density,Duration) Preset=Preset or BIGSMOKEPRESET.LargeSmokeAndFire Density=Density or 0.5 local vec3=self:GetVec3() local name=string.format("Vector-Fire-%d",self.uid) trigger.action.effectSmokeBig(vec3,Preset,Density,name) if Duration and Duration>0 then self:StopSmoke(name,Duration) end return name end function VECTOR:StopSmoke(Name,Delay) if Delay and Delay>0 then TIMER:New(VECTOR.StopSmoke,self,Name):Start(Delay) else if Name then trigger.action.effectSmokeStop(Name) else env.error(string.format("No name provided in VECTOR.StopSmoke function!")) end end return self end function VECTOR:IlluminationBomb(Power,Altitude) local vec3=self:GetVec3() if Altitude then vec3.y=Altitude end trigger.action.illuminationBomb(vec3,Power or 1000) return self end function VECTOR:Explosion(Power) local vec3=self:GetVec3() trigger.action.explosion(vec3,Power or 100) return self end function VECTOR:Flare(Color,Azimuth) local vec3=self:GetVec3() trigger.action.signalFlare(vec3,Color or 0,math.rad(Azimuth or 0)) return self end function VECTOR:ArrowTo(Vector,Coalition,Color,FillColor,LineType) local vec3End=self:GetVec3() local vec3Start=Vector:GetVec3() local id=UTILS.GetMarkID() Coalition=Coalition or-1 Color=Color or{1,0,0,0.7} FillColor=FillColor or{1,0,0,0.5} LineType=LineType or 1 local readOnly=false trigger.action.arrowToAll(Coalition,id,vec3Start,vec3End,Color,FillColor,LineType,readOnly,"") return id end function VECTOR:Mark(MarkText,Recipient,ReadOnly) Recipient=Recipient or-1 if type(Recipient)=="number"then local MarkID=UTILS.GetMarkID() if Recipient==-1 then trigger.action.markToAll(MarkID,MarkText,self:GetVec3(),ReadOnly,"") elseif Recipient==0 then trigger.action.markToCoalition(MarkID,MarkText,self:GetVec3(),coalition.side.NEUTRAL,ReadOnly,"") elseif Recipient==1 then trigger.action.markToCoalition(MarkID,MarkText,self:GetVec3(),coalition.side.RED,ReadOnly,"") elseif Recipient==2 then trigger.action.markToCoalition(MarkID,MarkText,self:GetVec3(),coalition.side.BLUE,ReadOnly,"") end return MarkID elseif type(Recipient)=="table"then local MarkID=UTILS.GetMarkID() local group=Recipient trigger.action.markToGroup(MarkID,MarkText,self:GetVec3(),group:GetID(),ReadOnly,"") return MarkID end return nil end function VECTOR._IsVector(t) return getmetatable(t)==VECTOR end function VECTOR.__add(a,b) assert(VECTOR._IsVector(a)and VECTOR._IsVector(b),"ERROR in VECTOR.__add: wrong argument types! (expected and )") local c=VECTOR:New(a.x+b.x,a.y+b.y,a.z+b.z) return c end function VECTOR.__sub(a,b) assert(VECTOR._IsVector(a)and VECTOR._IsVector(b),"ERROR in VECTOR.__sub: wrong argument types: (expected and )") local c=VECTOR:New(a.x-b.x,a.y-b.y,a.z-b.z) return c end function VECTOR.__mul(a,b) local c=nil if type(a)=='number'then c=VECTOR:New(a*b.x,a*b.y,a*b.z) elseif type(b)=='number'then c=VECTOR:New(b*a.x,b*a.y,b*a.z) else c=VECTOR:New(a.x*b.x,a.y*b.y,a.z*b.z) end return c end function VECTOR.__div(a,b) assert(VECTOR._IsVector(a)and(type(b)=="number"or VECTOR._IsVector(b)),"div: wrong argument types (expected and ( or ))") local c=nil if type(b)=="number"then c=VECTOR:New(a.x/b,a.y/b,a.z/b) else c=VECTOR:New(a.x/b.x,a.y/b.y,a.z/b.z) end return c end function VECTOR.__unm(v) local c=VECTOR:New(-v.x,-v.y,-v.z) return c end function VECTOR.__eq(a,b) return a.x==b.x and a.y==b.y and a.z==b.z end function VECTOR:__tostring() local text=string.format("VECTOR: x=%.1f, y=%.1f, z=%.1f |v|=%.1f Phi=%4.1f°",self.x,self.y,self.z,self:GetLength(),self:GetHeading(false)) return text 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:IsRed() return self:GetCoalition()==coalition.side.RED end function IDENTIFIABLE:IsBlue() return self:GetCoalition()==coalition.side.BLUE end function IDENTIFIABLE:IsNeutral() return self:GetCoalition()==coalition.side.NEUTRAL 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:GetOrientationVectors() local position=self:GetPosition() if position then local vecx=VECTOR:NewFromVec(position.x) local vecy=VECTOR:NewFromVec(position.y) local vecz=VECTOR:NewFromVec(position.z) return vecx,vecy,vecz else BASE:E({"Cannot GetOrientation",Positionable=self,Alive=self:IsAlive()}) return nil,nil,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() if not vec3 then local pos=DCSPositionable:getPosition() if pos and pos.p then vec3=pos.p else self:E({"Cannot get the position from DCS Object for GetVec3",Positionable=self,Alive=self:IsAlive()}) end end 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:GetVector() local DCSPositionable=self:GetDCSObject() if DCSPositionable then local Vec3=DCSPositionable:getPoint() local vector=VECTOR:NewFromVec(Vec3) return vector 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 PositionableVec3 then if self.coordinate then self.coordinate:UpdateFromVec3(PositionableVec3) else self.coordinate=COORDINATE:NewFromVec3(PositionableVec3) end return self.coordinate end 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() if PositionableVec3 then local coord=COORDINATE:NewFromVec3(PositionableVec3) local heading=self:GetHeading() coord.Heading=heading return coord end 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(FromGround) self:F2() local DCSPositionable=self:GetDCSObject() if DCSPositionable then local altitude=0 local point=DCSPositionable: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 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:GetVelocityVector() local DCSPositionable=self:GetDCSObject() if DCSPositionable and DCSPositionable:isExist()then local vec3=DCSPositionable:getVelocity() local vector=VECTOR:NewFromVec(vec3) return vector end BASE:E({"Cannot GetVelocityVector",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, ["Sd_Kfz_251"]=10*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,GroupAttack) local DCSTask={id='AttackGroup', params={ groupId=AttackGroup:GetID(), weaponType=WeaponType or ENUMS.WeaponFlag.Auto, 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 ENUMS.WeaponFlag.Auto, }, } 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 ENUMS.WeaponFlag.AnyBomb, 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 ENUMS.WeaponFlag.Auto, }, } 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) 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:OptionPreferVerticalLanding() 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.PREFER_VERTICAL,true) end return self end return nil end function CONTROLLABLE:OptionAllowFormationSideSwap() 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.ALLOW_FORMATION_SIDE_SWAP,true) end return self end return nil end function CONTROLLABLE:OptionAIRunwayLineUp() self:F2({self.ControllableName}) local DCSControllable=self:GetDCSObject() if DCSControllable then local Controller=self:_GetController() if self:IsAir()then Controller:setOption(37,true) end return self end return nil end function CONTROLLABLE:OptionDisengageAndRTBAfterFormationLoss() self:F2({self.ControllableName}) local DCSControllable=self:GetDCSObject() if DCSControllable then local Controller=self:_GetController() if self:IsAir()then Controller:setOption(38,1) 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:OptionAAAMinFiringHeightMeters(meters) self:F2({self.ControllableName}) local meters=meters or 20 local DCSControllable=self:GetDCSObject() if DCSControllable then local Controller=self:_GetController() if Controller then if self:IsGround()then self:SetOption(27,meters) end end return self end return nil end function CONTROLLABLE:OptionAAAMaxFiringHeightMeters(meters) self:F2({self.ControllableName}) local meters=meters or 1000 local DCSControllable=self:GetDCSObject() if DCSControllable then local Controller=self:_GetController() if Controller then if self:IsGround()then self:SetOption(29,meters) end end return self end return nil end function CONTROLLABLE:OptionAAAMinFiringHeightFeet(feet) self:F2({self.ControllableName}) local feet=feet or 60 return self:OptionAAAMinFiringHeightMeters(UTILS.FeetToMeters(feet)) end function CONTROLLABLE:OptionAAAMaxFiringHeightfeet(feet) self:F2({self.ControllableName}) local feet=feet or 3000 return self:OptionAAAMaxFiringHeightMeters(UTILS.FeetToMeters(feet)) 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:SetOptionJettisonEmptyTanks(Switch) self:F2({self.ControllableName}) Switch=Switch or true if self:IsAir()then self:SetOption(AI.Option.Air.id.JETT_TANKS_IF_EMPTY,Switch) end return self end function CONTROLLABLE:SetOptionLandingStraightIn() self:F2({self.ControllableName}) if self:IsAir()then self:SetOption("36","0") end return self end function CONTROLLABLE:SetOptionLandingForcePair() self:F2({self.ControllableName}) if self:IsAir()then self:SetOption("36","1") end return self end function CONTROLLABLE:SetOptionLandingRestrictPair() self:F2({self.ControllableName}) if self:IsAir()then self:SetOption("36","2") end return self end function CONTROLLABLE:SetOptionLandingOverheadBreak() self:F2({self.ControllableName}) if self:IsAir()then self:SetOption("36","3") end return self 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_SHORAD="Ground_SHORAD", GROUND_BALLISTICMISSILE="Ground_BallisticMissile", 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 unit=DCSGroup:getUnit(1) if unit then local GroupTypeName=unit:getTypeName() return(GroupTypeName) end end return nil end function GROUP:GetNatoReportingName() local DCSGroup=self:GetDCSObject() if DCSGroup then local unit=DCSGroup:getUnit(1) if unit then local GroupTypeName=unit:getTypeName() return UTILS.GetReportingName(GroupTypeName) end end return"Bogey" end function GROUP:GetPlayerName() local DCSGroup=self:GetDCSObject() if DCSGroup then local unit=DCSGroup:getUnit(1) if unit then local PlayerName=unit:getPlayerName() return(PlayerName) end end return nil end function GROUP:GetCallsign() local DCSGroup=self:GetDCSObject() if DCSGroup then local unit=DCSGroup:getUnit(1) if unit then local GroupCallSign=unit:getCallsign() return GroupCallSign end 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:GetVector() local unit=self:GetUnit(1) if unit then local vector=unit:GetVector() return vector end self:E("ERROR: Cannot get Vector 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 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() local template=_DATABASE:GetGroupTemplate(GroupName) if template and template.route and template.route.points then return UTILS.DeepCopy(template.route.points) end 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) if self.ValidateAndRepositionGroundUnits then UTILS.ValidateAndRepositionGroundUnits(Template.units) end 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]and _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]and _DATABASE.Templates.Groups[GroupName].Template or nil 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 BASE:E("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 ballisticMissile=artillery and self:HasAttribute("SS_missile") local shorad=self:HasAttribute("SR SAM") 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 and ballisticMissile then attribute=GROUP.Attribute.GROUND_BALLISTICMISSILE 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:SetFormation(Formation) self:SetOption(AI.Option.Air.id.FORMATION,Formation) 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 function GROUP:SetValidateAndRepositionGroundUnits(Enabled) self.ValidateAndRepositionGroundUnits=Enabled end function GROUP:GetBoundingBox() local bbox={min={x=math.huge,y=math.huge,z=math.huge}, max={x=-math.huge,y=-math.huge,z=-math.huge} } local Units=self:GetUnits()or{} if#Units==0 then return nil end for _,unit in pairs(Units)do if unit and unit:IsAlive()then local ubox=unit:GetBoundingBox() if ubox then if ubox.min.xbbox.max.x then bbox.max.x=ubox.max.x end if ubox.max.y>bbox.max.y then bbox.max.y=ubox.max.y end if ubox.max.z>bbox.max.z then bbox.max.z=ubox.max.z end end end end return bbox 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 if self.ValidateAndRepositionGroundUnits then UTILS.ValidateAndRepositionGroundUnits(SpawnGroupTemplate.units) end _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)or string.find(ammotable[w].desc.typeName,"HESH",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) 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] elseif 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] elseif 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 function UNIT:SetLife(Percent) net.dostring_in("mission",string.format("a_unit_set_life_percentage(%d, %f)",self:GetID(),Percent)) end function UNIT:SetCarrierIlluminationMode(Mode) UTILS.SetCarrierIlluminationMode(self:GetID(),Mode) end function UNIT:SetValidateAndRepositionGroundUnits(Enabled) self.ValidateAndRepositionGroundUnits=Enabled end function UNIT:GetFuelMassMax() local Desc=self:GetDesc()or{} local massFuelMax=Desc.fuelMassMax or 0 local relFuel=math.min(self:GetFuel()or 1.0,1.0) local massFuel=massFuelMax*relFuel return massFuel,massFuelMax end function UNIT:GetCurrentFuelKgs() local fuel,maxfuel=self:GetFuelMassMax() local relfuel=self:GetFuel() local maxfilling=math.max(fuel,maxfuel) local mass=maxfilling*relfuel return mass 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: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 self._vec3=self:GetVec3() 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:GetVec2Cached() local vec2=self:GetVec2() if not vec2 and self._vec3 then vec2={x=self._vec3.x,y=self._vec3.z} end return vec2 end function STATIC:GetVec3Cached() local vec3=self:GetVec3() if not vec3 and self._vec3 then vec3=self._vec3 end return vec3 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) self._vec3=self:GetVec3() 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) self._vec3=self:GetVec3() 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) self._vec3=self:GetVec3() 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", ["Alderney"]="Alderney", ["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", ["Bembridg"]="Bembridg", ["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", ["Eastchurch"]="Eastchurch", ["Essay"]="Essay", ["Evreux"]="Evreux", ["Farnborough"]="Farnborough", ["Fecamp_Benouville"]="Fecamp-Benouville", ["Flers"]="Flers", ["Ford"]="Ford", ["Friston"]="Friston", ["Funtington"]="Funtington", ["Goulet"]="Goulet", ["Gravesend"]="Gravesend", ["Guernsey"]="Guernsey", ["Guyancourt"]="Guyancourt", ["Hauterive"]="Hauterive", ["Hawkinge"]="Hawkinge", ["Headcorn"]="Headcorn", ["Heathrow"]="Heathrow", ["High_Halden"]="High Halden", ["Holmsley_South"]="Holmsley South", ["Jersey"]="Jersey", ["Kenley"]="Kenley", ["Lantheuil"]="Lantheuil", ["Lashenden"]="Lashenden", ["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", ["Northolt"]="Northolt", ["Odiham"]="Odiham", ["Orly"]="Orly", ["Picauville"]="Picauville", ["Poix"]="Poix", ["Ronai"]="Ronai", ["Rouen_Boos"]="Rouen-Boos", ["Rucqueville"]="Rucqueville", ["Saint_Pierre_du_Mont"]="Saint Pierre du Mont", ["Saint_Andre_de_lEure"]="Saint-Andre-de-lEure", ["Saint_Aubin"]="Saint-Aubin", ["Saint_Omer_Wizernes"]="Saint-Omer Wizernes", ["Saint_Pol_Bryas"]="Saint-Pol-Bryas", ["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", } AIRBASE.PersianGulf={ ["Abu_Dhabi_Intl"]="Abu Dhabi Intl", ["Abu_Musa_Island"]="Abu Musa Island", ["Al_Ain_Intl"]="Al Ain Intl", ["Al_Dhafra_AFB"]="Al Dhafra AFB", ["Al_Maktoum_Intl"]="Al Maktoum Intl", ["Al_Minhad_AFB"]="Al Minhad AFB", ["Al_Bateen"]="Al-Bateen", ["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_Qusayr"]="Al Qusayr", ["Al_Dumayr"]="Al-Dumayr", ["Aleppo"]="Aleppo", ["An_Nasiriyah"]="An Nasiriyah", ["At_Tanf"]="At Tanf", ["Bassel_Al_Assad"]="Bassel Al-Assad", ["Beirut_Rafic_Hariri"]="Beirut-Rafic Hariri", ["Ben_Gurion"]="Ben Gurion", ["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", ["Hatzor"]="Hatzor", ["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", ["Marka"]="Marka", ["Megiddo"]="Megiddo", ["Mezzeh"]="Mezzeh", ["Minakh"]="Minakh", ["Muwaffaq_Salti"]="Muwaffaq Salti", ["Naqoura"]="Naqoura", ["Nicosia"]="Nicosia", ["Palmachim"]="Palmachim", ["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", ["Tel_Nof"]="Tel Nof", ["Tha_lah"]="Tha'lah", ["Tiyas"]="Tiyas", ["Wujah_Al_Hajar"]="Wujah Al Hajar", } 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.MarianaIslandsWWII={ ["Agana"]="Agana", ["Airfield_3"]="Airfield 3", ["Charon_Kanoa"]="Charon Kanoa", ["Gurguan_Point"]="Gurguan Point", ["Isley"]="Isley", ["Kagman"]="Kagman", ["Marpi"]="Marpi", ["Orote"]="Orote", ["Pagan"]="Pagan", ["Rota"]="Rota", ["Ushi"]="Ushi", } 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", ["Borg_El_Arab_International_Airport"]="Borg El Arab International Airport", ["Cairo_International_Airport"]="Cairo International Airport", ["Cairo_West"]="Cairo West", ["Damascus_Intl"]="Damascus Intl", ["Difarsuwar_Airfield"]="Difarsuwar Airfield", ["Ein_Shamer"]="Ein Shamer", ["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", ["Khalkhalah_Air_Base"]="Khalkhalah Air Base", ["Kibrit_Air_Base"]="Kibrit Air Base", ["King_Feisal_Air_Base"]="King Feisal Air Base", ["Kom_Awshim"]="Kom Awshim", ["Megiddo"]="Megiddo", ["Melez"]="Melez", ["Mezzeh_Air_Base"]="Mezzeh Air Base", ["Nevatim"]="Nevatim", ["Ovda"]="Ovda", ["Palmachim"]="Palmachim", ["Quwaysina"]="Quwaysina", ["Rafic_Hariri_Intl"]="Rafic Hariri Intl", ["Ramat_David"]="Ramat David", ["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", ["Taba_International_Airport"]="Taba International Airport", ["Tabuk"]="Tabuk", ["TabukHeliBase"]="TabukHeliBase", ["Tel_Nof"]="Tel Nof", ["Wadi_Abu_Rish"]="Wadi Abu Rish", ["Wadi_al_Jandali"]="Wadi al Jandali", } AIRBASE.SinaiMap={ ["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", ["Borg_El_Arab_International_Airport"]="Borg El Arab International Airport", ["Cairo_International_Airport"]="Cairo International Airport", ["Cairo_West"]="Cairo West", ["Damascus_Intl"]="Damascus Intl", ["Difarsuwar_Airfield"]="Difarsuwar Airfield", ["Ein_Shamer"]="Ein Shamer", ["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", ["Khalkhalah_Air_Base"]="Khalkhalah Air Base", ["Kibrit_Air_Base"]="Kibrit Air Base", ["King_Feisal_Air_Base"]="King Feisal Air Base", ["Kom_Awshim"]="Kom Awshim", ["Megiddo"]="Megiddo", ["Melez"]="Melez", ["Mezzeh_Air_Base"]="Mezzeh Air Base", ["Nevatim"]="Nevatim", ["Ovda"]="Ovda", ["Palmachim"]="Palmachim", ["Quwaysina"]="Quwaysina", ["Rafic_Hariri_Intl"]="Rafic Hariri Intl", ["Ramat_David"]="Ramat David", ["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", ["Taba_International_Airport"]="Taba International Airport", ["Tabuk"]="Tabuk", ["TabukHeliBase"]="TabukHeliBase", ["Tel_Nof"]="Tel Nof", ["Wadi_Abu_Rish"]="Wadi Abu Rish", ["Wadi_al_Jandali"]="Wadi al Jandali", } AIRBASE.Kola={ ["Afrikanda"]="Afrikanda", ["Alakurtti"]="Alakurtti", ["Alta"]="Alta", ["Andoya"]="Andoya", ["Arvidsjaur"]="Arvidsjaur", ["Banak"]="Banak", ["Bardufoss"]="Bardufoss", ["Boden_Heli_Base"]="Boden Heli Base", ["Bodo"]="Bodo", ["Enontekio"]="Enontekio", ["Evenes"]="Evenes", ["Hemavan"]="Hemavan", ["Hosio"]="Hosio", ["Ivalo"]="Ivalo", ["Jokkmokk"]="Jokkmokk", ["Kalevala"]="Kalevala", ["Kalixfors"]="Kalixfors", ["Kallax"]="Kallax", ["Kemi_Tornio"]="Kemi Tornio", ["Kilpyavr"]="Kilpyavr", ["Kirkenes"]="Kirkenes", ["Kiruna"]="Kiruna", ["Kittila"]="Kittila", ["Koshka_Yavr"]="Koshka Yavr", ["Kuusamo"]="Kuusamo", ["Luostari_Pechenga"]="Luostari Pechenga", ["Monchegorsk"]="Monchegorsk", ["Murmansk_International"]="Murmansk International", ["Olenya"]="Olenya", ["Poduzhemye"]="Poduzhemye", ["Rovaniemi"]="Rovaniemi", ["Severomorsk_1"]="Severomorsk-1", ["Severomorsk_3"]="Severomorsk-3", ["Sodankyla"]="Sodankyla", ["Vidsel"]="Vidsel", ["Vuojarvi"]="Vuojarvi", } AIRBASE.Afghanistan={ ["Bagram"]="Bagram", ["Bamyan"]="Bamyan", ["Bost"]="Bost", ["Camp_Bastion"]="Camp Bastion", ["Camp_Bastion_Heliport"]="Camp Bastion Heliport", ["Chaghcharan"]="Chaghcharan", ["Dwyer"]="Dwyer", ["FOB_Camp_Dubs"]="FOB Camp Dubs", ["FOB_Clark"]="FOB Clark", ["FOB_Salerno"]="FOB Salerno", ["FOB_Thunder"]="FOB Thunder", ["Farah"]="Farah", ["Gardez"]="Gardez", ["Ghazni_Heliport"]="Ghazni Heliport", ["Herat"]="Herat", ["Jalalabad"]="Jalalabad", ["Kabul"]="Kabul", ["Kandahar"]="Kandahar", ["Kandahar_Heliport"]="Kandahar Heliport", ["Khost"]="Khost", ["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={ ["Al_Asad_Airbase"]="Al-Asad Airbase", ["Al_Kut_Airport"]="Al-Kut Airport", ["Al_Sahra_Airport"]="Al-Sahra Airport", ["Al_Salam_Airbase"]="Al-Salam Airbase", ["Al_Taji_Airport"]="Al-Taji Airport", ["Al_Taquddum_Airport"]="Al-Taquddum Airport", ["Baghdad_International_Airport"]="Baghdad International Airport", ["Balad_Airbase"]="Balad Airbase", ["Bashur_Airport"]="Bashur Airport", ["Erbil_International_Airport"]="Erbil International Airport", ["H_2_Airbase"]="H-2 Airbase", ["H_3_Main_Airbase"]="H-3 Main Airbase", ["H_3_Northwest_Airbase"]="H-3 Northwest Airbase", ["H_3_Southwest_Airbase"]="H-3 Southwest Airbase", ["K1_Base"]="K1 Base", ["Kirkuk_International_Airport"]="Kirkuk International Airport", ["Mosul_International_Airport"]="Mosul International Airport", ["Qayyarah_Airfield_West"]="Qayyarah Airfield West", ["Sulaimaniyah_International_Airport"]="Sulaimaniyah International Airport", } AIRBASE.GermanyCW={ ["Adelsheim"]="Adelsheim", ["Airracing_Frankfurt"]="Airracing Frankfurt", ["Airracing_Koblenz"]="Airracing Koblenz", ["Airracing_Luebeck"]="Airracing Lubeck", ["Allstedt"]="Allstedt", ["Altes_Lager"]="Altes Lager", ["Bad_Duerkheim"]="Bad Durkheim", ["Barth"]="Barth", ["Bienenfarm"]="Bienenfarm", ["Bindersleben"]="Bindersleben", ["Bitburg"]="Bitburg", ["Bornholm"]="Bornholm", ["Brand"]="Brand", ["Brandis"]="Brandis", ["Braunschweig"]="Braunschweig", ["Bremen"]="Bremen", ["Briest"]="Briest", ["Buechel"]="Buchel", ["Bueckeburg"]="Buckeburg", ["Celle"]="Celle", ["Chojna"]="Chojna", ["Cochstedt"]="Cochstedt", ["Cologne"]="Cologne", ["Damgarten"]="Damgarten", ["Dedelow"]="Dedelow", ["Dessau"]="Dessau", ["Duesseldorf"]="Dusseldorf", ["Falkenberg"]="Falkenberg", ["Fassberg"]="Fassberg", ["Finow"]="Finow", ["Frankfurt"]="Frankfurt", ["Fritzlar"]="Fritzlar", ["Fulda"]="Fulda", ["Gardelegen"]="Gardelegen", ["Garz"]="Garz", ["Gatow"]="Gatow", ["Gelnhausen"]="Gelnhausen", ["Giebelstadt"]="Giebelstadt", ["Glindbruchkippe"]="Glindbruchkippe", ["Gross_Mohrdorf"]="Gross Mohrdorf", ["Grosse_Wiese"]="Grosse Wiese", ["Guetersloh"]="Gutersloh", ["H_FRG_01"]="H FRG 01", ["H_FRG_02"]="H FRG 02", ["H_FRG_03"]="H FRG 03", ["H_FRG_04"]="H FRG 04", ["H_FRG_05"]="H FRG 05", ["H_FRG_06"]="H FRG 06", ["H_FRG_07"]="H FRG 07", ["H_FRG_08"]="H FRG 08", ["H_FRG_09"]="H FRG 09", ["H_FRG_10"]="H FRG 10", ["H_FRG_11"]="H FRG 11", ["H_FRG_12"]="H FRG 12", ["H_FRG_13"]="H FRG 13", ["H_FRG_14"]="H FRG 14", ["H_FRG_15"]="H FRG 15", ["H_FRG_16"]="H FRG 16", ["H_FRG_17"]="H FRG 17", ["H_FRG_18"]="H FRG 18", ["H_FRG_19"]="H FRG 19", ["H_FRG_20"]="H FRG 20", ["H_FRG_21"]="H FRG 21", ["H_FRG_23"]="H FRG 23", ["H_FRG_25"]="H FRG 25", ["H_FRG_27"]="H FRG 27", ["H_FRG_30"]="H FRG 30", ["H_FRG_31"]="H FRG 31", ["H_FRG_32"]="H FRG 32", ["H_FRG_34"]="H FRG 34", ["H_FRG_38"]="H FRG 38", ["H_FRG_39"]="H FRG 39", ["H_FRG_40"]="H FRG 40", ["H_FRG_41"]="H FRG 41", ["H_FRG_42"]="H FRG 42", ["H_FRG_43"]="H FRG 43", ["H_FRG_44"]="H FRG 44", ["H_FRG_45"]="H FRG 45", ["H_FRG_46"]="H FRG 46", ["H_FRG_47"]="H FRG 47", ["H_FRG_48"]="H FRG 48", ["H_FRG_49"]="H FRG 49", ["H_FRG_50"]="H FRG 50", ["H_FRG_51"]="H FRG 51", ["H_GDR_01"]="H GDR 01", ["H_GDR_02"]="H GDR 02", ["H_GDR_03"]="H GDR 03", ["H_GDR_04"]="H GDR 04", ["H_GDR_05"]="H GDR 05", ["H_GDR_06"]="H GDR 06", ["H_GDR_07"]="H GDR 07", ["H_GDR_08"]="H GDR 08", ["H_GDR_09"]="H GDR 09", ["H_GDR_10"]="H GDR 10", ["H_GDR_11"]="H GDR 11", ["H_GDR_12"]="H GDR 12", ["H_GDR_13"]="H GDR 13", ["H_GDR_14"]="H GDR 14", ["H_GDR_15"]="H GDR 15", ["H_GDR_16"]="H GDR 16", ["H_GDR_17"]="H GDR 17", ["H_GDR_18"]="H GDR 18", ["H_GDR_19"]="H GDR 19", ["H_GDR_21"]="H GDR 21", ["H_GDR_22"]="H GDR 22", ["H_GDR_24"]="H GDR 24", ["H_GDR_25"]="H GDR 25", ["H_GDR_26"]="H GDR 26", ["H_GDR_30"]="H GDR 30", ["H_GDR_31"]="H GDR 31", ["H_GDR_32"]="H GDR 32", ["H_GDR_33"]="H GDR 33", ["H_GDR_34"]="H GDR 34", ["H_Med_FRG_01"]="H Med FRG 01", ["H_Med_FRG_02"]="H Med FRG 02", ["H_Med_FRG_04"]="H Med FRG 04", ["H_Med_FRG_06"]="H Med FRG 06", ["H_Med_FRG_11"]="H Med FRG 11", ["H_Med_FRG_12"]="H Med FRG 12", ["H_Med_FRG_13"]="H Med FRG 13", ["H_Med_FRG_14"]="H Med FRG 14", ["H_Med_FRG_15"]="H Med FRG 15", ["H_Med_FRG_16"]="H Med FRG 16", ["H_Med_FRG_17"]="H Med FRG 17", ["H_Med_FRG_21"]="H Med FRG 21", ["H_Med_FRG_24"]="H Med FRG 24", ["H_Med_FRG_26"]="H Med FRG 26", ["H_Med_FRG_27"]="H Med FRG 27", ["H_Med_FRG_29"]="H Med FRG 29", ["H_Med_GDR_01"]="H Med GDR 01", ["H_Med_GDR_02"]="H Med GDR 02", ["H_Med_GDR_03"]="H Med GDR 03", ["H_Med_GDR_08"]="H Med GDR 08", ["H_Med_GDR_09"]="H Med GDR 09", ["H_Med_GDR_10"]="H Med GDR 10", ["H_Med_GDR_11"]="H Med GDR 11", ["H_Med_GDR_12"]="H Med GDR 12", ["H_Med_GDR_13"]="H Med GDR 13", ["H_Med_GDR_14"]="H Med GDR 14", ["H_Med_GDR_16"]="H Med GDR 16", ["H_Radar_FRG_02"]="H Radar FRG 02", ["H_Radar_GDR_01"]="H Radar GDR 01", ["H_Radar_GDR_02"]="H Radar GDR 02", ["H_Radar_GDR_03"]="H Radar GDR 03", ["H_Radar_GDR_04"]="H Radar GDR 04", ["H_Radar_GDR_05"]="H Radar GDR 05", ["H_Radar_GDR_06"]="H Radar GDR 06", ["H_Radar_GDR_07"]="H Radar GDR 07", ["H_Radar_GDR_08"]="H Radar GDR 08", ["H_Radar_GDR_09"]="H Radar GDR 09", ["Hahn"]="Hahn", ["Haina"]="Haina", ["Hamburg"]="Hamburg", ["Hamburg_Finkenwerder"]="Hamburg Finkenwerder", ["Hannover"]="Hannover", ["Hasselfelde"]="Hasselfelde", ["Heidelberg"]="Heidelberg", ["Herrenteich"]="Herrenteich", ["Hildesheim"]="Hildesheim", ["Hockenheim"]="Hockenheim", ["Holzdorf"]="Holzdorf", ["Kammermark"]="Kammermark", ["Kastrup"]="Kastrup", ["Kiel"]="Kiel", ["Koethen"]="Kothen", ["Laage"]="Laage", ["Landstuhl"]="Landstuhl", ["Langenselbold"]="Langenselbold", ["Laerz"]="Larz", ["Leipzig_Mockau"]="Leipzig Mockau", ["Luebeck"]="Lubeck", ["Lueneburg"]="Luneburg", ["Mahlwinkel"]="Mahlwinkel", ["Mainz_Finthen"]="Mainz Finthen", ["Marxwalde"]="Marxwalde", ["Mendig"]="Mendig", ["Merseburg"]="Merseburg", ["Neubrandenburg"]="Neubrandenburg", ["Neuruppin"]="Neuruppin", ["Nordholz"]="Nordholz", ["Northeim"]="Northeim", ["Noervenich"]="Norvenich", ["Ober_Moerlen"]="Ober-Morlen", ["Obermehler_Schlotheim"]="Obermehler Schlotheim", ["Oranienburg"]="Oranienburg", ["Parchim"]="Parchim", ["Peenemuende"]="Peenemunde", ["Perwenitz"]="Perwenitz", ["Pferdsfeld"]="Pferdsfeld", ["Pinnow"]="Pinnow", ["Pottschutthoehe"]="Pottschutthohe", ["Ramstein"]="Ramstein", ["Revinge"]="Revinge", ["Rinteln"]="Rinteln", ["Schkeuditz"]="Schkeuditz", ["Schoenefeld"]="Schonefeld", ["Schweinfurt"]="Schweinfurt", ["Sembach"]="Sembach", ["Sittensen"]="Sittensen", ["Spangdahlem"]="Spangdahlem", ["Sperenberg"]="Sperenberg", ["Sprendlingen"]="Sprendlingen", ["Stendal"]="Stendal", ["Sturup"]="Sturup", ["Szczecin_Goleniow"]="Szczecin-Goleniow", ["Tagra"]="Tagra", ["Tegel"]="Tegel", ["Tempelhof"]="Tempelhof", ["Templin"]="Templin", ["Thurland"]="Thurland", ["Tutow"]="Tutow", ["Uelzen"]="Uelzen", ["Uetersen"]="Uetersen", ["Ummern"]="Ummern", ["Verden_Scharnhorst"]="Verden-Scharnhorst", ["Walldorf"]="Walldorf", ["Waren_Vielist"]="Waren Vielist", ["Werneuchen"]="Werneuchen", ["Weser_Wuemme"]="Weser Wumme", ["Wiesbaden"]="Wiesbaden", ["Wismar"]="Wismar", ["Wittstock"]="Wittstock", ["Worms"]="Worms", ["Wunstorf"]="Wunstorf", ["Zerbst"]="Zerbst", ["Zoellschen"]="Zollschen", ["Zweibruecken"]="Zweibrucken", } 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 if Nrunways>0 then self:GetMinimumBoundingCircleFromParkingSpots() end self:T2(string.format("Registered airbase %s",tostring(self.AirbaseName))) return self end function AIRBASE:GetVec2() local runways=self:GetRunways() if runways and#runways>0 then return runways[1].center:GetVec2() end return self:GetCoordinate():GetVec2() 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)or{} 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)or{} 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)or{} 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:GetParkingSpotsVec2s(termtype) local parkingdata=self:GetParkingData(false) local spots={} for _,parkingspot in ipairs(parkingdata)do if AIRBASE._CheckTerminalType(parkingspot.Term_Type,termtype)then local vec2={x=parkingspot.vTerminalPos.x,y=parkingspot.vTerminalPos.z} table.insert(spots,vec2) end end return spots end function AIRBASE:GetMinimumBoundingCircleFromParkingSpots(mark) if self.isAirdrome then if not self.parkingCircle then local spots=self:GetParkingSpotsVec2s() if#spots==0 then return self.AirbaseZone end local center,radius=UTILS.GetMinimumBoundingCircle(spots) self.parkingCircle=ZONE_RADIUS:New(self.AirbaseName.." ParkingCircle",center,radius+50) if mark==true then self.parkingCircle:DrawZone(-1,{1,0,0},1,{0,1,0},0.2,3) end end return self.parkingCircle else return self.AirbaseZone end end function AIRBASE:_InitParkingSpots() local parkingdata=self:GetParkingData(false)or{} 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)or{} local parkingfree=self:GetParkingData(true)or{} 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)or{} 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()or{} 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)or{} 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 or{})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) self:T("Check Runway Name: "..name) 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) self:T("Create Runway: name = "..name) 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.idx=runway.magheading 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.magheading=idx runway.length=c1:Get2DDistance(c2) runway.position=c1 runway.endpoint=c2 self:T(string.format("Airbase %s: Adding runway id=%s, heading=%03d, length=%d m i=%d j=%d",self:GetName(),runway.idx,runway.heading,runway.length,i,j)) 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 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.position local a=UTILS.VecTranslate(c0,1000,ri.heading) local b=UTILS.VecSubstract(rj.position,ri.position) b=UTILS.VecAdd(ri.position,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 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", } _SCENERY={} function SCENERY:Register(SceneryName,SceneryObject,SceneryZone) local ID=(SceneryObject and SceneryObject.getID)and SceneryObject:getID()or SceneryName if _SCENERY[ID]and _SCENERY[ID].SceneryObject==nil then _SCENERY[ID].SceneryObject=SceneryObject SCENERY._UpdateFromDCSObject(_SCENERY[ID]) end if _SCENERY[ID]then return _SCENERY[ID]end local self=BASE:Inherit(self,POSITIONABLE:New(SceneryName)) self.SceneryName=tostring(SceneryName) self.ID=ID self.SceneryObject=SceneryObject self.SceneryZone=SceneryZone if SceneryZone then self.Vec3=SceneryZone:GetVec3() self.Vec2=SceneryZone:GetVec2() self.Vector=(self.Vec3 and VECTOR)and VECTOR:NewFromVec(self.Vec3)or nil end if SceneryObject and SceneryObject.getPoint then local vec3=SceneryObject:getPoint() self.Vec3={x=vec3.x,y=vec3.y,z=vec3.z} self.Vec2={x=vec3.x,y=vec3.z} self.Vector=(self.Vec3 and VECTOR)and VECTOR:NewFromVec(self.Vec3)or nil end if self.SceneryObject and self.SceneryObject.getLife then self.Life0=self.SceneryObject:getLife()or 1 else self.Life0=1 end self.Properties={} _SCENERY[self.ID]=self return self end function SCENERY._UpdateFromDCSObject(Scenery) env.info("APPLE _UpdateFromDCSObject "..tostring(Scenery.SceneryName)) local self=Scenery if self.Vec2==nil and self.SceneryObject~=nil then self.Vec3=self.SceneryObject:getPoint() if self.Vec3 then self.Vec2={x=self.Vec3.x,y=self.Vec3.z} self.Vector=VECTOR:NewFromVec(self.Vec3) end end if not self.Life0 or self.Life0==1 then if self.SceneryObject and self.SceneryObject.getLife()then self.Life=self.SceneryObject:getLife()or 1 self.Life0=self.Life end end 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:GetCoordinate() if self.Coordinate then return self.Coordinate elseif self.Vec3 then self.Coordinate=COORDINATE:NewFromVec3(self.Vec3):SetAlt() end return self.Coordinate end function SCENERY:GetVec3() return self.Vec3 end function SCENERY:GetVec2() return self.Vec2 end function SCENERY:GetVector() return self.Vector end function SCENERY:GetDCSObject() return self.SceneryObject end function SCENERY:GetID() return self.ID end function SCENERY:GetLife() local life=1 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 elseif self.Life then life=self.Life 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,Zone) local findme=self:_FindByName(Name) if findme then return findme end 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) 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 if not scenery then scenery=SCENERY:Register(Name,nil,Zone)end return scenery end function SCENERY:FindByID(ID) return _SCENERY[ID] end function SCENERY:_FindByName(Name) for _id,_object in pairs(_SCENERY)do if _object and _object.GetName and _object:GetName()then local name=_object:GetName() if Name==name then return _object end end end return nil 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():SetAlt() return self:FindByName(Name,coordinate,Radius,Zone:GetProperty("ROLE"),Zone) 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 local role=zone:GetProperty("ROLE") if role then scenery:SetProperty("ROLE",role)end return scenery end return nil else local coordinate=zone:GetCoordinate() coordinate:SetAlt() return self:FindByName(_id,coordinate,nil,zone:GetProperty("ROLE"),zone) end else local coordinate=zone:GetCoordinate() coordinate:SetAlt() return self:FindByName(_id,coordinate,nil,zone:GetProperty("ROLE"),zone) 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():SetAlt(),nil,zone:GetProperty("ROLE"),zone) 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(playerList[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=5, C130AttachDistance=10, C130DetachDistance=14, C130AirborneAGL=8, C130LandedAGL=0.5, C130StabilityEpsilon=0.05, C130RequireAirborne=true, C130OwnerResolveMove2D=10, C130OwnerResolveNear2D=4, C130OwnerResolveMax3D=250, } 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", ["Mi-8MTV2"]="Mi-8MTV2", ["Mi-8MT"]="Mi-8MT", ["UH-1H"]="UH-1H", ["Mi-24P"]="Mi-24P", ["UH-60L"]="UH-60L", ["UH-60L_DAP"]="UH-60L_DAP", ["C-130J-30"]="C-130J-30", } DYNAMICCARGO.AircraftDimensions={ ["CH-47Fbl1"]={ ["width"]=4, ["height"]=6, ["length"]=11, ["ropelength"]=30, }, ["Mi-8MTV2"]={ ["width"]=6, ["height"]=6, ["length"]=15, ["ropelength"]=30, }, ["Mi-8MT"]={ ["width"]=6, ["height"]=6, ["length"]=15, ["ropelength"]=30, }, ["UH-1H"]={ ["width"]=4, ["height"]=4, ["length"]=9, ["ropelength"]=25, }, ["Mi-24P"]={ ["width"]=4, ["height"]=5, ["length"]=11, ["ropelength"]=25, }, ["UH-60L"]={ ["width"]=4, ["height"]=5, ["length"]=10, ["ropelength"]=25, }, ["UH-60L_DAP"]={ ["width"]=4, ["height"]=5, ["length"]=10, ["ropelength"]=25, }, ["C-130J-30"]={ ["width"]=4, ["height"]=12, ["length"]=35, ["ropelength"]=0, ["attach"]=10, ["detach"]=14, }, } DYNAMICCARGO.version="0.1.0" DYNAMICCARGO._TrackedCargo=DYNAMICCARGO._TrackedCargo or{} DYNAMICCARGO._GlobalTimer=DYNAMICCARGO._GlobalTimer or nil DYNAMICCARGO._GlobalTimerInterval=DYNAMICCARGO._GlobalTimerInterval or nil function DYNAMICCARGO:Register(CargoName) local self=BASE:Inherit(self,POSITIONABLE:New(CargoName)) self.StaticName=CargoName self.LastPosition=self:GetCoordinate() self._spawnVec3=self.LastPosition and self.LastPosition:GetVec3()or nil self.CargoState=DYNAMICCARGO.State.NEW self._attached=false self._detached=false self._wasAirborne=false self._landAglConfirm=nil self._ownerResolved=false self._carrierUnitName=nil self._carrierGroupName=nil self._carrierTypeName=nil 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=nil DYNAMICCARGO._TrackCargo(self) 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:IsAttached() return self._attached==true end function DYNAMICCARGO:IsDetached() return self._detached==true end function DYNAMICCARGO:WasAirborneTransport() return self._wasAirborne==true end function DYNAMICCARGO:IsLandedStable() return self.CargoState==DYNAMICCARGO.State.UNLOADED and self._detached==true end function DYNAMICCARGO:GetCarrierUnitName() return self._carrierUnitName end function DYNAMICCARGO:GetCarrierTypeName() return self._carrierTypeName end function DYNAMICCARGO:GetCarrierGroupName() return self._carrierGroupName 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:_IsC130Type(TypeName) return TypeName=="C-130J-30" end function DYNAMICCARGO:_GetAGL(Coord) if not Coord then return-1 end return(Coord.y or 0)-Coord:GetLandHeight() end function DYNAMICCARGO:_GetPlayerNameForClient(Client) if not Client then return self.Owner or"None"end return Client:GetPlayerName()or _DATABASE:_FindPlayerNameByUnitName(Client:GetName())or self.Owner or"None" end function DYNAMICCARGO:_SetCarrierFromClient(Client,PlayerName) if not Client then return self end self._carrierUnitName=Client:GetName()or self._carrierUnitName self._carrierTypeName=Client:GetTypeName()or self._carrierTypeName local grp=Client:GetGroup() if grp then self._carrierGroupName=grp:GetName()or self._carrierGroupName end self.Owner=PlayerName or self:_GetPlayerNameForClient(Client) return self end function DYNAMICCARGO._GetSchedulerInterval() return DYNAMICCARGO.Interval or 5 end function DYNAMICCARGO._CountTracked() local n=0 for _,_ in pairs(DYNAMICCARGO._TrackedCargo or{})do n=n+1 end return n end function DYNAMICCARGO._StopGlobalSchedulerIfIdle() if DYNAMICCARGO._CountTracked()>0 then return end if DYNAMICCARGO._GlobalTimer and DYNAMICCARGO._GlobalTimer:IsRunning()then DYNAMICCARGO._GlobalTimer:Stop() end DYNAMICCARGO._GlobalTimer=nil DYNAMICCARGO._GlobalTimerInterval=nil end function DYNAMICCARGO._EnsureGlobalScheduler() local interval=DYNAMICCARGO._GetSchedulerInterval() if DYNAMICCARGO._GlobalTimer and DYNAMICCARGO._GlobalTimer:IsRunning()then if DYNAMICCARGO._GlobalTimerInterval==interval then return end DYNAMICCARGO._GlobalTimer:Stop() DYNAMICCARGO._GlobalTimer=nil DYNAMICCARGO._GlobalTimerInterval=nil end if DYNAMICCARGO._CountTracked()<1 then return end DYNAMICCARGO._GlobalTimer=TIMER:New(DYNAMICCARGO._UpdateAllTracked) DYNAMICCARGO._GlobalTimer:Start(interval,interval) DYNAMICCARGO._GlobalTimerInterval=interval end function DYNAMICCARGO._TrackCargo(Cargo) if not Cargo or not Cargo.StaticName then return end DYNAMICCARGO._TrackedCargo=DYNAMICCARGO._TrackedCargo or{} DYNAMICCARGO._TrackedCargo[Cargo.StaticName]=Cargo DYNAMICCARGO._EnsureGlobalScheduler() end function DYNAMICCARGO._UntrackCargo(CargoName) if not CargoName or not DYNAMICCARGO._TrackedCargo then DYNAMICCARGO._StopGlobalSchedulerIfIdle() return end DYNAMICCARGO._TrackedCargo[CargoName]=nil DYNAMICCARGO._StopGlobalSchedulerIfIdle() end function DYNAMICCARGO._UpdateAllTracked() local tracked=DYNAMICCARGO._TrackedCargo or{} local names={} for name,_ in pairs(tracked)do names[#names+1]=name end for _,name in ipairs(names)do local cargo=tracked[name] if cargo then cargo:_UpdatePosition() end end DYNAMICCARGO._StopGlobalSchedulerIfIdle() end function DYNAMICCARGO:_FindClientByUnitName(UnitName) if not UnitName or UnitName==""or not _DYNAMICCARGO_HELOS then return nil end for _,_helo in pairs(_DYNAMICCARGO_HELOS:GetAliveSet()or{})do local helo=_helo if helo and helo:IsAlive()and helo:GetName()==UnitName then return helo end end return nil end function DYNAMICCARGO:_GetKnownCarrierClient() local client=nil if self._carrierUnitName then client=self:_FindClientByUnitName(self._carrierUnitName) end if(not client)and self.Owner and self.Owner~="None"then local byPlayer=CLIENT:FindByPlayerName(self.Owner) if byPlayer and byPlayer:IsAlive()then client=byPlayer end end return client end function DYNAMICCARGO:_FindNearestC130(Pos,Max3D) if not Pos or not _DYNAMICCARGO_HELOS then return nil,nil,nil,nil end local bestClient=nil local bestName=nil local best2D=math.huge local best3D=math.huge local bestOwnerMatch=false local max3D=Max3D or DYNAMICCARGO.C130OwnerResolveMax3D local preferredOwner=self.Owner if preferredOwner==""or preferredOwner=="None"then preferredOwner=nil end for _,_helo in pairs(_DYNAMICCARGO_HELOS:GetAliveSet()or{})do local helo=_helo if helo and helo:IsAlive()then local typename=helo:GetTypeName() if self:_IsC130Type(typename)then local hpos=helo:GetCoordinate() if hpos then local d3=hpos:Get3DDistance(Pos) if d3<=max3D then local d2=hpos:Get2DDistance(Pos) local pname=self:_GetPlayerNameForClient(helo) local ownerMatch=preferredOwner and pname and pname==preferredOwner or false if(ownerMatch and not bestOwnerMatch)or((ownerMatch==bestOwnerMatch)and d3=airborneAgl or carrierAgl>=airborneAgl then self._wasAirborne=true end if self._attached and carrierInAir and dist2D>detachDist then self._attached=false self._detached=true self._landAglConfirm=nil self:T(self.lid.." C130 detach at d2="..tostring(UTILS.Round(dist2D,2))) end if self._attached and(not carrier or not carrier:IsAlive())and self._wasAirborne and cargoAgl<=airborneAgl then self._attached=false self._detached=true self._landAglConfirm=nil self:T(self.lid.." C130 detach fallback (carrier stale)") end local canUnload=self._detached and((not requireAirborne)or self._wasAirborne) if canUnload then local moved3D=self.LastPosition and UTILS.VecDist3D(Pos,self.LastPosition)or math.huge local stable=moved3D<=stableEps if cargoAgl<=landedAgl and stable then if self._landAglConfirm then self.CargoState=DYNAMICCARGO.State.UNLOADED self:T(self.lid.." C130 landed-stable unload by "..tostring(self.Owner)) _DATABASE:CreateEventDynamicCargoUnloaded(self) else self._landAglConfirm=true end else self._landAglConfirm=nil end end end return self 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() if not self:_IsC130Type(typename)then local dimensions=DYNAMICCARGO.AircraftDimensions[typename] if hpos and typename and dimensions then 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)) 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 if self:_ShouldUseC130State(pos)then self:_UpdatePositionC130(pos) self.LastPosition=pos elseif moved then if self.CargoState==DYNAMICCARGO.State.NEW or self.CargoState==DYNAMICCARGO.State.UNLOADED then local isloaded,client,playername=self:_GetPossibleHeloNearby(pos,true) if isloaded then self:T(self.lid.." moved! NEW -> LOADED by "..tostring(playername)) self.CargoState=DYNAMICCARGO.State.LOADED self.Owner=playername if client then self:_SetCarrierFromClient(client,playername) end _DATABASE:CreateEventDynamicCargoLoaded(self) end end self.LastPosition=pos 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 if client then self:_SetCarrierFromClient(client,playername) end _DATABASE:CreateEventDynamicCargoUnloaded(self) end end end else if self.CargoState~=DYNAMICCARGO.State.REMOVED then DYNAMICCARGO._UntrackCargo(self.StaticName) self.timer=nil self:T(self.lid.." dead! "..self.CargoState.."-> REMOVED") self.CargoState=DYNAMICCARGO.State.REMOVED _DATABASE:CreateEventDynamicCargoRemoved(self) end end return self end function DYNAMICCARGO:Destroy(GenerateEvent) local DCSObject=self:GetDCSObject() if DCSObject then local GenerateEvent=(GenerateEvent~=nil and GenerateEvent==false)and false or true if GenerateEvent and GenerateEvent==true then self:CreateEventDead(timer.getTime(),DCSObject) end DCSObject:destroy() self:_UpdatePosition() return true end return nil 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 SCORING={ ClassName="SCORING", ClassID=0, Players={}, AutoSave=true, version="1.18.5", ScoringScenery=nil, SceneryHitsInZone=false, LoadSave=false, } 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,LoadSave) 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.SceneryHitsInZone=false if LoadSave then self.LoadSave=LoadSave end 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==nil or AutoSave==true)and true or false if self.AutoSavePath and self.AutoSave==true then self:OpenCSV(GameName) end self:I("SCORING "..tostring(GameName).." started! v"..self.version) if LoadSave==true then self:_LoadPlayerSummaryScore() end return self end function SCORING:_LoadPlayerSummaryScore() if lfs and io and self.LoadSave==true then local path=self.AutoSavePath or lfs.writedir()..[[Logs\]] local filename=self.GameName or"PlayerScoresSummary" filename=filename..".csv" if UTILS.CheckFileExists(path,filename)then local ok,data=UTILS.LoadFromFile(path,filename) table.remove(data,1) for _,_data in pairs(data)do local line=UTILS.Split(_data,";;") local playername=tostring(line[1]) local score=tonumber(line[2]) local penalty=tonumber(line[3]) self:I(string.format("Player %s Score %d Penalty %d",playername,score,penalty)) local PlayerData=self.Players[playername] if not PlayerData then PlayerData={} PlayerData.Hit={} PlayerData.Destroy={} PlayerData.Goals={} PlayerData.Goals[self.GameName]={Score=score,Penalty=penalty} PlayerData.Mission={} PlayerData.HitPlayers={} PlayerData.Score=score PlayerData.Penalty=penalty PlayerData.PenaltyCoalition=0 PlayerData.PenaltyWarning=0 self.Players[playername]=PlayerData else PlayerData.Score=score PlayerData.Penalty=penalty self.Players[playername]=PlayerData PlayerData.Goals[self.GameName]={Score=score,Penalty=penalty} end end end end 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) return self:AddScoreStatic(ScoreStatic,Score) end function SCORING:AddScoreStatic(ScoreStatic,Score) if ScoreStatic==nil then BASE:E("SCORING.AddStaticScore: Parameter ScoreStatic is nil!") return self end local StaticName=ScoreStatic:GetName() self.ScoringObjects[StaticName]=Score return self end function SCORING:AddScoreScenery(ScoreScenery,Score) if ScoreScenery==nil then self:E("SCORING.ScoreScenery: Parameter ScoreScenery is nil!") return self end if not self.ScoringScenery then self.ScoringScenery=SET_SCENERY:New() end local StaticName=ScoreScenery:GetName() self:T("Scenery name = "..StaticName) self.ScoringScenery:AddScenery(ScoreScenery) return self end function SCORING:RemoveSceneryScore(ScoreScenery) local StaticName=ScoreScenery:GetName() self.ScoringObjects[StaticName]=nil return self end function SCORING:AddScoreSetScenery(Set,Score) local set=Set.Set for _,_static in pairs(set)do if _static~=nil then self:AddScoreScenery(_static,Score) end end 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:AddScoreSetStatic(Set,Score) local set=Set:GetSetObjects() for _,_static in pairs(set)do if _static and _static:IsAlive()then self:AddStaticScore(_static,Score) end end local function AddScore(static) self:AddStaticScore(static,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:EnableSceneryHitsinZones() self.SceneryHitsInZone=true return self end function SCORING:DisableSceneryHitsinZones() self.SceneryHitsInZone=false return self end function SCORING:AddZoneScoreSet(ScoreZoneSet,Score) for _,_zone in pairs(ScoreZoneSet.Set or{})do self:AddZoneScore(_zone,Score) end 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] if not PlayerData then PlayerData={} PlayerData.Goals={} PlayerData.Hit={} PlayerData.Destroy={} PlayerData.Goals={} PlayerData.Mission={} PlayerData.HitPlayers={} PlayerData.Score=0 PlayerData.Penalty=0 PlayerData.PenaltyCoalition=0 PlayerData.PenaltyWarning=0 self.Players[PlayerName]=PlayerData end 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) 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 local TargetIsScenery=false local TargetSceneryObject=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 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 if TargetUNIT~=nil and TargetUNIT:IsInstanceOf("SCENERY")then TargetCategory=Unit.Category.STRUCTURE TargetIsScenery=true TargetType="Scenery" TargetSceneryObject=TargetUNIT self:T("***** Target is Scenery and TargetUNIT is SCENERY object!") end TargetUnitCoalition=_SCORINGCoalition[TargetCoalition] TargetUnitCategory=_SCORINGCategory[TargetCategory] TargetUnitType=TargetType 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~=nil)and(TargetIsScenery==false)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 and 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()) elseif TargetIsScenery~=true then 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()) elseif TargetIsScenery==true then MESSAGE:NewType(self.DisplayMessagePrefix.."Player '"..InitPlayerName.."' hit scenery object.".." 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 nothing special.", 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 or TargetIsScenery==true then local playername=Event.WeaponPlayerName or Event.IniPlayerName or"Ghost" if self.Players[playername]then self:T("Hitting Scenery or Static") if Event.TgtObjectCategory then local Player=self.Players[playername] 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 and 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 and PlayerHit.UNIT.ThreatLevel or 1 PlayerHit.ThreatType=PlayerHit.UNIT and PlayerHit.UNIT.ThreatType or"Unknown" end if timer.getTime()-PlayerHit.TimeStamp>1 then PlayerHit.TimeStamp=timer.getTime() local Score=0 local TgtName=Event.TgtDCSUnit and Event.TgtDCSUnit.getName and Event.TgtDCSUnit:getName()or"Unknown" if TargetIsScenery==true and self.ScoringScenery:IsInSet(TargetSceneryObject)then Player.Score=Player.Score+self.ScoreIncrementOnHit PlayerHit.Score=PlayerHit.Score+self.ScoreIncrementOnHit PlayerHit.ScoreHit=PlayerHit.ScoreHit+1 MESSAGE:NewType(self.DisplayMessagePrefix.."Player '"..playername.."' hit scenery 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(playername,TargetPlayerName,"HIT_SCORE",1,1,Event.WeaponName,Event.WeaponCoalition,Event.WeaponCategory,Event.WeaponTypeName,TargetUnitName,TargetUnitCoalition,TargetUnitCategory,TargetUnitType) elseif TargetIsScenery==false and Event.TgtObjectCategory==Object.Category.STATIC and self.ScoringObjects[TgtName]then Player.Score=Player.Score+self.ScoreIncrementOnHit PlayerHit.Score=PlayerHit.Score+self.ScoreIncrementOnHit PlayerHit.ScoreHit=PlayerHit.ScoreHit+1 MESSAGE:NewType(self.DisplayMessagePrefix.."Player '"..playername.."' hit static 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(playername,TargetPlayerName,"HIT_SCORE",1,1,Event.WeaponName,Event.WeaponCoalition,Event.WeaponCategory,Event.WeaponTypeName,TargetUnitName,TargetUnitCoalition,TargetUnitCategory,TargetUnitType) else self:E("Hit unregistered scenery or static object - NO target! ("..TgtName..")") end end end end for ZoneName,ScoreZoneData in pairs(self.ScoringZones)do self:F({ScoringZone=ScoreZoneData}) local hit=Event.TgtUnit local ScoreZone=ScoreZoneData.ScoreZone local Score=ScoreZoneData.Score if TargetUNIT and ScoreZone:IsVec2InZone(TargetUNIT:GetVec2())then local PlayerName=Event.IniPlayerName or"Ghost" local Player=self.Players[PlayerName] if Player then Player.Score=Player.Score+Score Player.Score=Player.Score+self.ScoreIncrementOnHit MESSAGE:NewType(self.DisplayMessagePrefix.."hit 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,"","HIT_SCORE",1,Score,InitUnitName,InitUnitCoalition,InitUnitCategory,InitUnitType,TargetUnitName,"","Zone",TargetUnitType) 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 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 if self.SceneryHitsInZone==true then 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 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,JustScore) local PlayerMessage="" local ReportTable={} 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 if JustScore~=true then PlayerMessage= string.format("Player '%s' Score = %d ( %d Score, -%d Penalties )", PlayerName, PlayerScore-PlayerPenalty, PlayerScore, PlayerPenalty ) MESSAGE:NewType(PlayerMessage,MESSAGE.Type.Overview):ToGroup(PlayerGroup) else ReportTable[PlayerName]={["Score"]=PlayerScore,["Penalty"]=PlayerPenalty} end end end return ReportTable 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==true and self.CSVFile~=nil 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 if lfs and io and self.LoadSave==true then local path=self.AutoSavePath or lfs.writedir()..[[Logs\]] local filename=self.GameName or"PlayerScoresSummary" filename=filename..".csv" local data=self:ReportScoreAllSummary("",true) local text="-- Playername;;Score;;Penalty\n" for _playername,_data in pairs(data or{})do local Playername=_playername or"Ghost" local Score=_data.Score or 0 local Penalty=_data.Penalty or 0 text=text..string.format("%s;;%d;;%d\n",Playername,Score,Penalty) end UTILS.SaveToFile(path,filename,text) 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.11") 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 local WeaponWrapper=WEAPON:New(SEADWeapon) 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,WeaponWrapper) 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() local shoradactive=_targetgroup:GetProperty("SHORAD_ACTIVE") if not self.SuppressedGroups[_targetgroupname]and shoradactive~=true then local allow=true if self.UseCallBack and self.CallBack and self.CallBack.SeadAllowSuppression then allow=self.CallBack:SeadAllowSuppression(_targetgroup,_targetgroupname,SEADGroup,SEADWeaponName,Weapon,_tti,delay) end if not allow then self:T(string.format("*** SEAD - %s | Suppression vetoed by callback",_targetgroupname)) return self end 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 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() Intercept=Intercept or false 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) 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.Detection=Detection self.AttackSet=AttackSet self.RecceSet=Detection:GetDetectionSet() self.Recces={} self.Designating={} self:SetDesignateName() self:SetLaseDuration() self:SetFlashStatusMenu(false) self:SetFlashDetectionMessages(true) 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" MESSAGE:New(self.DesignateName..": Auto Lase "..AutoLaseOnOff..".",15):ToSet(self.AttackSet) end self:CoordinateLase() self:SetDesignateMenu() return self end function DESIGNATE:SetThreatLevelPrioritization(Prioritize) self.ThreatLevelPrioritization=Prioritize return self end function DESIGNATE:SetMission(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(", ") MESSAGE:New("Targets out of LOS\n"..DetectionText,10,self.DesignateName):ToGroup(AttackGroup) 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(", ") MESSAGE:New("Targets detected at \n"..DetectionText,10,self.DesignateName):ToGroup(AttackGroup) 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 MESSAGE:New(DetectedReport:Text("\n"),15,self.DesignateName):ToGroup(AttackGroup) 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 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 then self:T2(self.lid..string.format("Stopping RAT in %d sec!",delay)) self:ScheduleOnce(delay,RAT.Stop,self) else self:T(self.lid.."Stopping RAT: Clearing schedulers and unhandling events!") if self.sid_Activate then self.Scheduler:ScheduleStop(self.sid_Activate) end if self.sid_Spawn then self.Scheduler:ScheduleStop(self.sid_Spawn) end if self.sid_Status then self.Scheduler:ScheduleStop(self.sid_Status) end if self.Scheduler then self.Scheduler:Clear() end self.norespawn=true self:UnHandleEvent(EVENTS.Birth) self:UnHandleEvent(EVENTS.EngineStartup) self:UnHandleEvent(EVENTS.Takeoff) self:UnHandleEvent(EVENTS.Land) self:UnHandleEvent(EVENTS.EngineShutdown) self:UnHandleEvent(EVENTS.Dead) self:UnHandleEvent(EVENTS.Crash) self:UnHandleEvent(EVENTS.Hit) end end function RAT:Spawn(naircraft) if self.spawninitialized==true then self:E("ERROR: Spawn function should only be called once per RAT object! Exiting and returning nil.") return nil else self.spawninitialized=true end self.ngroups=naircraft or 1 if self.ATCswitch and not RAT.ATC.init then RAT._ATCInit(self.airports_map) end if self.f10menu and not RAT.MenuF10 then RAT.MenuF10=MENU_MISSION:New("RAT") end self:_SetCoalitionTable() self:_GetAirportsOfCoalition() if not self.SubMenuName then self.SubMenuName=self.alias end if self.departure_Azone~=nil then self.departure_ports=self:_GetAirportsInZone(self.departure_Azone) end if self.destination_Azone~=nil then self.destination_ports=self:_GetAirportsInZone(self.destination_Azone) end if self.addfriendlydepartures then self:_AddFriendlyAirports(self.departure_ports) end if self.addfriendlydestinations then self:_AddFriendlyAirports(self.destination_ports) end if self.FLcruise==nil then if self.category==RAT.cat.plane then self.FLcruise=200*RAT.unit.FL2m else self.FLcruise=005*RAT.unit.FL2m end end if self.category==RAT.cat.heli then self.mindist=50 end self:_CheckConsistency() local text=string.format("\n******************************************************\n") text=text..string.format("Spawning %i aircraft from template %s of type %s.\n",self.ngroups,self.SpawnTemplatePrefix,self.aircraft.type) text=text..string.format("Alias: %s\n",self.alias) text=text..string.format("Category: %s\n",self.category) text=text..string.format("Friendly coalitions: %s\n",self.friendly) text=text..string.format("Number of airports on map : %i\n",#self.airports_map) text=text..string.format("Number of friendly airports: %i\n",#self.airports) text=text..string.format("Totally random departure: %s\n",tostring(self.random_departure)) if not self.random_departure then text=text..string.format("Number of departure airports: %d\n",self.Ndeparture_Airports) text=text..string.format("Number of departure zones : %d\n",self.Ndeparture_Zones) end text=text..string.format("Totally random destination: %s\n",tostring(self.random_destination)) if not self.random_destination then text=text..string.format("Number of destination airports: %d\n",self.Ndestination_Airports) text=text..string.format("Number of destination zones : %d\n",self.Ndestination_Zones) end text=text..string.format("Min dist to destination: %4.1f\n",self.mindist) text=text..string.format("Max dist to destination: %4.1f\n",self.maxdist) text=text..string.format("Terminal type: %s\n",tostring(self.termtype)) text=text..string.format("Takeoff type: %i\n",self.takeoff) text=text..string.format("Landing type: %i\n",self.landing) text=text..string.format("Commute: %s\n",tostring(self.commute)) text=text..string.format("Journey: %s\n",tostring(self.continuejourney)) text=text..string.format("Destination Zone: %s\n",tostring(self.destinationzone)) text=text..string.format("Return Zone: %s\n",tostring(self.returnzone)) text=text..string.format("Spawn delay: %4.1f\n",self.spawndelay) text=text..string.format("Spawn interval: %4.1f\n",self.spawninterval) text=text..string.format("Respawn delay: %s\n",tostring(self.respawn_delay)) text=text..string.format("Respawn off: %s\n",tostring(self.norespawn)) text=text..string.format("Respawn after landing: %s\n",tostring(self.respawn_at_landing)) text=text..string.format("Respawn after take-off: %s\n",tostring(self.respawn_after_takeoff)) text=text..string.format("Respawn after crash: %s\n",tostring(self.respawn_after_crash)) text=text..string.format("Respawn in air: %s\n",tostring(self.respawn_inair)) text=text..string.format("ROE: %s\n",tostring(self.roe)) text=text..string.format("ROT: %s\n",tostring(self.rot)) text=text..string.format("Immortal: %s\n",tostring(self.immortal)) text=text..string.format("Invisible: %s\n",tostring(self.invisible)) text=text..string.format("Vclimb: %4.1f\n",self.Vclimb) text=text..string.format("AlphaDescent: %4.2f\n",self.AlphaDescent) text=text..string.format("Vcruisemax: %s\n",tostring(self.Vcruisemax)) text=text..string.format("FLcruise = %6.1f km = FL%3.0f\n",self.FLcruise/1000,self.FLcruise/RAT.unit.FL2m) text=text..string.format("FLuser: %s\n",tostring(self.Fluser)) text=text..string.format("FLminuser: %s\n",tostring(self.FLminuser)) text=text..string.format("FLmaxuser: %s\n",tostring(self.FLmaxuser)) text=text..string.format("Place markers: %s\n",tostring(self.placemarkers)) text=text..string.format("Report status: %s\n",tostring(self.reportstatus)) text=text..string.format("Status interval: %4.1f\n",self.statusinterval) text=text..string.format("Time inactive: %4.1f\n",self.Tinactive) text=text..string.format("Create F10 menu : %s\n",tostring(self.f10menu)) text=text..string.format("F10 submenu name: %s\n",self.SubMenuName) text=text..string.format("ATC enabled : %s\n",tostring(self.ATCswitch)) text=text..string.format("Radio comms : %s\n",tostring(self.radio)) text=text..string.format("Radio frequency : %s\n",tostring(self.frequency)) text=text..string.format("Radio modulation : %s\n",tostring(self.frequency)) text=text..string.format("Tail # prefix : %s\n",tostring(self.onboardnum)) text=text..string.format("Check on runway: %s\n",tostring(self.checkonrunway)) text=text..string.format("Max respawn attempts: %s\n",tostring(self.onrunwaymaxretry)) text=text..string.format("Check on top: %s\n",tostring(self.checkontop)) text=text..string.format("Uncontrolled: %s\n",tostring(self.uncontrolled)) if self.uncontrolled and self.activate_uncontrolled then text=text..string.format("Uncontrolled max : %4.1f\n",self.activate_max) text=text..string.format("Uncontrolled delay: %4.1f\n",self.activate_delay) text=text..string.format("Uncontrolled delta: %4.1f\n",self.activate_delta) text=text..string.format("Uncontrolled frand: %4.1f\n",self.activate_frand) end if self.livery then text=text..string.format("Available liveries:\n") for _,livery in pairs(self.livery)do text=text..string.format("- %s\n",livery) end end text=text..string.format("******************************************************\n") self:T(self.lid..text) if self.f10menu then self.Menu[self.SubMenuName]=MENU_MISSION:New(self.SubMenuName,RAT.MenuF10) self.Menu[self.SubMenuName]["groups"]=MENU_MISSION:New("Groups",self.Menu[self.SubMenuName]) MENU_MISSION_COMMAND:New("Spawn new group",self.Menu[self.SubMenuName],self._SpawnWithRoute,self) MENU_MISSION_COMMAND:New("Delete markers",self.Menu[self.SubMenuName],self._DeleteMarkers,self) MENU_MISSION_COMMAND:New("Status report",self.Menu[self.SubMenuName],self.Status,self,true) end local Tstart=self.spawndelay local dt=self.spawninterval if self.takeoff==RAT.wp.runway and not self.random_departure then dt=math.max(dt,180) end local Tstop=Tstart+dt*(self.ngroups-1) self.sid_Status=self:ScheduleRepeat(Tstart+1,self.statusinterval,nil,nil,RAT.Status,self) self:HandleEvent(EVENTS.Birth,self._OnBirth) self:HandleEvent(EVENTS.EngineStartup,self._OnEngineStartup) self:HandleEvent(EVENTS.Takeoff,self._OnTakeoff) self:HandleEvent(EVENTS.Land,self._OnLand) self:HandleEvent(EVENTS.EngineShutdown,self._OnEngineShutdown) self:HandleEvent(EVENTS.Dead,self._OnDeadOrCrash) self:HandleEvent(EVENTS.Crash,self._OnDeadOrCrash) self:HandleEvent(EVENTS.Hit,self._OnHit) if self.ngroups==0 then return nil end self.sid_Spawn=self:ScheduleRepeat(Tstart,dt,0.0,Tstop,RAT._SpawnWithRoute,self) if self.uncontrolled and self.activate_uncontrolled then self.sid_Activate=self:ScheduleRepeat(self.activate_delay,self.activate_delta,self.activate_frand,nil,RAT._ActivateUncontrolled,self) end return true end function RAT:_CheckConsistency() self:F2() if not self.random_departure then for _,name in pairs(self.departure_ports)do if self:_AirportExists(name)then self.Ndeparture_Airports=self.Ndeparture_Airports+1 elseif self:_ZoneExists(name)then self.Ndeparture_Zones=self.Ndeparture_Zones+1 end end if self.Ndeparture_Zones>0 and self.takeoff~=RAT.wp.air then self.takeoff=RAT.wp.air self:E(self.lid..string.format("WARNING: 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(self.lid.."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(self.lid.."WARNING: 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(self.lid.."ERROR: "..text) MESSAGE:New(text,30):ToAll() end end if self.destinationzone and self.returnzone then self:E(self.lid.."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(self.lid.."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(self.lid.."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(self.lid.."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(self.lid.."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:SetSpawnLimit(Nmax) self.NspawnMax=Nmax return self end function RAT:RespawnAfterLanding(delay) self:F2(delay) self.respawn_at_landing=true self:SetRespawnDelay(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: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:SetMinCruiseSpeed(speed) self:F2(speed) self.Vcruisemin=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() self:I({typename=DCStype}) UTILS.PrintTableToLog(DCSdesc.box,1,noprint,3,seen) 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(self.lid.."ERROR: Group of RAT is neither airplane nor helicopter!") end self.aircraft.type=DCStype self.aircraft.fuel=DCSunit:getFuel() self.aircraft.Rmax=DCSdesc.range*RAT.unit.nm2m self.aircraft.Reff=self.aircraft.Rmax*self.aircraft.fuel*0.95 self.aircraft.Vmax=DCSdesc.speedMax self.aircraft.Vymax=DCSdesc.VyMax self.aircraft.ceiling=DCSdesc.Hmax self.aircraft.length=12 self.aircraft.height=4 self.aircraft.width=10.3 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 elseif DCStype=="vwv_l-1049"then self.aircraft.length=35.41 self.aircraft.height=7.54 self.aircraft.width=38.47 elseif DCStype=="uh2b"then self.aircraft.length=11.48 self.aircraft.height=4.11 self.aircraft.width=13.41 elseif DCStype=="F-14A-135-GR"then self.aircraft.length=12 self.aircraft.height=4 self.aircraft.width=10.3 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(self.lid..text) end function RAT:_SpawnWithRoute(_departure,_destination,_takeoff,_landing,_livery,_waypoint,_lastpos,_nrespawn,parkingdata) self:F({rat=self.lid,departure=_departure,destination=_destination,takeoff=_takeoff,landing=_landing,livery=_livery,waypoint=_waypoint,lastpos=_lastpos,nrespawn=_nrespawn}) if self.NspawnMax and self.SpawnIndex>=self.NspawnMax then self:T(self.lid..string.format("Max limit of spawns reached %d >= %d! Will not spawn any more groups",self.NspawnMax,self.SpawnIndex)) return else self:T2(self.lid..string.format("Spawning with spawn index=%d",self.SpawnIndex)) end 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,wpdesc,wpstatus=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(self.lid..text) else livery=nil end local uncontrolled=self.uncontrolled local isFlightcontrol=self:_IsFlightControlAirbase(departure) if takeoff~=RAT.wp.air and departure and isFlightcontrol then takeoff=RAT.wp.cold uncontrolled=true end local successful=self:_ModifySpawnTemplate(waypoints,livery,_lastpos,departure,takeoff,parkingdata,uncontrolled) if not successful then return nil end local group=self:SpawnWithIndex(self.SpawnIndex) local groupname=group:GetName() local flightgroup=FLIGHTGROUP:New(group) if self.ATCswitch then flightgroup.holdtime=nil end flightgroup.stuckDespawn=false self.alive=self.alive+1 self:T(self.lid..string.format("Alive groups counter now = %d.",self.alive)) if self.ATCswitch and landing==RAT.wp.landing then local airbasename=destination:GetName() if self.returnzone then airbasename=departure:GetName() end if not self:_IsFlightControlAirbase(airbasename)then self:_ATCAddFlight(groupname,airbasename) end end if self.placemarkers then self:_PlaceMarkers(waypoints,wpdesc,self.SpawnIndex) end if isFlightcontrol and not self.activate_uncontrolled then local N=math.random(120) self:T(self.lid..string.format("Flight will be ready for takeoff in %d seconds",N)) flightgroup:SetReadyForTakeoff(true,N) end if self.invisible then flightgroup:SetDefaultInvisible(true) flightgroup:SwitchInvisible(true) end if self.immortal then flightgroup:SetDefaultImmortal(true) flightgroup:SwitchImmortal(true) end if self.eplrs then flightgroup:SetDefaultEPLRS(true) flightgroup:SwitchEPLRS(true) end self:_SetROE(flightgroup,self.roe) self:_SetROT(flightgroup,self.rot) local ratcraft={} ratcraft.index=self.SpawnIndex ratcraft.group=group ratcraft.flightgroup=flightgroup ratcraft.destination=destination ratcraft.departure=departure ratcraft.waypoints=waypoints ratcraft.airborne=group:InAir() ratcraft.nunits=group:GetInitialSize() ratcraft.Pnow=group:GetCoordinate() ratcraft.Distance=0 ratcraft.takeoff=takeoff ratcraft.landing=landing ratcraft.wpdesc=wpdesc ratcraft.wpstatus=wpstatus ratcraft.active=not uncontrolled ratcraft.status=RAT.status.Spawned ratcraft.livery=livery ratcraft.despawnme=false ratcraft.nrespawn=nrespawn self.ratcraft[self.SpawnIndex]=ratcraft 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,flightgroup,RAT.ROE.weaponhold) MENU_MISSION_COMMAND:New("Weapons free",self.Menu[self.SubMenuName].groups[self.SpawnIndex]["roe"],self._SetROE,self,flightgroup,RAT.ROE.weaponfree) MENU_MISSION_COMMAND:New("Return fire",self.Menu[self.SubMenuName].groups[self.SpawnIndex]["roe"],self._SetROE,self,flightgroup,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,flightgroup,RAT.ROT.noreaction) MENU_MISSION_COMMAND:New("Passive defense",self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"],self._SetROT,self,flightgroup,RAT.ROT.passive) MENU_MISSION_COMMAND:New("Evade on fire",self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"],self._SetROT,self,flightgroup,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 function flightgroup.OnAfterPassingWaypoint(Flightgroup,From,Event,To,Waypoint) local waypoint=Waypoint local flightgroup=Flightgroup local wpid=waypoint.uid local ratcraft=self:_GetRatcraftFromGroup(flightgroup.group) local wpdescription=tostring(ratcraft.wpdesc[wpid]) local wpstatus=ratcraft.wpstatus[wpid] self:T(self.lid..string.format("RAT passed waypoint %s [uid=%d]: %s [status=%s]",waypoint.name,wpid,wpdescription,wpstatus)) self:_SetStatus(group,wpstatus) if waypoint.uid==3 then end end function flightgroup.OnAfterPassedFinalWaypoint(flightgroup,From,Event,To) self:T(self.lid..string.format("RAT passed FINAL waypoint")) local ratcraft=self:_GetRatcraftFromGroup(flightgroup.group) local text=string.format("Flight %s arrived at final destination %s.",group:GetName(),destination:GetName()) MESSAGE:New(text,10):ToAllIf(self.reportstatus) self:T(self.lid..text) if landing==RAT.wp.air then local text=string.format("Activating despawn switch for flight %s! Group will be detroyed soon.",group:GetName()) MESSAGE:New(text,10):ToAllIf(self.Debug) self:T(self.lid..text) ratcraft.despawnme=true end end function flightgroup.OnAfterRTB(flightgroup,From,Event,To,airbase,SpeedTo,SpeedHold,SpeedLand) self:T(self.lid..string.format("RAT group is RTB")) end function flightgroup.OnAfterHolding(Flightgroup,From,Event,To) local flightgroup=Flightgroup local ratcraft=self:_GetRatcraftFromGroup(flightgroup.group) local destinationname=ratcraft.destination:GetName() local text=string.format("Flight %s to %s ATC: Holding and awaiting landing clearance.",groupname,destinationname) self:T(self.lid..text) MESSAGE:New(text,10):ToAllIf(self.reportstatus) local fc=_DATABASE:GetFlightControl(destinationname) if self.ATCswitch and not fc then self:T(self.lid..string.format("RAT group is HOLDING ==> ATCRegisterFlight")) if self.f10menu then MENU_MISSION_COMMAND:New("Clear for landing",self.Menu[self.SubMenuName].groups[self.SpawnIndex],flightgroup.ClearToLand,flightgroup) end RAT._ATCRegisterFlight(groupname,timer.getTime()) end end function flightgroup.OnAfterLanded(Flightgroup,From,Event,To,Airport) self:T(self.lid..string.format("RAT group landed at airbase")) end function flightgroup.OnAfterArrived(Flightgroup,From,Event,To) self:T(self.lid..string.format("RAT group arrived")) end function flightgroup.OnAfterStuck(Flightgroup,From,Event,To,Stucktime) local flightgroup=Flightgroup self:T(self.lid..string.format("Group %s got stuck for %d seconds",flightgroup:GetName(),Stucktime)) if Stucktime>10*60 then self:_Respawn(flightgroup.group) end end return self.SpawnIndex end function RAT:_IsFlightControlAirbase(airbase) if type(airbase)=="table"then airbase=airbase:GetName() end if airbase then local fc=_DATABASE:GetFlightControl(airbase) if fc then self:T(self.lid..string.format("Airbase %s has a FLIGHTCONTROL running",airbase)) return true else return false end end return nil end function RAT:ClearForLanding(name) trigger.action.setUserFlag(name,1) local flagvalue=trigger.misc.getUserFlag(name) self:T(self.lid.."ATC: User flag value (landing) for "..name.." set to "..flagvalue) end function RAT:_Respawn(group,lastpos,delay) if delay and delay>0 then self:ScheduleOnce(delay,RAT._Respawn,self,group,lastpos,0) else if group then self:T(self.lid..string.format("Respawning ratcraft from group %s",group:GetName())) else self:E(self.lid..string.format("ERROR: group is nil in _Respawn!")) return nil end local ratcraft=self:_GetRatcraftFromGroup(group) lastpos=lastpos or group:GetCoordinate() local departure=ratcraft.departure local destination=ratcraft.destination local takeoff=ratcraft.takeoff local landing=ratcraft.landing local livery=ratcraft.livery local lastwp=ratcraft.waypoints[#ratcraft.waypoints] local flightgroup=ratcraft.flightgroup local parkingdata=nil if self.continuejourney or self.commute then for _,_element in pairs(flightgroup.elements)do local element=_element if element.parking then if parkingdata==nil then parkingdata={} end self:T(self.lid..string.format("Element %s was parking at spot id=%d",element.name,element.parking.TerminalID)) table.insert(parkingdata,UTILS.DeepCopy(element.parking)) else self:E(self.lid..string.format("WARNING: Element %s did NOT have a not parking spot!",tostring(element.name))) end end end self:_Despawn(ratcraft.group) 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 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=self.respawn_delay or 1 self:T(self.lid..string.format("%s delayed respawn in %.1f seconds.",self.alias,respawndelay)) self:ScheduleOnce(respawndelay,RAT._SpawnWithRoute,self,_departure,_destination,_takeoff,_landing,_livery,nil,_lastpos,nil,parkingdata) end end function RAT:_Despawn(group,delay) if delay and delay>0 then self:ScheduleOnce(delay,RAT._Despawn,self,group,0) else if group then local index=self:GetSpawnIndexFromGroup(group) if index then self:T(self.lid..string.format("Despawning group %s (index=%d)",group:GetName(),index)) local ratcraft=self.ratcraft[index] ratcraft.flightgroup:Despawn() ratcraft.flightgroup:__Stop(0.1) self.ratcraft[index].group=nil self.ratcraft[index]["status"]="Dead" self.ratcraft[index]=nil if self.f10menu and self.SubMenuName~=nil then self.Menu[self.SubMenuName]["groups"][index]:Remove() end end end end 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 if self.Vcruisemin then VxCruiseMin=self.Vcruisemin else VxCruiseMin=math.min(VxCruiseMax*0.70,166) end 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(self.lid..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(self.lid..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(self.lid..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(self.lid.."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(self.lid.."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(self.lid.."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) elseif destination:IsShip()then local _ship=UNIT:FindByName(destination:GetName()) local _shipHeading=_ship:GetHeading() Pdestination=destination:GetCoordinate() local _transitTime=Pdeparture:Get2DDistance(Pdestination)/VxCruise Pdestination.x=Pdestination.x+(_ship:GetGroundSpeed()*math.cos(math.rad(_shipHeading))*_transitTime) Pdestination.z=Pdestination.z+(_ship:GetGroundSpeed()*math.sin(math.rad(_shipHeading))*_transitTime) 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(self.lid..text) if d_cruise<0 then d_cruise=100 end local wp={} local c={} local waypointdescriptions={} local waypointstatus={} 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) waypointdescriptions[#wp]="Departure" 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) waypointdescriptions[#wp]="Begin of Cruise" 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) waypointdescriptions[#wp]="Climb" waypointstatus[#wp]=RAT.status.Climb wp[#wp+1]=self:_Waypoint(#wp+1,"Begin of Cruise",RAT.wp.cruise,c[#wp+1],VxCruise,FLcruise) waypointdescriptions[#wp]="Begin of Cruise" 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) waypointdescriptions[#wp]="Return Zone" 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) waypointdescriptions[#wp]="Final Destination" 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) waypointdescriptions[#wp]="End of Cruise" 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) waypointdescriptions[#wp]="End of Cruise" 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) waypointdescriptions[#wp]="Descent" 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) waypointdescriptions[#wp]="Descent" waypointstatus[#wp]=RAT.status.DescentHolding end end if landing==RAT.wp.landing then c[#c+1]=Pdestination wp[#wp+1]=self:_Waypoint(#wp+1,"Final Destination",landing,c[#wp+1],VxFinal,H_destination,destination) waypointdescriptions[#wp]="Final Destination" 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:",waypointdescriptions) if self.returnzone then return departure,destination_returnzone,waypoints,waypointdescriptions,waypointstatus else return departure,destination,waypoints,waypointdescriptions,waypointstatus 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(self.lid..string.format("ERROR! Takeoff is not in air. Cannot use %s as departure.",name)) end else self:E(self.lid..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(self.lid..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(self.lid..text) else self:E(self.lid..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(self.lid..string.format("ERROR! Landing is not in air. Cannot use zone %s as destination!",name)) end else self:E(self.lid..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(self.lid..text) end end end end end self:T(self.lid..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(self.lid..text) else self:E(self.lid.."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(self.lid..text) else self:E(self.lid..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(self.lid..text) end end function RAT:Status(message,forID) self:T(self.lid.."Checking status") local Tnow=timer.getTime() local nalive=0 for spawnindex,_ratcraft in pairs(self.ratcraft)do local ratcraft=_ratcraft self:T(self.lid..string.format("Ratcraft Index=%s",tostring(spawnindex))) local group=ratcraft.group if group and group:IsAlive()then nalive=nalive+1 self:T(self.lid..string.format("Ratcraft Index=%s is ALIVE",tostring(spawnindex))) local prefix=self:_GetPrefixFromGroup(group) local life=self:_GetLife(group) local fuel=group:GetFuel()*100.0 local airborne=group:InAir() local coords=group:GetCoordinate() local alt=coords~=nil and coords.y or 1000 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 Dtravel=0 if coords and ratcraft.Pnow then local Dtravel=coords:Get2DDistance(ratcraft.Pnow) ratcraft.Pnow=coords end ratcraft.Distance=ratcraft.Distance+Dtravel local Ddestination=-1 if ratcraft.Pnow then Ddestination=ratcraft.Pnow:Get2DDistance(ratcraft.destination:GetCoordinate()) end 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 dest = %6.1f km",Ddestination/1000) self:T(self.lid..text) if message then MESSAGE:New(text,20):ToAll() end end if ratcraft.despawnme then if self.norespawn or self.respawn_after_takeoff then if self.despawnair then self:T(self.lid..string.format("[STATUS despawnme] Flight %s will be despawned NOW and NO new group is created!",self.alias)) self:_Despawn(group) end else self:T(self.lid..string.format("[STATUS despawnme] Flight %s will be despawned NOW and a new group is respawned!",self.alias)) self:_Respawn(group) end end else local text=string.format("Group does not exist in loop ratcraft status for spawn index=%d",spawnindex) self:T2(self.lid..text) self:T2(ratcraft) end end local text=string.format("Alive groups of %s: %d, nalive=%d/%d",self.alias,self.alive,nalive,self.ngroups) self:T(self.lid..text) MESSAGE:New(text,20):ToAllIf(message and not forID) end function RAT:_RemoveRatcraft(ratcraft) self.ratcraft[ratcraft.index]=nil return self end function RAT:_GetRatcraftFromGroup(group) local index=self:GetSpawnIndexFromGroup(group) local ratcraft=self.ratcraft[index] return ratcraft 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(self.lid.."ERROR! Unit does not exist in RAT_Getlife(). Returning zero.") end else self:T2(self.lid.."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 ratcraft=self:_GetRatcraftFromGroup(group) if ratcraft then ratcraft.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(self.lid..text) if not(no1 or no2 or no3)then MESSAGE:New(text,10):ToAllIf(self.reportstatus) end end end end function RAT:_GetRatcraftFromGroup(group) if group then local index=self:GetSpawnIndexFromGroup(group) if self.ratcraft[index]then return self.ratcraft[index] end end return nil end function RAT:GetStatus(group) if group and group:IsAlive()then local ratcraft=self:_GetRatcraftFromGroup(group) if ratcraft then return ratcraft.status end end return"nonexistant" end function RAT:_OnBirth(EventData) self:F3(EventData) self:T3(self.lid.."Captured event birth!") local SpawnGroup=EventData.IniGroup if SpawnGroup then local EventPrefix=self:_GetPrefixFromGroup(SpawnGroup) if EventPrefix and EventPrefix==self.alias then local text="Event: Group "..SpawnGroup:GetName().." was born." self:T(self.lid..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 ratcraft=self.ratcraft[i] local _departure=ratcraft.departure:GetName() local _destination=ratcraft.destination:GetName() local _nrespawn=ratcraft.nrespawn local _takeoff=ratcraft.takeoff local _landing=ratcraft.landing local _livery=ratcraft.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(self.lid..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(self.lid..text) self:_Respawn(SpawnGroup,nil,3) else text="Event: Group "..SpawnGroup:GetName().." will be destroyed now" self:T(self.lid..text) self:_Despawn(SpawnGroup) end end end end else self:T2(self.lid.."ERROR: Group does not exist in RAT:_OnEngineShutdown().") end end function RAT:_OnHit(EventData) self:F3(EventData) self:T(self.lid..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(self.lid..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(self.lid.."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(self.lid..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(self.lid.."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(self.lid..text) local status=RAT.status.EventDead self:_SetStatus(SpawnGroup,status) end end else self:T2(self.lid.."ERROR: Group does not exist in RAT:_OnDead().") end end function RAT:_OnCrash(EventData) self:F3(EventData) self:T3(self.lid.."Captured event Crash!") local SpawnGroup=EventData.IniGroup if SpawnGroup then local EventPrefix=self:_GetPrefixFromGroup(SpawnGroup) if EventPrefix and EventPrefix==self.alias then local ratcraft=self:_GetRatcraftFromGroup(SpawnGroup) if ratcraft then ratcraft.nunits=ratcraft.nunits-1 local _n0=SpawnGroup:GetInitialSize() local text=string.format("Event: Group %s crashed. Unit %s. Units still alive %d of %d.",SpawnGroup:GetName(),EventData.IniUnitName,ratcraft.nunits,_n0) self:T(self.lid..text) local status=RAT.status.EventCrash self:_SetStatus(SpawnGroup,status) if ratcraft.nunits==0 and self.respawn_after_crash and not self.norespawn then self:T(self.lid..string.format("No units left of group %s. Group will be respawned now.",SpawnGroup:GetName())) self:_Respawn(SpawnGroup) end else self:E(self.lid..string.format("ERROR: Could not find ratcraft object for crashed group %s!",SpawnGroup:GetName())) end end else if self.Debug then self:E(self.lid.."ERROR: Group does not exist in RAT:_OnCrash()!") end end 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(self.lid.."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(self.lid..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(self.lid.."Unknown Airport category in _Waypoint()!") end end return RoutePoint end function RAT:_Routeinfo(waypoints,comment,waypointdescriptions) 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,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,waypointdescriptions[i],waypointdescriptions[i+1]) end text=text..string.format("Total distance = %6.1f km\n",total/1000) text=text..string.format("******************************************************\n") self:T2(self.lid..text) return total end function RAT:_AnticipatedGroupName(index) local index=index or self.SpawnIndex+1 return string.format("%s#%03d",self.alias,index) end function RAT:_ActivateUncontrolled() local idx={} local rat={} local nactive=0 for spawnindex,_ratcraft in pairs(self.ratcraft)do local ratcraft=_ratcraft 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(self.lid..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(self.lid..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(self.lid..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(self.lid..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(self.lid..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(self.lid..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(self.lid..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(self.lid..string.format("RAT group %s unit number %d: Parking = %s",self.alias,UnitID,tostring(UnitTemplate.parking))) self:T2(self.lid..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="Starting RAT ATC.\nSimultanious = "..RAT.ATC.Nclearance.."\n".."Delay = "..RAT.ATC.delay BASE:I(RAT.id..text) for _,ap in pairs(airports_map)do local airbase=ap local name=airbase:GetName() local fc=_DATABASE:GetFlightControl(name) if not fc then local airport={} airport.queue={} airport.busy=false airport.onfinal={} airport.Nonfinal=0 airport.traffic=0 airport.Tlastclearance=nil RAT.ATC.airport[name]=airport end end SCHEDULER:New(nil,RAT._ATCCheck,{},5,15) SCHEDULER:New(nil,RAT._ATCStatus,{},5,60) RAT.ATC.T0=timer.getTime() end RAT.ATC.init=true end function RAT:_ATCAddFlight(name,dest) BASE:T(RAT.id..string.format("ATC %s: Adding flight %s with destination %s.",dest,name,dest)) local flight={} flight.destination=dest flight.Tarrive=-1 flight.holding=-1 flight.Tarrive=-1 RAT.ATC.flight[name]=flight end function RAT._ATCDelFlight(t,entry) for k,_ in pairs(t)do if k==entry then BASE:T(RAT.id..string.format("Removing flight %s from queue",entry)) t[entry]=nil end end end function RAT._ATCRegisterFlight(name,time) BASE:T(RAT.id..string.format("Flight %s registered at ATC for landing clearance.",name)) RAT.ATC.flight[name].Tarrive=time RAT.ATC.flight[name].holding=0 end function RAT._ATCStatus() local Tnow=timer.getTime() for name,_flight in pairs(RAT.ATC.flight)do local flight=_flight local hold=RAT.ATC.flight[name].holding local dest=RAT.ATC.flight[name].destination local airport=RAT.ATC.airport[dest] if airport then if hold>=0 then local busy="Runway state is unknown" if airport.Nonfinal>0 then busy="Runway is occupied by "..airport.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 Tonfinal=flight.Tonfinal or timer.getTime()-1 local Tfinal=Tnow-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 else end end end function RAT._ATCCheck() RAT._ATCQueue() local Tnow=timer.getTime() for airportname,_airport in pairs(RAT.ATC.airport)do local airport=_airport for qID,flightname in pairs(airport.queue)do local flight=RAT.ATC.flight[flightname] local nqueue=#airport.queue local landing1=false if airport.Tlastclearance then landing1=(Tnow-airport.Tlastclearance>RAT.ATC.delay)and airport.Nonfinal=0 then flight.holding=Tnow-flight.Tarrive end local hold=flight.holding local dest=flight.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:I(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) delay=delay or 5 if delay and delay>0 then 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) self:ScheduleOnce(delay,RATMANAGER.Start,self,0) else 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 self:ScheduleOnce(time,RAT._SpawnWithRoute,self.rat[i]) end end for i=1,self.nrat do local rat=self.rat[i] if rat.uncontrolled and rat.activate_uncontrolled then local Tactivate=math.max(time+1,rat.activate_delay) self:ScheduleRepeat(Tactivate,rat.activate_delta,rat.activate_frand,nil,rat._ActivateUncontrolled,rat) 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:I(text) end return self end function RATMANAGER:Stop(delay) delay=delay or 1 if delay and delay>0 then self:I(RATMANAGER.id..string.format("Manager will be stopped in %d seconds.",delay)) self:ScheduleOnce(delay,RATMANAGER.Stop,self,0) else self:I(RATMANAGER.id..string.format("Stopping manager with scheduler ID %s",self.managerid)) self.manager:Stop(self.managerid) end 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() self:T(RATMANAGER.id..string.format("Number of alive groups %d. New groups to be spawned %d.",ntot,self.ntot-ntot)) 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 self:ScheduleOnce(time,RATMANAGER._Spawn,self,i) 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.1" 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,Provider,Backend) 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) if self.rangezone then self.controlmsrs:SetCoordinate(self.rangezone:GetCoordinate()) end if Backend then self.controlmsrs:SetBackend(Backend) end 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) if self.rangezone then self.instructmsrs:SetCoordinate(self.rangezone:GetCoordinate()) end 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 if Backend then self.instructmsrs:SetBackend(Backend) end if Provider then self.controlmsrs:SetProvider(Provider) self.instructmsrs:SetProvider(Provider) 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,Speaker) 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) if Speaker then self.controlmsrs:SetSpeakerPiper(Speaker) end self.controlmsrs:SetCulture(culture or"en-US") self.controlmsrs:SetGender(gender or"female") self.rangecontrol=true if relayunitname then local unit=UNIT:FindByName(relayunitname) if unit then local Coordinate=unit:GetCoordinate() self.rangecontrolrelayname=relayunitname self.controlmsrs:SetCoordinate(Coordinate) else MESSAGE:New("RANGE: Control Relay Unit "..relayunitname.." not found!",15,"ERROR"):ToAllIf(self.Debug):ToLog() end end return self end function RANGE:SetSRSRangeInstructor(frequency,modulation,voice,culture,gender,relayunitname,Speaker) 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) if Speaker then self.instructmsrs:SetSpeakerPiper(Speaker) end self.instructmsrs:SetCulture(culture or"en-US") self.instructmsrs:SetGender(gender or"male") self.instructor=true if relayunitname then local unit=UNIT:FindByName(relayunitname) if unit then local Coordinate=unit:GetCoordinate() self.instructmsrs:SetCoordinate(Coordinate) self.instructorrelayname=relayunitname else MESSAGE:New("RANGE: Instructor Relay Unit "..relayunitname.." not found!",15,"ERROR"):ToAllIf(self.Debug):ToLog() end 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}) randommove=randommove or false 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.delaysmoke then impactcoord:Smoke(playerData.smokecolor,30,self.TdelaySmoke) else impactcoord:Smoke(playerData.smokecolor,30) 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 if os and os.date then result.date=os.date() else self:E(self.lid.."os or os.date() not available") result.date="n/a" end 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:_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.4" 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: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:T(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() self:T2({Waypoints}) 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:T(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:T(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.6" 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) if self.radios then missionCommands.addCommandForGroup(GID,"Radios",submenu,self.ReportRadios,self,GID,UID,pos,name) end 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:ReportRadios(GID,UID,position,location) self:F({GID=GID,UID=UID,position=position,location=location}) if self.radios then local Text="" local radio=self.radios:GetClosestRadio(position,9) if radio then Text=self.radios:_GetMarkerText(radio) else Text=self.group[GID].player[UID].playername..", no radio information found!" end self:_DisplayMessageToGroup(self.group[GID].player[UID].unit,Text,self.mdur,true) end return self 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.distance/Scripts`") end return self end WAREHOUSE={ ClassName="WAREHOUSE", Debug=false, verbosity=0, lid=nil, Report=true, warehouse=nil, alias=nil, zone=nil, airbase=nil, airbasename=nil, road=nil, rail=nil, spawnzone=nil, uid=nil, dTstatus=30, queueid=0, stock={}, queue={}, pending={}, transporting={}, delivered={}, defending={}, portzone=nil, harborzone=nil, shippinglanes={}, offroadpaths={}, autodefence=false, spawnzonemaxdist=5000, autosave=false, autosavepath=nil, autosavefile=nil, saveparking=false, isUnit=false, isShip=false, lowfuelthresh=0.15, respawnafterdestroyed=false, respawndelay=nil, } WAREHOUSE.Descriptor={ GROUPNAME="templatename", UNITTYPE="unittype", ATTRIBUTE="attribute", CATEGORY="category", ASSIGNMENT="assignment", ASSETLIST="assetlist," } WAREHOUSE.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", } WAREHOUSE.TransportType={ AIRPLANE="Air_TransportPlane", HELICOPTER="Air_TransportHelo", APC="Ground_APC", TRAIN="Ground_Train", SHIP="Naval_UnarmedShip", AIRCRAFTCARRIER="Naval_AircraftCarrier", WARSHIP="Naval_WarShip", ARMEDSHIP="Naval_ArmedShip", SELFPROPELLED="Selfpropelled", } WAREHOUSE.Quantity={ ALL="all", THREEQUARTERS="3/4", HALF="1/2", THIRD="1/3", QUARTER="1/4", } _WAREHOUSEDB={ AssetID=0, Assets={}, WarehouseID=0, Warehouses={} } WAREHOUSE.version="2.0.0" function WAREHOUSE:New(warehouse,alias) local self=BASE:Inherit(self,FSM:New()) if type(warehouse)=="string"then local warehousename=warehouse warehouse=UNIT:FindByName(warehousename) if warehouse==nil then warehouse=STATIC:FindByName(warehousename,true) end end if warehouse==nil then env.error("ERROR: Warehouse does not exist!") return nil end if warehouse:IsInstanceOf("STATIC")then self.isUnit=false elseif warehouse:IsInstanceOf("UNIT")then self.isUnit=true if warehouse:IsShip()then self.isShip=true end else env.error("ERROR: Warehouse is neither STATIC nor UNIT object!") return nil end self.alias=alias or warehouse:GetName() self.lid=string.format("WAREHOUSE %s | ",self.alias) self:I(self.lid..string.format("Adding warehouse v%s for structure %s [isUnit=%s, isShip=%s]",WAREHOUSE.version,warehouse:GetName(),tostring(self:IsUnit()),tostring(self:IsShip()))) self.warehouse=warehouse _WAREHOUSEDB.WarehouseID=_WAREHOUSEDB.WarehouseID+1 self.uid=_WAREHOUSEDB.WarehouseID self.coalition=self.warehouse:GetCoalition() self.countryid=self.warehouse:GetCountry() local _airbase=self:GetCoordinate():GetClosestAirbase(nil,self:GetCoalition()) if _airbase and _airbase:GetCoordinate():Get2DDistance(self:GetCoordinate())<=5000 then self:SetAirbase(_airbase) end if self.isShip then self.zone=ZONE_AIRBASE:New(self.warehouse:GetName(),1000) self.spawnzone=ZONE_AIRBASE:New(self.warehouse:GetName(),1000) else self.zone=ZONE_RADIUS:New(string.format("Warehouse zone %s",self.warehouse:GetName()),warehouse:GetVec2(),500) self.spawnzone=ZONE_RADIUS:New(string.format("Warehouse %s spawn zone",self.warehouse:GetName()),warehouse:GetVec2(),250) end self:SetMarker(true) self:SetReportOff() self:SetRunwayRepairtime() self.allowSpawnOnClientSpots=false _WAREHOUSEDB.Warehouses[self.uid]=self self:SetStartState("NotReadyYet") self:AddTransition("NotReadyYet","Load","Loaded") self:AddTransition("Stopped","Load","Loaded") self:AddTransition("NotReadyYet","Start","Running") self:AddTransition("Loaded","Start","Running") self:AddTransition("*","Status","*") self:AddTransition("*","AddAsset","*") self:AddTransition("*","NewAsset","*") self:AddTransition("*","AddRequest","*") self:AddTransition("Running","Request","*") self:AddTransition("Running","RequestSpawned","*") self:AddTransition("Attacked","Request","*") self:AddTransition("*","Unloaded","*") self:AddTransition("*","AssetSpawned","*") self:AddTransition("*","AssetLowFuel","*") self:AddTransition("*","Arrived","*") self:AddTransition("*","Delivered","*") self:AddTransition("Running","SelfRequest","*") self:AddTransition("Attacked","SelfRequest","*") self:AddTransition("Running","Pause","Paused") self:AddTransition("Paused","Unpause","Running") self:AddTransition("*","Stop","Stopped") self:AddTransition("Stopped","Restart","Running") self:AddTransition("Loaded","Restart","Running") self:AddTransition("*","Save","*") self:AddTransition("*","Attacked","Attacked") self:AddTransition("Attacked","Defeated","Running") self:AddTransition("*","ChangeCountry","*") self:AddTransition("Attacked","Captured","Running") self:AddTransition("*","AirbaseCaptured","*") self:AddTransition("*","AirbaseRecaptured","*") self:AddTransition("*","RunwayDestroyed","*") self:AddTransition("*","RunwayRepaired","*") self:AddTransition("*","AssetDead","*") self:AddTransition("*","Destroyed","Destroyed") self:AddTransition("Destroyed","Respawn","Running") return self end function WAREHOUSE:SetDebugOn() self.Debug=true return self end function WAREHOUSE:SetDebugOff() self.Debug=false return self end function WAREHOUSE:SetReportOn() self.Report=true return self end function WAREHOUSE:SetReportOff() self.Report=false return self end function WAREHOUSE:SetSafeParkingOn() self.safeparking=true return self end function WAREHOUSE:SetSafeParkingOff() self.safeparking=false return self end function WAREHOUSE:SetAllowSpawnOnClientParking() self.allowSpawnOnClientSpots=true return self end function WAREHOUSE:SetLowFuelThreshold(threshold) self.lowfuelthresh=threshold or 0.15 return self end function WAREHOUSE:SetStatusUpdate(timeinterval) self.dTstatus=timeinterval return self end function WAREHOUSE:SetVerbosityLevel(VerbosityLevel) self.verbosity=VerbosityLevel or 0 return self end function WAREHOUSE:SetSpawnZone(zone,maxdist) self.spawnzone=zone self.spawnzonemaxdist=maxdist or 5000 return self end function WAREHOUSE:GetSpawnZone() return self.spawnzone end function WAREHOUSE:SetWarehouseZone(zone) self.zone=zone return self end function WAREHOUSE:GetWarehouseZone() return self.zone end function WAREHOUSE:SetAutoDefenceOn() self.autodefence=true return self end function WAREHOUSE:SetAutoDefenceOff() self.autodefence=false return self end function WAREHOUSE:SetParkingIDs(ParkingIDs) if type(ParkingIDs)~="table"then ParkingIDs={ParkingIDs} end self.parkingIDs=ParkingIDs return self end function WAREHOUSE:_CheckParkingValid(spot) if self.parkingIDs==nil then return true end for _,id in pairs(self.parkingIDs or{})do if spot.TerminalID==id then return true end end return false end function WAREHOUSE:_CheckParkingAsset(spot,asset) if asset.parkingIDs==nil then return true end for _,id in pairs(asset.parkingIDs or{})do if spot.TerminalID==id then return true end end return false end function WAREHOUSE:SetSaveOnMissionEnd(path,filename) self.autosave=true self.autosavepath=path self.autosavefile=filename return self end function WAREHOUSE:SetMarker(switch) if switch==false then self.markerOn=false else self.markerOn=true end return self end function WAREHOUSE:SetRespawnAfterDestroyed(delay) self.respawnafterdestroyed=true self.respawndelay=delay return self end function WAREHOUSE:SetAirbase(airbase) self.airbase=airbase if airbase~=nil then self.airbasename=airbase:GetName() else self.airbasename=nil end return self end function WAREHOUSE:SetRoadConnection(coordinate) if coordinate then self.road=coordinate:GetClosestPointToRoad() else self.road=false end return self end function WAREHOUSE:SetRailConnection(coordinate) if coordinate then self.rail=coordinate:GetClosestPointToRoad(true) else self.rail=false end return self end function WAREHOUSE:SetPortZone(zone) self.portzone=zone return self end function WAREHOUSE:SetHarborZone(zone) self.harborzone=zone return self end function WAREHOUSE:AddShippingLane(remotewarehouse,group,oneway) if self.portzone==nil or remotewarehouse.portzone==nil then local text=string.format("ERROR: Sending or receiving warehouse does not have a port zone defined. Adding shipping lane not possible!") self:_ErrorMessage(text,5) return self end local startcoord=self.portzone:GetRandomCoordinate() local finalcoord=remotewarehouse.portzone:GetRandomCoordinate() local lane=self:_NewLane(group,startcoord,finalcoord) if self.Debug then for i=1,#lane do local coord=lane[i] local text=string.format("Shipping lane %s to %s. Point %d.",self.alias,remotewarehouse.alias,i) coord:MarkToCoalition(text,self:GetCoalition()) end end local remotename=remotewarehouse.warehouse:GetName() if self.shippinglanes[remotename]==nil then self.shippinglanes[remotename]={} end table.insert(self.shippinglanes[remotename],lane) if not oneway then remotewarehouse:AddShippingLane(self,group,true) end return self end function WAREHOUSE:AddOffRoadPath(remotewarehouse,group,oneway) local startcoord=self.spawnzone:GetRandomCoordinate() local finalcoord=remotewarehouse.spawnzone:GetRandomCoordinate() local path=self:_NewLane(group,startcoord,finalcoord) if path==nil then self:E(self.lid.."ERROR: Offroad path could not be added. Group present in ME?") return end if path and self.Debug then for i=1,#path do local coord=path[i] local text=string.format("Off road path from %s to %s. Point %d.",self.alias,remotewarehouse.alias,i) coord:MarkToCoalition(text,self:GetCoalition()) end end local remotename=remotewarehouse.warehouse:GetName() if self.offroadpaths[remotename]==nil then self.offroadpaths[remotename]={} end table.insert(self.offroadpaths[remotename],path) if not oneway then remotewarehouse:AddOffRoadPath(self,group,true) end return self end function WAREHOUSE:_NewLane(group,startcoord,finalcoord) local lane=nil if group then local lanepoints=group:GetTemplateRoutePoints() local laneF=lanepoints[1] local laneL=lanepoints[#lanepoints] local coordF=COORDINATE:New(laneF.x,0,laneF.y) local coordL=COORDINATE:New(laneL.x,0,laneL.y) local distF=startcoord:Get2DDistance(coordF) local distL=startcoord:Get2DDistance(coordL) lane={} if distF0 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 then ishome=inspawnzone and notmoving elseif category==Group.Category.AIRPLANE then ishome=athomebase and onground and notmoving elseif category==Group.Category.HELICOPTER then ishome=(athomebase or inspawnzone)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) 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 unit=group:GetUnit(1) local Descriptors=(unit and unit:IsAlive()~=nil)and unit:GetDesc()or{} local Category=group:GetCategory() local TypeName=group:GetTypeName()or"none" 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:SetValidateAndRepositionGroundUnits(Enabled) self.ValidateAndRepositionGroundUnits=Enabled 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) 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 CargoTransport if Request.transporttype==WAREHOUSE.TransportType.AIRPLANE then CargoTransport=OPSTRANSPORT:New(CargoGroupSet,ZONE_AIRBASE:New(self.airbase:GetName()),ZONE_AIRBASE:New(Request.airbase:GetName())) CargoTransport:SetEmbarkZone(self.spawnzone) CargoTransport:SetDisembarkZone(Request.warehouse.spawnzone) elseif Request.transporttype==WAREHOUSE.TransportType.HELICOPTER then CargoTransport=OPSTRANSPORT:New(CargoGroupSet,self.spawnzone,Request.warehouse.spawnzone) elseif Request.transporttype==WAREHOUSE.TransportType.APC then CargoTransport=OPSTRANSPORT:New(CargoGroupSet,self.spawnzone,Request.warehouse.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 CargoTransport=OPSTRANSPORT:New(CargoGroupSet,self.portzone,Request.warehouse.portzone) CargoTransport:SetEmbarkZone(self.spawnzone) CargoTransport:SetDisembarkZone(Request.warehouse.spawnzone) local remotename=Request.warehouse.warehouse:GetName() local ShippingLane=self.shippinglanes[remotename][math.random(#self.shippinglanes[remotename])] else self:E(self.lid.."ERROR: Unknown transporttype!") end 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 CargoTransport.warehouse=self function CargoTransport:OnAfterLoaded(From,Event,To,OpsGroupCargo,OpsGroupCarrier,CarrierElement) local warehouse=CargoTransport.warehouse local group=OpsGroupCargo:GetGroup() local request=warehouse:_GetRequestOfGroup(group,warehouse.pending) table.insert(request.carriercargo[CarrierElement.name],warehouse:_GetNameWithOut(group:GetName())) end function CargoTransport:OnAfterUnloaded(From,Event,To,OpsGroupCargo,OpsGroupCarrier) local warehouse=CargoTransport.warehouse local group=OpsGroupCargo:GetGroup() local text=string.format("Cargo group %s was unloaded from carrier group %s.",tostring(group:GetName()),tostring(OpsGroupCarrier:GetName())) warehouse:T(warehouse.lid..text) warehouse:Arrived(group) end for _,carriergroup in pairs(TransportGroupSet:GetSetObjects())do local opsgroup=nil if Request.transporttype==WAREHOUSE.TransportType.AIRPLANE or Request.transporttype==WAREHOUSE.TransportType.HELICOPTER then opsgroup=FLIGHTGROUP:New(carriergroup) elseif Request.transporttype==WAREHOUSE.TransportType.APC then opsgroup=ARMYGROUP:New(carriergroup) 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 opsgroup=NAVYGROUP:New(carriergroup) end opsgroup:AddOpsTransport(CargoTransport) end 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 if self.ValidateAndRepositionGroundUnits then UTILS.ValidateAndRepositionGroundUnits(template.units) end 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 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:GetCoord()) 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, 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", version="0.9.44", 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, DLinkCacheTime=120, logsamstatus=false, DetectAccoustic=false, DetectAccousticRadius=2000, DetectAccousticCategories={Unit.Category.HELICOPTER}, ARMWeaponSeen={}, InboundARMs={}, } 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=45,Blindspot=0,Height=12,Type="Medium",Radar="Hawk"}, ["NASAMS"]={Range=14,Blindspot=0,Height=7,Type="Short",Radar="NSAMS",ARMCapacity=1}, ["Patriot"]={Range=99,Blindspot=0,Height=25,Type="Long",Radar="Patriot str"}, ["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",ARMCapacity=4}, ["SA-11"]={Range=35,Blindspot=0,Height=20,Type="Medium",Radar="SA-11"}, ["Roland"]={Range=6,Blindspot=0,Height=5,Type="Short",Radar="Roland",ARMCapacity=1}, ["Gepard"]={Range=5,Blindspot=0,Height=4,Type="Point",Radar="Gepard"}, ["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",ARMCapacity=2}, ["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"}, ["C-RAM"]={Range=2,Blindspot=0,Height=2,Type="Point",Radar="HEMTT_C-RAM_Phalanx",Point="true"}, ["SA-10B"]={Range=75,Blindspot=0,Height=18,Type="Medium",Radar="SA-10B",ARMCapacity=4}, ["SA-17"]={Range=50,Blindspot=3,Height=50,Type="Medium",Radar="SA-17",ARMCapacity=4}, ["SA-20A"]={Range=150,Blindspot=5,Height=27,Type="Long",Radar="S-300PMU1",ARMCapacity=16}, ["SA-20B"]={Range=200,Blindspot=4,Height=27,Type="Long",Radar="S-300PMU2",ARMCapacity=18}, ["SA-21"]={Range=380,Blindspot=5,Height=30,Type="Long",Radar="92N6E"}, ["S-300VM"]={Range=200,Blindspot=5,Height=30,Type="Long",Radar="9S32M",ARMCapacity=4}, ["S-300V4"]={Range=380,Blindspot=5,Height=30,Type="Long",Radar="9S32M",ARMCapacity=4}, ["S-400"]={Range=250,Blindspot=5,Height=27,Type="Long",Radar="92N6E",ARMCapacity=4}, ["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"}, ["Nike"]={Range=155,Blindspot=6,Height=30,Type="Long",Radar="HIPAR"}, ["Dog Ear"]={Range=11,Blindspot=0,Height=9,Type="Point",Radar="Dog Ear",Point="true"}, ["Pantsir S1"]={Range=20,Blindspot=1.2,Height=15,Type="Point",Radar="PantsirS1",Point="true",ARMCapacity=3}, ["Tor M2"]={Range=12,Blindspot=1,Height=10,Type="Point",Radar="TorM2",Point="true",ARMCapacity=4}, ["IRIS-T SLM"]={Range=40,Blindspot=0.5,Height=20,Type="Medium",Radar="CH_IRIST_SLM",ARMCapacity=12}, } 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-10B HDS"]={Range=90,Blindspot=5,Height=25,Type="Long",Radar="5P85CE ln",ARMCapacity=8}, ["SA-10C HDS"]={Range=75,Blindspot=5,Height=25,Type="Long",Radar="5P85SE ln",ARMCapacity=3}, ["SA-17 HDS"]={Range=50,Blindspot=3,Height=50,Type="Medium",Radar="SA-17",ARMCapacity=4}, ["SA-12 HDS 2"]={Range=100,Blindspot=13,Height=30,Type="Long",Radar="S-300V 9A82 l",ARMCapacity=12}, ["SA-12 HDS 1"]={Range=75,Blindspot=6,Height=25,Type="Long",Radar="S-300V 9A83 l",ARMCapacity=12}, ["SA-23 HDS 2"]={Range=200,Blindspot=5,Height=37,Type="Long",Radar="S-300VM 9A82ME",ARMCapacity=14}, ["SA-23 HDS 1"]={Range=100,Blindspot=1,Height=50,Type="Long",Radar="S-300VM 9A83ME",ARMCapacity=14}, ["HQ-2 HDS"]={Range=50,Blindspot=6,Height=35,Type="Medium",Radar="HQ_2_Guideline_LN"}, ["SAMPT Block 1 HDS"]={Range=120,Blindspot=1,Height=20,Type="long",Radar="SAMPT_MLT_Blk1"}, ["SAMPT Block 1INT HDS"]={Range=150,Blindspot=1,Height=25,Type="long",Radar="SAMPT_MLT_Blk1NT"}, ["SAMPT Block 2 HDS"]={Range=200,Blindspot=10,Height=70,Type="long",Radar="SAMPT_MLT_Blk2"}, } MANTIS.SamDataSMA={ ["RBS98M SMA"]={Range=20,Blindspot=0.2,Height=8,Type="Short",Radar="RBS-98"}, ["RBS70 SMA"]={Range=8,Blindspot=0.25,Height=6,Type="Short",Radar="RBS-70"}, ["RBS70M SMA"]={Range=8,Blindspot=0.25,Height=6,Type="Short",Radar="BV410_RBS70"}, ["RBS90 SMA"]={Range=8,Blindspot=0.25,Height=6,Type="Short",Radar="RBS-90"}, ["RBS90M SMA"]={Range=8,Blindspot=0.25,Height=6,Type="Short",Radar="BV410_RBS90"}, ["RBS103A SMA"]={Range=160,Blindspot=1,Height=36,Type="Long",Radar="LvS-103_Lavett103_Rb103A"}, ["RBS103B SMA"]={Range=120,Blindspot=3,Height=24.5,Type="Long",Radar="LvS-103_Lavett103_Rb103B"}, ["RBS103AM SMA"]={Range=160,Blindspot=1,Height=36,Type="Long",Radar="LvS-103_Lavett103_HX_Rb103A"}, ["RBS103BM SMA"]={Range=120,Blindspot=3,Height=24.5,Type="Long",Radar="LvS-103_Lavett103_HX_Rb103B"}, ["Lvkv9040M SMA"]={Range=2,Blindspot=0.1,Height=1.2,Type="Point",Radar="LvKv9040",Point="true"}, } MANTIS.SamDataCH={ ["2S38 CHM"]={Range=6,Blindspot=0.1,Height=4.5,Type="Short",Radar="2S38"}, ["PantsirS1 CHM"]={Range=20,Blindspot=1.2,Height=15,Type="Point",Radar="PantsirS1",Point="true",ARMCapacity=3}, ["PantsirS2 CHM"]={Range=30,Blindspot=1.2,Height=18,Type="Medium",Radar="PantsirS2",ARMCapacity=4}, ["PGL-625 CHM"]={Range=10,Blindspot=1,Height=5,Type="Short",Radar="PGL_625"}, ["HQ-17A CHM"]={Range=15,Blindspot=1.5,Height=10,Type="Short",Radar="HQ17A"}, ["M903PAC2 CHM"]={Range=120,Blindspot=3,Height=24.5,Type="Long",Radar="MIM104_M903_PAC2"}, ["M903PAC3 CHM"]={Range=160,Blindspot=1,Height=40,Type="Long",Radar="MIM104_M903_PAC3"}, ["TorM2 CHM"]={Range=12,Blindspot=1,Height=10,Type="Point",Radar="TorM2",Point="true",ARMCapacity=3}, ["TorM2K CHM"]={Range=12,Blindspot=1,Height=10,Type="Point",Radar="TorM2K",Point="true",ARMCapacity=4}, ["TorM2M CHM"]={Range=16,Blindspot=1,Height=10,Type="Point",Radar="TorM2M",Point="true",ARMCapacity=4}, ["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.5,Height=3,Type="Point",Radar="CH_PGZ09",Point="true"}, ["S350-9M100 CHM"]={Range=15,Blindspot=1,Height=8,Type="Short",Radar="CH_S350_50P6_9M100",ARMCapacity=20}, ["S350-9M96D CHM"]={Range=150,Blindspot=2.5,Height=30,Type="Long",Radar="CH_S350_50P6_9M96D",ARMCapacity=20}, ["LAV-AD CHM"]={Range=8,Blindspot=0.16,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.5,Blindspot=0.5,Height=2,Type="Point",Radar="CH_PGZ95",Point="true"}, ["LD-3000 CHM"]={Range=2.5,Blindspot=0.1,Height=3,Type="Point",Radar="CH_LD3000_stationary",Point="true"}, ["LD-3000M CHM"]={Range=2.5,Blindspot=0.1,Height=3,Type="Point",Radar="CH_LD3000",Point="true"}, ["FlaRakRad CHM"]={Range=8,Blindspot=1.5,Height=6,Type="Short",Radar="CH_FlaRakRad"}, ["IRIS-T SLM CHM"]={Range=40,Blindspot=0.5,Height=20,Type="Medium",Radar="CH_IRIST_SLM"}, ["M903PAC2KAT1 CHM"]={Range=120,Blindspot=3,Height=24.5,Type="Long",Radar="CH_MIM104_M903_PAC2_KAT1"}, ["Skynex CHM"]={Range=3.5,Blindspot=0.1,Height=3.5,Type="Point",Radar="CH_SkynexHX",Point="true"}, ["Skyshield CHM"]={Range=3.5,Blindspot=0.1,Height=3.5,Type="Point",Radar="CH_Skyshield_Gun",Point="true"}, ["WieselOzelot CHM"]={Range=8,Blindspot=0.16,Height=4.8,Type="Short",Radar="CH_Wiesel2Ozelot"}, ["BukM3-9M317M CHM"]={Range=70,Blindspot=0.25,Height=35,Type="Medium",Radar="CH_BukM3_9A317M",ARMCapacity=20}, ["BukM3-9M317MA CHM"]={Range=70,Blindspot=0.25,Height=35,Type="Medium",Radar="CH_BukM3_9A317MA",ARMCapacity=20}, ["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.16,Height=4.8,Type="Short",Radar="CH_USInfantry_FIM92"}, ["RBS98M CHM"]={Range=20,Blindspot=0.2,Height=8,Type="Short",Radar="RBS-98"}, ["RBS70 CHM"]={Range=8,Blindspot=0.25,Height=6,Type="Short",Radar="RBS-70"}, ["RBS70M CHM"]={Range=8,Blindspot=0.25,Height=6,Type="Short",Radar="BV410_RBS70"}, ["RBS90 CHM"]={Range=8,Blindspot=0.25,Height=6,Type="Short",Radar="RBS-90"}, ["RBS90M CHM"]={Range=8,Blindspot=0.25,Height=6,Type="Short",Radar="BV410_RBS90"}, ["RBS103A CHM"]={Range=160,Blindspot=1,Height=36,Type="Long",Radar="LvS-103_Lavett103_Rb103A"}, ["RBS103B CHM"]={Range=120,Blindspot=3,Height=24.5,Type="Long",Radar="LvS-103_Lavett103_Rb103B"}, ["RBS103AM CHM"]={Range=160,Blindspot=1,Height=36,Type="Long",Radar="LvS-103_Lavett103_HX_Rb103A"}, ["RBS103BM CHM"]={Range=120,Blindspot=3,Height=24.5,Type="Long",Radar="LvS-103_Lavett103_HX_Rb103B"}, ["Lvkv9040M CHM"]={Range=2,Blindspot=0.1,Height=1.2,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.coalition=Coalition=="blue"and coalition.side.BLUE or coalition.side.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.LastThreatEval={} self.InboundARMs={} 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:SetDLinkCacheTime() 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.logsamstatus=false 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: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("*","SAMUnitHit","*") self:AddTransition("*","SAMUnitLost","*") self:AddTransition("*","Stop","Stopped") return self end function MANTIS:SetAccousticDetectionOn(Radius,UnitCategories) self.DetectAccoustic=true self.DetectAccousticRadius=Radius or 2000 self.DetectAccousticCategories=UnitCategories or{Unit.Category.HELICOPTER} return self end function MANTIS:SetAccousticDetectionOff() self.DetectAccoustic=false return self end function MANTIS:_EventHandler(EventData) self:T(self.lid.."_EventHandler") local function IsOneOfOurs(name) for _,_name in pairs(self.ewr_templates)do if string.find(name,_name,1,true)then return true end end return false end local function SwitchSAMOn(Name,Group) local suppressed=self.SuppressedGroups[Name]or false if not suppressed and self.SamStateTracker[Name]=="GREEN"then self.SamStateTracker[Name]="RED" if self.UseEmOnOff then Group:EnableEmission(true) elseif(not self.UseEmOnOff)then Group:OptionAlarmStateRed() end self:__RedState(1,Group) if self.SmokeDecoy==true then self:_SmokeUnits(Group) end end end local coordinate local Name local Group local lasthit=0 local firsthit=false local alerton=false local data=EventData if data.id==EVENTS.Hit then if data.TgtGroupName and IsOneOfOurs(data.TgtGroupName)then self:T("Unit hit in group: "..data.TgtGroupName) if data.TgtGroup then lasthit=data.TgtGroup:GetProperty("MANTIS_LASTHIT") firsthit=(lasthit==nil)and true or false if firsthit==true then alerton=true end if lasthit~=nil and timer.getTime()-lasthit>self.ShoradTime then alerton=true end coordinate=data.TgtGroup:GetCoordinate() Name=data.TgtGroupName Group=data.TgtGroup if alerton==true then self:__SAMUnitHit(1,Group,Name) SwitchSAMOn(Name,Group) end if coordinate and self.debug then local text=coordinate:ToStringMGRS() self:I("Location: "..text) end end end end if data.id==EVENTS.UnitLost then if data.IniGroupName and IsOneOfOurs(data.IniGroupName)then self:T("Unit lost in group: "..data.IniGroupName) if data.IniGroup then lasthit=data.IniGroup:GetProperty("MANTIS_LASTHIT") firsthit=(lasthit==nil)and true or false if firsthit==true then alerton=true end if lasthit~=nil and timer.getTime()-lasthit>self.ShoradTime then alerton=true end coordinate=data.IniGroup:GetCoordinate() Name=data.IniGroupName Group=data.IniGroup alerton=true SwitchSAMOn(Name,Group) self:__SAMUnitLost(1,Group,Name) if coordinate and self.debug then local text=coordinate:ToStringMGRS() self:I("Location: "..text) end end end end if firsthit==true or alerton==true then Group:SetProperty("MANTIS_LASTHIT",timer.getTime()) end if coordinate~=nil and Name~=nil and Group~=nil and alerton==true then if self.ShoradLink then self:T("Shorad activated for: "..Name) 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 if self.autorelocate and Group then Group:RelocateGroundRandomInRadius(20,500,true,true,nil,true) end end 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{} self.AcceptZonesNo=UTILS.TableLength(self.AcceptZones) self.RejectZonesNo=UTILS.TableLength(self.RejectZones) self.ConflictZonesNo=UTILS.TableLength(self.ConflictZones) self:T(string.format("AcceptZonesNo = %d | RejectZonesNo = %d | ConflictZonesNo = %d",self.AcceptZonesNo,self.RejectZonesNo,self.ConflictZonesNo)) if self.AcceptZonesNo>0 or self.RejectZonesNo>0 or self.ConflictZonesNo>0 then self.usezones=true end return self end function MANTIS:AddRejectZone(Zone) if Zone and Zone:IsInstanceOf("ZONE_BASE")then table.insert(self.RejectZones,Zone) self.usezones=true end return self end function MANTIS:AddAcceptZone(Zone) if Zone and Zone:IsInstanceOf("ZONE_BASE")then table.insert(self.AcceptZones,Zone) self.usezones=true end return self end function MANTIS:AddConflictZone(Zone) if Zone and Zone:IsInstanceOf("ZONE_BASE")then table.insert(self.ConflictZones,Zone) self.usezones=true end return self end function MANTIS:SetCorridorZones(CorridorZones) self:T(self.lid.."SetCorridorZones") if CorridorZones and CorridorZones:IsInstanceOf("SET_ZONE")then self.corridorzones=CorridorZones self.usecorridors=true elseif CorridorZones and CorridorZones:IsInstanceOf("ZONE_BASE")then if not self.corridorzones then self.corridorzones=SET_ZONE:New()end self.corridorzones:AddZone(CorridorZones) self.usecorridors=true end if self.intelset then for _,_intel in pairs(self.intelset)do _intel:SetCorridorZones(self.corridorzones) end end return self end function MANTIS:AddCorridorZone(CorridorZone) self:T(self.lid.."AddCorridorZone") self:SetCorridorZones(CorridorZone) return self end function MANTIS:SetCorridorZoneFloorAndCeiling(Floor,Ceiling) self.corridorfloor=UTILS.FeetToMeters(Floor) self.corridorceiling=UTILS.FeetToMeters(Ceiling) if self.intelset then for _,_intel in pairs(self.intelset)do _intel:SetCorridorLimits(self.corridorfloor,self.corridorceiling) end end return self end function MANTIS:SetCorridorZoneFloorAndCeilingMeters(Floor,Ceiling) self.corridorfloor=Floor self.corridorceiling=Ceiling if self.intelset then for _,_intel in pairs(self.intelset)do _intel:SetCorridorLimits(self.corridorfloor,self.corridorceiling) end 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:SetDLinkCacheTime(seconds) self.DLinkCacheTime=math.abs(seconds or 120) if self.DLinkCacheTime<5 then self.DLinkCacheTime=5 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 self:T(string.format("AcceptZonesNo = %d | RejectZonesNo = %d | ConflictZonesNo = %d",self.AcceptZonesNo,self.RejectZonesNo,self.ConflictZonesNo)) if self.AcceptZonesNo>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.RejectZonesNo>0 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.ConflictZonesNo>0 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,SamCoordinate) 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 local coord=grp:GetCoordinate() local dist=0 local include=true if grp:IsGround()then include=false end if grp:GetCoalition()==self.coalition then include=false end if coord and SamCoordinate and grp:IsHelicopter()then dist=coord:Get2DDistance(SamCoordinate)or 0 if dist>self.ShoradActDistance then include=false end end if self.debug then local text="Looking at Group: "..grp:GetName()or"N/A" text=text.." Include = "..tostring(include) MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.verbose):ToLog() end local grpalt=grp:GetHeight(true) if grpalt10 and include==true then table.insert(set,coord) end end end return set end function MANTIS:_CheckObjectInZone(dectset,samcoordinate,radius,height,dlink) self:T(self.lid.."_CheckObjectInZone") local rad=radius or self.checkradius local set=dectset if dlink then set=self:_PreFilterHeight(height,samcoordinate) end if self.checkforfriendlies==true and self.friendlyset==nil then self.friendlyset=SET_GROUP:New():FilterCoalitions(self.Coalition):FilterCategories({"plane","helicopter"}):FilterFunction(function(grp)if grp and grp:InAir()then return true else return false end end):FilterStart() end for _,_coord in pairs(set)do local coord=_coord local targetdistance=samcoordinate:DistanceFromPointVec2(coord) if not targetdistance then targetdistance=samcoordinate:Get2DDistance(coord) end local zonecheck=true self:T("self.usezones = "..tostring(self.usezones)) if self.usezones then zonecheck=self:_CheckCoordinateInZones(coord) end if self.verbose and self.debug then local samstring=samcoordinate:ToStringMGRS({MGRS_Accuracy=0}) samstring=string.gsub(samstring,"%s","") local inrange="false" if targetdistance<=rad then inrange="true" end local text=string.format("Checking SAM at %s | Tgtdist %.1fkm | Rad %.1fkm | Inrange %s",samstring,targetdistance/1000,rad/1000,inrange) local m=MESSAGE:New(text,10,"Check"):ToAllIf(self.debug) self:T(self.lid..text) end local nofriendlies=true if self.checkforfriendlies==true then local closestfriend,distance=self.friendlyset:GetClosestGroup(samcoordinate) if closestfriend and distance and distance0 then _group:SetProperty("ARMCapacity",ARMCapacity)end table.insert(SAM_Tbl,{grpname,grpcoord,grprange,grpheight,blind,type,ARMCapacity}) if type==MANTIS.SamType.LONG then table.insert(SAM_Tbl_lg,{grpname,grpcoord,grprange,grpheight,blind,type}) table.insert(SEAD_Grps,grpname) self:T("SAM "..grpname.." is type LONG") elseif type==MANTIS.SamType.MEDIUM then table.insert(SAM_Tbl_md,{grpname,grpcoord,grprange,grpheight,blind,type}) table.insert(SEAD_Grps,grpname) self:T("SAM "..grpname.." is type MEDIUM") elseif type==MANTIS.SamType.SHORT then table.insert(SAM_Tbl_sh,{grpname,grpcoord,grprange,grpheight,blind,type}) table.insert(SEAD_Grps,grpname) self:T("SAM "..grpname.." is type SHORT") elseif type==MANTIS.SamType.POINT then table.insert(SAM_Tbl_pt,{grpname,grpcoord,grprange,grpheight,blind,type}) self:T("SAM "..grpname.." is type POINT") self.ShoradGroupSet:Add(grpname,group) if not self.autoshorad then table.insert(SEAD_Grps,grpname) end end self.SamStateTracker[grpname]="GREEN" end end self.SAM_Table=SAM_Tbl self.SAM_Table_Long=SAM_Tbl_lg self.SAM_Table_Medium=SAM_Tbl_md self.SAM_Table_Short=SAM_Tbl_sh self.SAM_Table_PointDef=SAM_Tbl_pt local mysead=SEAD:New(SEAD_Grps,self.Padding) mysead:SetEngagementRange(engagerange) mysead:AddCallBack(self) if self.UseEmOnOff then mysead:SwitchEmissions(true) end self.mysead=mysead return self end function MANTIS:_RefreshSAMTable() self:T(self.lid.."RefreshSAMTable") local SAM_SET=self.SAM_Group local SAM_Grps=SAM_SET.Set local SAM_Tbl={} local SAM_Tbl_lg={} local SAM_Tbl_md={} local SAM_Tbl_sh={} local SAM_Tbl_pt={} local SEAD_Grps={} local engagerange=self.engagerange for _i,_group in pairs(SAM_Grps)do local group=_group group:OptionEngageRange(engagerange) if group:IsGround()and group:IsAlive()then local grpname=group:GetName() local grpcoord=group:GetCoordinate() local grprange,grpheight,type,blind,ARMCapacity=self:_GetSAMRange(grpname) if ARMCapacity and ARMCapacity>0 then _group:SetProperty("ARMCapacity",ARMCapacity)end local radaralive=true table.insert(SAM_Tbl,{grpname,grpcoord,grprange,grpheight,blind,type,ARMCapacity}) if type~=MANTIS.SamType.POINT then table.insert(SEAD_Grps,grpname) end if type==MANTIS.SamType.LONG and radaralive then table.insert(SAM_Tbl_lg,{grpname,grpcoord,grprange,grpheight,blind,type}) self:T({grpname,grprange,grpheight}) elseif type==MANTIS.SamType.MEDIUM and radaralive then table.insert(SAM_Tbl_md,{grpname,grpcoord,grprange,grpheight,blind,type}) self:T({grpname,grprange,grpheight}) elseif type==MANTIS.SamType.SHORT and radaralive then table.insert(SAM_Tbl_sh,{grpname,grpcoord,grprange,grpheight,blind,type}) self:T({grpname,grprange,grpheight}) elseif type==MANTIS.SamType.POINT or(not radaralive)then table.insert(SAM_Tbl_pt,{grpname,grpcoord,grprange,grpheight,blind,type}) self:T({grpname,grprange,grpheight}) self.ShoradGroupSet:Add(grpname,group) if self.autoshorad then self.Shorad.Groupset=self.ShoradGroupSet end end end end self.SAM_Table=SAM_Tbl self.SAM_Table_Long=SAM_Tbl_lg self.SAM_Table_Medium=SAM_Tbl_md self.SAM_Table_Short=SAM_Tbl_sh self.SAM_Table_PointDef=SAM_Tbl_pt if self.mysead~=nil then local mysead=self.mysead mysead:UpdateSet(SEAD_Grps) end return self end function MANTIS:AddShorad(Shorad,Shoradtime) self:T(self.lid.."AddShorad") local Shorad=Shorad or nil local ShoradTime=Shoradtime or 600 local ShoradLink=true if Shorad:IsInstanceOf("SHORAD")then self.ShoradLink=ShoradLink self.Shorad=Shorad self.ShoradTime=Shoradtime end return self end function MANTIS:RemoveShorad() self:T(self.lid.."RemoveShorad") self.ShoradLink=false return self end function MANTIS:_SmokeUnits(Group) self:T("Smoking") local LastSmoketime=Group:GetProperty("MANTIS_LASTSMOKE_TIME")or 0 local TNow=timer.getTime() if TNow-LastSmoketime>290 then Group:SetProperty("MANTIS_LASTSMOKE_TIME",TNow) local units=Group:GetUnits()or{} local smoke=self.SmokeDecoyColor or SMOKECOLOR.White for _,unit in pairs(units)do if unit and unit:IsAlive()then unit:GetCoordinate():Smoke(smoke) end end end return self end function MANTIS:SeadAllowSuppression(targetGroup,targetName,attackerGroup,weaponName,weaponWrapper,tti,delay) self:T(self.lid.."SeadAllowSuppression") self:T(string.format("MANTIS:SeadAllowSuppression REQUEST | target=%s | weapon=%s | tti=%s | delay=%s",tostring(targetName), tostring(weaponName),tostring(tti),tostring(delay))) local armcap=targetGroup:GetProperty("ARMCapacity") if not armcap then for _,sam in pairs(self.SAM_Table or{})do if sam[1]==targetName then armcap=sam[7] break end end end self:T(string.format("MANTIS:SeadAllowSuppression SAM DATA | target=%s | ARMCapacity=%s",tostring(targetName),armcap and tostring(armcap)or"nil")) local THREAT_WINDOW=0.1 self.LastThreatEval=self.LastThreatEval or{} self.InboundARMs=self.InboundARMs or{} local now=timer.getTime() local last=self.LastThreatEval[targetName]or 0 if(now-last)>=THREAT_WINDOW then self.InboundARMs[targetName]=(self.InboundARMs[targetName]or 0)+1 self.LastThreatEval[targetName]=now self:T(string.format("MANTIS:SeadAllowSuppression NEW threat accepted | Δt=%.3f",now-last)) else self:T(string.format("MANTIS:SeadAllowSuppression duplicate evaluation ignored | Δt=%.3f",now-last)) end local inbound=self.InboundARMs[targetName]or 0 self:T(string.format("MANTIS:SeadAllowSuppression THREAT COUNT | target=%s | inboundThreats=%d",tostring(targetName),inbound)) if targetGroup and targetGroup:IsAlive()then local AmmotT,AmmoS,_,_,AmmoM=targetGroup:GetAmmunition() if AmmoM and AmmoM==0 then self:T(string.format("MANTIS:SeadAllowSuppression DECISION -> APPROVED (no MISSILES) | target=%s",tostring(targetName))) return true end end if(not armcap)or armcap==0 then self:T(string.format("MANTIS:SeadAllowSuppression DECISION -> APPROVED (no ARMCAP) | target=%s",tostring(targetName))) return true end if inbound>=armcap then self:T(string.format("MANTIS:SeadAllowSuppression DECISION -> APPROVED (inbound %d >= cap %d) | target=%s",inbound,armcap,tostring(targetName))) return true end self:T(string.format("MANTIS:SeadAllowSuppression DECISION -> DENIED (inbound %d < cap %d) | target=%s",inbound,armcap,tostring(targetName))) return false end function MANTIS:_CheckLoop(samset,detset,dlink,limit) self:T(self.lid.."CheckLoop "..#detset.." Coordinates") local switchedon=0 local instatusred=0 local instatusgreen=0 local activeshorads=0 local SEADactive=0 for _,_data in pairs(samset)do local samcoordinate=_data[2] local name=_data[1] local radius=_data[3] local height=_data[4] local blind=_data[5]*1.25+1 local shortsam=(_data[6]~=MANTIS.SamType.LONG)and true or false if not shortsam then shortsam=(_data[6]==MANTIS.SamType.POINT)and true or false end local samgroup=GROUP:FindByName(name) local IsInZone,Distance=self:_CheckObjectInZone(detset,samcoordinate,radius,height,dlink) local suppressed=self.SuppressedGroups[name]or false local activeshorad=false if self.Shorad and self.Shorad.ActiveGroups and self.Shorad.ActiveGroups[name]then activeshorad=true end if samgroup:GetProperty("SHORAD_ACTIVE")==true and activeshorad==false then activeshorad=true end if IsInZone and(not suppressed)and(not activeshorad)then if samgroup:IsAlive()then local switch=false if self.UseEmOnOff and switchedon29)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.SmokeDecoy,self.SmokeDecoyColor) self.Shorad:SetDefenseLimits(80,95) self.ShoradLink=true self.Shorad.Groupset=self.ShoradGroupSet self.Shorad.debug=self.debug self.Shorad:AddCallBack(self) 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:HandleEvent(EVENTS.Hit,self._EventHandler) self:HandleEvent(EVENTS.UnitLost,self._EventHandler) 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,self.logsamstatus) 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 | Status %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 self.InboundARMs[Name]=0 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, SmokeDecoy=false, SmokeDecoyColor=SMOKECOLOR.White } 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,SmokeDecoy,SmokeDecoyColor) 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 if type(Coalition)=="number"then Coalition=string.lower(UTILS.GetCoalitionName(Coalition))end 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 if SmokeDecoy then self.SmokeDecoy=SmokeDecoy self.SmokeDecoyColor=SmokeDecoyColor or SMOKECOLOR.White end self:I("*** SHORAD - Started Version 0.3.6") 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(high%s",location) end text=Text..location.."!" local ttstext=Text..location.."! Repeat! "..location if _coalition==self.coalition then if self.verbose then MESSAGE:New(msgtxt,15,"AICSAR"):ToCoalition(self.coalition) end if self.SRSRadio then local sound=SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength) sound:SetPlayWithSRS(true) self.SRS:PlaySoundFile(sound,2) elseif self.DCSRadio then self:DCSRadioBroadcast(Soundfile,Soundlength,text) elseif self.SRSTTSRadio then if self.SRSPilotVoice then self.SRSQ:NewTransmission(ttstext,nil,self.SRSPilot,nil,1) else self.SRSQ:NewTransmission(ttstext,nil,self.SRS,nil,1) end end end if _coalition==self.coalition and distancetofarp<=self.maxdistance then self:T(self.lid.."Spawning new Pilot") self.pilotindex=self.pilotindex+1 local newpilot=SPAWN:NewWithAlias(self.template,string.format("%s-AICSAR-%d",self.template,self.pilotindex)) newpilot:InitDelayOff() newpilot:OnSpawnGroup( function(grp) self.pilotqueue[self.pilotindex]=grp end ) newpilot:SpawnFromCoordinate(_LandingPos) self:__PilotDown(2,_LandingPos,true) elseif _coalition==self.coalition and distancetofarp>self.maxdistance then self:T(self.lid.."Pilot out of reach") self:__PilotDown(2,_LandingPos,false) end return self end function AICSAR:_EventHandler(EventData,FromEject) self:T(self.lid.."OnEventLandingAfterEjection ID="..EventData.id) if self.autoonoff then if self.playerset:CountAlive()>0 then return self end end if self.UseEventEject and(not FromEject)then return self end local _event=EventData local _LandingPos=COORDINATE:NewFromVec3(_event.initiator:getPosition().p) local _country=_event.initiator:getCountry() local _coalition=coalition.getCountryCoalition(_country) local distancetofarp=_LandingPos:Get2DDistance(self.farp:GetCoordinate()) if self.UseRescueZone==true and self.RescueZone~=nil then if self.RescueZone:IsCoordinateInZone(_LandingPos)then distancetofarp=self.maxdistance-10 else distancetofarp=self.maxdistance+10 end end local Text,Soundfile,Soundlength,Subtitle=self.gettext:GetEntry("PILOTDOWN",self.locale) local text="" local setting={} setting.MGRS_Accuracy=self.MGRS_Accuracy local location=_LandingPos:ToStringMGRS(setting) local msgtxt=Text..location.."!" location=string.gsub(location,"MGRS ","") location=string.gsub(location,"%s+","") location=string.gsub(location,"([%a%d])","%1;") location=string.gsub(location,"0","zero") location=string.gsub(location,"9","niner") location="MGRS;"..location if self.SRSGoogle then location=string.format("%s",location) end text=Text..location.."!" local ttstext=Text..location.."! Repeat! "..location if _coalition==self.coalition then if self.verbose then MESSAGE:New(msgtxt,15,"AICSAR"):ToCoalition(self.coalition) end if self.SRSRadio then local sound=SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength) sound:SetPlayWithSRS(true) self.SRS:PlaySoundFile(sound,2) elseif self.DCSRadio then self:DCSRadioBroadcast(Soundfile,Soundlength,text) elseif self.SRSTTSRadio then if self.SRSPilotVoice then self.SRSQ:NewTransmission(ttstext,nil,self.SRSPilot,nil,1) else self.SRSQ:NewTransmission(ttstext,nil,self.SRS,nil,1) end end end if _coalition==self.coalition and distancetofarp<=self.maxdistance then self:T(self.lid.."Spawning new Pilot") self.pilotindex=self.pilotindex+1 local newpilot=SPAWN:NewWithAlias(self.template,string.format("%s-AICSAR-%d",self.template,self.pilotindex)) newpilot:InitDelayOff() newpilot:OnSpawnGroup( function(grp) self.pilotqueue[self.pilotindex]=grp end ) newpilot:SpawnFromCoordinate(_LandingPos) Unit.destroy(_event.initiator) self:__PilotDown(2,_LandingPos,true) elseif _coalition==self.coalition and distancetofarp>self.maxdistance then self:T(self.lid.."Pilot out of reach") self:__PilotDown(2,_LandingPos,false) end return self end function AICSAR:_GetFlight() self:T(self.lid.."_GetFlight") local newhelo=SPAWN:NewWithAlias(self.helotemplate,self.helotemplate..math.random(1,10000)) :InitDelayOff() :InitUnControlled(true) :OnSpawnGroup( function(Group) Group:OptionPreferVerticalLanding() self:__HeloOnDuty(1,Group) end ) :Spawn() local nhelo=FLIGHTGROUP:New(newhelo) nhelo:SetHomebase(self.farp) nhelo:Activate() return nhelo end function AICSAR:_InitMission(Pilot,Index) self:T(self.lid.."_InitMission") local pickupzone=ZONE_GROUP:New(Pilot:GetName(),Pilot,self.rescuezoneradius) local opstransport=OPSTRANSPORT:New(Pilot,pickupzone,self.farpzone) local helo=self:_GetFlight() helo.AICSARReserved=true helo:SetDefaultAltitude(self.Altitude or 1500) helo:SetDefaultSpeed(self.Speed or 100) helo:AddOpsTransport(opstransport) local function AICPickedUp(Helo,Cargo,Index) self:__PilotPickedUp(2,Helo,Cargo,Index) end local function AICHeloDead(Helo,Index) self:__HeloDown(2,Helo,Index) end local function AICHeloUnloaded(Helo,OpsGroup) self:__PilotUnloaded(2,Helo,OpsGroup) end function helo:OnAfterLoading(From,Event,To) AICPickedUp(helo,helo:GetCargoGroups(),Index) helo:__LoadingDone(5) end function helo:OnAfterDead(From,Event,To) AICHeloDead(helo,Index) end function helo:OnAfterUnloaded(From,Event,To,OpsGroupCargo) AICHeloUnloaded(helo,OpsGroupCargo) helo:__UnloadingDone(5) end function helo:OnAfterLandAtAirbase(From,Event,To,airbase) helo:Despawn(2) end self.helos[Index]=helo return self end function AICSAR:_CheckInMashZone(Pilot) self:T(self.lid.."_CheckInMashZone") if Pilot:IsInZone(self.farpzone)then return true else return false end end function AICSAR:SetDefaultSpeed(Knots) self:T(self.lid.."SetDefaultSpeed") self.Speed=Knots or 100 return self end function AICSAR:SetDefaultAltitude(Feet) self:T(self.lid.."SetDefaultAltitude") self.Altitude=Feet or 1500 return self end function AICSAR:_CheckHelos() self:T(self.lid.."_CheckHelos") for _index,_helo in pairs(self.helos)do local helo=_helo if helo and helo.ClassName=="FLIGHTGROUP"then local state=helo:GetState() local name=helo:GetName() self:T("Helo group "..name.." in state "..state) if state=="Arrived"then helo.OnAfterDead=nil helo:Despawn(35) self.helos[_index]=nil end else self.helos[_index]=nil end end return self end function AICSAR:_CountHelos() self:T(self.lid.."_CountHelos") local count=0 for _index,_helo in pairs(self.helos)do count=count+1 end return count end function AICSAR:_CheckQueue(OpsGroup) self:T(self.lid.."_CheckQueue") for _index,_pilot in pairs(self.pilotqueue)do local classname=_pilot.ClassName and _pilot.ClassName or"NONE" local name=_pilot.GroupName and _pilot.GroupName or"NONE" local playername="John Doe" local helocount=self:_CountHelos() if _pilot and _pilot.ClassName and _pilot.ClassName=="GROUP"then local flightgroup=self.helos[_index] if self:_CheckInMashZone(_pilot)then self:T("Pilot".._pilot.GroupName.." rescued!") if OpsGroup then else _pilot:Destroy(true,10) end self.pilotqueue[_index]=nil self.rescued[_index]=true if self.PilotStore:Count()>0 then playername=self.PilotStore:Pull() end self:__PilotRescued(2,playername) if flightgroup then flightgroup.AICSARReserved=false end end if not _pilot.AICSAR then if self.limithelos and helocount>=self.helonumber then break end _pilot.AICSAR={} _pilot.AICSAR.Status="Initiated" _pilot.AICSAR.Boarded=false self:_InitMission(_pilot,_index) break else if flightgroup then local state=flightgroup:GetState() _pilot.AICSAR.Status=state end end end end return self end function AICSAR:onafterStart(From,Event,To) self:T({From,Event,To}) self:__Status(3) return self end function AICSAR:onafterStatus(From,Event,To) self:T({From,Event,To}) self:_CheckHelos() self:__Status(30) return self end function AICSAR:onafterStop(From,Event,To) self:T({From,Event,To}) self:UnHandleEvent(EVENTS.LandingAfterEjection) if self.DCSRadioQueue then self.DCSRadioQueue:Stop() end return self end function AICSAR:onafterPilotDown(From,Event,To,Coordinate,InReach) self:T({From,Event,To}) local CoordinateText=Coordinate:ToStringMGRS() local inreach=tostring(InReach) if InReach then local text,Soundfile,Soundlength,Subtitle=self.gettext:GetEntry("INITIALOK",self.locale) self:T(text) if self.verbose then MESSAGE:New(text,15,"AICSAR"):ToCoalition(self.coalition) end if self.SRSRadio then local sound=SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength) sound:SetPlayWithSRS(true) self.SRS:PlaySoundFile(sound,2) elseif self.DCSRadio then self:DCSRadioBroadcast(Soundfile,Soundlength,text) elseif self.SRSTTSRadio then if self.SRSOperatorVoice then self.SRSQ:NewTransmission(text,nil,self.SRSOperator,nil,1) else self.SRSQ:NewTransmission(text,nil,self.SRS,nil,1) end end else local text,Soundfile,Soundlength,Subtitle=self.gettext:GetEntry("INITIALNOTOK",self.locale) self:T(text) if self.verbose then MESSAGE:New(text,15,"AICSAR"):ToCoalition(self.coalition) end if self.SRSRadio then local sound=SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength) sound:SetPlayWithSRS(true) self.SRS:PlaySoundFile(sound,2) elseif self.DCSRadio then self:DCSRadioBroadcast(Soundfile,Soundlength,text) elseif self.SRSTTSRadio then if self.SRSOperatorVoice then self.SRSQ:NewTransmission(text,nil,self.SRSOperator,nil,1) else self.SRSQ:NewTransmission(text,nil,self.SRS,nil,1) end end end self:_CheckQueue() return self end function AICSAR:onafterPilotKIA(From,Event,To) self:T({From,Event,To}) local text,Soundfile,Soundlength,Subtitle=self.gettext:GetEntry("PILOTKIA",self.locale) if self.verbose then MESSAGE:New(text,15,"AICSAR"):ToCoalition(self.coalition) end if self.SRSRadio then local sound=SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength) sound:SetPlayWithSRS(true) self.SRS:PlaySoundFile(sound,2) elseif self.DCSRadio then self:DCSRadioBroadcast(Soundfile,Soundlength,text) elseif self.SRSTTSRadio then self.SRSQ:NewTransmission(text,nil,self.SRS,nil,1) end return self end function AICSAR:onafterHeloDown(From,Event,To,Helo,Index) self:T({From,Event,To}) local text,Soundfile,Soundlength,Subtitle=self.gettext:GetEntry("HELODOWN",self.locale) if self.verbose then MESSAGE:New(text,15,"AICSAR"):ToCoalition(self.coalition) end if self.SRSRadio then local sound=SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength) sound:SetPlayWithSRS(true) self.SRS:PlaySoundFile(sound,2) elseif self.DCSRadio then self:DCSRadioBroadcast(Soundfile,Soundlength,text) elseif self.SRSTTSRadio then if self.SRSOperatorVoice then self.SRSQ:NewTransmission(text,nil,self.SRSOperator,nil,1) else self.SRSQ:NewTransmission(text,nil,self.SRS,nil,1) end end local findex=0 local fhname=Helo:GetName() if Index and Index>0 then findex=Index else for _index,_helo in pairs(self.helos)do local helo=_helo local hname=helo:GetName() if fhname==hname then findex=_index break end end end if findex>0 and not self.rescued[findex]then local pilot=self.pilotqueue[findex] self.helos[findex]=nil if pilot.AICSAR.Boarded then self:T("Helo Down: Found DEAD Pilot ID "..findex.." with name "..pilot:GetName()) self:__PilotKIA(2) self.pilotqueue[findex]=nil else self:T("Helo Down: Found ALIVE Pilot ID "..findex.." with name "..pilot:GetName()) self:_InitMission(pilot,findex) end end return self end function AICSAR:onafterPilotRescued(From,Event,To,PilotName) self:T({From,Event,To}) local text,Soundfile,Soundlength,Subtitle=self.gettext:GetEntry("PILOTRESCUED",self.locale) if self.verbose then MESSAGE:New(text,15,"AICSAR"):ToCoalition(self.coalition) end if self.SRSRadio then local sound=SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength) sound:SetPlayWithSRS(true) self.SRS:PlaySoundFile(sound,2) elseif self.DCSRadio then self:DCSRadioBroadcast(Soundfile,Soundlength,text) elseif self.SRSTTSRadio then self.SRSQ:NewTransmission(text,nil,self.SRS,nil,1) end return self end function AICSAR:onafterPilotUnloaded(From,Event,To,Helo,OpsGroup) self:T({From,Event,To}) self:_CheckQueue(OpsGroup) return self end function AICSAR:onafterPilotPickedUp(From,Event,To,Helo,CargoTable,Index) self:T({From,Event,To}) local text,Soundfile,Soundlength,Subtitle=self.gettext:GetEntry("PILOTINHELO",self.locale) if self.verbose then MESSAGE:New(text,15,"AICSAR"):ToCoalition(self.coalition) end if self.SRSRadio then local sound=SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength) sound:SetPlayWithSRS(true) self.SRS:PlaySoundFile(sound,2) elseif self.DCSRadio then self:DCSRadioBroadcast(Soundfile,Soundlength,text) elseif self.SRSTTSRadio then self.SRSQ:NewTransmission(text,nil,self.SRS,nil,1) end local findex=0 local fhname=Helo:GetName() if Index and Index>0 then findex=Index else for _index,_helo in pairs(self.helos)do local helo=_helo local hname=helo:GetName() if fhname==hname then findex=_index break end end end if findex>0 then local pilot=self.pilotqueue[findex] self:T("Boarded: Found Pilot ID "..findex.." with name "..pilot:GetName()) pilot.AICSAR.Boarded=true end return self end AMMOTRUCK={ ClassName="AMMOTRUCK", lid="", version="0.0.12", alias="", debug=false, trucklist={}, targetlist={}, coalition=nil, truckset=nil, targetset=nil, remunitionqueue={}, waitingtargets={}, ammothreshold=5, remunidist=20000, monitor=-60, unloadtime=600, waitingtime=1800, routeonroad=true, reloads=5, } AMMOTRUCK.State={ IDLE="idle", DRIVING="driving", ARRIVED="arrived", UNLOADING="unloading", RETURNING="returning", WAITING="waiting", RELOADING="reloading", OUTOFAMMO="outofammo", REQUESTED="requested", } function AMMOTRUCK:New(Truckset,Targetset,Coalition,Alias,Homezone) local self=BASE:Inherit(self,FSM:New()) self.truckset=Truckset self.targetset=Targetset self.coalition=Coalition self.alias=Alias self.debug=false self.remunitionqueue={} self.trucklist={} self.targetlist={} self.ammothreshold=5 self.remunidist=20000 self.homezone=Homezone self.waitingtime=1800 self.usearmygroup=false self.hasarmygroup=false self.lid=string.format("AMMOTRUCK %s | %s | ",self.version,self.alias) self:SetStartState("Stopped") self:AddTransition("Stopped","Start","Running") self:AddTransition("*","Monitor","*") self:AddTransition("*","RouteTruck","*") self:AddTransition("*","TruckArrived","*") self:AddTransition("*","TruckUnloading","*") self:AddTransition("*","TruckReturning","*") self:AddTransition("*","TruckHome","*") self:AddTransition("*","Stop","Stopped") self:__Start(math.random(5,10)) self:I(self.lid.."Started") return self end function AMMOTRUCK:CheckDrivingTrucks(dataset) self:T(self.lid.." CheckDrivingTrucks") local data=dataset for _,_data in pairs(data)do local truck=_data local coord=truck.group:GetCoordinate() local tgtcoord=truck.targetcoordinate local dist=coord:Get2DDistance(tgtcoord) if dist<=150 then truck.statusquo=AMMOTRUCK.State.ARRIVED truck.timestamp=timer.getAbsTime() truck.coordinate=coord self:__TruckArrived(1,truck) end local Tnow=timer.getAbsTime() if Tnow-truck.timestamp>30 then local group=truck.group if self.usearmygroup then group=truck.group:GetGroup() end local currspeed=group:GetVelocityKMH() if truck.lastspeed then if truck.lastspeed==0 and currspeed==0 then self:T(truck.group:GetName().." Is not moving!") truck.timestamp=timer.getAbsTime() if self.routeonroad then group:RouteGroundOnRoad(truck.targetcoordinate,30,2,"Vee") else group:RouteGroundTo(truck.targetcoordinate,30,"Vee",2) end end truck.lastspeed=currspeed else truck.lastspeed=currspeed truck.timestamp=timer.getAbsTime() end self:I({truck=truck.group:GetName(),currspeed=currspeed,lastspeed=truck.lastspeed}) end end return self end function AMMOTRUCK:GetAmmoStatus(Group) local ammotot,shells,rockets,bombs,missiles,narti=Group:GetAmmunition() return rockets+missiles+narti end function AMMOTRUCK:CheckWaitingTargets(dataset) self:T(self.lid.." CheckWaitingTargets") local data=dataset for _,_data in pairs(data)do local truck=_data local Tnow=timer.getAbsTime() local Tdiff=Tnow-truck.timestamp if Tdiff>self.waitingtime then local hasammo=self:GetAmmoStatus(truck.group) if hasammo<=self.ammothreshold then truck.statusquo=AMMOTRUCK.State.OUTOFAMMO else truck.statusquo=AMMOTRUCK.State.IDLE end end end return self end function AMMOTRUCK:CheckReturningTrucks(dataset) self:T(self.lid.." CheckReturningTrucks") local data=dataset local tgtcoord=self.homezone:GetCoordinate() local radius=self.homezone:GetRadius() for _,_data in pairs(data)do local truck=_data local coord=truck.group:GetCoordinate() local dist=coord:Get2DDistance(tgtcoord) self:T({name=truck.name,radius=radius,distance=dist}) if dist<=radius then truck.statusquo=AMMOTRUCK.State.IDLE truck.timestamp=timer.getAbsTime() truck.coordinate=coord truck.reloads=self.reloads or 5 self:__TruckHome(1,truck) end end return self end function AMMOTRUCK:FindTarget(name) self:T(self.lid.." FindTarget") local data=nil local dataset=self.targetlist for _,_entry in pairs(dataset)do local entry=_entry if entry.name==name then data=entry break end end return data end function AMMOTRUCK:FindTruck(name) self:T(self.lid.." FindTruck") local data=nil local dataset=self.trucklist for _,_entry in pairs(dataset)do local entry=_entry if entry.name==name then data=entry break end end return data end function AMMOTRUCK:CheckArrivedTrucks(dataset) self:T(self.lid.." CheckArrivedTrucks") local data=dataset for _,_data in pairs(data)do local truck=_data truck.statusquo=AMMOTRUCK.State.UNLOADING truck.timestamp=timer.getAbsTime() self:__TruckUnloading(2,truck) local aridata=self:FindTarget(truck.targetname) if aridata then aridata.statusquo=AMMOTRUCK.State.RELOADING aridata.timestamp=timer.getAbsTime() end end return self end function AMMOTRUCK:CheckUnloadingTrucks(dataset) self:T(self.lid.." CheckUnloadingTrucks") local data=dataset for _,_data in pairs(data)do local truck=_data local Tnow=timer.getAbsTime() local Tpassed=Tnow-truck.timestamp local hasammo=self:GetAmmoStatus(truck.targetgroup) if Tpassed>self.unloadtime and hasammo>self.ammothreshold then truck.statusquo=AMMOTRUCK.State.RETURNING truck.timestamp=timer.getAbsTime() self:__TruckReturning(2,truck) local aridata=self:FindTarget(truck.targetname) if aridata then aridata.statusquo=AMMOTRUCK.State.IDLE aridata.timestamp=timer.getAbsTime() end end end return self end function AMMOTRUCK:CheckTargetsAlive() self:T(self.lid.." CheckTargetsAlive") local arilist=self.targetlist for _,_ari in pairs(arilist)do local ari=_ari if ari.group and ari.group:IsAlive()then else self.targetlist[ari.name]=nil end end local aritable=self.targetset:GetSetObjects() for _,_ari in pairs(aritable)do local ari=_ari if ari and ari:IsAlive()and not self.targetlist[ari:GetName()]then local name=ari:GetName() local newari={} newari.name=name newari.group=ari newari.statusquo=AMMOTRUCK.State.IDLE newari.timestamp=timer.getAbsTime() newari.coordinate=ari:GetCoordinate() local hasammo=self:GetAmmoStatus(ari) newari.ammo=hasammo self.targetlist[name]=newari end end return self end function AMMOTRUCK:CheckTrucksAlive() self:T(self.lid.." CheckTrucksAlive") local trucklist=self.trucklist for _,_truck in pairs(trucklist)do local truck=_truck if truck.group and truck.group:IsAlive()then else local tgtname=truck.targetname local targetdata=self:FindTarget(tgtname) if targetdata then if targetdata.statusquo~=AMMOTRUCK.State.IDLE then targetdata.statusquo=AMMOTRUCK.State.IDLE end end self.trucklist[truck.name]=nil end end local trucktable=self.truckset:GetSetObjects() for _,_truck in pairs(trucktable)do local truck=_truck if truck and truck:IsAlive()and not self.trucklist[truck:GetName()]then local name=truck:GetName() local newtruck={} newtruck.name=name newtruck.group=truck if self.hasarmygroup then if truck.ClassName and truck.ClassName=="GROUP"then local trucker=ARMYGROUP:New(truck) trucker:Activate() newtruck.group=trucker end end newtruck.statusquo=AMMOTRUCK.State.IDLE newtruck.timestamp=timer.getAbsTime() newtruck.coordinate=truck:GetCoordinate() newtruck.reloads=self.reloads or 5 self.trucklist[name]=newtruck end end return self end function AMMOTRUCK:onafterStart(From,Event,To) self:T({From,Event,To}) if ARMYGROUP and self.usearmygroup then self.hasarmygroup=true else self.hasarmygroup=false end if self.debug then BASE:TraceOn() BASE:TraceClass("AMMOTRUCK") end self:CheckTargetsAlive() self:CheckTrucksAlive() self:__Monitor(-30) return self end function AMMOTRUCK:onafterMonitor(From,Event,To) self:T({From,Event,To}) self:CheckTargetsAlive() self:CheckTrucksAlive() local remunition=false local remunitionqueue={} local waitingtargets={} for _,_ari in pairs(self.targetlist)do local data=_ari if data.group and data.group:IsAlive()then data.ammo=self:GetAmmoStatus(data.group) data.timestamp=timer.getAbsTime() local text=string.format("Ari %s | Ammo %d | State %s",data.name,data.ammo,data.statusquo) self:T(text) if data.ammo<=self.ammothreshold and(data.statusquo==AMMOTRUCK.State.IDLE or data.statusquo==AMMOTRUCK.State.OUTOFAMMO)then data.statusquo=AMMOTRUCK.State.OUTOFAMMO remunitionqueue[#remunitionqueue+1]=data remunition=true elseif data.statusquo==AMMOTRUCK.State.WAITING then waitingtargets[#waitingtargets+1]=data end else self.targetlist[data.name]=nil end end local idletrucks={} local drivingtrucks={} local unloadingtrucks={} local arrivedtrucks={} local returningtrucks={} local found=false for _,_truckdata in pairs(self.trucklist)do local data=_truckdata if data.group and data.group:IsAlive()then local text=string.format("Truck %s | State %s",data.name,data.statusquo) self:T(text) if data.statusquo==AMMOTRUCK.State.IDLE then idletrucks[#idletrucks+1]=data found=true elseif data.statusquo==AMMOTRUCK.State.DRIVING then drivingtrucks[#drivingtrucks+1]=data elseif data.statusquo==AMMOTRUCK.State.ARRIVED then arrivedtrucks[#arrivedtrucks+1]=data elseif data.statusquo==AMMOTRUCK.State.UNLOADING then unloadingtrucks[#unloadingtrucks+1]=data elseif data.statusquo==AMMOTRUCK.State.RETURNING then returningtrucks[#returningtrucks+1]=data if data.reloads>0 or data.reloads==-1 then idletrucks[#idletrucks+1]=data found=true end end else self.truckset[data.name]=nil end end local n=0 if found and remunition then for _,_truckdata in pairs(idletrucks)do local truckdata=_truckdata local truckcoord=truckdata.group:GetCoordinate() for _,_aridata in pairs(remunitionqueue)do local aridata=_aridata local aricoord=aridata.coordinate local distance=truckcoord:Get2DDistance(aricoord) if distance<=self.remunidist and aridata.statusquo==AMMOTRUCK.State.OUTOFAMMO and n<=#idletrucks then n=n+1 aridata.statusquo=AMMOTRUCK.State.REQUESTED self:__RouteTruck(n*5,truckdata,aridata) break end end end end if#drivingtrucks>0 then self:CheckDrivingTrucks(drivingtrucks) end if#arrivedtrucks>0 then self:CheckArrivedTrucks(arrivedtrucks) end if#unloadingtrucks>0 then self:CheckUnloadingTrucks(unloadingtrucks) end if#returningtrucks>0 then self:CheckReturningTrucks(returningtrucks) end if#waitingtargets>0 then self:CheckWaitingTargets(waitingtargets) end self:__Monitor(self.monitor) return self end function AMMOTRUCK:onafterRouteTruck(From,Event,To,Truckdata,Aridata) self:T({From,Event,To,Truckdata.name,Aridata.name}) local truckdata=Truckdata local aridata=Aridata local tgtgrp=aridata.group local tgtzone=ZONE_GROUP:New(aridata.name,tgtgrp,30) local tgtcoord=tgtzone:GetRandomCoordinate(15) if self.hasarmygroup then local mission=AUFTRAG:NewONGUARD(tgtcoord) local oldmission=truckdata.group:GetMissionCurrent() if oldmission then oldmission:Cancel()end mission:SetTime(5) mission:SetTeleport(false) truckdata.group:AddMission(mission) elseif self.routeonroad then truckdata.group:RouteGroundOnRoad(tgtcoord,30) else truckdata.group:RouteGroundTo(tgtcoord,30) end truckdata.statusquo=AMMOTRUCK.State.DRIVING truckdata.targetgroup=tgtgrp truckdata.targetname=aridata.name truckdata.targetcoordinate=tgtcoord aridata.statusquo=AMMOTRUCK.State.WAITING aridata.timestamp=timer.getAbsTime() return self end function AMMOTRUCK:onafterTruckUnloading(From,Event,To,Truckdata) local m=MESSAGE:New("Truck "..Truckdata.name.." unloading!",15,"AmmoTruck"):ToCoalitionIf(self.coalition,self.debug) local truck=Truckdata local coord=truck.group:GetCoordinate() local heading=truck.group:GetHeading() heading=heading<180 and(360-heading)or(heading-180) local cid=self.coalition==coalition.side.BLUE and country.id.USA or country.id.RUSSIA cid=self.coalition==coalition.side.NEUTRAL and country.id.UN_PEACEKEEPERS or cid local ammo={} for i=1,5 do ammo[i]=SPAWNSTATIC:NewFromType("ammo_cargo","Cargos",cid) :InitCoordinate(coord:Translate((15+((i-1)*4)),heading)) :Spawn(0,"AmmoCrate-"..math.random(1,10000)) end local function destroyammo(ammo) for _,_crate in pairs(ammo)do _crate:Destroy(false) end end local scheduler=SCHEDULER:New(nil,destroyammo,{ammo},self.waitingtime) if truck.reloads~=-1 then truck.reloads=truck.reloads-1 end return self end function AMMOTRUCK:onafterTruckReturning(From,Event,To,Truck) self:T({From,Event,To,Truck.name}) local truckdata=Truck local tgtzone=self.homezone local tgtcoord=tgtzone:GetRandomCoordinate() if self.hasarmygroup then local mission=AUFTRAG:NewONGUARD(tgtcoord) local oldmission=truckdata.group:GetMissionCurrent() if oldmission then oldmission:Cancel()end mission:SetTime(5) mission:SetTeleport(false) truckdata.group:AddMission(mission) elseif self.routeonroad then truckdata.group:RouteGroundOnRoad(tgtcoord,30,1,"Cone") else truckdata.group:RouteGroundTo(tgtcoord,30,"Cone",1) end return self end function AMMOTRUCK:onafterStop(From,Event,To) self:T({From,Event,To}) return self end AUTOLASE={ ClassName="AUTOLASE", lid="", verbose=0, alias="", debug=false, smokemenu=true, RoundingPrecision=0, increasegroundawareness=false, MonitorFrequency=30, } AUTOLASE.version="0.1.31" function AUTOLASE:New(RecceSet,Coalition,Alias,PilotSet) BASE:T({RecceSet,Coalition,Alias,PilotSet}) local self=BASE:Inherit(self,BASE:New()) if Coalition and type(Coalition)=="string"then if Coalition=="blue"then self.coalition=coalition.side.BLUE elseif Coalition=="red"then self.coalition=coalition.side.RED elseif Coalition=="neutral"then self.coalition=coalition.side.NEUTRAL else self:E("ERROR: Unknown coalition in AUTOLASE!") end end if Alias then self.alias=tostring(Alias) else self.alias="Lion" if self.coalition then if self.coalition==coalition.side.RED then self.alias="Wolf" elseif self.coalition==coalition.side.BLUE then self.alias="Fox" end end end local self=BASE:Inherit(self,INTEL:New(RecceSet,Coalition,Alias)) self.RecceSet=RecceSet self.DetectVisual=true self.DetectOptical=true self.DetectRadar=true self.DetectIRST=true self.DetectRWR=true self.DetectDLINK=true self.LaserCodes=UTILS.GenerateLaserCodes() self.LaseDistance=5000 self.LaseDuration=300 self.GroupsByThreat={} self.UnitsByThreat={} self.RecceNames={} self.RecceLaserCode={} self.RecceSmokeColor={} self.RecceUnitNames={} self.maxlasing=4 self.CurrentLasing={} self.lasingindex=0 self.deadunitnotes={} self.usepilotset=false self.reporttimeshort=10 self.reporttimelong=30 self.smoketargets=false self.smokecolor=SMOKECOLOR.Red self.smokeoffset=nil self.notifypilots=true self.targetsperrecce={} self.RecceUnits={} self.forcecooldown=true self.cooldowntime=60 self.useSRS=false self.SRSPath="" self.SRSFreq=251 self.SRSMod=radio.modulation.AM self.NoMenus=false self.minthreatlevel=0 self.blacklistattributes={} self:SetLaserCodes({1688,1130,4785,6547,1465,4578}) self.playermenus={} self.smokemenu=true self.threatmenu=true self.RoundingPrecision=0 self.increasegroundawareness=false self.MonitorFrequency=30 self:EnableSmokeMenu({Angle=math.random(0,359),Distance=math.random(10,20)}) self.lid=string.format("AUTOLASE %s (%s) | ",self.alias,self.coalition and UTILS.GetCoalitionName(self.coalition)or"unknown") self:AddTransition("*","Monitor","*") self:AddTransition("*","Lasing","*") self:AddTransition("*","TargetLost","*") self:AddTransition("*","TargetDestroyed","*") self:AddTransition("*","RecceKIA","*") self:AddTransition("*","LaserTimeout","*") self:AddTransition("*","Cancel","*") if PilotSet then self.usepilotset=true self.pilotset=PilotSet self:HandleEvent(EVENTS.PlayerEnterAircraft,self._EventHandler) end self:SetClusterAnalysis(false,false) self:__Start(2) self:__Monitor(math.random(5,10)) return self end function AUTOLASE:SetMonitorFrequency(Seconds) self.MonitorFrequency=Seconds or 30 return self end function AUTOLASE:SetLaserCodes(LaserCodes) self.LaserCodes=(type(LaserCodes)=="table")and LaserCodes or{LaserCodes} return self end function AUTOLASE:EnableImproveGroundUnitsDetection() self.increasegroundawareness=true return self end function AUTOLASE:DisableImproveGroundUnitsDetection() self.increasegroundawareness=false return self end function AUTOLASE:SetPilotMenu() if self.usepilotset then local pilottable=self.pilotset:GetSetObjects()or{} local grouptable={} for _,_unit in pairs(pilottable)do local Unit=_unit if Unit and Unit:IsAlive()then local Group=Unit:GetGroup() local GroupName=Group:GetName()or"none" local unitname=Unit:GetName() if not grouptable[GroupName]==true then if self.playermenus[unitname]then self.playermenus[unitname]:Remove()end local lasetopm=MENU_GROUP:New(Group,"Autolase",nil) self.playermenus[unitname]=lasetopm local lasemenu=MENU_GROUP_COMMAND:New(Group,"Status",lasetopm,self.ShowStatus,self,Group,Unit) if self.smokemenu then local smoke=(self.smoketargets==true)and"off"or"on" local smoketext=string.format("Switch smoke targets to %s",smoke) local smokemenu=MENU_GROUP_COMMAND:New(Group,smoketext,lasetopm,self.SetSmokeTargets,self,(not self.smoketargets)) end if self.threatmenu then local threatmenutop=MENU_GROUP:New(Group,"Set min lasing threat",lasetopm) for i=0,10,2 do local text="Threatlevel "..tostring(i) local threatmenu=MENU_GROUP_COMMAND:New(Group,text,threatmenutop,self.SetMinThreatLevel,self,i) end end for _,_grp in pairs(self.RecceSet.Set)do local grp=_grp local unit=grp:GetUnit(1) if unit and unit:IsAlive()then local name=unit:GetName() local mname=string.gsub(name,".%d+.%d+$","") local code=self:GetLaserCode(name) local unittop=MENU_GROUP:New(Group,"Change laser code for "..mname,lasetopm) for _,_code in pairs(self.LaserCodes)do local text=tostring(_code) if _code==code then text=text.."(*)"end local changemenu=MENU_GROUP_COMMAND:New(Group,text,unittop,self.SetRecceLaserCode,self,name,_code,true) end end end grouptable[GroupName]=true end end end else if not self.NoMenus then self.Menu=MENU_COALITION_COMMAND:New(self.coalition,"Autolase",nil,self.ShowStatus,self) end end return self end function AUTOLASE:_EventHandler(EventData) self:SetPilotMenu() return self end function AUTOLASE:SetMinThreatLevel(Level) local level=Level or 0 if level<0 or level>10 then level=0 end self.minthreatlevel=level return self end function AUTOLASE:AddBlackListAttributes(Attributes) local attributes=Attributes if type(attributes)~="table"then attributes={attributes} end for _,_attr in pairs(attributes)do table.insert(self.blacklistattributes,_attr) end return self end function AUTOLASE:GetLaserCode(RecceName) local code=1688 if self.RecceLaserCode[RecceName]==nil then code=self.LaserCodes[math.random(#self.LaserCodes)] self.RecceLaserCode[RecceName]=code else code=self.RecceLaserCode[RecceName] end return code end function AUTOLASE:GetSmokeColor(RecceName) local color=self.smokecolor if self.RecceSmokeColor[RecceName]==nil then self.RecceSmokeColor[RecceName]=color else color=self.RecceSmokeColor[RecceName] end return color end function AUTOLASE:SetUsingSRS(OnOff,Path,Frequency,Modulation,Label,Gender,Culture,Port,Voice,Volume,PathToGoogleKey,Provider,Speaker) if OnOff then self.useSRS=true self.SRSPath=Path or MSRS.path or"C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio" self.SRSFreq=Frequency or 271 self.SRSMod=Modulation or radio.modulation.AM self.Gender=Gender or MSRS.gender or"male" self.Culture=Culture or MSRS.culture or"en-US" self.Port=Port or MSRS.port or 5002 self.Voice=Voice self.PathToGoogleKey=PathToGoogleKey self.Volume=Volume or 1.0 self.Label=Label self.SRS=MSRS:New(self.SRSPath,self.SRSFreq,self.SRSMod) self.SRS:SetCoalition(self.coalition) self.SRS:SetLabel(self.MenuName or self.Name) self.SRS:SetGender(self.Gender) self.SRS:SetCulture(self.Culture) self.SRS:SetPort(self.Port) self.SRS:SetVoice(self.Voice) if Speaker then self.SRS:SetSpeakerPiper(Speaker) end self.SRS:SetCoalition(self.coalition) self.SRS:SetVolume(self.Volume) if self.PathToGoogleKey and not Provider then self.SRS:SetProviderOptionsGoogle(PathToGoogleKey,PathToGoogleKey) self.SRS:SetProvider(MSRS.Provider.GOOGLE) end if Provider then self.SRS:SetProvider(Provider) end self.SRSQueue=MSRSQUEUE:New(self.alias) else self.useSRS=false self.SRS=nil self.SRSQueue=nil end return self end function AUTOLASE:SetMaxLasingTargets(Number) self.maxlasing=Number or 4 return self end function AUTOLASE:SetNotifyPilots(OnOff) self.notifypilots=OnOff and true return self end function AUTOLASE:SetRecceLaserCode(RecceName,Code,Refresh) local code=Code or 1688 self.RecceLaserCode[RecceName]=code if Refresh then self:SetPilotMenu() if self.notifypilots then if string.find(RecceName,"#")then RecceName=string.match(RecceName,"^(.*)#") end self:NotifyPilots(string.format("Code for %s set to: %d",RecceName,Code),15) end end return self end function AUTOLASE:SetRecceSmokeColor(RecceName,Color) local color=Color or self.smokecolor self.RecceSmokeColor[RecceName]=color return self end function AUTOLASE:SetLaserCoolDown(OnOff,Seconds) self.forcecooldown=OnOff and true self.cooldowntime=Seconds or 60 return self end function AUTOLASE:SetReportingTimes(long,short) self.reporttimeshort=short or 10 self.reporttimelong=long or 30 return self end function AUTOLASE:SetLasingParameters(Distance,Duration) self.LaseDistance=Distance or 5000 self.LaseDuration=Duration or 300 return self end function AUTOLASE:SetSmokeTargets(OnOff,Color) self.smoketargets=OnOff self.smokecolor=Color or SMOKECOLOR.Red local smktxt=OnOff==true and"on"or"off" local Message="Smoking targets is now "..smktxt.."!" self:NotifyPilots(Message,10) return self end function AUTOLASE:SetRoundingPrecsion(IDP) self.RoundingPrecision=IDP or 0 return self end function AUTOLASE:EnableSmokeMenu(Offset) self.smokemenu=true if Offset then self.smokeoffset={} self.smokeoffset.Distance=Offset.Distance or math.random(10,20) self.smokeoffset.Angle=Offset.Angle or math.random(0,359) end return self end function AUTOLASE:DisableSmokeMenu() self.smokemenu=false self.smokeoffset=nil return self end function AUTOLASE:EnableThreatLevelMenu() self.threatmenu=true return self end function AUTOLASE:DisableThreatLevelMenu() self.threatmenu=false return self end function AUTOLASE:GetLosFromUnit(Unit) local lasedistance=self.LaseDistance local unitheight=Unit:GetHeight() local coord=Unit:GetCoord() local landheight=coord:GetLandHeight() local asl=unitheight-landheight if asl>100 then local absquare=lasedistance^2+asl^2 lasedistance=math.sqrt(absquare) end return lasedistance end function AUTOLASE:CleanCurrentLasing() local lasingtable=self.CurrentLasing local newtable={} local newreccecount={} local lasing=0 for _ind,_entry in pairs(lasingtable)do local entry=_entry if not newreccecount[entry.reccename]then newreccecount[entry.reccename]=0 end end for _,_recce in pairs(self.RecceSet:GetSetObjects())do local recce=_recce if recce and recce:IsAlive()then local unit=recce:GetUnit(1) local name=unit:GetName() if not self.RecceUnits[name]then local isground=(unit and unit.IsGround)and unit:IsGround()or false self.RecceUnits[name]={name=name,unit=unit,cooldown=false,timestamp=timer.getAbsTime(),isground=isground} end end end for _ind,_entry in pairs(lasingtable)do local entry=_entry local valid=0 local reccedead=false local unitdead=false local lostsight=false local timeout=false local Tnow=timer.getAbsTime() local recce=entry.lasingunit if recce and recce:IsAlive()then valid=valid+1 else reccedead=true self:__RecceKIA(2,entry.reccename) end local unit=entry.lasedunit if unit and unit:IsAlive()==true then valid=valid+1 else unitdead=true if not self.deadunitnotes[entry.unitname]then self.deadunitnotes[entry.unitname]=true self:__TargetDestroyed(2,entry.unitname,entry.reccename) end end if not reccedead and not unitdead then if self:CanLase(recce,unit)then valid=valid+1 else lostsight=true entry.laserspot:LaseOff() self:__TargetLost(2,entry.unitname,entry.reccename) end end local timestamp=entry.timestamp if Tnow-timestamp10 or needsinit==true or TNow-_data.timestamp>29 then local hasunits,hasstatics,_,Units,Statics=position:ScanObjects(self.LaseDistance,true,true,false) if hasunits then self:T(self.lid.."Checking possibly visible UNITs for Recce "..unit:GetName()) for _,_target in pairs(Units)do local target=_target if target and target:GetCoalition()~=self.coalition then if unit:IsLOS(target)and(not target:IsUnitDetected(unit))then unit:KnowUnit(target,true,true) end end end end if hasstatics then self:T(self.lid.."Checking possibly visible STATICs for Recce "..unit:GetName()) for _,_static in pairs(Statics)do local static=STATIC:Find(_static) if static and static:GetCoalition()~=self.coalition and static:GetCoord()then local IsLOS=position:IsLOS(static:GetCoord()) if IsLOS then unit:KnowUnit(static,true,true) end end end end end end end end return self end function AUTOLASE:onbeforeMonitor(From,Event,To) self:T({From,Event,To}) if self.increasegroundawareness then self:_Prescient() end self:UpdateIntel() return self end function AUTOLASE:onafterMonitor(From,Event,To) self:T({From,Event,To}) local countlases=self:CleanCurrentLasing() self:SetPilotMenu() local detecteditems=self.Contacts or{} local groupsbythreat={} local report=REPORT:New("Detections") local lines=0 for _,_contact in pairs(detecteditems)do local contact=_contact local grp=contact.group local coord=contact.position local reccename=contact.recce or"none" local threat=contact.threatlevel or 0 local reccegrp=UNIT:FindByName(reccename) if reccegrp then local reccecoord=reccegrp:GetCoord() local distance=math.floor(reccecoord:Get3DDistance(coord)) local text=string.format("%s of %s | Distance %d km | Threatlevel %d",contact.attribute,contact.groupname,math.floor(distance/1000),contact.threatlevel) report:Add(text) self:T(text) if self.debug then self:I(text)end lines=lines+1 local lasedistance=self:GetLosFromUnit(reccegrp) if grp:IsGround()and lasedistance>=distance and threat>=self.minthreatlevel then table.insert(groupsbythreat,{contact.group,contact.threatlevel}) self.RecceNames[contact.groupname]=contact.recce end end end self.GroupsByThreat=groupsbythreat if self.verbose>2 and lines>0 then local m=MESSAGE:New(report:Text(),self.reporttimeshort,"Autolase"):ToAll() end table.sort(self.GroupsByThreat,function(a,b) local aNum=a[2] local bNum=b[2] return aNum>bNum end) local unitsbythreat={} for _,_entry in pairs(self.GroupsByThreat)do local group=_entry[1] if group and group:IsAlive()then local units=group:GetUnits() local reccename=self.RecceNames[group:GetName()] for _,_unit in pairs(units)do local unit=_unit if unit and unit:IsAlive()then local threat=unit:GetThreatLevel() if threat>=self.minthreatlevel then local unitname=unit:GetName() if unit:HasAttribute("RADAR_BAND1_FOR_ARM")or unit:HasAttribute("RADAR_BAND2_FOR_ARM")or unit:HasAttribute("Optical Tracker")then threat=11 end table.insert(unitsbythreat,{unit,threat}) self.RecceUnitNames[unitname]=reccename end end end end end self.UnitsByThreat=unitsbythreat table.sort(self.UnitsByThreat,function(a,b) local aNum=a[2] local bNum=b[2] return aNum>bNum end) local unitreport=REPORT:New("Detected Units") local lines=0 for _,_entry in pairs(self.UnitsByThreat)do local threat=_entry[2] local unit=_entry[1] local unitname=unit:GetName() local text=string.format("Unit %s | Threatlevel %d | Detected by %s",unitname,threat,self.RecceUnitNames[unitname]) unitreport:Add(text) lines=lines+1 self:T(text) if self.debug then self:I(text)end end if self.verbose>2 and lines>0 then local m=MESSAGE:New(unitreport:Text(),self.reporttimeshort,"Autolase"):ToAll() end for _,_detectingunit in pairs(self.RecceUnits)do local reccename=_detectingunit.name local recce=_detectingunit.unit local reccecount=self.targetsperrecce[reccename]or 0 local targets=0 for _,_entry in pairs(self.UnitsByThreat)do local unit=_entry[1] local unitname=unit:GetName() local canlase=self:CanLase(recce,unit) if targets+reccecount0 then local SwitchAAA=self.SwitchAAA local group_coalition=group:GetCoalition() ground:ForEachGroupAlive( function(grp) local tiresias_data=grp.Tiresias if grp:GetCoalition()~=group_coalition and tiresias_data and tiresias_data.type and not tiresias_data.exception==true then if tiresias_data.invisible==true then grp:SetCommandInvisible(false) tiresias_data.invisible=false end local grp_type=tiresias_data.type if grp_type=="Vehicle"and tiresias_data.AIOff==true then grp:SetAIOn() tiresias_data.AIOff=false elseif SwitchAAA==true and grp_type=="AAA"and tiresias_data.AIOff==true then grp:SetAIOn() grp:EnableEmission(true) tiresias_data.AIOff=false end else BASE:T("TIRESIAS - This group "..tostring(grp:GetName()).." has not been initialized or is an exception!") end end ) end return self end function TIRESIAS:onafterStart(From,Event,To) self:T({From,Event,To}) local VehicleSet=SET_GROUP:New():FilterCategoryGround():FilterFunction(TIRESIAS._FilterNotAAA):FilterFunction(TIRESIAS._FilterNotSAM):FilterStart() local AAASet=SET_GROUP:New():FilterCategoryGround():FilterFunction(TIRESIAS._FilterAAA):FilterStart() local SAMSet=SET_GROUP:New():FilterCategoryGround():FilterFunction(TIRESIAS._FilterSAM):FilterStart() local OpsGroupSet=SET_OPSGROUP:New():FilterActive(true):FilterStart() self.FlightSet=SET_GROUP:New():FilterCategories({" plane"," helicopter"}):FilterStart() local EngageRange=self.AAARange local SwitchAAA=self.SwitchAAA local ExceptionSet=self.ExceptionSet local exception_data={ type=" Exception", exception=true, } local vehicle_data={ type=" Vehicle", invisible=true, AIOff=true, exception=false, } local aaa_data={ type=" AAA", invisible=true, range=EngageRange, exception=false, AIOff=SwitchAAA, } local sam_data={ type=" SAM", invisible=true, exception=false, } if ExceptionSet then function ExceptionSet:OnAfterAdded(From,Event,To,ObjectName,Object) BASE:I(" TIRESIAS: EXCEPTION Object Added: "..Object:GetName()) if Object and Object:IsAlive()then Object.Tiresias=exception_data Object:SetAIOn() Object:SetCommandInvisible(false) Object:EnableEmission(true) end end local OGS=OpsGroupSet:GetAliveSet() for _,_OG in pairs(OGS or{})do local OG=_OG local grp=OG:GetGroup() ExceptionSet:AddGroup(grp,true) end function OpsGroupSet:OnAfterAdded(From,Event,To,ObjectName,Object) local grp=Object:GetGroup() ExceptionSet:AddGroup(grp,true) end end function VehicleSet:OnAfterAdded(From,Event,To,ObjectName,Object) BASE:T(" TIRESIAS: VEHICLE Object Added: "..Object:GetName()) if Object and Object:IsAlive()then Object:SetAIOff() Object:SetCommandInvisible(true) Object.Tiresias=vehicle_data end end function AAASet:OnAfterAdded(From,Event,To,ObjectName,Object) if Object and Object:IsAlive()then BASE:I(" TIRESIAS: AAA Object Added: "..Object:GetName()) Object:OptionEngageRange(EngageRange) Object:SetCommandInvisible(true) if SwitchAAA then Object:SetAIOff() Object:EnableEmission(false) end Object.Tiresias=aaa_data end end function SAMSet:OnAfterAdded(From,Event,To,ObjectName,Object) if Object and Object:IsAlive()then BASE:T(" TIRESIAS: SAM Object Added: "..Object:GetName()) Object:SetCommandInvisible(true) Object.Tiresias=sam_data end end self.VehicleSet=VehicleSet self.AAASet=AAASet self.SAMSet=SAMSet self.OpsGroupSet=OpsGroupSet self:_InitGroups() self:__Status(1) return self end function TIRESIAS:onbeforeStatus(From,Event,To) self:T({From,Event,To}) return self:GetState()~=" Stopped" end function TIRESIAS:onafterStatus(From,Event,To) self:T({From,Event,To}) if self.debug then local count=self.VehicleSet:CountAlive() local AAAcount=self.AAASet:CountAlive() local SAMcount=self.SAMSet:CountAlive() self:I(string.format(" Overall: %d | Vehicles: %d | AAA: %d | SAM: %d", count+AAAcount+SAMcount,count,AAAcount,SAMcount)) end self:_InitGroups() local flight_count=self.FlightSet:CountAlive() if flight_count>0 then local Set=self.FlightSet:GetAliveSet() local helo_range=self.HeloSwitchRange local plane_range=self.PlaneSwitchRange for _,_plane in pairs(Set or{})do local plane=_plane local radius=plane:IsHelicopter()and helo_range or plane_range self:_SwitchOnGroups(plane,radius) end end if self:GetState()~=" Stopped"then self:__Status(self.Interval) end return self end function TIRESIAS:onafterStop(From,Event,To) self:T({From,Event,To}) self:UnHandleEvent(EVENTS.PlayerEnterAircraft) self._cached_zones={} return self end STRATEGO={ ClassName="STRATEGO", debug=false, drawzone=false, markzone=false, version="0.3.1", portweight=3, POIweight=1, maxrunways=3, coalition=nil, colors=nil, airbasetable={}, nonconnectedab={}, easynames={}, maxdist=150, disttable={}, routexists={}, routefactor=5, OpsZones={}, NeutralBenefit=100, Budget=0, usebudget=false, CaptureUnits=3, CaptureThreatlevel=1, CaptureObjectCategories={Object.Category.UNIT}, ExcludeShips=true, } STRATEGO.Type={ AIRBASE="AIRBASE", PORT="PORT", POI="POI", FARP="FARP", SHIP="SHIP", } function STRATEGO:New(Name,Coalition,MaxDist) local self=BASE:Inherit(self,FSM:New()) self.coalition=Coalition self.coalitiontext=UTILS.GetCoalitionName(Coalition) self.name=Name or"Hannibal" self.maxdist=MaxDist or 150 self.disttable={} self.routexists={} self.ExcludeShips=true self.lid=string.format("STRATEGO %s %s | ",self.name,self.version) self.bases=SET_AIRBASE:New():FilterOnce() self.ports=SET_ZONE:New():FilterPrefixes("Port"):FilterOnce() self.POIs=SET_ZONE:New():FilterPrefixes("POI"):FilterOnce() self.colors={ [1]={0,1,0}, [2]={1,0,0}, [3]={0,0,1}, [4]={1,0.65,0}, } self:SetStartState("Stopped") self:AddTransition("Stopped","Start","Running") self:AddTransition("*","Update","*") self:AddTransition("*","NodeEvent","*") self:AddTransition("Running","Stop","Stopped") return self end function STRATEGO:onafterStart(From,Event,To) self:T(self.lid.."Start") self:AnalyseBases() self:AnalysePOIs(self.ports,self.portweight,"PORT") self:AnalysePOIs(self.POIs,self.POIweight,"POI") for i=self.maxrunways,1,-1 do self:AnalyseRoutes(i,i*self.routefactor,self.colors[(i%3)+1],i) end self:AnalyseUnconnected(self.colors[4]) self:I(self.lid.."Advisory ready.") self:__Update(180) return self end function STRATEGO:onafterUpdate(From,Event,To) self:T(self.lid.."Update") self:UpdateNodeCoalitions() if self:GetState()=="Running"then self:__Update(180) end return self end function STRATEGO:SetUsingBudget(Usebudget,StartBudget) self:T(self.lid.."SetUsingBudget") self.usebudget=Usebudget self.Budget=StartBudget return self end function STRATEGO:SetDebug(Debug,DrawZones,MarkZones) self:T(self.lid.."SetDebug") self.debug=Debug self.drawzone=DrawZones self.markzone=MarkZones return self end function STRATEGO:SetStrategoZone(Zone) self.StrategoZone=Zone return self end function STRATEGO:SetWeights(MaxRunways,PortWeight,POIWeight,RouteFactor) self:T(self.lid.."SetWeights") self.portweight=PortWeight or 3 self.POIweight=POIWeight or 1 self.maxrunways=MaxRunways or 3 self.routefactor=RouteFactor or 5 return self end function STRATEGO:SetNeutralBenefit(NeutralBenefit) self:T(self.lid.."SetNeutralBenefit") self.NeutralBenefit=NeutralBenefit or 100 return self end function STRATEGO:SetCaptureOptions(CaptureUnits,CaptureThreatlevel,CaptureCategories) self:T(self.lid.."SetCaptureOptions") self.CaptureUnits=CaptureUnits or 3 self.CaptureThreatlevel=CaptureThreatlevel or 1 self.CaptureObjectCategories=CaptureCategories or{Object.Category.UNIT} return self end function STRATEGO:AnalyseBases() self:T(self.lid.."AnalyseBases") local colors=self.colors local debug=self.debug local airbasetable=self.airbasetable local nonconnectedab=self.nonconnectedab local easynames=self.easynames local zone=self.StrategoZone self.bases:ForEach( function(afb) local ab=afb local abvec2=ab:GetVec2() if self.ExcludeShips and ab:IsShip()then return end if zone~=nil then if not zone:IsVec2InZone(abvec2)then return end end local abname=ab:GetName() local runways=ab:GetRunways() local numrwys=#runways if numrwys>=1 then numrwys=numrwys*0.5 end local abzone=ab:GetZone() if not abzone then abzone=ZONE_RADIUS:New(abname,ab:GetVec2(),500) end local coa=ab:GetCoalition() if coa==nil then return end coa=coa+1 local abtype=STRATEGO.Type.AIRBASE if ab:IsShip()then numrwys=1 abtype=STRATEGO.Type.SHIP end if ab:IsHelipad()then numrwys=1 abtype=STRATEGO.Type.FARP end local coord=ab:GetCoordinate() if debug then abzone:DrawZone(-1,colors[coa],1,colors[coa],0.3,1) coord:TextToAll(tostring(numrwys),-1,{0,0,0},1,colors[coa],0.3,20) end local opszone=self:GetNewOpsZone(abname,coa-1) local tbl={ name=abname, baseweight=numrwys, weight=0, coalition=coa-1, port=false, zone=abzone, coord=coord, type=abtype, opszone=opszone, connections=0, } airbasetable[abname]=tbl nonconnectedab[abname]=true local name=string.gsub(abname,"[%p%s]",".") easynames[name]=abname end ) return self end function STRATEGO:UpdateNodeCoalitions() self:T(self.lid.."UpdateNodeCoalitions") local newtable={} for _id,_data in pairs(self.airbasetable)do local data=_data if data.type==STRATEGO.Type.AIRBASE or data.type==STRATEGO.Type.FARP or data.type==STRATEGO.Type.SHIP then data.coalition=AIRBASE:FindByName(data.name):GetCoalition()or 0 else data.coalition=data.opszone:GetOwner()or 0 end newtable[_id]=_data end self.airbasetable=nil self.airbasetable=newtable return self end function STRATEGO:GetNewOpsZone(Zone,Coalition) self:T(self.lid.."GetNewOpsZone") local opszone=OPSZONE:New(Zone,Coalition or 0) opszone:SetCaptureNunits(self.CaptureUnits) opszone:SetCaptureThreatlevel(self.CaptureThreatlevel) opszone:SetObjectCategories(self.CaptureObjectCategories) opszone:SetDrawZone(self.drawzone) opszone:SetMarkZone(self.markzone) opszone:Start() local function Captured(opszone,coalition) self:__NodeEvent(1,opszone,coalition) end function opszone:OnBeforeCaptured(From,Event,To,Coalition) Captured(opszone,Coalition) end return opszone end function STRATEGO:AnalysePOIs(Set,Weight,Key) self:T(self.lid.."AnalysePOIs") local colors=self.colors local debug=self.debug local airbasetable=self.airbasetable local nonconnectedab=self.nonconnectedab local easynames=self.easynames Set:ForEach( function(port) local zone=port local zname=zone:GetName() local coord=zone:GetCoordinate() if debug then zone:DrawZone(-1,colors[1],1,colors[1],0.3,1) coord:TextToAll(tostring(Weight),-1,{0,0,0},1,colors[1],0.3,20) end local opszone=self:GetNewOpsZone(zone) local tbl={ name=zname, baseweight=Weight, weight=0, coalition=coalition.side.NEUTRAL, port=true, zone=zone, coord=coord, type=Key, opszone=opszone, connections=0, } airbasetable[zname]=tbl nonconnectedab[zname]=true local name=string.gsub(zname,"[%p%s]",".") easynames[name]=zname end ) return self end function STRATEGO:GetToFrom(StartPoint,EndPoint) self:T(self.lid.."GetToFrom "..tostring(StartPoint).." "..tostring(EndPoint)) local pstart=string.gsub(StartPoint,"[%p%s]",".") local pend=string.gsub(EndPoint,"[%p%s]",".") local fromto=pstart..";"..pend local tofrom=pend..";"..pstart return fromto,tofrom end function STRATEGO:GetRoutesFromNode(StartPoint) self:T(self.lid.."GetRoutesFromNode") local pstart=string.gsub(StartPoint,"[%p%s]",".") local found=false pstart=pstart..";" local routes={} local listed={} for _,_data in pairs(self.routexists)do if string.find(_data,pstart,1,true)and not listed[_data]then local target=string.gsub(_data,pstart,"") local fname=self.easynames[target] table.insert(routes,fname) found=true listed[_data]=true end end return found,routes end function STRATEGO:AddRoutesManually(Startpoint,Endpoint,Color,Linetype,Draw) self:T(self.lid.."AddRoutesManually") local fromto,tofrom=self:GetToFrom(Startpoint,Endpoint) local startcoordinate=self.airbasetable[Startpoint].coord local targetcoordinate=self.airbasetable[Endpoint].coord local dist=UTILS.Round(targetcoordinate:Get2DDistance(startcoordinate),-2)/1000 local color=Color or{136/255,0,1} local linetype=Linetype or 5 local data={ start=Startpoint, target=Endpoint, dist=dist, } self.disttable[fromto]=data self.disttable[tofrom]=data table.insert(self.routexists,fromto) table.insert(self.routexists,tofrom) self.nonconnectedab[Endpoint]=false self.nonconnectedab[Startpoint]=false local factor=self.airbasetable[Startpoint].baseweight*self.routefactor self.airbasetable[Startpoint].weight=self.airbasetable[Startpoint].weight+factor self.airbasetable[Endpoint].weight=self.airbasetable[Endpoint].weight+factor self.airbasetable[Endpoint].connections=self.airbasetable[Endpoint].connections+2 self.airbasetable[Startpoint].connections=self.airbasetable[Startpoint].connections+2 if self.debug or Draw then startcoordinate:LineToAll(targetcoordinate,-1,color,1,linetype,nil,string.format("%dkm",dist)) end return self end function STRATEGO:AnalyseRoutes(tgtrwys,factor,color,linetype) self:T(self.lid.."AnalyseRoutes") for _,_ab in pairs(self.airbasetable)do if _ab.baseweight>=1 then local startpoint=_ab.name local startcoord=_ab.coord for _,_data in pairs(self.airbasetable)do local fromto,tofrom=self:GetToFrom(startpoint,_data.name) if _data.name==startpoint then elseif _data.baseweight==tgtrwys and not(self.routexists[fromto]or self.routexists[tofrom])then local tgtc=_data.coord local dist=UTILS.Round(tgtc:Get2DDistance(startcoord),-2)/1000 if dist<=self.maxdist then local data={ start=startpoint, target=_data.name, dist=dist, } self.disttable[fromto]=data self.disttable[tofrom]=data table.insert(self.routexists,fromto) table.insert(self.routexists,tofrom) self.nonconnectedab[_data.name]=false self.nonconnectedab[startpoint]=false self.airbasetable[startpoint].weight=self.airbasetable[startpoint].weight+factor self.airbasetable[_data.name].weight=self.airbasetable[_data.name].weight+factor self.airbasetable[startpoint].connections=self.airbasetable[startpoint].connections+1 self.airbasetable[_data.name].connections=self.airbasetable[_data.name].connections+1 if self.debug then startcoord:LineToAll(tgtc,-1,color,1,linetype,nil,string.format("%dkm",dist)) end end end end end end return self end function STRATEGO:AnalyseUnconnected(Color) self:T(self.lid.."AnalyseUnconnected") for _name,_noconnect in pairs(self.nonconnectedab)do if _noconnect then local startpoint=_name local startcoord=self.airbasetable[_name].coord local shortest=1000*1000 local closest=nil local closestcoord=nil for _,_data in pairs(self.airbasetable)do if _name~=_data.name then local tgtc=_data.coord local dist=UTILS.Round(tgtc:Get2DDistance(startcoord),-2)/1000 if dist=weight and okay then weight=_data.weight if not airbases[weight]then airbases[weight]={}end table.insert(airbases[weight],_name) end if _data.weight>highest and okay then highest=_data.weight highname=_name end end return airbases[weight],weight,highest,highname end function STRATEGO:GetNextHighestWeightNodes(Weight,Coalition) self:T(self.lid.."GetNextHighestWeightNodes") local weight=0 local airbases={} for _name,_data in pairs(self.airbasetable)do local okay=true if Coalition then if _data.coalition~=Coalition then okay=false end end if _data.weight>=weight and _data.weight=targetweight)then self:T("Found Consolidation Target: "..cname) shortest=dist target=cname weight=self.airbasetable[cname].weight coa=coa end end end return shortest,target,weight,coa end function STRATEGO:FindClosestStrategicTarget(Startpoint,Weight) self:T(self.lid.."FindClosestStrategicTarget for "..Startpoint.." Weight "..Weight or 0) local shortest=1000*1000 local target=nil local weight=0 local coa=nil if not Weight then Weight=self.maxrunways end local startpoint=string.gsub(Startpoint,"[%p%s]",".") for _,_route in pairs(self.routexists)do if string.find(_route,startpoint,1,true)then local dist=self.disttable[_route].dist local tname=string.gsub(_route,startpoint,"") local tname=string.gsub(tname,";","") local cname=self.easynames[tname] local coa=self.airbasetable[cname].coalition local tweight=self.airbasetable[cname].baseweight local ttweight=self.airbasetable[cname].weight if(dist=Weight)then self:T("Found Strategic Target: "..cname) shortest=dist target=cname weight=self.airbasetable[cname].weight coa=self.airbasetable[cname].coalition end end end return shortest,target,weight,coa end function STRATEGO:FindStrategicTargets() self:T(self.lid.."FindStrategicTargets") local targets={} for _,_data in pairs(self.airbasetable)do local data=_data if data.coalition==self.coalition then local dist,name,points,coa=self:FindClosestStrategicTarget(data.name,data.weight) if points>0 then self:T({dist=dist,name=name,points=points,coa=coa}) end if points~=0 then local enemycoa=self.coalition==coalition.side.BLUE and coalition.side.RED or coalition.side.BLUE self:T("Enemycoa = "..enemycoa) if coa==coalition.side.NEUTRAL then local tdata={} tdata.name=name tdata.dist=dist tdata.points=points+self.NeutralBenefit tdata.coalition=coa tdata.coalitionname=UTILS.GetCoalitionName(coa) tdata.coordinate=self.airbasetable[name].coord table.insert(targets,tdata) else local tdata={} tdata.name=name tdata.dist=dist tdata.points=points tdata.coalition=coa tdata.coalitionname=UTILS.GetCoalitionName(coa) tdata.coordinate=self.airbasetable[name].coord table.insert(targets,tdata) end end end end return targets end function STRATEGO:FindConsolidationTargets() self:T(self.lid.."FindConsolidationTargets") local targets={} for _,_data in pairs(self.airbasetable)do local data=_data if data.coalition==self.coalition then local dist,name,points,coa=self:FindClosestConsolidationTarget(data.name,self.maxrunways-1) if points>0 then self:T({dist=dist,name=name,points=points,coa=coa}) end if points~=0 then local enemycoa=self.coalition==coalition.side.BLUE and coalition.side.RED or coalition.side.BLUE self:T("Enemycoa = "..enemycoa) if coa==coalition.side.NEUTRAL then local tdata={} tdata.name=name tdata.dist=dist tdata.points=points+self.NeutralBenefit tdata.coalition=coa tdata.coalitionname=UTILS.GetCoalitionName(coa) tdata.coordinate=self.airbasetable[name].coord table.insert(targets,tdata) else local tdata={} tdata.name=name tdata.dist=dist tdata.points=points tdata.coalition=coa tdata.coalitionname=UTILS.GetCoalitionName(coa) tdata.coordinate=self.airbasetable[name].coord table.insert(targets,tdata) end end end end return targets end function STRATEGO:FindNeighborNodes(Name,Enemies,Friends) self:T(self.lid.."FindNeighborNodes") local neighbors={} local name=string.gsub(Name,"[%p%s]",".") local shortestdist=1000*1000 local nearest=nil for _route,_data in pairs(self.disttable)do if string.find(_route,name,1,true)then local dist=self.disttable[_route] local tname=string.gsub(_route,name,"") local tname=string.gsub(tname,";","") local cname=self.easynames[tname] if cname then local encoa=self.coalition==coalition.side.BLUE and coalition.side.RED or coalition.side.BLUE if Enemies==true then if self.airbasetable[cname].coalition==encoa then neighbors[cname]=dist end elseif Friends==true then if self.airbasetable[cname].coalition~=encoa then neighbors[cname]=dist end else neighbors[cname]=dist end if neighbors[cname]and dist.dist2 and true or false if _name==End then nnodes=true end if kcoord~=nil and ecoord~=nil and nnodes==true and InRoute[_name]~=true then local dist=math.floor((kcoord:Get2DDistance(ecoord)/1000)+0.5) if(distlargestcut then largestcut=j-i cut={i=i,j=j} foundcut=true end end end end end if foundcut then local newroute={} for i=1,#Route do if i<=cut.i or i>=cut.j then table.insert(newroute,Route[i]) end end return newroute end return Route,foundcut end if routecomplete==true and NoOptimize~=true then local foundcut=true while foundcut~=false do Route,foundcut=OptimizeRoute(Route) end else Route,routecomplete=self:FindRoute(End,Start,Hops,Draw,Color,LineType) reverse=true end if(self.debug or Draw)then DrawRoute(Route)end return Route,routecomplete,reverse end function STRATEGO:AddBudget(Number) self:T(self.lid.."AddBudget") self.Budget=self.Budget+Number return self end function STRATEGO:SubtractBudget(Number) self:T(self.lid.."SubtractBudget") self.Budget=self.Budget-Number return self end function STRATEGO:GetBudget() self:T(self.lid.."GetBudget") return self.Budget end function STRATEGO:FindAffordableStrategicTarget() self:T(self.lid.."FindAffordableStrategicTarget") local Stargets=self:FindStrategicTargets() local budget=self.Budget local ftarget=nil local Targets={} for _,_data in pairs(Stargets)do local data=_data self:T("Considering Strategic Target "..data.name) if data.points<=budget then table.insert(Targets,data) self:T(self.lid.."Affordable strategic target: "..data.name) end end if#Targets==0 then self:T(self.lid.."No suitable target found!") return nil end if#Targets>1 then ftarget=Targets[math.random(1,#Targets)] else ftarget=Targets[1] end if ftarget then self:T(self.lid.."Final affordable strategic target: "..ftarget.name) return ftarget else return nil end end function STRATEGO:FindAffordableConsolidationTarget() self:T(self.lid.."FindAffordableConsolidationTarget") local Ctargets=self:FindConsolidationTargets() local budget=self.Budget local ftarget=nil local Targets={} for _,_data in pairs(Ctargets)do local data=_data self:T("Considering Consolidation Target "..data.name) if data.points<=budget then table.insert(Targets,data) self:T(self.lid.."Affordable consolidation target: "..data.name) end end if#Targets==0 then self:T(self.lid.."No suitable target found!") return nil end if#Targets>1 then ftarget=Targets[math.random(1,#Targets)] else ftarget=Targets[1] end if ftarget then self:T(self.lid.."Final affordable consolidation target: "..ftarget.name) return ftarget else return nil end end function STRATEGO:_FloodNext(next,filled,unfilled) local start=self:FindNeighborNodes(next) for _name,_ in pairs(start)do if filled[_name]~=true then self:T("Flooding ".._name) filled[_name]=true unfilled[_name]=nil self:_FloodNext(_name,filled,unfilled) end end return self end function STRATEGO:_FloodFill(Start,ABTable) self:T("Start = "..tostring(Start)) if Start==nil then return end local filled={} local unfilled={} if ABTable then unfilled=ABTable else for _name,_ in pairs(self.airbasetable)do unfilled[_name]=true end end filled[Start]=true unfilled[Start]=nil local start=self:FindNeighborNodes(Start) for _name,_ in pairs(start)do if filled[_name]~=true then self:T("Flooding ".._name) filled[_name]=true unfilled[_name]=nil self:_FloodNext(_name,filled,unfilled) end end return filled,unfilled end function STRATEGO:_FloodTest(connect,draw) local function GetElastic(bases) local vec2table={} for _name,_ in pairs(bases)do local coord=self.airbasetable[_name].coord local vec2=coord:GetVec2() table.insert(vec2table,vec2) end local zone=ZONE_ELASTIC:New("STRATEGO-Floodtest-"..math.random(1,10000),vec2table) return zone end local function DrawElastic(filled,drawit) local zone=GetElastic(filled) if drawit then zone:SetColor({1,1,1},1) zone:SetDrawCoalition(-1) zone:Update(1,true) end return zone end local _,_,weight,name=self:GetHighestWeightNodes() local filled,unfilled=self:_FloodFill(name) local allin=true if table.length(unfilled)>0 then MESSAGE:New("There is at least one node island!",15,"STRATEGO"):ToAllIf(self.debug):ToLog() allin=false if self.debug==true then local zone1=DrawElastic(filled,draw) local zone2=DrawElastic(unfilled,draw) local vertices1=zone1:GetVerticiesVec2() local vertices2=zone2:GetVerticiesVec2() local corner1=nil local corner2=nil local mindist=math.huge local found=false for _,_edge in pairs(vertices1)do for _,_edge2 in pairs(vertices2)do local dist=UTILS.VecDist2D(_edge,_edge2) if dist-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 AIRBOSS={ ClassName="AIRBOSS", Debug=false, lid=nil, theatre=nil, carrier=nil, carriertype=nil, carrierparam={}, alias=nil, airbase=nil, waypoints={}, currentwp=nil, beacon=nil, TACANon=nil, TACANchannel=nil, TACANmode=nil, TACANmorse=nil, ICLSon=nil, ICLSchannel=nil, ICLSmorse=nil, LSORadio=nil, LSOFreq=nil, LSOModu=nil, MarshalRadio=nil, MarshalFreq=nil, MarshalModu=nil, TowerFreq=nil, radiotimer=nil, zoneCCA=nil, zoneCCZ=nil, players={}, menuadded={}, BreakEntry={}, BreakEarly={}, BreakLate={}, Abeam={}, Ninety={}, Wake={}, Final={}, Groove={}, Platform={}, DirtyUp={}, Bullseye={}, defaultcase=nil, case=nil, defaultoffset=nil, holdingoffset=nil, recoverytimes={}, flights={}, Qpattern={}, Qmarshal={}, Qwaiting={}, Qspinning={}, RQMarshal={}, RQLSO={}, TQMarshal=0, TQLSO=0, Nmaxpattern=nil, Nmaxmarshal=nil, NmaxSection=nil, NmaxStack=nil, handleai=nil, xtVoiceOvers=nil, xtVoiceOversAI=nil, tanker=nil, Corientation=nil, Corientlast=nil, Cposition=nil, defaultskill=nil, adinfinitum=nil, magvar=nil, Tcollapse=nil, recoverywindow=nil, usersoundradio=nil, Tqueue=nil, dTqueue=nil, dTstatus=nil, menumarkzones=nil, menusmokezones=nil, playerscores=nil, autosave=nil, autosavefile=nil, autosavepath=nil, marshalradius=nil, airbossnice=nil, staticweather=nil, windowcount=0, LSOdT=nil, senderac=nil, radiorelayLSO=nil, radiorelayMSH=nil, turnintowind=nil, detour=nil, squadsetAI=nil, excludesetAI=nil, menusingle=nil, collisiondist=nil, holdtimestamp=nil, Tmessage=nil, soundfolder=nil, soundfolderLSO=nil, soundfolderMSH=nil, despawnshutdown=nil, dTbeacon=nil, Tbeacon=nil, LSOCall=nil, MarshalCall=nil, lowfuelAI=nil, emergency=nil, respawnAI=nil, gle={}, lue={}, trapsheet=nil, trappath=nil, trapprefix=nil, initialmaxalt=nil, welcome=nil, skipperMenu=nil, skipperSpeed=nil, skipperTime=nil, skipperOffset=nil, skipperUturn=nil, } AIRBOSS.AircraftCarrier={ AV8B="AV8BNA", HORNET="FA-18C_hornet", A4EC="A-4E-C", F14A="F-14A-135-GR", F14A_Early="F-14A-135-GR-Early", F14B="F-14B", F14A_AI="F-14A", FA18C="F/A-18C", T45C="T-45", S3B="S-3B", S3BTANKER="S-3B Tanker", E2D="E-2C", C2A="C2A_Greyhound", RHINOE="FA-18E", RHINOF="FA-18F", GROWLER="EA-18G", CORSAIR="F4U-1D", CORSAIR_CW="F4U-1D CW", } AIRBOSS.CarrierType={ ROOSEVELT="CVN_71", LINCOLN="CVN_72", WASHINGTON="CVN_73", TRUMAN="CVN_75", STENNIS="Stennis", FORRESTAL="Forrestal", ENTERPRISE66="USS Enterprise 1966", ENTERPRISEMODERN="cvn-65", VINSON="VINSON", ESSEX="Essex", BONHOMMERICHARD="USS Bon Homme Richard", ESSEXSCB125="essex_scb125", HERMES="HERMES81", INVINCIBLE="hms_invincible", TARAWA="LHA_Tarawa", AMERICA="USS America LHA-6", JCARLOS="L61", CANBERRA="L02", KUZNETSOV="KUZNECOW", } AIRBOSS.PatternStep={ UNDEFINED="Undefined", REFUELING="Refueling", SPINNING="Spinning", COMMENCING="Commencing", HOLDING="Holding", WAITING="Waiting for free Marshal stack", PLATFORM="Platform", ARCIN="Arc Turn In", ARCOUT="Arc Turn Out", DIRTYUP="Dirty Up", BULLSEYE="Bullseye", INITIAL="Initial", BREAKENTRY="Break Entry", EARLYBREAK="Early Break", LATEBREAK="Late Break", ABEAM="Abeam", NINETY="Ninety", WAKE="Wake", FINAL="Turn Final", GROOVE_XX="Groove X", GROOVE_IM="Groove In the Middle", GROOVE_IC="Groove In Close", GROOVE_AR="Groove At the Ramp", GROOVE_IW="Groove In the Wires", GROOVE_IWs="Groove In the Wires stopped?", GROOVE_AL="Groove Abeam Landing Spot", GROOVE_LC="Groove Level Cross", BOLTER="Bolter Pattern", EMERGENCY="Emergency Landing", DEBRIEF="Debrief", } AIRBOSS.GroovePos={ X0="X0", XX="XX", IM="IM", IC="IC", AR="AR", AL="AL", LC="LC", IW="IW", } AIRBOSS.Difficulty={ EASY="Flight Student", NORMAL="Naval Aviator", HARD="TOPGUN Graduate", } AIRBOSS.MenuF10={} AIRBOSS.MenuF10Root=nil AIRBOSS.version="1.4.2" function AIRBOSS:New(carriername,alias) local self=BASE:Inherit(self,FSM:New()) self:F2({carriername=carriername,alias=alias}) self.carrier=UNIT:FindByName(carriername) if self.carrier==nil then local text=string.format("ERROR: Carrier unit %s could not be found! Make sure this UNIT is defined in the mission editor and check the spelling of the unit name carefully.",carriername) MESSAGE:New(text,120):ToAll() self:E(text) return nil end self.lid=string.format("AIRBOSS %s | ",carriername) self.theatre=env.mission.theatre self:T2(self.lid..string.format("Theatre = %s.",tostring(self.theatre))) self.carriertype=self.carrier:GetTypeName() self.alias=alias or carriername self.airbase=AIRBASE:FindByName(carriername) self.beacon=BEACON:New(self.carrier) self:_GetTowerFrequency() self.playerscores={} self:_InitWaypoints() self.currentwp=1 self:_PatrolRoute() self:SetMarshalRadio() self:SetAirbossRadio() self:SetLSORadio() self:SetLSOCallInterval() self.radiotimer=SCHEDULER:New() self:SetMagneticDeclination() self:SetICLS() self:SetTACAN() self:SetBeaconRefresh() self:SetMaxLandingPattern() self:SetMaxMarshalStacks() self:SetMaxSectionSize() self:SetMaxSectionDistance() self:SetMaxFlightsPerStack() self:SetHandleAION() self:SetExtraVoiceOvers(false) self:SetExtraVoiceOversAI(false) self:SetAirbossNiceGuy() self:SetEmergencyLandings() self:SetDespawnOnEngineShutdown(false) self:SetRespawnAI(false) self:SetStaticWeather() self:SetRecoveryCase() self:SetRecoveryTurnTime() self:SetHoldingOffsetAngle() self:SetMarshalRadius() self:SetInitialMaxAlt() self:SetDefaultPlayerSkill(AIRBOSS.Difficulty.EASY) self:SetGlideslopeErrorThresholds() self:SetLineupErrorThresholds() self:SetCarrierControlledArea() self:SetCarrierControlledZone() self:SetPatrolAdInfinitum(true) self:SetCollisionDistance() self:SetQueueUpdateTime() self:SetStatusUpdateTime() self:SetDefaultMessageDuration() self:SetMenuMarkZones() self:SetMenuSmokeZones() self:SetMenuSingleCarrier(false) self:SetWelcomePlayers(true) self.landingcoord=COORDINATE:New(0,0,0) self.sterncoord=COORDINATE:New(0,0,0) self.landingspotcoord=COORDINATE:New(0,0,0) if self.carriertype==AIRBOSS.CarrierType.STENNIS then self:_InitNimitz() elseif self.carriertype==AIRBOSS.CarrierType.ROOSEVELT then self:_InitNimitz() elseif self.carriertype==AIRBOSS.CarrierType.LINCOLN then self:_InitNimitz() elseif self.carriertype==AIRBOSS.CarrierType.WASHINGTON then self:_InitNimitz() elseif self.carriertype==AIRBOSS.CarrierType.TRUMAN then self:_InitNimitz() elseif self.carriertype==AIRBOSS.CarrierType.FORRESTAL then self:_InitForrestal() elseif self.carriertype==AIRBOSS.CarrierType.ENTERPRISE66 then self:_InitEnterprise() elseif self.carriertype==AIRBOSS.CarrierType.ENTERPRISEMODERN then self:_InitEnterprise() elseif self.carriertype==AIRBOSS.CarrierType.VINSON then self:_InitStennis() elseif self.carriertype==AIRBOSS.CarrierType.ESSEX then self:_InitEssex() elseif self.carriertype==AIRBOSS.CarrierType.BONHOMMERICHARD then self:_InitBonHommeRichard() elseif self.carriertype==AIRBOSS.CarrierType.ESSEXSCB125 then self:_InitEssexSCB125() elseif self.carriertype==AIRBOSS.CarrierType.HERMES then self:_InitHermes() elseif self.carriertype==AIRBOSS.CarrierType.INVINCIBLE then self:_InitInvincible() elseif self.carriertype==AIRBOSS.CarrierType.TARAWA then self:_InitTarawa() elseif self.carriertype==AIRBOSS.CarrierType.AMERICA then self:_InitAmerica() elseif self.carriertype==AIRBOSS.CarrierType.JCARLOS then self:_InitJcarlos() elseif self.carriertype==AIRBOSS.CarrierType.CANBERRA then self:_InitCanberra() elseif self.carriertype==AIRBOSS.CarrierType.KUZNETSOV then self:_InitStennis() else self:E(self.lid..string.format("ERROR: Unknown carrier type %s!",tostring(self.carriertype))) return nil end self:_InitVoiceOvers() if false then self.Debug=true BASE:TraceOnOff(true) BASE:TraceClass(self.ClassName) BASE:TraceLevel(3) end if false then local case=3 self.holdingoffset=30 self:_GetZoneGroove():SmokeZone(SMOKECOLOR.Red,5) self:_GetZoneLineup():SmokeZone(SMOKECOLOR.Green,5) self:_GetZoneBullseye(case):SmokeZone(SMOKECOLOR.White,45) self:_GetZoneDirtyUp(case):SmokeZone(SMOKECOLOR.Orange,45) self:_GetZoneArcIn(case):SmokeZone(SMOKECOLOR.Blue,45) self:_GetZoneArcOut(case):SmokeZone(SMOKECOLOR.Blue,45) self:_GetZonePlatform(case):SmokeZone(SMOKECOLOR.Blue,45) self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green,45) self:_GetZoneHolding(case,1):SmokeZone(SMOKECOLOR.White,45) self:_GetZoneHolding(case,2):SmokeZone(SMOKECOLOR.White,45) self:_GetZoneInitial(case):SmokeZone(SMOKECOLOR.Orange,45) self:_GetZoneCommence(case,1):SmokeZone(SMOKECOLOR.Red,45) self:_GetZoneCommence(case,2):SmokeZone(SMOKECOLOR.Red,45) self:_GetZoneAbeamLandingSpot():SmokeZone(SMOKECOLOR.Red,5) self:_GetZoneLandingSpot():SmokeZone(SMOKECOLOR.Red,5) end if false then local FB=self:GetFinalBearing(false) local hdg=self:GetHeading(false) local stern=self:_GetSternCoord() local bow=stern:Translate(self.carrierparam.totlength,hdg,true) local rwy=stern:Translate(self.carrierparam.rwylength,FB,true) local function flareme() self:GetCoordinate():FlareYellow() stern:FlareYellow() bow:FlareYellow() local r1=stern:Translate(self.carrierparam.rwywidth*0.5,FB+90,true) local r2=stern:Translate(self.carrierparam.rwywidth*0.5,FB-90,true) rwy:FlareRed() local cR=stern:Translate(self.carrierparam.totwidthstarboard,hdg+90,true) local cL=stern:Translate(self.carrierparam.totwidthport,hdg-90,true) if self.carrier:GetTypeName()~=AIRBOSS.CarrierType.INVINCIBLE or self.carrier:GetTypeName()~=AIRBOSS.CarrierType.HERMES or self.carrier:GetTypeName()~=AIRBOSS.CarrierType.TARAWA or self.carrier:GetTypeName()~=AIRBOSS.CarrierType.AMERICA or self.carrier:GetTypeName()~=AIRBOSS.CarrierType.JCARLOS or self.carrier:GetTypeName()~=AIRBOSS.CarrierType.CANBERRA then local w1=stern:Translate(self.carrierparam.wire1,FB,true) local w2=stern:Translate(self.carrierparam.wire2,FB,true) local w3=stern:Translate(self.carrierparam.wire3,FB,true) local w4=stern:Translate(self.carrierparam.wire4,FB,true) w1:FlareWhite() w2:FlareYellow() w3:FlareWhite() w4:FlareYellow() else local ALSPT=self:_GetZoneAbeamLandingSpot() ALSPT:FlareZone(FLARECOLOR.Red,5,nil,UTILS.FeetToMeters(120)) local LSPT=self:_GetZoneLandingSpot() LSPT:FlareZone(FLARECOLOR.Green,5,nil,self.carrierparam.deckheight) local PLSC=self:_GetLandingSpotCoordinate() PLSC:FlareWhite() end local cbox=self:_GetZoneCarrierBox() local rbox=self:_GetZoneRunwayBox() cbox:FlareZone(FLARECOLOR.Green,5,nil,self.carrierparam.deckheight) rbox:FlareZone(FLARECOLOR.White,5,nil,self.carrierparam.deckheight) end SCHEDULER:New(nil,flareme,{},1,3,nil,180) end self:SetStartState("Stopped") self:AddTransition("Stopped","Load","Stopped") self:AddTransition("Stopped","Start","Idle") self:AddTransition("*","Idle","Idle") self:AddTransition("Idle","RecoveryStart","Recovering") self:AddTransition("Recovering","RecoveryStop","Idle") self:AddTransition("Recovering","RecoveryPause","Paused") self:AddTransition("Paused","RecoveryUnpause","Recovering") self:AddTransition("*","Status","*") self:AddTransition("*","RecoveryCase","*") self:AddTransition("*","PassingWaypoint","*") self:AddTransition("*","LSOGrade","*") self:AddTransition("*","Marshal","*") self:AddTransition("*","Save","*") self:AddTransition("*","Stop","Stopped") return self end function AIRBOSS:SetCarrierIllumination(Mode) self.carrier:SetCarrierIlluminationMode(Mode) return self end function AIRBOSS:SetWelcomePlayers(Switch) self.welcome=Switch return self end function AIRBOSS:SetCarrierControlledArea(Radius) Radius=UTILS.NMToMeters(Radius or 50) self.zoneCCA=ZONE_UNIT:New("Carrier Controlled Area",self.carrier,Radius) return self end function AIRBOSS:SetCarrierControlledZone(Radius) Radius=UTILS.NMToMeters(Radius or 5) self.zoneCCZ=ZONE_UNIT:New("Carrier Controlled Zone",self.carrier,Radius) return self end function AIRBOSS:SetCollisionDistance(Distance) self.collisiondist=UTILS.NMToMeters(Distance or 5) return self end function AIRBOSS:SetRecoveryCase(Case) self.defaultcase=Case or 1 self.case=self.defaultcase return self end function AIRBOSS:SetHoldingOffsetAngle(Offset) self.defaultoffset=Offset or 0 self.holdingoffset=self.defaultoffset return self end function AIRBOSS:SetMenuRecovery(Duration,WindOnDeck,Uturn,Offset) self.skipperMenu=true self.skipperTime=Duration or 30 self.skipperSpeed=WindOnDeck or 25 self.skipperOffset=Offset or 30 if Uturn then self.skipperUturn=true else self.skipperUturn=false end return self end function AIRBOSS:AddRecoveryWindow(starttime,stoptime,case,holdingoffset,turnintowind,speed,uturn) local Tnow=timer.getAbsTime() if starttime and type(starttime)=="number"then starttime=UTILS.SecondsToClock(Tnow+starttime) end if stoptime and type(stoptime)=="number"then stoptime=UTILS.SecondsToClock(Tnow+stoptime) end starttime=starttime or UTILS.SecondsToClock(Tnow) local Tstart=UTILS.ClockToSeconds(starttime) local Tstop=stoptime and UTILS.ClockToSeconds(stoptime)or Tstart+90*60 if Tstart>Tstop 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:SetPort(Port or MSRS.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 if(not Voice)and self.SRS and self.SRS:GetProvider()==MSRS.Provider.GOOGLE then self.SRS.voice=MSRS.Voices.Google.Standard.en_US_Standard_B if MSRS.poptions and MSRS.poptions["gcloud"]and MSRS.poptions["gcloud"].voice then self.SRS.voice=MSRS.poptions["gcloud"].voice end end 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:SetMaxSectionDistance(dmax) if dmax then if dmax<10 then dmax=10 elseif dmax>5000 then dmax=5000 end end self.maxsectiondistance=dmax or 100 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(7) 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(7) 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(7) 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=96 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:_InitEnterprise() self:_InitForrestal() self.carrierparam.sterndist=-164.30 self.carrierparam.deckheight=19.52 self.carrierparam.totlength=335 self.carrierparam.rwylength=223 self.carrierparam.wire1=57.7 self.carrierparam.wire2=69.6 self.carrierparam.wire3=79.5 self.carrierparam.wire4=90.0 end function AIRBOSS:_InitEssex() self:_InitNimitz() self.carrierparam.sterndist=-126 self.carrierparam.deckheight=19.27 self.carrierparam.totlength=268 self.carrierparam.totwidthport=23 self.carrierparam.totwidthstarboard=23 self.carrierparam.rwyangle=0.0 self.carrierparam.rwylength=265 self.carrierparam.rwywidth=20 self.carrierparam.wire1=21.9 self.carrierparam.wire2=28.3 self.carrierparam.wire3=34.7 self.carrierparam.wire4=41.1 self.carrierparam.wire5=47.4 self.carrierparam.wire6=53.7 self.carrierparam.wire7=59.0 self.carrierparam.wire8=64.1 self.carrierparam.wire9=72.7 self.carrierparam.wire10=78.0 self.carrierparam.wire11=85.5 self.carrierparam.wire12=105.9 self.carrierparam.wire13=113.3 self.carrierparam.wire14=121.0 self.carrierparam.wire15=128.5 self.carrierparam.landingdist=self.carrierparam.sterndist+self.carrierparam.wire3 end function AIRBOSS:_InitBonHommeRichard() self:_InitEssex() self.carrierparam.deckheight=16.95 self.carrierparam.rwyangle=-11.4 self.carrierparam.rwylength=97 self.carrierparam.rwywidth=20 self.carrierparam.wire1=40.4 self.carrierparam.wire2=45 self.carrierparam.wire3=51 self.carrierparam.wire4=58.1 end function AIRBOSS:_InitEssexSCB125() self:_InitBonHommeRichard() 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.POWERsoft.duration=0.9 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.POWERsoft.duration=0.9 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}, POWERsoft={file="LSO-Power-soft",suffix="ogg",loud=false,subtitle="Power-soft",duration=0.90,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 or playerData.actype==AIRBOSS.AircraftCarrier.F14A_Early local corsair=playerData.actype==AIRBOSS.AircraftCarrier.CORSAIR or playerData.actype==AIRBOSS.AircraftCarrier.CORSAIR_CW 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.5) aoa.Slow=self:_AoAUnit2Deg(playerData,16.5) aoa.OnSpeedMax=self:_AoAUnit2Deg(playerData,16.0) aoa.OnSpeed=self:_AoAUnit2Deg(playerData,15.0) aoa.OnSpeedMin=self:_AoAUnit2Deg(playerData,14.0) aoa.Fast=self:_AoAUnit2Deg(playerData,13.5) aoa.FAST=self:_AoAUnit2Deg(playerData,12.5) elseif goshawk then aoa.SLOW=9.5 aoa.Slow=9.25 aoa.OnSpeedMax=9.0 aoa.OnSpeed=8.5 aoa.OnSpeedMin=8.25 aoa.Fast=7.75 aoa.FAST=5.5 elseif skyhawk then aoa.SLOW=10.50 aoa.Slow=9.50 aoa.OnSpeedMax=9.25 aoa.OnSpeed=8.75 aoa.OnSpeedMin=8.25 aoa.Fast=8.00 aoa.FAST=7.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 elseif corsair 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 or playerData.actype==AIRBOSS.AircraftCarrier.F14A_Early 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 or playerData.actype==AIRBOSS.AircraftCarrier.F14A_Early 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 corsair=playerData.actype==AIRBOSS.AircraftCarrier.CORSAIR or playerData.actype==AIRBOSS.AircraftCarrier.CORSAIR_CW 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) elseif corsair then alt=UTILS.FeetToMeters(300) speed=UTILS.KnotsToMps(120) 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) elseif corsair then alt=UTILS.FeetToMeters(200) speed=UTILS.KnotsToMps(110) 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) elseif corsair then alt=UTILS.FeetToMeters(200) speed=UTILS.KnotsToMps(100) 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) elseif corsair then alt=UTILS.FeetToMeters(150) speed=UTILS.KnotsToMps(100) 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) elseif corsair then alt=UTILS.FeetToMeters(150) speed=UTILS.KnotsToMps(90) 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) elseif corsair then alt=UTILS.FeetToMeters(90) speed=UTILS.KnotsToMps(90) 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) elseif corsair then alt=UTILS.FeetToMeters(80) 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) elseif corsair then alt=UTILS.FeetToMeters(80) 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) elseif flight.actype==AIRBOSS.AircraftCarrier.CORSAIR or flight.actype==AIRBOSS.AircraftCarrier.CORSAIR_CW then Speed=UTILS.KnotsToKmph(100) 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) brc=brc%360 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=0 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 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 tomcat=playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B if playerData.step==AIRBOSS.PatternStep.WAKE and hornet then if math.abs(playerData.unit:GetRoll())>35 and math.abs(playerData.unit:GetRoll())<=40 then playerData.wrappedUpAtWakeLittle=true elseif math.abs(playerData.unit:GetRoll())>40 and math.abs(playerData.unit:GetRoll())<=45 then playerData.wrappedUpAtWakeFull=true elseif math.abs(playerData.unit:GetRoll())>45 then playerData.wrappedUpAtWakeUnderline=true elseif math.abs(playerData.unit:GetRoll())<20 and math.abs(playerData.unit:GetRoll())>=10 then playerData.AAatWakeLittle=true elseif math.abs(playerData.unit:GetRoll())<10 and math.abs(playerData.unit:GetRoll())>=2 then playerData.AAatWakeFull=true elseif math.abs(playerData.unit:GetRoll())<2 then playerData.AAatWakeUnderline=true else end if math.abs(playerData.unit:GetAoA())>=15 then playerData.AFU=true elseif math.abs(playerData.unit:GetAoA())<=5 then playerData.AFU=true else end end if playerData.step==AIRBOSS.PatternStep.WAKE and tomcat then if math.abs(playerData.unit:GetRoll())>35 and math.abs(playerData.unit:GetRoll())<=40 then playerData.wrappedUpAtWakeLittle=true elseif math.abs(playerData.unit:GetRoll())>40 and math.abs(playerData.unit:GetRoll())<=45 then playerData.wrappedUpAtWakeFull=true elseif math.abs(playerData.unit:GetRoll())>45 then playerData.wrappedUpAtWakeUnderline=true elseif math.abs(playerData.unit:GetRoll())<12 and math.abs(playerData.unit:GetRoll())>=5 then playerData.AAatWakeLittle=true elseif math.abs(playerData.unit:GetRoll())<5 and math.abs(playerData.unit:GetRoll())>=2 then playerData.AAatWakeFull=true elseif math.abs(playerData.unit:GetRoll())<2 then playerData.AAatWakeUnderline=true else end if math.abs(playerData.unit:GetAoA())>=15 then playerData.AFU=true elseif math.abs(playerData.unit:GetAoA())<=5 then playerData.AFU=true else end end 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-1.5 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:T(self.lid.."ERROR: EventData=nil in event REMOVEUNIT!") self:T(EventData) return end if EventData.IniUnit==nil then self:T(self.lid.."ERROR: EventData.IniUnit=nil in event REMOVEUNIT!") self:T(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 playerData.Tgroove=timer.getTime()-playerData.TIG0-1.5 playerData.wrappedUpAtWakeLittle=false playerData.wrappedUpAtWakeFull=false playerData.wrappedUpAtWakeUnderline=false playerData.wrappedUpAtStartLittle=false playerData.wrappedUpAtStartFull=false playerData.wrappedUpAtStartUnderline=false playerData.AAatWakeLittle=false playerData.AAatWakeFull=false playerData.AAatWakeUnderline=false playerData.AFU=false 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) elseif self.carriertype==AIRBOSS.CarrierType.ESSEX then self.sterncoord:Translate(self.carrierparam.sterndist,hdg,true,true):Translate(-1,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.." " elseif wire==2 then text=text.." " elseif wire==4 then text=text.." " elseif wire==1 then text=text.." " 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 local lueWire=self:_LineupWIRE(playerData.unit,true) text=text..string.format("\nLineUpForWireCalls=%.2f° | lineup for Groove calls=%.2f°",lueWire or 0,lue or 0) local unitClient=Unit.getByName(unit:GetName()) local hornet=playerData.actype==AIRBOSS.AircraftCarrier.HORNET local tomcat=playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B or playerData.actype==AIRBOSS.AircraftCarrier.F14A_Early if hornet then local nozzlePosL=0 local burnerPosL=unitClient:getDrawArgumentValue(28) if burnerPosL<0.2 then nozzlePosL=unitClient:getDrawArgumentValue(89) else nozzlePosL=0 end local nozzlePosR=0 local burnerPosR=unitClient:getDrawArgumentValue(29) if burnerPosR<0.2 then nozzlePosR=unitClient:getDrawArgumentValue(90) else nozzlePosR=0 end text=text..string.format("\n Left Nozzle position=%.2f | Right Nozzle position=%.2f ",nozzlePosL,nozzlePosR) end if tomcat then local nozzlePosL=unitClient:getDrawArgumentValue(434) local nozzlePosR=unitClient:getDrawArgumentValue(433) text=text..string.format("\n Left Nozzle position=%.2f | Right Nozzle position=%.2f ",nozzlePosL,nozzlePosR) 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:_LineupWIRE(unit,runway) local landingcoord=self:_GetOptLandingCoordinateWIRE() 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:_NozzleArgumentLeft(unit) local unitClient=Unit.getByName(unit:GetName()) local typeName=unit:GetTypeName() local nozzlePosL=0 local burnerPosL=0 if typeName=="FA-18C_hornet"then burnerPosL=unitClient:getDrawArgumentValue(28) if burnerPosL<0.2 then nozzlePosL=unitClient:getDrawArgumentValue(89) else nozzlePosL=0 end elseif typeName=="F-14A-135-GR"or typeName=="F-14B"or typeName=="F-14A-135-GR-Early"then nozzlePosL=unitClient:getDrawArgumentValue(434) end return nozzlePosL end function AIRBOSS:_NozzleArgumentRight(unit) local unitClient=Unit.getByName(unit:GetName()) local typeName=unit:GetTypeName() local nozzlePosR=0 local burnerPosR=0 if typeName=="FA-18C_hornet"then burnerPosR=unitClient:getDrawArgumentValue(29) if burnerPosR<0.2 then nozzlePosR=unitClient:getDrawArgumentValue(90) else nozzlePosR=0 end elseif typeName=="F-14A-135-GR"or typeName=="F-14B"or typeName=="F-14A-135-GR-Early"then nozzlePosR=unitClient:getDrawArgumentValue(433) end return nozzlePosR 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:_GetOptLandingCoordinateWIRE() self.landingcoord:UpdateFromCoordinate(self:_GetSternCoord()) local FB=self:GetFinalBearing(false) local case=self.case if self.carrierparam.wire3 then self.landingcoord:Translate(self.carrierparam.wire3+500,FB,true,true) end self.landingcoord.y=self.landingcoord.y+2 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 hdg=hdg%360 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=25 then grade="_LIG_" elseif playerData.actype==AIRBOSS.AircraftCarrier.AV8B and t<55 then grade="FAST V/STOL Groove" elseif playerData.actype==AIRBOSS.AircraftCarrier.AV8B and t<75 then grade="OK V/STOL Groove" elseif playerData.actype==AIRBOSS.AircraftCarrier.AV8B and t>=76 then grade="SLOW V/STOL Groove" else grade="LIG" end if t>=16.49 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 TIG="" if playerData.Tgroove and playerData.Tgroove<=360 and playerData.case<3 then TIG=self:_EvalGrooveTime(playerData)or"N/A" 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 GIW,nIW=self:_Flightdata2Text(playerData,AIRBOSS.GroovePos.IW) local vtol=playerData.actype==AIRBOSS.AircraftCarrier.AV8B local G=GXX.." "..GIM.." ".." "..GIC.." "..GAR.." "..GIW.." "..TIG local gradeWithDeviations=GXX.."["..nXX.."] "..GIM.."["..nIM.."] "..GIC.."["..nIC.."] "..GAR.."["..nAR.."] "..GIW.."["..nIW.."]" env.info("LSO Grade [with deviation count]: "..gradeWithDeviations) local N=nXX+nIM+nIC+nAR+nIW local nL=count(G,'_')/2 local nS=count(G,'%(') local nN=N-nS-nL if TIG=="_OK_"then nL=nL-1 end local Tgroove=playerData.Tgroove local TgrooveUnicorn=Tgroove and(Tgroove>=16.49 and Tgroove<=16.59)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 TgrooveVstolUnicorn then grade="_OK_" points=5.0 G="Unicorn" end if N==0 and TgrooveUnicorn then if playerData.wire==3 then grade="_OK_" points=5.0 G="Unicorn" else grade="OK" points=4.0 end 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) env.info(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 if playerData.wire==1 and points>=3 and N>4 then points=points-1 if points==4 then grade="OK" elseif points==3 then grade="(OK)" elseif points==2 then grade="--" end end env.info("Returning: "..grade.." "..points.." "..G) 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 LUEwire=fdata.LUEwire local Lnoz=fdata.LeftNozzle local Rnoz=fdata.RightNozzle local ROL=fdata.Roll local GT=fdata.GT local acaoa=self:_GetAircraftAoA(playerData) local P=nil if step==AIRBOSS.PatternStep.GROOVE_XX and ROL<=3.5 and playerData.case<3 then if LUE>3.2 then P=underline("LUL") elseif LUE>2.2 then P="LUL" elseif LUE>1.2 then P=little("LUL") end end local O=nil if step==AIRBOSS.PatternStep.GROOVE_XX and playerData.case<3 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 and step~=AIRBOSS.PatternStep.GROOVE_XX and step~=AIRBOSS.PatternStep.GROOVE_IW then D=underline("LUL") elseif LUE>self.lue.Right and step~=AIRBOSS.PatternStep.GROOVE_XX and step~=AIRBOSS.PatternStep.GROOVE_IW then D="LUL" elseif LUE>self.lue._max and step~=AIRBOSS.PatternStep.GROOVE_XX and step~=AIRBOSS.PatternStep.GROOVE_IW then D=little("LUL") elseif LUE1.2 then DW=underline("LL") elseif LUEwire>0.4 then DW="LL" elseif LUEwire>0.25 then DW=little("LL") elseif LUEwire<-1.17 then DW=underline("LR") elseif LUEwire<-0.46 then DW="LR" elseif LUEwire<-0.25 then DW=little("LR") else end if ROL>5 and ROL<=10 then Rol=little("LRWD") elseif ROL>10 and ROL<=15 then Rol=("LRWD") elseif ROL>15 then Rol=underline("LRWD") elseif ROL<-5 and ROL>=-10 then Rol=little("LLWD") elseif ROL<-10 and ROL>=-15 then Rol=("LLWD") elseif ROL<-15 then Rol=underline("LLWD") else end local hornet=playerData.actype==AIRBOSS.AircraftCarrier.HORNET local tomcat=playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B if hornet then if Lnoz>0.6 and Rnoz>0.6 then Noz=underline("EG") else end end if playerData.Tgroove and playerData.Tgroove<=360 and playerData.case<3 then local grooveTime=playerData.Tgroove if grooveTime>19 or grooveTime<15 then GT="" end end end local G="" local n=0 if stepMod=="XX"then if playerData.case<3 then if fdata.WrappedUp then env.info("Adding WrappedUp deviation.") G=G..fdata.WrappedUp n=n+1 end if fdata.AngledApch then env.info("Adding AngledApch deviation.") G=G..fdata.AngledApch n=n+1 end if fdata.AFU then env.info("Adding AFU deviation.") G=G..fdata.AFU n=n+1 end end end if fdata.FlyThrough then G=G..fdata.FlyThrough end if S then env.info("Adding speed deviation.") G=G..S n=n+1 end if A then env.info("Adding altitude deviation.") G=G..A n=n+1 end if D then env.info("Adding line up deviation.") G=G..D n=n+1 end if fdata.Drift then env.info("Adding drift deviation.") G=G..fdata.Drift n=n end if O then env.info("Adding overshoot deviation.") G=G..O n=n+1 end if DW then env.info("Adding landed L/R deviation.") G=G..DW n=n+1 end if Rol then env.info("Adding landed rol deviation.") G=G..Rol n=n+1 end if Noz then env.info("Adding eased guns deviation.") G=G..Noz n=n+1 end if GT then G=G..GT n=n+1 end local step=self:_GS(step) step=step:gsub("XX","X") if G~=""then G=G..step end local text=string.format("LSO Grade at %s:\n",step) text=text..string.format("AOA=%.1f\n",AOA) text=text..string.format("GSE=%.1f\n",GSE) text=text..string.format("LUE=%.1f\n",LUE) text=text..string.format("ROL=%.1f\n",ROL) text=text..G self:T3(self.lid..text) return G,n end function AIRBOSS:_GS(step,n) local gp n=n or 0 if step==AIRBOSS.PatternStep.FINAL then gp=AIRBOSS.GroovePos.X0 if n==-1 then gp=AIRBOSS.GroovePos.X0 elseif n==1 then gp=AIRBOSS.GroovePos.XX end elseif step==AIRBOSS.PatternStep.GROOVE_XX then gp=AIRBOSS.GroovePos.XX if n==-1 then gp=AIRBOSS.GroovePos.X0 elseif n==1 then gp=AIRBOSS.GroovePos.IM end elseif step==AIRBOSS.PatternStep.GROOVE_IM then gp=AIRBOSS.GroovePos.IM if n==-1 then gp=AIRBOSS.GroovePos.XX elseif n==1 then gp=AIRBOSS.GroovePos.IC end elseif step==AIRBOSS.PatternStep.GROOVE_IC then gp=AIRBOSS.GroovePos.IC if n==-1 then gp=AIRBOSS.GroovePos.IM elseif n==1 then gp=AIRBOSS.GroovePos.AR end elseif step==AIRBOSS.PatternStep.GROOVE_AR then gp=AIRBOSS.GroovePos.AR if n==-1 then gp=AIRBOSS.GroovePos.IC elseif n==1 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 gp=AIRBOSS.GroovePos.AL else gp=AIRBOSS.GroovePos.IW end end elseif step==AIRBOSS.PatternStep.GROOVE_AL then gp=AIRBOSS.GroovePos.AL if n==-1 then gp=AIRBOSS.GroovePos.AR elseif n==1 then gp=AIRBOSS.GroovePos.LC end elseif step==AIRBOSS.PatternStep.GROOVE_LC then gp=AIRBOSS.GroovePos.LC if n==-1 then gp=AIRBOSS.GroovePos.AL elseif n==1 then gp=AIRBOSS.GroovePos.LC end elseif step==AIRBOSS.PatternStep.GROOVE_IW then gp=AIRBOSS.GroovePos.IW if n==-1 then gp=AIRBOSS.GroovePos.AR elseif n==1 then gp=AIRBOSS.GroovePos.IW end end return gp end function AIRBOSS:_CheckAbort(X,Z,pos) local abort=false if pos.Xmin and Xpos.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.",playerData.Tgroove) 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 or actype==AIRBOSS.AircraftCarrier.F14A_Early 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!" playerData.wrappedUpAtWakeLittle=false playerData.wrappedUpAtWakeFull=false playerData.wrappedUpAtWakeUnderline=false playerData.wrappedUpAtStartLittle=false playerData.wrappedUpAtStartFull=false playerData.wrappedUpAtStartUnderline=false playerData.AAatWakeLittle=false playerData.AAatWakeFull=false playerData.AAatWakeUnderline=false 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=self.maxsectiondistance 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.11" 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) if self.callsignname and self.callsignnumber then local grp=GROUP:FindByName(self.tankergroupname) if grp then local typename=grp:GetTypeName()or"" local Name local enumerator=CALLSIGN.Tanker if typename=="A6E"then enumerator=CALLSIGN.Intruder end Name=self:_GetCallsignName(self.callsignname,enumerator) Spawn:InitCallSign(self.callsignname,Name,self.callsignnumber,self.callsignnumber) end end 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:_GetCallsignName(Callsign,Enumerator) for name,value in pairs(Enumerator or{})do if value==Callsign then return name end end return"" 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.2.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") self:I(self.lid.."Started.") 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 UsesAliveGroup=false local Spawn=GROUP:FindByName(self.helogroupname) if Spawn and Spawn:IsAlive()then self.helo=Spawn UsesAliveGroup=true delay=1 else Spawn=SPAWN:NewWithAlias(self.helogroupname,self.alias) Spawn:InitModex(self.modex) end if UsesAliveGroup==false and 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 elseif UsesAliveGroup==false and self.uncontrolledac then 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 end elseif UsesAliveGroup==false then 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 self.followset=SET_GROUP:New() self.followset:AddGroup(self.helo) self.HeloFuel0=self.helo:GetFuel() self.formation=FORMATION:New(self.carrier,self.followset,"Helo Formation with Carrier") self.formation:FormationCenterWing(-self.offsetX,50,math.abs(self.altitude),50,self.offsetZ,50) self.formation:SetFollowTimeInterval(self.dtFollow) 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 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 diff=11 and n<=14 then return"ов"end if d==1 then return""end if d>=2 and d<=4 then return"а"end return"ов" end if locale=="tr"then return""end if locale=="de"and Kind=="crate"then return count>1 and"n"or""end return count>1 and"s"or"" end function CTLD:SetSRS(Frequency,Modulation,PathToSRS,Gender,Culture,Port,Voice,Volume,PathToGoogleKey,AccessKey,Backend,Provider,Speaker) self:T(self.lid.."SetSRS") self.PathToSRS=PathToSRS or MSRS.path or"C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio" self.Gender=Gender or MSRS.gender or"male" self.Culture=Culture or MSRS.culture or"en-US" self.Port=Port or MSRS.port or 5002 self.Voice=Voice or MSRS.voice self.PathToGoogleKey=PathToGoogleKey self.AccessKey=AccessKey self.Volume=Volume or 1.0 self.usesrs=true self.Frequency=Frequency or{30,124.5} self.BCFrequency=self.Frequency self.Modulation=Modulation or{radio.modulation.FM,radio.modulation.AM} self.BCModulation=self.Modulation self.SRS=MSRS:New(self.PathToSRS,self.Frequency,self.Modulation,Backend) self.SRS:SetCoalition(self.Coalition) self.Label=self.MenuName or self.Name self.SRS:SetLabel(self.Label) self.SRS:SetGender(self.Gender) self.SRS:SetCulture(self.Culture) self.SRS:SetPort(self.Port) self.SRS:SetVolume(self.Volume) self.SRS.Label="CTLD" if Provider then self.SRS:SetProvider(Provider) end if self.PathToGoogleKey then self.SRS:SetProviderOptionsGoogle(self.PathToGoogleKey,self.AccessKey) self.SRS:SetProvider(Provider or MSRS.Provider.GOOGLE) end if(not PathToGoogleKey)and self.SRS:GetProvider()==MSRS.Provider.GOOGLE then self.PathToGoogleKey=MSRS.poptions.gcloud.credentials self.Voice=Voice or MSRS.poptions.gcloud.voice self.AccessKey=AccessKey or MSRS.poptions.gcloud.key end if Backend then self.SRS:SetBackend(Backend) end self.SRS:SetVoice(self.Voice) if Speaker then self.SRS:SetSpeakerPiper(Speaker) end self.SRSQueue=MSRSQUEUE:New(self.Label) self.SRSQueue:SetTransmitOnlyWithPlayers(true) self.SRSQueue.Label="CTLD" 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:_GetCargoDisplayName(Cargo) if type(Cargo)=="table"then if Cargo.GetDisplayName then local dname=Cargo:GetDisplayName() if type(dname)=="string"and dname~=""then return dname end end if Cargo.GetName then local name=Cargo:GetName() if type(name)=="string"and name~=""then return name end end if type(Cargo.Name)=="string"and Cargo.Name~=""then return Cargo.Name end return"Unknown" end if type(Cargo)=="string"and Cargo~=""then return Cargo end return"Unknown" end function CTLD:SetCargoDisplayFormatter(Formatter) self.CargoDisplayFormatter=Formatter return self end function CTLD:_FormatCargoDisplayText(BaseText,Cargo,Group) local label=BaseText or self:_GetCargoDisplayName(Cargo) if self.CargoDisplayFormatter then local formatted=self:CargoDisplayFormatter(label,Cargo,Group) if type(formatted)=="string"and formatted~=""then return formatted end end return label end function CTLD:_GetCargoDisplayNameForGroup(Cargo,Group) return self:_FormatCargoDisplayText(self:_GetCargoDisplayName(Cargo),Cargo,Group) end function CTLD:AllowCATransport(OnOff,ClientSet) self.allowCATransport=OnOff self.CATransportSet=ClientSet return self 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:_C130DcAutoIsBuildableCargo(Cargo) if not Cargo then return false end local ctype=Cargo:GetType() return ctype==CTLD_CARGO.Enum.VEHICLE or ctype==CTLD_CARGO.Enum.FOB end function CTLD:_C130DcAutoEnsureState() self._c130DcAutoSets=self._c130DcAutoSets or{} self._c130DcAutoMap=self._c130DcAutoMap or{} self._c130DcAutoBatches=self._c130DcAutoBatches or{} self._c130DcAutoSeq=self._c130DcAutoSeq or 0 return self end function CTLD:_C130DcAutoFilterCrates(Crates,SetIdOrScope) if not SetIdOrScope then local t=Crates or{} local n=0 for _,_ in pairs(t)do n=n+1 end return t,n end local scopeIds={} if type(SetIdOrScope)=="table"then for k,v in pairs(SetIdOrScope)do if type(k)=="number"and type(v)=="string"then scopeIds[v]=true elseif type(k)=="string"and v then scopeIds[k]=true end end elseif type(SetIdOrScope)=="string"then scopeIds[SetIdOrScope]=true end if not next(scopeIds)then return{},0 end local allowedIds={} local allowedNames={} for setId,_ in pairs(scopeIds)do local setData=self._c130DcAutoSets and self._c130DcAutoSets[setId]or nil if setData then for _,entry in ipairs(setData.entries or{})do if entry.cargoId then allowedIds[entry.cargoId]=true end if entry.cargoObject and entry.cargoObject.GetID then local id=entry.cargoObject:GetID() if id then allowedIds[id]=true end end if entry.proxyCargo and entry.proxyCargo.GetID then local id=entry.proxyCargo:GetID() if id then allowedIds[id]=true end end if entry.spawnName then allowedNames[entry.spawnName]=true end if entry.dynamicName then allowedNames[entry.dynamicName]=true end end end end local filtered={} for _,_crate in pairs(Crates or{})do local crate=_crate local include=false if crate then local cid=crate.GetID and crate:GetID()or nil if cid and allowedIds[cid]then include=true else local pos=crate.GetPositionable and crate:GetPositionable()or nil local pname=pos and pos.GetName and pos:GetName()or nil if pname and allowedNames[pname]then include=true end end end if include then filtered[#filtered+1]=crate end end return filtered,#filtered end function CTLD:_C130DcAutoRegisterDynamicCargo(Positionable) if not Positionable or not _DATABASE then return nil end local pname=Positionable.GetName and Positionable:GetName()or nil if not pname or pname==""then return nil end local dcargo=_DATABASE:FindDynamicCargo(pname) if not dcargo then dcargo=_DATABASE:AddDynamicCargo(pname) if dcargo then self:T(self.lid.." C130DcAuto RegisterDynamicCargo "..pname) _DATABASE:CreateEventNewDynamicCargo(dcargo) end end return dcargo end function CTLD:_C130DcAutoGetCarrierUnitName(DynamicCargo) if not DynamicCargo then return nil end if DynamicCargo.GetCarrierUnitName then local uname=DynamicCargo:GetCarrierUnitName() if uname and uname~=""then return uname end end local owner=DynamicCargo.Owner if owner and owner~=""and owner~="None"then local byPlayer=CLIENT:FindByPlayerName(owner) if byPlayer and byPlayer:IsAlive()then return byPlayer:GetName() end end return nil end function CTLD:_C130DcAutoGetCarrierGroupName(DynamicCargo) if not DynamicCargo then return nil end if DynamicCargo.GetCarrierGroupName then local gname=DynamicCargo:GetCarrierGroupName() if gname and gname~=""then return gname end end local uname=self:_C130DcAutoGetCarrierUnitName(DynamicCargo) if uname then local unit=UNIT:FindByName(uname) if unit and unit:IsAlive()then local grp=unit:GetGroup() if grp then return grp:GetName() end end end return nil end function CTLD:_C130DcAutoIsC130Event(DynamicCargo) if not DynamicCargo then return false end if DynamicCargo.GetCarrierTypeName then local tname=DynamicCargo:GetCarrierTypeName() if tname and tname~=""then return tname=="C-130J-30" end end local uname=self:_C130DcAutoGetCarrierUnitName(DynamicCargo) if uname then local unit=UNIT:FindByName(uname) if unit then local utype=unit:GetTypeName()or"none" if self.C130JTypes and self.C130JTypes[utype]then return true end return utype=="C-130J-30" end end return false end function CTLD:_C130DcAutoRegisterSet(Group,Unit,Cargo,PickupZone) if not Group or not Unit or not Cargo then return nil end if not self.UseC130LoadAndUnload or not self.UseC130DynamicCargoAutoBuild then return nil end if not self:IsC130J(Unit)then return nil end if not self:_C130DcAutoIsBuildableCargo(Cargo)then return nil end self:_C130DcAutoEnsureState() self._c130DcAutoSeq=self._c130DcAutoSeq+1 local seq=self._c130DcAutoSeq local setId=string.format("%s|%s|%d",Unit:GetName()or"none",Cargo:GetName()or"cargo",seq) local cc,ct,cs=Cargo:GetStaticTypeAndShape() local recipe={ cargoName=Cargo:GetName(), cargoDisplayName=Cargo:GetDisplayName(), templates=UTILS.DeepCopy(Cargo:GetTemplates()), cargoType=Cargo:GetType(), cratesNeeded=Cargo:GetCratesNeeded(), perCrateMass=Cargo:GetMass(), subcategory=Cargo.Subcategory, staticCategory=cc, staticType=ct, staticShape=cs, resourceMap=UTILS.DeepCopy(Cargo:GetStaticResourceMap()), typeNames=UTILS.DeepCopy(Cargo.TypeNames), } local now=timer.getTime() local setData={ id=setId, created=now, ttl=now+3600, groupName=Group:GetName(), unitName=Unit:GetName(), pickupZoneName=(PickupZone and PickupZone.GetName and PickupZone:GetName())or(type(PickupZone)=="string"and PickupZone or nil), recipe=recipe, entries={}, completed=false, failed=false, buildStarted=false, handoffClaimed=false, helperGroupName=nil, helperUnitName=nil, cleanupAt=nil, } self._c130DcAutoSets[setId]=setData self:T(self.lid.." C130DcAuto RegisterSet "..setId) return setId end function CTLD:_C130DcAutoRegisterEntry(SetId,Cargo) if not SetId or not Cargo then return false end self:_C130DcAutoEnsureState() local setData=self._c130DcAutoSets[SetId] if not setData then return false end local pos=Cargo:GetPositionable() local pname=pos and pos.GetName and pos:GetName()or nil local pcoord=pos and pos.GetCoordinate and pos:GetCoordinate()or nil local entryId=string.format("%s#%d",SetId,#setData.entries+1) local entry={ id=entryId, state="pending", cargoId=Cargo:GetID(), cargoObject=Cargo, cargoName=Cargo:GetName(), spawnName=pname, dynamicName=nil, spawnVec2=pcoord and pcoord:GetVec2()or nil, spawnVec3=pcoord and pcoord:GetVec3()or nil, landedVec2=nil, landedVec3=nil, proxyCargo=nil, proxyAdded=false, } setData.entries[#setData.entries+1]=entry if pname then self._c130DcAutoMap[pname]={setId=SetId,entryId=entryId} end return true end function CTLD:_C130DcAutoGetMappedEntry(DynamicCargoName) if not DynamicCargoName or not self._c130DcAutoMap then return nil,nil end local link=self._c130DcAutoMap[DynamicCargoName] if not link then return nil,nil end local setData=self._c130DcAutoSets and self._c130DcAutoSets[link.setId]or nil if not setData then return nil,nil end for _,entry in ipairs(setData.entries or{})do if entry.id==link.entryId then return setData,entry end end return nil,nil end function CTLD:_C130DcAutoResolveEntry(DynamicCargo,PreferLoaded) local cargoCoord=DynamicCargo and DynamicCargo.GetLastPosition and DynamicCargo:GetLastPosition()or nil local unitName=self:_C130DcAutoGetCarrierUnitName(DynamicCargo) local groupName=self:_C130DcAutoGetCarrierGroupName(DynamicCargo) local bestSet=nil local bestEntry=nil local bestDist=math.huge for _,setData in pairs(self._c130DcAutoSets or{})do if not setData.completed and not setData.failed then local ownerMatch=false if unitName and setData.unitName and setData.unitName==unitName then ownerMatch=true elseif groupName and setData.groupName and setData.groupName==groupName then ownerMatch=true end if ownerMatch then for _,entry in ipairs(setData.entries or{})do local stateOk=false if PreferLoaded then stateOk=entry.state=="loaded" else stateOk=entry.state=="pending"or entry.state=="loaded" end if stateOk then local dist=0 if cargoCoord and entry.spawnVec2 then local dx=(cargoCoord.x or 0)-(entry.spawnVec2.x or 0) local dz=(cargoCoord.z or 0)-(entry.spawnVec2.y or 0) dist=math.sqrt(dx*dx+dz*dz) elseif cargoCoord and entry.landedVec2 then local dx=(cargoCoord.x or 0)-(entry.landedVec2.x or 0) local dz=(cargoCoord.z or 0)-(entry.landedVec2.y or 0) dist=math.sqrt(dx*dx+dz*dz) else dist=999999 end if dist0 and landed==total and setData.handoffClaimed then setIds[#setIds+1]=setId else setData.handoffClaimed=false end end end self._c130DcAutoBatches[OwnerKey]=nil for _,setId in ipairs(failedSetIds)do self:_C130DcAutoCleanupSet(setId,"failed") end if#setIds<1 then return self end table.sort(setIds) local ok=self:_C130DcAutoSpawnBuildHelperForSets(OwnerKey,setIds) if not ok then for _,setId in ipairs(setIds)do local setData=self._c130DcAutoSets and self._c130DcAutoSets[setId]or nil if setData and not setData.failed then setData.handoffClaimed=false end end end return self end function CTLD:_C130DcAutoQueueReadySet(SetId) local setData=self._c130DcAutoSets and self._c130DcAutoSets[SetId]or nil if not setData or setData.failed then return false end if setData.completed or setData.buildStarted or setData.handoffClaimed then return true end local ownerKey=self:_C130DcAutoGetOwnerKey(setData)or SetId local window=self.C130DynamicCargoAutoBuildMergeSeconds or 0 if window<0 then window=0 end setData.handoffClaimed=true setData.readyAt=timer.getTime() local batch=self._c130DcAutoBatches[ownerKey] if not batch then batch={ ownerKey=ownerKey, setIds={}, created=timer.getTime(), dueAt=timer.getTime()+window, timer=nil } self._c130DcAutoBatches[ownerKey]=batch end batch.setIds[SetId]=true if window<=0 then self:_C130DcAutoFlushOwnerBatch(ownerKey) return true end if not batch.timer or(batch.timer.IsRunning and not batch.timer:IsRunning())then batch.timer=TIMER:New(CTLD._C130DcAutoFlushOwnerBatch,self,ownerKey) batch.timer:Start(window) self:T(self.lid.." C130DcAuto queue set "..SetId.." owner="..tostring(ownerKey).." merge="..tostring(window)) else self:T(self.lid.." C130DcAuto merge set "..SetId.." owner="..tostring(ownerKey)) end return true end function CTLD:_C130DcAutoCleanupSet(SetId,Result) self:_C130DcAutoEnsureState() local setData=self._c130DcAutoSets[SetId] if not setData then return self end if setData.helperGroupName then local helper=GROUP:FindByName(setData.helperGroupName) if helper and helper:IsAlive()then helper:Destroy(false) end end for _,entry in ipairs(setData.entries or{})do if entry.spawnName then self._c130DcAutoMap[entry.spawnName]=nil end if entry.dynamicName then self._c130DcAutoMap[entry.dynamicName]=nil end end local batchRemove={} for ownerKey,batch in pairs(self._c130DcAutoBatches or{})do if batch.setIds and batch.setIds[SetId]then batch.setIds[SetId]=nil if not next(batch.setIds)then if batch.timer and batch.timer.IsRunning and batch.timer:IsRunning()then batch.timer:Stop() end batchRemove[#batchRemove+1]=ownerKey end end end for _,ownerKey in ipairs(batchRemove)do self._c130DcAutoBatches[ownerKey]=nil end self._c130DcAutoSets[SetId]=nil self:T(self.lid.." C130DcAuto CleanupSet "..SetId.." result="..tostring(Result)) return self end function CTLD:_C130DcAutoTryCompleteSet(SetId) local setData=self._c130DcAutoSets and self._c130DcAutoSets[SetId]or nil if not setData or setData.failed then return false end if setData.completed or setData.buildStarted or setData.handoffClaimed then return true end local total=0 local landed=0 for _,entry in ipairs(setData.entries or{})do total=total+1 if entry.state=="failed"then setData.failed=true self:_C130DcAutoCleanupSet(SetId,"failed") return false end if entry.state=="landed"then landed=landed+1 end end if total>0 and landed==total then return self:_C130DcAutoQueueReadySet(SetId) end return false end function CTLD:_C130DcAutoOnDynamicLoaded(EventData) self:_C130DcAutoEnsureState() local dcargo=EventData.IniDynamicCargo if not dcargo then return false end local setData,entry=self:_C130DcAutoGetMappedEntry(EventData.IniDynamicCargoName) if not setData or not entry then setData,entry=self:_C130DcAutoResolveEntry(dcargo,false) end if not setData or not entry then return false end if not self:_C130DcAutoIsC130Event(dcargo)then return false end entry.state="loaded" entry.dynamicName=EventData.IniDynamicCargoName self._c130DcAutoMap[EventData.IniDynamicCargoName]={setId=setData.id,entryId=entry.id} local unitName=self:_C130DcAutoGetCarrierUnitName(dcargo) local groupName=self:_C130DcAutoGetCarrierGroupName(dcargo) if unitName then setData.unitName=unitName end if groupName then setData.groupName=groupName end setData.ttl=timer.getTime()+3600 self:T(self.lid.." C130DcAuto mapped loaded "..EventData.IniDynamicCargoName.." set="..setData.id) return true end function CTLD:_C130DcAutoOnDynamicUnloaded(EventData) self:_C130DcAutoEnsureState() local dcargo=EventData.IniDynamicCargo if not dcargo then return false end local setData,entry=self:_C130DcAutoGetMappedEntry(EventData.IniDynamicCargoName) if not setData or not entry then setData,entry=self:_C130DcAutoResolveEntry(dcargo,true) end if not setData or not entry then return false end if not self:_C130DcAutoIsC130Event(dcargo)then return false end if setData.completed or setData.buildStarted or setData.handoffClaimed then return true end if entry.state=="landed"then return true end if dcargo.IsDetached and not dcargo:IsDetached()then return false end if dcargo.IsLandedStable and not dcargo:IsLandedStable()then return false end if DYNAMICCARGO and DYNAMICCARGO.C130RequireAirborne and dcargo.WasAirborneTransport and not dcargo:WasAirborneTransport()then return false end entry.state="landed" entry.dynamicName=EventData.IniDynamicCargoName self._c130DcAutoMap[EventData.IniDynamicCargoName]={setId=setData.id,entryId=entry.id} local dpos=dcargo.GetLastPosition and dcargo:GetLastPosition()or nil if dpos then entry.landedVec2=dpos:GetVec2() entry.landedVec3=dpos:GetVec3() end self:_C130DcAutoCreateProxyCargo(setData,entry,dcargo) setData.ttl=timer.getTime()+3600 self:T(self.lid.." C130DcAuto mapped unloaded "..EventData.IniDynamicCargoName.." set="..setData.id) self:_C130DcAutoTryCompleteSet(setData.id) return true end function CTLD:_C130DcAutoOnDynamicRemoved(EventData) self:_C130DcAutoEnsureState() local setData,entry=self:_C130DcAutoGetMappedEntry(EventData.IniDynamicCargoName) if not setData or not entry then return false end if entry.state~="landed"then entry.state="failed" setData.failed=true self:T(self.lid.." C130DcAuto entry failed/removed "..EventData.IniDynamicCargoName.." set="..setData.id) self:_C130DcAutoCleanupSet(setData.id,"removed") end return true end function CTLD:_C130DcAutoTick() self:_C130DcAutoEnsureState() local now=timer.getTime() local cleanup={} for setId,setData in pairs(self._c130DcAutoSets or{})do if setData.failed then cleanup[#cleanup+1]={setId=setId,reason="failed"} elseif setData.completed then if setData.cleanupAt and now>=setData.cleanupAt then cleanup[#cleanup+1]={setId=setId,reason="completed"} end elseif setData.ttl and now>setData.ttl then cleanup[#cleanup+1]={setId=setId,reason="ttl"} end end for _,entry in ipairs(cleanup)do self:_C130DcAutoCleanupSet(entry.setId,entry.reason) end 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 self.C130JUnits=self.C130JUnits or{} local utype=_unit:GetTypeName() if self.C130JTypes and self.C130JTypes[utype]then self.C130JUnits[unitname]=true elseif utype=="C-130J-30"then self.C130JUnits[unitname]=true else self.C130JUnits[unitname]=false end 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 if _unit:IsGround()and self.allowCATransport 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) self:_RefreshPackMenus(_group,_unit) if self:IsFixedWing(_unit)and self.enableFixedWing then self:_RefreshDropCratesMenu(_group,_unit) end 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 if self.C130JUnits then self.C130JUnits[unitname]=nil end 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) if self.UseC130LoadAndUnload and self.UseC130DynamicCargoAutoBuild then local handled=self:_C130DcAutoOnDynamicLoaded(event) if handled then return self end end 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() local msg=self:_GetEntryForGroup("CRATE_LOADED_GROUNDCREW",Group) msg=string.format(msg,event.IniDynamicCargoName) self:_SendMessage(msg,10,false,Group) self:__CratesPickedUp(1,Group,client,dcargo) self:_RefreshCrateQuantityMenus(Group,client,nil) end elseif event.id==EVENTS.DynamicCargoUnloaded then self:T(self.lid.."GC Unload Event "..event.IniDynamicCargoName) if self.UseC130LoadAndUnload and self.UseC130DynamicCargoAutoBuild then local handled=self:_C130DcAutoOnDynamicUnloaded(event) if handled then return self end end 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() if not self:IsC130J(client,true)then local msg=self:_GetEntryForGroup("CRATE_UNLOADED_GROUNDCREW",Group) msg=string.format(msg,event.IniDynamicCargoName) self:_SendMessage(msg,10,false,Group) end self:__CratesDropped(1,Group,client,{dcargo}) self:_RefreshCrateQuantityMenus(Group,client,nil) end elseif event.id==EVENTS.DynamicCargoRemoved then self:T(self.lid.."GC Remove Event "..event.IniDynamicCargoName) if self.UseC130LoadAndUnload and self.UseC130DynamicCargoAutoBuild then self:_C130DcAutoOnDynamicRemoved(event) end self.DynamicCargo[event.IniDynamicCargoName]=nil end return self end function CTLD:IsC130J(Unit,IgnoreUseC130Flag) if not Unit then return false end if not IgnoreUseC130Flag and not self.UseC130LoadAndUnload then return false end self.C130JUnits=self.C130JUnits or{} local unitname=Unit:GetName()or"none" return self.C130JUnits[unitname]==true end function CTLD:_SendMessage(Text,Time,Clearscreen,Group,Silent) self:T(self.lid.." _SendMessage") if not self.suppressmessages then local m=MESSAGE:New(Text,Time,"CTLD",Clearscreen):ToGroup(Group) if self.usesrs==true and Silent~=true then self.SRSQueue:NewTransmission(Text,duration,self.SRS,tstart,1,subgroups,subtitle,subduration,self.Frequency,self.Modulation,self.Gender, self.Culture,self.Voice,self.Volume,self.Label,coordinate,self.Speed) end end return self end function CTLD:_FindTroopsCargoObject(Name) self:T(self.lid.." _FindTroopsCargoObject") self._troopsByName=self._troopsByName or{} local cached=self._troopsByName[Name] if cached then return cached end local cargo=nil for _,_cargo in pairs(self.Cargo_Troops)do local cargo=_cargo if cargo.Name==Name then self._troopsByName[Name]=cargo return cargo end end return nil end function CTLD:_FindCratesCargoObject(Name) self:T(self.lid.." _FindCratesCargoObject") self._crateOrStaticByName=self._crateOrStaticByName or{} local cached=self._crateOrStaticByName[Name] if cached then return cached end local cargo=nil for _,_cargo in pairs(self.Cargo_Crates)do local cargo=_cargo if cargo.Name==Name then self._crateOrStaticByName[Name]=cargo return cargo end end for _,_cargo in pairs(self.Cargo_Statics)do local cargo=_cargo if cargo.Name==Name then self._crateOrStaticByName[Name]=cargo 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 local msg=self:_GetEntryForGroup("CHOPPER_CANNOT_CARRY",Group) self:_SendMessage(msg,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 local msg=self:_GetEntryForGroup("CRATE_LOADED_ID",Group) msg=string.format(msg,crate:GetID(),crate:GetName()) self:_SendMessage(msg,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:CanGetTroops(Group,Unit,Cargo,quantity,Inject) return true 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 local msg=self:_GetEntryForGroup("ALL_GONE",Group) msg=string.format(msg,cgoname) self:_SendMessage(msg,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 local msg=self:_GetEntryForGroup("NOT_CLOSE_ENOUGH_LOGISTICS",Group) self:_SendMessage(msg,10,false,Group) if not self.debug then return self end elseif not grounded and not hoverload then local msg=self:_GetEntryForGroup("NEED_TO_LAND_OR_HOVER_LOAD",Group) self:_SendMessage(msg,10,false,Group) if not self.debug then return self end elseif self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName())then local msg=self:_GetEntryForGroup("OPEN_DOORS_LOAD_TROOPS",Group) self:_SendMessage(msg,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 local msg=self:_GetEntryForGroup("CRAMMED",Group) self:_SendMessage(msg,10,false,Group) return elseif maxloadableself.EngineerSearch then local msg=self:_GetEntryForGroup("NO_UNIT_TO_REPAIR",Group) self:_SendMessage(msg,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 local msg=self:_GetEntryForGroup("REPAIR_STARTED",Group) msg=string.format(msg,build.Name,self.repairtime) self:_SendMessage(msg,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 local msg=self:_GetEntryForGroup("CANT_REPAIR_WITH",Group) msg=string.format(msg,build.Name) self:_SendMessage(msg,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:IsCorrectHover(Unit) local hassecondaries=false if not grounded and not hoverload then local msg=self:_GetEntryForGroup("NEED_TO_LAND_OR_HOVER_LOAD",Group) self:_SendMessage(msg,10,false,Group) if not self.debug then return self end end if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName())then local msg=self:_GetEntryForGroup("OPEN_DOORS_EXTRACT_TROOPS",Group) self:_SendMessage(msg,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 local msg=self:_GetEntryForGroup("NO_UNITS_TO_EXTRACT",Group) self:_SendMessage(msg,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 local msg=self:_GetEntryForGroup("CANT_ONBOARD",Group) msg=string.format(msg,groupType) self:_SendMessage(msg,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 local msg=self:_GetEntryForGroup("CRAMMED",Group) self:_SendMessage(msg,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 local boardedtext=self:_GetEntryForGroup("BOARDED",Group) self:ScheduleOnce(running,self._SendMessage,self,string.format(boardedtext,Cargotype.Name),10,false,Group) local msg=self:_GetEntryForGroup("BOARDING",Group) msg=string.format(msg,Cargotype.Name) self:_SendMessage(msg,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:_LoadTroopsQuantity(Group,Unit,Cargo,quantity) local n=math.max(1,tonumber(quantity)or 1) 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 inzone then local msg=self:_GetEntryForGroup("NOT_CLOSE_ENOUGH_LOGISTICS",Group) self:_SendMessage(msg,10,false,Group) if not self.debug then return self end elseif not grounded and not hoverload then local msg=self:_GetEntryForGroup("NEED_TO_LAND_OR_HOVER_LOAD",Group) self:_SendMessage(msg,10,false,Group) if not self.debug then return self end elseif self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName())then local msg=self:_GetEntryForGroup("OPEN_DOORS_LOAD_TROOPS",Group) self:_SendMessage(msg,10,false,Group) if not self.debug then return self end end if not self:CanGetTroops(Group,Unit,Cargo,n,false)then return self end local prevSuppress=self.suppressmessages self.suppressmessages=true for i=1,n do timer.scheduleFunction(function()self:_LoadTroops(Group,Unit,Cargo)end,{},timer.getTime()+0.2*i) end timer.scheduleFunction(function() self.suppressmessages=prevSuppress local dname=Cargo:GetName() local msg=self:_GetEntryForGroup("LOADED_FULL",Group) msg=string.format(msg,n,dname) self:_SendMessage(msg,10,false,Group) end,{},timer.getTime()+0.2*n+0.05) return self end function CTLD:_AddTroopQuantityMenus(Group,Unit,parentMenu,cargoObj) local stock=cargoObj:GetStock() local maxQuantity=self.maxCrateMenuQuantity or 1 if type(stock)=="number"and stock>=0 and stock0 then local space=trooplimit-onboard if space=0 then availableSets=math.floor(stock) if availableSets<=0 then MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_OUT_OF_STOCK",Group),parentMenu,function()end) return self end if availableSets0 then local loadedData=nil if self.Loaded_Cargo then loadedData=self.Loaded_Cargo[Unit:GetName()] end local loadedCount=0 if loadedData and type(loadedData.Cratesloaded)=="number"then loadedCount=loadedData.Cratesloaded end local space=capacity-loadedCount if space<0 then space=0 end capacityCrates=space local perSet=needed>0 and needed or 1 capacitySets=math.floor(space/perSet) end end local allowLoad=true if type(capacitySets)=="number"then if capacitySets>=1 then if capacitySets0 and needed or 1) if type(maxload)=="number"and maxload>0 and setMass>0 then maxMassSets=math.floor(maxload/setMass) if maxMassSets<1 then maxQuantity=1 allowLoad=false elseif maxMassSets0 and perCrateMass>0 then maxMassCrates=math.floor(maxload/perCrateMass) end end self:T("_AddCrateQuantityMenus maxQuantity "..maxQuantity.." allowLoad "..tostring(allowLoad)) if maxQuantity<1 then return self end if maxQuantity==1 then self:T("_AddCrateQuantityMenus maxQuantity "..maxQuantity.." Menu for MaxQ=1 ".."parentMenu.MenuText = "..parentMenu.MenuText) local canLoad=(allowLoad and(not capacitySets or capacitySets>=1)and(not maxMassSets or maxMassSets>=1)) local isHerc=self:IsC130J(Unit) local cgotype=cargoObj:GetType()or nil local suppressGetAndLoad=(self.enableChinookGCLoading==true)and(cgotype==CTLD_CARGO.Enum.STATIC) local canPartiallyLoad=((not capacityCrates or capacityCrates>=1)and(not maxMassCrates or maxMassCrates>=1)) if suppressGetAndLoad or isHerc then if canLoad then MENU_GROUP_COMMAND:New(Group,"1",parentMenu,self._GetCrateQuantity,self,Group,Unit,cargoObj,1) else local msg if maxMassSets and(not capacitySets or capacitySets>=1)and maxMassSets<1 then msg=self:_GetEntryForGroup("WEIGHT_LIMIT",Group) else msg=self:_GetEntryForGroup("CRATE_LIMIT",Group) end MENU_GROUP_COMMAND:New(Group,msg,parentMenu,self._SendMessage,self,msg,10,false,Group) end return self end if canLoad and not isHerc and not suppressGetAndLoad then MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_GET",Group),parentMenu,self._GetCrateQuantity,self,Group,Unit,cargoObj,1) MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_GET_AND_LOAD",Group),parentMenu,self._GetAndLoad,self,Group,Unit,cargoObj,1) else local msg if not isHerc and not suppressGetAndLoad then if maxMassSets and(not capacitySets or capacitySets>=1)and maxMassSets<1 then msg=self:_GetEntryForGroup("WEIGHT_LIMIT",Group) else msg=self:_GetEntryForGroup("CRATE_LIMIT",Group) end MENU_GROUP_COMMAND:New(Group,msg,parentMenu,self._SendMessage,self,msg,10,false,Group) if canPartiallyLoad and(cgotype~=CTLD_CARGO.Enum.STATIC)and(not suppressGetAndLoad)then MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_GET_ANYWAY",Group),parentMenu,self._GetCrateQuantity,self,Group,Unit,cargoObj,1) MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_PARTIALLY_LOAD",Group),parentMenu,self._GetAndLoad,self,Group,Unit,cargoObj,1,true) end end end return self end for quantity=1,maxQuantity do self:T("_AddCrateQuantityMenus maxQuantity "..maxQuantity.." Menu for MaxQ>1") local label=tostring(quantity) self:T("_AddCrateQuantityMenus Label "..label) local canLoad=(allowLoad and(not capacitySets or capacitySets>=quantity)and(not maxMassSets or maxMassSets>=quantity)) local isHerc=self:IsC130J(Unit) local cgotype=cargoObj:GetType()or nil local suppressGetAndLoad=(self.enableChinookGCLoading==true)and(cgotype==CTLD_CARGO.Enum.STATIC) if canLoad and not isHerc and not suppressGetAndLoad then local qMenu=MENU_GROUP:New(Group,label,parentMenu) MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_GET",Group),qMenu,self._GetCrateQuantity,self,Group,Unit,cargoObj,quantity) MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_GET_AND_LOAD",Group),qMenu,self._GetAndLoad,self,Group,Unit,cargoObj,quantity) else MENU_GROUP_COMMAND:New(Group,label,parentMenu,self._GetCrateQuantity,self,Group,Unit,cargoObj,quantity) end end return self end function CTLD:CanGetUnits(Group,Unit,Config,quantity,quiet) return true end function CTLD:_C130GetUnits(Group,Unit,Name) self:T(self.lid.." _C130GetUnits") if not Group or not Unit then return self end local cfg=nil for _,entry in ipairs(self.C130GetUnits or{})do if entry.Name==Name then cfg=entry break end end if not cfg then local msg=self:_GetEntryForGroup("NO_UNIT_CONFIG",Group) msg=string.format(msg,Name) self:_SendMessage(msg,10,false,Group) return self end local stock=cfg.Stock if type(stock)=="number"and stock~=-1 and stock<=0 then local msg=self:_GetEntryForGroup("ALL_GONE",Group) msg=string.format(msg,cfg.Name or"units") self:_SendMessage(msg,10,false,Group) return self end local inzone=self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) if not inzone then local msg=self:_GetEntryForGroup("NOT_CLOSE_ENOUGH_LOGISTICS",Group) self:_SendMessage(msg,10,false,Group) return self end if not self:CanGetUnits(Group,Unit,cfg,1,false)then return self end local coord=Unit:GetCoordinate()or Group:GetCoordinate() local capabilities=self:_GetUnitCapabilities(Unit) local innerDist=(capabilities.length and capabilities.length/2)or 15 local maxUnitsNearby=self.maxUnitsNearby or 3 local searchRadius=self.UnitDistance or 90 local checkZone=ZONE_RADIUS:New("CTLD_C130UnitsZone",coord:GetVec2(),searchRadius,false) local nearGroups=SET_GROUP:New():FilterCoalitions("blue"):FilterZones({checkZone}):FilterOnce() local nearbyCount=0 for _,gr in pairs(nearGroups.Set)do local gc=gr:GetCoordinate() if gc then local dist=coord:Get2DDistance(gc) if dist>innerDist then for _,ucfg in pairs(self.C130GetUnits or{})do local templ=ucfg.Templates or{} if type(templ)=="string"then templ={templ} end local matched=false for _,tName in pairs(templ)do if string.match(gr:GetName(),tName)then nearbyCount=nearbyCount+1 matched=true break end end if matched or nearbyCount>=maxUnitsNearby then break end end end end if nearbyCount>=maxUnitsNearby then break end end if nearbyCount>=maxUnitsNearby then local msg=self:_GetEntryForGroup("TOO_MANY_UNITS_NEARBY",Group) msg=string.format(msg,maxUnitsNearby) self:_SendMessage(msg,10,false,Group) return self end local temptable=cfg.Templates or{} if type(temptable)=="string"then temptable={temptable} end local length=(capabilities.length+5)or 30 local heading=(Unit:GetHeading()+180)%360 local canmove=cfg.CanMove~=false local spawnedUnits={} local idx=1 for _,_template in pairs(temptable)do local cratedistance=(idx-1)*2.5+length local spawncoord=coord:Translate(cratedistance,heading) local randomcoord=spawncoord:GetVec2() self.TroopCounter=self.TroopCounter+1 local tc=self.TroopCounter local alias=string.format("%s-%d",_template,math.random(1,100000)) if canmove then SPAWN:NewWithAlias(_template,alias) :InitRandomizeUnits(true,10,2) :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits,70) :InitDelayOff() :OnSpawnGroup(function(grp,TimeStamp) grp.spawntime=TimeStamp or timer.getTime() self.DroppedTroops[tc]=grp table.insert(spawnedUnits,grp) self:__UnitsSpawn(1,Group,Unit,spawnedUnits) end) :SpawnFromVec2(randomcoord) else SPAWN:NewWithAlias(_template,alias) :InitRandomizeUnits(true,10,2) :InitDelayOff() :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits,70) :OnSpawnGroup(function(grp,TimeStamp) grp.spawntime=TimeStamp or timer.getTime() self.DroppedTroops[tc]=grp table.insert(spawnedUnits,grp) self:__UnitsSpawn(1,Group,Unit,spawnedUnits) end) :SpawnFromVec2(randomcoord) end idx=idx+1 end if type(stock)=="number"and stock~=-1 then cfg.Stock=stock-1 end local msg=self:_GetEntryForGroup("DEPLOYED_NEAR_YOU",Group) msg=string.format(msg,cfg.Name or"selection") self:_SendMessage(msg,10,false,Group) return self end function CTLD:CanGetCrates(Group,Unit,Cargo,number,drop,pack,quiet,suppressGetEvent) return true end function CTLD:_GetCrates(Group,Unit,Cargo,number,drop,pack,quiet,suppressGetEvent) self:T(self.lid.." _GetCrates") local perSet=Cargo:GetCratesNeeded()or 1 if perSet<1 then perSet=1 end local requestNumber=tonumber(number) if requestNumber then requestNumber=math.floor(requestNumber) if requestNumber<1 then requestNumber=perSet end else requestNumber=perSet end local requestedSets=math.floor((requestNumber+perSet-1)/perSet) if requestedSets<1 then requestedSets=1 end 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 local msg=self:_GetEntryForGroup("RAN_OUT_OF",Group) msg=string.format(msg,cgoname) self:_SendMessage(msg,10,false,Group) return false end end local inzone=false local drop=drop or false local suppressGetEvent=suppressGetEvent 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 local msg=self:_GetEntryForGroup("NOT_CLOSE_ENOUGH_LOGISTICS",Group) self:_SendMessage(msg,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 local msg=self:_GetEntryForGroup("CARGO_NOT_AVAILABLE_ZONE",Group) self:_SendMessage(msg,10,false,Group) if not self.debug then return false 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,true) if numbernearby>=canloadcratesno and(not drop)and(not pack)then local msg=self:_GetEntryForGroup("ENOUGH_CRATES_NEARBY",Group) self:_SendMessage(msg,10,false,Group) return false end if not drop and not self:CanGetCrates(Group,Unit,Cargo,requestNumber,drop,pack,quiet,suppressGetEvent)then return false end local IsHerc=self:IsFixedWing(Unit) local IsHook=self:IsHook(Unit) local IsHelo=Unit and Unit.IsHelicopter and Unit:IsHelicopter()or false local IsTruck=Unit:IsGround() local cargotype=Cargo local number=requestNumber local cratesneeded=cargotype:GetCratesNeeded() local cratename=cargotype:GetName() local cratedisplayname=self:_GetCargoDisplayNameForGroup(cargotype,Group) 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 obtainedcargo={} local cratedistance=0 local rheading=0 local angleOffNose=0 local addon=0 if IsHerc or IsHook or IsTruck 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 local shipdist=nil local shipoffset=nil local FW_STEP_BY_TYPE={ cds_crate=2.0, cds_barrels=2.0, ammo_cargo=1.0, iso_container_small=3.5, iso_container=3.5, uh1h_cargo=2.0, container_cargo=2.5, } local FW_WIDTH_BY_TYPE={ cds_crate=0.8, cds_barrels=0.8, ammo_cargo=0.4, iso_container_small=2.0, iso_container=2.0, uh1h_cargo=0.6, container_cargo=1.3, } local FW_ROW_GAP=0.6 local FW_BATCH_ANGLE_PATTERN={0,-20,20,-40,40,-60,60,-80,80} local fwBatchState=nil local fwBatchIndex=0 local fwBatchKey=nil if IsHerc or IsHelo then self._fwBatchState=self._fwBatchState or{} fwBatchKey=(Unit and Unit.GetName and Unit:GetName())or"FW" fwBatchState=self._fwBatchState[fwBatchKey]or{batch=0} if(tonumber(numbernearby)or 0)<=0 then fwBatchState.batch=0 end fwBatchIndex=fwBatchState.batch or 0 end local fwZeroAngleSetHeading=nil local fwNonZeroAngleSetHeading=nil local c130DcAutoSetId=nil if not drop and not pack and self.UseC130LoadAndUnload and self.UseC130DynamicCargoAutoBuild and self:IsC130J(Unit)and self:_C130DcAutoIsBuildableCargo(cargotype)then c130DcAutoSetId=self:_C130DcAutoRegisterSet(Group,Unit,cargotype,zone) end for i=1,number do local currentAngleOffset=0 local cratealias=string.format("%s-%d",cratename,math.random(1,100000)) local CCat,CType,CShape=Cargo:GetStaticTypeAndShape() local basetype=CType or self.basetype or"container_cargo" CCat=CCat or"Cargos" if not isstatic and self:IsC130J(Unit,true)then if Cargo.C130TypeName then basetype=Cargo.C130TypeName elseif self.C130basetype and(not CType or CType==self.basetype)then basetype=self.C130basetype end end if not self.placeCratesAhead or drop==true then local step=(IsHerc or IsHelo)and(FW_STEP_BY_TYPE[basetype]or 2.6)or 1.6 if(IsHerc or IsHelo)and not drop then local safeDistance=capabilities.length*0.9 local maxDist=self.CrateDistance or 35 if safeDistance>maxDist then safeDistance=maxDist end local angleIndex=(fwBatchIndex%#FW_BATCH_ANGLE_PATTERN)+1 currentAngleOffset=FW_BATCH_ANGLE_PATTERN[angleIndex]or 0 local centerHeading=math.fmod((heading+currentAngleOffset),360) if math.abs(currentAngleOffset)>=0.01 and not fwNonZeroAngleSetHeading then fwNonZeroAngleSetHeading=centerHeading end local baseDistance=safeDistance local crateWidth=FW_WIDTH_BY_TYPE[basetype]or step local zeroAngle=math.abs(currentAngleOffset)<0.01 local lateral=nil local lateralHeading=nil if zeroAngle then local totalRowWidth=(number*crateWidth)+(math.max(0,number-1)*FW_ROW_GAP) local leftEdge=-(totalRowWidth/2) lateral=leftEdge+((i-1)*(crateWidth+FW_ROW_GAP))+(crateWidth/2) if lateral>=0 then lateralHeading=math.fmod(centerHeading+90,360) else lateral=-lateral lateralHeading=math.fmod(centerHeading+270,360) end else lateral=(i-1)*(crateWidth+FW_ROW_GAP) if currentAngleOffset<0 then lateralHeading=math.fmod(centerHeading+270,360) else lateralHeading=math.fmod(centerHeading+90,360) end end local maxLateralSq=(maxDist*maxDist)-(baseDistance*baseDistance) if maxLateralSq<0 then maxLateralSq=0 end local maxLateral=math.sqrt(maxLateralSq) if lateral>maxLateral then lateral=maxLateral end local baseCoord=position:Translate(baseDistance,centerHeading) cratecoord=baseCoord:Translate(lateral,lateralHeading) cratedistance=baseDistance rheading=centerHeading else cratedistance=(i-1)*step+capabilities.length if cratedistance>self.CrateDistance then cratedistance=self.CrateDistance end rheading=UTILS.RandomGaussian(0,18,-55,55,100) rheading=math.fmod((heading+rheading),360) cratecoord=position:Translate(cratedistance,rheading) end 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 crateSpawnHeading=270 if(IsHerc or IsHelo)and not drop and cratecoord and type(ship)~="string"then if math.abs(currentAngleOffset)<0.01 then if not fwZeroAngleSetHeading then fwZeroAngleSetHeading=heading end crateSpawnHeading=fwZeroAngleSetHeading else crateSpawnHeading=fwNonZeroAngleSetHeading end 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 halfwidth=(width or 20)/2 local Offy=nil if i==1 or shipdist==nil or shipoffset==nil then Offy=math.random(-halfwidth,halfwidth) shipoffset=Offy shipdist=dist else dist=shipdist local step=math.max(1,math.min(3,halfwidth*0.2)) local slot=i-1 local ring=math.floor((slot+1)/2) local sign=(slot%2==1)and 1 or-1 Offy=shipoffset+(sign*ring*step) if Offy>halfwidth then Offy=halfwidth elseif Offy<-halfwidth then Offy=-halfwidth end end 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(crateSpawnHeading,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(crateSpawnHeading,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) realcargo:SetDisplayName(cargotype:GetDisplayName()) local map=cargotype:GetStaticResourceMap() realcargo:SetStaticResourceMap(map) local CCat3,CType3,CShape3=cargotype:GetStaticTypeAndShape() realcargo:SetStaticTypeAndShape(CCat3,CType3,CShape3) 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) realcargo:SetDisplayName(cargotype:GetDisplayName()) local map=cargotype:GetStaticResourceMap() realcargo:SetStaticResourceMap(map) if cargotype.TypeNames then realcargo.TypeNames=UTILS.DeepCopy(cargotype.TypeNames) end if self.UseC130LoadAndUnload and self:IsC130J(Unit)then realcargo:SetWasDropped(true,true) end end if not drop and not pack then table.insert(obtainedcargo,realcargo) end local CCat4,CType4,CShape4=cargotype:GetStaticTypeAndShape() realcargo:SetStaticTypeAndShape(CCat4,CType4,CShape4) if c130DcAutoSetId and realcargo then self:_C130DcAutoRegisterDynamicCargo(realcargo:GetPositionable()) self:_C130DcAutoRegisterEntry(c130DcAutoSetId,realcargo) end table.insert(self.Spawned_Cargo,realcargo) end if(IsHerc or IsHelo)and fwBatchState and fwBatchKey and not drop then local maxBatches=#FW_BATCH_ANGLE_PATTERN fwBatchState.batch=(fwBatchIndex+1)%maxBatches self._fwBatchState[fwBatchKey]=fwBatchState end if not(drop or pack)then Cargo:RemoveStock(requestedSets) self:_RefreshCrateQuantityMenus(Group,Unit,Cargo) end local text=string.format(self:_GetEntryForGroup("CRATES_POSITIONED",Group),number,cratedisplayname) if drop then text=string.format(self:_GetEntryForGroup("CRATES_DROPPED",Group),number,cratedisplayname) self:__CratesDropped(1,Group,Unit,droppedcargo) else if not quiet then self:_SendMessage(text,10,false,Group) end if not pack and not suppressGetEvent and#obtainedcargo>0 then self:__GetCrates(1,Group,Unit,obtainedcargo) end end self:_RefreshLoadCratesMenu(Group,Unit) self:_RefreshPackMenus(Group,Unit) return true 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,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,true) else local msg=self:_GetEntryForGroup("NO_CRATES_WITHIN",_group) msg=string.format(msg,finddist) self:_SendMessage(msg,10,false,_group,true) end return self end function CTLD:_C130RemoveUnitsNearby(_group,_unit) self:T(self.lid.." _C130RemoveUnitsNearby") if not _group or not _unit then return self end local location=_group:GetCoordinate() if not location then return self end local capabilities=self:_GetUnitCapabilities(_unit) local innerDist=(capabilities.length and capabilities.length/2)or 15 local finddist=self.PackDistance or(self.CrateDistance or 35) local zone=ZONE_RADIUS:New("CTLD_C130RemoveZone",location:GetVec2(),finddist,false) local nearestGroups=SET_GROUP:New():FilterCoalitions("blue"):FilterZones({zone}):FilterOnce() local removedAny=false local removedTable={} for _,gr in pairs(nearestGroups.Set)do local gc=gr:GetCoordinate() if gc then local dist=location:Get2DDistance(gc) if dist>innerDist then local didRemoveThis=false for _,cfg in pairs(self.C130GetUnits or{})do local templ=cfg.Templates or{} if type(templ)=="string"then templ={templ} end for _,tName in pairs(templ)do if string.match(gr:GetName(),tName)then local cname=cfg.Name or"Unit" table.insert(removedTable,{groupName=gr:GetName(),name=cname,template=tName,coordinate=gr:GetCoordinate()}) gr:Destroy(false) local msg=self:_GetEntryForGroup("UNITS_REMOVED",_group) msg=string.format(msg,cname) self:_SendMessage(msg,10,false,_group) removedAny=true didRemoveThis=true break end end if didRemoveThis then break end end end end end if not removedAny then local msg=self:_GetEntryForGroup("NOTHING_TO_REMOVE",_group) self:_SendMessage(msg,10,false,_group) else self:__RemoveCratesNearby(1,_group,_unit,removedTable) 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,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)) local pos=entry:GetPositionable() if pos then entry.coordinate=pos:GetCoordinate() pos: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,true) local done={} for _,e in pairs(crates)do local n=e:GetName()or"none" if not done[n]then local object=self:_FindCratesCargoObject(n) if object then self:_RefreshCrateQuantityMenus(_group,_unit,object)end done[n]=true end end self:_CleanupTrackedCrates(removedIDs) self:_RefreshLoadCratesMenu(_group,_unit) self:_RefreshPackMenus(_group,_unit) self:__RemoveCratesNearby(1,_group,_unit,crates) else local msg=self:_GetEntryForGroup("NO_CRATES_WITHIN",_group) msg=string.format(msg,finddist) self:_SendMessage(msg,10,false,_group,true) 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,ignoreHercInner) 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:T(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:T(self.lid.." Found cargo mass: "..weight) if static and static:IsAlive()then local restricthooktononstatics=self.enableChinookGCLoading and IsHook self:T(self.lid.." restricthooktononstatics: "..tostring(restricthooktononstatics)) local cargoisstatic=cargo:GetType()==CTLD_CARGO.Enum.STATIC and true or false self:T(self.lid.." Cargo is static: "..tostring(cargoisstatic)) local restricted=cargoisstatic and restricthooktononstatics self:T(self.lid.." Loading restricted: "..tostring(restricted)) local staticpos=static:GetCoordinate() local cando=cargo:UnitCanCarry(_unit) if ignoretype==true then cando=true restricted=false end self:T(self.lid.." Unit can carry: "..tostring(cando)) local distance=self:_GetDistance(location,staticpos) local hercInnerBlocked=false if self.UseC130LoadAndUnload and ignoreHercInner and _unit and self:IsC130J(_unit)then local capabilities=self:_GetUnitCapabilities(_unit) local innerDist=capabilities.length and(capabilities.length/2)or 4 if distanceb: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 local msg=self:_GetEntryForGroup("LOADED_FULL",Group) msg=string.format(msg,fullSets,self:_GetCargoDisplayNameForGroup(cName,Group)) self:_SendMessage(msg,10,false,Group) elseif fullSets>0 and leftover>0 then local msg=self:_GetEntryForGroup("LOADED_SETS_LEFTOVER",Group) msg=string.format(msg,fullSets,self:_GetCargoDisplayNameForGroup(cName,Group),leftover) self:_SendMessage(msg,10,false,Group) else local msg=self:_GetEntryForGroup("LOADED_PARTIAL",Group) msg=string.format(msg,loadedHere,needed,self:_GetCargoDisplayNameForGroup(cName,Group)) self:_SendMessage(msg,15,false,Group) end else local msg=self:_GetEntryForGroup("LOADED_SETS",Group) msg=string.format(msg,loadedHere,self:_GetCargoDisplayNameForGroup(cName,Group)) self:_SendMessage(msg,10,false,Group) end end end self.Loaded_Cargo[unitname]=loaded self:_UpdateUnitCargoMass(Unit) self:_RefreshDropCratesMenu(Group,Unit) self:_RefreshLoadCratesMenu(Group,Unit) self:_RefreshPackMenus(Group,Unit) self:_CleanupTrackedCrates(crateidsloaded) self:__CratesPickedUp(1,Group,Unit,loaded.Cargo) self:_RefreshCrateQuantityMenus(Group,Unit,nil) end end return self end function CTLD:_CleanupTrackedCrates(crateIdsToRemove) local existingcrates=self.Spawned_Cargo local newexcrates={} local remove={} for _,_ID in pairs(crateIdsToRemove or{})do remove[_ID]=true end for _,_crate in pairs(existingcrates)do local excrate=_crate local ID=excrate:GetID() local keep=not remove[ID] local static=_crate:GetPositionable() if not static or not static:IsAlive()then keep=false end if keep then newexcrates[#newexcrates+1]=_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 local hercInnerCrates=nil local hercInnerCount=0 if self:IsC130J(Unit)or self:IsHook(Unit)then local innerDist=(capabilities.length and capabilities.length/2)or 15 local innerCrates,innerCount=self:_FindCratesNearby(Group,Unit,innerDist,true,true) hercInnerCrates=innerCrates hercInnerCount=innerCount or 0 end if self.Loaded_Cargo[unitname]or hercInnerCount>0 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 if hercInnerCount>0 then local hercMass=0 for _,_cargo in pairs(hercInnerCrates or{})do local cargo=_cargo local type=cargo:GetType() if type~=CTLD_CARGO.Enum.TROOPS and type~=CTLD_CARGO.Enum.ENGINEERS then report:Add(string.format("Crate: %s size 1",cargo:GetName())) hercMass=hercMass+cargo:GetMass() end end loadedmass=loadedmass+hercMass end report:Add("------------------------------------------------------------") report:Add("Total Mass: "..loadedmass.." kg. Loadable: "..maxloadable.." kg.") local text=report:Text() self:_SendMessage(text,30,true,Group,true) else local msg=self:_GetEntryForGroup("NOTHING_LOADED",Group) msg=string.format(msg,trooplimit,cratelimit,maxloadable) self:_SendMessage(msg,10,false,Group,true) 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,true) else local msg=self:_GetEntryForGroup("NOTHING_IN_STOCK",Group) self:_SendMessage(msg,10,false,Group,true) end return self end function CTLD:IsFixedWing(Unit) local typename=Unit:GetTypeName()or"none" for _,_name in pairs(self.FixedWingTypes or{})do if _name and(typename==_name or string.find(typename,_name,1,true))then return true end end return false end function CTLD:IsHook(Unit) if not Unit then return false end local typeName=Unit:GetTypeName() if not typeName then return false end if string.find(typeName,"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:CanUnloadAllTroops(Group,Unit,LoadedCargo,IsGrounded,IsHoverUnload) return true 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 local msg=self:_GetEntryForGroup("OPEN_DOORS_UNLOAD_TROOPS",Group) self:_SendMessage(msg,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=self.returntroopstobase 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{} if not self:CanUnloadAllTroops(Group,Unit,loadedcargo,grounded,hoverunload)then return self end if not droppingatbase or self.debug then local cargotable=loadedcargo.Cargo local deployedTroopsByName={} local deployedEngineersByName={} 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) :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits) :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) deployedEngineersByName[name]=(deployedEngineersByName[name]or 0)+1 else deployedTroopsByName[name]=(deployedTroopsByName[name]or 0)+1 end end end local parts={} local troopLabel=self:_GetEntryForGroup("TROOPS_LABEL",Group) local engineerLabel=self:_GetEntryForGroup("ENGINEERS_LABEL",Group) for nName,nCount in pairs(deployedTroopsByName)do parts[#parts+1]=tostring(nCount).."x "..troopLabel.." "..nName end for nName,nCount in pairs(deployedEngineersByName)do parts[#parts+1]=tostring(nCount).."x "..engineerLabel.." "..nName end if#parts>0 then local msg=self:_GetEntryForGroup("DROPPED_INTO_ACTION",Group) msg=string.format(msg,table.concat(parts,", ")) self:_SendMessage(msg,10,false,Group) end else local msg=self:_GetEntryForGroup("TROOPS_RETURNED",Group) self:_SendMessage(msg,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() self:_RefreshTroopQuantityMenus(Group,Unit,_troop) end end end end end end self.Loaded_Cargo[unitname]=nil self.Loaded_Cargo[unitname]=loaded self:_RefreshDropTroopsMenu(Group,Unit) self:_UpdateUnitCargoMass(Unit) self:_RefreshTroopQuantityMenus(Group,Unit,nil) else if IsHerc then local msg=self:_GetEntryForGroup("NOTHING_LOADED_AIRDROP",Group) self:_SendMessage(msg,10,false,Group) else local msg=self:_GetEntryForGroup("NOTHING_LOADED_HOVER",Group) self:_SendMessage(msg,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 local msg=self:_GetEntryForGroup("NOT_CLOSE_ENOUGH_DROP",Group) self:_SendMessage(msg,10,false,Group) if not self.debug then return self end end end if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName())then local msg=self:_GetEntryForGroup("OPEN_DOORS_DROP_CARGO",Group) self:_SendMessage(msg,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 local droppedCount={} local neededMap={} 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) local cname=cargo:GetName()or"Unknown" droppedCount[cname]=(droppedCount[cname]or 0)+1 if not neededMap[cname]then neededMap[cname]=cargo:GetCratesNeeded()or 1 end end end for cname,count in pairs(droppedCount)do local needed=neededMap[cname]or 1 if needed>1 then local full=math.floor(count/needed) local left=count%needed if full>0 and left==0 then local msg=self:_GetEntryForGroup("DROPPED_FULL",Group) msg=string.format(msg,full,self:_GetCargoDisplayNameForGroup(cname,Group)) self:_SendMessage(msg,10,false,Group) elseif full>0 and left>0 then local msg=self:_GetEntryForGroup("DROPPED_SETS_LEFTOVER",Group) msg=string.format(msg,full,self:_GetCargoDisplayNameForGroup(cname,Group),left) self:_SendMessage(msg,10,false,Group) else local msg=self:_GetEntryForGroup("DROPPED_PARTIAL",Group) msg=string.format(msg,count,needed,self:_GetCargoDisplayNameForGroup(cname,Group)) self:_SendMessage(msg,15,false,Group) end else local msg=self:_GetEntryForGroup("DROPPED_SETS",Group) msg=string.format(msg,count,self:_GetCargoDisplayNameForGroup(cname,Group)) self:_SendMessage(msg,10,false,Group) 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) self:_RefreshCrateQuantityMenus(Group,Unit,nil) else if IsHerc then local msg=self:_GetEntryForGroup("NOTHING_LOADED_AIRDROP",Group) self:_SendMessage(msg,10,false,Group) else local msg=self:_GetEntryForGroup("NOTHING_LOADED_HOVER",Group) self:_SendMessage(msg,10,false,Group) end end return self end function CTLD:CanBuildCrates(Group,Unit,crates,number,Engineering,MultiDrop) return true end function CTLD:_BuildCrates(Group,Unit,Engineering,MultiDrop,NotifyGroup) self:T(self.lid.." _BuildCrates") if self:IsFixedWing(Unit)and self.enableFixedWing and not Engineering then local speed=Unit:GetVelocityKMH() if speed>1 then local msg=self:_GetEntryForGroup("NEED_TO_LAND_BUILD",Group) self:_SendMessage(msg,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 local msg=self:_GetEntryForGroup("CANNOT_BUILD_LOADING_AREA",Group) self:_SendMessage(msg,10,false,Group) return self end end local baseDist=self.CrateDistance or 35 local finddist=baseDist if Engineering and self.EngineerSearch and self.EngineerSearch>baseDist then finddist=self.EngineerSearch end local crates,number=self:_FindCratesNearby(Group,Unit,finddist,true,true,not Engineering) local activeSetId=Engineering and self._c130DcAutoActiveSetId or nil local isC130Auto=Engineering and activeSetId~=nil local notifyGroup=(not Engineering)and Group or nil if activeSetId then crates,number=self:_C130DcAutoFilterCrates(crates,activeSetId) local notifySetId=nil if type(activeSetId)=="table"then notifySetId=activeSetId[1]or next(activeSetId) else notifySetId=activeSetId end local setData=notifySetId and self._c130DcAutoSets and self._c130DcAutoSets[notifySetId]or nil if setData and setData.groupName then notifyGroup=GROUP:FindByName(setData.groupName)or notifyGroup end local scopeText=tostring(activeSetId) if type(activeSetId)=="table"then local ids={} for k,v in pairs(activeSetId)do if type(k)=="number"then ids[#ids+1]=tostring(v) else ids[#ids+1]=tostring(k) end end table.sort(ids) scopeText=table.concat(ids,",") end self:T(self.lid.." C130DcAuto engineer scope set="..scopeText.." crates="..tostring(number)) end if NotifyGroup then notifyGroup=NotifyGroup end local buildables={} local foundbuilds=false local canbuild=false if not self:CanBuildCrates(Group,Unit,crates,number,Engineering,MultiDrop)then return self end 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() local distToUnit=Unit and ccoord:Get2DDistance(Unit:GetCoordinate())or 0 local isHercDrop=Crate:WasDropped(true) if not isHercDrop and distToUnit>baseDist then elseif self:IsC130J(Unit)and distToUnit<15 then elseif self:IsHook(Unit)and distToUnit<5 then elseif(Unit:GetTypeName()=="Mi-8MTV2"or Unit:GetTypeName()=="Mi-8MT")and distToUnit<8 then else 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 end if not isC130Auto then 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,notifyGroup or Group,true) elseif notifyGroup then self:_SendMessage(text,30,true,notifyGroup,true) else self:T(text) end end if canbuild then local notified=false local function notifyBuildStarted(buildName,etaSeconds) if notified then return end local startMsgGroup=(not Engineering and(notifyGroup or Group))or notifyGroup if isC130Auto then if startMsgGroup then local msg if etaSeconds and etaSeconds>0 then msg=string.format("CTLD: Building %s (ETA %ds).",tostring(buildName),math.floor(etaSeconds)) else msg=string.format("CTLD: Building %s.",tostring(buildName)) end self:_SendMessage(msg,15,false,startMsgGroup) end else local msg=self:_GetEntryForGroup("BUILD_STARTED",startMsgGroup) msg=string.format(msg,self.buildtime) if startMsgGroup then self:_SendMessage(msg,15,false,startMsgGroup) end end notified=true end for _,_build in pairs(buildables)do local build=_build if build.CanBuild then local required=build.Required or 1 if required<1 then required=1 end local full=math.floor((build.Found or 0)/required) if full<1 then full=1 end local sep=self.buildPairSeparation or 25 local hdg=(Unit:GetHeading()+180)%360 local lat=(hdg+90)%360 local base=Unit:GetCoordinate():Translate(20,hdg) if full==1 then local cratesNow,numberNow=self:_FindCratesNearby(Group,Unit,finddist,true,true,not Engineering) if activeSetId then cratesNow,numberNow=self:_C130DcAutoFilterCrates(cratesNow,activeSetId) end self:_CleanUpCrates(cratesNow,build,numberNow) self:_RefreshLoadCratesMenu(Group,Unit) self:_RefreshPackMenus(Group,Unit) if self.buildtime and self.buildtime>0 then local buildtimer=TIMER:New(self._BuildObjectFromCrates,self,Group,Unit,build,false,Group:GetCoordinate(),MultiDrop) buildtimer:Start(self.buildtime) notifyBuildStarted(build.Name,self.buildtime) self:__CratesBuildStarted(1,Group,Unit,build.Name) else if isC130Auto then notifyBuildStarted(build.Name,nil) end self:_BuildObjectFromCrates(Group,Unit,build,false,nil,MultiDrop) end else local start=-((full-1)*sep)/2 for n=1,full do local cratesNow,numberNow=self:_FindCratesNearby(Group,Unit,finddist,true,true,not Engineering) if activeSetId then cratesNow,numberNow=self:_C130DcAutoFilterCrates(cratesNow,activeSetId) end self:_CleanUpCrates(cratesNow,build,numberNow) self:_RefreshLoadCratesMenu(Group,Unit) self:_RefreshPackMenus(Group,Unit) local off=start+(n-1)*sep local coord=base:Translate(off,lat):GetVec2() local b={Name=build.Name,Required=build.Required,Template=build.Template,CanBuild=true,Type=build.Type,Coord=coord} if self.buildtime and self.buildtime>0 then local buildtimer=TIMER:New(self._BuildObjectFromCrates,self,Group,Unit,b,false,Group:GetCoordinate(),MultiDrop) buildtimer:Start(self.buildtime) notifyBuildStarted(build.Name,self.buildtime) self:__CratesBuildStarted(1,Group,Unit,build.Name) else if isC130Auto then notifyBuildStarted(build.Name,nil) end self:_BuildObjectFromCrates(Group,Unit,b,false,nil,MultiDrop) end end end end end end else if not Engineering then local msg=self:_GetEntryForGroup("NO_CRATES_WITHIN_PLAIN",Group) msg=string.format(msg,finddist) self:_SendMessage(msg,10,false,Group) end end return self end function CTLD:_FindPackableGroupsNearby(Group,Unit) self:T(self.lid.." _FindPackableGroupsNearby") local location=Group:GetCoordinate() if not location then return{},0 end local capabilities=self:_GetUnitCapabilities(Unit) local innerDist=(capabilities.length and capabilities.length/2)or 15 local finddist=self.PackDistance or(self.CrateDistance or 35) local zone=ZONE_RADIUS:New("CTLD_PackableZone",location:GetVec2(),finddist,false) local nearestGroups=SET_GROUP:New():FilterCoalitions("blue"):FilterZones({zone}):FilterOnce() local packable={} for _,gr in pairs(nearestGroups.Set)do if gr and gr:GetName()~=Group:GetName()then local gc=gr:GetCoordinate() if gc then local dist=location:Get2DDistance(gc) if dist>innerDist and dist<=finddist then local generic=self:GetGenericCargoObjectFromGroupName(gr:GetName()) local cargo=generic and self:_FindCratesCargoObject(generic:GetName()or generic.Name)or nil if cargo then local display=self:_FormatCargoDisplayText(self:_GetCargoDisplayName(cargo),cargo,Group) packable[#packable+1]={ group=gr, groupName=gr:GetName(), cargo=cargo, distance=dist, display=display, } end end end end end table.sort(packable,function(a,b) if a.distance~=b.distance then return a.distance0 then eventCargo=packedCargo end local packParams={from,"CratesPacked",to,Group,Unit,eventCargo} self:_call_handler("onafter","CratesPacked",packParams,"CratesPacked") self:_call_handler("OnAfter","CratesPacked",packParams,"CratesPacked") end return packedCargo,cargoEntry,true end function CTLD:_LoadPackedCratesByIds(Group,Unit,crateIds,cargoName) self:T(self.lid.." _LoadPackedCratesByIds cargoName="..(cargoName or"nil")) local grounded=not self:IsUnitInAir(Unit) local hover=self:CanHoverLoad(Unit) if not grounded and not hover then local msg=self:_GetEntryForGroup("MUST_LAND_OR_HOVER_CRATES",Group) self:_SendMessage(msg,10,false,Group) return self end if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName())then local msg=self:_GetEntryForGroup("OPEN_DOORS_LOAD_CARGO",Group) self:_SendMessage(msg,10,false,Group) return self end local idLookup={} for _,id in pairs(crateIds or{})do idLookup[id]=true end local matchingCrates={} local finddist=self.CrateDistance or 35 local location=Group:GetCoordinate() for _,crateObj in pairs(self.Spawned_Cargo or{})do if crateObj and idLookup[crateObj:GetID()]then local pos=crateObj:GetPositionable() if pos and pos:IsAlive()then local dist=location:Get2DDistance(pos:GetCoordinate()) if dist<=finddist then matchingCrates[#matchingCrates+1]=crateObj end end end end if#matchingCrates==0 then local msg=self:_GetEntryForGroup("NO_NAMED_CRATES_IN_RANGE",Group) msg=string.format(msg,cargoName or"selection") self:_SendMessage(msg,10,false,Group) self:_RefreshPackMenus(Group,Unit) return self end table.sort(matchingCrates,function(a,b)return a:GetID()=capacity then local msg=self:_GetEntryForGroup("NO_MORE_CAPACITY",Group) self:_SendMessage(msg,10,false,Group) self:_RefreshPackMenus(Group,Unit) return self end local spaceLeft=capacity-loadedData.Cratesloaded local toLoad=math.min(#matchingCrates,needed,spaceLeft) if toLoad<1 then local msg=self:_GetEntryForGroup("CANNOT_LOAD_NONE_OR_FULL",Group) self:_SendMessage(msg,10,false,Group) self:_RefreshPackMenus(Group,Unit) 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 crateIDsLoaded[#crateIDsLoaded+1]=crate:GetID() end self.Loaded_Cargo[unitName]=loadedData self:_UpdateUnitCargoMass(Unit) self:_CleanupTrackedCrates(crateIDsLoaded) local loadedHere=toLoad local displayName=self:_GetCargoDisplayNameForGroup(cargoName or(matchingCrates[1]:GetName()or"selection"),Group) if loadedHere=capacity then local msg=self:_GetEntryForGroup("LOADED_PARTIAL_LIMIT",Group) msg=string.format(msg,loadedHere,needed,displayName) self:_SendMessage(msg,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 local msg=self:_GetEntryForGroup("LOADED_FULL",Group) msg=string.format(msg,fullSets,displayName) self:_SendMessage(msg,10,false,Group) elseif fullSets>0 and leftover>0 then local msg=self:_GetEntryForGroup("LOADED_SETS_LEFTOVER",Group) msg=string.format(msg,fullSets,displayName,leftover) self:_SendMessage(msg,10,false,Group) else local msg=self:_GetEntryForGroup("LOADED_PARTIAL",Group) msg=string.format(msg,loadedHere,needed,displayName) self:_SendMessage(msg,15,false,Group) end else local msg=self:_GetEntryForGroup("LOADED_SETS",Group) msg=string.format(msg,loadedHere,displayName) self:_SendMessage(msg,10,false,Group) end end self:_RefreshLoadCratesMenu(Group,Unit) self:_RefreshDropCratesMenu(Group,Unit) self:_RefreshPackMenus(Group,Unit) if cargoName then self:_RefreshCrateQuantityMenus(Group,Unit,self:_FindCratesCargoObject(cargoName)) end return self end function CTLD:_RemovePackedCratesByIds(Group,Unit,crateIds) self:T(self.lid.." _RemovePackedCratesByIds") local idLookup={} for _,id in pairs(crateIds or{})do idLookup[id]=true end local crates={} local finddist=self.CrateDistance or 35 local location=Group:GetCoordinate() for _,entry in pairs(self.Spawned_Cargo or{})do if entry and idLookup[entry:GetID()]then local pos=entry:GetPositionable() if pos and pos:IsAlive()then local dist=location:Get2DDistance(pos:GetCoordinate()) if dist<=finddist then crates[#crates+1]=entry end end end end if#crates==0 then local msg=self:_GetEntryForGroup("NOTHING_TO_REMOVE",Group) self:_SendMessage(msg,10,false,Group) self:_RefreshPackMenus(Group,Unit) return self end local text=REPORT:New(self:_GetEntryForGroup("REPORT_REMOVING_CRATES",Group)) text:Add("------------------------------------------------------------") local removedIDs={} for _,entry in pairs(crates)do local name=self:_GetCargoDisplayNameForGroup(entry:GetName()or"none",Group) text:Add(string.format(self:_GetEntryForGroup("REPORT_ROW_CRATE_REMOVED",Group),name,entry.PerCrateMass)) local pos=entry:GetPositionable() if pos then entry.coordinate=pos:GetCoordinate() pos:Destroy(false) end removedIDs[#removedIDs+1]=entry:GetID() end text:Add("------------------------------------------------------------") self:_SendMessage(text:Text(),30,true,Group,true) local done={} for _,e in pairs(crates)do local n=e:GetName()or"none" if not done[n]then local object=self:_FindCratesCargoObject(n) if object then self:_RefreshCrateQuantityMenus(Group,Unit,object)end done[n]=true end end self:_CleanupTrackedCrates(removedIDs) self:_RefreshLoadCratesMenu(Group,Unit) self:_RefreshPackMenus(Group,Unit) self:__RemoveCratesNearby(1,Group,Unit,crates) return self end function CTLD:_PackSelectedGroupAction(Group,Unit,TargetGroupName,Mode) self:T(self.lid.." _PackSelectedGroupAction") local targetGroup=GROUP:FindByName(TargetGroupName) if not targetGroup or not targetGroup:IsAlive()then local msg=self:_GetEntryForGroup("NOTHING_TO_PACK",Group) self:_SendMessage(msg,10,false,Group) self:_RefreshPackMenus(Group,Unit) return false end local emitPackedEvent=Mode=="pack" local packedCargo,cargoEntry,ok=self:_PackSingleGroupToCrates(Group,Unit,targetGroup,emitPackedEvent) if not ok then self:_RefreshPackMenus(Group,Unit) return false end if Mode=="load"or Mode=="remove"then local crateIds={} for _,cargo in ipairs(packedCargo or{})do crateIds[#crateIds+1]=cargo:GetID() end local cargoName=cargoEntry and(cargoEntry:GetName()or cargoEntry.Name)or nil if Mode=="load"then timer.scheduleFunction(function()self:_LoadPackedCratesByIds(Group,Unit,crateIds,cargoName)end,{},timer.getTime()+1) else timer.scheduleFunction(function()self:_RemovePackedCratesByIds(Group,Unit,crateIds)end,{},timer.getTime()+1) end end return true end function CTLD:_PackCratesNearby(Group,Unit,EmitPackedEvent) self:T(self.lid.." _PackCratesNearby") local packableGroups=self:_FindPackableGroupsNearby(Group,Unit) local packedAny=false local emitPackedEvent=EmitPackedEvent~=false for _,entry in ipairs(packableGroups)do local _,_,ok=self:_PackSingleGroupToCrates(Group,Unit,entry.group,emitPackedEvent,true) if ok then packedAny=true end end if not packedAny then local msg=self:_GetEntryForGroup("NOTHING_TO_PACK",Group) self:_SendMessage(msg,10,false,Group) return false end self:_RefreshLoadCratesMenu(Group,Unit) self:_RefreshPackMenus(Group,Unit) return true 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,true) 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 local msg=self:_GetEntryForGroup("NO_CRATES_WITHIN_PLAIN",Group) msg=string.format(msg,finddist) self:_SendMessage(msg,10,false,Group,true) end end return self end function CTLD:_BuildObjectFromCrates(Group,Unit,Build,Repair,RepairLocation,MultiDrop) 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 MultiDrop and(not Repair)and canmove then local randomcoord=zone:GetRandomCoordinate(35):GetVec2() end 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() :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits) :OnSpawnGroup(function(grp)grp.spawntime=timer.getTime()end) :SpawnFromVec2(randomcoord) else self.DroppedTroops[self.TroopCounter]=SPAWN:NewWithAlias(_template,alias) :InitDelayOff() :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits) :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 self:_RefreshLoadCratesMenu(Group,Unit) self:_RefreshPackMenus(Group,Unit) else self:T(self.lid.."Group KIA while building!") end return self end function CTLD:_GetVehicleFormation() local VehicleMoveFormation=self.VehicleMoveFormation or AI.Task.VehicleFormation.VEE if type(self.VehicleMoveFormation)=="table"then VehicleMoveFormation=self.VehicleMoveFormation[math.random(1,#self.VehicleMoveFormation)] end return VehicleMoveFormation 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) self:T({canmove=outcome,name=name,zone=zone,dist=distance,max=self.movetroopsdistance}) if(distance<=self.movetroopsdistance)and outcome==true and zone~=nil then local groupname=Group:GetName() local zonecoord=zone:GetRandomCoordinate(20,125) local formation=self:_GetVehicleFormation() Group:SetAIOn() Group:OptionAlarmStateAuto() Group:OptionDisperseOnAttack(30) Group:OptionROEOpenFire() Group:RouteGroundTo(zonecoord,25,formation) 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 local pos=nowcrate:GetPositionable() if pos then pos:Destroy(false)end nowcrate.Positionable=nil nowcrate.HasBeenDropped=false end if found==numberdest then break end end self:_CleanupTrackedCrates(destIDs) return self end function CTLD:_DropAndBuild(Group,Unit) if self.nobuildinloadzones then if self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD)then local msg=self:_GetEntryForGroup("CANNOT_BUILD_LOADING_AREA",Group) self:_SendMessage(msg,10,false,Group) return self end end self:_UnloadCrates(Group,Unit) timer.scheduleFunction(function()self:_BuildCrates(Group,Unit,false,true)end,{},timer.getTime()+1) end function CTLD:_DropSingleAndBuild(Group,Unit,setIndex) if self.nobuildinloadzones then if self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD)then local msg=self:_GetEntryForGroup("CANNOT_BUILD_LOADING_AREA",Group) self:_SendMessage(msg,10,false,Group) return self end end self:_UnloadSingleCrateSet(Group,Unit,setIndex) timer.scheduleFunction(function()self:_BuildCrates(Group,Unit,false)end,{},timer.getTime()+1) end function CTLD:_PackAndLoad(Group,Unit) if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName())then local msg=self:_GetEntryForGroup("OPEN_DOORS_LOAD_CARGO",Group) self:_SendMessage(msg,10,false,Group) return self end if not self:_PackCratesNearby(Group,Unit,false)then return self end timer.scheduleFunction(function()self:_LoadCratesNearby(Group,Unit)end,{},timer.getTime()+1) return self end function CTLD:_PackAndRemove(Group,Unit) if not self:_PackCratesNearby(Group,Unit)then return self end timer.scheduleFunction(function()self:_RemoveCratesNearby(Group,Unit)end,{},timer.getTime()+1) return self end function CTLD:_GetAndLoad(Group,Unit,cargoObj,quantity,LoadAnyWay) if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName())then local msg=self:_GetEntryForGroup("OPEN_DOORS_LOAD_CARGO",Group) self:_SendMessage(msg,10,false,Group) return self end local needed=cargoObj and cargoObj:GetCratesNeeded()or 1 local count=math.max(1,tonumber(quantity)or 1) local capacitySets=nil local cap=self:_GetUnitCapabilities(Unit) local limit=cap and cap.cratelimit or 0 if limit>0 then local ld=self.Loaded_Cargo and self.Loaded_Cargo[Unit:GetName()]or nil local loaded=(ld and type(ld.Cratesloaded)=="number")and ld.Cratesloaded or 0 local space=limit-loaded if space<0 then space=0 end local perSet=needed>0 and needed or 1 capacitySets=math.floor(space/perSet) if capacitySets<1 and not LoadAnyWay then local msg=self:_GetEntryForGroup("NO_CAPACITY_NOW",Group) self:_SendMessage(msg,10,false,Group) return self end if capacitySets<1 and LoadAnyWay then count=1 elseif count>capacitySets then count=capacitySets end end local inzone=self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) if not inzone then local ship=nil local width=20 local distance=nil local zone=nil inzone,ship,zone,distance,width=self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP) end if not inzone then local msg=self:_GetEntryForGroup("NOT_CLOSE_ENOUGH_LOGISTICS",Group) self:_SendMessage(msg,10,false,Group) return self end local total=needed*count local ok=self:_GetCrates(Group,Unit,cargoObj,total,false,false,true,true) if ok then local uname=Unit:GetName() self._batchCrateLoad=self._batchCrateLoad or{} self._batchCrateLoad[uname]={remaining=count,group=Group,cname=cargoObj.Name,loaded=0,partials=0} local details=(LoadAnyWay==true) for i=1,count do timer.scheduleFunction(function()self:_LoadSingleCrateSet(Group,Unit,cargoObj.Name,details)end,{},timer.getTime()+0.2*i) end end return self end function CTLD:_GetAllAndLoad(Group,Unit) if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName())then local msg=self:_GetEntryForGroup("OPEN_DOORS_LOAD_CARGO",Group) self:_SendMessage(msg,10,false,Group) return self end timer.scheduleFunction(function()self:_LoadCratesNearby(Group,Unit)end,{},timer.getTime()+1) end function CTLD:_GetCrateStockEntry(cargoObj,stockSummary) if not cargoObj or not stockSummary then return nil end local name=cargoObj:GetName() if not name then return nil end return stockSummary[name] end function CTLD:_FormatCrateStockSuffix(cargoObj,stockSummary) if not cargoObj then return nil end local stockEntry=self:_GetCrateStockEntry(cargoObj,stockSummary) local available=nil if stockEntry and type(stockEntry.Stock)=="number"then available=stockEntry.Stock end if type(available)~="number"then local direct=cargoObj:GetStock() if type(direct)=="number"then available=direct end end if type(available)~="number"or available<0 then return nil end local rounded=math.floor(available+0.5) local total=nil if stockEntry and type(stockEntry.Stock0)=="number"and stockEntry.Stock0>=0 then total=math.floor(stockEntry.Stock0+0.5) elseif stockEntry and type(stockEntry.Sum)=="number"and stockEntry.Sum>=0 then total=math.floor(stockEntry.Sum+0.5) end if type(total)~="number"then local baseTotal=cargoObj.GetStock0 and cargoObj:GetStock0()or nil if type(baseTotal)=="number"and baseTotal>=0 then total=math.floor(baseTotal+0.5) end end if type(total)=="number"and total>0 and total~=rounded then return string.format("[%d/%d]",rounded,total) else return string.format("[%d]",rounded) end end function CTLD:_RefreshCrateQuantityMenus(Group,Unit,CargoObj) if not Group and Unit then Group=Unit:GetGroup()end if Group and Unit then local uname=Unit:GetName()or"none" self._qtySnap=self._qtySnap or{} self._qtySnap[uname]=self._qtySnap[uname]or{} if Group.CTLD_CrateMenus then local present={} for item,_ in pairs(Group.CTLD_CrateMenus)do present["C:"..tostring(item)]=true end for key,_ in pairs(self._qtySnap[uname])do if string.sub(key,1,2)=="C:"and not present[key]then self._qtySnap[uname][key]=nil end end local stockSummary=self.showstockinmenuitems and self:_CountStockPlusInHeloPlusAliveGroups(false)or nil for item,menu in pairs(Group.CTLD_CrateMenus)do menu:RemoveSubMenus() local obj=self:_FindCratesCargoObject(item) if obj then self:_AddCrateQuantityMenus(Group,Unit,menu,obj,stockSummary)end end end end if CargoObj and Group and Unit then local uname=Unit:GetName()or"none" local cap=(self:_GetUnitCapabilities(Unit).cratelimit or 0) local loaded=(self.Loaded_Cargo[uname]and self.Loaded_Cargo[uname].Cratesloaded)or 0 local avail=math.max(0,cap-loaded) local per=CargoObj:GetCratesNeeded()or 1 if per<1 then per=1 end local unitAvail=math.max(0,math.min(self.maxCrateMenuQuantity or 1,math.floor(avail/per))) local s=CargoObj:GetStock() self._qtySnap=self._qtySnap or{} self._qtySnap[uname]=self._qtySnap[uname]or{} local k="C:"..(CargoObj:GetName()or"none") local snap=tostring(type(s)=="number"and s or-1)..":"..tostring(unitAvail) if self._qtySnap[uname][k]~=snap then self._qtySnap[uname][k]=snap if type(s)=="number"and s>=0 and s=0 and s=0 and s=0 and s1 or(subcatcount==1 and onlycat~="Other") if useTroopSubcats then for catName,_ in pairs(self.subcatsTroop)do subcatmenus[catName]=MENU_GROUP:New(_group,catName,troopsmenu) end end for _,cargoObj in pairs(self.Cargo_Troops)do if not cargoObj.DontShowInMenu then local menutext=self:_FormatCargoDisplayText(self:_GetCargoDisplayName(cargoObj),cargoObj,_group) local parent=troopsmenu if useTroopSubcats and cargoObj.Subcategory and subcatmenus[cargoObj.Subcategory]then parent=subcatmenus[cargoObj.Subcategory] end local mSet=MENU_GROUP:New(_group,menutext,parent) _group.CTLD_TroopMenus[cargoObj.Name]=mSet self:_AddTroopQuantityMenus(_group,_unit,mSet,cargoObj) end end else for _,cargoObj in pairs(self.Cargo_Troops)do if not cargoObj.DontShowInMenu then local menutext=self:_FormatCargoDisplayText(self:_GetCargoDisplayName(cargoObj),cargoObj,_group) local mSet=MENU_GROUP:New(_group,menutext,troopsmenu) _group.CTLD_TroopMenus[cargoObj.Name]=mSet self:_AddTroopQuantityMenus(_group,_unit,mSet,cargoObj) end end end local dropTroopsMenu=MENU_GROUP:New(_group,self:_GetEntryForGroup("MENU_DROP_TROOPS",_group),toptroops):Refresh() if self.maxUnloadTroopsAllowed==-1 then MENU_GROUP_COMMAND:New(_group,self:_GetEntryForGroup("MENU_DROP_ALL_TROOPS",_group),dropTroopsMenu,self._UnloadTroops,self,_group,_unit):Refresh() end MENU_GROUP_COMMAND:New(_group,self:_GetEntryForGroup("MENU_EXTRACT_TROOPS",_group),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=self:_GetCargoDisplayName(cargoObj) 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,self:_GetEntryForGroup("MENU_MANAGE_CRATES",_group),topmenu) _group.MyTopCratesMenu=topcrates local cratesmenu=MENU_GROUP:New(_group,self:_GetEntryForGroup("MENU_GET_CRATES",_group),topcrates) if self.onestepmenu then _group.CTLD_CrateMenus={} local crateStockSummary=nil if self.showstockinmenuitems then crateStockSummary=self:_CountStockPlusInHeloPlusAliveGroups(false) end local function addCrateMenuEntry(cargoObj,parentMenu,subcatmenus) if cargoObj.DontShowInMenu then return end local isStaticCargo=false if(cargoObj.GetType and cargoObj:GetType()==CTLD_CARGO.Enum.STATIC)or cargoObj.CargoType==CTLD_CARGO.Enum.STATIC then isStaticCargo=true end if isStaticCargo and cargoObj.UnitCanCarry and not cargoObj:UnitCanCarry(_unit)then return end local parent=parentMenu if subcatmenus and cargoObj.Subcategory then parent=subcatmenus[cargoObj.Subcategory] if not parent then parent=MENU_GROUP:New(_group,cargoObj.Subcategory,cratesmenu) subcatmenus[cargoObj.Subcategory]=parent end end local needed=cargoObj:GetCratesNeeded()or 1 local txt local cargoLabel=self:_FormatCargoDisplayText(self:_GetCargoDisplayName(cargoObj),cargoObj,_group) if needed>1 then local plural=self:_GetMenuPluralSuffix(needed,"crate",_group) txt=string.format(self:_GetEntryForGroup("MENU_CRATES_NEEDED",_group),needed,plural,cargoLabel,cargoObj.PerCrateMass or 0) else txt=string.format(self:_GetEntryForGroup("MENU_CRATE_SINGLE",_group),cargoLabel,cargoObj.PerCrateMass or 0) end if cargoObj.Location then txt=txt.."[R]"end if self.showstockinmenuitems then local suffix=self:_FormatCrateStockSuffix(cargoObj,crateStockSummary) if suffix then txt=txt..suffix end end local mSet=MENU_GROUP:New(_group,txt,parent) _group.CTLD_CrateMenus[cargoObj.Name]=mSet self:_AddCrateQuantityMenus(_group,_unit,mSet,cargoObj,crateStockSummary) end if self.usesubcats then local subcatmenus={} for _,cargoObj in pairs(self.Cargo_Crates)do addCrateMenuEntry(cargoObj,cratesmenu,subcatmenus) end for _,cargoObj in pairs(self.Cargo_Statics)do addCrateMenuEntry(cargoObj,cratesmenu,subcatmenus) end else for _,cargoObj in pairs(self.Cargo_Crates)do addCrateMenuEntry(cargoObj,cratesmenu) end for _,cargoObj in pairs(self.Cargo_Statics)do addCrateMenuEntry(cargoObj,cratesmenu) end end else if self.usesubcats==true then local subcatmenus={} local function getSubcatMenu(catName) if not catName then return cratesmenu end if not subcatmenus[catName]then subcatmenus[catName]=MENU_GROUP:New(_group,catName,cratesmenu) end return subcatmenus[catName] end for _,cargoObj in pairs(self.Cargo_Crates)do if not cargoObj.DontShowInMenu then local needed=cargoObj:GetCratesNeeded()or 1 local txt local cargoLabel=self:_FormatCargoDisplayText(self:_GetCargoDisplayName(cargoObj),cargoObj,_group) if needed>1 then local plural=self:_GetMenuPluralSuffix(needed,"crate",_group) txt=string.format(self:_GetEntryForGroup("MENU_CRATES_NEEDED",_group),needed,plural,cargoLabel,cargoObj.PerCrateMass or 0) else txt=string.format(self:_GetEntryForGroup("MENU_CRATE_SINGLE",_group),cargoLabel,cargoObj.PerCrateMass or 0) end 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,getSubcatMenu(cargoObj.Subcategory),self._GetCrates,self,_group,_unit,cargoObj) end end for _,cargoObj in pairs(self.Cargo_Statics)do if(not cargoObj.DontShowInMenu)and(not cargoObj.UnitCanCarry or cargoObj:UnitCanCarry(_unit))then local needed=cargoObj:GetCratesNeeded()or 1 local txt local cargoLabel=self:_FormatCargoDisplayText(self:_GetCargoDisplayName(cargoObj),cargoObj,_group) if needed>1 then local plural=self:_GetMenuPluralSuffix(needed,"crate",_group) txt=string.format(self:_GetEntryForGroup("MENU_CRATES_NEEDED",_group),needed,plural,cargoLabel,cargoObj.PerCrateMass or 0) else txt=string.format(self:_GetEntryForGroup("MENU_CRATE_SINGLE",_group),cargoLabel,cargoObj.PerCrateMass or 0) end 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,getSubcatMenu(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 needed=cargoObj:GetCratesNeeded()or 1 local txt local cargoLabel=self:_FormatCargoDisplayText(self:_GetCargoDisplayName(cargoObj),cargoObj,_group) if needed>1 then local plural=self:_GetMenuPluralSuffix(needed,"crate",_group) txt=string.format(self:_GetEntryForGroup("MENU_CRATES_NEEDED",_group),needed,plural,cargoLabel,cargoObj.PerCrateMass or 0) else txt=string.format(self:_GetEntryForGroup("MENU_CRATE_SINGLE",_group),cargoLabel,cargoObj.PerCrateMass or 0) end 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)and(not cargoObj.UnitCanCarry or cargoObj:UnitCanCarry(_unit))then local needed=cargoObj:GetCratesNeeded()or 1 local txt local cargoLabel=self:_FormatCargoDisplayText(self:_GetCargoDisplayName(cargoObj),cargoObj,_group) if needed>1 then local plural=self:_GetMenuPluralSuffix(needed,"crate",_group) txt=string.format(self:_GetEntryForGroup("MENU_CRATES_NEEDED",_group),needed,plural,cargoLabel,cargoObj.PerCrateMass or 0) else txt=string.format(self:_GetEntryForGroup("MENU_CRATE_SINGLE",_group),cargoLabel,cargoObj.PerCrateMass or 0) end 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 end local loadCratesMenu=MENU_GROUP:New(_group,self:_GetEntryForGroup("MENU_LOAD_CRATES",_group),topcrates) _group.MyLoadCratesMenu=loadCratesMenu MENU_GROUP_COMMAND:New(_group,self:_GetEntryForGroup("MENU_LOAD_ALL",_group),loadCratesMenu,self._LoadCratesNearby,self,_group,_unit) MENU_GROUP_COMMAND:New(_group,self:_GetEntryForGroup("MENU_SHOW_LOADABLE_CRATES",_group),loadCratesMenu,self._RefreshLoadCratesMenu,self,_group,_unit) local dropCratesMenu=MENU_GROUP:New(_group,self:_GetEntryForGroup("MENU_DROP_CRATES",_group),topcrates) topcrates.DropCratesMenu=dropCratesMenu if not self.nobuildmenu then MENU_GROUP_COMMAND:New(_group,self:_GetEntryForGroup("MENU_BUILD_CRATES",_group),topcrates,self._BuildCrates,self,_group,_unit) MENU_GROUP_COMMAND:New(_group,self:_GetEntryForGroup("MENU_REPAIR",_group),topcrates,self._RepairCrates,self,_group,_unit):Refresh() end local removecratesmenu=MENU_GROUP:New(_group,self:_GetEntryForGroup("MENU_REMOVE_CRATES",_group),topcrates) MENU_GROUP_COMMAND:New(_group,self:_GetEntryForGroup("MENU_REMOVE_CRATES_NEARBY",_group),removecratesmenu,self._RemoveCratesNearby,self,_group,_unit) if self.onestepmenu then topcrates.PackRootMenu=MENU_GROUP:New(_group,self:_GetEntryForGroup("MENU_PACK",_group),topcrates) topcrates.PackMenu=MENU_GROUP:New(_group,self:_GetEntryForGroup("MENU_PACK",_group),topcrates.PackRootMenu) local showPackAndLoad=not(self.UseC130LoadAndUnload and self:IsC130J(_unit)) if showPackAndLoad then topcrates.PackAndLoadMenu=MENU_GROUP:New(_group,self:_GetEntryForGroup("MENU_PACK_AND_LOAD",_group),topcrates.PackRootMenu) end topcrates.PackAndRemoveMenu=MENU_GROUP:New(_group,self:_GetEntryForGroup("MENU_PACK_AND_REMOVE",_group),topcrates.PackRootMenu) MENU_GROUP_COMMAND:New(_group,self:_GetEntryForGroup("MENU_LIST_CRATES_NEARBY",_group),topcrates,self._ListCratesNearby,self,_group,_unit) else MENU_GROUP_COMMAND:New(_group,self:_GetEntryForGroup("MENU_PACK_CRATES",_group),topcrates,self._PackCratesNearby,self,_group,_unit) MENU_GROUP_COMMAND:New(_group,self:_GetEntryForGroup("MENU_LIST_CRATES_NEARBY",_group),topcrates,self._ListCratesNearby,self,_group,_unit) end 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() local cdisplay=self:_FormatCargoDisplayText(self:_GetCargoDisplayName(cgo),cgo,_group) cargoByName[cname]=cargoByName[cname]or{count=0,needed=cneeded,display=cdisplay} cargoByName[cname].count=cargoByName[cname].count+1 end end for name,info in pairs(cargoByName)do local line=string.format("Drop %s (%d/%d)",info.display or name,info.count,info.needed) MENU_GROUP_COMMAND:New(_group,line,dropCratesMenu,self._UnloadSingleCrateSet,self,_group,_unit,name) end end end if self:IsC130J(_unit)then local topunits=MENU_GROUP:New(_group,self:_GetEntryForGroup("MENU_MANAGE_UNITS",_group),topmenu) local getunits=MENU_GROUP:New(_group,self:_GetEntryForGroup("MENU_GET_UNITS",_group),topunits) MENU_GROUP_COMMAND:New(_group,self:_GetEntryForGroup("MENU_REMOVE_UNITS_NEARBY",_group),topunits,self._C130RemoveUnitsNearby,self,_group,_unit) local unitentries=self.C130GetUnits or{} local unittype=_unit:GetTypeName()or"none" local subcatmenus=self.usesubcats and{}or nil for _,cargoObj in ipairs(unitentries)do local ok=true if cargoObj.UnitTypes then ok=false if type(cargoObj.UnitTypes)=="string"then if unittype==cargoObj.UnitTypes then ok=true end else for _,ut in pairs(cargoObj.UnitTypes)do if unittype==ut then ok=true break end end end end if ok and(not cargoObj.Stock or cargoObj.Stock==-1 or cargoObj.Stock>0)then local parent=getunits if self.usesubcats==true and cargoObj.SubCategory then local sub=subcatmenus[cargoObj.SubCategory] if not sub then sub=MENU_GROUP:New(_group,cargoObj.SubCategory,getunits) subcatmenus[cargoObj.SubCategory]=sub end parent=sub end local menutext=self:_FormatCargoDisplayText(self:_GetCargoDisplayName(cargoObj),cargoObj,_group) if type(cargoObj.Stock)=="number"and cargoObj.Stock>=0 and self.showstockinmenuitems then menutext=menutext.."["..cargoObj.Stock.."]" end MENU_GROUP_COMMAND:New(_group,menutext,parent,self._C130GetUnits,self,_group,_unit,cargoObj.Name) end end end MENU_GROUP_COMMAND:New(_group,self:_GetEntryForGroup("MENU_LIST_BOARDED_CARGO",_group),topmenu,self._ListCargo,self,_group,_unit) MENU_GROUP_COMMAND:New(_group,self:_GetEntryForGroup("MENU_INVENTORY",_group),topmenu,self._ListInventory,self,_group,_unit) MENU_GROUP_COMMAND:New(_group,self:_GetEntryForGroup("MENU_LIST_ZONE_BEACONS",_group),topmenu,self._ListRadioBeacons,self,_group,_unit) local smoketopmenu=MENU_GROUP:New(_group,self:_GetEntryForGroup("MENU_SMOKES_FLARES_BEACONS",_group),topmenu) MENU_GROUP_COMMAND:New(_group,self:_GetEntryForGroup("MENU_SMOKE_ZONES_NEARBY",_group),smoketopmenu,self.SmokeZoneNearBy,self,_unit,false) local smokeself=MENU_GROUP:New(_group,self:_GetEntryForGroup("MENU_DROP_SMOKE_NOW",_group),smoketopmenu) MENU_GROUP_COMMAND:New(_group,self:_GetEntryForGroup("MENU_RED_SMOKE",_group),smokeself,self.SmokePositionNow,self,_unit,false,SMOKECOLOR.Red) MENU_GROUP_COMMAND:New(_group,self:_GetEntryForGroup("MENU_BLUE_SMOKE",_group),smokeself,self.SmokePositionNow,self,_unit,false,SMOKECOLOR.Blue) MENU_GROUP_COMMAND:New(_group,self:_GetEntryForGroup("MENU_GREEN_SMOKE",_group),smokeself,self.SmokePositionNow,self,_unit,false,SMOKECOLOR.Green) MENU_GROUP_COMMAND:New(_group,self:_GetEntryForGroup("MENU_ORANGE_SMOKE",_group),smokeself,self.SmokePositionNow,self,_unit,false,SMOKECOLOR.Orange) MENU_GROUP_COMMAND:New(_group,self:_GetEntryForGroup("MENU_WHITE_SMOKE",_group),smokeself,self.SmokePositionNow,self,_unit,false,SMOKECOLOR.White) MENU_GROUP_COMMAND:New(_group,self:_GetEntryForGroup("MENU_FLARE_ZONES_NEARBY",_group),smoketopmenu,self.SmokeZoneNearBy,self,_unit,true) MENU_GROUP_COMMAND:New(_group,self:_GetEntryForGroup("MENU_FIRE_FLARE_NOW",_group),smoketopmenu,self.SmokePositionNow,self,_unit,true) MENU_GROUP_COMMAND:New(_group,self:_GetEntryForGroup("MENU_DROP_BEACON_NOW",_group),smoketopmenu,self.DropBeaconNow,self,_unit):Refresh() if self:IsFixedWing(_unit)then MENU_GROUP_COMMAND:New(_group,self:_GetEntryForGroup("MENU_SHOW_FLIGHT_PARAMS",_group),topmenu,self._ShowFlightParams,self,_group,_unit):Refresh() else MENU_GROUP_COMMAND:New(_group,self:_GetEntryForGroup("MENU_SHOW_HOVER_PARAMS",_group),topmenu,self._ShowHoverParams,self,_group,_unit):Refresh() end self.MenusDone[_unitName]=true self:_RefreshLoadCratesMenu(_group,_unit) self:_RefreshPackMenus(_group,_unit) self:_RefreshDropCratesMenu(_group,_unit) if firstBuild then menucount=menucount+1 end if firstBuild and not self.showstockinmenuitems then self:_RefreshQuantityMenusForGroup(_group,_unit)end 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() if self:IsC130J(Unit)then MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_USE_C130_LOAD",Group),Group.MyLoadCratesMenu,function()end) return end 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,self:_GetEntryForGroup("MENU_NO_CRATES_FOUND_RESCAN",Group),Group.MyLoadCratesMenu,function()self:_RefreshLoadCratesMenu(Group,Unit)end) return end MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_LOAD_ALL",Group),Group.MyLoadCratesMenu,self._LoadCratesNearby,self,Group,Unit) local cargoByName={} for _,crate in pairs(nearby)do local name=crate:GetName() cargoByName[name]=cargoByName[name]or{} table.insert(cargoByName[name],crate) end 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 label=string.format("%d. %s %s",lineIndex,loadkey,cName) i=i+needed else label=string.format("%d. %s %s (%d/%d)",lineIndex,loadkey,cName,left,needed) i=#list+1 end MENU_GROUP_COMMAND:New(Group,label,Group.MyLoadCratesMenu,self._LoadSingleCrateSet,self,Group,Unit,cName) lineIndex=lineIndex+1 end end end function CTLD:_RefreshPackMenus(Group,Unit) if not self.onestepmenu then return end if not Group.CTLDTopmenu then return end local topCrates=Group.MyTopCratesMenu if not topCrates then return end if not topCrates.PackRootMenu and not topCrates.PackMenu and not topCrates.PackAndLoadMenu and not topCrates.PackAndRemoveMenu then return end local packableGroups,n=self:_FindPackableGroupsNearby(Group,Unit) local function refreshPackMenu(menu,mode,allKey,bulkFunc) if not menu then return end menu:RemoveSubMenus() if n>0 then for idx,entry in ipairs(packableGroups)do local label=string.format("%d. %s (%dm)",idx,entry.display or entry.groupName,math.floor((entry.distance or 0)+0.5)) MENU_GROUP_COMMAND:New(Group,label,menu,self._PackSelectedGroupAction,self,Group,Unit,entry.groupName,mode) end end MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup(allKey,Group),menu,bulkFunc,self,Group,Unit) MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_SCAN_PACKABLE_UNITS",Group),menu,self._RefreshPackMenus,self,Group,Unit) end refreshPackMenu(topCrates.PackMenu,"pack","MENU_PACK_ALL",self._PackCratesNearby) if topCrates.PackAndLoadMenu then refreshPackMenu(topCrates.PackAndLoadMenu,"load","MENU_PACK_AND_LOAD_ALL",self._PackAndLoad) end refreshPackMenu(topCrates.PackAndRemoveMenu,"remove","MENU_PACK_AND_REMOVE_ALL",self._PackAndRemove) end function CTLD:_LoadSingleCrateSet(Group,Unit,cargoName,details) 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 local msg=self:_GetEntryForGroup("MUST_LAND_OR_HOVER_CRATES",Group) self:_SendMessage(msg,10,false,Group) return self end if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName())then local msg=self:_GetEntryForGroup("OPEN_DOORS_LOAD_CARGO",Group) self:_SendMessage(msg,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 local msg=self:_GetEntryForGroup("NO_CRATES_IN_RANGE",Group) self:_SendMessage(msg,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 local msg=self:_GetEntryForGroup("NO_NAMED_CRATES_IN_RANGE",Group) msg=string.format(msg,self:_GetCargoDisplayNameForGroup(cargoName,Group)) self:_SendMessage(msg,10,false,Group) return self end local found=#matchingCrates local batch=self._batchCrateLoad and self._batchCrateLoad[Unit:GetName()]or nil local prevSuppress=self.suppressmessages if batch and(not details)and batch.cname==cargoName then self.suppressmessages=true end 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 local msg=self:_GetEntryForGroup("NO_MORE_CAPACITY",Group) self:_SendMessage(msg,10,false,Group) self.suppressmessages=prevSuppress return self end local spaceLeft=capacity-loadedData.Cratesloaded local toLoad=math.min(found,needed,spaceLeft) if toLoad<1 then local msg=self:_GetEntryForGroup("CANNOT_LOAD_NONE_OR_FULL",Group) self:_SendMessage(msg,10,false,Group) self.suppressmessages=prevSuppress 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 details or(not batch)then if loadedHere=capacity then local msg=self:_GetEntryForGroup("LOADED_PARTIAL_LIMIT",Group) msg=string.format(msg,loadedHere,needed,self:_GetCargoDisplayNameForGroup(cargoName,Group)) self:_SendMessage(msg,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 local msg=self:_GetEntryForGroup("LOADED_FULL",Group) msg=string.format(msg,fullSets,self:_GetCargoDisplayNameForGroup(cargoName,Group)) self:_SendMessage(msg,10,false,Group) elseif fullSets>0 and leftover>0 then local msg=self:_GetEntryForGroup("LOADED_SETS_LEFTOVER",Group) msg=string.format(msg,fullSets,self:_GetCargoDisplayNameForGroup(cargoName,Group),leftover) self:_SendMessage(msg,10,false,Group) else local msg=self:_GetEntryForGroup("LOADED_PARTIAL",Group) msg=string.format(msg,loadedHere,needed,self:_GetCargoDisplayNameForGroup(cargoName,Group)) self:_SendMessage(msg,15,false,Group) end else local msg=self:_GetEntryForGroup("LOADED_SETS",Group) msg=string.format(msg,loadedHere,self:_GetCargoDisplayNameForGroup(cargoName,Group)) self:_SendMessage(msg,10,false,Group) end end end self:_RefreshLoadCratesMenu(Group,Unit) self:_RefreshDropCratesMenu(Group,Unit) self:_RefreshPackMenus(Group,Unit) self:_RefreshCrateQuantityMenus(Group,Unit,self:_FindCratesCargoObject(cargoName)) if batch and batch.cname==cargoName then local setsLoaded=math.floor((loadedHere or 0)/(needed or 1)) batch.loaded=(batch.loaded or 0)+(setsLoaded or 0) if loadedHere<(needed or 1)then batch.partials=(batch.partials or 0)+1 end batch.remaining=(batch.remaining or 1)-1 if batch.remaining<=0 then self.suppressmessages=prevSuppress if not details then local txt=string.format(self:_GetEntryForGroup("LOADED_BATCH",batch.group),batch.loaded,self:_GetCargoDisplayNameForGroup(cargoName,batch.group)) if batch.partials and batch.partials>0 then txt=txt.." "..self:_GetEntryForGroup("LOADED_BATCH_PARTIAL",batch.group) end self:_SendMessage(txt,10,false,batch.group) end self._batchCrateLoad[Unit:GetName()]=nil else self.suppressmessages=prevSuppress end end 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 local msg=self:_GetEntryForGroup("NOT_CLOSE_ENOUGH_DROP",Group) self:_SendMessage(msg,10,false,Group) if not self.debug then return self end end end if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName())then local msg=self:_GetEntryForGroup("OPEN_DOORS_DROP_CARGO",Group) self:_SendMessage(msg,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 local msg=self:_GetEntryForGroup("NO_CRATE_GROUPS",Group) self:_SendMessage(msg,10,false,Group) if not self.debug then return self end return self end local chunk=self.CrateGroupList[unitName][setIndex] if not chunk then local msg=self:_GetEntryForGroup("NO_CRATE_SET",Group) self:_SendMessage(msg,10,false,Group) if not self.debug then return self end return self end if#chunk==0 then local msg=self:_GetEntryForGroup("NO_CRATE_IN_SET",Group) self:_SendMessage(msg,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 local msg=self:_GetEntryForGroup("NOTHING_LOADED_AIRDROP",Group) self:_SendMessage(msg,10,false,Group) else local msg=self:_GetEntryForGroup("NOTHING_LOADED_HOVER",Group) self:_SendMessage(msg,10,false,Group) end if not self.debug then return self end return self end local crateObj=chunk[1] if not crateObj then local msg=self:_GetEntryForGroup("NO_CRATE_IN_SET",Group) self:_SendMessage(msg,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 cname=crateObj:GetName()or"Unknown" local count=#chunk if needed>1 then if count==needed then local msg=self:_GetEntryForGroup("DROPPED_FULL",Group) msg=string.format(msg,1,self:_GetCargoDisplayNameForGroup(cname,Group)) self:_SendMessage(msg,10,false,Group) else local msg=self:_GetEntryForGroup("DROPPED_PARTIAL",Group) msg=string.format(msg,count,needed,self:_GetCargoDisplayNameForGroup(cname,Group)) self:_SendMessage(msg,15,false,Group) end else local msg=self:_GetEntryForGroup("DROPPED_SETS",Group) msg=string.format(msg,count,self:_GetCargoDisplayNameForGroup(cname,Group)) self:_SendMessage(msg,10,false,Group) 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) self:_RefreshPackMenus(Group,Unit) self:_RefreshCrateQuantityMenus(Group,Unit,nil) 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,self:_GetEntryForGroup("MENU_DROP_CRATES",Group),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,self:_GetEntryForGroup("MENU_NO_CRATES_TO_DROP",Group),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,self:_GetEntryForGroup("MENU_NO_CRATES_TO_DROP",Group),dropCratesMenu,function()end) return end if not self.onestepmenu then MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_DROP_ALL_CRATES",Group),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()0 and needed or 1)) if sets>0 then local parentLabel=string.format("%d. %s (%d SET)",lineIndex,cName,sets) local parentMenu=MENU_GROUP:New(Group,parentLabel,dropCratesMenu) for s=1,sets do local chunk={} for n=i,i+needed-1 do table.insert(chunk,list[n])end table.insert(self.CrateGroupList[Unit:GetName()],chunk) i=i+needed end if sets==1 then MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_DROP",Group),parentMenu,function(selfArg,GroupArg,UnitArg,cNameArg,neededArg,qty) local uName=UnitArg:GetName() for k=1,qty do local lst=selfArg.CrateGroupList and selfArg.CrateGroupList[uName] if not lst then break end local idx=nil for j=1,#lst do local ch=lst[j] local first=ch and ch[1] if first and(not first:WasDropped())and first:GetName()==cNameArg and#ch>=neededArg then idx=j break end end if not idx then break end selfArg:_UnloadSingleCrateSet(GroupArg,UnitArg,idx) end end,self,Group,Unit,cName,needed,1) else for q=1,sets do local qm=MENU_GROUP:New(Group,string.format(self:_GetEntryForGroup("MENU_DROP_N_SETS",Group),q,self:_GetMenuPluralSuffix(q,"set",Group)),parentMenu) MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_DROP",Group),qm,function(selfArg,GroupArg,UnitArg,cNameArg,neededArg,qty) local uName=UnitArg:GetName() for k=1,qty do local lst=selfArg.CrateGroupList and selfArg.CrateGroupList[uName] if not lst then break end local idx=nil for j=1,#lst do local ch=lst[j] local first=ch and ch[1] if first and(not first:WasDropped())and first:GetName()==cNameArg and#ch>=neededArg then idx=j break end end if not idx then break end selfArg:_UnloadSingleCrateSet(GroupArg,UnitArg,idx) end end,self,Group,Unit,cName,needed,q) end end lineIndex=lineIndex+1 end if i<=#list then local left=#list-i+1 local chunk={} for n=i,#list do table.insert(chunk,list[n])end table.insert(self.CrateGroupList[Unit:GetName()],chunk) local setIndex=#self.CrateGroupList[Unit:GetName()] local label=string.format("%d. %s %d/%d",lineIndex,cName,left,needed) MENU_GROUP_COMMAND:New(Group,label,dropCratesMenu,self._UnloadSingleCrateSet,self,Group,Unit,setIndex) lineIndex=lineIndex+1 end end else local mAll=MENU_GROUP:New(Group,self:_GetEntryForGroup("MENU_DROP_ALL_CRATES",Group),dropCratesMenu) MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_DROP",Group),mAll,self._UnloadCrates,self,Group,Unit) if not(self:IsUnitInAir(Unit)and self:IsFixedWing(Unit))then MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_DROP_AND_BUILD",Group),mAll,self._DropAndBuild,self,Group,Unit) end 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()0 and needed or 1)) if sets>0 then local parentLabel=string.format("%d. %s (%d SET)",lineIndex,cName,sets) local parentMenu=MENU_GROUP:New(Group,parentLabel,dropCratesMenu) for s=1,sets do local chunk={} for n=i,i+needed-1 do table.insert(chunk,list[n])end table.insert(self.CrateGroupList[Unit:GetName()],chunk) i=i+needed end if sets==1 then MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_DROP",Group),parentMenu,function(selfArg,GroupArg,UnitArg,cNameArg,neededArg,qty) local uName=UnitArg:GetName() for k=1,qty do local lst=selfArg.CrateGroupList and selfArg.CrateGroupList[uName] if not lst then break end local idx=nil for j=1,#lst do local ch=lst[j] local first=ch and ch[1] if first and(not first:WasDropped())and first:GetName()==cNameArg and#ch>=neededArg then idx=j break end end if not idx then break end selfArg:_UnloadSingleCrateSet(GroupArg,UnitArg,idx) end end,self,Group,Unit,cName,needed,1) if not(self:IsUnitInAir(Unit)and self:IsFixedWing(Unit))then MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_DROP_AND_BUILD",Group),parentMenu,function(selfArg,GroupArg,UnitArg,cNameArg,neededArg,qty) local uName=UnitArg:GetName() for k=1,qty do local lst=selfArg.CrateGroupList and selfArg.CrateGroupList[uName] if not lst then break end local idx=nil for j=1,#lst do local ch=lst[j] local first=ch and ch[1] if first and(not first:WasDropped())and first:GetName()==cNameArg and#ch>=neededArg then idx=j break end end if not idx then break end selfArg:_UnloadSingleCrateSet(GroupArg,UnitArg,idx) end selfArg:_BuildCrates(GroupArg,UnitArg) end,self,Group,Unit,cName,needed,1) end else for q=1,sets do local qm=MENU_GROUP:New(Group,string.format(self:_GetEntryForGroup("MENU_DROP_N_SETS",Group),q,self:_GetMenuPluralSuffix(q,"set",Group)),parentMenu) MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_DROP",Group),qm,function(selfArg,GroupArg,UnitArg,cNameArg,neededArg,qty) local uName=UnitArg:GetName() for k=1,qty do local lst=selfArg.CrateGroupList and selfArg.CrateGroupList[uName] if not lst then break end local idx=nil for j=1,#lst do local ch=lst[j] local first=ch and ch[1] if first and(not first:WasDropped())and first:GetName()==cNameArg and#ch>=neededArg then idx=j break end end if not idx then break end selfArg:_UnloadSingleCrateSet(GroupArg,UnitArg,idx) end end,self,Group,Unit,cName,needed,q) if not(self:IsUnitInAir(Unit)and self:IsFixedWing(Unit))then MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_DROP_AND_BUILD",Group),qm,function(selfArg,GroupArg,UnitArg,cNameArg,neededArg,qty) local uName=UnitArg:GetName() for k=1,qty do local lst=selfArg.CrateGroupList and selfArg.CrateGroupList[uName] if not lst then break end local idx=nil for j=1,#lst do local ch=lst[j] local first=ch and ch[1] if first and(not first:WasDropped())and first:GetName()==cNameArg and#ch>=neededArg then idx=j break end end if not idx then break end selfArg:_UnloadSingleCrateSet(GroupArg,UnitArg,idx) end selfArg:_BuildCrates(GroupArg,UnitArg) end,self,Group,Unit,cName,needed,q) end end end lineIndex=lineIndex+1 end if i<=#list then local left=#list-i+1 local chunk={} for n=i,#list do table.insert(chunk,list[n])end table.insert(self.CrateGroupList[Unit:GetName()],chunk) local setIndex=#self.CrateGroupList[Unit:GetName()] local label=string.format("%d. %s %d/%d",lineIndex,cName,left,needed) MENU_GROUP_COMMAND:New(Group,label,dropCratesMenu,self._UnloadSingleCrateSet,self,Group,Unit,setIndex) lineIndex=lineIndex+1 end end end end function CTLD:CanUnloadSingleTroopByID(Group,Unit,ChunkID,Quantity,LoadedCargo,IsGrounded,IsHoverUnload) return true end function CTLD:_UnloadSingleTroopByID(Group,Unit,chunkID,qty) self:T(self.lid.." _UnloadSingleTroopByID chunkID="..tostring(chunkID)) qty=qty or 1 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=self.returntroopstobase end if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName())then local msg=self:_GetEntryForGroup("OPEN_DOORS_UNLOAD_TROOPS",Group) self:_SendMessage(msg,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{} if not self:CanUnloadSingleTroopByID(Group,Unit,chunkID,qty,loadedcargo,grounded,hoverunload)then return self end if not droppingatbase or self.debug then if not self.TroopsIDToChunk or not self.TroopsIDToChunk[chunkID]then local msg=self:_GetEntryForGroup("NO_TROOP_CHUNK",Group) msg=string.format(msg,chunkID) self:_SendMessage(msg,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 local msg=self:_GetEntryForGroup("TROOP_CHUNK_EMPTY",Group) msg=string.format(msg,chunkID) self:_SendMessage(msg,10,false,Group) if not self.debug then return self end return self end local deployedTroopsByName={} local deployedEngineersByName={} for n=1,qty do local foundCargo=chunk[1] if not foundCargo then break 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) :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits) :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 local grpname=self.DroppedTroops[self.TroopCounter]:GetName() self.EngineersInField[self.Engineers]=CTLD_ENGINEERING:New(name,grpname) deployedEngineersByName[name]=(deployedEngineersByName[name]or 0)+1 else deployedTroopsByName[name]=(deployedTroopsByName[name]or 0)+1 end table.remove(chunk,1) if#chunk==0 then self.TroopsIDToChunk[chunkID]=nil break end end local parts={} local troopLabel=self:_GetEntryForGroup("TROOPS_LABEL",Group) local engineerLabel=self:_GetEntryForGroup("ENGINEERS_LABEL",Group) for nName,nCount in pairs(deployedTroopsByName)do parts[#parts+1]=tostring(nCount).."x "..troopLabel.." "..nName end for nName,nCount in pairs(deployedEngineersByName)do parts[#parts+1]=tostring(nCount).."x "..engineerLabel.." "..nName end if#parts>0 then local msg=self:_GetEntryForGroup("DROPPED_INTO_ACTION",Group) msg=string.format(msg,table.concat(parts,", ")) self:_SendMessage(msg,10,false,Group) end else local msg=self:_GetEntryForGroup("TROOPS_RETURNED",Group) self:_SendMessage(msg,10,false,Group) self:__TroopsRTB(1,Group,Unit,zonename,zone) if self.TroopsIDToChunk and self.TroopsIDToChunk[chunkID]then local chunk=self.TroopsIDToChunk[chunkID] for n=1,qty do if#chunk==0 then break end 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() self:_RefreshTroopQuantityMenus(Group,Unit,_troop) end end end firstObj:SetWasDropped(true) table.remove(chunk,1) end if#chunk==0 then self.TroopsIDToChunk[chunkID]=nil 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) self:_RefreshTroopQuantityMenus(Group,Unit,nil) else local isHerc=self:IsFixedWing(Unit) if isHerc then local msg=self:_GetEntryForGroup("NOTHING_LOADED_AIRDROP",Group) self:_SendMessage(msg,10,false,Group) else local msg=self:_GetEntryForGroup("NOTHING_LOADED_HOVER",Group) self:_SendMessage(msg,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 local dropTroopsMenu=topTroops.DropTroopsMenu if dropTroopsMenu then dropTroopsMenu:RemoveSubMenus() else dropTroopsMenu=MENU_GROUP:New(theGroup,self:_GetEntryForGroup("MENU_DROP_TROOPS",theGroup),topTroops) topTroops.DropTroopsMenu=dropTroopsMenu end if self.maxUnloadTroopsAllowed==-1 then MENU_GROUP_COMMAND:New(theGroup,self:_GetEntryForGroup("MENU_DROP_ALL_TROOPS",theGroup),dropTroopsMenu,self._UnloadTroops,self,theGroup,theUnit) end 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()0 then local chunkID=objList[1]:GetID() self.TroopsIDToChunk[chunkID]=objList local label=string.format(self:_GetEntryForGroup("MENU_DROP_N_TROOPS",theGroup),count,tName) if count==1 then MENU_GROUP_COMMAND:New(theGroup,label,dropTroopsMenu,self._UnloadSingleTroopByID,self,theGroup,theUnit,chunkID,1) else local parentMenu=MENU_GROUP:New(theGroup,label,dropTroopsMenu) for q=1,count do if q>self.maxUnloadTroopsAllowed and self.maxUnloadTroopsAllowed>-1 then break end MENU_GROUP_COMMAND:New(theGroup,string.format(self:_GetEntryForGroup("MENU_DROP_N_TROOPS",theGroup),q,tName),parentMenu,self._UnloadSingleTroopByID,self,theGroup,theUnit,chunkID,q) end end end end end function CTLD:_CheckTemplates(temptable) self:T(self.lid.." _CheckTemplates") local outcome=true if type(temptable)~="table"then temptable={temptable} end for _,_name in pairs(temptable)do if not _DATABASE.Templates.Groups[_name]then outcome=false self:E(self.lid.."ERROR: Template name ".._name.." is missing!") end end return outcome end function CTLD:AddTroopsCargo(Name,Templates,Type,NoTroops,PerTroopMass,Stock,SubCategory) self:T(self.lid.." AddTroopsCargo") self:T({Name,Templates,Type,NoTroops,PerTroopMass,Stock}) if not self:_CheckTemplates(Templates)then self:E(self.lid.."Troops Cargo for "..Name.." has missing template(s)!") return self end self.CargoCounter=self.CargoCounter+1 local cargo=CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,true,NoTroops,nil,nil,PerTroopMass,Stock,SubCategory) table.insert(self.Cargo_Troops,cargo) self._troopsByName=self._troopsByName or{} self._troopsByName[cargo.Name]=cargo self._cargoByTemplate=self._cargoByTemplate or{} local _template=cargo.Templates if type(_template)=="table"then _template=_template[1] end if type(_template)=="string"and _template~=""then self._cargoByTemplate[_template]=cargo end if SubCategory and self.usesubcats~=true then self.usesubcats=true end return self end function CTLD:AddUnits(Name,Templates,Type,Stock,SubCategory,UnitTypes) self:T(self.lid.." AddUnits") if not self:_CheckTemplates(Templates)then self:E(self.lid.."Units for "..Name.." has missing template(s)!") return self end self.C130GetUnits=self.C130GetUnits or{} local entry={} entry.Name=Name entry.Templates=Templates entry.Type=Type entry.Stock=Stock or nil entry.Stock0=Stock or nil entry.SubCategory=SubCategory or"Other" entry.UnitTypes=UnitTypes entry.CanMove=true table.insert(self.C130GetUnits,entry) return self end function CTLD:AddUnitsNoMove(Name,Templates,Type,Stock,SubCategory,UnitTypes) self:T(self.lid.." AddUnitsNoMove") if not self:_CheckTemplates(Templates)then self:E(self.lid.."UnitsNoMove for "..Name.." has missing template(s)!") return self end self.C130GetUnits=self.C130GetUnits or{} local entry={} entry.Name=Name entry.Templates=Templates entry.Type=Type entry.Stock=Stock entry.Stock0=Stock entry.SubCategory=SubCategory or"Other" entry.UnitTypes=UnitTypes entry.CanMove=false table.insert(self.C130GetUnits,entry) return self end function CTLD:AddCratesCargo(Name,Templates,Type,NoCrates,PerCrateMass,Stock,SubCategory,DontShowInMenu,Location,UnitTypes,Category,TypeName,ShapeName,C130TypeName) self:T(self.lid.." AddCratesCargo") if not self:_CheckTemplates(Templates)then self:E(self.lid.."Crates Cargo for "..Name.." has missing template(s)!") return self end self.CargoCounter=self.CargoCounter+1 local cargo=CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,false,NoCrates,nil,nil,PerCrateMass,Stock,SubCategory,DontShowInMenu,Location) if UnitTypes then cargo:AddUnitTypeName(UnitTypes) end cargo:SetStaticTypeAndShape("Cargos",self.basetype) if TypeName then cargo:SetStaticTypeAndShape(Category,TypeName,ShapeName) end cargo.C130TypeName=C130TypeName table.insert(self.Cargo_Crates,cargo) self._crateOrStaticByName=self._crateOrStaticByName or{} self._crateOrStaticByName[cargo.Name]=cargo self._cargoByTemplate=self._cargoByTemplate or{} local _template=cargo.Templates if type(_template)=="table"then _template=_template[1] end if type(_template)=="string"and _template~=""then self._cargoByTemplate[_template]=cargo end if SubCategory and self.usesubcats~=true then self.usesubcats=true end return self end function CTLD:AddCratesCargoNoMove(Name,Templates,Type,NoCrates,PerCrateMass,Stock,SubCategory,DontShowInMenu,Location,UnitTypes,Category,TypeName,ShapeName,C130TypeName) self:T(self.lid.." AddCratesCargoNoMove") if not self:_CheckTemplates(Templates)then self:E(self.lid.."Crates Cargo for "..Name.." has missing template(s)!") return self end self.CargoCounter=self.CargoCounter+1 local cargo=CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,false,NoCrates,nil,nil,PerCrateMass,Stock,SubCategory,DontShowInMenu,Location) cargo.NoMoveToZone=true if UnitTypes then cargo:AddUnitTypeName(UnitTypes) end cargo:SetStaticTypeAndShape("Cargos",self.basetype) if TypeName then cargo:SetStaticTypeAndShape(Category,TypeName,ShapeName) end cargo.C130TypeName=C130TypeName table.insert(self.Cargo_Crates,cargo) self._crateOrStaticByName=self._crateOrStaticByName or{} self._crateOrStaticByName[cargo.Name]=cargo self._cargoByTemplate=self._cargoByTemplate or{} local _template=cargo.Templates if type(_template)=="table"then _template=_template[1] end if type(_template)=="string"and _template~=""then self._cargoByTemplate[_template]=cargo end self.templateToCargoName=self.templateToCargoName or{} if type(Templates)=="table"then for _,t in pairs(Templates)do self.templateToCargoName[t]=Name end else self.templateToCargoName[Templates]=Name end self.nomovetozone_names=self.nomovetozone_names or{} self.nomovetozone_names[Name]=true if SubCategory and self.usesubcats~=true then self.usesubcats=true end return self end function CTLD:AddStaticsCargo(Name,Mass,Stock,SubCategory,DontShowInMenu,Location,UnitTypes,DisplayName) self:T(self.lid.." AddStaticsCargo") self.CargoCounter=self.CargoCounter+1 local cargotype=CTLD_CARGO.Enum.STATIC local template=STATIC:FindByName(Name,true):GetTypeName() local unittemplate=_DATABASE:GetStaticUnitTemplate(Name) local ResourceMap=nil if unittemplate and unittemplate.resourcePayload then ResourceMap=UTILS.DeepCopy(unittemplate.resourcePayload) end local cargo=CTLD_CARGO:New(self.CargoCounter,Name,template,cargotype,false,false,1,nil,nil,Mass,Stock,SubCategory,DontShowInMenu,Location) if UnitTypes then cargo:AddUnitTypeName(UnitTypes) end cargo:SetDisplayName(DisplayName or Name) cargo:SetStaticResourceMap(ResourceMap) table.insert(self.Cargo_Statics,cargo) self._crateOrStaticByName=self._crateOrStaticByName or{} self._crateOrStaticByName[cargo.Name]=cargo self._cargoByTemplate=self._cargoByTemplate or{} local _template=cargo.Templates if type(_template)=="table"then _template=_template[1] end if type(_template)=="string"and _template~=""then self._cargoByTemplate[_template]=cargo end if SubCategory and self.usesubcats~=true then self.usesubcats=true end return cargo end function CTLD:AddStaticsCargoFromType(Name,TypeName,Mass,Stock,SubCategory,DontShowInMenu,Location,UnitTypes,Category,ShapeName,ResourceMap,DisplayName) self:T(self.lid.." AddStaticsCargoFromType") self.CargoCounter=self.CargoCounter+1 local cargotype=CTLD_CARGO.Enum.STATIC local template=TypeName or self.basetype or"container_cargo" local cargo=CTLD_CARGO:New(self.CargoCounter,Name,template,cargotype,false,false,1,nil,nil,Mass,Stock,SubCategory,DontShowInMenu,Location) if UnitTypes then cargo:AddUnitTypeName(UnitTypes) end cargo:SetStaticTypeAndShape(Category or"Cargos",template,ShapeName) cargo:SetDisplayName(DisplayName or Name) if ResourceMap then ResourceMap=UTILS.DeepCopy(ResourceMap) end cargo:SetStaticResourceMap(ResourceMap) table.insert(self.Cargo_Statics,cargo) self._crateOrStaticByName=self._crateOrStaticByName or{} self._crateOrStaticByName[cargo.Name]=cargo self._cargoByTemplate=self._cargoByTemplate or{} local _template=cargo.Templates if type(_template)=="table"then _template=_template[1] end if type(_template)=="string"and _template~=""then self._cargoByTemplate[_template]=cargo end if SubCategory and self.usesubcats~=true then self.usesubcats=true end return cargo end function CTLD:GetStaticsCargoFromTemplate(Name,Mass,DisplayName) self:T(self.lid.." GetStaticsCargoFromTemplate") self.CargoCounter=self.CargoCounter+1 local cargotype=CTLD_CARGO.Enum.STATIC local template=STATIC:FindByName(Name,true):GetTypeName() local unittemplate=_DATABASE:GetStaticUnitTemplate(Name) local ResourceMap=nil if unittemplate and unittemplate.resourcePayload then ResourceMap=UTILS.DeepCopy(unittemplate.resourcePayload) end local cargo=CTLD_CARGO:New(self.CargoCounter,Name,template,cargotype,false,false,1,nil,nil,Mass,1) cargo:SetDisplayName(DisplayName or Name) cargo:SetStaticResourceMap(ResourceMap) return cargo end function CTLD:GetStaticsCargoFromType(Name,TypeName,Mass,Category,ShapeName,ResourceMap,DisplayName) self:T(self.lid.." GetStaticsCargoFromType") self.CargoCounter=self.CargoCounter+1 local cargotype=CTLD_CARGO.Enum.STATIC local template=TypeName or self.basetype or"container_cargo" local cargo=CTLD_CARGO:New(self.CargoCounter,Name,template,cargotype,false,false,1,nil,nil,Mass,1) cargo:SetStaticTypeAndShape(Category or"Cargos",template,ShapeName) cargo:SetDisplayName(DisplayName or Name) if ResourceMap then ResourceMap=UTILS.DeepCopy(ResourceMap) end cargo:SetStaticResourceMap(ResourceMap) return cargo end function CTLD:AddCratesRepair(Name,Template,Type,NoCrates,PerCrateMass,Stock,SubCategory,DontShowInMenu,Location,UnitTypes,Category,TypeName,ShapeName) self:T(self.lid.." AddCratesRepair") if not self:_CheckTemplates(Template)then self:E(self.lid.."Repair Cargo for "..Name.." has a missing template!") return self end self.CargoCounter=self.CargoCounter+1 local cargo=CTLD_CARGO:New(self.CargoCounter,Name,Template,Type,false,false,NoCrates,nil,nil,PerCrateMass,Stock,SubCategory,DontShowInMenu,Location) if UnitTypes then cargo:AddUnitTypeName(UnitTypes) end cargo:SetStaticTypeAndShape("cargos",self.basetype) if TypeName then cargo:SetStaticTypeAndShape(Category,TypeName,ShapeName) end table.insert(self.Cargo_Crates,cargo) self._crateOrStaticByName=self._crateOrStaticByName or{} self._crateOrStaticByName[cargo.Name]=cargo self._cargoByTemplate=self._cargoByTemplate or{} local _template=cargo.Templates if type(_template)=="table"then _template=_template[1] end if type(_template)=="string"and _template~=""then self._cargoByTemplate[_template]=cargo end return self end function CTLD:AddZone(Zone) self:T(self.lid.." AddZone") local zone=Zone if zone.type==CTLD.CargoZoneType.LOAD then table.insert(self.pickupZones,zone) elseif zone.type==CTLD.CargoZoneType.DROP then table.insert(self.dropOffZones,zone) elseif zone.type==CTLD.CargoZoneType.SHIP then table.insert(self.shipZones,zone) elseif zone.type==CTLD.CargoZoneType.BEACON then table.insert(self.droppedBeacons,zone) else table.insert(self.wpZones,zone) end return self end function CTLD:ActivateZone(Name,ZoneType,NewState) self:T(self.lid.." ActivateZone") local newstate=true if NewState~=nil then newstate=NewState end local table={} if ZoneType==CTLD.CargoZoneType.LOAD then table=self.pickupZones elseif ZoneType==CTLD.CargoZoneType.DROP then table=self.dropOffZones elseif ZoneType==CTLD.CargoZoneType.SHIP then table=self.shipZones elseif ZoneType==CTLD.CargoZoneType.BEACON then table=self.droppedBeacons else table=self.wpZones end for _,_zone in pairs(table)do local thiszone=_zone if thiszone.name==Name then thiszone.active=newstate break end end return self end function CTLD:DeactivateZone(Name,ZoneType) self:T(self.lid.." DeactivateZone") self:ActivateZone(Name,ZoneType,false) return self end function CTLD:_GetFMBeacon(Name) self:T(self.lid.." _GetFMBeacon") local beacon={} if#self.FreeFMFrequencies<=1 then self.FreeFMFrequencies=self.UsedFMFrequencies self.UsedFMFrequencies={} end local FM=table.remove(self.FreeFMFrequencies,math.random(#self.FreeFMFrequencies)) table.insert(self.UsedFMFrequencies,FM) beacon.name=Name beacon.frequency=FM/1000000 beacon.modulation=CTLD.RadioModulation.FM return beacon end function CTLD:_GetUHFBeacon(Name) self:T(self.lid.." _GetUHFBeacon") local beacon={} if#self.FreeUHFFrequencies<=1 then self.FreeUHFFrequencies=self.UsedUHFFrequencies self.UsedUHFFrequencies={} end local UHF=table.remove(self.FreeUHFFrequencies,math.random(#self.FreeUHFFrequencies)) table.insert(self.UsedUHFFrequencies,UHF) beacon.name=Name beacon.frequency=UHF/1000000 beacon.modulation=CTLD.RadioModulation.AM return beacon end function CTLD:_GetVHFBeacon(Name) self:T(self.lid.." _GetVHFBeacon") local beacon={} if#self.FreeVHFFrequencies<=3 then self.FreeVHFFrequencies=self.UsedVHFFrequencies self.UsedVHFFrequencies={} end local VHF=table.remove(self.FreeVHFFrequencies,math.random(#self.FreeVHFFrequencies)) table.insert(self.UsedVHFFrequencies,VHF) beacon.name=Name beacon.frequency=VHF/1000000 beacon.modulation=CTLD.RadioModulation.FM return beacon end function CTLD:AddCTLDZone(Name,Type,Color,Active,HasBeacon,Shiplength,Shipwidth,BeaconFrequencies) self:T(self.lid.." AddCTLDZone") local zone=ZONE:FindByName(Name) if not zone and Type~=CTLD.CargoZoneType.SHIP then self:E(self.lid.."**** Zone does not exist: "..Name) return self end if Type==CTLD.CargoZoneType.SHIP then local Ship=UNIT:FindByName(Name) if not Ship then self:E(self.lid.."**** Ship does not exist: "..Name) return self end end local exists=true local ctldzone=self:GetCTLDZone(Name,Type) if not ctldzone then exists=false ctldzone={} end ctldzone.active=Active or false ctldzone.color=Color or SMOKECOLOR.Red ctldzone.name=Name or"NONE" ctldzone.type=Type or CTLD.CargoZoneType.MOVE ctldzone.hasbeacon=HasBeacon or false if Type==CTLD.CargoZoneType.BEACON then self.droppedbeaconref[ctldzone.name]=zone:GetCoordinate() ctldzone.timestamp=timer.getTime() end if HasBeacon then ctldzone.fmbeacon=self:_GetFMBeacon(Name) ctldzone.uhfbeacon=self:_GetUHFBeacon(Name) ctldzone.vhfbeacon=self:_GetVHFBeacon(Name) if BeaconFrequencies then ctldzone.fmbeacon.frequency=BeaconFrequencies.FM or ctldzone.fmbeacon.frequency ctldzone.vhfbeacon.frequency=BeaconFrequencies.VHF or ctldzone.vhfbeacon.frequency ctldzone.uhfbeacon.frequency=BeaconFrequencies.UHF or ctldzone.uhfbeacon.frequency end else ctldzone.fmbeacon=nil ctldzone.uhfbeacon=nil ctldzone.vhfbeacon=nil end if Type==CTLD.CargoZoneType.SHIP then ctldzone.shiplength=Shiplength or 100 ctldzone.shipwidth=Shipwidth or 10 end if not exists then self:AddZone(ctldzone) end return self end function CTLD:GetCTLDZone(Name,Type) if Type==CTLD.CargoZoneType.LOAD then for _,z in pairs(self.pickupZones)do if z.name==Name then return z end end elseif Type==CTLD.CargoZoneType.DROP then for _,z in pairs(self.dropOffZones)do if z.name==Name then return z end end elseif Type==CTLD.CargoZoneType.SHIP then for _,z in pairs(self.shipZones)do if z.name==Name then return z end end elseif Type==CTLD.CargoZoneType.BEACON then for _,z in pairs(self.droppedBeacons)do if z.name==Name then return z end end else for _,z in pairs(self.wpZones)do if z.name==Name then return z end end end return nil end function CTLD:AddCTLDZoneFromAirbase(AirbaseName,Type,Color,Active,HasBeacon) self:T(self.lid.." AddCTLDZoneFromAirbase") local AFB=AIRBASE:FindByName(AirbaseName) local name=AFB:GetZone():GetName() self:T(self.lid.."AFB "..AirbaseName.." ZoneName "..name) self:AddCTLDZone(name,Type,Color,Active,HasBeacon) return self end function CTLD:DropBeaconNow(Unit) self:T(self.lid.." DropBeaconNow") local ctldzone={} ctldzone.active=true ctldzone.color=math.random(0,4) ctldzone.name="Beacon "..math.random(1,10000) ctldzone.type=CTLD.CargoZoneType.BEACON ctldzone.hasbeacon=true ctldzone.fmbeacon=self:_GetFMBeacon(ctldzone.name) ctldzone.uhfbeacon=self:_GetUHFBeacon(ctldzone.name) ctldzone.vhfbeacon=self:_GetVHFBeacon(ctldzone.name) ctldzone.timestamp=timer.getTime() self.droppedbeaconref[ctldzone.name]=Unit:GetCoordinate() self:AddZone(ctldzone) local FMbeacon=ctldzone.fmbeacon local VHFbeacon=ctldzone.vhfbeacon local UHFbeacon=ctldzone.uhfbeacon local Name=ctldzone.name local FM=FMbeacon.frequency local VHF=VHFbeacon.frequency*1000 local UHF=UHFbeacon.frequency local text=string.format(self:_GetEntryForGroup("DROPPED_BEACON",Unit:GetGroup()),Name,FM,VHF,UHF) self:_SendMessage(text,15,false,Unit:GetGroup()) return self end function CTLD:CheckDroppedBeacons() self:T(self.lid.." CheckDroppedBeacons") local timeout=self.droppedbeacontimeout or 600 local livebeacontable={} for _,_beacon in pairs(self.droppedBeacons)do local beacon=_beacon if not beacon.timestamp then beacon.timestamp=timer.getTime()+timeout end local T0=beacon.timestamp if timer.getTime()-T0>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,true) 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) if not Unit then if Zonetype==CTLD.CargoZoneType.SHIP then return false,nil,nil,1000000,nil else return false,nil,nil,1000000 end end 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() if not unitcoord then if Zonetype==CTLD.CargoZoneType.SHIP then return false,nil,nil,1000000,nil else return false,nil,nil,1000000 end end 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) if not ZoneUNIT then return false end 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) self:T("Zone Active: "..tostring(active)) if(zone:IsVec2InZone(unitVec2)or Zonetype==CTLD.CargoZoneType.MOVE)and active==true and 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(self:_GetEntryForGroup("HOVER_PARAMS_METRIC",Group),self.minimumHoverHeight,self.maximumHoverHeight,htxt) else local minheight=UTILS.MetersToFeet(self.minimumHoverHeight) local maxheight=UTILS.MetersToFeet(self.maximumHoverHeight) text=string.format(self:_GetEntryForGroup("HOVER_PARAMS_IMPERIAL",Group),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(self:_GetEntryForGroup("FLIGHT_PARAMS_IMPERIAL",Group),minheight,maxheight,htxt) else local minheight=self.FixedMinAngels local maxheight=self.FixedMaxAngels text=string.format(self:_GetEntryForGroup("FLIGHT_PARAMS_METRIC",Group),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,_unit in pairs(self.C130GetUnits or{})do local genname=_unit.Name local stock0=_unit.Stock0 or 0 if stock0>0 and not Troopstable[genname]then local stock=_unit.Stock or 0 local rel=stock0>0 and math.floor((stock/stock0)*100)or 100 Troopstable[genname]={ Stock0=stock0, Stock=stock, StockR=rel, Infield=0, Inhelo=0, CratesInfield=0, Sum=stock, } 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 local gname=_group:GetName() local uName=nil for _,cfg in pairs(self.C130GetUnits or{})do local templ=cfg.Templates or{} if type(templ)=="string"then templ={templ} end for _,tName in pairs(templ)do if string.find(gname,tName,1,true)then uName=cfg.Name break end end if uName then break end end if uName and Troopstable[uName]then self:T("Found C-130 unit "..uName.." in the field. Adding.") Troopstable[uName].Infield=Troopstable[uName].Infield+1 Troopstable[uName].Sum=Troopstable[uName].Infield+Troopstable[uName].Stock+Troopstable[uName].Inhelo else self:E(self.lid.."Group without Cargo Generic: ".._group:GetName()) end 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:GetName() local gcargo=self:_FindCratesCargoObject(gname)or self:_FindTroopsCargoObject(gname) self:T("Looking at "..gname.." in the helo - type = "..tostring(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 = "..tostring(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 pilots=0 for _,_pilot in pairs(self.CtldUnits)do pilots=pilots+1 end local boxes=0 for _,_pilot in pairs(self.Spawned_Cargo)do boxes=boxes+1 end local cc=self.CargoCounter local tc=self.TroopCounter 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}) if self._c130DcAutoTimer and self._c130DcAutoTimer:IsRunning()then self._c130DcAutoTimer:Stop() end self._c130DcAutoTimer=nil local cleanup={} for setId,_ in pairs(self._c130DcAutoSets or{})do cleanup[#cleanup+1]=setId end for _,setId in ipairs(cleanup)do self:_C130DcAutoCleanupSet(setId,"stop") end for _,batch in pairs(self._c130DcAutoBatches or{})do if batch.timer and batch.timer.IsRunning and batch.timer:IsRunning()then batch.timer:Stop() end end self._c130DcAutoSets={} self._c130DcAutoMap={} self._c130DcAutoBatches={} self._c130DcAutoActiveSetId=nil 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() self:T2({Name=Groupname,Property=task:GetProperty("ExtractName")}) if task:GetProperty("ExtractName")then 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 else self:T({Text="'ExtractName' Property not set",Name=Groupname,Property=task.Type}) 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) if not Group or not Unit then self:_RefreshQuantityMenusForGroup()end end return self end function CTLD:onbeforeCratesDropped(From,Event,To,Group,Unit,Cargotable) self:T({From,Event,To}) if Unit and Unit:IsPlayer()and self.PlayerTaskQueue then local playername=Unit:GetPlayerName() for _,_cargo in pairs(Cargotable)do local Vehicle=_cargo.Positionable if Vehicle then 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 end end return self end function CTLD:OnAfterGetCrates(From,Event,To,Group,Unit,Cargotable) self:T({From,Event,To}) return self end function CTLD:OnAfterRemoveCratesNearby(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 and Vehicle then local cg=self:GetGenericCargoObjectFromGroupName(Vehicle:GetName()) if not(cg and(cg.NoMoveToZone or(self.nomovetozone_names and self.nomovetozone_names[cg:GetName()])))then self:_MoveGroupToZone(Vehicle) end end if not Group or not Unit then self:_RefreshQuantityMenusForGroup()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) if self.C130GetUnits then for _,_unit in pairs(self.C130GetUnits)do if _unit.Name==cargoname then if type(_unit.Stock)=="number"and _unit.Stock~=-1 then _unit.Stock0=_unit.Stock0 or _unit.Stock _unit.Stock=math.max(0,(_unit.Stock or 0)-1) end break end end end 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 self.loadSavedCrates and(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 unittemplate=_DATABASE:GetStaticUnitTemplate(cargoname) local ResourceMap=nil if unittemplate and unittemplate.resourcePayload then ResourceMap=UTILS.DeepCopy(unittemplate.resourcePayload) end injectstatic:SetStaticResourceMap(ResourceMap) 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_CARGO={ ClassName="CTLD_CARGO", ID=0, Name="none", DisplayName="none", Templates={}, CargoType="none", HasBeenMoved=false, LoadDirectly=false, CratesNeeded=0, Positionable=nil, HasBeenDropped=false, PerCrateMass=0, Stock=nil, Stock0=nil, Mark=nil, DontShowInMenu=false, Location=nil, } CTLD_CARGO.Enum={ VEHICLE="Vehicle", TROOPS="Troops", FOB="FOB", CRATE="Crate", REPAIR="Repair", ENGINEERS="Engineers", STATIC="Static", GCLOADABLE="GC_Loadable", } function CTLD_CARGO:New(ID,Name,Templates,Sorte,HasBeenMoved,LoadDirectly,CratesNeeded,Positionable,Dropped,PerCrateMass,Stock,Subcategory,DontShowInMenu,Location) local self=BASE:Inherit(self,BASE:New()) self:T({ID,Name,Templates,Sorte,HasBeenMoved,LoadDirectly,CratesNeeded,Positionable,Dropped}) self.ID=ID or math.random(100000,1000000) self.Name=Name or"none" self.DisplayName=Name or"none" self.Templates=Templates or{} self.CargoType=Sorte or"type" self.HasBeenMoved=HasBeenMoved or false self.LoadDirectly=LoadDirectly or false self.CratesNeeded=CratesNeeded or 0 self.Positionable=Positionable or nil self.HasBeenDropped=Dropped or false self.PerCrateMass=PerCrateMass or 0 self.Stock=Stock or nil self.Stock0=Stock or nil self.Mark=nil self.Subcategory=Subcategory or"Other" self.DontShowInMenu=DontShowInMenu or false self.ResourceMap=nil self.StaticType="container_cargo" if self:IsStatic()then self.StaticType=self.Templates end self.StaticShape=nil self.TypeNames=nil self.StaticCategory="Cargos" if type(Location)=="string"then Location=ZONE:New(Location) end self.Location=Location self.NoMoveToZone=false return self end function CTLD_CARGO:SetStaticTypeAndShape(Category,TypeName,ShapeName) self.StaticCategory=Category or"Cargos" self.StaticType=TypeName or"container_cargo" self.StaticShape=ShapeName return self end function CTLD_CARGO:GetStaticTypeAndShape() return self.StaticCategory,self.StaticType,self.StaticShape end function CTLD_CARGO:AddUnitTypeName(UnitTypes) if not self.TypeNames then self.TypeNames={}end if type(UnitTypes)~="table"then UnitTypes={UnitTypes}end for _,_singletype in pairs(UnitTypes or{})do self.TypeNames[_singletype]=_singletype end return self end function CTLD_CARGO:UnitCanCarry(Unit) if not Unit then return false end if self.TypeNames==nil then return true end local typename=Unit:GetTypeName()or"none" if self.TypeNames[typename]then return true else return false end end function CTLD_CARGO:SetStaticResourceMap(ResourceMap) self.ResourceMap=ResourceMap return self end function CTLD_CARGO:GetStaticResourceMap() return self.ResourceMap end function CTLD_CARGO:GetLocation() return self.Location end function CTLD_CARGO:GetID() return self.ID end function CTLD_CARGO:GetSubCat() return self.Subcategory end function CTLD_CARGO:GetMass() return self.PerCrateMass end function CTLD_CARGO:GetName() return self.Name end function CTLD_CARGO:SetDisplayName(DisplayName) if type(DisplayName)=="string"and DisplayName~=""then self.DisplayName=DisplayName else self.DisplayName=self.Name end return self end function CTLD_CARGO:GetDisplayName() return self.DisplayName or self.Name end function CTLD_CARGO:GetTemplates() return self.Templates end function CTLD_CARGO:HasMoved() return self.HasBeenMoved end function CTLD_CARGO:WasDropped(hercOnly) if hercOnly then return self.HasBeenDropped and self.IsHercDrop==true end return self.HasBeenDropped end function CTLD_CARGO:CanLoadDirectly() return self.LoadDirectly end function CTLD_CARGO:GetCratesNeeded() return self.CratesNeeded end function CTLD_CARGO:GetType() return self.CargoType end function CTLD_CARGO:GetPositionable() return self.Positionable end function CTLD_CARGO:SetHasMoved(moved) self.HasBeenMoved=moved or false end function CTLD_CARGO:Isloaded() if self.HasBeenMoved and not self:WasDropped()then return true else return false end end function CTLD_CARGO:SetWasDropped(dropped,isHercDrop) self.HasBeenDropped=dropped or false self.IsHercDrop=isHercDrop or false end function CTLD_CARGO:GetStock() if self.Stock then return self.Stock else return-1 end end function CTLD_CARGO:GetStock0() if self.Stock0 then return self.Stock0 else return-1 end end function CTLD_CARGO:GetRelativeStock() if self.Stock and self.Stock0 then return math.floor((self.Stock/self.Stock0)*100) else return-1 end end function CTLD_CARGO:AddStock(Number) if self.Stock then local number=Number or 1 self.Stock=self.Stock+number end return self end function CTLD_CARGO:RemoveStock(Number) if self.Stock then local number=Number or 1 self.Stock=self.Stock-number if self.Stock<0 then self.Stock=0 end end return self end function CTLD_CARGO:SetStock(Number) self.Stock=Number return self end function CTLD_CARGO:IsRepair() if self.CargoType=="Repair"then return true else return false end end function CTLD_CARGO:IsStatic() if self.CargoType=="Static"then return true else return false end end function CTLD_CARGO:AddMark(Mark) self.Mark=Mark return self end function CTLD_CARGO:GetMark(Mark) return self.Mark end function CTLD_CARGO:WipeMark() self.Mark=nil return self end function CTLD_CARGO:GetNetMass() return self.CratesNeeded*self.PerCrateMass end end do CTLD_ENGINEERING={ ClassName="CTLD_ENGINEERING", lid="", Name="none", Group=nil, Unit=nil, HeliGroup=nil, HeliUnit=nil, State="", } CTLD_ENGINEERING.Version="0.0.3" function CTLD_ENGINEERING:New(Name,GroupName,HeliGroup,HeliUnit) local self=BASE:Inherit(self,BASE:New()) self.Name=Name or"Engineer Squad" self.Group=GROUP:FindByName(GroupName) self.Unit=self.Group:GetUnit(1) self.HeliGroup=HeliGroup self.HeliUnit=HeliUnit self.currwpt=nil self.lid=string.format("%s (%s) | ",self.Name,self.Version) self.State="Stopped" self.marktimer=300 self:Start() local parent=self:GetParent(self) return self end function CTLD_ENGINEERING:SetStatus(State) self.State=State return self end function CTLD_ENGINEERING:GetStatus() return self.State end function CTLD_ENGINEERING:IsStatus(State) return self.State==State end function CTLD_ENGINEERING:IsNotStatus(State) return self.State~=State end function CTLD_ENGINEERING:Start() self:T(self.lid.."Start") self:SetStatus("Running") return self end function CTLD_ENGINEERING:Stop() self:T(self.lid.."Stop") self:SetStatus("Stopped") return self end function CTLD_ENGINEERING:Build() self:T(self.lid.."Build") self:SetStatus("Building") return self end function CTLD_ENGINEERING:Done() self:T(self.lid.."Done") local grp=self.Group grp:RelocateGroundRandomInRadius(7,100,false,false,"Diamond") self:SetStatus("Running") return self end function CTLD_ENGINEERING:Search(crates,number) self:T(self.lid.."Search") self:SetStatus("Searching") local dist=self.distance local group=self.Group local ctable={} local ind=0 if number>0 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:GetCoord() 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:GetCoord() 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 CTLD.Messages={ EN={ CRATE_LOADED_GROUNDCREW="Crate %s loaded by ground crew!", CRATE_UNLOADED_GROUNDCREW="Crate %s unloaded by ground crew!", CRATE_LOADED_ID="Crate ID %d for %s loaded!", LOADED_FULL="Loaded %d %s.", LOADED_SETS_LEFTOVER="Loaded %d %s(s), with %d leftover crate(s).", LOADED_SETS="Loaded %d %s(s).", LOADED_PARTIAL="Loaded only %d/%d crate(s) of %s.", LOADED_PARTIAL_LIMIT="Loaded only %d/%d crate(s) of %s. Cargo limit is now reached!", LOADED_BATCH="Loaded %d %s.", LOADED_BATCH_PARTIAL="Some sets could not be fully loaded.", DROPPED_FULL="Dropped %d %s.", DROPPED_SETS_LEFTOVER="Dropped %d %s(s), with %d leftover crate(s).", DROPPED_SETS="Dropped %d %s(s).", DROPPED_PARTIAL="Dropped %d/%d crate(s) of %s.", DROPPED_INTO_ACTION="Dropped %s into action!", DROPPED_BEACON="Dropped %s | FM %s Mhz | VHF %s KHz | UHF %s Mhz ", CRATES_POSITIONED="%d crates for %s have been positioned near you!", CRATES_DROPPED="%d crates for %s have been dropped!", BOARDED="%s boarded!", BOARDING="%s boarding!", TROOPS_RETURNED="Troops have returned to base!", TROOPS_LABEL="Troops", ENGINEERS_LABEL="Engineers", DEPLOYED_NEAR_YOU="%s have been deployed near you!", UNITS_REMOVED="%s have been removed", BUILD_STARTED="Build started, ready in %d seconds!", REPAIR_STARTED="Repair started using %s taking %d secs", NO_UNIT_TO_REPAIR="No unit close enough to repair!", CANT_REPAIR_WITH="Can't repair this unit with %s", CRATES_MOVE_BEFORE_BUILD="*** Crates need to be moved before building!", CHOPPER_CANNOT_CARRY="Sorry this chopper cannot carry crates!", TOO_HEAVY="Sorry, that's too heavy to load!", FULLY_LOADED="Sorry, we are fully loaded!", CRAMMED="Sorry, we're crammed already!", NO_CAPACITY_NOW="No capacity to load more now!", NO_MORE_CAPACITY="No more capacity to load crates!", CANNOT_LOAD_NONE_OR_FULL="Cannot load crates: either none found or no capacity left.", NEED_TO_LAND_OR_HOVER_LOAD="You need to land or hover in position to load!", HOVER_OVER_CRATES="Hover over the crates to pick them up!", LAND_OR_HOVER_OVER_CRATES="Land or hover over the crates to pick them up!", MUST_LAND_OR_HOVER_CRATES="You must land or hover to load crates!", NEED_TO_LAND_BUILD="You need to land / stop to build something, Pilot!", NOT_CLOSE_ENOUGH_LOGISTICS="You are not close enough to a logistics zone!", NOT_CLOSE_ENOUGH_DROP="You are not close enough to a drop zone!", NOT_CLOSE_ENOUGH_ZONE_NM="Negative, need to be closer than %dnm to a zone!", CANNOT_BUILD_LOADING_AREA="You cannot build in a loading area, Pilot!", OPEN_DOORS_LOAD_CARGO="You need to open the door(s) to load cargo!", OPEN_DOORS_LOAD_TROOPS="You need to open the door(s) to load troops!", OPEN_DOORS_EXTRACT_TROOPS="You need to open the door(s) to extract troops!", OPEN_DOORS_UNLOAD_TROOPS="You need to open the door(s) to unload troops!", OPEN_DOORS_DROP_CARGO="You need to open the door(s) to drop cargo!", ALL_GONE="Sorry, all %s are gone!", RAN_OUT_OF="Sorry, we ran out of %s", CARGO_NOT_AVAILABLE_ZONE="The requested cargo is not available in this zone!", ENOUGH_CRATES_NEARBY="There are enough crates nearby already! Take care of those first!", NO_CRATES_WITHIN="No (loadable) crates within %d meters!", NO_CRATES_WITHIN_PLAIN="No crates within %d meters!", NO_CRATES_IN_RANGE="No crates found in range!", NO_NAMED_CRATES_IN_RANGE="No \"%s\" crates found in range!", NO_LOADABLE_CRATES="Sorry, no loadable crates nearby or max cargo weight reached!", NO_UNITS_TO_EXTRACT="No units close enough to extract!", NO_UNIT_CONFIG="No unit configuration found for %s", CANT_ONBOARD="Can't onboard %s", TOO_MANY_UNITS_NEARBY="You already have %d units nearby!", NO_CRATE_GROUPS="No crate groups found for this unit!", NO_CRATE_SET="No crate set found or index invalid!", NO_CRATE_IN_SET="No crate found in that set!", NO_TROOP_CHUNK="No troop cargo chunk found for ID %d!", TROOP_CHUNK_EMPTY="Troop chunk is empty for ID %d!", NOTHING_LOADED="Nothing loaded!\nTroop limit: %d | Crate limit %d | Weight limit %d kgs", NOTHING_LOADED_AIRDROP="Nothing loaded or not within airdrop parameters!", NOTHING_LOADED_HOVER="Nothing loaded or not hovering within parameters!", NOTHING_IN_STOCK="Nothing in stock!", NOTHING_TO_PACK="Nothing to pack at this distance pilot!", NOTHING_TO_REMOVE="Nothing to remove at this distance pilot!", ROGER_ZONE="Roger, %s zone %s!", HOVER_PARAMS_METRIC="Hover parameters (autoload/drop):\n - Min height %dm \n - Max height %dm \n - Max speed 2mps \n - In parameter: %s", HOVER_PARAMS_IMPERIAL="Hover parameters (autoload/drop):\n - Min height %dft \n - Max height %dft \n - Max speed 6ftps \n - In parameter: %s", FLIGHT_PARAMS_IMPERIAL="Flight parameters (airdrop):\n - Min height %dft \n - Max height %dft \n - In parameter: %s", FLIGHT_PARAMS_METRIC="Flight parameters (airdrop):\n - Min height %dm \n - Max height %dm \n - In parameter: %s", REPORT_CRATES_FOUND="Crates Found Nearby:", REPORT_REMOVING_CRATES="Removing Crates Found Nearby:", REPORT_TRANSPORT_CHECKOUT="Transport Checkout Sheet", REPORT_INVENTORY="Inventory Sheet", REPORT_BUILD_CHECKLIST="Checklist Buildable Crates", REPORT_REPAIR_CHECKLIST="Checklist Repairs", REPORT_BEACONS="Active Zone Beacons", REPORT_SECTION_TROOPS=" -- TROOPS --", REPORT_SECTION_CRATES=" -- CRATES --", REPORT_SECTION_CRATES_GC=" -- CRATES loaded via Ground Crew --", REPORT_SECTION_NONE=" N O N E", REPORT_SECTION_NONE_ALT=" --- None found! ---", REPORT_SECTION_NONE_REPAIR=" --- None Found ---", REPORT_GC_LOADABLE_HINT="Probably ground crew loadable (F8)", REPORT_TOTAL_MASS="Total Mass: %s kg. Loadable: %s kg.", REPORT_TROOPS_CRATES_COUNT="Troops: %d(%d), Crates: %d(%d)", REPORT_TROOPS_CRATETYPES_COUNT="Troops: %d, Cratetypes: %d", REPORT_ROW_TROOP="Troop: %s size %d", REPORT_ROW_CRATE="Crate: %s %d/%d", REPORT_ROW_CRATE_SIZE1="Crate: %s size 1", REPORT_ROW_GC_CRATE="GC loaded Crate: %s size 1", REPORT_ROW_DROPPED_CRATE="Dropped crate for %s, %dkg", REPORT_ROW_CRATE_KG="Crate for %s, %dkg", REPORT_ROW_CRATE_REMOVED="Crate for %s, %dkg removed", REPORT_ROW_UNIT_STOCK="Unit: %s | Soldiers: %d | Stock: %s", REPORT_ROW_TYPE_CRATE_STOCK="Type: %s | Crates per Set: %d | Stock: %s", REPORT_ROW_TYPE_STOCK="Type: %s | Stock: %s", REPORT_ROW_BUILD_CHECK="Type: %s | Required %d | Found %d | Can Build %s", REPORT_ROW_REPAIR_CHECK="Type: %s | Required %d | Found %d | Can Repair %s", REPORT_ROW_BEACON=" %s | FM %s Mhz | VHF %s KHz | UHF %s Mhz ", WEIGHT_LIMIT="Weight limit reached", CRATE_LIMIT="Crate limit reached", MENU_CTLD="CTLD", MENU_MANAGE_TROOPS="Manage Troops", MENU_MANAGE_CRATES="Manage Crates", MENU_MANAGE_UNITS="Manage Units", MENU_LOAD_TROOPS="Load troops", MENU_DROP_TROOPS="Drop Troops", MENU_DROP_ALL_TROOPS="Drop ALL troops", MENU_EXTRACT_TROOPS="Extract troops", MENU_DROP_N_TROOPS="Drop (%d) %s", MENU_GET_CRATES="Get Crates", MENU_GET="Get", MENU_GET_AND_LOAD="Get and Load", MENU_GET_ANYWAY="Get anyway", MENU_PARTIALLY_LOAD="Partially load", MENU_OUT_OF_STOCK="Out of stock", MENU_TROOP_LIMIT="Troop limit reached", MENU_LOAD_CRATES="Load Crates", MENU_LOAD_ALL="Load ALL", MENU_SHOW_LOADABLE_CRATES="Show loadable crates", MENU_NO_CRATES_FOUND_RESCAN="No crates found! Rescan?", MENU_USE_C130_LOAD="Use C-130 Load system", MENU_LOAD_SINGLE="Load", MENU_DROP_CRATES="Drop Crates", MENU_DROP_ALL_CRATES="Drop ALL crates", MENU_DROP="Drop", MENU_DROP_AND_BUILD="Drop and build", MENU_DROP_N_SETS="Drop %d Set%s", MENU_NO_CRATES_TO_DROP="No crates to drop!", MENU_BUILD_CRATES="Build crates", MENU_REPAIR="Repair", MENU_PACK_CRATES="Pack crates", MENU_PACK="Pack", MENU_SCAN_PACKABLE_UNITS="Scan packable units nearby", MENU_NO_PACKABLE_UNITS_FOUND_RESCAN="No packable units found! Rescan?", MENU_PACK_ALL="Pack nearby", MENU_PACK_AND_LOAD="Pack and Load", MENU_PACK_AND_LOAD_ALL="Pack and Load nearby", MENU_PACK_AND_REMOVE="Pack and Remove", MENU_PACK_AND_REMOVE_ALL="Pack and Remove nearby", MENU_REMOVE_CRATES="Remove crates", MENU_REMOVE_CRATES_NEARBY="Remove crates nearby", MENU_LIST_CRATES_NEARBY="List crates nearby", MENU_CRATES_NEEDED="%d crate%s %s (%dkg)", MENU_CRATE_SINGLE="%s (%dkg)", MENU_GET_UNITS="Get Units", MENU_REMOVE_UNITS_NEARBY="Remove units nearby", MENU_LIST_BOARDED_CARGO="List boarded cargo", MENU_INVENTORY="Inventory", MENU_LIST_ZONE_BEACONS="List active zone beacons", MENU_SMOKES_FLARES_BEACONS="Smokes, Flares, Beacons", MENU_SMOKE_ZONES_NEARBY="Smoke zones nearby", MENU_DROP_SMOKE_NOW="Drop smoke now", MENU_RED_SMOKE="Red smoke", MENU_BLUE_SMOKE="Blue smoke", MENU_GREEN_SMOKE="Green smoke", MENU_ORANGE_SMOKE="Orange smoke", MENU_WHITE_SMOKE="White smoke", MENU_FLARE_ZONES_NEARBY="Flare zones nearby", MENU_FIRE_FLARE_NOW="Fire flare now", MENU_DROP_BEACON_NOW="Drop beacon now", MENU_SHOW_FLIGHT_PARAMS="Show flight parameters", MENU_SHOW_HOVER_PARAMS="Show hover parameters", STOCK_NONE="none", STOCK_UNLIMITED="unlimited", BUILD_YES="YES", BUILD_NO="NO", }, DE={ CRATE_LOADED_GROUNDCREW="Kiste %s vom Bodenpersonal geladen!", CRATE_UNLOADED_GROUNDCREW="Kiste %s vom Bodenpersonal entladen!", CRATE_LOADED_ID="Kiste ID %d für %s geladen!", LOADED_FULL="%d %s geladen.", LOADED_SETS_LEFTOVER="%d %s geladen, %d Kiste(n) übrig.", LOADED_SETS="%d %s geladen.", LOADED_PARTIAL="Nur %d/%d Kiste(n) von %s geladen.", LOADED_PARTIAL_LIMIT="Nur %d/%d Kiste(n) von %s geladen. Frachtlimit erreicht!", LOADED_BATCH="%d %s geladen.", LOADED_BATCH_PARTIAL="Einige Sets konnten nicht vollständig geladen werden.", DROPPED_FULL="%d %s abgeworfen.", DROPPED_SETS_LEFTOVER="%d %s abgeworfen, %d Kiste(n) übrig.", DROPPED_SETS="%d %s abgeworfen.", DROPPED_PARTIAL="%d/%d Kiste(n) von %s abgeworfen.", DROPPED_INTO_ACTION="%s im Einsatz abgesetzt!", DROPPED_BEACON="%s abgesetzt | FM %s Mhz | VHF %s KHz | UHF %s Mhz ", CRATES_POSITIONED="%d Kisten für %s in Ihrer Nähe positioniert!", CRATES_DROPPED="%d Kisten für %s abgeworfen!", BOARDED="%s eingestiegen!", BOARDING="%s steigt ein!", TROOPS_RETURNED="Truppen zur Basis zurückgekehrt!", TROOPS_LABEL="Truppen", ENGINEERS_LABEL="Pioniere", DEPLOYED_NEAR_YOU="%s in Ihrer Nähe eingesetzt!", UNITS_REMOVED="%s entfernt", BUILD_STARTED="Bau gestartet, fertig in %d Sekunden!", REPAIR_STARTED="Reparatur mit %s gestartet, dauert %d Sek.", NO_UNIT_TO_REPAIR="Keine Einheit in Reichweite zum Reparieren!", CANT_REPAIR_WITH="Diese Einheit kann nicht mit %s repariert werden", CRATES_MOVE_BEFORE_BUILD="*** Kisten müssen vor dem Bau verschoben werden!", CHOPPER_CANNOT_CARRY="Dieser Hubschrauber kann keine Kisten transportieren!", TOO_HEAVY="Entschuldigung, das ist zu schwer zum Laden!", FULLY_LOADED="Entschuldigung, wir sind voll beladen!", CRAMMED="Entschuldigung, wir sind bereits voll besetzt!", NO_CAPACITY_NOW="Aktuell keine Ladekapazität mehr vorhanden!", NO_MORE_CAPACITY="Keine Kapazität mehr für weitere Kisten!", CANNOT_LOAD_NONE_OR_FULL="Laden nicht möglich: keine Kisten gefunden oder Kapazität erschöpft.", NEED_TO_LAND_OR_HOVER_LOAD="Bitte landen oder schweben Sie zum Laden!", HOVER_OVER_CRATES="Schweben Sie über die Kisten, um sie aufzunehmen!", LAND_OR_HOVER_OVER_CRATES="Landen oder schweben Sie über die Kisten, um sie aufzunehmen!", MUST_LAND_OR_HOVER_CRATES="Sie müssen landen oder schweben, um Kisten zu laden!", NEED_TO_LAND_BUILD="Sie müssen landen / anhalten, um etwas zu bauen, Pilot!", NOT_CLOSE_ENOUGH_LOGISTICS="Sie sind nicht nah genug an einer Logistikzone!", NOT_CLOSE_ENOUGH_DROP="Sie sind nicht nah genug an einer Abwurfzone!", NOT_CLOSE_ENOUGH_ZONE_NM="Negativ, Sie müssen näher als %d Seemeilen an einer Zone sein!", CANNOT_BUILD_LOADING_AREA="In einem Ladebereich kann nicht gebaut werden, Pilot!", OPEN_DOORS_LOAD_CARGO="Bitte öffnen Sie die Tür(en) zum Laden von Fracht!", OPEN_DOORS_LOAD_TROOPS="Bitte öffnen Sie die Tür(en) zum Einladen von Truppen!", OPEN_DOORS_EXTRACT_TROOPS="Bitte öffnen Sie die Tür(en) zum Aussteigen der Truppen!", OPEN_DOORS_UNLOAD_TROOPS="Bitte öffnen Sie die Tür(en) zum Entladen der Truppen!", OPEN_DOORS_DROP_CARGO="Bitte öffnen Sie die Tür(en) zum Abwerfen der Fracht!", ALL_GONE="Entschuldigung, alle %s sind vergriffen!", RAN_OUT_OF="Entschuldigung, %s ist nicht mehr vorrätig", CARGO_NOT_AVAILABLE_ZONE="Die angeforderte Fracht ist in dieser Zone nicht verfügbar!", ENOUGH_CRATES_NEARBY="Es sind bereits genügend Kisten in der Nähe! Bitte zuerst um diese kümmern!", NO_CRATES_WITHIN="Keine (ladbaren) Kisten in %d Metern Umkreis!", NO_CRATES_WITHIN_PLAIN="Keine Kisten in %d Metern Umkreis!", NO_CRATES_IN_RANGE="Keine Kisten in Reichweite gefunden!", NO_NAMED_CRATES_IN_RANGE="Keine \"%s\"-Kisten in Reichweite gefunden!", NO_LOADABLE_CRATES="Entschuldigung, keine ladbaren Kisten in der Nähe oder maximales Frachtgewicht erreicht!", NO_UNITS_TO_EXTRACT="Keine Einheiten nah genug zum Aussteigen!", NO_UNIT_CONFIG="Keine Einheitenkonfiguration für %s gefunden", CANT_ONBOARD="%s kann nicht eingeladen werden", TOO_MANY_UNITS_NEARBY="Sie haben bereits %d Einheiten in der Nähe!", NO_CRATE_GROUPS="Keine Kistengruppen für diese Einheit gefunden!", NO_CRATE_SET="Kein Kistenset gefunden oder Index ungültig!", NO_CRATE_IN_SET="Keine Kiste in diesem Set gefunden!", NO_TROOP_CHUNK="Kein Truppenfracht-Block für ID %d gefunden!", TROOP_CHUNK_EMPTY="Truppenfracht-Block für ID %d ist leer!", NOTHING_LOADED="Nichts geladen!\nTruppenlimit: %d | Kistenlimit: %d | Gewichtslimit: %d kg", NOTHING_LOADED_AIRDROP="Nichts geladen oder nicht innerhalb der Abwurfparameter!", NOTHING_LOADED_HOVER="Nichts geladen oder Schwebeparameter nicht erfüllt!", NOTHING_IN_STOCK="Nichts vorrätig!", NOTHING_TO_PACK="Nichts in dieser Entfernung zum Verpacken, Pilot!", NOTHING_TO_REMOVE="Nichts in dieser Entfernung zum Entfernen, Pilot!", ROGER_ZONE="Verstanden, %s Zone %s!", HOVER_PARAMS_METRIC="Schwebeparameter (Autoladen/Abwurf):\n - Min. Höhe %dm \n - Max. Höhe %dm \n - Max. Geschwindigkeit 2m/s \n - Im Parameter: %s", HOVER_PARAMS_IMPERIAL="Schwebeparameter (Autoladen/Abwurf):\n - Min. Höhe %dft \n - Max. Höhe %dft \n - Max. Geschwindigkeit 6ft/s \n - Im Parameter: %s", FLIGHT_PARAMS_IMPERIAL="Flugparameter (Luftabwurf):\n - Min. Höhe %dft \n - Max. Höhe %dft \n - Im Parameter: %s", FLIGHT_PARAMS_METRIC="Flugparameter (Luftabwurf):\n - Min. Höhe %dm \n - Max. Höhe %dm \n - Im Parameter: %s", REPORT_CRATES_FOUND="Kisten in der Nähe:", REPORT_REMOVING_CRATES="Entferne Kisten in der Nähe:", REPORT_TRANSPORT_CHECKOUT="Transport-Checkliste", REPORT_INVENTORY="Inventarliste", REPORT_BUILD_CHECKLIST="Checkliste baubare Kisten", REPORT_REPAIR_CHECKLIST="Checkliste Reparaturen", REPORT_BEACONS="Aktive Zonenfeuer", REPORT_SECTION_TROOPS=" -- TRUPPEN --", REPORT_SECTION_CRATES=" -- KISTEN --", REPORT_SECTION_CRATES_GC=" -- KISTEN via Bodenpersonal geladen --", REPORT_SECTION_NONE=" K E I N E", REPORT_SECTION_NONE_ALT=" --- Keine gefunden! ---", REPORT_SECTION_NONE_REPAIR=" --- Keine gefunden ---", REPORT_GC_LOADABLE_HINT="Wahrscheinlich durch Bodenpersonal ladbar (F8)", REPORT_TOTAL_MASS="Gesamtgewicht: %s kg. Ladbar: %s kg.", REPORT_TROOPS_CRATES_COUNT="Truppen: %d(%d), Kisten: %d(%d)", REPORT_TROOPS_CRATETYPES_COUNT="Truppen: %d, Kistentypen: %d", REPORT_ROW_TROOP="Truppe: %s Größe %d", REPORT_ROW_CRATE="Kiste: %s %d/%d", REPORT_ROW_CRATE_SIZE1="Kiste: %s Größe 1", REPORT_ROW_GC_CRATE="Bodenpersonal-Kiste: %s Größe 1", REPORT_ROW_DROPPED_CRATE="Abgeworfene Kiste für %s, %dkg", REPORT_ROW_CRATE_KG="Kiste für %s, %dkg", REPORT_ROW_CRATE_REMOVED="Kiste für %s, %dkg entfernt", REPORT_ROW_UNIT_STOCK="Einheit: %s | Soldaten: %d | Bestand: %s", REPORT_ROW_TYPE_CRATE_STOCK="Typ: %s | Kisten pro Set: %d | Bestand: %s", REPORT_ROW_TYPE_STOCK="Typ: %s | Bestand: %s", REPORT_ROW_BUILD_CHECK="Typ: %s | Benötigt: %d | Gefunden: %d | Baubar: %s", REPORT_ROW_REPAIR_CHECK="Typ: %s | Benötigt: %d | Gefunden: %d | Reparierbar: %s", REPORT_ROW_BEACON=" %s | FM %s Mhz | VHF %s KHz | UHF %s Mhz ", WEIGHT_LIMIT="Gewichtslimit erreicht", CRATE_LIMIT="Kistenlimit erreicht", MENU_CTLD="CTLD", MENU_MANAGE_TROOPS="Truppen verwalten", MENU_MANAGE_CRATES="Kisten verwalten", MENU_MANAGE_UNITS="Einheiten verwalten", MENU_LOAD_TROOPS="Truppen einladen", MENU_DROP_TROOPS="Truppen absetzen", MENU_DROP_ALL_TROOPS="ALLE Truppen absetzen", MENU_EXTRACT_TROOPS="Truppen aufnehmen", MENU_DROP_N_TROOPS="(%d) %s absetzen", MENU_GET_CRATES="Kisten holen", MENU_GET="Holen", MENU_GET_AND_LOAD="Holen und laden", MENU_GET_ANYWAY="Trotzdem holen", MENU_PARTIALLY_LOAD="Teilweise laden", MENU_OUT_OF_STOCK="Nicht vorrätig", MENU_TROOP_LIMIT="Truppenlimit erreicht", MENU_LOAD_CRATES="Kisten laden", MENU_LOAD_ALL="ALLE laden", MENU_SHOW_LOADABLE_CRATES="Ladbare Kisten anzeigen", MENU_NO_CRATES_FOUND_RESCAN="Keine Kisten gefunden! Neu scannen?", MENU_USE_C130_LOAD="C-130-Ladesystem verwenden", MENU_LOAD_SINGLE="Lade", MENU_DROP_CRATES="Kisten abwerfen", MENU_DROP_ALL_CRATES="ALLE Kisten abwerfen", MENU_DROP="Abwerfen", MENU_DROP_AND_BUILD="Abwerfen und bauen", MENU_DROP_N_SETS="%d Set%s abwerfen", MENU_NO_CRATES_TO_DROP="Keine Kisten zum Abwerfen!", MENU_BUILD_CRATES="Kisten bauen", MENU_REPAIR="Reparieren", MENU_PACK_CRATES="Kisten packen", MENU_PACK="Packen", MENU_SCAN_PACKABLE_UNITS="Packbare Einheiten in der Nähe scannen", MENU_NO_PACKABLE_UNITS_FOUND_RESCAN="Keine packbaren Einheiten gefunden! Neu scannen?", MENU_PACK_ALL="In der Nähe packen", MENU_PACK_AND_LOAD="Packen und laden", MENU_PACK_AND_LOAD_ALL="In der Nähe packen und laden", MENU_PACK_AND_REMOVE="Packen und entfernen", MENU_PACK_AND_REMOVE_ALL="In der Nähe packen und entfernen", MENU_REMOVE_CRATES="Kisten entfernen", MENU_REMOVE_CRATES_NEARBY="Nahe Kisten entfernen", MENU_LIST_CRATES_NEARBY="Nahe Kisten auflisten", MENU_CRATES_NEEDED="%d Kiste%s %s (%dkg)", MENU_CRATE_SINGLE="%s (%dkg)", MENU_GET_UNITS="Einheiten holen", MENU_REMOVE_UNITS_NEARBY="Nahe Einheiten entfernen", MENU_LIST_BOARDED_CARGO="Geladene Fracht anzeigen", MENU_INVENTORY="Inventar", MENU_LIST_ZONE_BEACONS="Aktive Zonenfeuer anzeigen", MENU_SMOKES_FLARES_BEACONS="Rauch, Leuchtfeuer, Baken", MENU_SMOKE_ZONES_NEARBY="Nahe Zonen einrauchen", MENU_DROP_SMOKE_NOW="Rauch jetzt setzen", MENU_RED_SMOKE="Roter Rauch", MENU_BLUE_SMOKE="Blauer Rauch", MENU_GREEN_SMOKE="Grüner Rauch", MENU_ORANGE_SMOKE="Oranger Rauch", MENU_WHITE_SMOKE="Weißer Rauch", MENU_FLARE_ZONES_NEARBY="Nahe Zonen befeuern", MENU_FIRE_FLARE_NOW="Leuchtfeuer jetzt abfeuern", MENU_DROP_BEACON_NOW="Bake jetzt setzen", MENU_SHOW_FLIGHT_PARAMS="Flugparameter anzeigen", MENU_SHOW_HOVER_PARAMS="Schwebeparameter anzeigen", STOCK_NONE="keiner", STOCK_UNLIMITED="unbegrenzt", BUILD_YES="JA", BUILD_NO="NEIN", }, FR={ CRATE_LOADED_GROUNDCREW="Caisse(s) %s chargée(s) par l'équipe au sol !", CRATE_UNLOADED_GROUNDCREW="Caisse(s) %s déchargée(s) par l'équipe au sol !", CRATE_LOADED_ID="Caisse(s) ID %d pour %s chargée(s) !", LOADED_FULL="%d %s chargé(s).", LOADED_SETS_LEFTOVER="%d %s chargé(s), %d caisse(s) restante(s).", LOADED_SETS="%d %s chargé(s).", LOADED_PARTIAL="Seulement %d/%d caisse(s) de %s chargée(s).", LOADED_PARTIAL_LIMIT="Seulement %d/%d caisse(s) de %s chargée(s). Limite de fret atteinte !", LOADED_BATCH="%d %s chargé(s).", LOADED_BATCH_PARTIAL="Certains ensembles n'ont pas pu être complètement chargés.", DROPPED_FULL="%d %s largué(s).", DROPPED_SETS_LEFTOVER="%d %s largué(s), %d caisse(s) restante(s).", DROPPED_SETS="%d %s largué(s).", DROPPED_PARTIAL="%d/%d caisse(s) de %s larguée(s).", DROPPED_INTO_ACTION="%s engagé(s) en action !", DROPPED_BEACON="%s largué | FM %s Mhz | VHF %s KHz | UHF %s Mhz ", CRATES_POSITIONED="%d caisses pour %s positionnées près de vous !", CRATES_DROPPED="%d caisses pour %s larguées !", BOARDED="%s embarqué(s) !", BOARDING="%s en cours d'embarquement !", TROOPS_RETURNED="Les troupes sont retournées à la base !", TROOPS_LABEL="troupes", ENGINEERS_LABEL="sapeurs", DEPLOYED_NEAR_YOU="%s déployé(s) près de vous !", UNITS_REMOVED="%s supprimé(s)", BUILD_STARTED="Construction démarrée, prête dans %d secondes !", REPAIR_STARTED="Réparation démarrée avec %s, durée %d sec.", NO_UNIT_TO_REPAIR="Aucune unité(s) assez proche pour être réparée(s) !", CANT_REPAIR_WITH="Impossible de réparer cette unité avec %s", CRATES_MOVE_BEFORE_BUILD="*** Les caisses doivent être déplacées avant la construction !", CHOPPER_CANNOT_CARRY="Cet hélicoptère ne peut pas transporter de caisses !", TOO_HEAVY="Désolé, c'est trop lourd à charger !", FULLY_LOADED="Désolé, capacité maximale atteinte !", CRAMMED="Désolé, nous sommes déjà au complet !", NO_CAPACITY_NOW="Aucune capacité de chargement disponible pour le moment !", NO_MORE_CAPACITY="Plus de capacité pour charger des caisses !", CANNOT_LOAD_NONE_OR_FULL="Chargement impossible : aucune caisse trouvée ou capacité épuisée.", NEED_TO_LAND_OR_HOVER_LOAD="Vous devez atterrir ou rester en vol stationnaire pour charger !", HOVER_OVER_CRATES="Survolez les caisses en stationnaire pour les récupérer !", LAND_OR_HOVER_OVER_CRATES="Atterrissez ou survolez les caisses en stationnaire pour les récupérer !", MUST_LAND_OR_HOVER_CRATES="Vous devez atterrir ou rester en stationnaire pour charger les caisses !", NEED_TO_LAND_BUILD="Vous devez atterrir / vous arrêter pour construire quelque chose, Pilote !", NOT_CLOSE_ENOUGH_LOGISTICS="Vous n'êtes pas assez proche d'une zone logistique !", NOT_CLOSE_ENOUGH_DROP="Vous n'êtes pas assez proche d'une zone de largage !", NOT_CLOSE_ENOUGH_ZONE_NM="Négatif, vous devez être à moins de %d nm d'une zone !", CANNOT_BUILD_LOADING_AREA="Vous ne pouvez pas construire dans une zone de chargement, Pilote !", OPEN_DOORS_LOAD_CARGO="Vous devez ouvrir la/les porte(s) pour charger du fret !", OPEN_DOORS_LOAD_TROOPS="Vous devez ouvrir la/les porte(s) pour embarquer des troupes !", OPEN_DOORS_EXTRACT_TROOPS="Vous devez ouvrir la/les porte(s) pour extraire des troupes !", OPEN_DOORS_UNLOAD_TROOPS="Vous devez ouvrir la/les porte(s) pour débarquer des troupes !", OPEN_DOORS_DROP_CARGO="Vous devez ouvrir la/les porte(s) pour larguer du fret !", ALL_GONE="Désolé, tous les %s sont épuisés !", RAN_OUT_OF="Désolé, nous n'avons plus de %s !", CARGO_NOT_AVAILABLE_ZONE="Le fret demandé n'est pas disponible dans cette zone !", ENOUGH_CRATES_NEARBY="Il y a déjà suffisamment de caisses à proximité ! Occupez-vous d'abord de celles-ci !", NO_CRATES_WITHIN="Aucune caisse (chargeable) dans un rayon de %d mètres !", NO_CRATES_WITHIN_PLAIN="Aucune caisse dans un rayon de %d mètres !", NO_CRATES_IN_RANGE="Aucune caisse trouvée à portée !", NO_NAMED_CRATES_IN_RANGE="Aucune caisse \"%s\" trouvée à portée !", NO_LOADABLE_CRATES="Désolé, aucune caisse chargeable à proximité ou poids maximum atteint !", NO_UNITS_TO_EXTRACT="Aucune unité assez proche pour être extraite !", NO_UNIT_CONFIG="Aucune configuration d'unité trouvée pour %s", CANT_ONBOARD="Impossible d'embarquer %s", TOO_MANY_UNITS_NEARBY="Vous avez déjà %d unités à proximité !", NO_CRATE_GROUPS="Aucun groupe de caisses trouvé pour cette unité !", NO_CRATE_SET="Aucun ensemble de caisses trouvé ou index invalide !", NO_CRATE_IN_SET="Aucune caisse trouvée dans cet ensemble !", NO_TROOP_CHUNK="Aucun bloc de fret de troupes trouvé pour l'ID %d !", TROOP_CHUNK_EMPTY="Le bloc de fret de troupes pour l'ID %d est vide !", NOTHING_LOADED="Rien de chargé !\nLimite de troupes : %d | Limite de caisses : %d | Limite en poids : %d kg", NOTHING_LOADED_AIRDROP="Rien de chargé ou paramètres de largage non respectés !", NOTHING_LOADED_HOVER="Rien de chargé ou paramètres de vol stationnaire non respectés !", NOTHING_IN_STOCK="Rien en stock !", NOTHING_TO_PACK="Rien à charger à cette distance, Pilote !", NOTHING_TO_REMOVE="Rien à retirer à cette distance, Pilote !", ROGER_ZONE="Compris, zone %s %s !", HOVER_PARAMS_METRIC="Paramètres stationnaires (autochargement/largage) :\n - Hauteur min. %dm \n - Hauteur max. %dm \n - Vitesse max. 2m/s \n - Dans les paramètres : %s", HOVER_PARAMS_IMPERIAL="Paramètres stationnaires (autochargement/largage) :\n - Hauteur min. %dft \n - Hauteur max. %dft \n - Vitesse max. 6ft/s \n - Dans les paramètres : %s", FLIGHT_PARAMS_IMPERIAL="Paramètres de vol (largage aérien) :\n - Hauteur min. %dft \n - Hauteur max. %dft \n - Dans les paramètres : %s", FLIGHT_PARAMS_METRIC="Paramètres de vol (largage aérien) :\n - Hauteur min. %dm \n - Hauteur max. %dm \n - Dans les paramètres : %s", REPORT_CRATES_FOUND="Caisses trouvées à proximité :", REPORT_REMOVING_CRATES="Suppression des caisses à proximité :", REPORT_TRANSPORT_CHECKOUT="Fiche de contrôle transport", REPORT_INVENTORY="Fiche d'inventaire", REPORT_BUILD_CHECKLIST="Checklist caisses constructibles", REPORT_REPAIR_CHECKLIST="Checklist réparations", REPORT_BEACONS="Balises de zone actives", REPORT_SECTION_TROOPS=" -- TROUPES --", REPORT_SECTION_CRATES=" -- CAISSES --", REPORT_SECTION_CRATES_GC=" -- CAISSES chargées via équipe au sol --", REPORT_SECTION_NONE=" A U C U N", REPORT_SECTION_NONE_ALT=" --- Aucun trouvé ! ---", REPORT_SECTION_NONE_REPAIR=" --- Aucun trouvé ---", REPORT_GC_LOADABLE_HINT="Probablement chargeable via l’équipe au sol (F8)", REPORT_TOTAL_MASS="Masse totale : %s kg. Chargeable : %s kg.", REPORT_TROOPS_CRATES_COUNT="Troupes : %d(%d), Caisses : %d(%d)", REPORT_TROOPS_CRATETYPES_COUNT="Troupes : %d, Types de caisses : %d", REPORT_ROW_TROOP="Troupe : %s taille %d", REPORT_ROW_CRATE="Caisse : %s %d/%d", REPORT_ROW_CRATE_SIZE1="Caisse : %s taille 1", REPORT_ROW_GC_CRATE="Caisses chargées par l'équipe au sol : %s taille 1", REPORT_ROW_DROPPED_CRATE="Caisses larguées pour %s, %dkg", REPORT_ROW_CRATE_KG="Caisses pour %s, %dkg", REPORT_ROW_CRATE_REMOVED="Caisses pour %s, %dkg retirées", REPORT_ROW_UNIT_STOCK="Unités : %s | Soldats : %d | Stock : %s", REPORT_ROW_TYPE_CRATE_STOCK="Type : %s | Caisses par ensemble : %d | Stock : %s", REPORT_ROW_TYPE_STOCK="Type : %s | Stock : %s", REPORT_ROW_BUILD_CHECK="Type : %s | Requis : %d | Trouvé : %d | Constructible : %s", REPORT_ROW_REPAIR_CHECK="Type : %s | Requis : %d | Trouvé : %d | Réparable : %s", REPORT_ROW_BEACON=" %s | FM %s Mhz | VHF %s KHz | UHF %s Mhz ", WEIGHT_LIMIT="Limite de poids atteinte", CRATE_LIMIT="Limite de caisses atteinte", MENU_CTLD="CTLD", MENU_MANAGE_TROOPS="Gérer les troupes", MENU_MANAGE_CRATES="Gérer les caisses", MENU_MANAGE_UNITS="Gérer les unités", MENU_LOAD_TROOPS="Embarquer troupes", MENU_DROP_TROOPS="Déposer troupes", MENU_DROP_ALL_TROOPS="Déposer TOUTES les troupes", MENU_EXTRACT_TROOPS="Extraire troupes", MENU_DROP_N_TROOPS="Déposer (%d) %s", MENU_GET_CRATES="Récupérer caisses", MENU_GET="Récupérer", MENU_GET_AND_LOAD="Récupérer et charger", MENU_GET_ANYWAY="Récupérer quand même", MENU_PARTIALLY_LOAD="Chargement partiel", MENU_OUT_OF_STOCK="Rupture de stock", MENU_TROOP_LIMIT="Limite de troupes atteinte", MENU_LOAD_CRATES="Charger caisses", MENU_LOAD_ALL="Tout charger", MENU_SHOW_LOADABLE_CRATES="Afficher caisses chargeables", MENU_NO_CRATES_FOUND_RESCAN="Aucune caisse trouvée ! Rescanner ?", MENU_USE_C130_LOAD="Utiliser le système de chargement C-130", MENU_LOAD_SINGLE="Charger", MENU_DROP_CRATES="Larguer caisses", MENU_DROP_ALL_CRATES="Larguer TOUTES les caisses", MENU_DROP="Larguer", MENU_DROP_AND_BUILD="Larguer et construire", MENU_DROP_N_SETS="Larguer %d ensemble%s", MENU_NO_CRATES_TO_DROP="Aucune caisse à larguer !", MENU_BUILD_CRATES="Construire caisses", MENU_REPAIR="Réparer", MENU_PACK_CRATES="Emballer caisses", MENU_PACK="Emballer", MENU_SCAN_PACKABLE_UNITS="Scanner unités emballables à proximité", MENU_NO_PACKABLE_UNITS_FOUND_RESCAN="Aucune unité emballable trouvée ! Rescanner ?", MENU_PACK_ALL="Emballer à proximité", MENU_PACK_AND_LOAD="Emballer et charger", MENU_PACK_AND_LOAD_ALL="Emballer et charger à proximité", MENU_PACK_AND_REMOVE="Emballer et retirer", MENU_PACK_AND_REMOVE_ALL="Emballer et retirer à proximité", MENU_REMOVE_CRATES="Retirer caisses", MENU_REMOVE_CRATES_NEARBY="Retirer caisses proches", MENU_LIST_CRATES_NEARBY="Lister caisses proches", MENU_CRATES_NEEDED="%d caisse%s %s (%dkg)", MENU_CRATE_SINGLE="%s (%dkg)", MENU_GET_UNITS="Récupérer unités", MENU_REMOVE_UNITS_NEARBY="Retirer les unités proches", MENU_LIST_BOARDED_CARGO="Lister le fret embarqué", MENU_INVENTORY="Inventaire", MENU_LIST_ZONE_BEACONS="Lister les balises de zones actives", MENU_SMOKES_FLARES_BEACONS="Fumigènes, Fusées, Balises", MENU_SMOKE_ZONES_NEARBY="Fumigène sur les zones proches", MENU_DROP_SMOKE_NOW="Poser fumigène maintenant", MENU_RED_SMOKE="Fumigène rouge", MENU_BLUE_SMOKE="Fumigène bleu", MENU_GREEN_SMOKE="Fumigène vert", MENU_ORANGE_SMOKE="Fumigène orange", MENU_WHITE_SMOKE="Fumigène blanc", MENU_FLARE_ZONES_NEARBY="Baliser zones proches", MENU_FIRE_FLARE_NOW="Tirer une fusée maintenant", MENU_DROP_BEACON_NOW="Poser une balise maintenant", MENU_SHOW_FLIGHT_PARAMS="Afficher paramètres de vol", MENU_SHOW_HOVER_PARAMS="Afficher les paramètres stationnaire", STOCK_NONE="aucun", STOCK_UNLIMITED="illimité", BUILD_YES="OUI", BUILD_NO="NON", }, ES={ CRATE_LOADED_GROUNDCREW="Contenedor %s cargado por el quipo de tierra.", CRATE_UNLOADED_GROUNDCREW="Contenedor %s descargado por el quipo de tierra.", CRATE_LOADED_ID="Contenedor ID %d para %s cargado.", LOADED_FULL="Cargado %d %s.", LOADED_SETS_LEFTOVER="Cargado %d %s(s), de %d contenedor(es) restante(s).", LOADED_SETS="Cargado %d %s(s).", LOADED_PARTIAL="Cargado sólo %d/%d contenedor(es) de %s.", LOADED_PARTIAL_LIMIT="Cargado sólo %d/%d contenedor(es) de %s. Límite de carga alcanzado.", LOADED_BATCH="Cargado %d %s.", LOADED_BATCH_PARTIAL="Some sets could not be fully loaded.", DROPPED_FULL="Entregado %d %s.", DROPPED_SETS_LEFTOVER="Entregado %d %s(s), de %d conenedor(es) restante(s).", DROPPED_SETS="Entregado %d %s(s).", DROPPED_PARTIAL="Entregado %d/%d contenedor(es) de %s.", DROPPED_INTO_ACTION="¡Soltados %s en acción!", DROPPED_BEACON="Entregado %s | FM %s Mhz | VHF %s KHz | UHF %s Mhz ", CRATES_POSITIONED="%d contenedores para %s servidos cerca de ti.", CRATES_DROPPED="%d contenedores para %s han sido entregados.", BOARDED="¡%s a bordo!", BOARDING="¡%s entrando!", TROOPS_RETURNED="¡Las tropas han vuelto a la base!", TROOPS_LABEL="tropas", ENGINEERS_LABEL="ingenieros", DEPLOYED_NEAR_YOU="%s han sido servidas cerca de tí.", UNITS_REMOVED="%s ha sido eliminado", BUILD_STARTED="Construcción comenzada, listo en %d segundos.", REPAIR_STARTED="Reparación comenzaza usando %s, tardará %d segundos.", NO_UNIT_TO_REPAIR="No hay unidades cercanas que necesiten reparación.", CANT_REPAIR_WITH="No se puede reparar esta unidad con %s", CRATES_MOVE_BEFORE_BUILD="*** Los contenedores deben ser movidos antes de construirse.", CHOPPER_CANNOT_CARRY="Lo siento, este helicóptero no puede transportar contenedores.", TOO_HEAVY="Lo siento, esta carga es muy pesada.", FULLY_LOADED="Lo siento, vamos hasta arriba.", CRAMMED="Lo siento, vamos a tope.", NO_CAPACITY_NOW="No queda capacidad para cargar más.", NO_MORE_CAPACITY="No queda capacidad para cargar más contenedores.", CANNOT_LOAD_NONE_OR_FULL="No se pueden cargar contenedores: n hay o no queda capacidad.", NEED_TO_LAND_OR_HOVER_LOAD="Necesitas aterrizar o mantenerte en estacionario para cargar.", HOVER_OVER_CRATES="Mantente en estacionario sobre el contenedor para recogerlo.", LAND_OR_HOVER_OVER_CRATES="Aterriza o mantente en estacionario para recoger el contenedor.", MUST_LAND_OR_HOVER_CRATES="Necesitas aterrizar o mantenerte en eestacionario para cargar contenedores.", NEED_TO_LAND_BUILD="Necesitas aterrizar/parar para construir algo.", NOT_CLOSE_ENOUGH_LOGISTICS="No estás cerca de una zona de logística.", NOT_CLOSE_ENOUGH_DROP="No estás en una zona de entrega.", NOT_CLOSE_ENOUGH_ZONE_NM="Negativo, tienes que estar a menos de %dnm de la zona.", CANNOT_BUILD_LOADING_AREA="No puedes construir en la zona de carga.", OPEN_DOORS_LOAD_CARGO="Necesitas abrir las puertas para cargar.", OPEN_DOORS_LOAD_TROOPS="Necesitas abrir las puertas para que embarquen las tropas.", OPEN_DOORS_EXTRACT_TROOPS="Necesitas abrir las puertas para poder sacar a las tropas de aquí.", OPEN_DOORS_UNLOAD_TROOPS="Necesitas abrir las puertas para que desembarquen las tropas.", OPEN_DOORS_DROP_CARGO="Necesitas abrir las puertas para descargar la carga.", ALL_GONE="Lo siento, todos %s se han servido.", RAN_OUT_OF="Lo siento, nos hemos quedad sin %s", CARGO_NOT_AVAILABLE_ZONE="La carga solicitada no está disponible en esta zona.", ENOUGH_CRATES_NEARBY="Hay contenedores cerca de ti listas. Encárgate primero de ellos.", NO_CRATES_WITHIN="No ha contenedores (cargables) en %d metros.", NO_CRATES_WITHIN_PLAIN="No hay contenedores en %d metros.", NO_CRATES_IN_RANGE="No se han encontrado contenedores en rango.", NO_NAMED_CRATES_IN_RANGE="No se han encontrado \"%s\" conenedores en rango.", NO_LOADABLE_CRATES="Lo siento, no hay contenedores cercanos o se ha alcanzado el peso máximo.", NO_UNITS_TO_EXTRACT="No hay unidades cercanas para extracción.", NO_UNIT_CONFIG="No se ha encontrado configuración de unidad para %s", CANT_ONBOARD="No puede subir %s", TOO_MANY_UNITS_NEARBY="Ya tienes %d unidades próximas.", NO_CRATE_GROUPS="No se han encontrado grupos de contendeores para esta unidad.", NO_CRATE_SET="No se ha encontrado contenedor o su index es inválido.", NO_CRATE_IN_SET="No se ha encontrado contenedor para este set.", NO_TROOP_CHUNK="No se han encontrado tropas para el id %d!", TROOP_CHUNK_EMPTY="Troop chunk is empty for ID %d!", NOTHING_LOADED="Nada cargado.\nLímite tropas: %d | Límite contenedores: %d | Peso límite: %d kg.", NOTHING_LOADED_AIRDROP="Nada cargado o no estás en parámetros de lanzamiento aéreo.", NOTHING_LOADED_HOVER="Nada cargado o no estás en parámetros de estacionario.", NOTHING_IN_STOCK="¡Nada en stock!", NOTHING_TO_PACK="Nada para empaquetar a esta distancia.", NOTHING_TO_REMOVE="Nada para eliminar a esta distancia.", ROGER_ZONE="Recibido, %s cona %s!", HOVER_PARAMS_METRIC="Parámetros en estacionario (autocarga/suelta):\n - Altura mínima %dm \n - Altura máxima %dm \n - Velocidad máxima 2mps \n - En parámetros: %s", HOVER_PARAMS_IMPERIAL="Parámetros en estacionario (autocarga/suelta):\n - Altura mínima %dft \n - Altura máxima %dft \n - Velocidad máxima 6ftps \n - En parámetros: %s", FLIGHT_PARAMS_IMPERIAL="Parámetros vuelo (lanzamiento aéreo):\n - Altura mínima %dft \n - Altura máxima %dft \n - En parámetros: %s", FLIGHT_PARAMS_METRIC="Parámetros vuelo (lanzamiento aéreo):\n - Altura mínima %dm \n - Altura máxima %dm \n - En parámetros: %s", REPORT_CRATES_FOUND="Contenedores encontrados cerca:", REPORT_REMOVING_CRATES="Contenedores eliminados cerca:", REPORT_TRANSPORT_CHECKOUT="Informe de transporte", REPORT_INVENTORY="Inventario", REPORT_BUILD_CHECKLIST="Contenedores construibles", REPORT_REPAIR_CHECKLIST="Reparaciones", REPORT_BEACONS="Active Zone Beacons", REPORT_SECTION_TROOPS=" -- TROPAS --", REPORT_SECTION_CRATES=" -- CONTENEDORES --", REPORT_SECTION_CRATES_GC=" -- Contenedores cargados por equipo de tierra --", REPORT_SECTION_NONE=" N A D A", REPORT_SECTION_NONE_ALT=" --- Nada encontrado ---", REPORT_SECTION_NONE_REPAIR=" --- Nada encontrado ---", REPORT_GC_LOADABLE_HINT="Probablemente cargable por el equipo de tierra (F8)", REPORT_TOTAL_MASS="Peso total: %s kg. Cargable: %s kg.", REPORT_TROOPS_CRATES_COUNT="Tropas: %d(%d), Contenedores: %d(%d)", REPORT_TROOPS_CRATETYPES_COUNT="Tropas: %d, Tipos contenedores: %d", REPORT_ROW_TROOP="Tropas: %s tamaño %d", REPORT_ROW_CRATE="Contenedores: %s %d/%d", REPORT_ROW_CRATE_SIZE1="Contenedores: %s tamaño 1", REPORT_ROW_GC_CRATE="Contenedores cargados: %s tamaño 1", REPORT_ROW_DROPPED_CRATE="Entregado contenedor para %s, %dkg", REPORT_ROW_CRATE_KG="Contenedor para %s, %dkg", REPORT_ROW_CRATE_REMOVED="Contenedor para %s, %dkg eliminado", REPORT_ROW_UNIT_STOCK="Unidad: %s | Soldados: %d | Stock: %s", REPORT_ROW_TYPE_CRATE_STOCK="Tipo: %s | Contenedores por set: %d | Stock: %s", REPORT_ROW_TYPE_STOCK="Tipo: %s | Stock: %s", REPORT_ROW_BUILD_CHECK="Tipo: %s | Rquiere %d | Encontrados %d | Construible %s", REPORT_ROW_REPAIR_CHECK="Tipo: %s | Requiere %d | Encontrados %d | Reparable %s", REPORT_ROW_BEACON=" %s | FM %s Mhz | VHF %s KHz | UHF %s Mhz ", WEIGHT_LIMIT="Alcanzado límite de pesoWeight limit reached", CRATE_LIMIT="Alcanzado límite contenedores", MENU_CTLD="CTLD", MENU_MANAGE_TROOPS="Gestionar tropas", MENU_MANAGE_CRATES="Gestionar contenedores", MENU_MANAGE_UNITS="Gestionar unidades", MENU_LOAD_TROOPS="Cargar tropas", MENU_DROP_TROOPS="Entregar tropas", MENU_DROP_ALL_TROOPS="Entregar TODAS tropas", MENU_EXTRACT_TROOPS="Extraer tropas", MENU_DROP_N_TROOPS="Soltar (%d) %s", MENU_GET_CRATES="Solicitar contenedores", MENU_GET="Solicitar", MENU_GET_AND_LOAD="Solicitar y cargar", MENU_GET_ANYWAY="Solicitar de todas formas", MENU_PARTIALLY_LOAD="Carga parcial", MENU_OUT_OF_STOCK="Sin stock", MENU_TROOP_LIMIT="Limite de tropas alcanzado", MENU_LOAD_CRATES="Cargar contenedores", MENU_LOAD_ALL="Cargar TODO", MENU_SHOW_LOADABLE_CRATES="Mostrar contenedores carbables", MENU_NO_CRATES_FOUND_RESCAN="Contenedores no encontrados, ¿buscar?", MENU_USE_C130_LOAD="Usar sistmea de carga del C-130", MENU_LOAD_SINGLE="Cargar", MENU_DROP_CRATES="Soltar cargas", MENU_DROP_ALL_CRATES="Soltar TODAS cargas", MENU_DROP="Soltar", MENU_DROP_AND_BUILD="Soltar y constuir", MENU_DROP_N_SETS="Soltar %d Set %s", MENU_NO_CRATES_TO_DROP="No hay cargas para soltar", MENU_BUILD_CRATES="Construir contenedores", MENU_REPAIR="Reparar", MENU_PACK_CRATES="Empaquetar cargas", MENU_PACK="Empaquetar", MENU_SCAN_PACKABLE_UNITS="Buscar unidades empaquetables cercanas", MENU_NO_PACKABLE_UNITS_FOUND_RESCAN="No se encontraron unidades empaquetables. ¿Buscar de nuevo?", MENU_PACK_ALL="Empaquetar cercanas", MENU_PACK_AND_LOAD="Empaquetar y cargar", MENU_PACK_AND_LOAD_ALL="Empaquetar y cargar cercanas", MENU_PACK_AND_REMOVE="Empaquetar y eliminar", MENU_PACK_AND_REMOVE_ALL="Empaquetar y eliminar cercanas", MENU_REMOVE_CRATES="Eliminar cargas", MENU_REMOVE_CRATES_NEARBY="Eliminar cargas cercanas", MENU_LIST_CRATES_NEARBY="Listar cargas cercanas", MENU_CRATES_NEEDED="%d contenedor%s %s (%dkg)", MENU_CRATE_SINGLE="%s (%dkg)", MENU_GET_UNITS="Obtener unidades", MENU_REMOVE_UNITS_NEARBY="Eliminar unidades cercanas", MENU_LIST_BOARDED_CARGO="Lista de cargas a bordo", MENU_INVENTORY="Inventario", MENU_LIST_ZONE_BEACONS="Lista de balizas activas", MENU_SMOKES_FLARES_BEACONS="Humos, Bengalas, Balizas", MENU_SMOKE_ZONES_NEARBY="Humo en zonas cercanas", MENU_DROP_SMOKE_NOW="Lanzar humo ahora", MENU_RED_SMOKE="Humo rojo", MENU_BLUE_SMOKE="Humo azul", MENU_GREEN_SMOKE="Humo verde", MENU_ORANGE_SMOKE="Humo naranja", MENU_WHITE_SMOKE="Humo blanco", MENU_FLARE_ZONES_NEARBY="Bengalas en zonas cercanas", MENU_FIRE_FLARE_NOW="Disparar bengala ahora", MENU_DROP_BEACON_NOW="Soltar baliza ahora", MENU_SHOW_FLIGHT_PARAMS="Mostrar parámetros de vuelo", MENU_SHOW_HOVER_PARAMS="Mostrar parámetros estacionario", STOCK_NONE="Nada", STOCK_UNLIMITED="ilimitado", BUILD_YES="SI", BUILD_NO="NO", }, ["PT-BR"]={ CRATE_LOADED_GROUNDCREW="Caixa %s carregada pela equipe de solo!", CRATE_UNLOADED_GROUNDCREW="Caixa %s descarregada pela equipe de solo!", CRATE_LOADED_ID="Caixa ID %d para %s carregada!", LOADED_FULL="%d %s carregado.", LOADED_SETS_LEFTOVER="%d %s carregado(s), com %d caixa(s) sobrando.", LOADED_SETS="%d %s carregado(s).", LOADED_PARTIAL="Carregado apenas %d/%d caixa(s) de %s.", LOADED_PARTIAL_LIMIT="Carregado apenas %d/%d caixa(s) de %s. O limite de carga foi atingido!", LOADED_BATCH="%d %s carregado.", LOADED_BATCH_PARTIAL="Alguns conjuntos não puderam ser carregados completamente.", DROPPED_FULL="%d %s solto.", DROPPED_SETS_LEFTOVER="%d %s solto(s), com %d caixa(s) sobrando.", DROPPED_SETS="%d %s solto(s).", DROPPED_PARTIAL="%d/%d caixa(s) de %s solta(s).", DROPPED_INTO_ACTION="Unidades desembarcadas para a ação: %s!", DROPPED_BEACON="Baliza %s posicionada | FM %s Mhz | VHF %s KHz | UHF %s Mhz ", CRATES_POSITIONED="%d caixas para %s foram posicionadas perto de você!", CRATES_DROPPED="%d caixas para %s foram soltas!", BOARDED="%s embarcou!", BOARDING="%s embarcando!", TROOPS_RETURNED="As tropas retornaram à base!", TROOPS_LABEL="tropas", ENGINEERS_LABEL="engenheiros", DEPLOYED_NEAR_YOU="%s foram posicionados perto de você!", UNITS_REMOVED="%s foram removidos", BUILD_STARTED="Construção iniciada, pronta em %d segundos!", REPAIR_STARTED="Reparo iniciado usando %s, levando %d segundos", NO_UNIT_TO_REPAIR="Nenhuma unidade perto o suficiente para reparar!", CANT_REPAIR_WITH="Não é possível reparar esta unidade com %s", CRATES_MOVE_BEFORE_BUILD="*** As caixas precisam ser movidas antes da construção!", CHOPPER_CANNOT_CARRY="Desculpe, este helicóptero não pode carregar caixas!", TOO_HEAVY="Desculpe, isso é pesado demais para carregar!", FULLY_LOADED="Desculpe, estamos totalmente carregados!", CRAMMED="Desculpe, já estamos lotados!", NO_CAPACITY_NOW="Sem capacidade para carregar mais agora!", NO_MORE_CAPACITY="Não há mais capacidade para carregar caixas!", CANNOT_LOAD_NONE_OR_FULL="Não é possível carregar caixas: nenhuma encontrada ou sem capacidade restante.", NEED_TO_LAND_OR_HOVER_LOAD="Você precisa pousar ou pairar na posição para carregar!", HOVER_OVER_CRATES="Paire sobre as caixas para pegá-las!", LAND_OR_HOVER_OVER_CRATES="Pouse ou paire sobre as caixas para pegá-las!", MUST_LAND_OR_HOVER_CRATES="Você deve pousar ou pairar para carregar caixas!", NEED_TO_LAND_BUILD="Você precisa pousar / parar para construir algo, piloto!", NOT_CLOSE_ENOUGH_LOGISTICS="Você não está perto o suficiente de uma zona logística!", NOT_CLOSE_ENOUGH_DROP="Você não está perto o suficiente de uma zona de lançamento!", NOT_CLOSE_ENOUGH_ZONE_NM="Negativo, precisa estar a menos de %d nm de uma zona!", CANNOT_BUILD_LOADING_AREA="Você não pode construir em uma área de carregamento, piloto!", OPEN_DOORS_LOAD_CARGO="Você precisa abrir a(s) porta(s) para carregar carga!", OPEN_DOORS_LOAD_TROOPS="Você precisa abrir a(s) porta(s) para carregar tropas!", OPEN_DOORS_EXTRACT_TROOPS="Você precisa abrir a(s) porta(s) para extrair tropas!", OPEN_DOORS_UNLOAD_TROOPS="Você precisa abrir a(s) porta(s) para descarregar tropas!", OPEN_DOORS_DROP_CARGO="Você precisa abrir a(s) porta(s) para soltar carga!", ALL_GONE="Desculpe, todos os %s acabaram!", RAN_OUT_OF="Desculpe, ficamos sem %s", CARGO_NOT_AVAILABLE_ZONE="A carga solicitada não está disponível nesta zona!", ENOUGH_CRATES_NEARBY="Já há caixas suficientes por perto! Cuide delas primeiro!", NO_CRATES_WITHIN="Nenhuma caixa carregável em um raio de %d metros!", NO_CRATES_WITHIN_PLAIN="Nenhuma caixa em um raio de %d metros!", NO_CRATES_IN_RANGE="Nenhuma caixa encontrada no alcance!", NO_NAMED_CRATES_IN_RANGE="Nenhuma caixa \"%s\" encontrada no alcance!", NO_LOADABLE_CRATES="Desculpe, nenhuma caixa carregável por perto ou peso máximo de carga atingido!", NO_UNITS_TO_EXTRACT="Nenhuma unidade perto o suficiente para extrair!", NO_UNIT_CONFIG="Nenhuma configuração de unidade encontrada para %s", CANT_ONBOARD="Não é possível embarcar %s", TOO_MANY_UNITS_NEARBY="Você já tem %d unidades por perto!", NO_CRATE_GROUPS="Nenhum grupo de caixas encontrado para esta unidade!", NO_CRATE_SET="Nenhum conjunto de caixas encontrado ou índice inválido!", NO_CRATE_IN_SET="Nenhuma caixa encontrada nesse conjunto!", NO_TROOP_CHUNK="Nenhum bloco de carga de tropas encontrado para ID %d!", TROOP_CHUNK_EMPTY="O bloco de tropas está vazio para ID %d!", NOTHING_LOADED="Nada carregado!\nLimite de tropas: %d | Limite de caixas %d | Limite de peso %d kg", NOTHING_LOADED_AIRDROP="Nada carregado ou fora dos parâmetros de lançamento aéreo!", NOTHING_LOADED_HOVER="Nada carregado ou fora dos parâmetros de pairado!", NOTHING_IN_STOCK="Nada em estoque!", NOTHING_TO_PACK="Nada para empacotar nesta distância, piloto!", NOTHING_TO_REMOVE="Nada para remover nesta distância, piloto!", ROGER_ZONE="Entendido, zona %s %s!", HOVER_PARAMS_METRIC="Parâmetros de pairado (carregamento automático/soltar):\n - Altura mínima %dm \n - Altura máxima %dm \n - Velocidade máxima 2mps \n - Dentro dos parâmetros: %s", HOVER_PARAMS_IMPERIAL="Parâmetros de pairado (carregamento automático/soltar):\n - Altura mínima %dft \n - Altura máxima %dft \n - Velocidade máxima 6ftps \n - Dentro dos parâmetros: %s", FLIGHT_PARAMS_IMPERIAL="Parâmetros de voo (lançamento aéreo):\n - Altura mínima %dft \n - Altura máxima %dft \n - Dentro dos parâmetros: %s", FLIGHT_PARAMS_METRIC="Parâmetros de voo (lançamento aéreo):\n - Altura mínima %dm \n - Altura máxima %dm \n - Dentro dos parâmetros: %s", REPORT_CRATES_FOUND="Caixas encontradas por perto:", REPORT_REMOVING_CRATES="Removendo caixas encontradas por perto:", REPORT_TRANSPORT_CHECKOUT="Ficha de verificação de transporte", REPORT_INVENTORY="Ficha de inventário", REPORT_BUILD_CHECKLIST="Checklist de caixas construíveis", REPORT_REPAIR_CHECKLIST="Checklist de reparos", REPORT_BEACONS="Balizas de zona ativas", REPORT_SECTION_TROOPS=" -- TROPAS --", REPORT_SECTION_CRATES=" -- CAIXAS --", REPORT_SECTION_CRATES_GC=" -- CAIXAS carregadas pela equipe de solo --", REPORT_SECTION_NONE=" N E N H U M", REPORT_SECTION_NONE_ALT=" --- Nada encontrado! ---", REPORT_SECTION_NONE_REPAIR=" --- Nada encontrado ---", REPORT_GC_LOADABLE_HINT="Provavelmente carregável pela equipe de solo (F8)", REPORT_TOTAL_MASS="Massa total: %s kg. Carregável: %s kg.", REPORT_TROOPS_CRATES_COUNT="Tropas: %d(%d), Caixas: %d(%d)", REPORT_TROOPS_CRATETYPES_COUNT="Tropas: %d, Tipos de caixas: %d", REPORT_ROW_TROOP="Tropa: %s tamanho %d", REPORT_ROW_CRATE="Caixa: %s %d/%d", REPORT_ROW_CRATE_SIZE1="Caixa: %s tamanho 1", REPORT_ROW_GC_CRATE="Caixa carregada pela equipe de solo: %s tamanho 1", REPORT_ROW_DROPPED_CRATE="Caixa solta para %s, %dkg", REPORT_ROW_CRATE_KG="Caixa para %s, %dkg", REPORT_ROW_CRATE_REMOVED="Caixa para %s, %dkg removida", REPORT_ROW_UNIT_STOCK="Unidade: %s | Soldados: %d | Estoque: %s", REPORT_ROW_TYPE_CRATE_STOCK="Tipo: %s | Caixas por conjunto: %d | Estoque: %s", REPORT_ROW_TYPE_STOCK="Tipo: %s | Estoque: %s", REPORT_ROW_BUILD_CHECK="Tipo: %s | Necessário %d | Encontrado %d | Pode construir %s", REPORT_ROW_REPAIR_CHECK="Tipo: %s | Necessário %d | Encontrado %d | Pode reparar %s", REPORT_ROW_BEACON=" %s | FM %s Mhz | VHF %s KHz | UHF %s Mhz ", WEIGHT_LIMIT="Limite de peso atingido", CRATE_LIMIT="Limite de caixas atingido", MENU_CTLD="CTLD", MENU_MANAGE_TROOPS="Gerenciar tropas", MENU_MANAGE_CRATES="Gerenciar caixas", MENU_MANAGE_UNITS="Gerenciar unidades", MENU_LOAD_TROOPS="Carregar tropas", MENU_DROP_TROOPS="Desembarcar tropas", MENU_DROP_ALL_TROOPS="Desembarcar TODAS as tropas", MENU_EXTRACT_TROOPS="Extrair tropas", MENU_DROP_N_TROOPS="Desembarcar (%d) %s", MENU_GET_CRATES="Solicitar caixas", MENU_GET="Solicitar", MENU_GET_AND_LOAD="Solicitar e carregar", MENU_GET_ANYWAY="Solicitar mesmo assim", MENU_PARTIALLY_LOAD="Carregar parcialmente", MENU_OUT_OF_STOCK="Sem estoque", MENU_TROOP_LIMIT="Limite de tropas atingido", MENU_LOAD_CRATES="Carregar caixas", MENU_LOAD_ALL="Carregar TUDO", MENU_SHOW_LOADABLE_CRATES="Mostrar caixas carregáveis", MENU_NO_CRATES_FOUND_RESCAN="Nenhuma caixa encontrada! Procurar novamente?", MENU_USE_C130_LOAD="Usar sistema de carga do C-130", MENU_LOAD_SINGLE="Carregar", MENU_DROP_CRATES="Soltar caixas", MENU_DROP_ALL_CRATES="Soltar TODAS as caixas", MENU_DROP="Soltar", MENU_DROP_AND_BUILD="Soltar e construir", MENU_DROP_N_SETS="Soltar %d conjunto%s", MENU_NO_CRATES_TO_DROP="Nenhuma caixa para soltar!", MENU_BUILD_CRATES="Construir caixas", MENU_REPAIR="Reparar", MENU_PACK_CRATES="Empacotar caixas", MENU_PACK="Empacotar", MENU_SCAN_PACKABLE_UNITS="Procurar unidades empacotáveis por perto", MENU_NO_PACKABLE_UNITS_FOUND_RESCAN="Nenhuma unidade empacotável encontrada! Procurar novamente?", MENU_PACK_ALL="Empacotar próximas", MENU_PACK_AND_LOAD="Empacotar e carregar", MENU_PACK_AND_LOAD_ALL="Empacotar e carregar próximas", MENU_PACK_AND_REMOVE="Empacotar e remover", MENU_PACK_AND_REMOVE_ALL="Empacotar e remover próximas", MENU_REMOVE_CRATES="Remover caixas", MENU_REMOVE_CRATES_NEARBY="Remover caixas próximas", MENU_LIST_CRATES_NEARBY="Listar caixas próximas", MENU_CRATES_NEEDED="%d caixa%s %s (%dkg)", MENU_CRATE_SINGLE="%s (%dkg)", MENU_GET_UNITS="Solicitar unidades", MENU_REMOVE_UNITS_NEARBY="Remover unidades próximas", MENU_LIST_BOARDED_CARGO="Listar carga embarcada", MENU_INVENTORY="Inventário", MENU_LIST_ZONE_BEACONS="Listar balizas de zona ativas", MENU_SMOKES_FLARES_BEACONS="Fumaças, sinalizadores, balizas", MENU_SMOKE_ZONES_NEARBY="Fumaça em zonas próximas", MENU_DROP_SMOKE_NOW="Lançar fumaça agora", MENU_RED_SMOKE="Fumaça vermelha", MENU_BLUE_SMOKE="Fumaça azul", MENU_GREEN_SMOKE="Fumaça verde", MENU_ORANGE_SMOKE="Fumaça laranja", MENU_WHITE_SMOKE="Fumaça branca", MENU_FLARE_ZONES_NEARBY="Sinalizadores em zonas próximas", MENU_FIRE_FLARE_NOW="Disparar sinalizador agora", MENU_DROP_BEACON_NOW="Posicionar baliza agora", MENU_SHOW_FLIGHT_PARAMS="Mostrar parâmetros de voo", MENU_SHOW_HOVER_PARAMS="Mostrar parâmetros de pairado", STOCK_NONE="nenhum", STOCK_UNLIMITED="ilimitado", BUILD_YES="SIM", BUILD_NO="NÃO", }, TR={ CRATE_LOADED_GROUNDCREW="%s sandığı yer ekibi tarafından yüklendi!", CRATE_UNLOADED_GROUNDCREW="%s sandığı yer ekibi tarafından boşaltıldı!", CRATE_LOADED_ID="Sandık ID %d %s için yüklendi!", LOADED_FULL="%d %s yüklendi.", LOADED_SETS_LEFTOVER="%d %s yüklendi, %d sandık kaldı.", LOADED_SETS="%d %s yüklendi.", LOADED_PARTIAL="Yalnızca %d/%d sandık %s için yüklendi.", LOADED_PARTIAL_LIMIT="Yalnızca %d/%d sandık %s için yüklendi. Kargo limiti artık doldu!", LOADED_BATCH="%d %s yüklendi.", LOADED_BATCH_PARTIAL="Bazı setler tamamen yüklenemedi.", DROPPED_FULL="%d %s bırakıldı.", DROPPED_SETS_LEFTOVER="%d %s bırakıldı, %d sandık kaldı.", DROPPED_SETS="%d %s bırakıldı.", DROPPED_PARTIAL="%d/%d sandık %s için bırakıldı.", DROPPED_INTO_ACTION="Çatışma alanına konuşlandırılanlar: %s!", DROPPED_BEACON="Radyo işaretçisi %s bırakıldı | FM %s Mhz | VHF %s KHz | UHF %s Mhz ", CRATES_POSITIONED="%d sandık %s için yakınınıza yerleştirildi!", CRATES_DROPPED="%d sandık %s için bırakıldı!", BOARDED="%s bindirildi!", BOARDING="%s biniyor!", TROOPS_RETURNED="Birlikler üsse döndü!", TROOPS_LABEL="birlik", ENGINEERS_LABEL="mühendis", DEPLOYED_NEAR_YOU="%s yakınınıza konuşlandırıldı!", UNITS_REMOVED="%s kaldırıldı", BUILD_STARTED="İnşa başladı, %d saniye içinde hazır!", REPAIR_STARTED="%s kullanılarak onarım başladı, %d saniye sürecek", NO_UNIT_TO_REPAIR="Onarmak için yeterince yakın bir birim yok!", CANT_REPAIR_WITH="Bu birim %s ile onarılamaz", CRATES_MOVE_BEFORE_BUILD="*** İnşa etmeden önce sandıkların taşınması gerekiyor!", CHOPPER_CANNOT_CARRY="Üzgünüm, bu helikopter sandık taşıyamaz!", TOO_HEAVY="Üzgünüm, bu yük çok ağır!", FULLY_LOADED="Üzgünüm, tamamen doluyuz!", CRAMMED="Üzgünüm, zaten tıka basa doluyuz!", NO_CAPACITY_NOW="Şu anda daha fazla yüklemek için kapasite yok!", NO_MORE_CAPACITY="Sandık yüklemek için daha fazla kapasite yok!", CANNOT_LOAD_NONE_OR_FULL="Sandıklar yüklenemiyor: ya hiç bulunamadı ya da kapasite kalmadı.", NEED_TO_LAND_OR_HOVER_LOAD="Yüklemek için inmen veya pozisyonda asılı kalman gerekiyor!", HOVER_OVER_CRATES="Sandıkları almak için üzerlerinde asılı kal!", LAND_OR_HOVER_OVER_CRATES="Sandıkları almak için in veya üzerlerinde asılı kal!", MUST_LAND_OR_HOVER_CRATES="Sandık yüklemek için inmen veya asılı kalman gerekiyor!", NEED_TO_LAND_BUILD="Bir şey inşa etmek için inmen / durman gerekiyor, pilot!", NOT_CLOSE_ENOUGH_LOGISTICS="Lojistik bölgesine yeterince yakın değilsin!", NOT_CLOSE_ENOUGH_DROP="Bırakma bölgesine yeterince yakın değilsin!", NOT_CLOSE_ENOUGH_ZONE_NM="Negatif, bir bölgeye %d nm'den daha yakın olmalısın!", CANNOT_BUILD_LOADING_AREA="Yükleme alanında inşa yapamazsın, pilot!", OPEN_DOORS_LOAD_CARGO="Kargo yüklemek için kapı(ları) açman gerekiyor!", OPEN_DOORS_LOAD_TROOPS="Birlik yüklemek için kapı(ları) açman gerekiyor!", OPEN_DOORS_EXTRACT_TROOPS="Birlik tahliye etmek için kapı(ları) açman gerekiyor!", OPEN_DOORS_UNLOAD_TROOPS="Birlik boşaltmak için kapı(ları) açman gerekiyor!", OPEN_DOORS_DROP_CARGO="Kargo bırakmak için kapı(ları) açman gerekiyor!", ALL_GONE="Üzgünüm, tüm %s bitti!", RAN_OUT_OF="Üzgünüm, %s tükendi", CARGO_NOT_AVAILABLE_ZONE="İstenen kargo bu bölgede mevcut değil!", ENOUGH_CRATES_NEARBY="Yakında zaten yeterince sandık var! Önce onlarla ilgilen!", NO_CRATES_WITHIN="%d metre içinde yüklenebilir sandık yok!", NO_CRATES_WITHIN_PLAIN="%d metre içinde sandık yok!", NO_CRATES_IN_RANGE="Menzilde sandık bulunamadı!", NO_NAMED_CRATES_IN_RANGE="Menzilde \"%s\" sandığı bulunamadı!", NO_LOADABLE_CRATES="Üzgünüm, yakında yüklenebilir sandık yok veya maksimum kargo ağırlığına ulaşıldı!", NO_UNITS_TO_EXTRACT="Tahliye etmek için yeterince yakın birim yok!", NO_UNIT_CONFIG="%s için birim yapılandırması bulunamadı", CANT_ONBOARD="%s bindirilemiyor", TOO_MANY_UNITS_NEARBY="Yakında zaten %d birimin var!", NO_CRATE_GROUPS="Bu birim için sandık grubu bulunamadı!", NO_CRATE_SET="Sandık seti bulunamadı veya indeks geçersiz!", NO_CRATE_IN_SET="Bu sette sandık bulunamadı!", NO_TROOP_CHUNK="ID %d için birlik kargo parçası bulunamadı!", TROOP_CHUNK_EMPTY="ID %d için birlik parçası boş!", NOTHING_LOADED="Hiçbir şey yüklü değil!\nBirlik limiti: %d | Sandık limiti %d | Ağırlık limiti %d kg", NOTHING_LOADED_AIRDROP="Hiçbir şey yüklü değil veya havadan bırakma parametreleri dahilinde değil!", NOTHING_LOADED_HOVER="Hiçbir şey yüklü değil veya asılı kalma parametreleri dahilinde değil!", NOTHING_IN_STOCK="Stokta hiçbir şey yok!", NOTHING_TO_PACK="Bu mesafede paketlenecek bir şey yok, pilot!", NOTHING_TO_REMOVE="Bu mesafede kaldırılacak bir şey yok, pilot!", ROGER_ZONE="Anlaşıldı, %s bölgesi %s!", HOVER_PARAMS_METRIC="Asılı kalma parametreleri (otomatik yükleme/bırakma):\n - Minimum irtifa %dm \n - Maksimum irtifa %dm \n - Maksimum hız 2mps \n - Parametreler dahilinde: %s", HOVER_PARAMS_IMPERIAL="Asılı kalma parametreleri (otomatik yükleme/bırakma):\n - Minimum irtifa %dft \n - Maksimum irtifa %dft \n - Maksimum hız 6ftps \n - Parametreler dahilinde: %s", FLIGHT_PARAMS_IMPERIAL="Uçuş parametreleri (havadan bırakma):\n - Minimum irtifa %dft \n - Maksimum irtifa %dft \n - Parametreler dahilinde: %s", FLIGHT_PARAMS_METRIC="Uçuş parametreleri (havadan bırakma):\n - Minimum irtifa %dm \n - Maksimum irtifa %dm \n - Parametreler dahilinde: %s", REPORT_CRATES_FOUND="Yakında bulunan sandıklar:", REPORT_REMOVING_CRATES="Yakında bulunan sandıklar kaldırılıyor:", REPORT_TRANSPORT_CHECKOUT="Taşıma kontrol formu", REPORT_INVENTORY="Envanter formu", REPORT_BUILD_CHECKLIST="İnşa edilebilir sandık kontrol listesi", REPORT_REPAIR_CHECKLIST="Onarım kontrol listesi", REPORT_BEACONS="Aktif bölge radyo işaretçileri", REPORT_SECTION_TROOPS=" -- BİRLİKLER --", REPORT_SECTION_CRATES=" -- SANDIKLAR --", REPORT_SECTION_CRATES_GC=" -- Yer ekibiyle yüklenen SANDIKLAR --", REPORT_SECTION_NONE=" Y O K", REPORT_SECTION_NONE_ALT=" --- Hiçbir şey bulunamadı! ---", REPORT_SECTION_NONE_REPAIR=" --- Hiçbir şey bulunamadı ---", REPORT_GC_LOADABLE_HINT="Muhtemelen yer ekibiyle yüklenebilir (F8)", REPORT_TOTAL_MASS="Toplam kütle: %s kg. Yüklenebilir: %s kg.", REPORT_TROOPS_CRATES_COUNT="Birlikler: %d(%d), Sandıklar: %d(%d)", REPORT_TROOPS_CRATETYPES_COUNT="Birlikler: %d, Sandık tipleri: %d", REPORT_ROW_TROOP="Birlik: %s boyut %d", REPORT_ROW_CRATE="Sandık: %s %d/%d", REPORT_ROW_CRATE_SIZE1="Sandık: %s boyut 1", REPORT_ROW_GC_CRATE="Yer ekibiyle yüklenen sandık: %s boyut 1", REPORT_ROW_DROPPED_CRATE="%s için bırakılan sandık, %dkg", REPORT_ROW_CRATE_KG="%s için sandık, %dkg", REPORT_ROW_CRATE_REMOVED="%s için sandık, %dkg kaldırıldı", REPORT_ROW_UNIT_STOCK="Birim: %s | Askerler: %d | Stok: %s", REPORT_ROW_TYPE_CRATE_STOCK="Tip: %s | Set başına sandık: %d | Stok: %s", REPORT_ROW_TYPE_STOCK="Tip: %s | Stok: %s", REPORT_ROW_BUILD_CHECK="Tip: %s | Gerekli %d | Bulunan %d | İnşa edilebilir %s", REPORT_ROW_REPAIR_CHECK="Tip: %s | Gerekli %d | Bulunan %d | Onarılabilir %s", REPORT_ROW_BEACON=" %s | FM %s Mhz | VHF %s KHz | UHF %s Mhz ", WEIGHT_LIMIT="Ağırlık limitine ulaşıldı", CRATE_LIMIT="Sandık limitine ulaşıldı", MENU_CTLD="CTLD", MENU_MANAGE_TROOPS="Birlikleri yönet", MENU_MANAGE_CRATES="Sandıkları yönet", MENU_MANAGE_UNITS="Birimleri yönet", MENU_LOAD_TROOPS="Birlik yükle", MENU_DROP_TROOPS="Birlik indir", MENU_DROP_ALL_TROOPS="TÜM birlikleri indir", MENU_EXTRACT_TROOPS="Birlik tahliye et", MENU_DROP_N_TROOPS="(%d) %s indir", MENU_GET_CRATES="Sandık talep et", MENU_GET="Talep et", MENU_GET_AND_LOAD="Talep et ve yükle", MENU_GET_ANYWAY="Yine de talep et", MENU_PARTIALLY_LOAD="Kısmen yükle", MENU_OUT_OF_STOCK="Stokta yok", MENU_TROOP_LIMIT="Birlik limitine ulaşıldı", MENU_LOAD_CRATES="Sandık yükle", MENU_LOAD_ALL="TÜMÜNÜ yükle", MENU_SHOW_LOADABLE_CRATES="Yüklenebilir sandıkları göster", MENU_NO_CRATES_FOUND_RESCAN="Sandık bulunamadı! Tekrar tara?", MENU_USE_C130_LOAD="C-130 yükleme sistemini kullan", MENU_LOAD_SINGLE="Yükle", MENU_DROP_CRATES="Sandık bırak", MENU_DROP_ALL_CRATES="TÜM sandıkları bırak", MENU_DROP="Bırak", MENU_DROP_AND_BUILD="Bırak ve inşa et", MENU_DROP_N_SETS="%d set%.0s bırak", MENU_NO_CRATES_TO_DROP="Bırakılacak sandık yok!", MENU_BUILD_CRATES="Sandıkları inşa et", MENU_REPAIR="Onar", MENU_PACK_CRATES="Sandıkları paketle", MENU_PACK="Paketle", MENU_SCAN_PACKABLE_UNITS="Yakındaki paketlenebilir birimleri tara", MENU_NO_PACKABLE_UNITS_FOUND_RESCAN="Paketlenebilir birim bulunamadı! Tekrar tara?", MENU_PACK_ALL="Yakındakileri paketle", MENU_PACK_AND_LOAD="Paketle ve yükle", MENU_PACK_AND_LOAD_ALL="Yakındakileri paketle ve yükle", MENU_PACK_AND_REMOVE="Paketle ve kaldır", MENU_PACK_AND_REMOVE_ALL="Yakındakileri paketle ve kaldır", MENU_REMOVE_CRATES="Sandıkları kaldır", MENU_REMOVE_CRATES_NEARBY="Yakındaki sandıkları kaldır", MENU_LIST_CRATES_NEARBY="Yakındaki sandıkları listele", MENU_CRATES_NEEDED="%d sandık%.0s %s (%dkg)", MENU_CRATE_SINGLE="%s (%dkg)", MENU_GET_UNITS="Birim talep et", MENU_REMOVE_UNITS_NEARBY="Yakındaki birimleri kaldır", MENU_LIST_BOARDED_CARGO="Bindirilmiş kargoyu listele", MENU_INVENTORY="Envanter", MENU_LIST_ZONE_BEACONS="Aktif bölge radyo işaretçilerini listele", MENU_SMOKES_FLARES_BEACONS="Dumanlar, işaret fişekleri, radyo işaretçileri", MENU_SMOKE_ZONES_NEARBY="Yakındaki bölgelerde duman", MENU_DROP_SMOKE_NOW="Şimdi duman işareti bırak", MENU_RED_SMOKE="Kırmızı duman", MENU_BLUE_SMOKE="Mavi duman", MENU_GREEN_SMOKE="Yeşil duman", MENU_ORANGE_SMOKE="Turuncu duman", MENU_WHITE_SMOKE="Beyaz duman", MENU_FLARE_ZONES_NEARBY="Yakındaki bölgelerde işaret fişekleri", MENU_FIRE_FLARE_NOW="Şimdi işaret fişeği ateşle", MENU_DROP_BEACON_NOW="Şimdi radyo işaretçisi bırak", MENU_SHOW_FLIGHT_PARAMS="Uçuş parametrelerini göster", MENU_SHOW_HOVER_PARAMS="Asılı kalma parametrelerini göster", STOCK_NONE="yok", STOCK_UNLIMITED="sınırsız", BUILD_YES="EVET", BUILD_NO="HAYIR", }, RU={ CRATE_LOADED_GROUNDCREW="Ящик %s загружен наземной службой!", CRATE_UNLOADED_GROUNDCREW="Ящик %s выгружен наземной службой!", CRATE_LOADED_ID="Ящик ID %d для %s загружен!", LOADED_FULL="Загружено %d %s.", LOADED_SETS_LEFTOVER="Загружено %d %s, осталось %d ящик(ов).", LOADED_SETS="Загружено %d %s.", LOADED_PARTIAL="Загружено только %d/%d ящик(ов) для %s.", LOADED_PARTIAL_LIMIT="Загружено только %d/%d ящик(ов) для %s. Достигнут лимит груза!", LOADED_BATCH="Загружено %d %s.", LOADED_BATCH_PARTIAL="Некоторые комплекты не удалось загрузить полностью.", DROPPED_FULL="Сброшено %d %s.", DROPPED_SETS_LEFTOVER="Сброшено %d %s, осталось %d ящик(ов).", DROPPED_SETS="Сброшено %d %s.", DROPPED_PARTIAL="Сброшено %d/%d ящик(ов) для %s.", DROPPED_INTO_ACTION="%s сброшено в бой!", DROPPED_BEACON="Сброшен %s | FM %s Mhz | VHF %s KHz | UHF %s Mhz ", CRATES_POSITIONED="%d ящик(ов) для %s размещены рядом с вами!", CRATES_DROPPED="%d ящик(ов) для %s сброшены!", BOARDED="%s на борту!", BOARDING="%s грузится!", TROOPS_RETURNED="Войска вернулись на базу!", TROOPS_LABEL="войска", ENGINEERS_LABEL="инженеры", DEPLOYED_NEAR_YOU="%s развернуты рядом с вами!", UNITS_REMOVED="%s удалены", BUILD_STARTED="Строительство начато, готово через %d секунд!", REPAIR_STARTED="Ремонт начат с использованием %s, займет %d сек", NO_UNIT_TO_REPAIR="Нет достаточно близкого юнита для ремонта!", CANT_REPAIR_WITH="Нельзя отремонтировать этот юнит с помощью %s", CRATES_MOVE_BEFORE_BUILD="*** Ящики нужно переместить перед строительством!", CHOPPER_CANNOT_CARRY="Извините, этот вертолет не может перевозить ящики!", TOO_HEAVY="Извините, это слишком тяжело для загрузки!", FULLY_LOADED="Извините, мы полностью загружены!", CRAMMED="Извините, у нас уже нет места!", NO_CAPACITY_NOW="Сейчас нет места для дополнительной загрузки!", NO_MORE_CAPACITY="Больше нет места для загрузки ящиков!", CANNOT_LOAD_NONE_OR_FULL="Нельзя загрузить ящики: ничего не найдено или нет свободной вместимости.", NEED_TO_LAND_OR_HOVER_LOAD="Нужно приземлиться или зависнуть на месте для загрузки!", HOVER_OVER_CRATES="Зависните над ящиками, чтобы подобрать их!", LAND_OR_HOVER_OVER_CRATES="Приземлитесь или зависните над ящиками, чтобы подобрать их!", MUST_LAND_OR_HOVER_CRATES="Нужно приземлиться или зависнуть, чтобы загрузить ящики!", NEED_TO_LAND_BUILD="Нужно приземлиться / остановиться, чтобы что-то построить, пилот!", NOT_CLOSE_ENOUGH_LOGISTICS="Вы недостаточно близко к логистической зоне!", NOT_CLOSE_ENOUGH_DROP="Вы недостаточно близко к зоне сброса!", NOT_CLOSE_ENOUGH_ZONE_NM="Отказ, нужно быть ближе чем %d nm к зоне!", CANNOT_BUILD_LOADING_AREA="Нельзя строить в зоне погрузки, пилот!", OPEN_DOORS_LOAD_CARGO="Нужно открыть дверь(и), чтобы загрузить груз!", OPEN_DOORS_LOAD_TROOPS="Нужно открыть дверь(и), чтобы загрузить войска!", OPEN_DOORS_EXTRACT_TROOPS="Нужно открыть дверь(и), чтобы эвакуировать войска!", OPEN_DOORS_UNLOAD_TROOPS="Нужно открыть дверь(и), чтобы выгрузить войска!", OPEN_DOORS_DROP_CARGO="Нужно открыть дверь(и), чтобы сбросить груз!", ALL_GONE="Извините, все %s закончились!", RAN_OUT_OF="Извините, у нас закончились %s", CARGO_NOT_AVAILABLE_ZONE="Запрошенный груз недоступен в этой зоне!", ENOUGH_CRATES_NEARBY="Рядом уже достаточно ящиков! Сначала разберитесь с ними!", NO_CRATES_WITHIN="Нет загружаемых ящиков в пределах %d метров!", NO_CRATES_WITHIN_PLAIN="Нет ящиков в пределах %d метров!", NO_CRATES_IN_RANGE="Ящики в радиусе не найдены!", NO_NAMED_CRATES_IN_RANGE="Ящики «%s» в радиусе не найдены!", NO_LOADABLE_CRATES="Извините, рядом нет загружаемых ящиков или достигнут максимальный вес груза!", NO_UNITS_TO_EXTRACT="Нет достаточно близких юнитов для эвакуации!", NO_UNIT_CONFIG="Конфигурация юнита для %s не найдена", CANT_ONBOARD="Нельзя взять на борт %s", TOO_MANY_UNITS_NEARBY="У вас уже есть %d юнитов поблизости!", NO_CRATE_GROUPS="Группы ящиков для этого юнита не найдены!", NO_CRATE_SET="Набор ящиков не найден или индекс недействителен!", NO_CRATE_IN_SET="Ящик в этом наборе не найден!", NO_TROOP_CHUNK="Часть войскового груза с ID %d не найдена!", TROOP_CHUNK_EMPTY="Часть войскового груза с ID %d пуста!", NOTHING_LOADED="Ничего не загружено!\nЛимит войск: %d | Лимит ящиков %d | Лимит веса %d кг", NOTHING_LOADED_AIRDROP="Ничего не загружено или параметры воздушного сброса не соблюдены!", NOTHING_LOADED_HOVER="Ничего не загружено или висение вне допустимых параметров!", NOTHING_IN_STOCK="На складе ничего нет!", NOTHING_TO_PACK="На этой дистанции нечего упаковывать, пилот!", NOTHING_TO_REMOVE="На этой дистанции нечего удалять, пилот!", ROGER_ZONE="Принято, зона %s %s!", HOVER_PARAMS_METRIC="Параметры висения (автозагрузка/сброс):\n - Мин. высота %dм \n - Макс. высота %dм \n - Макс. скорость 2м/с \n - В параметрах: %s", HOVER_PARAMS_IMPERIAL="Параметры висения (автозагрузка/сброс):\n - Мин. высота %dфт \n - Макс. высота %dфт \n - Макс. скорость 6фт/с \n - В параметрах: %s", FLIGHT_PARAMS_IMPERIAL="Параметры полета (воздушный сброс):\n - Мин. высота %dфт \n - Макс. высота %dфт \n - В параметрах: %s", FLIGHT_PARAMS_METRIC="Параметры полета (воздушный сброс):\n - Мин. высота %dм \n - Макс. высота %dм \n - В параметрах: %s", REPORT_CRATES_FOUND="Ящики поблизости:", REPORT_REMOVING_CRATES="Удаление найденных поблизости ящиков:", REPORT_TRANSPORT_CHECKOUT="Транспортная ведомость", REPORT_INVENTORY="Инвентарная ведомость", REPORT_BUILD_CHECKLIST="Чек-лист строящихся ящиков", REPORT_REPAIR_CHECKLIST="Чек-лист ремонта", REPORT_BEACONS="Активные маяки зон", REPORT_SECTION_TROOPS=" -- ВОЙСКА --", REPORT_SECTION_CRATES=" -- ЯЩИКИ --", REPORT_SECTION_CRATES_GC=" -- ЯЩИКИ загружены наземной службой --", REPORT_SECTION_NONE=" Н Е Т", REPORT_SECTION_NONE_ALT=" --- Ничего не найдено! ---", REPORT_SECTION_NONE_REPAIR=" --- Ничего не найдено ---", REPORT_GC_LOADABLE_HINT="Вероятно, можно загрузить наземной службой (F8)", REPORT_TOTAL_MASS="Общая масса: %s кг. Можно загрузить: %s кг.", REPORT_TROOPS_CRATES_COUNT="Войска: %d(%d), Ящики: %d(%d)", REPORT_TROOPS_CRATETYPES_COUNT="Войска: %d, Типы ящиков: %d", REPORT_ROW_TROOP="Войска: %s размер %d", REPORT_ROW_CRATE="Ящик: %s %d/%d", REPORT_ROW_CRATE_SIZE1="Ящик: %s размер 1", REPORT_ROW_GC_CRATE="Ящик загружен НС: %s размер 1", REPORT_ROW_DROPPED_CRATE="Сброшен ящик для %s, %dкг", REPORT_ROW_CRATE_KG="Ящик для %s, %dкг", REPORT_ROW_CRATE_REMOVED="Ящик для %s, %dкг удален", REPORT_ROW_UNIT_STOCK="Юнит: %s | Солдаты: %d | Запас: %s", REPORT_ROW_TYPE_CRATE_STOCK="Тип: %s | Ящиков на комплект: %d | Запас: %s", REPORT_ROW_TYPE_STOCK="Тип: %s | Запас: %s", REPORT_ROW_BUILD_CHECK="Тип: %s | Требуется %d | Найдено %d | Можно строить %s", REPORT_ROW_REPAIR_CHECK="Тип: %s | Требуется %d | Найдено %d | Можно ремонтировать %s", REPORT_ROW_BEACON=" %s | FM %s Mhz | VHF %s KHz | UHF %s Mhz ", WEIGHT_LIMIT="Достигнут лимит веса", CRATE_LIMIT="Достигнут лимит ящиков", MENU_CTLD="CTLD", MENU_MANAGE_TROOPS="Управление войсками", MENU_MANAGE_CRATES="Управление ящиками", MENU_MANAGE_UNITS="Управление юнитами", MENU_LOAD_TROOPS="Загрузить войска", MENU_DROP_TROOPS="Высадить войска", MENU_DROP_ALL_TROOPS="Высадить ВСЕ войска", MENU_EXTRACT_TROOPS="Эвакуировать войска", MENU_DROP_N_TROOPS="Высадить (%d) %s", MENU_GET_CRATES="Получить ящики", MENU_GET="Получить", MENU_GET_AND_LOAD="Получить и загрузить", MENU_GET_ANYWAY="Все равно получить", MENU_PARTIALLY_LOAD="Частично загрузить", MENU_OUT_OF_STOCK="Нет в наличии", MENU_TROOP_LIMIT="Достигнут лимит войск", MENU_LOAD_CRATES="Загрузить ящики", MENU_LOAD_ALL="Загрузить ВСЕ", MENU_SHOW_LOADABLE_CRATES="Показать загружаемые ящики", MENU_NO_CRATES_FOUND_RESCAN="Ящики не найдены! Сканировать снова?", MENU_USE_C130_LOAD="Использовать систему загрузки C-130", MENU_LOAD_SINGLE="Загрузить", MENU_DROP_CRATES="Сбросить ящики", MENU_DROP_ALL_CRATES="Сбросить ВСЕ ящики", MENU_DROP="Сбросить", MENU_DROP_AND_BUILD="Сбросить и построить", MENU_DROP_N_SETS="Сбросить %d комплект%s", MENU_NO_CRATES_TO_DROP="Нет ящиков для сброса!", MENU_BUILD_CRATES="Построить из ящиков", MENU_REPAIR="Ремонт", MENU_PACK_CRATES="Упаковать ящики", MENU_PACK="Упаковать", MENU_SCAN_PACKABLE_UNITS="Сканировать упаковываемые юниты поблизости", MENU_NO_PACKABLE_UNITS_FOUND_RESCAN="Упаковываемые юниты не найдены! Сканировать снова?", MENU_PACK_ALL="Упаковать поблизости", MENU_PACK_AND_LOAD="Упаковать и загрузить", MENU_PACK_AND_LOAD_ALL="Упаковать и загрузить поблизости", MENU_PACK_AND_REMOVE="Упаковать и удалить", MENU_PACK_AND_REMOVE_ALL="Упаковать и удалить поблизости", MENU_REMOVE_CRATES="Удалить ящики", MENU_REMOVE_CRATES_NEARBY="Удалить ящики поблизости", MENU_LIST_CRATES_NEARBY="Список ящиков поблизости", MENU_CRATES_NEEDED="%d ящик%s %s (%dкг)", MENU_CRATE_SINGLE="%s (%dкг)", MENU_GET_UNITS="Получить юниты", MENU_REMOVE_UNITS_NEARBY="Удалить юниты поблизости", MENU_LIST_BOARDED_CARGO="Список груза на борту", MENU_INVENTORY="Инвентарь", MENU_LIST_ZONE_BEACONS="Список активных маяков зон", MENU_SMOKES_FLARES_BEACONS="Дымы, ракеты, маяки", MENU_SMOKE_ZONES_NEARBY="Дым в ближайших зонах", MENU_DROP_SMOKE_NOW="Сбросить дым сейчас", MENU_RED_SMOKE="Красный дым", MENU_BLUE_SMOKE="Синий дым", MENU_GREEN_SMOKE="Зеленый дым", MENU_ORANGE_SMOKE="Оранжевый дым", MENU_WHITE_SMOKE="Белый дым", MENU_FLARE_ZONES_NEARBY="Ракеты в ближайших зонах", MENU_FIRE_FLARE_NOW="Выпустить ракету сейчас", MENU_DROP_BEACON_NOW="Сбросить маяк сейчас", MENU_SHOW_FLIGHT_PARAMS="Показать параметры полета", MENU_SHOW_HOVER_PARAMS="Показать параметры висения", STOCK_NONE="нет", STOCK_UNLIMITED="без ограничений", BUILD_YES="ДА", BUILD_NO="НЕТ", }, ["zh-TW"]={ CRATE_LOADED_GROUNDCREW="地勤已裝載貨箱 %s!", CRATE_UNLOADED_GROUNDCREW="地勤已卸載貨箱 %s!", CRATE_LOADED_ID="貨箱 ID %d(%s)已裝載!", LOADED_FULL="已裝載 %d 個 %s。", LOADED_SETS_LEFTOVER="已裝載 %d 組 %s,剩餘 %d 個貨箱。", LOADED_SETS="已裝載 %d 組 %s。", LOADED_PARTIAL="僅裝載 %d/%d 個 %s 的貨箱。", LOADED_PARTIAL_LIMIT="僅裝載 %d/%d 個 %s 的貨箱,已達載運上限!", LOADED_BATCH="已裝載 %d 個 %s。", LOADED_BATCH_PARTIAL="部分組合無法完全裝載。", DROPPED_FULL="已投放 %d 個 %s。", DROPPED_SETS_LEFTOVER="已投放 %d 組 %s,剩餘 %d 個貨箱。", DROPPED_SETS="已投放 %d 組 %s。", DROPPED_PARTIAL="已投放 %d/%d 個 %s 的貨箱。", DROPPED_INTO_ACTION="已將 %s 投入作戰!", DROPPED_BEACON="已投放 %s | FM %s MHz | VHF %s KHz | UHF %s MHz ", CRATES_POSITIONED="已在你附近部署 %d 個 %s 貨箱!", CRATES_DROPPED="已投放 %d 個 %s 貨箱!", BOARDED="%s 已登機!", BOARDING="%s 正在登機!", TROOPS_RETURNED="部隊已返回基地!", TROOPS_LABEL="部隊", ENGINEERS_LABEL="工程兵", DEPLOYED_NEAR_YOU="%s 已在你附近部署!", UNITS_REMOVED="%s 已移除", BUILD_STARTED="建設開始,將於%d 秒後完成!", REPAIR_STARTED="使用 %s 開始維修,需時 %d 秒", NO_UNIT_TO_REPAIR="附近沒有可維修單位!", CANT_REPAIR_WITH="無法使用 %s 維修此單位", CRATES_MOVE_BEFORE_BUILD="*** 建設前需先移動貨箱!", CHOPPER_CANNOT_CARRY="此直升機無法運載貨箱!", TOO_HEAVY="重量過重,無法裝載!", FULLY_LOADED="已達滿載!", CRAMMED="空間已滿!", NO_CAPACITY_NOW="目前無法再裝載!", NO_MORE_CAPACITY="已無空間裝載貨箱!", CANNOT_LOAD_NONE_OR_FULL="無法裝載貨箱:未找到或已無容量。", NEED_TO_LAND_OR_HOVER_LOAD="需降落或穩定懸停才能裝載!", HOVER_OVER_CRATES="請懸停於貨箱上方進行拾取!", LAND_OR_HOVER_OVER_CRATES="請降落或懸停於貨箱上方進行拾取!", MUST_LAND_OR_HOVER_CRATES="必須降落或懸停才能裝載貨箱!", NEED_TO_LAND_BUILD="飛行員,需降落或停止才能建設!", NOT_CLOSE_ENOUGH_LOGISTICS="距離後勤區域過遠!", NOT_CLOSE_ENOUGH_DROP="距離投放區域過遠!", NOT_CLOSE_ENOUGH_ZONE_NM="距離區域需小於 %d 海浬!", CANNOT_BUILD_LOADING_AREA="無法在裝載區域進行建設!", OPEN_DOORS_LOAD_CARGO="需開啟艙門才能裝載貨物!", OPEN_DOORS_LOAD_TROOPS="需開啟艙門才能裝載部隊!", OPEN_DOORS_EXTRACT_TROOPS="需開啟艙門才能撤離部隊!", OPEN_DOORS_UNLOAD_TROOPS="需開啟艙門才能卸載部隊!", OPEN_DOORS_DROP_CARGO="需開啟艙門才能投放貨物!", ALL_GONE="%s 已全部耗盡!", RAN_OUT_OF="%s 已用盡", CARGO_NOT_AVAILABLE_ZONE="此區域無法取得該貨物!", ENOUGH_CRATES_NEARBY="附近已有足夠貨箱,請先處理現有貨箱!", NO_CRATES_WITHIN="%d 公尺內沒有可裝載貨箱!", NO_CRATES_WITHIN_PLAIN="%d 公尺內沒有貨箱!", NO_CRATES_IN_RANGE="範圍內未發現貨箱!", NO_NAMED_CRATES_IN_RANGE="範圍內未發現「%s」貨箱!", NO_LOADABLE_CRATES="附近沒有可裝載貨箱或已達重量上限!", NO_UNITS_TO_EXTRACT="附近沒有可撤離單位!", NO_UNIT_CONFIG="未找到 %s 的單位設定", CANT_ONBOARD="無法登載 %s", TOO_MANY_UNITS_NEARBY="附近已有 %d 個單位!", NO_CRATE_GROUPS="未找到此單位的貨箱群組!", NO_CRATE_SET="未找到貨箱組或索引無效!", NO_CRATE_IN_SET="該組中沒有貨箱!", NO_TROOP_CHUNK="未找到 ID %d 的部隊資料!", TROOP_CHUNK_EMPTY="ID %d 的部隊資料為空!", NOTHING_LOADED="未裝載任何內容!\n部隊上限:%d | 貨箱上限 %d | 重量上限 %d 公斤", NOTHING_LOADED_AIRDROP="未裝載或不符合空投條件!", NOTHING_LOADED_HOVER="未裝載或未達懸停條件!", NOTHING_IN_STOCK="庫存為空!", NOTHING_TO_PACK="此距離內無可打包目標!", NOTHING_TO_REMOVE="此距離內無可移除目標!", ROGER_ZONE="收到,%s 區域 %s!", HOVER_PARAMS_METRIC="懸停參數(自動裝載/投放):\n - 最低高度 %d 公尺 \n - 最高高度 %d 公尺 \n - 最大速度 2 公尺/秒 \n - 是否符合:%s", HOVER_PARAMS_IMPERIAL="懸停參數(自動裝載/投放):\n - 最低高度 %d 英尺 \n - 最高高度 %d 英尺 \n - 最大速度 6 英尺/秒 \n - 是否符合:%s", FLIGHT_PARAMS_IMPERIAL="飛行參數(空投):\n - 最低高度 %d 英尺 \n - 最高高度 %d 英尺 \n - 是否符合:%s", FLIGHT_PARAMS_METRIC="飛行參數(空投):\n - 最低高度 %d 公尺 \n - 最高高度 %d 公尺 \n - 是否符合:%s", REPORT_CRATES_FOUND="附近發現貨箱:", REPORT_REMOVING_CRATES="移除附近貨箱:", REPORT_TRANSPORT_CHECKOUT="運輸檢查表", REPORT_INVENTORY="庫存報表", REPORT_BUILD_CHECKLIST="建設檢查清單", REPORT_REPAIR_CHECKLIST="維修檢查清單", REPORT_BEACONS="區域信標狀態", REPORT_SECTION_TROOPS=" -- 部隊 --", REPORT_SECTION_CRATES=" -- 貨箱 --", REPORT_SECTION_CRATES_GC=" -- 地勤裝載貨箱 --", REPORT_SECTION_NONE=" 無", REPORT_SECTION_NONE_ALT=" --- 未發現 ---", REPORT_SECTION_NONE_REPAIR=" --- 未發現 ---", REPORT_GC_LOADABLE_HINT="可能可由地勤裝載(F8)", REPORT_TOTAL_MASS="總重量:%s 公斤,可裝載:%s 公斤", REPORT_TROOPS_CRATES_COUNT="部隊:%d(%d),貨箱:%d(%d)", REPORT_TROOPS_CRATETYPES_COUNT="部隊:%d,貨箱種類:%d", REPORT_ROW_TROOP="部隊:%s 人數 %d", REPORT_ROW_CRATE="貨箱:%s %d/%d", REPORT_ROW_CRATE_SIZE1="貨箱:%s 尺寸 1", REPORT_ROW_GC_CRATE="地勤裝載貨箱:%s 尺寸 1", REPORT_ROW_DROPPED_CRATE="已投放 %s 貨箱,%d 公斤", REPORT_ROW_CRATE_KG="%s 貨箱,%d 公斤", REPORT_ROW_CRATE_REMOVED="%s 貨箱,%d 公斤 已移除", REPORT_ROW_UNIT_STOCK="單位:%s | 士兵:%d | 庫存:%s", REPORT_ROW_TYPE_CRATE_STOCK="類型:%s | 每組貨箱:%d | 庫存:%s", REPORT_ROW_TYPE_STOCK="類型:%s | 庫存:%s", REPORT_ROW_BUILD_CHECK="類型:%s | 需求 %d | 已有 %d | 可建設 %s", REPORT_ROW_REPAIR_CHECK="類型:%s | 需求 %d | 已有 %d | 可維修 %s", REPORT_ROW_BEACON=" %s | FM %s MHz | VHF %s KHz | UHF %s MHz ", WEIGHT_LIMIT="已達重量上限", CRATE_LIMIT="已達貨箱上限", MENU_CTLD="後勤管理(CTLD)", MENU_MANAGE_TROOPS="部隊管理", MENU_MANAGE_CRATES="貨箱管理", MENU_MANAGE_UNITS="單位管理", MENU_LOAD_TROOPS="裝載部隊", MENU_DROP_TROOPS="投放部隊", MENU_DROP_ALL_TROOPS="投放全部部隊", MENU_EXTRACT_TROOPS="撤離部隊", MENU_DROP_N_TROOPS="投放 (%d) %s", MENU_GET_CRATES="取得貨箱", MENU_GET="取得", MENU_GET_AND_LOAD="取得並裝載", MENU_GET_ANYWAY="強制取得", MENU_PARTIALLY_LOAD="部分裝載", MENU_OUT_OF_STOCK="無庫存", MENU_TROOP_LIMIT="已達部隊上限", MENU_LOAD_CRATES="裝載貨箱", MENU_LOAD_ALL="全部裝載", MENU_SHOW_LOADABLE_CRATES="顯示可裝載貨箱", MENU_NO_CRATES_FOUND_RESCAN="未發現貨箱,重新掃描?", MENU_USE_C130_LOAD="使用 C-130 裝載系統", MENU_LOAD_SINGLE="裝載", MENU_DROP_CRATES="投放貨箱", MENU_DROP_ALL_CRATES="投放全部貨箱", MENU_DROP="投放", MENU_DROP_AND_BUILD="投放並建設", MENU_DROP_N_SETS="投放 %d 組%s", MENU_NO_CRATES_TO_DROP="沒有可投放貨箱", MENU_BUILD_CRATES="建設貨箱", MENU_REPAIR="維修", MENU_PACK_CRATES="打包貨箱", MENU_PACK="打包", MENU_SCAN_PACKABLE_UNITS="掃描可打包單位", MENU_NO_PACKABLE_UNITS_FOUND_RESCAN="未發現可打包單位,重新掃描?", MENU_PACK_ALL="打包附近", MENU_PACK_AND_LOAD="打包並裝載", MENU_PACK_AND_LOAD_ALL="打包並裝載附近", MENU_PACK_AND_REMOVE="打包並移除", MENU_PACK_AND_REMOVE_ALL="打包並移除附近", MENU_REMOVE_CRATES="移除貨箱", MENU_REMOVE_CRATES_NEARBY="移除附近貨箱", MENU_LIST_CRATES_NEARBY="列出附近貨箱", MENU_CRATES_NEEDED="%d 個貨箱%s %s(%d 公斤)", MENU_CRATE_SINGLE="%s(%d 公斤)", MENU_GET_UNITS="取得單位", MENU_REMOVE_UNITS_NEARBY="移除附近單位", MENU_LIST_BOARDED_CARGO="查看已裝載貨物", MENU_INVENTORY="庫存", MENU_LIST_ZONE_BEACONS="列出區域信標", MENU_SMOKES_FLARES_BEACONS="煙霧、照明彈、信標", MENU_SMOKE_ZONES_NEARBY="標記附近區域", MENU_DROP_SMOKE_NOW="立即投放煙霧", MENU_RED_SMOKE="紅色煙霧", MENU_BLUE_SMOKE="藍色煙霧", MENU_GREEN_SMOKE="綠色煙霧", MENU_ORANGE_SMOKE="橙色煙霧", MENU_WHITE_SMOKE="白色煙霧", MENU_FLARE_ZONES_NEARBY="標記附近區域(照明彈)", MENU_FIRE_FLARE_NOW="立即發射照明彈", MENU_DROP_BEACON_NOW="立即投放信標", MENU_SHOW_FLIGHT_PARAMS="顯示飛行參數", MENU_SHOW_HOVER_PARAMS="顯示懸停參數", STOCK_NONE="無", STOCK_UNLIMITED="無限制", BUILD_YES="是", BUILD_NO="否", }, ["zh-CN"]={ CRATE_LOADED_GROUNDCREW="地勤已装载货箱 %s!", CRATE_UNLOADED_GROUNDCREW="地勤已卸载货箱 %s!", CRATE_LOADED_ID="货箱 ID %d(%s)已装载!", LOADED_FULL="已装载 %d 个 %s。", LOADED_SETS_LEFTOVER="已装载 %d 组 %s,剩余 %d 个货箱。", LOADED_SETS="已装载 %d 组 %s。", LOADED_PARTIAL="仅装载 %d/%d 个 %s 的货箱。", LOADED_PARTIAL_LIMIT="仅装载 %d/%d 个 %s 的货箱,已达到载重上限!", LOADED_BATCH="已装载 %d 个 %s。", LOADED_BATCH_PARTIAL="部分组合未能完整装载。", DROPPED_FULL="已投放 %d 个 %s。", DROPPED_SETS_LEFTOVER="已投放 %d 组 %s,剩余 %d 个货箱。", DROPPED_SETS="已投放 %d 组 %s。", DROPPED_PARTIAL="已投放 %d/%d 个 %s 的货箱。", DROPPED_INTO_ACTION="已将 %s 投入作战!", DROPPED_BEACON="已投放 %s | FM %s MHz | VHF %s KHz | UHF %s MHz ", CRATES_POSITIONED="已在你附近部署 %d 个 %s 货箱!", CRATES_DROPPED="已投放 %d 个 %s 货箱!", BOARDED="%s 已登机!", BOARDING="%s 正在登机!", TROOPS_RETURNED="部队已返回基地!", TROOPS_LABEL="部队", ENGINEERS_LABEL="工兵", DEPLOYED_NEAR_YOU="%s 已在你附近部署!", UNITS_REMOVED="%s 已移除", BUILD_STARTED="开始建造,%d 秒后完成!", REPAIR_STARTED="使用 %s 开始维修,耗时 %d 秒", NO_UNIT_TO_REPAIR="附近没有可维修单位!", CANT_REPAIR_WITH="无法使用 %s 修复该单位", CRATES_MOVE_BEFORE_BUILD="*** 建造前请先移动货箱!", CHOPPER_CANNOT_CARRY="该直升机无法运输货箱!", TOO_HEAVY="重量超限,无法装载!", FULLY_LOADED="已满载!", CRAMMED="空间已满!", NO_CAPACITY_NOW="当前无法继续装载!", NO_MORE_CAPACITY="没有剩余空间可装载货箱!", CANNOT_LOAD_NONE_OR_FULL="无法装载货箱:未找到或已满载。", NEED_TO_LAND_OR_HOVER_LOAD="需要降落或稳定悬停才能装载!", HOVER_OVER_CRATES="请在货箱上方悬停进行拾取!", LAND_OR_HOVER_OVER_CRATES="请降落或在货箱上方悬停进行拾取!", MUST_LAND_OR_HOVER_CRATES="必须降落或悬停才能装载货箱!", NEED_TO_LAND_BUILD="飞行员,请降落或停止后再建造!", NOT_CLOSE_ENOUGH_LOGISTICS="距离后勤区域过远!", NOT_CLOSE_ENOUGH_DROP="距离投放区域过远!", NOT_CLOSE_ENOUGH_ZONE_NM="距离区域必须小于 %d 海里!", CANNOT_BUILD_LOADING_AREA="无法在装载区域建造!", OPEN_DOORS_LOAD_CARGO="需要打开舱门才能装载货物!", OPEN_DOORS_LOAD_TROOPS="需要打开舱门才能装载部队!", OPEN_DOORS_EXTRACT_TROOPS="需要打开舱门才能撤离部队!", OPEN_DOORS_UNLOAD_TROOPS="需要打开舱门才能卸载部队!", OPEN_DOORS_DROP_CARGO="需要打开舱门才能投放货物!", ALL_GONE="%s 已全部耗尽!", RAN_OUT_OF="%s 已用完", CARGO_NOT_AVAILABLE_ZONE="该区域无法获取此类货物!", ENOUGH_CRATES_NEARBY="附近已有足够货箱,请先处理现有货箱!", NO_CRATES_WITHIN="%d 米内没有可装载货箱!", NO_CRATES_WITHIN_PLAIN="%d 米内没有货箱!", NO_CRATES_IN_RANGE="范围内未发现货箱!", NO_NAMED_CRATES_IN_RANGE="范围内未发现“%s”货箱!", NO_LOADABLE_CRATES="附近没有可装载货箱或已超重!", NO_UNITS_TO_EXTRACT="附近没有可撤离单位!", NO_UNIT_CONFIG="未找到 %s 的单位配置", CANT_ONBOARD="无法装载 %s", TOO_MANY_UNITS_NEARBY="附近已有 %d 个单位!", NO_CRATE_GROUPS="未找到该单位对应的货箱组!", NO_CRATE_SET="未找到货箱组或索引无效!", NO_CRATE_IN_SET="该组中没有货箱!", NO_TROOP_CHUNK="未找到 ID %d 的部队数据!", TROOP_CHUNK_EMPTY="ID %d 的部队数据为空!", NOTHING_LOADED="未装载任何内容!\n部队上限:%d | 货箱上限 %d | 重量上限 %d 公斤", NOTHING_LOADED_AIRDROP="未装载或不满足空投条件!", NOTHING_LOADED_HOVER="未装载或未满足悬停条件!", NOTHING_IN_STOCK="库存为空!", NOTHING_TO_PACK="该范围内没有可打包目标!", NOTHING_TO_REMOVE="该范围内没有可移除目标!", ROGER_ZONE="收到,%s 区域 %s!", HOVER_PARAMS_METRIC="悬停参数(自动装载/投放):\n - 最低高度 %d 米 \n - 最高高度 %d 米 \n - 最大速度 2 米/秒 \n - 是否符合条件:%s", HOVER_PARAMS_IMPERIAL="悬停参数(自动装载/投放):\n - 最低高度 %d 英尺 \n - 最高高度 %d 英尺 \n - 最大速度 6 英尺/秒 \n - 是否符合条件:%s", FLIGHT_PARAMS_IMPERIAL="飞行参数(空投):\n - 最低高度 %d 英尺 \n - 最高高度 %d 英尺 \n - 是否符合条件:%s", FLIGHT_PARAMS_METRIC="飞行参数(空投):\n - 最低高度 %d 米 \n - 最高高度 %d 米 \n - 是否符合条件:%s", REPORT_CRATES_FOUND="附近发现货箱:", REPORT_REMOVING_CRATES="删除附近货箱:", REPORT_TRANSPORT_CHECKOUT="运输检查表", REPORT_INVENTORY="库存报表", REPORT_BUILD_CHECKLIST="建造检查清单", REPORT_REPAIR_CHECKLIST="维修检查清单", REPORT_BEACONS="区域信标状态", REPORT_SECTION_TROOPS=" -- 部队 --", REPORT_SECTION_CRATES=" -- 货箱 --", REPORT_SECTION_CRATES_GC=" -- 地勤装载货箱 --", REPORT_SECTION_NONE=" 无", REPORT_SECTION_NONE_ALT=" --- 未发现 ---", REPORT_SECTION_NONE_REPAIR=" --- 未发现 ---", REPORT_GC_LOADABLE_HINT="可能可由地勤装载(F8)", REPORT_TOTAL_MASS="总重量:%s 公斤,可装载:%s 公斤", REPORT_TROOPS_CRATES_COUNT="部队:%d(%d),货箱:%d(%d)", REPORT_TROOPS_CRATETYPES_COUNT="部队:%d,货箱种类:%d", REPORT_ROW_TROOP="部队:%s 人数 %d", REPORT_ROW_CRATE="货箱:%s %d/%d", REPORT_ROW_CRATE_SIZE1="货箱:%s 尺寸 1", REPORT_ROW_GC_CRATE="地勤装载货箱:%s 尺寸 1", REPORT_ROW_DROPPED_CRATE="已投放 %s 货箱,%d 公斤", REPORT_ROW_CRATE_KG="%s 货箱,%d 公斤", REPORT_ROW_CRATE_REMOVED="%s 货箱,%d 公斤 已移除", REPORT_ROW_UNIT_STOCK="单位:%s | 士兵:%d | 库存:%s", REPORT_ROW_TYPE_CRATE_STOCK="类型:%s | 每组货箱:%d | 库存:%s", REPORT_ROW_TYPE_STOCK="类型:%s | 库存:%s", REPORT_ROW_BUILD_CHECK="类型:%s | 需求 %d | 已有 %d | 可建造 %s", REPORT_ROW_REPAIR_CHECK="类型:%s | 需求 %d | 已有 %d | 可维修 %s", REPORT_ROW_BEACON=" %s | FM %s MHz | VHF %s KHz | UHF %s MHz ", WEIGHT_LIMIT="已达重量上限", CRATE_LIMIT="已达货箱上限", MENU_CTLD="后勤系统(CTLD)", MENU_MANAGE_TROOPS="部队管理", MENU_MANAGE_CRATES="货箱管理", MENU_MANAGE_UNITS="单位管理", MENU_LOAD_TROOPS="装载部队", MENU_DROP_TROOPS="投放部队", MENU_DROP_ALL_TROOPS="投放全部部队", MENU_EXTRACT_TROOPS="撤离部队", MENU_DROP_N_TROOPS="投放 (%d) %s", MENU_GET_CRATES="获取货箱", MENU_GET="获取", MENU_GET_AND_LOAD="获取并装载", MENU_GET_ANYWAY="强制获取", MENU_PARTIALLY_LOAD="部分装载", MENU_OUT_OF_STOCK="无库存", MENU_TROOP_LIMIT="已达部队上限", MENU_LOAD_CRATES="装载货箱", MENU_LOAD_ALL="全部装载", MENU_SHOW_LOADABLE_CRATES="显示可装载货箱", MENU_NO_CRATES_FOUND_RESCAN="未检测到货箱,是否重新扫描?", MENU_USE_C130_LOAD="使用 C-130 装载系统", MENU_LOAD_SINGLE="装载", MENU_DROP_CRATES="投放货箱", MENU_DROP_ALL_CRATES="投放全部货箱", MENU_DROP="投放", MENU_DROP_AND_BUILD="投放并建造", MENU_DROP_N_SETS="投放 %d 组%s", MENU_NO_CRATES_TO_DROP="无可投放货箱", MENU_BUILD_CRATES="建造货箱", MENU_REPAIR="维修", MENU_PACK_CRATES="打包货箱", MENU_PACK="打包", MENU_SCAN_PACKABLE_UNITS="扫描可打包单位", MENU_NO_PACKABLE_UNITS_FOUND_RESCAN="未发现可打包单位,重新扫描?", MENU_PACK_ALL="打包附近单位", MENU_PACK_AND_LOAD="打包并装载", MENU_PACK_AND_LOAD_ALL="打包并装载附近", MENU_PACK_AND_REMOVE="打包并移除", MENU_PACK_AND_REMOVE_ALL="打包并移除附近", MENU_REMOVE_CRATES="移除货箱", MENU_REMOVE_CRATES_NEARBY="删除附近货箱", MENU_LIST_CRATES_NEARBY="显示附近货箱", MENU_CRATES_NEEDED="%d 个货箱%s %s(%d 公斤)", MENU_CRATE_SINGLE="%s(%d 公斤)", MENU_GET_UNITS="获取单位", MENU_REMOVE_UNITS_NEARBY="移除附近单位", MENU_LIST_BOARDED_CARGO="查看已装载货物", MENU_INVENTORY="库存", MENU_LIST_ZONE_BEACONS="显示区域信标", MENU_SMOKES_FLARES_BEACONS="烟雾、照明弹、信标", MENU_SMOKE_ZONES_NEARBY="标记附近区域", MENU_DROP_SMOKE_NOW="立即释放烟雾", MENU_RED_SMOKE="红色烟雾", MENU_BLUE_SMOKE="蓝色烟雾", MENU_GREEN_SMOKE="绿色烟雾", MENU_ORANGE_SMOKE="橙色烟雾", MENU_WHITE_SMOKE="白色烟雾", MENU_FLARE_ZONES_NEARBY="标记附近区域(照明弹)", MENU_FIRE_FLARE_NOW="立即发射照明弹", MENU_DROP_BEACON_NOW="立即投放信标", MENU_SHOW_FLIGHT_PARAMS="显示飞行参数", MENU_SHOW_HOVER_PARAMS="显示悬停参数", STOCK_NONE="无", STOCK_UNLIMITED="无限制", BUILD_YES="是", BUILD_NO="否", }, } 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, useFIFOLimitReplacement=false, allheligroupset=nil, topmenuname="CSAR", ADFRadioPwr=1000, PilotWeight=80, CreateRadioBeacons=true, UserSetGroup=nil, AllowIRStrobe=false, IRStrobeRuntime=300, FARPRescueDistance=500, EnableMenuSmokeMASH=true, locale="en", } CSAR.Messages={ EN={ HEARYOULONG="%s: %s. I hear you! Finally, that is music in my ears!\nI'll pop a smoke when you are %s away.\nLand or hover by the smoke.", HEARYOUSHORT="%s: %s. I hear you! Finally, that is music in my ears!\nRequest a flare or smoke if you need.", WEARECRAMMED="%s, %s. We\'re already crammed with %d guys! Sorry!", IAMINHELO="%s: %s I\'m in! Get to the MASH ASAP! ", YOUARECLOSE="%s: %s. You\'re close now! Land or hover at the smoke.", YOUARECLOSELONG="%s: %s. You\'re close now! Land in a safe place, I will go there ", WAITMORE="Wait till %s gets in. \nETA %d more seconds.", OPENTHEDOORIN="Open the door to let me in!", HOVERABOVE="Hovering above %s. \n\nHold hover for %d seconds to winch them up. \n\nIf the countdown stops you\'re too far away!", TOOHIGHWINCH="Too high to winch %s \nReduce height and hover for 10 seconds!", OPENTHEDOOROUT="Open the door to let me out!", TAKECLINIC="%s: The %d pilot(s) have been taken to the\nmedical clinic. Good job!", KILOMETERS=" kilometer", NAUTMILES=" nautical miles", FIRINGFLARE="%s - Firing signal flare at your %s o\'clock. Distance %s", NOPILOTSINRANGE="No Pilots within %s", IRSTROBE="%s - IR Strobe active at your %s o\'clock. Distance %s", POPPINGSMOKE="%s - Popping smoke at your %s o\'clock. Distance %s", POPPINGSMOKEMASH="%s - Popping smoke at the closest rescue point: %s", NORESCUEPOINTWITHIN="No rescue point within %s", NOPILOTSONBOARD="No Rescued Pilots onboard", MENUTOP="CSAR", MENUACTIVE="List Active CSAR", MENUCHECK="Check Onboard", MENUFLARE="Request Signal Flare", MENUSMOKE="Request Smoke", MENUSTROBE="Request IR Strobe", MENUMASH="Smoke Closest MASH", BOARDED="Onboard - RTB to FARP/Airfield or MASH: ", MAYDAY="MAYDAY MAYDAY! %s is down. ", CONTACT="Troops In Contact. %s requests CASEVAC. ", PICKUPZONE="Pickup Zone at %s.", REQUESTSAR="%s requests SAR at %s, beacon at %.2f KHz!", REQUESTSARBEACON="%s requests SAR at %s, beacon at %.2f KHz!", KHZ="kilo hertz", FILLAT="at", FILLFOR="for", }, DE={ HEARYOULONG="%s: %s. Ich höre Sie! Endlich, das ist Musik in meinen Ohren!\nIch zünde eine Rauchgranate, wenn Sie %s entfernt sind.\nLanden Sie oder hovern Sie beim Rauch.", HEARYOUSHORT="%s: %s. Ich höre Sie! Endlich, das ist Musik in meinen Ohren!\nFordern Sie eine Leuchtrakete oder Rauch an, falls nötig.", IAMINHELO="%s: %s Ich bin drin! Jetzt sofort zum Lazarett! ", YOUARECLOSE="%s: %s. Sie sind jetzt nah dran! Landen Sie oder hovern Sie beim Rauch.", YOUARECLOSELONG="%s: %s. Sie sind jetzt nah dran! Landen Sie an einem sicheren Ort, ich komme dorthin.", WAITMORE="Warten Sie, bis %s eingestiegen ist. \nNoch %d Sekunden.", OPENTHEDOORIN="Öffnen Sie die Tür, damit ich einsteigen kann!", HOVERABOVE="Hovere über %s. \n\nPositon für %d Sekunden halten, um zu winschen. \n\nWenn der Countdown stoppt, sind Sie zu weit entfernt!", TOOHIGHWINCH="Zu hoch, um %s zu winschen. \nHöhe reduzieren und %d Sekunden halten!", OPENTHEDOOROUT="Öffnen Sie die Tür, damit ich aussteigen kann!", FIRINGFLARE="%s - Feuere Signalrakete auf Ihrer %s-Uhr-Position ab. Entfernung %s", NOPILOTSINRANGE="Keine Piloten in %s Reichweite", IRSTROBE="%s - IR-Blinklicht aktiv auf Ihrer %s-Uhr-Position. Entfernung %s", POPPINGSMOKE="%s - Zünde Rauchgranate auf Ihrer %s-Uhr-Position. Entfernung %s", POPPINGSMOKEMASH="%s - Zünde Rauchgranate am nächsten Rettungspunkt: %s", NORESCUEPOINTWITHIN="Kein Rettungspunkt innerhalb von %s", NOPILOTSONBOARD="Keine geretteten Piloten an Bord", MENUTOP="CSAR", MENUACTIVE="Aktive CSAR", MENUCHECK="Ladung prüfen", MENUFLARE="Signalrakete anfordern", MENUSMOKE="Rauch anfordern", MENUSTROBE="IR-Blinklicht anfordern", MENUMASH="Nächstes MASH markieren", WEARECRAMMED="%s, %s. Wir sind bereits voll mit %d Mann! Tut mir leid!", TAKECLINIC="%s: Die %d Pilot(en) wurden ins\nLazarett gebracht. Gute Arbeit!", KILOMETERS=" Kilometer", NAUTMILES=" Meilen", BOARDED="An Bord - RTB zu FARP/Flugplatz oder Lazarett: ", MAYDAY="MAYDAY MAYDAY! %s ist abgestürzt. ", CONTACT="Truppen im Kontakt. %s fordern CASEVAC an. ", PICKUPZONE="Aufnahmezone bei %s.", REQUESTSAR="%s fordert SAR bei %s an, ADF %.2f KHz!", REQUESTSARBEACON="%s fordert SAR bei %s an, ADF %.2f KHz!", KHZ="Kilohertz", FILLAT="bei", FILLFOR="für", }, FR={ HEARYOULONG="%s: %s. Je vous entends! Enfin, c'est de la musique dans mes oreilles!\nJe lancerai une fumée quand vous serez à %s.\nAtterrissez ou survolez la fumée.", HEARYOUSHORT="%s: %s. Je vous entends! Enfin, c'est de la musique dans mes oreilles!\nDemandez une fusée éclairante ou de la fumée si nécessaire.", IAMINHELO="%s: %s Je suis à bord! Direction le MASH immédiatement! ", YOUARECLOSE="%s: %s. Vous êtes proche maintenant! Atterrissez ou survolez la fumée.", YOUARECLOSELONG="%s: %s. Vous êtes proche maintenant! Atterrissez dans un endroit sûr, j'y vais.", WAITMORE="Attendez que %s monte à bord. \nEncore %d secondes.", OPENTHEDOORIN="Ouvrez la porte pour me laisser entrer!", HOVERABOVE="En vol stationnaire au-dessus de %s. \n\nMaintenir la position pendant %d secondes pour hélitreuiller. \n\nSi le compte à rebours s'arrête, vous êtes trop loin!", TOOHIGHWINCH="Trop haut pour hélitreuiller %s. \nRéduisez l'altitude et maintenez la position pendant %d secondes!", OPENTHEDOOROUT="Ouvrez la porte pour me laisser sortir!", FIRINGFLARE="%s - Tir d'une fusée éclairante à vos %s heures. Distance %s", NOPILOTSINRANGE="Aucun pilote dans un rayon de %s", IRSTROBE="%s - Stroboscope IR actif à vos %s heures. Distance %s", POPPINGSMOKE="%s - Lancement de fumigène à vos %s heures. Distance %s", POPPINGSMOKEMASH="%s - Lancement de fumigène au point de sauvetage le plus proche: %s", NORESCUEPOINTWITHIN="Aucun point de sauvetage dans un rayon de %s", NOPILOTSONBOARD="Aucun pilote secouru à bord", MENUTOP="CSAR", MENUACTIVE="Lister les CSAR actifs", MENUCHECK="Vérifier qui est à bord", MENUFLARE="Demander une fusée éclairante", MENUSMOKE="Demander un fumigène", MENUSTROBE="Demander un stroboscope IR", MENUMASH="Fumée au MASH le plus proche", WEARECRAMMED="%s, %s. Nous sommes déjà pleins avec %d hommes! Désolé!", TAKECLINIC="%s: Les %d pilote(s) ont été transportés à \n l'hôpital. Bon travail!", KILOMETERS=" kilomètres", NAUTMILES=" milles nautiques", BOARDED="À bord - RTB vers FARP/Aérodrome ou MASH: ", MAYDAY="MAYDAY MAYDAY ! %s est à terre. ", CONTACT="Troupes au contact. %s demande un CASEVAC. ", PICKUPZONE="Zone de ramassage à %s.", REQUESTSAR="%s demande un SAR à %s, balise à %.2f KHz!", REQUESTSARBEACON="%s demande un SAR à %s, balise à %.2f KHz!", KHZ="kilohertz", FILLAT="au", FILLFOR="pour", }, } 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["UH-60L_DAP"]=2 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.AircraftType["AH-6J"]=2 CSAR.AircraftType["MH-6J"]=2 CSAR.AircraftType["Ka-50_3"]=0 CSAR.AircraftType["Ka-50"]=0 CSAR.AircraftType["AV8BNA"]=0 CSAR.version="1.1.39" 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=500 self.max_units=6 self.useprefix=true self.csarPrefix={"helicargo","MEDEVAC"} self.template=Template or"generic" self.mashprefix={"MASH"} self.EnableMenuSmokeMASH=true 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=500 self.PilotWeight=80 self.UserSetGroup=nil self.useSRS=false self.SRSPath="E:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio" 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.SRSBackend=MSRS.Backend.SRSEXE self.SRSProvider=MSRS.Provider.WINDOWS self.SRSSpeed=1.0 self.coordinate=nil self.locale="en" 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:_InitLocalization() self:T(self.lid.."_InitLocalization") self.gettext=TEXTANDSOUND:New("CSAR","en") for locale,table in pairs(self.Messages)do local Locale=string.lower(tostring(locale)) self:T("**** Adding locale: "..Locale) for ID,Text in pairs(table)do self:T(string.format('Adding ID %s',tostring(ID))) self.gettext:AddEntry(Locale,tostring(ID),Text) end end 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 local text=self.gettext:GetEntry("MAYDAY",self.locale) text=string.format(text,_typeName) self:_DisplayToAllSAR(text,self.coalition,self.messageTime) else local text=self.gettext:GetEntry("CONTACT",self.locale) text=string.format(text,_typeName) self:_DisplayToAllSAR(text,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 _playerName==nil or _playerName==""then _playerName="AI MIA" 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 local initdcscoord=nil local initcoord=nil if _event.id==EVENTS.Ejection and _event.TgtDCSUnit then initdcscoord=_event.TgtDCSUnit:getPoint() initcoord=COORDINATE:NewFromVec3(initdcscoord) self:T({initdcscoord}) elseif _event.IniDCSUnit then initdcscoord=_event.IniDCSUnit:getPoint() initcoord=COORDINATE:NewFromVec3(initdcscoord) self:T({initdcscoord}) end if _event.IniPlayerName then local PilotTable=self.downedPilots local _foundPilot=nil for _,_pilot in pairs(PilotTable)do if _pilot.player==_event.IniPlayerName and _pilot.alive==true then _foundPilot=_pilot break end end if _foundPilot then self:T("Downed pilot already exists!") _foundPilot.group:Destroy(false) self:_RemoveNameFromDownedPilots(_foundPilot.name) self:_CheckDownedPilotTable() end end if self.limitmaxdownedpilots and self:_ReachedPilotLimit()then self:T("Maxed Downed Pilot!") return self end local wetfeet=false 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,self.suppressmessages,"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) local IsHeloBase=false local ABName=_place:GetName() if ABName and string.find(ABName,"^H")then IsHeloBase=true end self:_ScheduledSARFlight(_event.IniUnitName,_event.IniGroupName,true,true,IsHeloBase) 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:T({coalition=_coalition,country=_country,coord=_LandingPos,name=_unitname,player=_event.IniPlayerName,freq=_freq}) self:_AddCsar(_coalition,_country,_LandingPos,nil,_unitname,_event.IniPlayerName,_freq,self.suppressmessages,"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 request=self.gettext:GetEntry("REQUESTSAR",self.locale) local _text=string.format(request,_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,self.SRSBackend) local request=self.gettext:GetEntry("REQUESTSARBEACON",self.locale) local _text=string.format(request,_groupName,coordtext,_freqk) self:_DisplayToAllSAR(_text,self.coalition,self.messageTime,true,false,self.SRSBackend) end else local request=self.gettext:GetEntry("PICKUPZONE",self.locale) local _text=string.format(request,_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,self.SRSBackend) local _text=string.format(request,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 local text=self.gettext:GetEntry("WEARECRAMMED",self.locale) self:_DisplayMessageToSAR(_heliUnit,string.format(text,_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) local text=self.gettext:GetEntry("IAMINHELO",self.locale) self:_DisplayMessageToSAR(_heliUnit,string.format(text,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 local text=self.gettext:GetEntry("YOUARECLOSE",self.locale) self:_DisplayMessageToSAR(_heliUnit,string.format(text,self:_GetCustomCallSign(_heliName),_pilotName),self.messageTime,false,true) else local text=self.gettext:GetEntry("YOUARECLOSELONG",self.locale) self:_DisplayMessageToSAR(_heliUnit,string.format(text,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) local text=self.gettext:GetEntry("HOVERABOVE",self.locale) text=string.format(text,_pilotName,_time) self:_DisplayMessageToSAR(_heliUnit,text,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 local text=self.gettext:GetEntry("OPENTHEDOORIN",self.locale) self:_DisplayMessageToSAR(_heliUnit,text,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) local text=self.gettext:GetEntry("TOOHIGHWINCH",self.locale) self:_DisplayMessageToSAR(_heliUnit,string.format(text,_pilotName),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,IsHeloBase) 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 self.verbose>0 then local debugtext=string.format("Distance %dm | Rescuedist %dm | IsAirport %s | IsInAir %s | IsHeloBase %s\n",_dist,self.FARPRescueDistance,tostring(isairport),tostring(_heliUnit:InAir()),tostring(IsHeloBase)) self:T("*******************************") self:T(debugtext) self:T("*******************************") end 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.distanceself.FARPRescueDistance*1.1 then _shortestDistance=100 end end end end for _,_mashes in pairs(MashSets)do for _,_mashUnit in pairs(_mashes or{})do local _mashcoord if _mashUnit and(not _mashUnit:IsInstanceOf("ZONE_BASE"))and _mashUnit:IsAlive()then _mashcoord=_mashUnit:GetCoordinate() elseif _mashUnit and _mashUnit:IsInstanceOf("ZONE_BASE")then _mashcoord=_mashUnit:GetCoordinate() end _distance=self:_GetDistance(_helicoord,_mashcoord) if _distance~=nil and(_shortestDistance==-1 or _distance<_shortestDistance)then _shortestDistance=_distance MashName=_mashUnit:GetName()or"Unknown" Coordinate=_mashcoord end end end if _shortestDistance~=-1 then return _shortestDistance,MashName,Coordinate else return-1 end end function CSAR:_CheckOnboard(_unitName) self:T(self.lid.." _CheckOnboard") local _unit=self:_GetSARHeli(_unitName) if _unit==nil then return end local _inTransit=self.inTransitGroups[_unitName] if _inTransit==nil then local text=self.gettext:GetEntry("NOPILOTSONBOARD",self.locale) self:_DisplayMessageToSAR(_unit,text,self.messageTime,false,false,true) else local _text=self.gettext:GetEntry("BOARDED",self.locale) for _,_onboard in pairs(self.inTransitGroups[_unitName])do _text=_text.."\n".._onboard.desc end self:_DisplayMessageToSAR(_unit,_text,self.messageTime*2,false,false,true) end return self end function CSAR:_AddMedevacMenuItem() self:T(self.lid.." _AddMedevacMenuItem") local coalition=self.coalition local allheligroupset=self.allheligroupset local _allHeliGroups=allheligroupset:GetSetObjects() local _UnitList={} for _key,_group in pairs(_allHeliGroups)do local _unit=_group:GetFirstUnitAlive() if _unit then if _unit:IsAlive()and _unit:IsPlayer()then local _maxUnits=self.AircraftType[_unit:GetTypeName()] if _maxUnits==nil or _maxUnits>0 then local unitName=_unit:GetName() _UnitList[unitName]=unitName end end end end self.csarUnits=_UnitList for _,_unitName in pairs(self.csarUnits)do local _unit=self:_GetSARHeli(_unitName) if _unit then local _group=_unit:GetGroup() if _group then local groupname=_group:GetName() if self.addedTo[groupname]==nil then self.addedTo[groupname]=true local menuname=self.gettext:GetEntry("MENUTOP",self.locale) menuname=self.topmenuname or menuname local Menu1T=self.gettext:GetEntry("MENUACTIVE",self.locale) local Menu2T=self.gettext:GetEntry("MENUCHECK",self.locale) local Menu3T=self.gettext:GetEntry("MENUFLARE",self.locale) local Menu4T=self.gettext:GetEntry("MENUSMOKE",self.locale) local Menu5T=self.gettext:GetEntry("MENUSTROBE",self.locale) local Menu6T=self.gettext:GetEntry("MENUMASH",self.locale) local _rootPath=MENU_GROUP:New(_group,menuname) local _rootMenu1=MENU_GROUP_COMMAND:New(_group,Menu1T,_rootPath,self._DisplayActiveSAR,self,_unitName) local _rootMenu2=MENU_GROUP_COMMAND:New(_group,Menu2T,_rootPath,self._CheckOnboard,self,_unitName) local _rootMenu3=MENU_GROUP_COMMAND:New(_group,Menu3T,_rootPath,self._SignalFlare,self,_unitName) local _rootMenu4=MENU_GROUP_COMMAND:New(_group,Menu4T,_rootPath,self._Reqsmoke,self,_unitName) if self.AllowIRStrobe then local _rootMenu5=MENU_GROUP_COMMAND:New(_group,Menu5T,_rootPath,self._ReqIRStrobe,self,_unitName):Refresh() end if self.EnableMenuSmokeMASH then local _rootMenu6=MENU_GROUP_COMMAND:New(_group,Menu6T,_rootPath,self._ReqsmokeMash,self,_unitName) else _rootMenu4:Refresh() end end end end end return self end function CSAR:_GetDistance(_point1,_point2) self:T(self.lid.." _GetDistance") if _point1 and _point2 then local distance1=_point1:Get2DDistance(_point2) local distance2=_point1:DistanceFromPointVec2(_point2) MESSAGE:New(string.format("_GetDistance: d1 = %dm | d2 = %dm",distance1,distance2)):ToAllIf(self.verbose>1):ToLogIf(self.verbose>1) 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 CSAR:_GenerateVHFrequencies() self:T(self.lid.." _GenerateVHFrequencies") local FreeVHFFrequencies={} FreeVHFFrequencies=UTILS.GenerateVHFrequencies() self.FreeVHFFrequencies=FreeVHFFrequencies return self end function CSAR:_GenerateADFFrequency() self:T(self.lid.." _GenerateADFFrequency") if#self.FreeVHFFrequencies<=3 then self.FreeVHFFrequencies=self.UsedVHFFrequencies self.UsedVHFFrequencies={} end local _vhf=table.remove(self.FreeVHFFrequencies,math.random(#self.FreeVHFFrequencies)) return _vhf end function CSAR:_GetClockDirection(_heli,_group) self:T(self.lid.." _GetClockDirection") local _playerPosition=_heli:GetCoordinate() local _targetpostions=_group:GetCoordinate() local _heading=_heli:GetHeading() local DirectionVec3=_playerPosition:GetDirectionVec3(_targetpostions) local Angle=_playerPosition:GetAngleDegrees(DirectionVec3) self:T(self.lid.." _GetClockDirection"..tostring(Angle).." "..tostring(_heading)) local hours=0 local clock=12 if _heading and Angle then clock=12 clock=_heading-Angle hours=(clock/30)*-1 clock=12+hours clock=UTILS.Round(clock,0) if clock>12 then clock=clock-12 end end return clock end function CSAR:_AddBeaconToGroup(_group,_freq,BeaconName) 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 Sound="l10n/DEFAULT/"..self.radioSound local vec3=_radioUnit:GetVec3()or _radioUnit:GetPositionVec3()or{x=0,y=0,z=0} self:T(self.lid..string.format("Added Radio Beacon %d Hertz | Name %s | Position {%d,%d,%d}",Frequency,BeaconName,vec3.x,vec3.y,vec3.z)) trigger.action.radioTransmission(Sound,vec3,0,true,Frequency,self.ADFRadioPwr or 500,BeaconName) 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) if group and group:IsAlive()and frequency>0 then else if frequency>0 then trigger.action.stopRadioTransmission(bname) end 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 if self.useFIFOLimitReplacement then local oldIndex=-1 local oldDownedPilot=nil for _index,_downedpilot in pairs(self.downedPilots)do oldIndex=_index oldDownedPilot=_downedpilot break end if oldDownedPilot then oldDownedPilot.group:Destroy(false) oldDownedPilot.alive=false self:_CheckDownedPilotTable() return false end end 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() self.staticmashes=SET_STATIC:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(self.mashprefix):FilterStart() self.zonemashes=SET_ZONE:New():FilterPrefixes(self.mashprefix):FilterStart() 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:SetBackend(self.SRSBackend) self.msrs.speed=self.SRSSpeed self.msrs:SetCulture(self.SRSCulture) self.msrs:SetCoalition(self.coalition) self.msrs:SetVoice(self.SRSVoice) self.msrs:SetGender(self.SRSGender) if self.SRSGPathToCredentials and(not self.SRSProvider)then self.msrs:SetProviderOptionsGoogle(self.SRSGPathToCredentials,self.SRSGPathToCredentials) self.msrs:SetProvider(MSRS.Provider.GOOGLE) end if self.SRSProvider then self.msrs:SetProvider(self.SRSProvider) end if self.SRSSpeaker then self.msrs:SetSpeakerPiper(self.SRSSpeaker) 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 self:_InitLocalization(self.locale) 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 if playerName==nil or playerName==""then playerName="AI MIA"end 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:T(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.verbose>0) self:T(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,false,description,nil) end return self end AIRWING={ ClassName="AIRWING", verbose=0, lid=nil, menu=nil, payloads={}, payloadcounter=0, pointsCAP={}, pointsTANKER={}, pointsAWACS={}, pointsRecon={}, markpoints=false, capOptionPatrolRaceTrack=false, capFormation=nil, capOptionVaryStartTime=nil, capOptionVaryEndTime=nil, } AIRWING.version="0.9.7" function AIRWING:New(warehousename,airwingname) local self=BASE:Inherit(self,LEGION:New(warehousename,airwingname)) if not self then BASE:E(string.format("ERROR: Could not find warehouse %s!",warehousename)) return nil end self.lid=string.format("AIRWING %s | ",self.alias) self.nflightsCAP=0 self.nflightsAWACS=0 self.nflightsRecon=0 self.nflightsTANKERboom=0 self.nflightsTANKERprobe=0 self.nflightsRecoveryTanker=0 self.nflightsRescueHelo=0 self.markpoints=false self:AddTransition("*","FlightOnMission","*") return self end function AIRWING:AddSquadron(Squadron) table.insert(self.cohorts,Squadron) self:AddAssetToSquadron(Squadron,Squadron.Ngroups) if Squadron.attribute==GROUP.Attribute.AIR_AWACS then self:NewPayload(Squadron.templategroup,-1,AUFTRAG.Type.AWACS) elseif Squadron.attribute==GROUP.Attribute.AIR_TANKER then self:NewPayload(Squadron.templategroup,-1,AUFTRAG.Type.TANKER) end self:NewPayload(Squadron.templategroup,-1,AUFTRAG.Type.RELOCATECOHORT,0) Squadron:SetAirwing(self) if Squadron:IsStopped()then Squadron:Start() end local airbasename=self:GetAirbaseName() if airbasename then local group=Squadron.templategroup if group then local Nunits=1 local units if group then units=group:GetUnits()end if units then Nunits=#units end local typename=Squadron.aircrafttype or"none" local NAssets=Squadron.Ngroups*Nunits local storage=STORAGE:New(airbasename) self:T(self.lid.."Adding "..typename.." #"..NAssets) if storage and storage.warehouse and storage:IsLimitedAircraft()and typename~="none"then local NInStore=storage:GetItemAmount(typename)or 0 if NAssets>NInStore then storage:AddItem(typename,NAssets) end end local unit=group:GetUnit(1) if unit and storage and storage.warehouse and storage:IsLimitedLiquids()and typename~="none"then local fuel=unit:GetFuelMassMax() local neededfuel=(fuel*NAssets) local NInStore=storage:GetLiquidAmount(STORAGE.Liquid.JETFUEL)or 0 self:T(string.format(self.lid.."Fuel Needed: %dt | Fuel in store: %dt",neededfuel/1000,NInStore/1000)) if neededfuel>NInStore then storage:AddLiquid(STORAGE.Liquid.JETFUEL,neededfuel) end end end end return self end function AIRWING:NewPayload(Unit,Npayloads,MissionTypes,Performance) Performance=Performance or 50 if type(Unit)=="string"then local name=Unit Unit=UNIT:FindByName(name) if not Unit then Unit=GROUP:FindByName(name) end end if Unit then if Unit:IsInstanceOf("GROUP")then Unit=Unit:GetUnit(1) end if MissionTypes and type(MissionTypes)~="table"then MissionTypes={MissionTypes} end local payload={} payload.uid=self.payloadcounter payload.unitname=Unit:GetName() payload.aircrafttype=Unit:GetTypeName() payload.pylons=Unit:GetTemplatePayload() self:SetPayloadAmount(payload,Npayloads) payload.capabilities={} for _,missiontype in pairs(MissionTypes)do local capability={} capability.MissionType=missiontype capability.Performance=Performance table.insert(payload.capabilities,capability) end if not AUFTRAG.CheckMissionType(AUFTRAG.Type.ORBIT,MissionTypes)then local capability={} capability.MissionType=AUFTRAG.Type.ORBIT capability.Performance=50 table.insert(payload.capabilities,capability) end if not AUFTRAG.CheckMissionType(AUFTRAG.Type.RELOCATECOHORT,MissionTypes)then local capability={} capability.MissionType=AUFTRAG.Type.RELOCATECOHORT capability.Performance=50 table.insert(payload.capabilities,capability) end self:T(self.lid..string.format("Adding new payload from unit %s for aircraft type %s: ID=%d, N=%d (unlimited=%s), performance=%d, missions: %s", payload.unitname,payload.aircrafttype,payload.uid,payload.navail,tostring(payload.unlimited),Performance,table.concat(MissionTypes,", "))) table.insert(self.payloads,payload) self.payloadcounter=self.payloadcounter+1 return payload end self:E(self.lid.."ERROR: No UNIT found to create PAYLOAD!") return nil end function AIRWING:SetPayloadAmount(Payload,Navailable) Navailable=Navailable or 99 if Payload then Payload.unlimited=Navailable<0 if Payload.unlimited then Payload.navail=1 else Payload.navail=Navailable end end return self end function AIRWING:IncreasePayloadAmount(Payload,N) N=N or 1 if Payload and Payload.navail>=0 then Payload.navail=Payload.navail+N Payload.navail=math.max(Payload.navail,0) end return self end function AIRWING:GetPayloadAmount(Payload) return Payload.navail end function AIRWING:GetPayloadCapabilities(Payload) return Payload.capabilities end function AIRWING:AddPayloadCapability(Payload,MissionTypes,Performance) if MissionTypes and type(MissionTypes)~="table"then MissionTypes={MissionTypes} end Payload.capabilities=Payload.capabilities or{} for _,missiontype in pairs(MissionTypes)do local capability={} capability.MissionType=missiontype capability.Performance=Performance table.insert(Payload.capabilities,capability) end return self end function AIRWING:FetchPayloadFromStock(UnitType,MissionType,Payloads) if not self.payloads or#self.payloads==0 then self:T(self.lid.."WARNING: No payloads in stock!") return nil end if self.verbose>=4 then self:I(self.lid..string.format("Looking for payload for unit type=%s and mission type=%s",UnitType,MissionType)) for i,_payload in pairs(self.payloads)do local payload=_payload local performance=self:GetPayloadPeformance(payload,MissionType) self:I(self.lid..string.format("[%d] Payload type=%s navail=%d unlimited=%s",i,payload.aircrafttype,payload.navail,tostring(payload.unlimited))) end end local function sortpayloads(a,b) local pA=a local pB=b if a and b then local performanceA=self:GetPayloadPeformance(a,MissionType) local performanceB=self:GetPayloadPeformance(b,MissionType) return(performanceA>performanceB)or(performanceA==performanceB and a.unlimited==true and b.unlimited~=true)or(performanceA==performanceB and a.unlimited==true and b.unlimited==true and a.navail>b.navail) elseif not a then self:I(self.lid..string.format("FF ERROR in sortpayloads: a is nil")) return false elseif not b then self:I(self.lid..string.format("FF ERROR in sortpayloads: b is nil")) return true else self:I(self.lid..string.format("FF ERROR in sortpayloads: a and b are nil")) return false end end local function _checkPayloads(payload) if Payloads then for _,Payload in pairs(Payloads)do if Payload.uid==payload.uid then return true end end else return nil end return false end local payloads={} for _,_payload in pairs(self.payloads)do local payload=_payload local specialpayload=_checkPayloads(payload) local compatible=AUFTRAG.CheckMissionCapability(MissionType,payload.capabilities) local goforit=specialpayload or(specialpayload==nil and compatible) if payload.aircrafttype==UnitType and payload.navail>0 and goforit then table.insert(payloads,payload) end end if self.verbose>=4 then self:I(self.lid..string.format("Sorted payloads for mission type %s and aircraft type=%s:",MissionType,UnitType)) for _,_payload in ipairs(self.payloads)do local payload=_payload if payload.aircrafttype==UnitType and AUFTRAG.CheckMissionCapability(MissionType,payload.capabilities)then local performace=self:GetPayloadPeformance(payload,MissionType) self:I(self.lid..string.format("- %s payload for %s: avail=%d performace=%d",MissionType,payload.aircrafttype,payload.navail,performace)) end end end if#payloads==0 then self:T(self.lid..string.format("WARNING: Could not find a payload for airframe %s mission type %s!",UnitType,MissionType)) return nil elseif#payloads==1 then local payload=payloads[1] if not payload.unlimited then payload.navail=payload.navail-1 end return payload else table.sort(payloads,sortpayloads) local payload=payloads[1] if not payload.unlimited then payload.navail=payload.navail-1 end return payload end end function AIRWING:ReturnPayloadFromAsset(asset) local payload=asset.payload if payload then if not payload.unlimited then payload.navail=payload.navail+1 end asset.payload=nil else self:E(self.lid.."ERROR: asset had no payload attached!") end end function AIRWING:AddAssetToSquadron(Squadron,Nassets) if Squadron then local Group=GROUP:FindByName(Squadron.templatename) if Group then local text=string.format("Adding asset %s to squadron %s",Group:GetName(),Squadron.name) self:T(self.lid..text) self:AddAsset(Group,Nassets,nil,nil,nil,nil,Squadron.skill,Squadron.livery,Squadron.name) else self:E(self.lid.."ERROR: Group does not exist!") end else self:E(self.lid.."ERROR: Squadron does not exit!") end return self end function AIRWING:GetSquadron(SquadronName) local squad=self:_GetCohort(SquadronName) return squad end function AIRWING:GetSquadronOfAsset(Asset) return self:GetSquadron(Asset.squadname) end function AIRWING:RemoveAssetFromSquadron(Asset) local squad=self:GetSquadronOfAsset(Asset) if squad then squad:DelAsset(Asset) end end function AIRWING:SetNumberCAP(n) self.nflightsCAP=n or 1 return self end function AIRWING:SetCAPFormation(Formation) self.capFormation=Formation return self end function AIRWING:SetCapCloseRaceTrack(OnOff) self.capOptionPatrolRaceTrack=OnOff return self end function AIRWING:SetCapStartTimeVariation(Start,End) self.capOptionVaryStartTime=Start or 5 self.capOptionVaryEndTime=End or 60 return self end function AIRWING:SetNumberTankerBoom(Nboom) self.nflightsTANKERboom=Nboom or 1 return self end function AIRWING:ShowPatrolPointMarkers(onoff) if onoff then self.markpoints=true else self.markpoints=false end return self end function AIRWING:SetNumberTankerProbe(Nprobe) self.nflightsTANKERprobe=Nprobe or 1 return self end function AIRWING:SetNumberAWACS(n) self.nflightsAWACS=n or 1 return self end function AIRWING:SetNumberRecon(n) self.nflightsRecon=n or 1 return self end function AIRWING:SetNumberRescuehelo(n) self.nflightsRescueHelo=n or 1 return self end function AIRWING:_PatrolPointMarkerText(point) local text=string.format("%s Occupied=%d, \nheading=%03d, leg=%d NM, alt=%d ft, speed=%d kts", point.type,point.noccupied,point.heading,point.leg,point.altitude,point.speed) return text end function AIRWING:UpdatePatrolPointMarker(point) if self and self.markpoints then local text=string.format("%s Occupied=%d\nheading=%03d, leg=%d NM, alt=%d ft, speed=%d kts", point.type,point.noccupied,point.heading,point.leg,point.altitude,point.speed) if point.IsZonePoint and point.IsZonePoint==true and point.patrolzone then local Coordinate=point.patrolzone:GetCoordinate() point.marker:UpdateCoordinate(Coordinate) point.marker:UpdateText(text,1.5) else point.marker:UpdateText(text,1) end end end function AIRWING:NewPatrolPoint(Type,Coordinate,Altitude,Speed,Heading,LegLength,RefuelSystem) local patrolpoint={} patrolpoint.type=Type or"Unknown" patrolpoint.coord=Coordinate or self:GetCoordinate():Translate(UTILS.NMToMeters(math.random(10,15)),math.random(360)) if Coordinate and Coordinate:IsInstanceOf("ZONE_BASE")then patrolpoint.IsZonePoint=true patrolpoint.patrolzone=Coordinate patrolpoint.coord=patrolpoint.patrolzone:GetCoordinate() else patrolpoint.IsZonePoint=false end patrolpoint.heading=Heading or math.random(360) patrolpoint.leg=LegLength or 15 patrolpoint.altitude=Altitude or math.random(10,20)*1000 patrolpoint.speed=Speed or 350 patrolpoint.noccupied=0 patrolpoint.refuelsystem=RefuelSystem if self.markpoints then patrolpoint.marker=MARKER:New(Coordinate,"New Patrol Point"):ToAll() self:UpdatePatrolPointMarker(patrolpoint) end return patrolpoint end function AIRWING:AddPatrolPointCAP(Coordinate,Altitude,Speed,Heading,LegLength) local patrolpoint=self:NewPatrolPoint("CAP",Coordinate,Altitude,Speed,Heading,LegLength) table.insert(self.pointsCAP,patrolpoint) return self end function AIRWING:AddPatrolPointRecon(Coordinate,Altitude,Speed,Heading,LegLength) local patrolpoint=self:NewPatrolPoint("RECON",Coordinate,Altitude,Speed,Heading,LegLength) table.insert(self.pointsRecon,patrolpoint) return self end function AIRWING:AddPatrolPointTANKER(Coordinate,Altitude,Speed,Heading,LegLength,RefuelSystem) local patrolpoint=self:NewPatrolPoint("Tanker",Coordinate,Altitude,Speed,Heading,LegLength,RefuelSystem) table.insert(self.pointsTANKER,patrolpoint) return self end function AIRWING:AddPatrolPointAWACS(Coordinate,Altitude,Speed,Heading,LegLength) local patrolpoint=self:NewPatrolPoint("AWACS",Coordinate,Altitude,Speed,Heading,LegLength) table.insert(self.pointsAWACS,patrolpoint) return self end function AIRWING:SetAirboss(airboss) self.airboss=airboss return self end function AIRWING:SetTakeoffType(TakeoffType) TakeoffType=TakeoffType or"Cold" if TakeoffType:lower()=="hot"then self.takeoffType=COORDINATE.WaypointType.TakeOffParkingHot elseif TakeoffType:lower()=="cold"then self.takeoffType=COORDINATE.WaypointType.TakeOffParking elseif TakeoffType:lower()=="air"then self.takeoffType=COORDINATE.WaypointType.TurningPoint else self.takeoffType=COORDINATE.WaypointType.TakeOffParking end return self end function AIRWING:SetTakeoffCold() self:SetTakeoffType("Cold") return self end function AIRWING:SetTakeoffHot() self:SetTakeoffType("Hot") return self end function AIRWING:SetTakeoffAir() self:SetTakeoffType("Air") return self end function AIRWING:SetLandingStraightIn() self.OptionLandingStraightIn=true return self end function AIRWING:SetLandingForcePair() self.OptionLandingForcePair=true return self end function AIRWING:SetLandingRestrictPair() self.OptionLandingRestrictPair=true return self end function AIRWING:SetLandingOverheadBreak() self.OptionLandingOverheadBreak=true return self end function AIRWING:SetOptionPreferVerticalLanding() self.OptionPreferVerticalLanding=true return self end function AIRWING:SetDespawnAfterLanding(Switch) if Switch then self.despawnAfterLanding=Switch else self.despawnAfterLanding=true end return self end function AIRWING:SetDespawnAfterHolding(Switch) if Switch then self.despawnAfterHolding=Switch else self.despawnAfterHolding=true end return self end function AIRWING:onafterStart(From,Event,To) self:GetParent(self,AIRWING).onafterStart(self,From,Event,To) self:I(self.lid..string.format("Starting AIRWING v%s",AIRWING.version)) end function AIRWING:onafterStatus(From,Event,To) self:GetParent(self).onafterStatus(self,From,Event,To) local fsmstate=self:GetState() self:CheckCAP() self:CheckTANKER() self:CheckAWACS() self:CheckRescuhelo() self:CheckRECON() self:_TacticalOverview() self:CheckTransportQueue() self:CheckMissionQueue() if self.verbose>=1 then local Nmissions=self:CountMissionsInQueue() local Npayloads=self:CountPayloadsInStock(AUFTRAG.Type) local Npq,Np,Nq=self:CountAssetsOnMission() local assets=string.format("%d (OnMission: Total=%d, Active=%d, Queued=%d)",self:CountAssets(),Npq,Np,Nq) local text=string.format("%s: Missions=%d, Payloads=%d (%d), Squads=%d, Assets=%s",fsmstate,Nmissions,Npayloads,#self.payloads,#self.cohorts,assets) self:I(self.lid..text) end if self.verbose>=2 then local text=string.format("Missions Total=%d:",#self.missionqueue) for i,_mission in pairs(self.missionqueue)do local mission=_mission local prio=string.format("%d/%s",mission.prio,tostring(mission.importance));if mission.urgent then prio=prio.." (!)"end local assets=string.format("%d/%d",mission:CountOpsGroups(),mission:GetNumberOfRequiredAssets()) local target=string.format("%d/%d Damage=%.1f",mission:CountMissionTargets(),mission:GetTargetInitialNumber(),mission:GetTargetDamage()) local mystatus=mission:GetLegionStatus(self) text=text..string.format("\n[%d] %s %s: Status=%s [%s], Prio=%s, Assets=%s, Targets=%s",i,mission.name,mission.type,mystatus,mission.status,prio,assets,target) end self:I(self.lid..text) end if self.verbose>=3 then local text="Squadrons:" for i,_squadron in pairs(self.cohorts)do local squadron=_squadron local callsign=squadron.callsignName and UTILS.GetCallsignName(squadron.callsignName)or"N/A" local modex=squadron.modex and squadron.modex or-1 local skill=squadron.skill and tostring(squadron.skill)or"N/A" text=text..string.format("\n* %s %s: %s*%d/%d, Callsign=%s, Modex=%d, Skill=%s",squadron.name,squadron:GetState(),squadron.aircrafttype,squadron:CountAssets(true),#squadron.assets,callsign,modex,skill) end self:I(self.lid..text) end end function AIRWING:_GetPatrolData(PatrolPoints,RefuelSystem) local function sort(a,b) return a.noccupied0 then table.sort(PatrolPoints,sort) for _,_patrolpoint in pairs(PatrolPoints)do local patrolpoint=_patrolpoint if patrolpoint.IsZonePoint and patrolpoint.IsZonePoint==true and patrolpoint.patrolzone then patrolpoint.coord=patrolpoint.patrolzone:GetCoordinate() end if(RefuelSystem and patrolpoint.refuelsystem and RefuelSystem==patrolpoint.refuelsystem)or RefuelSystem==nil or patrolpoint.refuelsystem==nil then return patrolpoint end end end return self:NewPatrolPoint() end function AIRWING:CheckCAP() local Ncap=0 for _,_mission in pairs(self.missionqueue)do local mission=_mission if mission:IsNotOver()and(mission.type==AUFTRAG.Type.GCICAP or mission.type==AUFTRAG.Type.PATROLRACETRACK)and mission.patroldata then Ncap=Ncap+1 end end for i=1,self.nflightsCAP-Ncap do local patrol=self:_GetPatrolData(self.pointsCAP) local altitude=patrol.altitude+1000*patrol.noccupied local missionCAP=nil if self.capOptionPatrolRaceTrack then missionCAP=AUFTRAG:NewPATROL_RACETRACK(patrol.coord,altitude,patrol.speed,patrol.heading,patrol.leg,self.capFormation) else missionCAP=AUFTRAG:NewGCICAP(patrol.coord,altitude,patrol.speed,patrol.heading,patrol.leg) end if self.capOptionVaryStartTime then local ClockStart=math.random(self.capOptionVaryStartTime,self.capOptionVaryEndTime) missionCAP:SetTime(ClockStart) end missionCAP.patroldata=patrol patrol.noccupied=patrol.noccupied+1 if self.markpoints then self:UpdatePatrolPointMarker(patrol)end self:AddMission(missionCAP) end return self end function AIRWING:CheckRECON() local Ncap=0 for _,_mission in pairs(self.missionqueue)do local mission=_mission if mission:IsNotOver()and mission.type==AUFTRAG.Type.RECON and mission.patroldata then Ncap=Ncap+1 end end for i=1,self.nflightsRecon-Ncap do local patrol=self:_GetPatrolData(self.pointsRecon) local altitude=patrol.altitude local ZoneSet=SET_ZONE:New() local Zone=ZONE_RADIUS:New(self.alias.." Recon "..math.random(1,10000),patrol.coord:GetVec2(),UTILS.NMToMeters(patrol.leg/2)) ZoneSet:AddZone(Zone) if self.Debug then Zone:DrawZone(self.coalition,{0,0,1},Alpha,FillColor,FillAlpha,2,true) end local missionRECON=AUFTRAG:NewRECON(ZoneSet,patrol.speed,patrol.altitude,true) missionRECON.patroldata=patrol missionRECON.categories={AUFTRAG.Category.AIRCRAFT} patrol.noccupied=patrol.noccupied+1 if self.markpoints then self:UpdatePatrolPointMarker(patrol)end self:AddMission(missionRECON) end return self end function AIRWING:CheckTANKER() local Nboom=0 local Nprob=0 for _,_mission in pairs(self.missionqueue)do local mission=_mission if mission:IsNotOver()and mission.type==AUFTRAG.Type.TANKER and mission.patroldata then if mission.refuelSystem==Unit.RefuelingSystem.BOOM_AND_RECEPTACLE then Nboom=Nboom+1 elseif mission.refuelSystem==Unit.RefuelingSystem.PROBE_AND_DROGUE then Nprob=Nprob+1 end end end for i=1,self.nflightsTANKERboom-Nboom do local patrol=self:_GetPatrolData(self.pointsTANKER) local altitude=patrol.altitude+1000*patrol.noccupied local mission=AUFTRAG:NewTANKER(patrol.coord,altitude,patrol.speed,patrol.heading,patrol.leg,Unit.RefuelingSystem.BOOM_AND_RECEPTACLE) mission.patroldata=patrol patrol.noccupied=patrol.noccupied+1 if self.markpoints then self:UpdatePatrolPointMarker(patrol)end self:AddMission(mission) end for i=1,self.nflightsTANKERprobe-Nprob do local patrol=self:_GetPatrolData(self.pointsTANKER) local altitude=patrol.altitude+1000*patrol.noccupied local mission=AUFTRAG:NewTANKER(patrol.coord,altitude,patrol.speed,patrol.heading,patrol.leg,Unit.RefuelingSystem.PROBE_AND_DROGUE) mission.patroldata=patrol patrol.noccupied=patrol.noccupied+1 if self.markpoints then self:UpdatePatrolPointMarker(patrol)end self:AddMission(mission) end return self end function AIRWING:CheckAWACS() local N=0 for _,_mission in pairs(self.missionqueue)do local mission=_mission if mission:IsNotOver()and mission.type==AUFTRAG.Type.AWACS and mission.patroldata then N=N+1 end end for i=1,self.nflightsAWACS-N do local patrol=self:_GetPatrolData(self.pointsAWACS) local altitude=patrol.altitude+1000*patrol.noccupied local mission=AUFTRAG:NewAWACS(patrol.coord,altitude,patrol.speed,patrol.heading,patrol.leg) mission.patroldata=patrol patrol.noccupied=patrol.noccupied+1 if self.markpoints then self:UpdatePatrolPointMarker(patrol)end self:AddMission(mission) end return self end function AIRWING:CheckRescuhelo() local N=self:CountMissionsInQueue({AUFTRAG.Type.RESCUEHELO}) if self.airbase then local name=self.airbase:GetName() local carrier=UNIT:FindByName(name) for i=1,self.nflightsRescueHelo-N do local mission=AUFTRAG:NewRESCUEHELO(carrier) self:AddMission(mission) end end return self end function AIRWING:GetTankerForFlight(flightgroup) local tankers=self:GetAssetsOnMission(AUFTRAG.Type.TANKER) if#tankers>0 then local tankeropt={} for _,_tanker in pairs(tankers)do local tanker=_tanker if flightgroup.refueltype and flightgroup.refueltype==tanker.flightgroup.tankertype then local tankercoord=tanker.flightgroup.group:GetCoordinate() local assetcoord=flightgroup.group:GetCoordinate() local dist=assetcoord:Get2DDistance(tankercoord) if dist>5 then table.insert(tankeropt,{tanker=tanker,dist=dist}) end end end table.sort(tankeropt,function(a,b)return a.dist0 then return tankeropt[1].tanker else return nil end end return nil end function AIRWING:SetUsingOpsAwacs(ConnectecdAwacs) self:I(self.lid.."Added AWACS Object: "..ConnectecdAwacs:GetName()or"unknown") self.UseConnectedOpsAwacs=true self.ConnectedOpsAwacs=ConnectecdAwacs return self end function AIRWING:RemoveUsingOpsAwacs() self:I(self.lid.."Reomve AWACS Object: "..self.ConnectedOpsAwacs:GetName()or"unknown") self.UseConnectedOpsAwacs=false return self end function AIRWING:onafterFlightOnMission(From,Event,To,FlightGroup,Mission) self:T(self.lid..string.format("Group %s on %s mission %s",FlightGroup:GetName(),Mission:GetType(),Mission:GetName())) if self.UseConnectedOpsAwacs and self.ConnectedOpsAwacs then self.ConnectedOpsAwacs:__FlightOnMission(2,FlightGroup,Mission) end if self.OptionLandingForcePair then FlightGroup:SetOptionLandingForcePair() elseif self.OptionLandingOverheadBreak then FlightGroup:SetOptionLandingOverheadBreak() elseif self.OptionLandingRestrictPair then FlightGroup:SetOptionLandingRestrictPair() elseif self.OptionLandingStraightIn then FlightGroup:SetOptionLandingStraightIn() end if self.OptionPreferVerticalLanding then FlightGroup:SetOptionPreferVertical() end end function AIRWING:CountPayloadsInStock(MissionTypes,UnitTypes,Payloads) if MissionTypes then if type(MissionTypes)=="string"then MissionTypes={MissionTypes} end end if UnitTypes then if type(UnitTypes)=="string"then UnitTypes={UnitTypes} end end local function _checkUnitTypes(payload) if UnitTypes then for _,unittype in pairs(UnitTypes)do if unittype==payload.aircrafttype then return true end end else return true end return false end local function _checkPayloads(payload) if Payloads then for _,Payload in pairs(Payloads)do if Payload.uid==payload.uid then return true end end else return nil end return false end local n=0 for _,_payload in pairs(self.payloads)do local payload=_payload for _,MissionType in pairs(MissionTypes)do local specialpayload=_checkPayloads(payload) local compatible=AUFTRAG.CheckMissionCapability(MissionType,payload.capabilities) local goforit=specialpayload or(specialpayload==nil and compatible) if goforit and _checkUnitTypes(payload)then if payload.unlimited then return 999 else n=n+payload.navail end end end end return n end function AIRWING:GetPayloadPeformance(Payload,MissionType) if Payload then for _,Capability in pairs(Payload.capabilities)do local capability=Capability if capability.MissionType==MissionType then return capability.Performance end end else self:E(self.lid.."ERROR: Payload is nil!") end return-1 end function AIRWING:GetPayloadMissionTypes(Payload) local missiontypes={} for _,Capability in pairs(Payload.capabilities)do local capability=Capability table.insert(missiontypes,capability.MissionType) end return missiontypes end ARMYGROUP={ ClassName="ARMYGROUP", formationPerma=nil, engage={}, } ARMYGROUP.version="1.0.4" function ARMYGROUP:New(group) local og=_DATABASE:GetOpsGroup(group) if og then og:I(og.lid..string.format("WARNING: OPS group already exists in data base!")) return og end local self=BASE:Inherit(self,OPSGROUP:New(group)) self.lid=string.format("ARMYGROUP %s | ",self.groupname) self:SetDefaultROE() self:SetDefaultAlarmstate() self:SetDefaultEPLRS(self.isEPLRS) self:SetDefaultEmission() self:SetDetection() self:SetPatrolAdInfinitum(false) self:SetRetreatZones() self:AddTransition("*","FullStop","Holding") self:AddTransition("*","Cruise","Cruising") self:AddTransition("*","RTZ","Returning") self:AddTransition("Holding","Returned","Returned") self:AddTransition("Returning","Returned","Returned") self:AddTransition("*","Detour","OnDetour") self:AddTransition("OnDetour","DetourReached","Cruising") self:AddTransition("*","Retreat","Retreating") self:AddTransition("Retreating","Retreated","Retreated") self:AddTransition("*","Suppressed","*") self:AddTransition("*","Unsuppressed","*") self:AddTransition("Cruising","EngageTarget","Engaging") self:AddTransition("Holding","EngageTarget","Engaging") self:AddTransition("OnDetour","EngageTarget","Engaging") self:AddTransition("Engaging","Disengage","Cruising") self:AddTransition("*","Rearm","Rearm") self:AddTransition("Rearm","Rearming","Rearming") self:AddTransition("*","Rearmed","Cruising") self:_InitWaypoints() self:_InitGroup() self:HandleEvent(EVENTS.Birth,self.OnEventBirth) self:HandleEvent(EVENTS.Dead,self.OnEventDead) self:HandleEvent(EVENTS.RemoveUnit,self.OnEventRemoveUnit) self:HandleEvent(EVENTS.UnitLost,self.OnEventRemoveUnit) self:HandleEvent(EVENTS.Hit,self.OnEventHit) self.timerStatus=TIMER:New(self.Status,self):Start(1,30) self.timerQueueUpdate=TIMER:New(self._QueueUpdate,self):Start(2,5) self.timerCheckZone=TIMER:New(self._CheckInZones,self):Start(2,30) _DATABASE:AddOpsGroup(self) return self end function ARMYGROUP:SetPatrolAdInfinitum(switch) if switch==false then self.adinfinitum=false else self.adinfinitum=true end return self end function ARMYGROUP:GetClosestRoad() local coord=self:GetCoordinate():GetClosestPointToRoad() return coord end function ARMYGROUP:GetClosestRoadDist() local road=self:GetClosestRoad() if road then local dist=road:Get2DDistance(self:GetCoordinate()) return dist end return math.huge end function ARMYGROUP:AddTaskFireAtPoint(Coordinate,Clock,Radius,Nshots,WeaponType,Prio) Coordinate=self:_CoordinateFromObject(Coordinate) local DCStask=CONTROLLABLE.TaskFireAtPoint(nil,Coordinate:GetVec2(),Radius,Nshots,WeaponType) local task=self:AddTask(DCStask,Clock,nil,Prio) return task end function ARMYGROUP:AddTaskBarrage(Clock,Heading,Alpha,Altitude,Radius,Nshots,WeaponType,Prio) Heading=Heading or 0 Alpha=Alpha or 60 Altitude=Altitude or 100 local distance=Altitude/math.tan(math.rad(Alpha)) local a=self:GetVec2() local vec2=UTILS.Vec2Translate(a,distance,Heading) local DCStask=CONTROLLABLE.TaskFireAtPoint(nil,vec2,Radius,Nshots,WeaponType,Altitude) local task=self:AddTask(DCStask,Clock,nil,Prio) return task end function ARMYGROUP:AddTaskWaypointFireAtPoint(Coordinate,Waypoint,Radius,Nshots,WeaponType,Prio) Coordinate=self:_CoordinateFromObject(Coordinate) Waypoint=Waypoint or self:GetWaypointNext() local DCStask=CONTROLLABLE.TaskFireAtPoint(nil,Coordinate:GetVec2(),Radius,Nshots,WeaponType) local task=self:AddTaskWaypoint(DCStask,Waypoint,nil,Prio) return task end function ARMYGROUP:AddTaskAttackGroup(TargetGroup,WeaponExpend,WeaponType,Clock,Prio) local DCStask=CONTROLLABLE.TaskAttackGroup(nil,TargetGroup,WeaponType,WeaponExpend,AttackQty,Direction,Altitude,AttackQtyLimit,GroupAttack) local task=self:AddTask(DCStask,Clock,nil,Prio) return task end function ARMYGROUP:AddTaskCargoGroup(GroupSet,PickupZone,DeployZone,Clock,Prio) local DCStask={} DCStask.id="CargoTransport" DCStask.params={} DCStask.params.cargoqueu=1 local task=self:AddTask(DCStask,Clock,nil,Prio) return task end function ARMYGROUP:SetRetreatZones(RetreatZoneSet) self.retreatZones=RetreatZoneSet or SET_ZONE:New() return self end function ARMYGROUP:AddRetreatZone(RetreatZone) self.retreatZones:AddZone(RetreatZone) return self end function ARMYGROUP:SetSuppressionOn(Tave,Tmin,Tmax) self.suppressionOn=true self.TsuppressMin=Tmin or 1 self.TsuppressMin=math.max(self.TsuppressMin,1) self.TsuppressMax=Tmax or 15 self.TsuppressMax=math.max(self.TsuppressMax,self.TsuppressMin) self.TsuppressAve=Tave or 10 self.TsuppressAve=math.max(self.TsuppressMin) self.TsuppressAve=math.min(self.TsuppressMax) self:T(self.lid..string.format("Set ave suppression time to %d seconds.",self.TsuppressAve)) self:T(self.lid..string.format("Set min suppression time to %d seconds.",self.TsuppressMin)) self:T(self.lid..string.format("Set max suppression time to %d seconds.",self.TsuppressMax)) return self end function ARMYGROUP:SetSuppressionOff() self.suppressionOn=false end function ARMYGROUP:IsHolding() return self:Is("Holding") end function ARMYGROUP:IsCruising() return self:Is("Cruising") end function ARMYGROUP:IsOnDetour() return self:Is("OnDetour") end function ARMYGROUP:IsCombatReady() local combatready=true if self:IsRearming()or self:IsRetreating()or self:IsOutOfAmmo()or self:IsEngaging()or self:IsDead()or self:IsStopped()or self:IsInUtero()then combatready=false end if self:IsPickingup()or self:IsLoading()or self:IsTransporting()or self:IsLoaded()or self:IsCargo()or self:IsCarrier()then combatready=false end return combatready end function ARMYGROUP:Status() local fsmstate=self:GetState() local alive=self:IsAlive() if alive then self:_UpdatePosition() self:_CheckDetectedUnits() self:_CheckAmmoStatus() self:_CheckDamage() self:_CheckStuck() if self:IsEngaging()then self:_UpdateEngageTarget() end if self:IsWaiting()then if self.Twaiting and self.dTwait then if timer.getAbsTime()>self.Twaiting+self.dTwait then self.Twaiting=nil self.dTwait=nil if self:_CountPausedMissions()>0 then self:UnpauseMission() else self:Cruise() end end end end local mission=self:GetMissionCurrent() if mission and mission.updateDCSTask then if mission.type==AUFTRAG.Type.CAPTUREZONE then local Task=mission:GetGroupWaypointTask(self) if mission:GetGroupStatus(self)==AUFTRAG.GroupStatus.EXECUTING or mission:GetGroupStatus(self)==AUFTRAG.GroupStatus.STARTED then self:_UpdateTask(Task,mission) end end end else self:_CheckDamage() end if alive~=nil then if self.verbose>=1 then local nelem=self:CountElements() local Nelem=#self.elements local nTaskTot,nTaskSched,nTaskWP=self:CountRemainingTasks() local nMissions=self:CountRemainingMissison() local roe=self:GetROE()or-1 local als=self:GetAlarmstate()or-1 local wpidxCurr=self.currentwp local wpuidCurr=self:GetWaypointUIDFromIndex(wpidxCurr)or 0 local wpidxNext=self:GetWaypointIndexNext()or 0 local wpuidNext=self:GetWaypointUIDFromIndex(wpidxNext)or 0 local wpN=#self.waypoints or 0 local wpF=tostring(self.passedfinalwp) local speed=UTILS.MpsToKnots(self.velocity or 0) local speedEx=UTILS.MpsToKnots(self:GetExpectedSpeed()) local alt=self.position and self.position.y or 0 local hdg=self.heading or 0 local formation=self.option.Formation or"unknown" local life=self.life or 0 local ammo=self:GetAmmoTot().Total local ndetected=self.detectionOn and tostring(self.detectedunits:Count())or"Off" local cargo=0 for _,_element in pairs(self.elements)do local element=_element cargo=cargo+element.weightCargo end local text=string.format("%s [%d/%d]: ROE/AS=%d/%d | T/M=%d/%d | Wp=%d[%d]-->%d[%d]/%d [%s] | Life=%.1f | v=%.1f (%d) [%s] | Hdg=%03d | Ammo=%d | Detect=%s | Cargo=%.1f", fsmstate,nelem,Nelem,roe,als,nTaskTot,nMissions,wpidxCurr,wpuidCurr,wpidxNext,wpuidNext,wpN,wpF,life,speed,speedEx,formation,hdg,ammo,ndetected,cargo) self:I(self.lid..text) end else if self.verbose>=1 then local text=string.format("State %s: Alive=%s",fsmstate,tostring(self:IsAlive())) self:I(self.lid..text) end end if self.verbose>=2 then local text="Elements:" for i,_element in pairs(self.elements)do local element=_element local name=element.name local status=element.status local unit=element.unit local life,life0=self:GetLifePoints(element) local life0=element.life0 local ammo=self:GetAmmoElement(element) text=text..string.format("\n[%d] %s: status=%s, life=%.1f/%.1f, guns=%d, cannons=%d, rockets=%d, missiles=%d, cargo=%d/%d kg", i,name,status,life,life0,ammo.Guns,ammo.Cannons,ammo.Rockets,ammo.Missiles,element.weightCargo,element.weightMaxCargo) end if#self.elements==0 then text=text.." none!" end self:T(self.lid..text) end if self:IsCruising()and self.detectionOn and self.engagedetectedOn then local targetgroup,targetdist=self:_GetDetectedTarget() if targetgroup then self:T(self.lid..string.format("Engaging target group %s at distance %d meters",targetgroup:GetName(),targetdist)) self:EngageTarget(targetgroup) end end self:_CheckCargoTransport() self:_PrintTaskAndMissionStatus() end function ARMYGROUP:onafterElementSpawned(From,Event,To,Element) self:T(self.lid..string.format("Element spawned %s",Element.name)) self:_UpdateStatus(Element,OPSGROUP.ElementStatus.SPAWNED) end function ARMYGROUP:onafterSpawned(From,Event,To) self:T(self.lid..string.format("Group spawned!")) if self.verbose>=1 then local text=string.format("Initialized Army Group %s:\n",self.groupname) text=text..string.format("Unit type = %s\n",self.actype) text=text..string.format("Speed max = %.1f Knots\n",UTILS.KmphToKnots(self.speedMax)) text=text..string.format("Speed cruise = %.1f Knots\n",UTILS.KmphToKnots(self.speedCruise)) text=text..string.format("Weight = %.1f kg\n",self:GetWeightTotal()) text=text..string.format("Cargo bay = %.1f kg\n",self:GetFreeCargobay()) text=text..string.format("Has EPLRS = %s\n",tostring(self.isEPLRS)) text=text..string.format("Elements = %d\n",#self.elements) text=text..string.format("Waypoints = %d\n",#self.waypoints) text=text..string.format("Radio = %.1f MHz %s %s\n",self.radio.Freq,UTILS.GetModulationName(self.radio.Modu),tostring(self.radio.On)) text=text..string.format("Ammo = %d (G=%d/R=%d/M=%d)\n",self.ammo.Total,self.ammo.Guns,self.ammo.Rockets,self.ammo.Missiles) text=text..string.format("FSM state = %s\n",self:GetState()) text=text..string.format("Is alive = %s\n",tostring(self:IsAlive())) text=text..string.format("LateActivate = %s\n",tostring(self:IsLateActivated())) self:I(self.lid..text) end self:_UpdatePosition() self.isDead=false self.isDestroyed=false if self.isAI then self:SwitchROE(self.option.ROE) self:SwitchAlarmstate(self.option.Alarm) self:SwitchEmission(self.option.Emission) self:SwitchEPLRS(self.option.EPLRS) self:SwitchInvisible(self.option.Invisible) self:SwitchImmortal(self.option.Immortal) self:_SwitchTACAN() if self.radioDefault then self:SwitchRadio(self.radioDefault.Freq,self.radioDefault.Modu) else self:SetDefaultRadio(self.radio.Freq,self.radio.Modu,true) end if not self.option.Formation then end local Nwp=#self.waypoints if Nwp>1 and self.isMobile then self:T(self.lid..string.format("Got %d waypoints on spawn ==> Cruise in -1.0 sec!",Nwp)) local wp=self:GetWaypointNext() self.option.Formation=wp.action self:__Cruise(-1) else self:T(self.lid.."No waypoints on spawn ==> Full Stop!") self:FullStop() end end end function ARMYGROUP:onbeforeUpdateRoute(From,Event,To,n,N,Speed,Formation) local allowed=true local trepeat=nil if self:IsWaiting()then self:T(self.lid.."Update route denied. Group is WAITING!") return false elseif self:IsInUtero()then self:T(self.lid.."Update route denied. Group is INUTERO!") return false elseif self:IsDead()then self:T(self.lid.."Update route denied. Group is DEAD!") return false elseif self:IsStopped()then self:T(self.lid.."Update route denied. Group is STOPPED!") return false elseif self:IsHolding()then self:T(self.lid.."Update route denied. Group is holding position!") return false elseif self:IsEngaging()then self:T(self.lid.."Update route allowed. Group is engaging!") return true end if self.taskcurrent>0 then local task=self:GetTaskByID(self.taskcurrent) if task then if task.dcstask.id==AUFTRAG.SpecialTask.PATROLZONE then self:T2(self.lid.."Allowing update route for Task: PatrolZone") elseif task.dcstask.id==AUFTRAG.SpecialTask.RECON then self:T2(self.lid.."Allowing update route for Task: ReconMission") elseif task.dcstask.id==AUFTRAG.SpecialTask.RELOCATECOHORT then self:T2(self.lid.."Allowing update route for Task: Relocate Cohort") elseif task.dcstask.id==AUFTRAG.SpecialTask.REARMING then self:T2(self.lid.."Allowing update route for Task: Rearming") else local taskname=task and task.description or"No description" self:T(self.lid..string.format("WARNING: Update route denied because taskcurrent=%d>0! Task description = %s",self.taskcurrent,tostring(taskname))) allowed=false end else self:T(self.lid..string.format("WARNING: before update route taskcurrent=%d (>0!) but no task?!",self.taskcurrent)) allowed=false end end if not self.isAI then allowed=false end self:T2(self.lid..string.format("Onbefore Updateroute in state %s: allowed=%s (repeat in %s)",self:GetState(),tostring(allowed),tostring(trepeat))) if trepeat then self:__UpdateRoute(trepeat,n) end return allowed end function ARMYGROUP:onafterUpdateRoute(From,Event,To,n,N,Speed,Formation) n=n or self:GetWaypointIndexNext(self.adinfinitum) N=N or#self.waypoints N=math.min(N,#self.waypoints) local text=string.format("Update route state=%s: n=%s, N=%s, Speed=%s, Formation=%s",self:GetState(),tostring(n),tostring(N),tostring(Speed),tostring(Formation)) self:T(self.lid..text) local waypoints={} local wp=self.waypoints[n] local coordinate=self:GetCoordinate() local coordRoad=coordinate:GetClosestPointToRoad() local roaddist=coordinate:Get2DDistance(coordRoad) local formation0=wp.action if formation0==ENUMS.Formation.Vehicle.OnRoad then if roaddist>10 then formation0=ENUMS.Formation.Vehicle.OffRoad else formation0=ENUMS.Formation.Vehicle.OnRoad end end local current=coordinate:WaypointGround(UTILS.MpsToKmph(self.speedWp),formation0) table.insert(waypoints,1,current) if N-n>0 then for j=n,N do local i=j-1 if i==0 then i=self.currentwp end local wp=UTILS.DeepCopy(self.waypoints[j]) local wp0=self.waypoints[i] if false and self.attribute==GROUP.Attribute.GROUND_APC then local text=string.format("FF Update: i=%d, wp[i]=%s, wp[i-1]=%s",i,wp.action,wp0.action) env.info(text) end if Speed then wp.speed=UTILS.KnotsToMps(tonumber(Speed)) else if wp.speed<0.1 then wp.speed=UTILS.KmphToMps(self.speedCruise) end end if self.formationPerma then wp.action=self.formationPerma elseif Formation then wp.action=Formation end if wp.action==ENUMS.Formation.Vehicle.OnRoad and wp0.roaddist>=0 then local wproad=wp0.roadcoord:WaypointGround(UTILS.MpsToKmph(wp.speed),ENUMS.Formation.Vehicle.OnRoad) table.insert(waypoints,wproad) end if wp.action==ENUMS.Formation.Vehicle.OnRoad and wp.roaddist>=0 then wp.action=ENUMS.Formation.Vehicle.OffRoad local wproad=wp.roadcoord:WaypointGround(UTILS.MpsToKmph(wp.speed),ENUMS.Formation.Vehicle.OnRoad) table.insert(waypoints,wproad) end table.insert(waypoints,wp) end else local wp=UTILS.DeepCopy(self.waypoints[n]) if wp.speed<0.1 then wp.speed=UTILS.KmphToMps(self.speedCruise) end local formation=wp.action if self.formationPerma then formation=self.formationPerma elseif Formation then formation=Formation end if formation==ENUMS.Formation.Vehicle.OnRoad then if roaddist>10 then local wproad=coordRoad:WaypointGround(UTILS.MpsToKmph(wp.speed),ENUMS.Formation.Vehicle.OnRoad) table.insert(waypoints,wproad) end if wp.roaddist>10 then local wproad=wp.roadcoord:WaypointGround(UTILS.MpsToKmph(wp.speed),ENUMS.Formation.Vehicle.OnRoad) table.insert(waypoints,wproad) end end if wp.action==ENUMS.Formation.Vehicle.OnRoad and wp.roaddist>10 then wp.action=ENUMS.Formation.Vehicle.OffRoad end table.insert(waypoints,wp) end local wp=waypoints[1] self.option.Formation=wp.action self.speedWp=wp.speed self:T(self.lid..string.format("Expected/waypoint speed=%.1f m/s",self.speedWp)) if self.verbose>=10 then for i,_wp in pairs(waypoints)do local wp=_wp local text=string.format("WP #%d UID=%d Formation=%s: Speed=%d m/s, Alt=%d m, Type=%s",i,wp.uid and wp.uid or-1,wp.action,wp.speed,wp.alt,wp.type) local coord=COORDINATE:NewFromWaypoint(wp):MarkToAll(text) self:I(text) end end if self:IsEngaging()or not self.passedfinalwp then self:T(self.lid..string.format("Updateing route: WP %d-->%d (%d/%d), Speed=%.1f knots, Formation=%s", self.currentwp,n,#waypoints,#self.waypoints,UTILS.MpsToKnots(self.speedWp),tostring(self.option.Formation))) self:Route(waypoints) else self:T(self.lid..string.format("WARNING: Passed final WP when UpdateRoute() ==> Full Stop!")) self:FullStop() end end function ARMYGROUP:onafterGotoWaypoint(From,Event,To,UID,Speed,Formation) local n=self:GetWaypointIndex(UID) if n then Speed=Speed or self:GetSpeedToWaypoint(n) self:__UpdateRoute(-0.01,n,nil,Speed,Formation) end end function ARMYGROUP:onafterDetour(From,Event,To,Coordinate,Speed,Formation,ResumeRoute) for _,_wp in pairs(self.waypoints)do local wp=_wp if wp.detour then self:RemoveWaypointByID(wp.uid) end end Speed=Speed or self:GetSpeedCruise() local uid=self:GetWaypointCurrentUID() local wp=self:AddWaypoint(Coordinate,Speed,uid,Formation,true) if ResumeRoute then wp.detour=1 else wp.detour=0 end end function ARMYGROUP:onafterOutOfAmmo(From,Event,To) self:T(self.lid..string.format("Group is out of ammo at t=%.3f",timer.getTime())) local task=self:GetTaskCurrent() if task then if task.dcstask.id=="FireAtPoint"or task.dcstask.id==AUFTRAG.SpecialTask.BARRAGE then self:T(self.lid..string.format("Cancelling current %s task because out of ammo!",task.dcstask.id)) self:TaskCancel(task) end end if self.rearmOnOutOfAmmo then local truck,dist=self:FindNearestAmmoSupply(30) if truck then self:T(self.lid..string.format("Found Ammo Truck %s [%s]",truck:GetName(),truck:GetTypeName())) local Coordinate=truck:GetCoordinate() self:__Rearm(-1,Coordinate) return end end if self.retreatOnOutOfAmmo then self:T(self.lid.."Retreat on out of ammo") self:__Retreat(-1) return end if self.rtzOnOutOfAmmo and not self:IsMissionTypeInQueue(AUFTRAG.Type.REARMING)then self:T(self.lid.."RTZ on out of ammo") self:__RTZ(-1) end end function ARMYGROUP:onbeforeRearm(From,Event,To,Coordinate,Formation) local dt=nil local allowed=true if self:IsOnMission()then local mission=self:GetMissionCurrent() if mission and mission.type~=AUFTRAG.Type.REARMING then self:T(self.lid.."Rearm command but have current mission ==> Pausing mission!") self:PauseMission() dt=-0.1 allowed=false else self:T(self.lid.."Rearm command and current mission is REARMING ==> Transition ALLOWED!") end end if self:IsEngaging()then self:T(self.lid.."Rearm command but currently engaging ==> Disengage!") self:Disengage() dt=-0.1 allowed=false end if allowed and not Coordinate then local truck=self:FindNearestAmmoSupply() if truck and truck:IsAlive()then self:__Rearm(-0.1,truck:GetCoordinate(),Formation) end return false end if dt then self:T(self.lid..string.format("Trying Rearm again in %.2f sec",dt)) self:__Rearm(dt,Coordinate,Formation) allowed=false end return allowed end function ARMYGROUP:onafterRearm(From,Event,To,Coordinate,Formation) self:T(self.lid..string.format("Group send to rearm")) local uid=self:GetWaypointCurrentUID() local wp=self:AddWaypoint(Coordinate,nil,uid,Formation,true) wp.detour=0 end function ARMYGROUP:onafterRearmed(From,Event,To) self:T(self.lid.."Group rearmed") local mission=self:GetMissionCurrent() if mission and mission.type==AUFTRAG.Type.REARMING then self:MissionDone(mission) else self:_CheckGroupDone(1) end end function ARMYGROUP:onbeforeRTZ(From,Event,To,Zone,Formation) self:T2(self.lid.."onbeforeRTZ") local zone=Zone or self.homezone if zone then if(not self.isMobile)and(not self:IsInZone(zone))then self:Teleport(zone:GetCoordinate(),0,true) self:__RTZ(-1,Zone,Formation) return false end else return false end return true end function ARMYGROUP:onafterRTZ(From,Event,To,Zone,Formation) self:T(self.lid.."onafterRTZ") local zone=Zone or self.homezone self:CancelAllMissions() if zone then if self:IsInZone(zone)then self:Returned() else self:T(self.lid..string.format("RTZ to Zone %s",zone:GetName())) local Coordinate=zone:GetRandomCoordinate() local uid=self:GetWaypointCurrentUID() local wp=self:AddWaypoint(Coordinate,nil,uid,Formation,true) wp.detour=0 end else self:T(self.lid.."ERROR: No RTZ zone given!") end end function ARMYGROUP:onafterReturned(From,Event,To) self:T(self.lid..string.format("Group returned")) if self.legion then self:T(self.lid..string.format("Adding group back to warehouse stock")) self.legion:__AddAsset(10,self.group,1) end end function ARMYGROUP:onafterRearming(From,Event,To) local pos=self:GetCoordinate() local wp=pos:WaypointGround(0) self:Route({wp}) end function ARMYGROUP:onbeforeRetreat(From,Event,To,Zone,Formation) if not Zone then local a=self:GetVec2() local distmin=math.huge local zonemin=nil for _,_zone in pairs(self.retreatZones:GetSet())do local zone=_zone local b=zone:GetVec2() local dist=UTILS.VecDist2D(a,b) if dist Pausing mission!") self:PauseMission() dt=-0.1 allowed=false end if dt then self:T(self.lid..string.format("Trying Engage again in %.2f sec",dt)) self:__EngageTarget(dt,Target) allowed=false end return allowed end function ARMYGROUP:onafterEngageTarget(From,Event,To,Target,Speed,Formation) self:T(self.lid.."Engaging Target") if Target:IsInstanceOf("TARGET")then self.engage.Target=Target else self.engage.Target=TARGET:New(Target) end self.engage.Coordinate=UTILS.DeepCopy(self.engage.Target:GetCoordinate()) local intercoord=self:GetCoordinate():GetIntermediateCoordinate(self.engage.Coordinate,0.95) self.engage.roe=self:GetROE() self.engage.alarmstate=self:GetAlarmstate() self:SwitchAlarmstate(ENUMS.AlarmState.Auto) self:SwitchROE(ENUMS.ROE.OpenFire) local uid=self:GetWaypointCurrentUID() self.engage.Formation=Formation or ENUMS.Formation.Vehicle.Vee self.engage.Speed=Speed self.engage.Waypoint=self:AddWaypoint(intercoord,self.engage.Speed,uid,self.engage.Formation,true) self.engage.Waypoint.detour=1 end function ARMYGROUP:_UpdateEngageTarget() if self.engage.Target and self.engage.Target:IsAlive()then local vec3=self.engage.Target:GetVec3() if vec3 then local dist=UTILS.VecDist3D(vec3,self.engage.Coordinate:GetVec3()) local los=self:HasLoS(vec3) if dist>100 or los==false then self.engage.Coordinate:UpdateFromVec3(vec3) local uid=self:GetWaypointCurrentUID() self:RemoveWaypointByID(self.engage.Waypoint.uid) local intercoord=self:GetCoordinate():GetIntermediateCoordinate(self.engage.Coordinate,0.95) self.engage.Waypoint=self:AddWaypoint(intercoord,self.engage.Speed,uid,self.engage.Formation,true) self.engage.Waypoint.detour=1 end else self:T(self.lid.."Could not get position of target ==> Disengage!") self:Disengage() end else self:T(self.lid.."Target not ALIVE ==> Disengage!") self:Disengage() end end function ARMYGROUP:onafterDisengage(From,Event,To) self:T(self.lid.."Disengage Target") self:SwitchROE(self.engage.roe) self:SwitchAlarmstate(self.engage.alarmstate) local task=self:GetTaskCurrent() if task and task.dcstask.id==AUFTRAG.SpecialTask.GROUNDATTACK then self:T(self.lid.."Disengage with current task GROUNDATTACK ==> Task Done!") self:TaskDone(task) end if self.engage.Waypoint then self:RemoveWaypointByID(self.engage.Waypoint.uid) end self:_CheckGroupDone(1) end function ARMYGROUP:onafterDetourReached(From,Event,To) self:T(self.lid.."Group reached detour coordinate") end function ARMYGROUP:onafterFullStop(From,Event,To) self:T(self.lid..string.format("Full stop!")) local pos=self:GetCoordinate() local wp=pos:WaypointGround(0) self:Route({wp}) end function ARMYGROUP:onafterCruise(From,Event,To,Speed,Formation) self.Twaiting=nil self.dTwait=nil self:T(self.lid..string.format("Cruise ==> Update route in 0.01 sec (speed=%s, formation=%s)",tostring(Speed),tostring(Formation))) self:__UpdateRoute(-0.01,nil,nil,Speed,Formation) end function ARMYGROUP:onafterHit(From,Event,To,Enemy) self:T(self.lid..string.format("ArmyGroup hit by %s",Enemy and Enemy:GetName()or"unknown")) if self.suppressionOn then env.info(self.lid.."FF suppress") self:_Suppress() end end function ARMYGROUP:AddWaypoint(Coordinate,Speed,AfterWaypointWithID,Formation,Updateroute) self:T(self.lid..string.format("AddWaypoint Formation = %s",tostring(Formation))) local coordinate=self:_CoordinateFromObject(Coordinate) local wpnumber=self:GetWaypointIndexAfterID(AfterWaypointWithID) Speed=Speed or self:GetSpeedCruise() if not Formation then if self.formationPerma then Formation=self.formationPerma elseif self.optionDefault.Formation then Formation=self.optionDefault.Formation elseif self.option.Formation then Formation=self.option.Formation else Formation=ENUMS.Formation.Vehicle.OnRoad end self:T2(self.lid..string.format("Formation set to = %s",tostring(Formation))) end local wp=coordinate:WaypointGround(UTILS.KnotsToKmph(Speed),Formation) local waypoint=self:_CreateWaypoint(wp) self:_AddWaypoint(waypoint,wpnumber) waypoint.roadcoord=coordinate:GetClosestPointToRoad(false) if waypoint.roadcoord then waypoint.roaddist=coordinate:Get2DDistance(waypoint.roadcoord) else waypoint.roaddist=1000*1000 end self:T(self.lid..string.format("Adding waypoint UID=%d (index=%d), Speed=%.1f knots, Dist2Road=%d m, Action=%s",waypoint.uid,wpnumber,Speed,waypoint.roaddist,waypoint.action)) if Updateroute==nil or Updateroute==true then self:__UpdateRoute(-0.01) end return waypoint end function ARMYGROUP:_InitGroup(Template,Delay) if Delay and Delay>0 then self:ScheduleOnce(Delay,ARMYGROUP._InitGroup,self,Template,0) else if self.groupinitialized then self:T(self.lid.."WARNING: Group was already initialized! Will NOT do it again!") return end local template=Template or self:_GetTemplate() self.isAI=true self.isLateActivated=template.lateActivation self.isUncontrolled=false self.speedMax=self.group:GetSpeedMax() if self.speedMax and self.speedMax>3.6 then self.isMobile=true else self.isMobile=false self.speedMax=0 end self.speedCruise=self.speedMax*0.7 self.ammo=self:GetAmmoTot() self.radio.On=false self.radio.Freq=133 self.radio.Modu=radio.modulation.AM self:SetDefaultRadio(self.radio.Freq,self.radio.Modu,self.radio.On) self.option.Formation=template.route.points[1].action self.optionDefault.Formation=ENUMS.Formation.Vehicle.OnRoad if not self.tacanDefault then self:SetDefaultTACAN(nil,nil,nil,nil,true) end if not self.tacan then self.tacan=UTILS.DeepCopy(self.tacanDefault) end local units=self.group:GetUnits() local dcsgroup=Group.getByName(self.groupname) local size0=dcsgroup:getInitialSize() local u=dcsgroup:getUnits() if#units~=size0 then self:T(self.lid..string.format("ERROR: Got #units=%d but group consists of %d units! u=%d",#units,size0,#u)) end for _,unit in pairs(units)do local unitname=unit:GetName() self:_AddElementByName(unitname) end self.groupinitialized=true end return self end function ARMYGROUP:SwitchFormation(Formation,Permanently,NoRouteUpdate) if self:IsAlive()or self:IsInUtero()then Formation=Formation or(self.optionDefault.Formation or"Off road") Permanently=Permanently or false if Permanently then self.formationPerma=Formation else self.formationPerma=nil end self.option.Formation=Formation or"Off road" if self:IsInUtero()then self:T(self.lid..string.format("Will switch formation to %s (permanently=%s) when group is spawned",tostring(self.option.Formation),tostring(Permanently))) else if NoRouteUpdate then else self:__UpdateRoute(-1,nil,nil,Formation) end self:T(self.lid..string.format("Switching formation to %s (permanently=%s)",tostring(self.option.Formation),tostring(Permanently))) end end return self end function ARMYGROUP:FindNearestAmmoSupply(Radius) Radius=UTILS.NMToMeters(Radius or 30) local coord=self:GetCoordinate() local myCoalition=self:GetCoalition() local units=coord:ScanUnits(Radius) local dmin=math.huge local truck=nil for _,_unit in pairs(units.Set)do local unit=_unit if unit:IsAlive()and unit:GetCoalition()==myCoalition and unit:IsAmmoSupply()and unit:GetVelocityKMH()<1 then local d=coord:Get2DDistance(unit:GetCoord()) if dself.TsuppressionOver then self.TsuppressionOver=Tnow+Tsuppress else renew=false end end if renew then self:__Unsuppressed(self.TsuppressionOver-Tnow) end self:T(self.lid..string.format("Suppressed for %d sec",Tsuppress)) end function ARMYGROUP:onbeforeUnsuppressed(From,Event,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 ARMYGROUP:onafterUnsuppressed(From,Event,To) local text=string.format("Group %s has recovered!",self:GetName()) MESSAGE:New(text,10):ToAll() self:T(self.lid..text) self:SwitchROE(self.suppressionROE) if true then self.group:FlareGreen() end end ARMYGROUP.HuntingPatrol={} function ARMYGROUP:EnableHuntingPatrol(Zone,Speed,Formation,Interval) self.hp_zone=Zone self.hp_speed=tonumber(Speed)or 20 self.hp_formation=Formation or nil local intervalNum=tonumber(Interval) if not intervalNum or intervalNum<=0 then intervalNum=5 end self.hp_interval=intervalNum self.hp_target=nil if self.hp_timer then self.hp_timer:Stop() self.hp_timer=nil end self.hp_timer=TIMER:New(self._HuntingPatrolUpdate,self):Start(1,self.hp_interval) self:T(self.lid.."HuntingPatrol: enabled. Interval="..tostring(self.hp_interval)) return self end function ARMYGROUP:DisableHuntingPatrol() if self.hp_timer then self.hp_timer:Stop() self.hp_timer=nil end if self.HuntingEnemySet then self.HuntingEnemySet:FilterStop() self.HuntingEnemySet=nil end self.hp_target=nil self:T(self.lid.."HuntingPatrol: disabled.") return self end function ARMYGROUP:_HuntingPatrolUpdate() if not self:IsAlive()then self:DisableHuntingPatrol() return end local mission=self:GetMissionCurrent() if not mission or mission.type~=AUFTRAG.Type.PATROLZONE then return end if not self:IsCombatReady()then return end if self.hp_target then if not self.hp_target:IsAlive()then self.hp_target=nil self:Disengage() end return end local enemy=self:_HuntingPatrolFindEnemyInZone(self.hp_zone) if enemy then self.hp_target=enemy self:EngageTarget(enemy,self.hp_speed,self.hp_formation) end end function ARMYGROUP:_HuntingPatrolFindEnemyInZone(Zone) if not Zone then return nil end local myCoalition=self:GetCoalition() if not self.HuntingEnemySet then self.HuntingEnemySet=SET_UNIT:New() :FilterActive(true) :FilterCategories("ground") :FilterStart() end local enemies={} self.HuntingEnemySet:ForEachUnitCompletelyInZone(Zone,function(unit) if unit:GetCoalition()~=myCoalition and unit:IsAlive()then table.insert(enemies,unit) end end) if#enemies==0 then return nil end local myCoord=self:GetCoordinate() table.sort(enemies,function(a,b) return myCoord:Get2DDistance(a:GetCoordinate())< myCoord:Get2DDistance(b:GetCoordinate()) end) return enemies[1] end function ARMYGROUP.EnableHuntingPatrolForGroup(group,zone,speed,formation,interval) local army=ARMYGROUP:New(group) army:SetPatrolAdInfinitum(true) army:EnableHuntingPatrol(zone,speed,formation,interval) return army end AUFTRAG={ ClassName="AUFTRAG", verbose=0, lid=nil, auftragsnummer=nil, groupdata={}, legions={}, statusLegion={}, requestID={}, assets={}, NassetsLegMin={}, NassetsLegMax={}, missionFraction=0.5, enrouteTasks={}, marker=nil, markerOn=nil, markerCoalition=nil, conditionStart={}, conditionSuccess={}, conditionFailure={}, conditionPush={}, conditionSuccessSet=false, conditionFailureSet=false, repeatDelay=1, } _AUFTRAGSNR=0 AUFTRAG.Type={ ANTISHIP="Anti Ship", AWACS="AWACS", BAI="BAI", BOMBING="Bombing", BOMBRUNWAY="Bomb Runway", BOMBCARPET="Carpet Bombing", CAP="CAP", CAS="CAS", ESCORT="Escort", FAC="FAC", FACA="FAC-A", FERRY="Ferry Flight", GROUNDESCORT="Ground Escort", INTERCEPT="Intercept", ORBIT="Orbit", GCICAP="Ground Controlled CAP", RECON="Recon", RECOVERYTANKER="Recovery Tanker", RESCUEHELO="Rescue Helo", SEAD="SEAD", STRIKE="Strike", TANKER="Tanker", TROOPTRANSPORT="Troop Transport", ARTY="Fire At Point", PATROLZONE="Patrol Zone", OPSTRANSPORT="Ops Transport", AMMOSUPPLY="Ammo Supply", FUELSUPPLY="Fuel Supply", ALERT5="Alert5", ONGUARD="On Guard", ARMOREDGUARD="Armored Guard", BARRAGE="Barrage", ARMORATTACK="Armor Attack", CASENHANCED="CAS Enhanced", HOVER="Hover", LANDATCOORDINATE="Land at Coordinate", GROUNDATTACK="Ground Attack", NAVALENGAGEMENT="Naval Engagement", CARGOTRANSPORT="Cargo Transport", RELOCATECOHORT="Relocate Cohort", AIRDEFENSE="Air Defence", EWR="Early Warning Radar", REARMING="Rearming", CAPTUREZONE="Capture Zone", NOTHING="Nothing", PATROLRACETRACK="Patrol Racetrack", STRAFING="Strafing", FREIGHTTRANSPORT="FREIGHTTRANSPORT", } AUFTRAG.SpecialTask={ FORMATION="Formation", PATROLZONE="PatrolZone", RECON="ReconMission", AMMOSUPPLY="Ammo Supply", FUELSUPPLY="Fuel Supply", ALERT5="Alert5", ONGUARD="On Guard", ARMOREDGUARD="ArmoredGuard", BARRAGE="Barrage", ARMORATTACK="AmorAttack", HOVER="Hover", GROUNDATTACK="Ground Attack", NAVALENGAGEMENT="Naval Engagement", FERRY="Ferry", RELOCATECOHORT="Relocate Cohort", AIRDEFENSE="Air Defense", EWR="Early Warning Radar", RECOVERYTANKER="Recovery Tanker", REARMING="Rearming", CAPTUREZONE="Capture Zone", NOTHING="Nothing", PATROLRACETRACK="Patrol Racetrack", } AUFTRAG.Status={ PLANNED="planned", QUEUED="queued", REQUESTED="requested", SCHEDULED="scheduled", STARTED="started", EXECUTING="executing", DONE="done", CANCELLED="cancelled", SUCCESS="success", FAILED="failed", } AUFTRAG.GroupStatus={ SCHEDULED="scheduled", STARTED="started", EXECUTING="executing", PAUSED="paused", DONE="done", CANCELLED="cancelled", } AUFTRAG.TargetType={ GROUP="Group", UNIT="Unit", STATIC="Static", COORDINATE="Coordinate", AIRBASE="Airbase", SETGROUP="SetGroup", SETUNIT="SetUnit", } AUFTRAG.Category={ ALL="All", AIRCRAFT="Aircraft", AIRPLANE="Airplane", HELICOPTER="Helicopter", GROUND="Ground", NAVAL="Naval", } AUFTRAG.version="1.4.2" function AUFTRAG:New(Type) local self=BASE:Inherit(self,FSM:New()) _AUFTRAGSNR=_AUFTRAGSNR+1 self.type=Type self.auftragsnummer=_AUFTRAGSNR self:_SetLogID() self.status=AUFTRAG.Status.PLANNED self:SetName() self:SetPriority() self:SetTime() self:SetRequiredAssets() self.engageAsGroup=true self.dTevaluate=5 self.repeated=0 self.repeatedSuccess=0 self.repeatedFailure=0 self.Nrepeat=0 self.NrepeatFailure=0 self.NrepeatSuccess=0 self.Ncasualties=0 self.Nkills=0 self.Nelements=0 self.Ngroups=0 self.Nassigned=nil self.Ndead=0 self:SetStartState(self.status) self:AddTransition("*","Planned",AUFTRAG.Status.PLANNED) self:AddTransition(AUFTRAG.Status.PLANNED,"Queued",AUFTRAG.Status.QUEUED) self:AddTransition(AUFTRAG.Status.QUEUED,"Requested",AUFTRAG.Status.REQUESTED) self:AddTransition(AUFTRAG.Status.REQUESTED,"Scheduled",AUFTRAG.Status.SCHEDULED) self:AddTransition(AUFTRAG.Status.PLANNED,"Scheduled",AUFTRAG.Status.SCHEDULED) self:AddTransition(AUFTRAG.Status.SCHEDULED,"Started",AUFTRAG.Status.STARTED) self:AddTransition(AUFTRAG.Status.STARTED,"Executing",AUFTRAG.Status.EXECUTING) self:AddTransition("*","Done",AUFTRAG.Status.DONE) self:AddTransition("*","Cancel",AUFTRAG.Status.CANCELLED) self:AddTransition("*","Success",AUFTRAG.Status.SUCCESS) self:AddTransition("*","Failed",AUFTRAG.Status.FAILED) self:AddTransition("*","Status","*") self:AddTransition("*","Stop","*") self:AddTransition("*","Repeat",AUFTRAG.Status.PLANNED) self:AddTransition("*","ElementDestroyed","*") self:AddTransition("*","GroupDead","*") self:AddTransition("*","AssetDead","*") self:__Status(-1) return self end function AUFTRAG:NewANTISHIP(Target,Altitude) local mission=AUFTRAG:New(AUFTRAG.Type.ANTISHIP) mission:_TargetFromObject(Target) mission.engageWeaponType=ENUMS.WeaponFlag.Auto mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL mission.engageAltitude=UTILS.FeetToMeters(Altitude or 2000) mission.missionTask=ENUMS.MissionTask.ANTISHIPSTRIKE mission.missionAltitude=mission.engageAltitude mission.missionFraction=0.4 mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.EvadeFire mission.categories={AUFTRAG.Category.AIRCRAFT} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewHOVER(Coordinate,Altitude,Time,Speed,MissionAlt) local mission=AUFTRAG:New(AUFTRAG.Type.HOVER) if Altitude then mission.hoverAltitude=Coordinate:GetLandHeight()+UTILS.FeetToMeters(Altitude) else mission.hoverAltitude=Coordinate:GetLandHeight()+UTILS.FeetToMeters(50) end mission:_TargetFromObject(Coordinate) mission.hoverSpeed=0.1 mission.hoverTime=Time or 300 self:SetMissionSpeed(Speed or 150) self:SetMissionAltitude(MissionAlt or 1000) mission.missionFraction=0.9 mission.optionROE=ENUMS.ROE.ReturnFire mission.optionROT=ENUMS.ROT.PassiveDefense mission.categories={AUFTRAG.Category.HELICOPTER} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewLANDATCOORDINATE(Coordinate,OuterRadius,InnerRadius,Time,Speed,MissionAlt,CombatLanding,DirectionAfterLand) local mission=AUFTRAG:New(AUFTRAG.Type.LANDATCOORDINATE) mission:_TargetFromObject(Coordinate) mission.stayTime=Time or 300 mission.stayAt=Coordinate mission.combatLand=CombatLanding mission.directionAfter=DirectionAfterLand self:SetMissionSpeed(Speed or 150) self:SetMissionAltitude(MissionAlt or 1000) if OuterRadius then mission.stayAt=Coordinate:GetRandomCoordinateInRadius(UTILS.FeetToMeters(OuterRadius),UTILS.FeetToMeters(InnerRadius or 0)) end mission.missionFraction=0.9 mission.optionROE=ENUMS.ROE.ReturnFire mission.optionROT=ENUMS.ROT.PassiveDefense mission.categories={AUFTRAG.Category.HELICOPTER} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewPATROL_RACETRACK(Coordinate,Altitude,Speed,Heading,Leg,Formation) local mission=AUFTRAG:New(AUFTRAG.Type.PATROLRACETRACK) mission:_TargetFromObject(Coordinate) if Altitude then mission.TrackAltitude=UTILS.FeetToMeters(Altitude) else mission.TrackAltitude=UTILS.FeetToMeters(20000) end mission.TrackPoint1=Coordinate local leg=UTILS.NMToMeters(Leg)or UTILS.NMToMeters(10) local heading=Heading or 90 if heading<0 or heading>360 then heading=90 end mission.TrackPoint2=Coordinate:Translate(leg,heading,true) mission.TrackSpeed=UTILS.IasToTas(UTILS.KnotsToKmph(Speed or 300),mission.TrackAltitude) mission.missionSpeed=UTILS.KnotsToKmph(Speed or 300) mission.missionAltitude=mission.TrackAltitude*0.9 mission.missionTask=ENUMS.MissionTask.CAP mission.optionROE=ENUMS.ROE.ReturnFire mission.optionROT=ENUMS.ROT.PassiveDefense mission.categories={AUFTRAG.Category.AIRCRAFT} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewORBIT(Coordinate,Altitude,Speed,Heading,Leg) local mission=AUFTRAG:New(AUFTRAG.Type.ORBIT) mission:_TargetFromObject(Coordinate) if Altitude then mission.orbitAltitude=UTILS.FeetToMeters(Altitude) else mission.orbitAltitude=Coordinate.y end mission.orbitSpeed=UTILS.IasToTas(UTILS.KnotsToMps(Speed or 350),mission.orbitAltitude) mission.missionSpeed=UTILS.KnotsToKmph(Speed or 350) if Leg then mission.orbitLeg=UTILS.NMToMeters(Leg) if Heading and Heading<0 then mission.orbitHeadingRel=true Heading=-Heading end mission.orbitHeading=Heading end mission.missionAltitude=mission.orbitAltitude*0.9 mission.missionFraction=0.9 mission.optionROE=ENUMS.ROE.ReturnFire mission.optionROT=ENUMS.ROT.PassiveDefense mission.categories={AUFTRAG.Category.AIRCRAFT} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewORBIT_CIRCLE(Coordinate,Altitude,Speed) local mission=AUFTRAG:NewORBIT(Coordinate,Altitude,Speed) return mission end function AUFTRAG:NewORBIT_RACETRACK(Coordinate,Altitude,Speed,Heading,Leg) Heading=Heading or math.random(360) Leg=Leg or 10 local mission=AUFTRAG:NewORBIT(Coordinate,Altitude,Speed,Heading,Leg) return mission end function AUFTRAG:NewORBIT_GROUP(Group,Altitude,Speed,Leg,Heading,OffsetVec2,Distance) Altitude=Altitude or 6000 local mission=AUFTRAG:NewORBIT(Group,Altitude,Speed,Heading,Leg) mission.updateDCSTask=true if OffsetVec2 then if OffsetVec2.x then OffsetVec2.x=UTILS.NMToMeters(OffsetVec2.x) end if OffsetVec2.y then OffsetVec2.y=UTILS.NMToMeters(OffsetVec2.y) end if OffsetVec2.r then OffsetVec2.r=UTILS.NMToMeters(OffsetVec2.r) end end mission.orbitOffsetVec2=OffsetVec2 mission.orbitDeltaR=UTILS.NMToMeters(Distance or 5) mission:GetDCSMissionTask() return mission end function AUFTRAG:NewGCICAP(Coordinate,Altitude,Speed,Heading,Leg) local mission=AUFTRAG:NewORBIT_RACETRACK(Coordinate,Altitude,Speed,Heading,Leg) mission.type=AUFTRAG.Type.GCICAP mission:_SetLogID() mission.missionTask=ENUMS.MissionTask.INTERCEPT mission.optionROT=ENUMS.ROT.PassiveDefense mission.categories={AUFTRAG.Category.AIRCRAFT} return mission end function AUFTRAG:NewTANKER(Coordinate,Altitude,Speed,Heading,Leg,RefuelSystem) local mission if Leg==0 then mission=AUFTRAG:NewORBIT_CIRCLE(Coordinate,Altitude,Speed) else mission=AUFTRAG:NewORBIT_RACETRACK(Coordinate,Altitude,Speed,Heading,Leg) end mission.type=AUFTRAG.Type.TANKER mission:_SetLogID() mission.refuelSystem=RefuelSystem mission.missionTask=ENUMS.MissionTask.REFUELING mission.optionROE=ENUMS.ROE.WeaponHold mission.optionROT=ENUMS.ROT.PassiveDefense mission.categories={AUFTRAG.Category.AIRCRAFT} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewAWACS(Coordinate,Altitude,Speed,Heading,Leg) local mission=nil if Coordinate:IsInstanceOf("COORDINATE")then mission=AUFTRAG:NewORBIT_RACETRACK(Coordinate,Altitude,Speed,Heading,Leg) elseif Coordinate:IsInstanceOf("UNIT")then local OffsetVec2={r=6,phi=180} mission=AUFTRAG:NewORBIT_GROUP(Coordinate,Altitude,Speed,Leg,Heading,OffsetVec2) else BASE:E("ERROR in AUFTRAG:NewAWACS: You must pass a COORDINATE or UNIT object!") return nil end mission.type=AUFTRAG.Type.AWACS mission:_SetLogID() mission.missionTask=ENUMS.MissionTask.AWACS mission.optionROE=ENUMS.ROE.WeaponHold mission.optionROT=ENUMS.ROT.PassiveDefense mission.categories={AUFTRAG.Category.AIRCRAFT} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewRECOVERYTANKER(Carrier,Altitude,Speed,Leg,RelHeading,OffsetDist,OffsetAngle,UpdateDistance) local OffsetVec2={r=OffsetDist or 6,phi=OffsetAngle or 180} Leg=Leg or 14 Speed=Speed or 250 local Heading=nil if RelHeading then Heading=-math.abs(RelHeading) end local mission=AUFTRAG:NewORBIT_GROUP(Carrier,Altitude,Speed,Leg,Heading,OffsetVec2,UpdateDistance) mission.type=AUFTRAG.Type.RECOVERYTANKER mission.missionTask=ENUMS.MissionTask.REFUELING mission.missionFraction=0.9 mission.optionROE=ENUMS.ROE.WeaponHold mission.optionROT=ENUMS.ROT.NoReaction mission.categories={AUFTRAG.Category.AIRPLANE} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewINTERCEPT(Target) local mission=AUFTRAG:New(AUFTRAG.Type.INTERCEPT) mission:_TargetFromObject(Target) mission.missionTask=ENUMS.MissionTask.INTERCEPT mission.missionFraction=0.1 mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.EvadeFire mission.categories={AUFTRAG.Category.AIRCRAFT} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewCAP(ZoneCAP,Altitude,Speed,Coordinate,Heading,Leg,TargetTypes) TargetTypes=UTILS.EnsureTable(TargetTypes,true) Altitude=Altitude or 10000 local mission=AUFTRAG:NewORBIT(Coordinate or ZoneCAP:GetCoordinate(),Altitude,Speed or 350,Heading,Leg) mission.type=AUFTRAG.Type.CAP mission:_SetLogID() mission.engageZone=ZoneCAP or Coordinate mission.engageTargetTypes=TargetTypes or{"Air"} mission.missionTask=ENUMS.MissionTask.CAP mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.EvadeFire mission.missionSpeed=UTILS.KnotsToKmph(UTILS.KnotsToAltKIAS(Speed or 350,Altitude)) mission.categories={AUFTRAG.Category.AIRCRAFT} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewCAPGROUP(Grp,Altitude,Speed,RelHeading,Leg,OffsetDist,OffsetAngle,UpdateDistance,TargetTypes,EngageRange) TargetTypes=UTILS.EnsureTable(TargetTypes,true) local OffsetVec2={r=OffsetDist or 6,phi=OffsetAngle or 180} Leg=Leg or 14 local Heading=nil if RelHeading then Heading=-math.abs(RelHeading) end local mission=AUFTRAG:NewORBIT_GROUP(Grp,Altitude,Speed,Leg,Heading,OffsetVec2,UpdateDistance) mission.type=AUFTRAG.Type.CAP mission:_SetLogID() local engage=EngageRange or 32 local zoneCAPGroup=ZONE_GROUP:New("CAPGroup",Grp,UTILS.NMToMeters(engage)) mission.engageZone=zoneCAPGroup mission.engageTargetTypes=TargetTypes or{"Air"} mission.missionTask=ENUMS.MissionTask.CAP mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.EvadeFire mission.categories={AUFTRAG.Category.AIRCRAFT} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewCAS(ZoneCAS,Altitude,Speed,Coordinate,Heading,Leg,TargetTypes) TargetTypes=UTILS.EnsureTable(TargetTypes,true) local mission=AUFTRAG:NewORBIT(Coordinate or ZoneCAS:GetCoordinate(),Altitude or 10000,Speed,Heading,Leg) mission.type=AUFTRAG.Type.CAS mission:_SetLogID() mission.engageZone=ZoneCAS mission.engageTargetTypes=TargetTypes or{"Helicopters","Ground Units","Light armed ships"} mission.missionTask=ENUMS.MissionTask.CAS mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.EvadeFire mission.categories={AUFTRAG.Category.AIRCRAFT} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewCASENHANCED(CasZone,Altitude,Speed,RangeMax,NoEngageZoneSet,TargetTypes) local mission=AUFTRAG:New(AUFTRAG.Type.CASENHANCED) if type(CasZone)=="string"then CasZone=ZONE:New(CasZone) end mission:_TargetFromObject(CasZone) mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.CASENHANCED) mission:SetEngageDetected(RangeMax,TargetTypes or{"Helicopters","Ground Units","Light armed ships"},CasZone,NoEngageZoneSet) mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.EvadeFire mission.missionFraction=0.5 mission.missionSpeed=Speed and UTILS.KnotsToKmph(Speed)or nil mission.missionAltitude=Altitude and UTILS.FeetToMeters(Altitude)or nil mission.dTevaluate=15 mission.categories={AUFTRAG.Category.AIRCRAFT} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewFAC(FacZone,Speed,Altitude,Frequency,Modulation) local mission=AUFTRAG:New(AUFTRAG.Type.FAC) if type(FacZone)=="string"then FacZone=ZONE:FindByName(FacZone) end mission:_TargetFromObject(FacZone) mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.FAC) mission.facFreq=Frequency or 133 mission.facModu=Modulation or radio.modulation.AM mission.optionROE=ENUMS.ROE.ReturnFire mission.optionROT=ENUMS.ROT.EvadeFire mission.optionAlarm=ENUMS.AlarmState.Auto mission.missionFraction=1.0 mission.missionSpeed=Speed and UTILS.KnotsToKmph(Speed)or nil mission.missionAltitude=Altitude and UTILS.FeetToMeters(Altitude)or nil mission.categories={AUFTRAG.Category.AIRCRAFT,AUFTRAG.Category.GROUND} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewFACA(Target,Designation,DataLink,Frequency,Modulation) local mission=AUFTRAG:New(AUFTRAG.Type.FACA) mission:_TargetFromObject(Target) mission.facDesignation=Designation mission.facDatalink=true mission.facFreq=Frequency or 133 mission.facModu=Modulation or radio.modulation.AM mission.missionTask=ENUMS.MissionTask.AFAC mission.missionAltitude=nil mission.missionFraction=0.5 mission.optionROE=ENUMS.ROE.ReturnFire mission.optionROT=ENUMS.ROT.PassiveDefense mission.categories={AUFTRAG.Category.AIRCRAFT} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewBAI(Target,Altitude) local mission=AUFTRAG:New(AUFTRAG.Type.BAI) mission:_TargetFromObject(Target) mission.engageWeaponType=ENUMS.WeaponFlag.Auto mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL mission.engageAltitude=UTILS.FeetToMeters(Altitude or 5000) mission.missionTask=ENUMS.MissionTask.GROUNDATTACK mission.missionAltitude=mission.engageAltitude mission.missionFraction=0.75 mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.PassiveDefense mission.categories={AUFTRAG.Category.AIRCRAFT} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewSEAD(Target,Altitude) local mission=AUFTRAG:New(AUFTRAG.Type.SEAD) mission:_TargetFromObject(Target) mission.engageWeaponType=ENUMS.WeaponFlag.Auto mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL mission.engageAltitude=UTILS.FeetToMeters(Altitude or 25000) mission.missionTask=ENUMS.MissionTask.SEAD mission.missionAltitude=mission.engageAltitude mission.missionFraction=0.2 mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.EvadeFire mission.categories={AUFTRAG.Category.AIRCRAFT} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewSEADInZone(TargetZone,Altitude,TargetTypes,Duration) local mission=AUFTRAG:New(AUFTRAG.Type.SEAD) mission.engageWeaponType=ENUMS.WeaponFlag.Auto mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL mission.engageAltitude=UTILS.FeetToMeters(Altitude or 25000) mission.engageZone=TargetZone mission.engageTargetTypes=TargetTypes or{"Air Defence"} mission.missionTask=ENUMS.MissionTask.SEAD mission.missionAltitude=mission.engageAltitude mission.missionFraction=0.2 mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.EvadeFire mission.categories={AUFTRAG.Category.AIRCRAFT} mission.DCStask=mission:GetDCSMissionTask() mission:SetDuration(Duration or 1800) return mission end function AUFTRAG:NewSTRIKE(Target,Altitude,EngageWeaponType) local mission=AUFTRAG:New(AUFTRAG.Type.STRIKE) mission:_TargetFromObject(Target) mission.engageWeaponType=EngageWeaponType or ENUMS.WeaponFlag.Auto mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL mission.engageAltitude=UTILS.FeetToMeters(Altitude or 2000) mission.missionTask=ENUMS.MissionTask.GROUNDATTACK mission.missionAltitude=mission.engageAltitude mission.missionFraction=0.75 mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.PassiveDefense mission.categories={AUFTRAG.Category.AIRCRAFT} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewBOMBING(Target,Altitude,EngageWeaponType,Divebomb) local mission=AUFTRAG:New(AUFTRAG.Type.BOMBING) mission:_TargetFromObject(Target) mission.engageWeaponType=EngageWeaponType or ENUMS.WeaponFlag.Auto mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL mission.engageAltitude=UTILS.FeetToMeters(Altitude or 25000) mission.missionTask=ENUMS.MissionTask.GROUNDATTACK mission.missionAltitude=mission.engageAltitude*0.8 mission.missionFraction=0.5 mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.NoReaction mission.optionDivebomb=Divebomb or nil mission.dTevaluate=5*60 mission.categories={AUFTRAG.Category.AIRCRAFT} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewSTRAFING(Target,Altitude,Length) local mission=AUFTRAG:New(AUFTRAG.Type.STRAFING) mission:_TargetFromObject(Target) mission.engageWeaponType=805337088 mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL mission.engageAltitude=UTILS.FeetToMeters(Altitude or 1000) mission.engageLength=Length mission.missionTask=ENUMS.MissionTask.GROUNDATTACK mission.missionAltitude=mission.engageAltitude*0.8 mission.missionFraction=0.5 mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.NoReaction mission.dTevaluate=5*60 mission.categories={AUFTRAG.Category.AIRCRAFT} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewBOMBRUNWAY(Airdrome,Altitude) if type(Airdrome)=="string"then Airdrome=AIRBASE:FindByName(Airdrome) end local mission=AUFTRAG:New(AUFTRAG.Type.BOMBRUNWAY) mission:_TargetFromObject(Airdrome) mission.engageWeaponType=ENUMS.WeaponFlag.Auto mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL mission.engageAltitude=UTILS.FeetToMeters(Altitude or 25000) mission.missionTask=ENUMS.MissionTask.RUNWAYATTACK mission.missionAltitude=mission.engageAltitude*0.8 mission.missionFraction=0.75 mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.PassiveDefense mission.dTevaluate=5*60 mission.categories={AUFTRAG.Category.AIRCRAFT} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewBOMBCARPET(Target,Altitude,CarpetLength) local mission=AUFTRAG:New(AUFTRAG.Type.BOMBCARPET) mission:_TargetFromObject(Target) mission.engageWeaponType=ENUMS.WeaponFlag.Auto mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL mission.engageAltitude=UTILS.FeetToMeters(Altitude or 25000) mission.engageLength=CarpetLength or 500 mission.engageAsGroup=false mission.engageDirection=nil mission.missionTask=ENUMS.MissionTask.GROUNDATTACK mission.missionAltitude=mission.engageAltitude*0.8 mission.missionFraction=0.5 mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.NoReaction mission.dTevaluate=5*60 mission.categories={AUFTRAG.Category.AIRCRAFT} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewGROUNDESCORT(EscortGroup,OrbitDistance,TargetTypes) local mission=AUFTRAG:New(AUFTRAG.Type.GROUNDESCORT) if type(EscortGroup)=="string"then mission.escortGroupName=EscortGroup mission:_TargetFromObject() else mission:_TargetFromObject(EscortGroup) end mission.orbitDistance=OrbitDistance and UTILS.NMToMeters(OrbitDistance)or UTILS.NMToMeters(1.5) mission.engageTargetTypes=TargetTypes or{"Ground vehicles"} mission.missionTask=ENUMS.MissionTask.GROUNDESCORT mission.missionFraction=0.1 mission.missionAltitude=100 mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.EvadeFire mission.categories={AUFTRAG.Category.HELICOPTER} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewESCORT(EscortGroup,OffsetVector,EngageMaxDistance,TargetTypes) local mission=AUFTRAG:New(AUFTRAG.Type.ESCORT) if type(EscortGroup)=="string"then mission.escortGroupName=EscortGroup mission:_TargetFromObject() else mission:_TargetFromObject(EscortGroup) end mission.escortVec3=OffsetVector or{x=-100,y=0,z=200} mission.engageMaxDistance=EngageMaxDistance and UTILS.NMToMeters(EngageMaxDistance)or UTILS.NMToMeters(32) mission.engageTargetTypes=TargetTypes or{"Air"} mission.missionTask=ENUMS.MissionTask.ESCORT mission.missionFraction=0.1 mission.missionAltitude=1000 mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.PassiveDefense mission.categories={AUFTRAG.Category.AIRCRAFT} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewRESCUEHELO(Carrier) local mission=AUFTRAG:New(AUFTRAG.Type.RESCUEHELO) mission:_TargetFromObject(Carrier) mission.missionTask=ENUMS.MissionTask.NOTHING mission.missionFraction=0.9 mission.optionROE=ENUMS.ROE.WeaponHold mission.optionROT=ENUMS.ROT.NoReaction mission.categories={AUFTRAG.Category.HELICOPTER} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewTROOPTRANSPORT(TransportGroupSet,DropoffCoordinate,PickupCoordinate,PickupRadius) local mission=AUFTRAG:New(AUFTRAG.Type.TROOPTRANSPORT) if TransportGroupSet:IsInstanceOf("GROUP")then mission.transportGroupSet=SET_GROUP:New() mission.transportGroupSet:AddGroup(TransportGroupSet) elseif TransportGroupSet:IsInstanceOf("SET_GROUP")then mission.transportGroupSet=TransportGroupSet else mission:E(mission.lid.."ERROR: TransportGroupSet must be a GROUP or SET_GROUP object!") return nil end mission:_TargetFromObject(mission.transportGroupSet) mission.transportPickup=PickupCoordinate or mission:GetTargetCoordinate() mission.transportDropoff=DropoffCoordinate mission.transportPickupRadius=PickupRadius or 100 mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.TROOPTRANSPORT) mission.optionROE=ENUMS.ROE.ReturnFire mission.optionROT=ENUMS.ROT.PassiveDefense mission.categories={AUFTRAG.Category.HELICOPTER,AUFTRAG.Category.GROUND} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewCARGOTRANSPORT(StaticCargo,DropZone) local mission=AUFTRAG:New(AUFTRAG.Type.CARGOTRANSPORT) mission:_TargetFromObject(StaticCargo) mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.CARGOTRANSPORT) mission.optionROE=ENUMS.ROE.ReturnFire mission.optionROT=ENUMS.ROT.PassiveDefense mission.categories={AUFTRAG.Category.HELICOPTER} mission.DCStask=mission:GetDCSMissionTask() mission.DCStask.params.groupId=StaticCargo:GetID() mission.DCStask.params.zoneId=DropZone.ZoneID mission.DCStask.params.zone=DropZone mission.DCStask.params.cargo=StaticCargo return mission end function AUFTRAG:NewFREIGHTTRANSPORT(StaticCargo,Destination) if Destination==nil then BASE:E(self.lid..string.format("ERROR: Destination is nil for AUFTRAG:NewFREIGHTTRANSPORT! You must specify the destination airbase")) return nil elseif type(Destination)=="string"then Destination=AIRBASE:FindByName(Destination) end if StaticCargo==nil then BASE:E(self.lid..string.format("ERROR: StaticCargo is nil for AUFTRAG:NewFREIGHTTRANSPORT! You must specify the static object that represents the cargo")) return nil elseif type(StaticCargo)=="string"then StaticCargo=STATIC:FindByName(StaticCargo) end if StaticCargo:IsInstanceOf("STATIC")then local StaticCargoSet=SET_STATIC:New() StaticCargoSet:AddCargo(StaticCargo) StaticCargo=StaticCargoSet end local mission=AUFTRAG:New(AUFTRAG.Type.FREIGHTTRANSPORT) local Ncargo=StaticCargo:Count() if Ncargo==0 then mission:E(mission.lid..string.format("ERROR: No cargo items in set!")) return nil else mission:T(mission.lid..string.format("FREIGHTTRANSPORT with N=%d cargo items in set",Ncargo)) end mission:_TargetFromObject(StaticCargo) mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.FREIGHTTRANSPORT) mission.optionROE=ENUMS.ROE.ReturnFire mission.optionROT=ENUMS.ROT.PassiveDefense mission.categories={AUFTRAG.Category.HELICOPTER,AUFTRAG.Category.AIRCRAFT} mission.DCStask=mission:GetDCSMissionTask() mission.DCStask.params.cargo=StaticCargo mission.DCStask.params.destination=Destination return mission end function AUFTRAG:NewARTY(Target,Nshots,Radius,Altitude) local mission=AUFTRAG:New(AUFTRAG.Type.ARTY) mission:_TargetFromObject(Target) mission.artyShots=Nshots or nil mission.artyRadius=Radius or 100 mission.artyAltitude=Altitude mission.engageWeaponType=ENUMS.WeaponFlag.Auto mission.optionROE=ENUMS.ROE.OpenFire mission.optionAlarm=0 mission.missionFraction=0.0 mission.missionWaypointRadius=0.0 mission.dTevaluate=8*60 mission.categories={AUFTRAG.Category.GROUND,AUFTRAG.Category.NAVAL} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewBARRAGE(Zone,Heading,Angle,Radius,Altitude,Nshots) local mission=AUFTRAG:New(AUFTRAG.Type.BARRAGE) mission:_TargetFromObject(Zone) mission.artyShots=Nshots mission.artyRadius=Radius or 100 mission.artyAltitude=Altitude mission.artyHeading=Heading mission.artyAngle=Angle mission.engageWeaponType=ENUMS.WeaponFlag.Auto mission.optionROE=ENUMS.ROE.OpenFire mission.optionAlarm=0 mission.missionFraction=0.0 mission.dTevaluate=10 mission.categories={AUFTRAG.Category.GROUND,AUFTRAG.Category.NAVAL} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewPATROLZONE(Zone,Speed,Altitude,Formation) local mission=AUFTRAG:New(AUFTRAG.Type.PATROLZONE) if type(Zone)=="string"then Zone=ZONE:New(Zone) end mission:_TargetFromObject(Zone) mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.PATROLZONE) mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.PassiveDefense mission.optionAlarm=ENUMS.AlarmState.Auto mission.missionFraction=1.0 mission.missionSpeed=Speed and UTILS.KnotsToKmph(Speed)or nil mission.missionAltitude=Altitude and UTILS.FeetToMeters(Altitude)or nil mission.categories={AUFTRAG.Category.ALL} mission.DCStask=mission:GetDCSMissionTask() mission.DCStask.params.formation=Formation or"Off Road" return mission end function AUFTRAG:NewCAPTUREZONE(OpsZone,Coalition,Speed,Altitude,Formation,StayInZoneTime) local mission=AUFTRAG:New(AUFTRAG.Type.CAPTUREZONE) mission:_TargetFromObject(OpsZone) mission.coalition=Coalition mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.CAPTUREZONE) mission.optionROE=ENUMS.ROE.ReturnFire mission.optionROT=ENUMS.ROT.PassiveDefense mission.optionAlarm=ENUMS.AlarmState.Auto mission.StayInZoneTime=StayInZoneTime mission.missionFraction=0.1 mission.missionSpeed=Speed and UTILS.KnotsToKmph(Speed)or nil mission.missionAltitude=Altitude and UTILS.FeetToMeters(Altitude)or nil mission.categories={AUFTRAG.Category.ALL} mission.DCStask=mission:GetDCSMissionTask() mission.updateDCSTask=true local params={} params.formation=Formation or"Off Road" params.zone=mission:GetObjective() params.altitude=mission.missionAltitude params.speed=mission.missionSpeed and UTILS.KmphToMps(mission.missionSpeed)or nil mission.DCStask.params=params return mission end function AUFTRAG:NewARMORATTACK(Target,Speed,Formation) local mission=AUFTRAG:NewGROUNDATTACK(Target,Speed,Formation) mission.type=AUFTRAG.Type.ARMORATTACK return mission end function AUFTRAG:NewGROUNDATTACK(Target,Speed,Formation) local mission=AUFTRAG:New(AUFTRAG.Type.GROUNDATTACK) mission:_TargetFromObject(Target) mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.GROUNDATTACK) mission.optionROE=ENUMS.ROE.OpenFire mission.optionAlarm=ENUMS.AlarmState.Auto mission.optionFormation="On Road" mission.missionFraction=0.70 mission.missionSpeed=Speed and UTILS.KnotsToKmph(Speed)or nil mission.categories={AUFTRAG.Category.GROUND} mission.DCStask=mission:GetDCSMissionTask() mission.DCStask.params.speed=mission.missionSpeed and UTILS.KmphToMps(mission.missionSpeed)or nil mission.DCStask.params.formation=Formation or ENUMS.Formation.Vehicle.Vee return mission end function AUFTRAG:NewNAVALENGAGEMENT(Target,Speed,Depth) local mission=AUFTRAG:New(AUFTRAG.Type.NAVALENGAGEMENT) mission:_TargetFromObject(Target) mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.NAVALENGAGEMENT) mission.optionROE=ENUMS.ROE.OpenFire mission.optionAlarm=ENUMS.AlarmState.Auto mission.missionFraction=0.70 mission.missionSpeed=Speed and UTILS.KnotsToKmph(Speed)or nil mission.missionAltitude=Depth or 0 mission.categories={AUFTRAG.Category.NAVAL} mission.DCStask=mission:GetDCSMissionTask() mission.DCStask.params.speed=mission.missionSpeed and UTILS.KmphToMps(mission.missionSpeed)or nil return mission end function AUFTRAG:NewRECON(ZoneSet,Speed,Altitude,Adinfinitum,Randomly,Formation) local mission=AUFTRAG:New(AUFTRAG.Type.RECON) mission:_TargetFromObject(ZoneSet) if ZoneSet:IsInstanceOf("SET_ZONE")then mission.missionZoneSet=ZoneSet elseif ZoneSet:IsInstanceOf("ZONE_BASE")then mission.missionZoneSet=SET_ZONE:New() mission.missionZoneSet:AddZone(ZoneSet) end mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.RECON) mission.optionROE=ENUMS.ROE.WeaponHold mission.optionROT=ENUMS.ROT.PassiveDefense mission.optionAlarm=ENUMS.AlarmState.Auto mission.missionFraction=0.5 mission.missionSpeed=Speed and UTILS.KnotsToKmph(Speed)or nil mission.missionAltitude=Altitude and UTILS.FeetToMeters(Altitude)or UTILS.FeetToMeters(2000) mission.categories={AUFTRAG.Category.ALL} mission.DCStask=mission:GetDCSMissionTask() mission.DCStask.params.adinfinitum=Adinfinitum mission.DCStask.params.randomly=Randomly mission.DCStask.params.formation=Formation return mission end function AUFTRAG:NewAMMOSUPPLY(Zone) local mission=AUFTRAG:New(AUFTRAG.Type.AMMOSUPPLY) mission:_TargetFromObject(Zone) mission.optionROE=ENUMS.ROE.WeaponHold mission.optionAlarm=ENUMS.AlarmState.Auto mission.missionFraction=1.0 mission.missionWaypointRadius=0 mission.categories={AUFTRAG.Category.GROUND} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewFUELSUPPLY(Zone) local mission=AUFTRAG:New(AUFTRAG.Type.FUELSUPPLY) mission:_TargetFromObject(Zone) mission.optionROE=ENUMS.ROE.WeaponHold mission.optionAlarm=ENUMS.AlarmState.Auto mission.missionFraction=1.0 mission.categories={AUFTRAG.Category.GROUND} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewREARMING(Zone) local mission=AUFTRAG:New(AUFTRAG.Type.REARMING) mission:_TargetFromObject(Zone) mission.optionROE=ENUMS.ROE.WeaponHold mission.optionAlarm=ENUMS.AlarmState.Auto mission.missionFraction=1.0 mission.missionWaypointRadius=0 mission.categories={AUFTRAG.Category.GROUND} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewALERT5(MissionType) local mission=AUFTRAG:New(AUFTRAG.Type.ALERT5) mission.missionTask=self:GetMissionTaskforMissionType(MissionType) mission.optionROE=ENUMS.ROE.WeaponHold mission.optionROT=ENUMS.ROT.NoReaction mission.alert5MissionType=MissionType mission.missionFraction=1.0 mission.categories={AUFTRAG.Category.AIRCRAFT} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewONGUARD(Coordinate) local mission=AUFTRAG:New(AUFTRAG.Type.ONGUARD) mission:_TargetFromObject(Coordinate) mission.optionROE=ENUMS.ROE.OpenFire mission.optionAlarm=ENUMS.AlarmState.Auto mission.missionFraction=1.0 mission.categories={AUFTRAG.Category.GROUND,AUFTRAG.Category.NAVAL} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewAIRDEFENSE(Zone) local mission=AUFTRAG:New(AUFTRAG.Type.AIRDEFENSE) mission:_TargetFromObject(Zone) mission.optionROE=ENUMS.ROE.OpenFire mission.optionAlarm=ENUMS.AlarmState.Auto mission.missionFraction=1.0 mission.categories={AUFTRAG.Category.GROUND,AUFTRAG.Category.NAVAL} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewEWR(Zone) local mission=AUFTRAG:New(AUFTRAG.Type.EWR) mission:_TargetFromObject(Zone) mission.optionROE=ENUMS.ROE.WeaponHold mission.optionAlarm=ENUMS.AlarmState.Auto mission.missionFraction=1.0 mission.categories={AUFTRAG.Category.GROUND} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:_NewRELOCATECOHORT(Legion,Cohort) local mission=AUFTRAG:New(AUFTRAG.Type.RELOCATECOHORT) mission:_TargetFromObject(Legion.spawnzone) mission.optionROE=ENUMS.ROE.ReturnFire mission.optionAlarm=ENUMS.AlarmState.Auto mission.missionFraction=0.0 mission.categories={AUFTRAG.Category.ALL} mission.DCStask=mission:GetDCSMissionTask() if Cohort.isGround then mission.optionFormation=ENUMS.Formation.Vehicle.OnRoad end mission.DCStask.params.legion=Legion mission.DCStask.params.cohort=Cohort return mission end function AUFTRAG:NewNOTHING(RelaxZone) local mission=AUFTRAG:New(AUFTRAG.Type.NOTHING) mission:_TargetFromObject(RelaxZone) mission.optionROE=ENUMS.ROE.WeaponHold mission.optionAlarm=ENUMS.AlarmState.Auto mission.missionFraction=1.0 mission.categories={AUFTRAG.Category.GROUND,AUFTRAG.Category.NAVAL} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewARMOREDGUARD(Coordinate,Formation) local mission=AUFTRAG:New(AUFTRAG.Type.ARMOREDGUARD) mission:_TargetFromObject(Coordinate) mission.optionROE=ENUMS.ROE.OpenFire mission.optionAlarm=ENUMS.AlarmState.Auto mission.optionFormation=Formation or"On Road" mission.missionFraction=1.0 mission.categories={AUFTRAG.Category.GROUND} mission.DCStask=mission:GetDCSMissionTask() return mission end function AUFTRAG:NewFromTarget(Target,MissionType) local mission=nil if MissionType==AUFTRAG.Type.ANTISHIP then mission=self:NewANTISHIP(Target) elseif MissionType==AUFTRAG.Type.ARTY then mission=self:NewARTY(Target,0.3) elseif MissionType==AUFTRAG.Type.BAI then mission=self:NewBAI(Target) elseif MissionType==AUFTRAG.Type.BOMBCARPET then mission=self:NewBOMBCARPET(Target) elseif MissionType==AUFTRAG.Type.BOMBING then mission=self:NewBOMBING(Target) elseif MissionType==AUFTRAG.Type.BOMBRUNWAY then mission=self:NewBOMBRUNWAY(Target) elseif MissionType==AUFTRAG.Type.STRAFING then mission=self:NewSTRAFING(Target) elseif MissionType==AUFTRAG.Type.CAS then mission=self:NewCAS(ZONE_RADIUS:New(Target:GetName(),Target:GetVec2(),1000),nil,nil,Target:GetAverageCoordinate()) elseif MissionType==AUFTRAG.Type.CASENHANCED then mission=self:NewCASENHANCED(ZONE_RADIUS:New(Target:GetName(),Target:GetVec2(),1000)) elseif MissionType==AUFTRAG.Type.INTERCEPT then mission=self:NewINTERCEPT(Target) elseif MissionType==AUFTRAG.Type.SEAD then mission=self:NewSEAD(Target) elseif MissionType==AUFTRAG.Type.STRIKE then mission=self:NewSTRIKE(Target) elseif MissionType==AUFTRAG.Type.ARMORATTACK then mission=self:NewARMORATTACK(Target) elseif MissionType==AUFTRAG.Type.GROUNDATTACK then mission=self:NewGROUNDATTACK(Target) elseif MissionType==AUFTRAG.Type.NAVALENGAGEMENT then mission=self:NewNAVALENGAGEMENT(Target) else return nil end return mission end function AUFTRAG:_DetermineAuftragType(Target) local group=nil local airbase=nil local scenery=nil local coordinate=nil local auftrag=nil if Target:IsInstanceOf("GROUP")then group=Target elseif Target:IsInstanceOf("UNIT")then group=Target:GetGroup() elseif Target:IsInstanceOf("AIRBASE")then airbase=Target elseif Target:IsInstanceOf("SCENERY")then scenery=Target end if group then local category=group:GetCategory() local attribute=group:GetAttribute() if category==Group.Category.AIRPLANE or category==Group.Category.HELICOPTER then auftrag=AUFTRAG.Type.INTERCEPT elseif category==Group.Category.GROUND or category==Group.Category.TRAIN then if attribute==GROUP.Attribute.GROUND_SAM then auftrag=AUFTRAG.Type.SEAD elseif attribute==GROUP.Attribute.GROUND_AAA then auftrag=AUFTRAG.Type.BAI elseif attribute==GROUP.Attribute.GROUND_ARTILLERY then auftrag=AUFTRAG.Type.BAI elseif attribute==GROUP.Attribute.GROUND_INFANTRY then auftrag=AUFTRAG.Type.CAS elseif attribute==GROUP.Attribute.GROUND_TANK then auftrag=AUFTRAG.Type.BAI else auftrag=AUFTRAG.Type.BAI end elseif category==Group.Category.SHIP then auftrag=AUFTRAG.Type.ANTISHIP else self:T(self.lid.."ERROR: Unknown Group category!") end elseif airbase then auftrag=AUFTRAG.Type.BOMBRUNWAY elseif scenery then auftrag=AUFTRAG.Type.STRIKE elseif coordinate then auftrag=AUFTRAG.Type.BOMBING end return auftrag end function AUFTRAG:NewAUTO(EngageGroup) local mission=nil local Target=EngageGroup local auftrag=self:_DetermineAuftragType(EngageGroup) if auftrag==AUFTRAG.Type.ANTISHIP then mission=AUFTRAG:NewANTISHIP(Target) elseif auftrag==AUFTRAG.Type.ARTY then mission=AUFTRAG:NewARTY(Target,0.2) elseif auftrag==AUFTRAG.Type.AWACS then mission=AUFTRAG:NewAWACS(Coordinate,Altitude,Speed,Heading,Leg) elseif auftrag==AUFTRAG.Type.BAI then mission=AUFTRAG:NewBAI(Target,Altitude) elseif auftrag==AUFTRAG.Type.BOMBING then mission=AUFTRAG:NewBOMBING(Target,Altitude) elseif auftrag==AUFTRAG.Type.BOMBRUNWAY then mission=AUFTRAG:NewBOMBRUNWAY(Airdrome,Altitude) elseif auftrag==AUFTRAG.Type.BOMBCARPET then mission=AUFTRAG:NewBOMBCARPET(Target,Altitude,CarpetLength) elseif auftrag==AUFTRAG.Type.CAP then mission=AUFTRAG:NewCAP(ZoneCAP,Altitude,Speed,Coordinate,Heading,Leg,TargetTypes) elseif auftrag==AUFTRAG.Type.CAS then mission=AUFTRAG:NewCAS(ZoneCAS,Altitude,Speed,Coordinate,Heading,Leg,TargetTypes) elseif auftrag==AUFTRAG.Type.ESCORT then mission=AUFTRAG:NewESCORT(EscortGroup,OffsetVector,EngageMaxDistance,TargetTypes) elseif auftrag==AUFTRAG.Type.FACA then mission=AUFTRAG:NewFACA(Target,Designation,DataLink,Frequency,Modulation) elseif auftrag==AUFTRAG.Type.FERRY then elseif auftrag==AUFTRAG.Type.GCICAP then mission=AUFTRAG:NewGCICAP(Coordinate,Altitude,Speed,Heading,Leg) elseif auftrag==AUFTRAG.Type.INTERCEPT then mission=AUFTRAG:NewINTERCEPT(Target) elseif auftrag==AUFTRAG.Type.ORBIT then mission=AUFTRAG:NewORBIT(Coordinate,Altitude,Speed,Heading,Leg) elseif auftrag==AUFTRAG.Type.RECON then mission=AUFTRAG:NewRECON(ZoneSet,Speed,Altitude,Adinfinitum,Randomly,Formation) elseif auftrag==AUFTRAG.Type.RESCUEHELO then mission=AUFTRAG:NewRESCUEHELO(Carrier) elseif auftrag==AUFTRAG.Type.SEAD then mission=AUFTRAG:NewSEAD(Target,Altitude) elseif auftrag==AUFTRAG.Type.STRIKE then mission=AUFTRAG:NewSTRIKE(Target,Altitude) elseif auftrag==AUFTRAG.Type.TANKER then mission=AUFTRAG:NewTANKER(Coordinate,Altitude,Speed,Heading,Leg,RefuelSystem) elseif auftrag==AUFTRAG.Type.TROOPTRANSPORT then mission=AUFTRAG:NewTROOPTRANSPORT(TransportGroupSet,DropoffCoordinate,PickupCoordinate) elseif auftrag==AUFTRAG.Type.PATROLRACETRACK then mission=AUFTRAG:NewPATROL_RACETRACK(Coordinate,Altitude,Speed,Heading,Leg,Formation) else end if mission then mission:SetPriority(10,true) end return mission end function AUFTRAG:SetTime(ClockStart,ClockStop) local Tnow=timer.getAbsTime() local Tstart=Tnow+5 if ClockStart and type(ClockStart)=="number"then Tstart=Tnow+ClockStart elseif ClockStart and type(ClockStart)=="string"then Tstart=UTILS.ClockToSeconds(ClockStart) end local Tstop=nil if ClockStop and type(ClockStop)=="number"then Tstop=Tnow+ClockStop elseif ClockStop and type(ClockStop)=="string"then Tstop=UTILS.ClockToSeconds(ClockStop) end self.Tstart=Tstart self.Tstop=Tstop if Tstop then self.duration=self.Tstop-self.Tstart end return self end function AUFTRAG:SetDuration(Duration) self.durationExe=Duration return self end function AUFTRAG:SetTeleport(Switch) if Switch==nil then Switch=true end self.teleport=Switch return self end function AUFTRAG:SetReturnToLegion(Switch) self.legionReturn=Switch self:T(self.lid..string.format("Setting ReturnToLetion=%s",tostring(self.legionReturn))) return self end function AUFTRAG:SetPushTime(ClockPush) if ClockPush then if type(ClockPush)=="string"then self.Tpush=UTILS.ClockToSeconds(ClockPush) elseif type(ClockPush)=="number"then self.Tpush=timer.getAbsTime()+ClockPush end end return self end function AUFTRAG:SetPriority(Prio,Urgent,Importance) self.prio=Prio or 50 self.urgent=Urgent self.importance=Importance return self end function AUFTRAG:SetRepeat(Nrepeat) self.Nrepeat=Nrepeat or 0 return self end function AUFTRAG:SetRepeatDelay(RepeatDelay) self.repeatDelay=RepeatDelay return self end function AUFTRAG:SetRepeatOnFailure(Nrepeat) self.NrepeatFailure=Nrepeat or 0 return self end function AUFTRAG:SetRepeatOnSuccess(Nrepeat) self.NrepeatSuccess=Nrepeat or 0 return self end function AUFTRAG:SetReinforce(Nreinforce) self.reinforce=Nreinforce return self end function AUFTRAG:SetRequiredAssets(NassetsMin,NassetsMax) self.NassetsMin=NassetsMin or 1 self.NassetsMax=NassetsMax or self.NassetsMin if self.NassetsMax0 then local N=self:CountOpsGroups() if N Nmin=%d",self.NassetsMin,N,self.reinforce,Nmin)) end end end return Nmin,Nmax end function AUFTRAG:SetAssetsStayAlive(Switch) if Switch==nil then Switch=true end self.assetStayAlive=Switch return self end function AUFTRAG:SetRequiredEscorts(NescortMin,NescortMax,MissionType,TargetTypes,EngageRange) self.NescortMin=NescortMin or 1 self.NescortMax=NescortMax or self.NescortMin if self.NescortMaxself.Tstop then return false end local startme=self:EvalConditionsAll(self.conditionStart) if not startme then return false end if self.type==AUFTRAG.Type.FREIGHTTRANSPORT then local cargoset=self.DCStask.params.cargo for _,_opsgroup in pairs(self:GetOpsGroups())do local opsgroup=_opsgroup local vec2=opsgroup.group:GetFirstUnitAlive():GetVec2() local zone=ZONE_RADIUS:New("Freighttransport",vec2,40,true) local inzone=cargoset:IsInZone(zone) if not inzone then self:T(self.lid.."FREIGHTTRANSPORT: cargo is not inside zone ==> mission not ready to start yet!") return false end end end return true end function AUFTRAG:IsReadyToCancel() local Tnow=timer.getAbsTime() if self.Tstop and Tnow>=self.Tstop then return true end local failure=self:EvalConditionsAny(self.conditionFailure) if failure then self.failurecondition=true return true end local success=self:EvalConditionsAny(self.conditionSuccess) if success then self.successcondition=true return true end return false end function AUFTRAG:IsReadyToPush() local Tnow=timer.getAbsTime() if self.Tpush and Tnow<=self.Tpush then return false end local push=self:EvalConditionsAll(self.conditionPush) return push end function AUFTRAG:EvalConditionsAll(Conditions) for _,_condition in pairs(Conditions or{})do local condition=_condition local istrue=condition.func(unpack(condition.arg)) if not istrue then return false end end return true end function AUFTRAG:EvalConditionsAny(Conditions) for _,_condition in pairs(Conditions or{})do local condition=_condition local istrue=condition.func(unpack(condition.arg)) if istrue then return true end end return false end function AUFTRAG:onafterStatus(From,Event,To) local Tnow=timer.getAbsTime() if self.escortGroupName then local group=GROUP:FindByName(self.escortGroupName) if group and group:IsAlive()then self:T(self.lid..string.format("ESCORT group %s is now alive. Updating DCS task and adding group to TARGET",tostring(self.escortGroupName))) self.engageTarget:AddObject(group) self.DCStask=self:GetDCSMissionTask() self.escortGroupName=nil end end local Ntargets=self:CountMissionTargets() local Ntargets0=self:GetTargetInitialNumber() local Ngroups=self:CountOpsGroups() local Nassigned=self.Nassigned and self.Nassigned-self.Ndead or 0 local conditionDone=false if self.conditionFailureSet then conditionDone=self:EvalConditionsAny(self.conditionFailure) end if self.conditionSuccessSet and not conditionDone then conditionDone=self:EvalConditionsAny(self.conditionSuccess) end if self:IsNotOver()then if self:CheckGroupsDone()then self:Done() elseif(self.Tstop and Tnow>self.Tstop+10)then self:Cancel() elseif conditionDone then self:Cancel() elseif self.durationExe and self.Texecuting and Tnow-self.Texecuting>self.durationExe then local Nrepeat=self.Nrepeat local NrepeatS=self.NrepeatSuccess local NrepeatF=self.NrepeatFailure self:Cancel() self.Nrepeat=Nrepeat self.NrepeatSuccess=NrepeatS self.NrepeatFailure=NrepeatF elseif(Ntargets0>0 and Ntargets==0)then self:T(self.lid.."No targets left cancelling mission!") self:Cancel() elseif self:IsExecuting()and self:_IsNotReinforcing()then if Ngroups==0 then self:Done() else local done=true for groupname,data in pairs(self.groupdata or{})do local groupdata=data local opsgroup=groupdata.opsgroup if opsgroup:IsAlive()then done=false end end if done then self:Done() end end end end local fsmstate=self:GetState() if fsmstate~=self.status then self:T(self.lid..string.format("ERROR: FSM state %s != %s mission status!",fsmstate,self.status)) end if self.verbose>=1 then local Cstart=UTILS.SecondsToClock(self.Tstart,true) local Cstop=self.Tstop and UTILS.SecondsToClock(self.Tstop,true)or"INF" local targetname=self:GetTargetName()or"unknown" local Nlegions=#self.legions local commander=self.commander and self.statusCommander or"N/A" local chief=self.chief and self.statusChief or"N/A" self:T(self.lid..string.format("Status %s: Target=%s, T=%s-%s, assets=%d, groups=%d, targets=%d, legions=%d, commander=%s, chief=%s", self.status,targetname,Cstart,Cstop,#self.assets,Ngroups,Ntargets,Nlegions,commander,chief)) end if self.verbose>=2 then local text="Group data:" for groupname,_groupdata in pairs(self.groupdata)do local groupdata=_groupdata text=text..string.format("\n- %s: status mission=%s opsgroup=%s",groupname,groupdata.status,groupdata.opsgroup and groupdata.opsgroup:GetState()or"N/A") end self:I(self.lid..text) end if self.verbose>=3 then local text=string.format("Assets [N=%d, Nassigned=%s, Ndead=%s]:",self.Nassets or 0,self.Nassigned or 0,self.Ndead or 0) for i,_asset in pairs(self.assets or{})do local asset=_asset text=text..string.format("\n[%d] %s: spawned=%s, requested=%s, reserved=%s",i,asset.spawngroupname,tostring(asset.spawned),tostring(asset.requested),tostring(asset.reserved)) end self:I(self.lid..text) end local ready2evaluate=self.Tover and Tnow-self.Tover>=self.dTevaluate or false if self:IsOver()and ready2evaluate then self:Evaluate() else self:__Status(-30) end if self.markerOn then self:UpdateMarker() end end function AUFTRAG:Evaluate() local failed=false local targetdamage=self:GetTargetDamage() local owndamage=self.Ncasualties/self.Nelements*100 local Ntargets=self:CountMissionTargets(true) local Ntargets0=self:GetTargetInitialNumber() local Life=self:GetTargetLife() local Life0=self:GetTargetInitialLife() if Ntargets0>0 then if self.type==AUFTRAG.Type.TROOPTRANSPORT or self.type==AUFTRAG.Type.ESCORT then if Ntargets0 then failed=true end end else if self.Nelements==self.Ncasualties then failed=true end end local successCondition=self:EvalConditionsAny(self.conditionSuccess) local failureCondition=self:EvalConditionsAny(self.conditionFailure) if failureCondition then failed=true elseif successCondition then failed=false end if self.verbose>0 then local text=string.format("Evaluating mission:\n") text=text..string.format("Own casualties = %d/%d\n",self.Ncasualties,self.Nelements) text=text..string.format("Own losses = %.1f %%\n",owndamage) text=text..string.format("Killed units = %d\n",self.Nkills) text=text..string.format("--------------------------\n") text=text..string.format("Targets left = %d/%d\n",Ntargets,Ntargets0) text=text..string.format("Targets life = %.1f/%.1f\n",Life,Life0) text=text..string.format("Enemy losses = %.1f %%\n",targetdamage) text=text..string.format("--------------------------\n") text=text..string.format("Success Cond = %s\n",tostring(successCondition)) text=text..string.format("Failure Cond = %s\n",tostring(failureCondition)) text=text..string.format("--------------------------\n") text=text..string.format("Final Success = %s\n",tostring(not failed)) text=text..string.format("=========================") self:I(self.lid..text) end if failed then self:I(self.lid..string.format("Mission %d [%s] failed!",self.auftragsnummer,self.type)) if self.chief then self.chief.Nfailure=self.chief.Nfailure+1 end self:Failed() else self:I(self.lid..string.format("Mission %d [%s] success!",self.auftragsnummer,self.type)) if self.chief then self.chief.Nsuccess=self.chief.Nsuccess+1 end self:Success() end return self end function AUFTRAG:GetOpsGroups() local opsgroups={} for _,_groupdata in pairs(self.groupdata or{})do local groupdata=_groupdata table.insert(opsgroups,groupdata.opsgroup) end return opsgroups end function AUFTRAG:GetAssetDataByName(AssetName) return self.groupdata[tostring(AssetName)] end function AUFTRAG:GetGroupData(opsgroup) if opsgroup and self.groupdata then return self.groupdata[opsgroup.groupname] end return nil end function AUFTRAG:SetGroupStatus(opsgroup,status) local oldstatus=self:GetGroupStatus(opsgroup) self:T(self.lid..string.format("Setting OPSGROUP %s to status %s-->%s",opsgroup and opsgroup.groupname or"nil",tostring(oldstatus),tostring(status))) if oldstatus==AUFTRAG.GroupStatus.CANCELLED and status==AUFTRAG.GroupStatus.DONE then else local groupdata=self:GetGroupData(opsgroup) if groupdata then groupdata.status=status else self:T(self.lid.."WARNING: Could not SET flight data for flight group. Setting status to DONE") end end local isNotOver=self:IsNotOver() local groupsDone=self:CheckGroupsDone() self:T2(self.lid..string.format("Setting OPSGROUP %s status to %s. IsNotOver=%s CheckGroupsDone=%s",opsgroup.groupname,self:GetGroupStatus(opsgroup),tostring(self:IsNotOver()),tostring(groupsDone))) if isNotOver and groupsDone then self:T3(self.lid.."All assigned OPSGROUPs done ==> mission DONE!") self:Done() else self:T3(self.lid.."Mission NOT DONE yet!") end return self end function AUFTRAG:GetGroupStatus(opsgroup) self:T3(self.lid..string.format("Trying to get Flight status for flight group %s",opsgroup and opsgroup.groupname or"nil")) local groupdata=self:GetGroupData(opsgroup) if groupdata then return groupdata.status else self:T(self.lid..string.format("WARNING: Could not GET groupdata for opsgroup %s. Returning status DONE.",opsgroup and opsgroup.groupname or"nil")) return AUFTRAG.GroupStatus.DONE end end function AUFTRAG:AddLegion(Legion) self:T(self.lid..string.format("Adding legion %s",Legion.alias)) table.insert(self.legions,Legion) return self end function AUFTRAG:RemoveLegion(Legion) for i=#self.legions,1,-1 do local legion=self.legions[i] if legion.alias==Legion.alias then self:T(self.lid..string.format("Removing legion %s",Legion.alias)) table.remove(self.legions,i) self.statusLegion[Legion.alias]=nil return self end end self:T(self.lid..string.format("ERROR: Legion %s not found and could not be removed!",Legion.alias)) return self end function AUFTRAG:SetLegionStatus(Legion,Status) local status=self:GetLegionStatus(Legion) self:T(self.lid..string.format("Setting LEGION %s to status %s-->%s",Legion.alias,tostring(status),tostring(Status))) self.statusLegion[Legion.alias]=Status return self end function AUFTRAG:GetLegionStatus(Legion) local status=self.statusLegion[Legion.alias]or"unknown" return status end function AUFTRAG:SetGroupWaypointCoordinate(opsgroup,coordinate) local groupdata=self:GetGroupData(opsgroup) if groupdata then groupdata.waypointcoordinate=coordinate end return self end function AUFTRAG:SetIngressCoordinate(coordinate) self.missionIngressCoord=coordinate self.missionIngressCoordAlt=UTILS.MetersToFeet(coordinate.y)or 10000 return self end function AUFTRAG:GetGroupWaypointCoordinate(opsgroup) local groupdata=self:GetGroupData(opsgroup) if groupdata then return groupdata.waypointcoordinate end end function AUFTRAG:SetGroupWaypointTask(opsgroup,task) self:T2(self.lid..string.format("Setting waypoint task %s",task and task.description or"WTF")) local groupdata=self:GetGroupData(opsgroup) if groupdata then groupdata.waypointtask=task end end function AUFTRAG:GetGroupWaypointTask(opsgroup) local groupdata=self:GetGroupData(opsgroup) if groupdata then return groupdata.waypointtask end end function AUFTRAG:SetGroupWaypointIndex(opsgroup,waypointindex) self:T2(self.lid..string.format("Setting Mission waypoint UID=%d",waypointindex)) local groupdata=self:GetGroupData(opsgroup) if groupdata then groupdata.waypointindex=waypointindex end return self end function AUFTRAG:GetGroupWaypointIndex(opsgroup) local groupdata=self:GetGroupData(opsgroup) if groupdata then return groupdata.waypointindex end end function AUFTRAG:SetGroupEgressWaypointUID(opsgroup,waypointindex) self:T2(self.lid..string.format("Setting Egress waypoint UID=%d",waypointindex)) local groupdata=self:GetGroupData(opsgroup) if groupdata then groupdata.waypointEgressUID=waypointindex end return self end function AUFTRAG:GetGroupEgressWaypointUID(opsgroup) local groupdata=self:GetGroupData(opsgroup) if groupdata then return groupdata.waypointEgressUID end end function AUFTRAG:CheckGroupsDone() local fsmState=self:GetState() for groupname,data in pairs(self.groupdata)do local groupdata=data if groupdata then if not(groupdata.status==AUFTRAG.GroupStatus.DONE or groupdata.status==AUFTRAG.GroupStatus.CANCELLED)then self:T2(self.lid..string.format("CheckGroupsDone: OPSGROUP %s is not DONE or CANCELLED but in state %s. Mission NOT DONE!",groupdata.opsgroup.groupname,groupdata.status:upper())) return false end end end for _,_legion in pairs(self.legions)do local legion=_legion local status=self:GetLegionStatus(legion) if not status==AUFTRAG.Status.CANCELLED then self:T2(self.lid..string.format("CheckGroupsDone: LEGION %s is not CANCELLED but in state %s. Mission NOT DONE!",legion.alias,status)) return false end end if self.commander then if not self.statusCommander==AUFTRAG.Status.CANCELLED then self:T2(self.lid..string.format("CheckGroupsDone: COMMANDER is not CANCELLED but in state %s. Mission NOT DONE!",self.statusCommander)) return false end end if self.chief then if not self.statusChief==AUFTRAG.Status.CANCELLED then self:T2(self.lid..string.format("CheckGroupsDone: CHIEF is not CANCELLED but in state %s. Mission NOT DONE!",self.statusChief)) return false end end if self:IsPlanned()or self:IsQueued()or self:IsRequested()then self:T2(self.lid..string.format("CheckGroupsDone: Mission is still in state %s [FSM=%s] (PLANNED or QUEUED or REQUESTED). Mission NOT DONE!",self.status,self:GetState())) return false end if self:IsExecuting()and self:_IsReinforcing()then self:T2(self.lid..string.format("CheckGroupsDone: Mission is still in state %s [FSM=%s] and reinfoce=%d. Mission NOT DONE!",self.status,self:GetState(),self.reinforce)) return false end local NopsgroupsAlive=self:CountOpsGroups() local NopsgroupsDone=self:CountOpsGroupsInStatus(AUFTRAG.GroupStatus.DONE)+self:CountOpsGroupsInStatus(AUFTRAG.GroupStatus.CANCELLED) if self:IsStarted()and NopsgroupsAlive==0 then self:T(self.lid..string.format("CheckGroupsDone: Mission is STARTED state %s [FSM=%s] but count of alive OPSGROUP is zero. Mission DONE!",self.status,self:GetState())) return true end if NopsgroupsAlive==NopsgroupsDone then self:T(self.lid..string.format("CheckGroupsDone: Mission is in state %s [FSM=%s] but all groups [=%d] are done or cancelled. Mission DONE!",self.status,self:GetState(),NopsgroupsAlive)) return true end if(self:IsStarted()or self:IsExecuting())and(fsmState==AUFTRAG.Status.STARTED or fsmState==AUFTRAG.Status.EXECUTING)and NopsgroupsAlive>0 then self:T(self.lid..string.format("CheckGroupsDone: Mission is in state %s [FSM=%s] and count of alive OPSGROUP > zero. Mission NOT DONE!",self.status,self:GetState())) return false end return true end function AUFTRAG:OnEventUnitLost(EventData) if EventData and EventData.IniGroup and EventData.IniUnit then local unit=EventData.IniUnit local group=EventData.IniGroup local unitname=EventData.IniUnitName for _,_groupdata in pairs(self.groupdata)do local groupdata=_groupdata if groupdata and groupdata.opsgroup and groupdata.opsgroup.groupname==EventData.IniGroupName then self:T(self.lid..string.format("UNIT LOST event for opsgroup %s unit %s",groupdata.opsgroup.groupname,EventData.IniUnitName)) end end end end function AUFTRAG:onafterPlanned(From,Event,To) self.status=AUFTRAG.Status.PLANNED self:T(self.lid..string.format("New mission status=%s",self.status)) end function AUFTRAG:onafterQueued(From,Event,To,Airwing) self.status=AUFTRAG.Status.QUEUED self:T(self.lid..string.format("New mission status=%s",self.status)) end function AUFTRAG:onafterRequested(From,Event,To) self.status=AUFTRAG.Status.REQUESTED self:T(self.lid..string.format("New mission status=%s",self.status)) end function AUFTRAG:onafterAssign(From,Event,To) self.status=AUFTRAG.Status.ASSIGNED self:T(self.lid..string.format("New mission status=%s",self.status)) end function AUFTRAG:onafterScheduled(From,Event,To) self.status=AUFTRAG.Status.SCHEDULED self:T(self.lid..string.format("New mission status=%s",self.status)) end function AUFTRAG:onafterStarted(From,Event,To) self.status=AUFTRAG.Status.STARTED self.Tstarted=timer.getAbsTime() self:T(self.lid..string.format("New mission status=%s",self.status)) end function AUFTRAG:onafterExecuting(From,Event,To) self.status=AUFTRAG.Status.EXECUTING self.Texecuting=timer.getAbsTime() self:T(self.lid..string.format("New mission status=%s",self.status)) end function AUFTRAG:onafterElementDestroyed(From,Event,To,OpsGroup,Element) self.Ncasualties=self.Ncasualties+1 end function AUFTRAG:onafterGroupDead(From,Event,To,OpsGroup) local asset=self:GetAssetByName(OpsGroup.groupname) if asset then self:AssetDead(asset) end self.Ndead=self.Ndead+1 end function AUFTRAG:onafterAssetDead(From,Event,To,Asset) local N=self:CountOpsGroups() local notreinforcing=self:_IsNotReinforcing() self:T(self.lid..string.format("Asset %s dead! Number of ops groups remaining %d (reinforcing=%s)",tostring(Asset.spawngroupname),N,tostring(not notreinforcing))) if N==0 and notreinforcing then if self:IsNotOver()then self:Cancel() else end end self:DelAsset(Asset) end function AUFTRAG:onafterCancel(From,Event,To) local Ngroups=self:CountOpsGroups() self:T(self.lid..string.format("CANCELLING mission in status %s. Will wait for %d groups to report mission DONE before evaluation",self.status,Ngroups)) self.Tover=timer.getAbsTime() self.Nrepeat=self.repeated self.NrepeatFailure=self.repeatedFailure self.NrepeatSuccess=self.repeatedSuccess self.dTevaluate=0 if self.chief then self:T(self.lid..string.format("CHIEF will cancel the mission. Will wait for mission DONE before evaluation!")) self.chief:MissionCancel(self) elseif self.commander then self:T(self.lid..string.format("COMMANDER will cancel the mission. Will wait for mission DONE before evaluation!")) self.commander:MissionCancel(self) elseif self.legions and#self.legions>0 then for _,_legion in pairs(self.legions or{})do local legion=_legion self:T(self.lid..string.format("LEGION %s will cancel the mission. Will wait for mission DONE before evaluation!",legion.alias)) legion:MissionCancel(self) end else self:T(self.lid..string.format("No legion, commander or chief. Attached groups will cancel the mission on their own. Will wait for mission DONE before evaluation!")) for _,_groupdata in pairs(self.groupdata or{})do local groupdata=_groupdata groupdata.opsgroup:MissionCancel(self) end end if self:IsPlanned()or self:IsQueued()or self:IsRequested()or Ngroups==0 then self:T(self.lid..string.format("Cancelled mission was in %s stage with %d groups assigned and alive. Call it done!",self.status,Ngroups)) self:Done() end end function AUFTRAG:onafterDone(From,Event,To) self.status=AUFTRAG.Status.DONE self:T(self.lid..string.format("New mission status=%s",self.status)) self.Tover=timer.getAbsTime() self.Texecuting=nil self.statusChief=AUFTRAG.Status.DONE self.statusCommander=AUFTRAG.Status.DONE for _,_legion in pairs(self.legions)do local Legion=_legion self:SetLegionStatus(Legion,AUFTRAG.Status.DONE) if self.type==AUFTRAG.Type.RELOCATECOHORT then local requestid=self.requestID[Legion.alias] if requestid then self:T(self.lid.."Removing request from pending queue") Legion:_DeleteQueueItemByID(requestid,Legion.pending) local Cohort=self.DCStask.params.cohort Legion:DelCohort(Cohort) else self:E(self.lid.."WARNING: Could NOT remove relocation request from from pending queue (all assets were spawned?)") end end end if self.type==AUFTRAG.Type.RELOCATECOHORT then local cohort=self.DCStask.params.cohort cohort:Relocated() end end function AUFTRAG:onafterSuccess(From,Event,To) self.status=AUFTRAG.Status.SUCCESS self:T(self.lid..string.format("New mission status=%s",self.status)) self.statusChief=self.status self.statusCommander=self.status for _,_legion in pairs(self.legions)do local Legion=_legion self:SetLegionStatus(Legion,self.status) end local repeatme=self.repeatedSuccess Repeat mission!",self.repeated+1,N)) self:__Repeat(self.repeatDelay) else self:T(self.lid..string.format("Mission SUCCESS! Number of max repeats %d reached ==> Stopping mission!",self.repeated+1)) self:Stop() end end function AUFTRAG:onafterFailed(From,Event,To) self.status=AUFTRAG.Status.FAILED self:T(self.lid..string.format("New mission status=%s",self.status)) self.statusChief=self.status self.statusCommander=self.status for _,_legion in pairs(self.legions)do local Legion=_legion self:SetLegionStatus(Legion,self.status) end local repeatme=self.repeatedFailure Repeat mission!",self.repeated+1,N)) self:__Repeat(self.repeatDelay) else self:T(self.lid..string.format("Mission FAILED! Number of max repeats %d reached ==> Stopping mission!",self.repeated+1)) self:Stop() end end function AUFTRAG:onbeforeRepeat(From,Event,To) if not(self.chief or self.commander or#self.legions>0)then self:E(self.lid.."ERROR: Mission can only be repeated by a CHIEF, COMMANDER or LEGION! Stopping AUFTRAG") self:Stop() return false end return true end function AUFTRAG:onafterRepeat(From,Event,To) self.status=AUFTRAG.Status.PLANNED self:T(self.lid..string.format("New mission status=%s (on Repeat)",self.status)) self.statusChief=self.status self.statusCommander=self.status for _,_legion in pairs(self.legions)do local Legion=_legion self:SetLegionStatus(Legion,self.status) end self.repeated=self.repeated+1 if self.chief then self.statusChief=AUFTRAG.Status.PLANNED if self.commander then self.statusCommander=AUFTRAG.Status.PLANNED end for _,_legion in pairs(self.legions)do local legion=_legion legion:RemoveMission(self) end elseif self.commander then self.statusCommander=AUFTRAG.Status.PLANNED for _,_legion in pairs(self.legions)do local legion=_legion legion:RemoveMission(self) self:SetLegionStatus(legion,AUFTRAG.Status.PLANNED) end elseif#self.legions>0 then for _,_legion in pairs(self.legions)do local legion=_legion legion:RemoveMission(self) self:SetLegionStatus(legion,AUFTRAG.Status.PLANNED) legion:AddMission(self) end else self:E(self.lid.."ERROR: Mission can only be repeated by a CHIEF, COMMANDER or LEGION! Stopping AUFTRAG") self:Stop() return end self.assets={} for groupname,_groupdata in pairs(self.groupdata)do local groupdata=_groupdata local opsgroup=groupdata.opsgroup if opsgroup then self:DelOpsGroup(opsgroup) end end self.groupdata={} self.Ncasualties=0 self.Nelements=0 self.Ngroups=0 self.Nassigned=nil self.Ndead=0 self.DCStask=self:GetDCSMissionTask() self:__Status(-30) end function AUFTRAG:onafterStop(From,Event,To) self:T(self.lid..string.format("STOPPED mission in status=%s. Removing missions from queues. Stopping CallScheduler!",self.status)) if self.chief then self.chief:RemoveMission(self) end if self.commander then self.commander:RemoveMission(self) end if#self.legions>0 then for _,_legion in pairs(self.legions)do local legion=_legion legion:RemoveMission(self) end end for _,_groupdata in pairs(self.groupdata)do local groupdata=_groupdata groupdata.opsgroup:RemoveMission(self) end self.assets={} self.groupdata={} self.CallScheduler:Clear() end function AUFTRAG:_TargetFromObject(Object) if not self.engageTarget then if Object and Object:IsInstanceOf("TARGET")then self.engageTarget=Object else self.engageTarget=TARGET:New(Object) end else end return self end function AUFTRAG:CountMissionTargets(OnlyReallyAlive) local N=0 local Coalitions=self.coalition and UTILS.GetCoalitionEnemy(self.coalition,true)or nil if self.engageTarget then N=self.engageTarget:CountTargets(Coalitions,OnlyReallyAlive) end return N end function AUFTRAG:GetTargetInitialNumber() local target=self:GetTargetData() if target then return target.N0 else return 0 end end function AUFTRAG:GetTargetInitialLife() local target=self:GetTargetData() if target then return target.life0 else return 0 end end function AUFTRAG:GetTargetDamage() local target=self:GetTargetData() if target then return target:GetDamage() else return 0 end end function AUFTRAG:GetTargetLife() local target=self:GetTargetData() if target then return target:GetLife() else return 0 end end function AUFTRAG:GetCargoSet() if self.type==AUFTRAG.Type.CARGOTRANSPORT then local set=SET_STATIC:New() set:AddObject(self.DCStask.params.cargo) return set elseif self.type==AUFTRAG.Type.TROOPTRANSPORT then return self.transportGroupSet elseif self.type==AUFTRAG.Type.FREIGHTTRANSPORT then return self.DCStask.params.cargo else self:E(self.lid.."ERROR: GetCargoSet() is only for transport types!") return nil end end function AUFTRAG:GetTargetData() return self.engageTarget end function AUFTRAG:GetObjective(RefCoordinate,Coalitions) local objective=self:GetTargetData():GetObject(RefCoordinate,Coalitions) return objective end function AUFTRAG:GetTargetType() local target=self.engageTarget if target then local to=target:GetObjective() if to then return to.Type else return"Unknown" end else return"Unknown" end end function AUFTRAG:GetTargetVec2() local coord=self:GetTargetCoordinate() if coord then local vec2=coord:GetVec2() return vec2 end return nil end function AUFTRAG:GetTargetCoordinate() if self.transportPickup then return self.transportPickup elseif self.missionZoneSet and self.type==AUFTRAG.Type.RECON then return self.missionZoneSet:GetAverageCoordinate() elseif self.engageTarget then local coord=self.engageTarget:GetCoordinate() return coord elseif self.type==AUFTRAG.Type.ALERT5 then return nil else self:T(self.lid.."ERROR: Cannot get target coordinate!") end return nil end function AUFTRAG:GetTargetHeading() if self.engageTarget then local heading=self.engageTarget:GetHeading() return heading end return nil end function AUFTRAG:GetTargetName() if self.engageTarget then local name=self.engageTarget:GetName() return name end return"N/A" end function AUFTRAG:GetTargetDistance(FromCoord) local TargetCoord=self:GetTargetCoordinate() if TargetCoord and FromCoord then return TargetCoord:Get2DDistance(FromCoord) else self:T(self.lid.."ERROR: TargetCoord or FromCoord does not exist in AUFTRAG:GetTargetDistance() function! Returning 0") end return 0 end function AUFTRAG:AddAsset(Asset) self:T(self.lid..string.format("Adding asset \"%s\" to mission",tostring(Asset.spawngroupname))) self.assets=self.assets or{} local asset=self:GetAssetByName(Asset.spawngroupname) if not asset then table.insert(self.assets,Asset) self.Nassigned=self.Nassigned or 0 self.Nassigned=self.Nassigned+1 end return self end function AUFTRAG:_AddAssets(Assets) for _,asset in pairs(Assets)do self:AddAsset(asset) end return self end function AUFTRAG:DelAsset(Asset) for i,_asset in pairs(self.assets or{})do local asset=_asset if asset.uid==Asset.uid then self:T(self.lid..string.format("Removing asset \"%s\" from mission",tostring(Asset.spawngroupname))) table.remove(self.assets,i) return self end end return self end function AUFTRAG:GetAssetByName(Name) for i,_asset in pairs(self.assets or{})do local asset=_asset if asset.spawngroupname==Name then return asset end end return nil end function AUFTRAG:CountOpsGroups() local N=0 for _,_groupdata in pairs(self.groupdata)do local groupdata=_groupdata if groupdata and groupdata.opsgroup and groupdata.opsgroup:IsAlive()and not groupdata.opsgroup:IsDead()then N=N+1 end end return N end function AUFTRAG:CountOpsGroupsInStatus(Status) local N=0 for _,_groupdata in pairs(self.groupdata)do local groupdata=_groupdata if groupdata and groupdata.status==Status then N=N+1 end end return N end function AUFTRAG:GetMissionTypesText(MissionTypes) local text="" for _,missiontype in pairs(MissionTypes)do text=text..string.format("%s, ",missiontype) end return text end function AUFTRAG:SetMissionWaypointCoord(Coordinate) if Coordinate:IsInstanceOf("ZONE_BASE")then Coordinate=Coordinate:GetCoordinate() end self.missionWaypointCoord=Coordinate return self end function AUFTRAG:SetMissionWaypointRandomization(Radius) self.missionWaypointRadius=Radius return self end function AUFTRAG:SetMissionEgressCoord(Coordinate,Altitude,Speed) if Coordinate:IsInstanceOf("ZONE_BASE")then Coordinate=Coordinate:GetCoordinate() end self.missionEgressCoord=Coordinate if Altitude then self.missionEgressCoord.y=UTILS.FeetToMeters(Altitude) self.missionEgressCoordAlt=UTILS.FeetToMeters(Altitude) end self.missionEgressCoordSpeed=Speed and Speed or nil return self end function AUFTRAG:SetMissionIngressCoord(Coordinate,Altitude,Speed) if Coordinate:IsInstanceOf("ZONE_BASE")then Coordinate=Coordinate:GetCoordinate() end self.missionIngressCoord=Coordinate if Altitude then self.missionIngressCoord.y=UTILS.FeetToMeters(Altitude) self.missionIngressCoordAlt=UTILS.FeetToMeters(Altitude or 10000) end self.missionIngressCoordSpeed=Speed and Speed or nil return self end function AUFTRAG:SetMissionHoldingCoord(Coordinate,Altitude,Speed,Duration) if Coordinate:IsInstanceOf("ZONE_BASE")then Coordinate=Coordinate:GetCoordinate() end self.missionHoldingCoord=Coordinate self.missionHoldingDuration=Duration or 900 if Altitude then self.missionHoldingCoord.y=UTILS.FeetToMeters(Altitude) self.missionHoldingCoordAlt=UTILS.FeetToMeters(Altitude or 10000) end self.missionHoldingCoordSpeed=Speed and Speed or nil return self end function AUFTRAG:GetMissionEgressCoord() return self.missionEgressCoord end function AUFTRAG:GetMissionIngressCoord() return self.missionIngressCoord end function AUFTRAG:GetMissionHoldingCoord() return self.missionHoldingCoord end function AUFTRAG:_GetMissionWaypointCoordSet() if self.missionWaypointCoord then local coord=self.missionWaypointCoord if self.missionAltitude then coord.y=self.missionAltitude end return coord end end function AUFTRAG:GetMissionWaypointCoord(group,randomradius,surfacetypes) if self.missionWaypointCoord then local coord=self.missionWaypointCoord if self.missionAltitude then coord.y=self.missionAltitude end return coord end local coord=group:GetCoordinate() if self.missionHoldingCoord then coord=self.missionHoldingCoord if self.missionHoldingCoorddAlt then coord:SetAltitude(self.missionHoldingCoordAlt,true) end end if self.missionIngressCoord then coord=self.missionIngressCoord if self.missionIngressCoordAlt then coord:SetAltitude(self.missionIngressCoordAlt,true) end end local waypointcoord=COORDINATE:New(0,0,0) if coord then waypointcoord=coord:GetIntermediateCoordinate(self:GetTargetCoordinate(),self.missionFraction) else self:E(self.lid..string.format("ERROR: Cannot get coordinate of group %s (alive=%s)!",tostring(group:GetName()),tostring(group:IsAlive()))) end local alt=waypointcoord.y if randomradius then waypointcoord=ZONE_RADIUS:New("Temp",waypointcoord:GetVec2(),randomradius):GetRandomCoordinate(nil,nil,surfacetypes):SetAltitude(alt,false) end if self.missionAltitude then waypointcoord:SetAltitude(self.missionAltitude,true) end return waypointcoord end function AUFTRAG:_SetLogID() self.lid=string.format("Auftrag #%d %s | ",self.auftragsnummer,tostring(self.type)) return self end function AUFTRAG:_GetRequestID(Legion) local requestid=nil local name=nil if type(Legion)=="string"then name=Legion else name=Legion.alias end if name then requestid=self.requestID[name] end return nil end function AUFTRAG:_GetRequest(Legion) local request=nil local requestID=self:_GetRequestID(Legion) if requestID then request=Legion:GetRequestByID(requestID) end return request end function AUFTRAG:_SetRequestID(Legion,RequestID) local requestid=nil local name=nil if type(Legion)=="string"then name=Legion else name=Legion.alias end if name then if self.requestID[name]then self:I(self.lid..string.format("WARNING: Mission already has a request ID=%d!",self.requestID[name])) end self.requestID[name]=RequestID end return self end function AUFTRAG:_IsNotReinforcing() local Nassigned=self.Nassigned and self.Nassigned-self.Ndead or 0 local notreinforcing=((not self.reinforce)or(self.reinforce==0 and Nassigned<=0)) return notreinforcing end function AUFTRAG:_IsReinforcing() local reinforcing=not self:_IsNotReinforcing() return reinforcing end function AUFTRAG:UpdateMarker() local text=string.format("%s %s: %s",self.name,self.type:upper(),self.status:upper()) text=text..string.format("\n%s",self:GetTargetName()) text=text..string.format("\nTargets %d/%d, Life Points=%d/%d",self:CountMissionTargets(),self:GetTargetInitialNumber(),self:GetTargetLife(),self:GetTargetInitialLife()) text=text..string.format("\nOpsGroups %d/%d",self:CountOpsGroups(),self:GetNumberOfRequiredAssets()) if not self.marker then local targetcoord=self:GetTargetCoordinate() if targetcoord then if self.markerCoaliton and self.markerCoaliton>=0 then self.marker=MARKER:New(targetcoord,text):ReadOnly():ToCoalition(self.markerCoaliton) else self.marker=MARKER:New(targetcoord,text):ReadOnly():ToAll() end end else if self.marker:GetText()~=text then self.marker:UpdateText(text) end end return self end function AUFTRAG:GetDCSMissionTask(MissionGroup) local DCStasks={} if self.type==AUFTRAG.Type.ANTISHIP then local DCStask=CONTROLLABLE.EnRouteTaskAntiShip(nil) table.insert(self.enrouteTasks,DCStask) self:_GetDCSAttackTask(self.engageTarget,DCStasks) elseif self.type==AUFTRAG.Type.AWACS then local DCStask=CONTROLLABLE.EnRouteTaskAWACS(nil) table.insert(self.enrouteTasks,DCStask) elseif self.type==AUFTRAG.Type.BAI then self:_GetDCSAttackTask(self.engageTarget,DCStasks) elseif self.type==AUFTRAG.Type.BOMBING then local coords=self.engageTarget:GetCoordinates() for _,coord in pairs(coords)do local DCStask=CONTROLLABLE.TaskBombing(nil,coord:GetVec2(),self.engageAsGroup,self.engageWeaponExpend,self.engageQuantity,self.engageDirection,self.engageAltitude,self.engageWeaponType,self.optionDivebomb) table.insert(DCStasks,DCStask) end elseif self.type==AUFTRAG.Type.STRAFING then local DCStask=CONTROLLABLE.TaskStrafing(nil,self:GetTargetVec2(),self.engageQuantity,self.engageLength,self.engageWeaponType,self.engageWeaponExpend,self.engageDirection,self.engageAsGroup) table.insert(DCStasks,DCStask) elseif self.type==AUFTRAG.Type.BOMBRUNWAY then local DCStask=CONTROLLABLE.TaskBombingRunway(nil,self.engageTarget:GetObject(),self.engageWeaponType,self.engageWeaponExpend,self.engageQuantity,self.engageDirection,self.engageAsGroup) table.insert(DCStasks,DCStask) elseif self.type==AUFTRAG.Type.BOMBCARPET then local DCStask=CONTROLLABLE.TaskCarpetBombing(nil,self:GetTargetVec2(),self.engageAsGroup,self.engageWeaponExpend,self.engageQuantity,self.engageDirection,self.engageAltitude,self.engageWeaponType,self.engageLength) table.insert(DCStasks,DCStask) elseif self.type==AUFTRAG.Type.CAP then local Vec2=self.engageZone:GetVec2() local Radius if self.engageZone:IsInstanceOf("COORDINATE")then Radius=UTILS.NMToMeters(20) else Radius=self.engageZone:GetRadius() end local DCStask=CONTROLLABLE.EnRouteTaskEngageTargetsInZone(nil,Vec2,Radius,self.engageTargetTypes,Priority) table.insert(self.enrouteTasks,DCStask) elseif self.type==AUFTRAG.Type.CAS then local DCStask=CONTROLLABLE.EnRouteTaskEngageTargetsInZone(nil,self.engageZone:GetVec2(),self.engageZone:GetRadius(),self.engageTargetTypes,Priority) table.insert(self.enrouteTasks,DCStask) elseif self.type==AUFTRAG.Type.ESCORT then local DCStask=CONTROLLABLE.TaskEscort(nil,self.engageTarget:GetObject(),self.escortVec3,nil,self.engageMaxDistance,self.engageTargetTypes) table.insert(DCStasks,DCStask) elseif self.type==AUFTRAG.Type.GROUNDESCORT then local DCSTask=CONTROLLABLE.TaskGroundEscort(nil,self.engageTarget:GetObject(),nil,self.orbitDistance,self.engageTargetTypes) table.insert(DCStasks,DCSTask) elseif self.type==AUFTRAG.Type.FACA then local DCStask=CONTROLLABLE.TaskFAC_AttackGroup(nil,self.engageTarget:GetObject(),self.engageWeaponType,self.facDesignation,self.facDatalink,self.facFreq,self.facModu,CallsignName,CallsignNumber) table.insert(DCStasks,DCStask) elseif self.type==AUFTRAG.Type.FAC then local DCStask={} DCStask.id=AUFTRAG.SpecialTask.PATROLZONE local param={} param.zone=self:GetObjective() param.altitude=self.missionAltitude param.speed=self.missionSpeed and UTILS.KmphToMps(self.missionSpeed)or nil DCStask.params=param table.insert(DCStasks,DCStask) local DCSenroute=CONTROLLABLE.EnRouteTaskFAC(self,self.facFreq,self.facModu) table.insert(self.enrouteTasks,DCSenroute) elseif self.type==AUFTRAG.Type.FERRY then local DCStask={} DCStask.id=AUFTRAG.SpecialTask.FERRY local param={} DCStask.params=param table.insert(DCStasks,DCStask) elseif self.type==AUFTRAG.Type.RELOCATECOHORT then local DCStask={} DCStask.id=AUFTRAG.SpecialTask.RELOCATECOHORT local param={} DCStask.params=param table.insert(DCStasks,DCStask) elseif self.type==AUFTRAG.Type.INTERCEPT then self:_GetDCSAttackTask(self.engageTarget,DCStasks) elseif self.type==AUFTRAG.Type.ORBIT then elseif self.type==AUFTRAG.Type.GCICAP then elseif self.type==AUFTRAG.Type.RECON then local DCStask={} DCStask.id=AUFTRAG.SpecialTask.RECON local param={} param.target=self.engageTarget param.altitude=self.missionAltitude param.speed=self.missionSpeed and UTILS.KmphToMps(self.missionSpeed)or nil param.lastindex=nil DCStask.params=param table.insert(DCStasks,DCStask) elseif self.type==AUFTRAG.Type.SEAD then if self.engageZone then self.engageZone:Scan({Object.Category.UNIT},{Unit.Category.GROUND_UNIT}) local ScanUnitSet=self.engageZone:GetScannedSetUnit() local SeadUnitSet=SET_UNIT:New() for _,_unit in pairs(ScanUnitSet.Set)do local unit=_unit if unit and unit:IsAlive()and unit.HasSEAD and unit:HasSEAD()then self:T("Adding UNIT for SEAD: "..unit:GetName()) local task=CONTROLLABLE.TaskAttackUnit(nil,unit,GroupAttack,AI.Task.WeaponExpend.ALL,1,Direction,self.engageAltitude,2956984318) table.insert(DCStasks,task) SeadUnitSet:AddUnit(unit) end end self.engageTarget=TARGET:New(SeadUnitSet) else self:_GetDCSAttackTask(self.engageTarget,DCStasks) end elseif self.type==AUFTRAG.Type.STRIKE then local coords=self.engageTarget:GetCoordinates() for _,coord in pairs(coords)do local DCStask=CONTROLLABLE.TaskAttackMapObject(nil,coord:GetVec2(),self.engageAsGroup,self.engageWeaponExpend,self.engageQuantity,self.engageDirection,self.engageAltitude,self.engageWeaponType) table.insert(DCStasks,DCStask) end elseif self.type==AUFTRAG.Type.TANKER or self.type==AUFTRAG.Type.RECOVERYTANKER then local DCStask=CONTROLLABLE.EnRouteTaskTanker(nil) table.insert(self.enrouteTasks,DCStask) elseif self.type==AUFTRAG.Type.TROOPTRANSPORT then local TaskEmbark=CONTROLLABLE.TaskEmbarking(TaskControllable,self.transportPickup,self.transportGroupSet,self.transportWaitForCargo) local TaskDisEmbark=CONTROLLABLE.TaskDisembarking(TaskControllable,self.transportDropoff,self.transportGroupSet) table.insert(DCStasks,TaskEmbark) table.insert(DCStasks,TaskDisEmbark) elseif self.type==AUFTRAG.Type.OPSTRANSPORT then local DCStask={} DCStask.id="OpsTransport" local param={} DCStask.params=param table.insert(DCStasks,DCStask) elseif self.type==AUFTRAG.Type.CARGOTRANSPORT then local TaskCargoTransportation={ id="CargoTransportation", params={} } table.insert(DCStasks,TaskCargoTransportation) elseif self.type==AUFTRAG.Type.FREIGHTTRANSPORT then local statics=self.engageTarget:GetObjects() for _,StaticObject in pairs(statics)do local static=StaticObject self:T(static) local TaskCargoUnload={ ["id"]="CargoUnloadPlane", ["params"]= { ["groupId"]=static:GetID(), ["unitId"]=static:GetID(), } } table.insert(DCStasks,TaskCargoUnload) end elseif self.type==AUFTRAG.Type.RESCUEHELO then local DCStask={} DCStask.id=AUFTRAG.SpecialTask.FORMATION local param={} param.unitname=self:GetTargetName() param.offsetX=200 param.offsetZ=240 param.altitude=70 param.dtFollow=1.0 DCStask.params=param table.insert(DCStasks,DCStask) elseif self.type==AUFTRAG.Type.ARTY then if self.artyShots==1 or self.artyRadius<10 or true then local DCStask=CONTROLLABLE.TaskFireAtPoint(nil,self:GetTargetVec2(),self.artyRadius,self.artyShots,self.engageWeaponType,self.artyAltitude) table.insert(DCStasks,DCStask) else local Vec2=self:GetTargetVec2() local zone=ZONE_RADIUS:New("temp",Vec2,self.artyRadius) for i=1,self.artyShots do local vec2=zone:GetRandomVec2() local DCStask=CONTROLLABLE.TaskFireAtPoint(nil,vec2,0,1,self.engageWeaponType,self.artyAltitude) table.insert(DCStasks,DCStask) end end elseif self.type==AUFTRAG.Type.BARRAGE then local DCStask={} DCStask.id=AUFTRAG.SpecialTask.BARRAGE local param={} param.zone=self:GetObjective() param.altitude=self.artyAltitude param.radius=self.artyRadius param.heading=self.artyHeading param.angle=self.artyAngle param.shots=self.artyShots param.weaponTypoe=self.engageWeaponType DCStask.params=param table.insert(DCStasks,DCStask) elseif self.type==AUFTRAG.Type.PATROLZONE then local DCStask={} DCStask.id=AUFTRAG.SpecialTask.PATROLZONE local param={} param.zone=self:GetObjective() param.altitude=self.missionAltitude param.speed=self.missionSpeed and UTILS.KmphToMps(self.missionSpeed)or nil DCStask.params=param table.insert(DCStasks,DCStask) elseif self.type==AUFTRAG.Type.CAPTUREZONE then local DCStask={} DCStask.id=AUFTRAG.SpecialTask.CAPTUREZONE local param={} DCStask.params=param table.insert(DCStasks,DCStask) elseif self.type==AUFTRAG.Type.CASENHANCED then local DCStask={} DCStask.id=AUFTRAG.SpecialTask.PATROLZONE local param={} param.zone=self:GetObjective() param.altitude=self.missionAltitude param.speed=self.missionSpeed and UTILS.KmphToMps(self.missionSpeed)or nil DCStask.params=param table.insert(DCStasks,DCStask) elseif self.type==AUFTRAG.Type.GROUNDATTACK then local DCStask={} DCStask.id=AUFTRAG.SpecialTask.GROUNDATTACK local param={} param.target=self:GetTargetData() param.action="Wedge" param.speed=self.missionSpeed and UTILS.KmphToMps(self.missionSpeed)or nil DCStask.params=param table.insert(DCStasks,DCStask) elseif self.type==AUFTRAG.Type.NAVALENGAGEMENT then local DCStask={} DCStask.id=AUFTRAG.SpecialTask.NAVALENGAGEMENT local param={} param.target=self:GetTargetData() param.speed=self.missionSpeed and UTILS.KmphToMps(self.missionSpeed)or nil param.altitude=self.missionAltitude or 0 DCStask.params=param table.insert(DCStasks,DCStask) elseif self.type==AUFTRAG.Type.AMMOSUPPLY then local DCStask={} DCStask.id=AUFTRAG.SpecialTask.AMMOSUPPLY local param={} param.zone=self:GetObjective() DCStask.params=param table.insert(DCStasks,DCStask) elseif self.type==AUFTRAG.Type.FUELSUPPLY then local DCStask={} DCStask.id=AUFTRAG.SpecialTask.FUELSUPPLY local param={} param.zone=self:GetObjective() DCStask.params=param table.insert(DCStasks,DCStask) elseif self.type==AUFTRAG.Type.REARMING then local DCStask={} DCStask.id=AUFTRAG.SpecialTask.REARMING local param={} param.zone=self:GetObjective() DCStask.params=param table.insert(DCStasks,DCStask) elseif self.type==AUFTRAG.Type.ALERT5 then local DCStask={} DCStask.id=AUFTRAG.SpecialTask.ALERT5 local param={} DCStask.params=param table.insert(DCStasks,DCStask) elseif self.type==AUFTRAG.Type.NOTHING then local DCStask={} DCStask.id=AUFTRAG.SpecialTask.NOTHING local param={} DCStask.params=param table.insert(DCStasks,DCStask) elseif self.type==AUFTRAG.Type.PATROLRACETRACK then local DCStask={} DCStask.id=AUFTRAG.SpecialTask.PATROLRACETRACK local param={} param.TrackAltitude=self.TrackAltitude param.TrackSpeed=self.TrackSpeed param.TrackPoint1=self.TrackPoint1 param.TrackPoint2=self.TrackPoint2 param.missionSpeed=self.missionSpeed param.missionAltitude=self.missionAltitude param.TrackFormation=self.TrackFormation DCStask.params=param table.insert(DCStasks,DCStask) elseif self.type==AUFTRAG.Type.HOVER then local DCStask={} DCStask.id=AUFTRAG.SpecialTask.HOVER local param={} param.hoverAltitude=self.hoverAltitude param.hoverTime=self.hoverTime param.missionSpeed=self.missionSpeed param.missionAltitude=self.missionAltitude DCStask.params=param table.insert(DCStasks,DCStask) elseif self.type==AUFTRAG.Type.LANDATCOORDINATE then local DCStask={} local Vec2=self.stayAt:GetVec2() local DCStask=CONTROLLABLE.TaskLandAtVec2(nil,Vec2,self.stayTime,self.combatLand,self.directionAfter) table.insert(DCStasks,DCStask) elseif self.type==AUFTRAG.Type.ONGUARD or self.type==AUFTRAG.Type.ARMOREDGUARD then local DCStask={} DCStask.id=self.type==AUFTRAG.Type.ONGUARD and AUFTRAG.SpecialTask.ONGUARD or AUFTRAG.SpecialTask.ARMOREDGUARD local param={} param.coordinate=self:GetObjective() DCStask.params=param table.insert(DCStasks,DCStask) elseif self.type==AUFTRAG.Type.AIRDEFENSE then local DCStask={} DCStask.id=AUFTRAG.SpecialTask.AIRDEFENSE local param={} param.zone=self:GetObjective() DCStask.params=param table.insert(DCStasks,DCStask) elseif self.type==AUFTRAG.Type.EWR then local DCStask={} DCStask.id=AUFTRAG.SpecialTask.EWR local param={} param.zone=self:GetObjective() DCStask.params=param table.insert(DCStasks,DCStask) local Enroutetask=CONTROLLABLE.EnRouteTaskEWR() table.insert(self.enrouteTasks,Enroutetask) else self:T(self.lid..string.format("ERROR: Unknown mission task!")) return nil end if self.type==AUFTRAG.Type.ORBIT or self.type==AUFTRAG.Type.CAP or self.type==AUFTRAG.Type.CAS or self.type==AUFTRAG.Type.GCICAP or self.type==AUFTRAG.Type.AWACS or self.type==AUFTRAG.Type.TANKER or self.type==AUFTRAG.Type.RECOVERYTANKER then self.orbitVec2=self:GetTargetVec2() if self.orbitVec2 then self.targetHeading=self:GetTargetHeading() local OffsetVec2=nil if(self.orbitOffsetVec2~=nil)then OffsetVec2=UTILS.DeepCopy(self.orbitOffsetVec2) end if OffsetVec2 then if self.orbitOffsetVec2.r then local r=self.orbitOffsetVec2.r local phi=(self.orbitOffsetVec2.phi or 0)+self.targetHeading OffsetVec2.x=r*math.cos(math.rad(phi)) OffsetVec2.y=r*math.sin(math.rad(phi)) else OffsetVec2.x=self.orbitOffsetVec2.x OffsetVec2.y=self.orbitOffsetVec2.y end end local orbitVec2=OffsetVec2 and UTILS.Vec2Add(self.orbitVec2,OffsetVec2)or self.orbitVec2 local orbitRaceTrack=nil if self.orbitLeg then local heading=0 if self.orbitHeading then if self.orbitHeadingRel then heading=self.targetHeading+self.orbitHeading else heading=self.orbitHeading end else heading=self.targetHeading or 0 end orbitRaceTrack=UTILS.Vec2Translate(orbitVec2,self.orbitLeg,heading) end local orbitRaceTrackCoord=nil if orbitRaceTrack then orbitRaceTrackCoord=COORDINATE:NewFromVec2(orbitRaceTrack) end local DCStask=CONTROLLABLE.TaskOrbit(nil,COORDINATE:NewFromVec2(orbitVec2),self.orbitAltitude,self.orbitSpeed,orbitRaceTrackCoord) table.insert(DCStasks,DCStask) end end self:T3({missiontask=DCStasks}) if#DCStasks==1 then return DCStasks[1] else return CONTROLLABLE.TaskCombo(nil,DCStasks) end end function AUFTRAG:_GetDCSAttackTask(Target,DCStasks) DCStasks=DCStasks or{} for _,_target in pairs(Target.targets)do local target=_target if target.Type==TARGET.ObjectType.GROUP then local DCStask=CONTROLLABLE.TaskAttackGroup(nil,target.Object,self.engageWeaponType,self.engageWeaponExpend,self.engageQuantity,self.engageDirection,self.engageAltitude,self.engageAsGroup) table.insert(DCStasks,DCStask) elseif target.Type==TARGET.ObjectType.UNIT or target.Type==TARGET.ObjectType.STATIC then local DCStask=CONTROLLABLE.TaskAttackUnit(nil,target.Object,self.engageAsGroup,self.engageWeaponExpend,self.engageQuantity,self.engageDirection,self.engageAltitude,self.engageWeaponType) table.insert(DCStasks,DCStask) end end return DCStasks end function AUFTRAG:GetMissionTaskforMissionType(MissionType) local mtask=ENUMS.MissionTask.NOTHING if MissionType==AUFTRAG.Type.ANTISHIP then mtask=ENUMS.MissionTask.ANTISHIPSTRIKE elseif MissionType==AUFTRAG.Type.AWACS then mtask=ENUMS.MissionTask.AWACS elseif MissionType==AUFTRAG.Type.BAI then mtask=ENUMS.MissionTask.GROUNDATTACK elseif MissionType==AUFTRAG.Type.BOMBCARPET then mtask=ENUMS.MissionTask.GROUNDATTACK elseif MissionType==AUFTRAG.Type.BOMBING then mtask=ENUMS.MissionTask.GROUNDATTACK elseif MissionType==AUFTRAG.Type.BOMBRUNWAY then mtask=ENUMS.MissionTask.RUNWAYATTACK elseif MissionType==AUFTRAG.Type.CAP then mtask=ENUMS.MissionTask.CAP elseif MissionType==AUFTRAG.Type.GCICAP then mtask=ENUMS.MissionTask.CAP elseif MissionType==AUFTRAG.Type.CAS then mtask=ENUMS.MissionTask.CAS elseif MissionType==AUFTRAG.Type.PATROLZONE then mtask=ENUMS.MissionTask.CAS elseif MissionType==AUFTRAG.Type.CASENHANCED then mtask=ENUMS.MissionTask.CAS elseif MissionType==AUFTRAG.Type.ESCORT then mtask=ENUMS.MissionTask.ESCORT elseif MissionType==AUFTRAG.Type.FACA then mtask=ENUMS.MissionTask.AFAC elseif MissionType==AUFTRAG.Type.FAC then mtask=ENUMS.MissionTask.AFAC elseif MissionType==AUFTRAG.Type.FERRY then mtask=ENUMS.MissionTask.NOTHING elseif MissionType==AUFTRAG.Type.GROUNDESCORT then mtask=ENUMS.MissionTask.GROUNDESCORT elseif MissionType==AUFTRAG.Type.INTERCEPT then mtask=ENUMS.MissionTask.INTERCEPT elseif MissionType==AUFTRAG.Type.RECON then mtask=ENUMS.MissionTask.RECONNAISSANCE elseif MissionType==AUFTRAG.Type.SEAD then mtask=ENUMS.MissionTask.SEAD elseif MissionType==AUFTRAG.Type.STRIKE then mtask=ENUMS.MissionTask.GROUNDATTACK elseif MissionType==AUFTRAG.Type.TANKER then mtask=ENUMS.MissionTask.REFUELING elseif MissionType==AUFTRAG.Type.TROOPTRANSPORT then mtask=ENUMS.MissionTask.TRANSPORT elseif MissionType==AUFTRAG.Type.CARGOTRANSPORT then mtask=ENUMS.MissionTask.TRANSPORT elseif MissionType==AUFTRAG.Type.FREIGHTTRANSPORT then mtask=ENUMS.MissionTask.TRANSPORT elseif MissionType==AUFTRAG.Type.ARMORATTACK then mtask=ENUMS.MissionTask.NOTHING elseif MissionType==AUFTRAG.Type.HOVER then mtask=ENUMS.MissionTask.NOTHING elseif MissionType==AUFTRAG.Type.PATROLRACETRACK then mtask=ENUMS.MissionTask.CAP end return mtask end function AUFTRAG.CheckMissionType(MissionType,PossibleTypes) if type(PossibleTypes)=="string"then PossibleTypes={PossibleTypes} end for _,canmission in pairs(PossibleTypes)do if canmission==MissionType then return true end end return false end function AUFTRAG.CheckMissionCapability(MissionTypes,Capabilities,All) if type(MissionTypes)~="table"then MissionTypes={MissionTypes} end for _,cap in pairs(Capabilities)do local capability=cap for _,MissionType in pairs(MissionTypes)do if All==true then if capability.MissionType~=MissionType then return false end else if capability.MissionType==MissionType then return true end end end end if All==true then return true else return false end end function AUFTRAG.CheckMissionCapabilityAny(MissionTypes,Capabilities) local res=AUFTRAG.CheckMissionCapability(MissionTypes,Capabilities,false) return res end function AUFTRAG.CheckMissionCapabilityAll(MissionTypes,Capabilities) local res=AUFTRAG.CheckMissionCapability(MissionTypes,Capabilities,true) return res end do AWACS={ ClassName="AWACS", version="0.2.74", lid="", coalition=coalition.side.BLUE, coalitiontxt="blue", OpsZone=nil, StationZone=nil, AirWing=nil, Frequency=271, Modulation=radio.modulation.AM, Airbase=nil, AwacsAngels=25, OrbitZone=nil, CallSign=CALLSIGN.AWACS.Magic, CallSignNo=1, debug=false, verbose=false, ManagedGrps={}, ManagedGrpID=0, ManagedTaskID=0, AnchorStacks={}, CAPIdleAI={}, CAPIdleHuman={}, TaskedCAPAI={}, TaskedCAPHuman={}, OpenTasks={}, ManagedTasks={}, PictureAO={}, PictureEWR={}, Contacts={}, Countactcounter=0, ContactsAO={}, RadioQueue={}, PrioRadioQueue={}, TacticalQueue={}, AwacsTimeOnStation=4, AwacsTimeStamp=0, EscortsTimeOnStation=4, EscortsTimeStamp=0, CAPTimeOnStation=4, AwacsROE="", AwacsROT="", MenuStrict=true, MaxAIonCAP=3, AIonCAP=0, AICAPMissions={}, ShiftChangeAwacsFlag=false, ShiftChangeEscortsFlag=false, ShiftChangeAwacsRequested=false, ShiftChangeEscortsRequested=false, CAPAirwings={}, MonitoringData={}, MonitoringOn=false, FlightGroups={}, AwacsMission=nil, AwacsInZone=false, AwacsReady=false, CatchAllMissions={}, CatchAllFGs={}, PictureInterval=300, ReassignTime=120, PictureTimeStamp=0, BorderZone=nil, RejectZone=nil, maxassigndistance=100, PlayerGuidance=true, ModernEra=true, callsignshort=true, keepnumber=true, callsignTranslations=nil, TacDistance=45, MeldDistance=35, ThreatDistance=25, AOName="Rock", AOCoordinate=nil, clientmenus=nil, RadarBlur=15, ReassignmentPause=180, NoGroupTags=false, SuppressScreenOutput=false, NoMissileCalls=true, GoogleTTSPadding=1, WindowsTTSPadding=2.5, PlayerCapAssignment=true, AllowMarkers=false, PlayerStationName=nil, GCI=false, GCIGroup=nil, locale="en", IncludeHelicopters=false, TacticalMenu=false, TacticalFrequencies={}, TacticalSubscribers={}, TacticalBaseFreq=130, TacticalIncrFreq=0.5, TacticalModulation=radio.modulation.AM, TacticalInterval=120, DetectionSet=nil, MaxMissionRange=125, } AWACS.CallSignClear={ [1]="Overlord", [2]="Magic", [3]="Wizard", [4]="Focus", [5]="Darkstar", } AWACS.AnchorNames={ [1]="One", [2]="Two", [3]="Three", [4]="Four", [5]="Five", [6]="Six", [7]="Seven", [8]="Eight", [9]="Nine", [10]="Ten", } AWACS.IFF= { SPADES="Spades", NEUTRAL="Neutral", FRIENDLY="Friendly", ENEMY="Hostile", BOGEY="Bogey", } AWACS.Phonetic= { [1]='Alpha', [2]='Bravo', [3]='Charlie', [4]='Delta', [5]='Echo', [6]='Foxtrot', [7]='Golf', [8]='Hotel', [9]='India', [10]='Juliett', [11]='Kilo', [12]='Lima', [13]='Mike', [14]='November', [15]='Oscar', [16]='Papa', [17]='Quebec', [18]='Romeo', [19]='Sierra', [20]='Tango', [21]='Uniform', [22]='Victor', [23]='Whiskey', [24]='Xray', [25]='Yankee', [26]='Zulu', } AWACS.Shipsize= { [1]="Singleton", [2]="Two-Ship", [3]="Heavy", [4]="Gorilla", } AWACS.ROE={ POLICE="Police", VID="Visual ID", IFF="IFF", BVR="Beyond Visual Range", } AWACS.ROT={ BYPASSESCAPE="Bypass and Escape", EVADE="Evade Fire", PASSIVE="Passive Defense", RETURNFIRE="Return Fire", OPENFIRE="Open Fire", } AWACS.THREATLEVEL={ GREEN=3, AMBER=7, RED=10, } AWACS.CapVoices={ [1]="de-DE-Wavenet-A", [2]="de-DE-Wavenet-B", [3]="fr-FR-Wavenet-A", [4]="fr-FR-Wavenet-B", [5]="en-GB-Wavenet-A", [6]="en-GB-Wavenet-B", [7]="en-GB-Wavenet-D", [8]="en-AU-Wavenet-B", [9]="en-US-Wavenet-J", [10]="en-US-Wavenet-H", } AWACS.Messages={ EN= { DEFEND="%s, %s! %s! %s! Defend!", VECTORTO="%s, %s. Vector%s %s", VECTORTOTTS="%s, %s, Vector%s %s", ANGELS=". Angels ", ZERO="zero", VANISHED="%s, %s Group. Vanished.", VANISHEDTTS="%s, %s group vanished.", SHIFTCHANGE="%s shift change for %s control.", GROUPCAP="Group", GROUP="group", MILES="miles", THOUSAND="thousand", BOGEY="Bogey", ALLSTATIONS="All Stations", PICCLEAN="%s. %s. Picture Clean.", PICTURE="Picture", ONE="One", GROUPMULTI="groups", NOTCHECKEDIN="%s. %s. Negative. You are not checked in.", CLEAN="%s. %s. Clean.", DOPE="%s. %s. Bogey Dope. ", VIDPOS="%s. %s. Copy, target identified as %s.", VIDNEG="%s. %s. Negative, get closer to target.", FFNEUTRAL="Neutral", FFFRIEND="Friendly", FFHOSTILE="Hostile", FFSPADES="Spades", FFCLEAN="Clean", COPY="%s. %s. Copy.", TARGETEDBY="Targeted by %s.", STATUS="Status", ALREADYCHECKEDIN="%s. %s. Negative. You are already checked in.", ALPHACHECK="Alpha Check", CHECKINAI="%s. %s. Checking in as fragged. Expected playtime %d hours. Request Alpha Check %s.", SAFEFLIGHT="%s. %s. Copy. Have a safe flight home.", VERYLOW="very low", AIONSTATION="%s. %s. On station over anchor %d at angels %d. Ready for tasking.", POPUP="Pop-up", NEWGROUP="New group", HIGH=" High.", VERYFAST=" Very fast.", FAST=" Fast.", THREAT="Threat", MERGED="Merged", SCREENVID="Intercept and VID %s group.", SCREENINTER="Intercept %s group.", ENGAGETAG="Targeted by %s.", REQCOMMIT="%s. %s group. %s. %s, request commit.", AICOMMIT="%s. %s group. %s. %s, commit.", COMMIT="Commit", SUNRISE="%s. All stations, SUNRISE SUNRISE SUNRISE, %s.", AWONSTATION="%s on station for %s control.", STATIONAT="%s. %s. Station at %s at angels %d.", STATIONATLONG="%s. %s. Station at %s at angels %d doing %d knots.", STATIONSCREEN="%s. %s.\nStation at %s\nAngels %d\nSpeed %d knots\nCoord %s\nROE %s.", STATIONTASK="Station at %s\nAngels %d\nSpeed %d knots\nCoord %s\nROE %s", VECTORSTATION=" to Station", TEXTOPTIONS1="Lost friendly flight", TEXTOPTIONS2="Vanished friendly flight", TEXTOPTIONS3="Faded friendly contact", TEXTOPTIONS4="Lost contact with", }, } AWACS.TaskDescription={ ANCHOR="Anchor", REANCHOR="Re-Anchor", VID="VID", IFF="IFF", INTERCEPT="Intercept", SWEEP="Sweep", RTB="RTB", } AWACS.TaskStatus={ IDLE="Idle", UNASSIGNED="Unassigned", REQUESTED="Requested", ASSIGNED="Assigned", EXECUTING="Executing", SUCCESS="Success", FAILED="Failed", DEAD="Dead", } function AWACS:New(Name,AirWing,Coalition,AirbaseName,AwacsOrbit,OpsZone,StationZone,Frequency,Modulation) 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 AWACS!") end else self.coalition=Coalition self.coalitiontxt=string.lower(UTILS.GetCoalitionName(self.coalition)) end self.Name=Name self.AirWing=AirWing AirWing:SetUsingOpsAwacs(self) self.CAPAirwings=FIFO:New() self.CAPAirwings:Push(AirWing,1) self.AwacsFG=nil self.RadarBlur=15 if type(OpsZone)=="string"then self.OpsZone=ZONE:New(OpsZone) elseif type(OpsZone)=="table"and OpsZone.ClassName and string.find(OpsZone.ClassName,"ZONE")then self.OpsZone=OpsZone else self:E("AWACS - Invalid Zone passed!") return end self.AOCoordinate=COORDINATE:NewFromVec3(coalition.getMainRefPoint(self.coalition)) self.AOName=self.OpsZone:GetName() self.UseBullsAO=true self.ControlZoneRadius=100 self.StationZone=ZONE:New(StationZone) self.StationZoneName=StationZone self.Frequency=Frequency or 271 self.Modulation=Modulation or radio.modulation.AM self.MultiFrequency={self.Frequency} self.MultiModulation={self.Modulation} self.Airbase=AIRBASE:FindByName(AirbaseName) self.AwacsAngels=25 if AwacsOrbit then self.OrbitZone=ZONE:New(AwacsOrbit) end self.BorderZone=nil self.CallSign=CALLSIGN.AWACS.Magic self.CallSignNo=1 self.NoHelos=true self.AIRequested=0 self.AIonCAP=0 self.AICAPMissions=FIFO:New() self.FlightGroups=FIFO:New() self.Countactcounter=0 self.PictureInterval=300 self.PictureTimeStamp=0 self.ReassignTime=120 self.intelstarted=false self.sunrisedone=false local speed=250 self.SpeedBase=speed self.Speed=speed self.Heading=0 self.Leg=50 self.invisible=false self.immortal=false self.callsigntxt="AWACS" self.AwacsTimeOnStation=4 self.AwacsTimeStamp=0 self.EscortsTimeOnStation=4 self.EscortsTimeStamp=0 self.ShiftChangeTime=0.25 self.ShiftChangeAwacsFlag=false self.ShiftChangeEscortsFlag=false self.CapSpeedBase=270 self.CAPTimeOnStation=4 self.MaxAIonCAP=4 self.AICAPCAllName=CALLSIGN.Aircraft.Colt self.AICAPCAllNumber=0 self.CAPGender="male" self.CAPCulture="en-US" self.CAPVoice=nil self.AwacsMission=nil self.AwacsInZone=false self.AwacsReady=false self.AwacsROE=AWACS.ROE.IFF self.AwacsROT=AWACS.ROT.BYPASSESCAPE self.HasEscorts=false self.EscortTemplate="" self.EscortMission={} self.EscortMissionReplacement={} self.PathToSRS="C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio" self.Gender="female" self.Culture="en-GB" self.Voice=nil self.Port=5002 self.Volume=1.0 self.RadioQueue=FIFO:New() self.PrioRadioQueue=FIFO:New() self.TacticalQueue=FIFO:New() self.maxspeakentries=3 self.GoogleTTSPadding=1 self.WindowsTTSPadding=2.5 self.clientset=SET_CLIENT:New():FilterActive(true):FilterCoalitions(self.coalitiontxt):FilterCategories("plane"):FilterStart() self.PlayerGuidance=true self.ModernEra=true self.NoGroupTags=false self.SuppressScreenOutput=false self.ReassignmentPause=180 self.callsignshort=true self.DeclareRadius=5 self.MenuStrict=true self.maxassigndistance=100 self.NoMissileCalls=true self.PlayerCapAssignment=true self.ManagedGrps={} self.ManagedGrpID=0 self.callsignTranslations=nil self.AnchorStacks=FIFO:New() self.AnchorBaseAngels=22 self.AnchorStackDistance=2 self.AnchorMaxStacks=4 self.AnchorMaxAnchors=2 self.AnchorMaxZones=6 self.AnchorCurrZones=1 self.AnchorTurn=-(360/self.AnchorMaxZones) self:_CreateAnchorStack() self.ManagedTasks=FIFO:New() local MonitoringData={} MonitoringData.AICAPCurrent=0 MonitoringData.AICAPMax=self.MaxAIonCAP MonitoringData.Airwings=1 MonitoringData.PlayersCheckedin=0 MonitoringData.Players=0 MonitoringData.AwacsShiftChange=false MonitoringData.AwacsStateFG="unknown" MonitoringData.AwacsStateMission="unknown" MonitoringData.EscortsShiftChange=false MonitoringData.EscortsStateFG={} MonitoringData.EscortsStateMission={} self.MonitoringOn=false self.MonitoringData=MonitoringData self.CatchAllMissions={} self.CatchAllFGs={} self.PictureAO=FIFO:New() self.PictureEWR=FIFO:New() self.Contacts=FIFO:New() self.CID=0 self.ContactsAO=FIFO:New() self.clientmenus=FIFO:New() self.TacticalMenu=false self.TacticalBaseFreq=130 self.TacticalIncrFreq=0.5 self.TacticalModulation=radio.modulation.AM self.acticalFrequencies={} self.TacticalSubscribers={} self.TacticalInterval=120 self.DetectionSet=SET_GROUP:New() self.lid=string.format("%s (%s) | ",self.Name,self.coalition and UTILS.GetCoalitionName(self.coalition)or"unknown") self:SetStartState("Stopped") self:AddTransition("Stopped","Start","StartUp") self:AddTransition("StartUp","Started","Running") self:AddTransition("*","Status","*") self:AddTransition("*","CheckedIn","*") self:AddTransition("*","CheckedOut","*") self:AddTransition("*","AssignAnchor","*") self:AddTransition("*","AssignedAnchor","*") self:AddTransition("*","ReAnchor","*") self:AddTransition("*","NewCluster","*") self:AddTransition("*","NewContact","*") self:AddTransition("*","LostCluster","*") self:AddTransition("*","LostContact","*") self:AddTransition("*","CheckRadioQueue","*") self:AddTransition("*","CheckTacticalQueue","*") self:AddTransition("*","EscortShiftChange","*") self:AddTransition("*","AwacsShiftChange","*") self:AddTransition("*","FlightOnMission","*") self:AddTransition("*","Intercept","*") self:AddTransition("*","InterceptSuccess","*") self:AddTransition("*","InterceptFailure","*") self:AddTransition("*","VIDSuccess","*") self:AddTransition("*","VIDFailure","*") self:AddTransition("*","Stop","Stopped") local text=string.format("%sAWACS Version %s Initiated",self.lid,self.version) self:I(text) self:HandleEvent(EVENTS.PlayerEnterAircraft,self._EventHandler) self:HandleEvent(EVENTS.PlayerEnterUnit,self._EventHandler) self:HandleEvent(EVENTS.PlayerLeaveUnit,self._EventHandler) self:HandleEvent(EVENTS.Ejection,self._EventHandler) self:HandleEvent(EVENTS.Crash,self._EventHandler) self:HandleEvent(EVENTS.Dead,self._EventHandler) self:HandleEvent(EVENTS.UnitLost,self._EventHandler) self:HandleEvent(EVENTS.BDA,self._EventHandler) self:HandleEvent(EVENTS.PilotDead,self._EventHandler) self:HandleEvent(EVENTS.Shot,self._EventHandler) self:_InitLocalization() return self end function AWACS:SetTacticalRadios(BaseFreq,Increase,Modulation,Interval,Number,Provider,Speaker) self:T(self.lid.."SetTacticalRadios") if not self.AwacsSRS then MESSAGE:New("AWACS: Setup SRS in your code BEFORE trying to add tactical radios please!",30,"ERROR",true):ToLog():ToAll() return self end self.TacticalMenu=true self.TacticalBaseFreq=BaseFreq or 130 self.TacticalIncrFreq=Increase or 0.5 self.TacticalModulation=Modulation or radio.modulation.AM self.TacticalInterval=Interval or 120 local number=Number or 10 if number<1 then number=1 end if number>10 then number=10 end for i=1,number do local freq=self.TacticalBaseFreq+((i-1)*self.TacticalIncrFreq) self.TacticalFrequencies[freq]=freq end if self.AwacsSRS then self.TacticalSRS=MSRS:New(self.PathToSRS,self.TacticalBaseFreq,self.TacticalModulation,self.Backend) self.TacticalSRS:SetCoalition(self.coalition) self.TacticalSRS:SetGender(self.Gender) self.TacticalSRS:SetCulture(self.Culture) self.TacticalSRS:SetVoice(self.Voice) if Speaker then self.TacticalSRS:SetSpeakerPiper(Speaker) end self.TacticalSRS:SetPort(self.Port) self.TacticalSRS:SetLabel("AWACS") self.TacticalSRS:SetVolume(self.Volume) if self.PathToGoogleKey then self.TacticalSRS:SetProviderOptionsGoogle(self.PathToGoogleKey,self.AccessKey) self.TacticalSRS:SetProvider(MSRS.Provider.GOOGLE) end if Provider then self.TacticalSRS:SetProvider(Provider) end self.TacticalSRSQ=MSRSQUEUE:New("Tactical AWACS") end return self end function AWACS:_RefreshMenuNonSubscribed() self:T(self.lid.."_RefreshMenuNonSubscribed") local aliveset=self.clientset:GetAliveSet() for _,_group in pairs(aliveset)do local grp=_group local Group=grp:GetGroup() local gname=nil if Group and Group:IsAlive()then gname=Group:GetName() self:T(gname) end local menustr=self.clientmenus:ReadByID(gname) local menu=menustr.tactical if not self.TacticalSubscribers[gname]and menu then menu:RemoveSubMenus() for _,_freq in UTILS.spairs(self.TacticalFrequencies)do local modu=UTILS.GetModulationName(self.TacticalModulation) local text=string.format("Subscribe to %.3f %s",_freq,modu) local entry=MENU_GROUP_COMMAND:New(Group,text,menu,self._SubScribeTactRadio,self,Group,_freq) end end end return self end function AWACS:_UnsubScribeTactRadio(Group) self:T(self.lid.."_UnsubScribeTactRadio") local text="" local textScreen="" local GID,Outcome=self:_GetManagedGrpID(Group) local gcallsign=self:_GetCallSign(Group,GID)or"Ghost 1" local gname=Group:GetName()or"unknown" if Outcome and self.TacticalSubscribers[gname]then local Freq=self.TacticalSubscribers[gname] self.TacticalFrequencies[Freq]=Freq self.TacticalSubscribers[gname]=nil local modu=self.TacticalModulation==0 and"AM"or"FM" text=string.format("%s, %s, switch back to AWACS main frequency!",gcallsign,self.callsigntxt) self:_NewRadioEntry(text,text,GID,true,true,true,false,true) self:_RefreshMenuNonSubscribed() elseif self.AwacsFG then local nocheckin=self.gettext:GetEntry("NOTCHECKEDIN",self.locale) text=string.format(nocheckin,self:_GetCallSign(Group,GID)or"Ghost 1",self.callsigntxt) self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) end return self end function AWACS:_SubScribeTactRadio(Group,Frequency) self:T(self.lid.."_SubScribeTactRadio") local text="" local textScreen="" local GID,Outcome=self:_GetManagedGrpID(Group) local gcallsign=self:_GetCallSign(Group,GID)or"Ghost 1" local gname=Group:GetName()or"unknown" if Outcome then self.TacticalSubscribers[gname]=Frequency self.TacticalFrequencies[Frequency]=nil local modu=self.TacticalModulation==0 and"AM"or"FM" text=string.format("%s, %s, switch to %.3f %s for tactical information!",gcallsign,self.callsigntxt,Frequency,modu) self:_NewRadioEntry(text,text,GID,true,true,true,false,true) local menustr=self.clientmenus:ReadByID(gname) local menu=menustr.tactical if menu then menu:RemoveSubMenus() local text=string.format("Unsubscribe %.3f %s",Frequency,modu) local entry=MENU_GROUP_COMMAND:New(Group,text,menu,self._UnsubScribeTactRadio,self,Group) end elseif self.AwacsFG then local nocheckin=self.gettext:GetEntry("NOTCHECKEDIN",self.locale) text=string.format(nocheckin,self:_GetCallSign(Group,GID)or"Ghost 1",self.callsigntxt) self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) end return self end function AWACS:_CheckSubscribers() self:T(self.lid.."_InitLocalization") for _name,_freq in pairs(self.TacticalSubscribers or{})do local grp=GROUP:FindByName(_name) if(not grp)or(not grp:IsAlive())then self.TacticalFrequencies[_freq]=_freq self.TacticalSubscribers[_name]=nil end end return self end function AWACS:_InitLocalization() self:T(self.lid.."_InitLocalization") self.gettext=TEXTANDSOUND:New("AWACS","en") self.locale="en" for locale,table in pairs(self.Messages)do local Locale=string.lower(tostring(locale)) self:T("**** Adding locale: "..Locale) for ID,Text in pairs(table)do self:T(string.format('Adding ID %s',tostring(ID))) self.gettext:AddEntry(Locale,tostring(ID),Text) end end return self end function AWACS:SetLocale(Locale) self:T(self.lid.."SetLocale") self.locale=Locale or"en" return self end function AWACS:SetBullsCoordinate(Coordinate) self:T(self.lid.."SetBullsCoordinate") self.AOCoordinate=Coordinate return self end function AWACS:SetMaxMissionRange(NM) self.MaxMissionRange=NM or 125 return self end function AWACS:AddFrequencyAndModulation(Frequency,Modulation) self:T(self.lid.."AddFrequencyAndModulation") table.insert(self.MultiFrequency,Frequency) table.insert(self.MultiModulation,Modulation) if self.AwacsSRS then self.AwacsSRS:SetFrequencies(self.MultiFrequency) self.AwacsSRS:SetModulations(self.MultiModulation) end return self end function AWACS:SetAsGCI(EWR,Delay) self:T(self.lid.."SetGCI") local delay=Delay or-5 if type(EWR)=="string"then self.GCIGroup=GROUP:FindByName(EWR) else self.GCIGroup=EWR end self.GCI=true self:SetEscort(0) return self end function AWACS:_NewRadioEntry(TextTTS,TextScreen,GID,IsGroup,ToScreen,IsNew,FromAI,IsPrio,Tactical) self:T(self.lid.."_NewRadioEntry") local RadioEntry={} RadioEntry.IsNew=IsNew RadioEntry.TextTTS=TextTTS RadioEntry.TextScreen=TextScreen or TextTTS RadioEntry.GroupID=GID RadioEntry.ToScreen=ToScreen RadioEntry.Duration=MSRS.getSpeechTime(TextTTS,0.95,false)or 8 RadioEntry.FromAI=FromAI RadioEntry.IsGroup=IsGroup if Tactical then self.TacticalQueue:Push(RadioEntry) elseif IsPrio then self.PrioRadioQueue:Push(RadioEntry) else self.RadioQueue:Push(RadioEntry) end return self end function AWACS:SetBullsEyeAlias(Name) self:T(self.lid.."_SetBullsEyeAlias") self.AOName=Name or"Rock" return self end function AWACS:SetTOS(AICHours,CapHours) self:T(self.lid.."SetTOS") self.AwacsTimeOnStation=AICHours or 4 self.CAPTimeOnStation=CapHours or 4 return self end function AWACS:SetReassignmentPause(Seconds) self.ReassignmentPause=Seconds or 180 return self end function AWACS:SuppressScreenMessages(Switch) self:T(self.lid.."_SetBullsEyeAlias") self.SuppressScreenOutput=Switch or false return self end function AWACS:ZipLip() self:T(self.lid.."ZipLip") self:SuppressScreenMessages(true) self.PlayerGuidance=false self.callsignshort=true self.NoMissileCalls=true return self end function AWACS:SetCustomCallsigns(translationTable) self.callsignTranslations=translationTable end function AWACS:_GetGIDFromGroupOrName(Group) self:T(self.lid.."_GetGIDFromGroupOrName") self:T({Group}) local GID=0 local Outcome=false local CallSign="Ghost 1" local nametocheck=CallSign if Group and type(Group)=="string"then nametocheck=Group elseif Group and Group:IsInstanceOf("GROUP")then nametocheck=Group:GetName() else return false,0,CallSign end local managedgrps=self.ManagedGrps or{} for _,_managed in pairs(managedgrps)do local managed=_managed if managed.GroupName==nametocheck then GID=managed.GID Outcome=true CallSign=managed.CallSign end end self:T({Outcome,GID,CallSign}) return Outcome,GID,CallSign end function AWACS:_EventHandler(EventData) self:T(self.lid.."_EventHandler") self:T({Event=EventData.id}) local Event=EventData if Event.id==EVENTS.PlayerEnterAircraft or Event.id==EVENTS.PlayerEnterUnit then if Event.IniCoalition==self.coalition then self:_SetClientMenus() end end if Event.id==EVENTS.PlayerLeaveUnit and Event.IniGroupName then self:T("Player group left unit: "..Event.IniGroupName) self:T("Player name left: "..Event.IniPlayerName) self:T("Coalition = "..UTILS.GetCoalitionName(Event.IniCoalition)) if Event.IniCoalition==self.coalition then local Outcome,GID,CallSign=self:_GetGIDFromGroupOrName(Event.IniGroupName) if Outcome and GID>0 then self:T("Task Abort and Checkout Called") self:_TaskAbort(Event.IniGroupName) self:_CheckOut(nil,GID,true) end end end if Event.id==EVENTS.Ejection or Event.id==EVENTS.Crash or Event.id==EVENTS.Dead or Event.id==EVENTS.PilotDead then if Event.IniCoalition==self.coalition then local Outcome,GID,CallSign=self:_GetGIDFromGroupOrName(Event.IniGroupName) if Outcome and GID>0 then self:_TaskAbort(Event.IniGroupName) self:_CheckOut(nil,GID,true) end end end if Event.id==EVENTS.Shot and self.PlayerGuidance and not self.NoMissileCalls then if Event.IniCoalition~=self.coalition then self:T("Shot from: "..Event.IniGroupName) local position=Event.IniGroup:GetCoordinate() if not position then return self end local Category=Event.WeaponCategory local WeaponDesc=EventData.Weapon:getDesc() self:T({WeaponDesc}) if WeaponDesc.category==1 and(WeaponDesc.missileCategory==1 or WeaponDesc.missileCategory==2)then self:T("AAM or SAM Missile fired") local warndist=25 local Type="SAM" if WeaponDesc.category==1 then Type="Missile" local guidance=WeaponDesc.guidance or 4 if guidance==2 then warndist=10 elseif guidance==3 then warndist=25 elseif guidance==4 then warndist=15 elseif guidance==5 then warndist=10 end end self:_MissileWarning(position,Type,warndist) end end end return self end function AWACS:_MissileWarning(Coordinate,Type,Warndist) self:T(self.lid.."_MissileWarning Type="..Type.." WarnDist="..Warndist) if not Coordinate then return self end local shotzone=ZONE_RADIUS:New("WarningZone",Coordinate:GetVec2(),UTILS.NMToMeters(Warndist)) local targetgrpset=SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterCategoryAirplane():FilterActive():FilterZones({shotzone}):FilterOnce() if targetgrpset:Count()>0 then local targets=targetgrpset:GetSetObjects() for _,_grp in pairs(targets)do if _grp and _grp:IsAlive()then local isPlayer=_grp:IsPlayer() if isPlayer then local callsign=self:_GetCallSign(_grp) local defend=self.gettext:GetEntry("DEFEND",self.locale) local text=string.format(defend,callsign,Type,Type,Type) self:_NewRadioEntry(text,text,0,false,self.debug,true,false,true) end end end end return self end function AWACS:SetRadarBlur(Percent) local percent=Percent or 15 if percent<0 then percent=0 end if percent>100 then percent=100 end self.RadarBlur=Percent return self end function AWACS:SetColdWar() self.ModernEra=false self.AwacsROT=AWACS.ROT.PASSIVE self.AwacsROE=AWACS.ROE.VID self.RadarBlur=25 self:SetInterceptTimeline(35,25,15) return self end function AWACS:SetModernEra() self.ModernEra=true self.AwacsROT=AWACS.ROT.EVADE self.AwacsROE=AWACS.ROE.BVR self.RadarBlur=15 return self end function AWACS:SetModernEraDefensive() self.ModernEra=true self.AwacsROT=AWACS.ROT.EVADE self.AwacsROE=AWACS.ROE.IFF self.RadarBlur=15 return self end function AWACS:SetModernEraAggressive() self.ModernEra=true self.AwacsROT=AWACS.ROT.RETURNFIRE self.AwacsROE=AWACS.ROE.BVR self.RadarBlur=15 return self end function AWACS:SetPolicingModern() self.ModernEra=true self.AwacsROT=AWACS.ROT.BYPASSESCAPE self.AwacsROE=AWACS.ROE.VID self.RadarBlur=15 return self end function AWACS:SetPolicingColdWar() self.ModernEra=false self.AwacsROT=AWACS.ROT.BYPASSESCAPE self.AwacsROE=AWACS.ROE.VID self.RadarBlur=25 self:SetInterceptTimeline(35,25,15) return self end function AWACS:SetPlayerGuidance(Switch) if(Switch==nil)or(Switch==true)then self.PlayerGuidance=true else self.PlayerGuidance=false end return self end function AWACS:GetName() return self.Name or"not set" end function AWACS:SetInterceptTimeline(TacDistance,MeldDistance,ThreatDistance) self.TacDistance=TacDistance or 45 self.MeldDistance=MeldDistance or 35 self.ThreatDistance=ThreatDistance or 25 return self end function AWACS:SetAdditionalZone(Zone,Draw) self:T(self.lid.."SetAdditionalZone") self.BorderZone=Zone if self.debug then Zone:DrawZone(self.coalition,{1,0.64,0},1,{1,0.64,0},0.2,1,true) if self.AllowMarkers then MARKER:New(Zone:GetCoordinate(),"Defensive Zone"):ToCoalition(self.coalition) end elseif Draw then Zone:DrawZone(self.coalition,{1,0.64,0},1,{1,0.64,0},0.2,1,true) end return self end function AWACS:SetRejectionZone(Zone,Draw) self:T(self.lid.."SetRejectionZone") self.RejectZone=Zone if Draw then Zone:DrawZone(self.coalition,{1,0.64,0},1,{1,0.64,0},0.2,1,true) elseif self.debug then Zone:DrawZone(self.coalition,{1,0.64,0},1,{1,0.64,0},0.2,1,true) if self.AllowMarkers then MARKER:New(Zone:GetCoordinate(),"Rejection Zone"):ToCoalition(self.coalition) end end return self end function AWACS:SetCorridorZones(CorridorZones) self:T(self.lid.."SetCorridorZones") if CorridorZones and CorridorZones:IsInstanceOf("SET_ZONE")then self.corridorzones=CorridorZones self.usecorridors=true elseif CorridorZones and CorridorZones:IsInstanceOf("ZONE_BASE")then if not self.corridorzones then self.corridorzones=SET_ZONE:New()end self.corridorzones:AddZone(CorridorZones) self.usecorridors=true end return self end function AWACS:AddCorridorZone(CorridorZone) self:T(self.lid.."AddCorridorZone") self:SetCorridorZones(CorridorZone) return self end function AWACS:SetCorridorZoneFloorAndCeiling(Floor,Ceiling) self.corridorfloor=UTILS.FeetToMeters(Floor) self.corridorceiling=UTILS.FeetToMeters(Ceiling) return self end function AWACS:SetCorridorZoneFloorAndCeilingMeters(Floor,Ceiling) self.corridorfloor=Floor self.corridorceiling=Ceiling return self end function AWACS:DrawFEZ() self.OpsZone:DrawZone(self.coalition,{1,0,0},1,{1,0,0},0.2,5,true) return self end function AWACS:SetAwacsDetails(CallSign,CallSignNo,Angels,Speed,Heading,Leg) self:T(self.lid.."SetAwacsDetails") self.CallSign=CallSign or CALLSIGN.AWACS.Magic self.CallSignNo=CallSignNo or 1 self.AwacsAngels=Angels or 25 local speed=Speed or 250 self.SpeedBase=speed self.Speed=speed self.Heading=Heading or 0 self.Leg=Leg or 25 return self end function AWACS:SetCustomAWACSCallSign(CallsignTable) self:T(self.lid.."SetCustomAWACSCallSign") self.CallSignClear=CallsignTable return self end function AWACS:AddGroupToDetection(Group) self:T(self.lid.."AddGroupToDetection") if Group and Group.ClassName and Group.ClassName=="GROUP"then self.DetectionSet:AddGroup(Group) elseif Group and Group.ClassName and Group.ClassName=="SET_GROUP"then self.DetectionSet:AddSet(Group) end return self end function AWACS:SetSRS(PathToSRS,Gender,Culture,Port,Voice,Volume,PathToGoogleKey,AccessKey,Backend,Provider,Speaker) self:T(self.lid.."SetSRS") self.PathToSRS=PathToSRS or MSRS.path or"C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio" self.Gender=Gender or MSRS.gender or"male" self.Culture=Culture or MSRS.culture or"en-US" self.Port=Port or MSRS.port or 5002 self.Voice=Voice or MSRS.voice self.PathToGoogleKey=PathToGoogleKey self.AccessKey=AccessKey self.Volume=Volume or 1.0 self.Backend=Backend or MSRS.backend self.Provider=Provider BASE:I({backend=self.Backend}) self.AwacsSRS=MSRS:New(self.PathToSRS,self.MultiFrequency,self.MultiModulation,self.Backend) self.AwacsSRS:SetCoalition(self.coalition) self.AwacsSRS:SetGender(self.Gender) self.AwacsSRS:SetCulture(self.Culture) self.AwacsSRS:SetPort(self.Port) self.AwacsSRS:SetLabel("AWACS") self.AwacsSRS:SetVolume(Volume) if Speaker then self.AwacsSRS:SetSpeakerPiper(Speaker) end if self.PathToGoogleKey then self.AwacsSRS:SetProviderOptionsGoogle(self.PathToGoogleKey,self.AccessKey) self.AwacsSRS:SetProvider(MSRS.Provider.GOOGLE) end if Provider then self.AwacsSRS:SetProvider(Provider) end if(not PathToGoogleKey)and self.AwacsSRS:GetProvider()==MSRS.Provider.GOOGLE then self.PathToGoogleKey=MSRS.poptions.gcloud.credentials self.Voice=Voice or MSRS.poptions.gcloud.voice self.AccessKey=AccessKey or MSRS.poptions.gcloud.key end self.AwacsSRS:SetVoice(self.Voice) return self end function AWACS:SetSRSVoiceCAP(Gender,Culture,Voice,Speaker) self:T(self.lid.."SetSRSVoiceCAP") self.CAPGender=Gender or"male" self.CAPCulture=Culture or"en-US" self.CAPVoice=Voice or"en-GB-Standard-B" self.CAPSpeaker=Speaker return self end function AWACS:SetAICAPDetails(Callsign,MaxAICap,TOS,Speed) self:T(self.lid.."SetAICAPDetails") self.CapSpeedBase=Speed or 270 self.CAPTimeOnStation=TOS or 4 self.MaxAIonCAP=MaxAICap or 4 self.AICAPCAllName=Callsign or CALLSIGN.Aircraft.Colt return self end function AWACS:SetEscort(EscortNumber,Formation,OffsetVector,EscortEngageMaxDistance) self:T(self.lid.."SetEscort") if EscortNumber and EscortNumber>0 then self.HasEscorts=true self.EscortNumber=EscortNumber else self.HasEscorts=false self.EscortNumber=0 end self.EscortFormation=Formation self.OffsetVec=OffsetVector or{x=500,y=100,z=500} self.EscortEngageMaxDistance=EscortEngageMaxDistance or 45 return self end function AWACS:_MessageVector(GID,Tag,Coordinate,Angels) self:T(self.lid.."_MessageVector") local managedgroup=self.ManagedGrps[GID] local Tag=Tag or"" if managedgroup and Coordinate then local tocallsign=managedgroup.CallSign or"Ghost 1" local group=managedgroup.Group local groupposition=group:GetCoordinate() local BRtext,BRtextTTS=self:_ToStringBR(groupposition,Coordinate) local vector=self.gettext:GetEntry("VECTORTO",self.locale) local vectortts=self.gettext:GetEntry("VECTORTOTTS",self.locale) local angelstxt=self.gettext:GetEntry("ANGELS",self.locale) local text=string.format(vectortts,tocallsign,self.callsigntxt,Tag,BRtextTTS) local textScreen=string.format(vector,tocallsign,self.callsigntxt,Tag,BRtext) if Angels then text=text..angelstxt..tostring(Angels).."." textScreen=textScreen..angelstxt..tostring(Angels).."." end self:_NewRadioEntry(text,textScreen,0,false,self.debug,true,false) end return self end function AWACS:_StartEscorts(Shiftchange) self:T(self.lid.."_StartEscorts") local AwacsFG=self.AwacsFG local group=AwacsFG:GetGroup() local timeonstation=(self.EscortsTimeOnStation+self.ShiftChangeTime)*3600 local OffsetX=500 local OffsetY=500 local OffsetZ=500 if self.OffsetVec then OffsetX=self.OffsetVec.x or 500 OffsetY=self.OffsetVec.y or 500 OffsetZ=self.OffsetVec.z or 500 end for i=1,self.EscortNumber do local escort=AUFTRAG:NewESCORT(group,{x=OffsetX*((i+(i%2))/2),y=OffsetY*((i+(i%2))/2),z=(OffsetZ+OffsetZ*((i+(i%2))/2))*(-1)^i},self.EscortEngageMaxDistance,{"Air"}) escort:SetTime(nil,timeonstation) if self.Escortformation then escort:SetFormation(self.Escortformation) end escort:SetMissionRange(self.MaxMissionRange) self.AirWing:AddMission(escort) self.CatchAllMissions[#self.CatchAllMissions+1]=escort if Shiftchange then self.EscortMissionReplacement[i]=escort else self.EscortMission[i]=escort end end return self end function AWACS:_StartSettings(FlightGroup,Mission) self:T(self.lid.."_StartSettings") local Mission=Mission local AwacsFG=FlightGroup if self.AwacsMission:GetName()==Mission:GetName()then self:T("Setting up Awacs") AwacsFG:SetDefaultRadio(self.Frequency,self.Modulation,false) AwacsFG:SwitchRadio(self.Frequency,self.Modulation) AwacsFG:SetDefaultAltitude(self.AwacsAngels*1000) AwacsFG:SetHomebase(self.Airbase) AwacsFG:SetDefaultCallsign(self.CallSign,self.CallSignNo) AwacsFG:SetDefaultROE(ENUMS.ROE.WeaponHold) AwacsFG:SetDefaultAlarmstate(AI.Option.Ground.val.ALARM_STATE.GREEN) AwacsFG:SetDefaultEPLRS(self.ModernEra) AwacsFG:SetDespawnAfterLanding() AwacsFG:SetFuelLowRTB(true) AwacsFG:SetFuelLowThreshold(20) local group=AwacsFG:GetGroup() group:SetCommandInvisible(self.invisible) group:SetCommandImmortal(self.immortal) group:CommandSetCallsign(self.CallSign,self.CallSignNo,2) group:CommandEPLRS(self.ModernEra,5) self.AwacsFG=AwacsFG self.callsigntxt=string.format("%s",self.CallSignClear[self.CallSign]) self:__CheckRadioQueue(10) if self.HasEscorts then self:_StartEscorts() end self.AwacsTimeStamp=timer.getTime() self.EscortsTimeStamp=timer.getTime() self.PictureTimeStamp=timer.getTime()+10*60 self.AwacsReady=true self:Started() elseif self.ShiftChangeAwacsRequested and self.AwacsMissionReplacement and self.AwacsMissionReplacement:GetName()==Mission:GetName()then self:T("Setting up Awacs Replacement") AwacsFG:SetDefaultRadio(self.Frequency,self.Modulation,false) AwacsFG:SwitchRadio(self.Frequency,self.Modulation) AwacsFG:SetDefaultAltitude(self.AwacsAngels*1000) AwacsFG:SetHomebase(self.Airbase) self.CallSignNo=self.CallSignNo+1 AwacsFG:SetDefaultCallsign(self.CallSign,self.CallSignNo) AwacsFG:SetDefaultROE(ENUMS.ROE.WeaponHold) AwacsFG:SetDefaultAlarmstate(AI.Option.Ground.val.ALARM_STATE.GREEN) AwacsFG:SetDefaultEPLRS(self.ModernEra) AwacsFG:SetDespawnAfterLanding() AwacsFG:SetFuelLowRTB(true) AwacsFG:SetFuelLowThreshold(20) local group=AwacsFG:GetGroup() group:SetCommandInvisible(self.invisible) group:SetCommandImmortal(self.immortal) group:CommandSetCallsign(self.CallSign,self.CallSignNo,2) self.callsigntxt=string.format("%s",self.CallSignClear[self.CallSign]) local shifting=self.gettext:GetEntry("SHIFTCHANGE",self.locale) local text=string.format(shifting,self.callsigntxt,self.AOName or"Rock") self:T(self.lid..text) AwacsFG:RadioTransmission(text,1,false) self.AwacsFG=AwacsFG if self.HasEscorts then self:_StartEscorts(true) end self.AwacsTimeStamp=timer.getTime() self.EscortsTimeStamp=timer.getTime() self.AwacsReady=true end return self end function AWACS:_ToStringBULLS(Coordinate,ssml,TTS) self:T(self.lid.."_ToStringBULLS") local bullseyename=self.AOName or"Rock" local BullsCoordinate=self.AOCoordinate local DirectionVec3=BullsCoordinate:GetDirectionVec3(Coordinate) local AngleRadians=Coordinate:GetAngleRadians(DirectionVec3) local Distance=Coordinate:Get2DDistance(BullsCoordinate) local AngleDegrees=UTILS.Round(UTILS.ToDegree(AngleRadians),0) local Bearing=string.format('%03d',AngleDegrees) local Distance=UTILS.Round(UTILS.MetersToNM(Distance),0) if ssml then return string.format("%s %03d, %d",bullseyename,Bearing,Distance) end if TTS then Bearing=self:_ToStringBullsTTS(Bearing) local zero=self.gettext:GetEntry("ZERO",self.locale) local BearingTTS=string.gsub(Bearing,"0",zero) return string.format("%s %s, %d",bullseyename,BearingTTS,Distance) else return string.format("%s %s, %d",bullseyename,Bearing,Distance) end end function AWACS:_ToStringBullsTTS(Text) local text=Text text=string.gsub(text,"Bullseye","Bulls eye") text=string.gsub(text,"%d","%1 ") text=string.gsub(text," ,",".") text=string.gsub(text," $","") return text end function AWACS:_GetManagedGrpID(Group) if not Group or not Group:IsAlive()then self:T(self.lid.."_GetManagedGrpID - Requested Group is not alive!") return 0,false,"" end self:T(self.lid.."_GetManagedGrpID for "..Group:GetName()) local GID=0 local Outcome=false local CallSign="Ghost 1" local nametocheck=Group:GetName() local managedgrps=self.ManagedGrps or{} for _,_managed in pairs(managedgrps)do local managed=_managed if managed.GroupName==nametocheck then GID=managed.GID Outcome=true CallSign=managed.CallSign end end return GID,Outcome,CallSign end function AWACS:_GetCallSign(Group,GID,IsPlayer) self:T(self.lid.."_GetCallSign - GID "..tostring(GID)) if GID and type(GID)=="number"and GID>0 then local managedgroup=self.ManagedGrps[GID] self:T("Saved Callsign for TTS = "..tostring(managedgroup.CallSign)) return managedgroup.CallSign end local callsign="Ghost 1" if Group and Group:IsAlive()then callsign=Group:GetCustomCallSign(self.callsignshort,self.keepnumber,self.callsignTranslations,self.callsignCustomFunc,self.callsignCustomArgs) end return callsign end function AWACS:SetCallSignOptions(ShortCallsign,Keepnumber,CallsignTranslations,CallsignCustomFunc,...) if not ShortCallsign or ShortCallsign==false then self.callsignshort=false else self.callsignshort=true end self.keepnumber=Keepnumber or false self.callsignTranslations=CallsignTranslations self.callsignCustomFunc=CallsignCustomFunc self.callsignCustomArgs=arg or{} return self end function AWACS:_UpdateContactFromCluster(CID) self:T(self.lid.."_UpdateContactFromCluster CID="..CID) local existingcontact=self.Contacts:PullByID(CID) local ContactTable=existingcontact.Cluster.Contacts or{} local function GetFirstAliveContact(table) for _,_contact in pairs(table)do local contact=_contact if contact and contact.group and contact.group:IsAlive()then return contact end end return nil end local NewContact=GetFirstAliveContact(ContactTable) if NewContact then existingcontact.Contact=NewContact self.Contacts:Push(existingcontact,existingcontact.CID) end return self end function AWACS:_CheckMerges() self:T(self.lid.."_CheckMerges") for _id,_pilot in pairs(self.ManagedGrps)do local pilot=_pilot if pilot.Group and pilot.Group:IsAlive()then local ppos=pilot.Group:GetCoordinate() local pcallsign=pilot.CallSign self:T(self.lid.."Checking for "..pcallsign) if ppos then self.Contacts:ForEach( function(Contact) local contact=Contact local cpos=contact.Cluster.coordinate or contact.Contact.position or contact.Contact.group:GetCoordinate() local dist=ppos:Get2DDistance(cpos) local distnm=UTILS.Round(UTILS.MetersToNM(dist),0) if(pilot.IsPlayer or self.debug)and distnm<=5 then self:T(self.lid.."Merged") self:_MergedCall(_id) end if(pilot.IsPlayer or self.debug)and distnm>5 and distnm<=self.ThreatDistance then self:_ThreatRangeCall(_id,Contact) end if(pilot.IsPlayer or self.debug)and distnm>self.ThreatDistance and distnm<=self.MeldDistance then self:_MeldRangeCall(_id,Contact) end if(pilot.IsPlayer or self.debug)and distnm>self.MeldDistance and distnm<=self.TacDistance then self:_TACRangeCall(_id,Contact) end end ) end end end return self end function AWACS:_CleanUpContacts() self:T(self.lid.."_CleanUpContacts") if self.Contacts:Count()>0 then local deadcontacts=FIFO:New() self.Contacts:ForEach( function(Contact) local contact=Contact if not contact.Contact.group:IsAlive()or contact.Target:IsDead()or contact.Target:IsDestroyed()or contact.Target:CountTargets()==0 then deadcontacts:Push(contact,contact.CID) self:T("DEAD contact CID="..contact.CID) end end ) if deadcontacts:Count()>0 and(not self.NoGroupTags)then self:T("DEAD count="..deadcontacts:Count()) deadcontacts:ForEach( function(Contact) local contact=Contact local vanished=self.gettext:GetEntry("VANISHED",self.locale) local vanishedtts=self.gettext:GetEntry("VANISHEDTTS",self.locale) local text=string.format(vanishedtts,self.callsigntxt,contact.TargetGroupNaming) local textScreen=string.format(vanished,self.callsigntxt,contact.TargetGroupNaming) self:_NewRadioEntry(text,textScreen,0,false,self.debug,true,false,true) self.Contacts:PullByID(contact.CID) end ) end if self.Contacts:Count()>0 then self.Contacts:ForEach( function(Contact) local contact=Contact self:_UpdateContactFromCluster(contact.CID) end ) end deadcontacts:Clear() end return self end function AWACS:_GetIdlePilots() self:T(self.lid.."_GetIdlePilots") local AIPilots={} local HumanPilots={} for _name,_entry in pairs(self.ManagedGrps)do local entry=_entry self:T("Looking at entry "..entry.GID.." Name "..entry.GroupName) local managedtask=self:_ReadAssignedTaskFromGID(entry.GID) local overridetask=false if managedtask then self:T("Current task = "..(managedtask.ToDo or"Unknown")) if managedtask.ToDo==AWACS.TaskDescription.ANCHOR then overridetask=true end end if entry.IsAI then if entry.FlightGroup:IsAirborne()and((not entry.HasAssignedTask)or overridetask)then self:T("Adding AI with Callsign: "..entry.CallSign) AIPilots[#AIPilots+1]=_entry end elseif entry.IsPlayer and(not entry.Blocked)and(not entry.Group:IsHelicopter())then if(not entry.HasAssignedTask)or overridetask then local TNow=timer.getTime() if entry.LastTasking and(TNow-entry.LastTasking>self.ReassignTime)then self:T("Adding Human with Callsign: "..entry.CallSign) HumanPilots[#HumanPilots+1]=_entry end end end end return AIPilots,HumanPilots end function AWACS:_TargetSelectionProcess(Untargeted) self:T(self.lid.."_TargetSelectionProcess") local maxtargets=3 local contactstable=self.Contacts:GetDataTable() local targettable=FIFO:New() local sortedtargets=FIFO:New() local prefiltered=FIFO:New() local HaveTargets=false self:T(self.lid.."Initial count: "..self.Contacts:Count()) if Untargeted then self.Contacts:ForEach( function(Contact) local contact=Contact if contact.Contact.group:IsAlive()and(contact.Status==AWACS.TaskStatus.IDLE or contact.Status==AWACS.TaskStatus.UNASSIGNED)then if self.AwacsROE==AWACS.ROE.POLICE or self.AwacsROE==AWACS.ROE.VID then if not(contact.IFF==AWACS.IFF.FRIENDLY or contact.IFF==AWACS.IFF.NEUTRAL)then prefiltered:Push(contact,contact.CID) end else prefiltered:Push(contact,contact.CID) end end end ) contactstable=prefiltered:GetDataTable() self:T(self.lid.."Untargeted: "..prefiltered:Count()) end for _,_contact in pairs(contactstable)do local contact=_contact local checked=false local contactname=contact.TargetGroupNaming or"ZETA" local typename=contact.ReportingName or"Unknown" self:T(self.lid..string.format("Looking at group %s type %s",contactname,typename)) local contactcoord=contact.Cluster.coordinate or contact.Contact.position or contact.Contact.group:GetCoordinate() local contactvec2=contactcoord:GetVec2() if self.RejectZone then local isinrejzone=self.RejectZone:IsVec2InZone(contactvec2) if isinrejzone then self:T(self.lid.."Across Border = YES - ignore") checked=true end end if not self.GCI then local HVTCoordinate=self.OrbitZone:GetCoordinate() local distance=UTILS.NMToMeters(200) if contactcoord then distance=HVTCoordinate:Get2DDistance(contactcoord) end self:T(self.lid.."HVT Distance = "..UTILS.Round(UTILS.MetersToNM(distance),0)) if UTILS.MetersToNM(distance)<=45 and not checked then self:T(self.lid.."In HVT Distance = YES") targettable:Push(contact,distance) checked=true end end local isinopszone=self.OpsZone:IsVec2InZone(contactvec2) local distance=self.OpsZone:Get2DDistance(contactcoord) if isinopszone and not checked then self:T(self.lid.."In FEZ = YES") targettable:Push(contact,distance) checked=true end local isinopszone=self.ControlZone:IsVec2InZone(contactvec2) if isinopszone and not checked then self:T(self.lid.."In Radar Zone = YES") local distance=self.AOCoordinate:Get2DDistance(contactcoord) local AOdist=UTILS.Round(UTILS.MetersToNM(distance),0) if not contactcoord.Heading then contactcoord.Heading=self.intel:CalcClusterDirection(contact.Cluster) end local aspect=contactcoord:ToStringAspect(self.ControlZone:GetCoordinate()) local sizing=contact.Cluster.size or self.intel:ClusterCountUnits(contact.Cluster)or 1 sizing=math.fmod((sizing*0.1),1) local AOdist2=(AOdist/2)*sizing AOdist2=UTILS.Round((AOdist/2)+((AOdist/2)-AOdist2),0) self:T(self.lid.."Aspect = "..aspect.." | Size = "..sizing) if(AOdist2<75)or(aspect=="Hot")then local text=string.format("In AO(Adj) dist = %d(%d) NM",AOdist,AOdist2) self:T(self.lid..text) targettable:Push(contact,distance) checked=true end end if self.BorderZone then local isinborderzone=self.BorderZone:IsVec2InZone(contactvec2) if isinborderzone and not checked then self:T(self.lid.."In BorderZone = YES") targettable:Push(contact,distance) checked=true end end end self:T(self.lid.."Post filter count: "..targettable:Count()) if targettable:Count()>maxtargets then local targets=targettable:GetSortedDataTable() targettable:Clear() for i=1,maxtargets do targettable:Push(targets[i]) end end sortedtargets:Clear() prefiltered:Clear() if targettable:Count()>0 then HaveTargets=true end return HaveTargets,targettable end function AWACS:_CreatePicture(AO,Callsign,GID,MaxEntries,IsGeneral) self:T(self.lid.."_CreatePicture AO="..tostring(AO).." for "..Callsign.." GID "..GID) local managedgroup=nil local group=nil local groupcoord=nil if not IsGeneral then managedgroup=self.ManagedGrps[GID] group=managedgroup.Group groupcoord=group:GetCoordinate() end local fifo=self.PictureAO local maxentries=self.maxspeakentries or 3 if MaxEntries and MaxEntries>0 and MaxEntries<=3 then maxentries=MaxEntries end local counter=0 if not AO then end local entries=fifo:GetSize() if entries1 then text=text.." "..threatsizetext.."." textScreen=textScreen.." "..threatsizetext.."." end if contact.EngagementTag then text=text.." "..contact.EngagementTag textScreen=textScreen.." "..contact.EngagementTag end local RadioEntry_IsGroup=false local RadioEntry_ToScreen=self.debug if managedgroup and not IsGeneral then RadioEntry_IsGroup=managedgroup.IsPlayer RadioEntry_ToScreen=managedgroup.IsPlayer end self:_NewRadioEntry(text,textScreen,GID,RadioEntry_IsGroup,RadioEntry_ToScreen,true,false) end end fifo:Clear() return self end function AWACS:_CreateBogeyDope(Callsign,GID,Tactical) self:T(self.lid.."_CreateBogeyDope for "..Callsign.." GID "..GID) local managedgroup=self.ManagedGrps[GID] local group=managedgroup.Group local groupcoord=group:GetCoordinate() local fifo=self.ContactsAO local maxentries=1 local counter=0 local entries=fifo:GetSize() if entries0 then local IDstack=self.PictureEWR:GetSortedDataTable() local weneed=3-clustersAO self:T(string.format("Picture - adding %d/%d contacts from EWR",weneed,clustersEWR)) if weneed>clustersEWR then weneed=clustersEWR end for i=1,weneed do self.PictureAO:Push(IDstack[i]) end end clustersAO=self.PictureAO:GetSize() if clustersAO==0 and clustersEWR==0 then local picclean=self.gettext:GetEntry("PICCLEAN",self.locale) text=string.format(picclean,gcallsign,self.callsigntxt) textScreen=text self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) else if clustersAO>0 then local picture=self.gettext:GetEntry("PICTURE",self.locale) text=string.format("%s, %s. %s. ",gcallsign,self.callsigntxt,picture) textScreen=string.format("%s, %s. %s. ",gcallsign,self.callsigntxt,picture) local onetxt=self.gettext:GetEntry("ONE",self.locale) local grptxt=self.gettext:GetEntry("GROUP",self.locale) local groupstxt=self.gettext:GetEntry("GROUPMULTI",self.locale) if clustersAO==1 then text=string.format("%s%s %s. ",text,onetxt,grptxt) textScreen=string.format("%s%s %s.\n",textScreen,onetxt,grptxt) else text=string.format("%s%d %s. ",text,clustersAO,groupstxt) textScreen=string.format("%s%d %s.\n",textScreen,clustersAO,groupstxt) end self:_NewRadioEntry(text,textScreen,GID,Outcome,true,true,false) self:_CreatePicture(true,gcallsign,GID,3,general) self.PictureAO:Clear() self.PictureEWR:Clear() end end elseif self.AwacsFG then local nocheckin=self.gettext:GetEntry("NOTCHECKEDIN",self.locale) text=string.format(nocheckin,gcallsign,self.callsigntxt) self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) end return self end function AWACS:_BogeyDope(Group,Tactical) self:T(self.lid.."_BogeyDope") local text="" local textScreen="" local GID,Outcome=self:_GetManagedGrpID(Group) local gcallsign=self:_GetCallSign(Group,GID)or"Ghost 1" if not self.intel then local clean=self.gettext:GetEntry("CLEAN",self.locale) text=string.format(clean,self:_GetCallSign(Group,GID)or"Ghost 1",self.callsigntxt) self:_NewRadioEntry(text,text,0,false,true,true,false,true,Tactical) return self end if Outcome then local managedgroup=self.ManagedGrps[GID] local pilotgroup=managedgroup.Group local pilotcoord=managedgroup.Group:GetCoordinate() local contactstable=self.Contacts:GetDataTable() for _,_contact in pairs(contactstable)do local managedcontact=_contact local contactposition=managedcontact.Cluster.coordinate or managedcontact.Contact.position local coordVec2=contactposition:GetVec2() local dist=pilotcoord:Get2DDistance(contactposition) if self.ControlZone:IsVec2InZone(coordVec2)then self.ContactsAO:Push(managedcontact,dist) elseif self.BorderZone and self.BorderZone:IsVec2InZone(coordVec2)then self.ContactsAO:Push(managedcontact,dist) else if self.OrbitZone then local distance=contactposition:Get2DDistance(self.OrbitZone:GetCoordinate()) if(distance<=UTILS.NMToMeters(45))then self.ContactsAO:Push(managedcontact,distance) end end end end local contactsAO=self.ContactsAO:GetSize() if contactsAO==0 then local clean=self.gettext:GetEntry("CLEAN",self.locale) text=string.format(clean,self:_GetCallSign(Group,GID)or"Ghost 1",self.callsigntxt) self:_NewRadioEntry(text,text,GID,Outcome,Outcome,true,false,true,Tactical) else if contactsAO>0 then local dope=self.gettext:GetEntry("DOPE",self.locale) text=string.format(dope,self:_GetCallSign(Group,GID)or"Ghost 1",self.callsigntxt) textScreen=string.format(dope,self:_GetCallSign(Group,GID)or"Ghost 1",self.callsigntxt) local onetxt=self.gettext:GetEntry("ONE",self.locale) local grptxt=self.gettext:GetEntry("GROUP",self.locale) local groupstxt=self.gettext:GetEntry("GROUPMULTI",self.locale) if contactsAO==1 then text=string.format("%s%s %s. ",text,onetxt,grptxt) textScreen=string.format("%s%s %s.\n",textScreen,onetxt,grptxt) else text=string.format("%s%d %s. ",text,contactsAO,groupstxt) textScreen=string.format("%s%d %s.\n",textScreen,contactsAO,groupstxt) end self:_NewRadioEntry(text,textScreen,GID,Outcome,true,true,false,true,Tactical) self:_CreateBogeyDope(self:_GetCallSign(Group,GID)or"Ghost 1",GID,Tactical) end end elseif self.AwacsFG then local nocheckin=self.gettext:GetEntry("NOTCHECKEDIN",self.locale) text=string.format(nocheckin,self:_GetCallSign(Group,GID)or"Ghost 1",self.callsigntxt) self:_NewRadioEntry(text,text,GID,Outcome,true,true,false,Tactical) end return self end function AWACS:_ShowAwacsInfo(Group) self:T(self.lid.."_ShowAwacsInfo") local report=REPORT:New("Info") local STN=self.STN report:Add("====================") report:Add(string.format("AWACS %s",self.callsigntxt)) report:Add(string.format("Radio: %.3f %s",self.Frequency,UTILS.GetModulationName(self.Modulation))) if STN then report:Add(string.format("Link-16 STN: %s",STN)) end report:Add(string.format("Bulls Alias: %s",self.AOName)) report:Add(string.format("Coordinate: %s",self.AOCoordinate:ToStringLLDDM())) report:Add("====================") report:Add(string.format("Assignment Distance: %d NM",self.maxassigndistance)) report:Add(string.format("TAC Distance: %d NM",self.TacDistance)) report:Add(string.format("MELD Distance: %d NM",self.MeldDistance)) report:Add(string.format("THREAT Distance: %d NM",self.ThreatDistance)) report:Add("====================") report:Add(string.format("ROE/ROT: %s, %s",self.AwacsROE,self.AwacsROT)) MESSAGE:New(report:Text(),45,"AWACS"):ToGroup(Group) return self end function AWACS:_VID(Group,Declaration) self:T(self.lid.."_VID") local GID,Outcome,Callsign=self:_GetManagedGrpID(Group) local text="" local TextTTS="" if Outcome then local managedgroup=self.ManagedGrps[GID] local group=managedgroup.Group local position=group:GetCoordinate() local radius=UTILS.NMToMeters(self.DeclareRadius)or UTILS.NMToMeters(5) local TID=managedgroup.CurrentTask or 0 if TID>0 then local task=self.ManagedTasks:ReadByID(TID) if task.ToDo~=AWACS.TaskDescription.VID then return self end if task.Status~=AWACS.TaskStatus.ASSIGNED then return self end local CID=task.Cluster.CID local cluster=self.Contacts:ReadByID(CID) if cluster then local gposition=cluster.Contact.group:GetCoordinate() local cposition=gposition or cluster.Cluster.coordinate or cluster.Contact.position local distance=cposition:Get2DDistance(position) distance=UTILS.Round(distance,0)+1 if distance<=radius or self.debug then self:T("Contact VID as "..Declaration) cluster.IFF=Declaration task.Status=AWACS.TaskStatus.SUCCESS self.ManagedTasks:PullByID(TID) self.ManagedTasks:Push(task,TID) self.Contacts:PullByID(CID) self.Contacts:Push(cluster,CID) local vidpos=self.gettext:GetEntry("VIDPOS",self.locale) text=string.format(vidpos,Callsign,self.callsigntxt,Declaration) self:T(text) self:__VIDSuccess(3,GID,group,cluster) else self:T("Contact VID not close enough") local vidneg=self.gettext:GetEntry("VIDNEG",self.locale) text=string.format(vidneg,Callsign,self.callsigntxt) self:T(text) self:__VIDFailure(3,GID,group,cluster) end self:_NewRadioEntry(text,text,GID,Outcome,true,true,false,true) end end elseif self.AwacsFG then local nocheckin=self.gettext:GetEntry("NOTCHECKEDIN",self.locale) text=string.format(nocheckin,self:_GetCallSign(Group,GID)or"Ghost 1",self.callsigntxt) self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) end return self end function AWACS:_Declare(Group) self:T(self.lid.."_Declare") local GID,Outcome,Callsign=self:_GetManagedGrpID(Group) local text="" local TextTTS="" if Outcome then local managedgroup=self.ManagedGrps[GID] local group=managedgroup.Group local position=group:GetCoordinate() local radius=UTILS.NMToMeters(self.DeclareRadius)or UTILS.NMToMeters(5) local groupzone=ZONE_GROUP:New(group:GetName(),group,radius) local Coalitions={"red","neutral"} if self.coalition==coalition.side.NEUTRAL then Coalitions={"red","blue"} elseif self.coalition==coalition.side.RED then Coalitions={"blue","neutral"} end local contactset=SET_GROUP:New():FilterCategoryAirplane():FilterCoalitions(Coalitions):FilterZones({groupzone}):FilterOnce() local numbercontacts=contactset:CountAlive()or 0 local foundcontacts={} if numbercontacts>0 then contactset:ForEach( function(airpl) local distance=position:Get2DDistance(airpl:GetCoordinate()) distance=UTILS.Round(distance,0)+1 foundcontacts[distance]=airpl end ,{} ) for _dist,_contact in UTILS.spairs(foundcontacts)do local distanz=_dist local contact=_contact local ccoalition=contact:GetCoalition() local ctypename=contact:GetTypeName() local ffneutral=self.gettext:GetEntry("FFNEUTRAL",self.locale) local fffriend=self.gettext:GetEntry("FFFRIEND",self.locale) local ffhostile=self.gettext:GetEntry("FFHOSTILE",self.locale) local ffspades=self.gettext:GetEntry("FFSPADES",self.locale) local friendorfoe=ffneutral if self.self.ModernEra then if ccoalition==self.coalition then friendorfoe=fffriend elseif ccoalition==coalition.side.NEUTRAL then friendorfoe=ffneutral elseif ccoalition~=self.coalition then friendorfoe=ffhostile end else friendorfoe=ffspades end self:T(string.format("Distance %d ContactName %s Coalition %d (%s) TypeName %s",distanz,contact:GetName(),ccoalition,friendorfoe,ctypename)) text=string.format("%s. %s. %s.",Callsign,self.callsigntxt,friendorfoe) TextTTS=text if self.ModernEra then text=string.format("%s %s.",text,ctypename) end break end else local ffclean=self.gettext:GetEntry("FFCLEAN",self.locale) text=string.format("%s. %s. %s.",Callsign,self.callsigntxt,ffclean) TextTTS=text end self:_NewRadioEntry(TextTTS,text,GID,Outcome,true,true,false,true) elseif self.AwacsFG then local nocheckin=self.gettext:GetEntry("NOTCHECKEDIN",self.locale) text=string.format(nocheckin,self:_GetCallSign(Group,GID)or"Ghost 1",self.callsigntxt) self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) end return self end function AWACS:_Commit(Group) self:T(self.lid.."_Commit") local GID,Outcome=self:_GetManagedGrpID(Group) local text="" if Outcome then local Pilot=self.ManagedGrps[GID] local currtaskid=Pilot.CurrentTask local managedtask=self.ManagedTasks:ReadByID(currtaskid) self:T(string.format("TID %d(%d) | ToDo %s | Status %s",currtaskid,managedtask.TID,managedtask.ToDo,managedtask.Status)) if managedtask then if managedtask.Status==AWACS.TaskStatus.REQUESTED then managedtask=self.ManagedTasks:PullByID(currtaskid) managedtask.Status=AWACS.TaskStatus.ASSIGNED self.ManagedTasks:Push(managedtask,currtaskid) self:T(string.format("COMMITTED - TID %d(%d) for GID %d | ToDo %s | Status %s",currtaskid,GID,managedtask.TID,managedtask.ToDo,managedtask.Status)) Pilot.HasAssignedTask=true Pilot.CurrentTask=currtaskid self.ManagedGrps[GID]=Pilot local copy=self.gettext:GetEntry("COPY",self.locale) local targetedby=self.gettext:GetEntry("TARGETEDBY",self.locale) text=string.format(copy,self:_GetCallSign(Group,GID)or"Ghost 1",self.callsigntxt) local EngagementTag=string.format(targetedby,Pilot.CallSign) self:_UpdateContactEngagementTag(Pilot.ContactCID,EngagementTag,false,false,AWACS.TaskStatus.ASSIGNED) self:_NewRadioEntry(text,text,GID,Outcome,true,true,false,true) else self:E(self.lid.."Cannot find REQUESTED managed task with TID="..currtaskid.." for GID="..GID) end else self:E(self.lid.."Cannot find managed task with TID="..currtaskid.." for GID="..GID) end elseif self.AwacsFG then local nocheckin=self.gettext:GetEntry("NOTCHECKEDIN",self.locale) text=string.format(nocheckin,self:_GetCallSign(Group,GID)or"Ghost 1",self.callsigntxt) self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) end return self end function AWACS:_Judy(Group) self:T(self.lid.."_Judy") local GID,Outcome=self:_GetManagedGrpID(Group) local text="" if Outcome then local Pilot=self.ManagedGrps[GID] local currtaskid=Pilot.CurrentTask local managedtask=self.ManagedTasks:ReadByID(currtaskid) if managedtask then if managedtask.Status==AWACS.TaskStatus.REQUESTED or managedtask.Status==AWACS.TaskStatus.UNASSIGNED then managedtask=self.ManagedTasks:PullByID(currtaskid) managedtask.Status=AWACS.TaskStatus.ASSIGNED self.ManagedTasks:Push(managedtask,currtaskid) local copy=self.gettext:GetEntry("COPY",self.locale) local targetedby=self.gettext:GetEntry("TARGETEDBY",self.locale) text=string.format(copy,self:_GetCallSign(Group,GID)or"Ghost 1",self.callsigntxt) local EngagementTag=string.format(targetedby,Pilot.CallSign) self:_UpdateContactEngagementTag(Pilot.ContactCID,EngagementTag,false,false,AWACS.TaskStatus.ASSIGNED) self:_NewRadioEntry(text,text,GID,Outcome,true,true,false,true) else self:E(self.lid.."Cannot find REQUESTED or UNASSIGNED managed task with TID="..currtaskid.." for GID="..GID) end else self:E(self.lid.."Cannot find managed task with TID="..currtaskid.." for GID="..GID) end elseif self.AwacsFG then local nocheckin=self.gettext:GetEntry("NOTCHECKEDIN",self.locale) text=string.format(nocheckin,self:_GetCallSign(Group,GID)or"Ghost 1",self.callsigntxt) self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) end return self end function AWACS:_Unable(Group) self:T(self.lid.."_Unable") local GID,Outcome=self:_GetManagedGrpID(Group) local text="" if Outcome then local Pilot=self.ManagedGrps[GID] local currtaskid=Pilot.CurrentTask local managedtask=self.ManagedTasks:ReadByID(currtaskid) self:T(string.format("UNABLE for TID %d(%d) | ToDo %s | Status %s",currtaskid,managedtask.TID,managedtask.ToDo,managedtask.Status)) if managedtask then if managedtask.Status==AWACS.TaskStatus.REQUESTED then managedtask=self.ManagedTasks:PullByID(currtaskid) managedtask.IsUnassigned=true managedtask.Status=AWACS.TaskStatus.FAILED self.ManagedTasks:Push(managedtask,currtaskid) self:T(string.format("REJECTED - TID %d(%d) for GID %d | ToDo %s | Status %s",currtaskid,GID,managedtask.TID,managedtask.ToDo,managedtask.Status)) Pilot.HasAssignedTask=false Pilot.CurrentTask=0 Pilot.LastTasking=timer.getTime() self.ManagedGrps[GID]=Pilot local copy=self.gettext:GetEntry("COPY",self.locale) text=string.format(copy,self:_GetCallSign(Group,GID)or"Ghost 1",self.callsigntxt) local EngagementTag="" self:_UpdateContactEngagementTag(Pilot.ContactCID,EngagementTag,false,false,AWACS.TaskStatus.UNASSIGNED) self:_NewRadioEntry(text,text,GID,Outcome,true,true,false,true) else self:E(self.lid.."Cannot find REQUESTED managed task with TID="..currtaskid.." for GID="..GID) end else self:E(self.lid.."Cannot find managed task with TID="..currtaskid.." for GID="..GID) end elseif self.AwacsFG then local nocheckin=self.gettext:GetEntry("NOTCHECKEDIN",self.locale) text=string.format(nocheckin,self:_GetCallSign(Group,GID)or"Ghost 1",self.callsigntxt) self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) end return self end function AWACS:_TaskAbort(Group) self:T(self.lid.."_TaskAbort") local Outcome,GID=self:_GetGIDFromGroupOrName(Group) local text="" if Outcome then local Pilot=self.ManagedGrps[GID] self:T({Pilot}) local currtaskid=Pilot.CurrentTask local managedtask=self.ManagedTasks:ReadByID(currtaskid) if managedtask then self:T(string.format("ABORT for TID %d(%d) | ToDo %s | Status %s",currtaskid,managedtask.TID,managedtask.ToDo,managedtask.Status)) if managedtask.Status==AWACS.TaskStatus.ASSIGNED then managedtask=self.ManagedTasks:PullByID(currtaskid) managedtask.Status=AWACS.TaskStatus.FAILED managedtask.IsUnassigned=true self.ManagedTasks:Push(managedtask,currtaskid) self:T(string.format("ABORTED - TID %d(%d) for GID %d | ToDo %s | Status %s",currtaskid,GID,managedtask.TID,managedtask.ToDo,managedtask.Status)) Pilot.HasAssignedTask=false Pilot.CurrentTask=0 Pilot.LastTasking=timer.getTime() self.ManagedGrps[GID]=Pilot local copy=self.gettext:GetEntry("COPY",self.locale) text=string.format(copy,self:_GetCallSign(Group,GID)or"Ghost 1",self.callsigntxt) local EngagementTag="" self:_UpdateContactEngagementTag(Pilot.ContactCID,EngagementTag,false,false,AWACS.TaskStatus.UNASSIGNED) self:_NewRadioEntry(text,text,GID,Outcome,true,true,false,true) else self:E(self.lid.."Cannot find ASSIGNED managed task with TID="..currtaskid.." for GID="..GID) end else self:E(self.lid.."Cannot find managed task with TID="..currtaskid.." for GID="..GID) end elseif self.AwacsFG then local nocheckin=self.gettext:GetEntry("NOTCHECKEDIN",self.locale) text=string.format(nocheckin,self:_GetCallSign(Group,GID)or"Ghost 1",self.callsigntxt) self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) end return self end function AWACS:_Showtask(Group) self:T(self.lid.."_Showtask") local GID,Outcome,Callsign=self:_GetManagedGrpID(Group) local text="" if Outcome then local managedgroup=self.ManagedGrps[GID] if managedgroup.IsPlayer then if managedgroup.CurrentTask>0 and self.ManagedTasks:HasUniqueID(managedgroup.CurrentTask)then local currenttask=self.ManagedTasks:ReadByID(managedgroup.CurrentTask) if currenttask then local status=currenttask.Status local targettype=currenttask.Target:GetCategory() local targetstatus=currenttask.Target:GetState() local ToDo=currenttask.ToDo local description=currenttask.ScreenText local descTTS=currenttask.ScreenText local callsign=Callsign if self.debug then local taskreport=REPORT:New("AWACS Tasking Display") taskreport:Add("===============") taskreport:Add(string.format("Task for Callsign: %s",Callsign)) taskreport:Add(string.format("Task: %s with Status: %s",ToDo,status)) taskreport:Add(string.format("Target of Type: %s",targettype)) taskreport:Add(string.format("Target in State: %s",targetstatus)) taskreport:Add("===============") self:I(taskreport:Text()) end local pposition=managedgroup.Group:GetCoordinate()or managedgroup.LastKnownPosition if currenttask.ToDo==AWACS.TaskDescription.INTERCEPT or currenttask.ToDo==AWACS.TaskDescription.VID then local targetpos=currenttask.Target:GetCoordinate() if pposition and targetpos then local alti=currenttask.Cluster.altitude or currenttask.Contact.altitude or currenttask.Contact.group:GetAltitude() local direction,direcTTS=self:_ToStringBRA(pposition,targetpos,alti) description=description.."\nBRA "..direction descTTS=descTTS..";BRA "..direcTTS end elseif currenttask.ToDo==AWACS.TaskDescription.ANCHOR or currenttask.ToDo==AWACS.TaskDescription.REANCHOR then local targetpos=currenttask.Target:GetCoordinate() local direction,direcTTS=self:_ToStringBR(pposition,targetpos) description=description.."\nBR "..direction descTTS=descTTS..";BR "..direcTTS end local statustxt=self.gettext:GetEntry("STATUS",self.locale) local text=string.format("%s\n%s %s",description,statustxt,status) local ttstext=string.format("%s. %s. %s",managedgroup.CallSign,self.callsigntxt,descTTS) ttstext=string.gsub(ttstext,"\\n",";") ttstext=string.gsub(ttstext,"VID","V I D") self:_NewRadioEntry(ttstext,text,GID,true,true,false,false,true) end end end elseif self.AwacsFG then local nocheckin=self.gettext:GetEntry("NOTCHECKEDIN",self.locale) text=string.format(nocheckin,self:_GetCallSign(Group,GID)or"Ghost 1",self.callsigntxt) self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) end return self end function AWACS:_CheckIn(Group) self:T(self.lid.."_CheckIn "..Group:GetName()) local GID,Outcome=self:_GetManagedGrpID(Group) local text="" local textTTS="" if not Outcome then self.ManagedGrpID=self.ManagedGrpID+1 local managedgroup={} managedgroup.Group=Group managedgroup.GroupName=Group:GetName() managedgroup.IsPlayer=true managedgroup.IsAI=false managedgroup.CallSign=self:_GetCallSign(Group,GID,true)or"Ghost 1" managedgroup.CurrentAuftrag=0 managedgroup.CurrentTask=0 managedgroup.HasAssignedTask=true managedgroup.Blocked=true managedgroup.GID=self.ManagedGrpID managedgroup.LastKnownPosition=Group:GetCoordinate() managedgroup.LastTasking=timer.getTime() GID=managedgroup.GID self.ManagedGrps[self.ManagedGrpID]=managedgroup local alphacheckbulls=self:_ToStringBULLS(Group:GetCoordinate()) local alphacheckbullstts=self:_ToStringBULLS(Group:GetCoordinate(),false,true) local alpha=self.gettext:GetEntry("ALPHACHECK",self.locale) text=string.format("%s. %s. %s. %s",managedgroup.CallSign,self.callsigntxt,alpha,alphacheckbulls) textTTS=string.format("%s. %s. %s. %s",managedgroup.CallSign,self.callsigntxt,alpha,alphacheckbullstts) self:__CheckedIn(1,managedgroup.GID) if self.PlayerStationName then self:__AssignAnchor(5,managedgroup.GID,true,self.PlayerStationName) else self:__AssignAnchor(5,managedgroup.GID) end elseif self.AwacsFG then local nocheckin=self.gettext:GetEntry("ALREADYCHECKEDIN",self.locale) text=string.format(nocheckin,self:_GetCallSign(Group,GID)or"Ghost 1",self.callsigntxt) textTTS=text end self:_NewRadioEntry(textTTS,text,GID,Outcome,true,true,false) return self end function AWACS:_CheckInAI(FlightGroup,Group,AuftragsNr) self:T(self.lid.."_CheckInAI "..Group:GetName().." to Auftrag Nr "..AuftragsNr) local GID,Outcome=self:_GetManagedGrpID(Group) local text="" if not Outcome then self.ManagedGrpID=self.ManagedGrpID+1 local managedgroup={} managedgroup.Group=Group managedgroup.GroupName=Group:GetName() managedgroup.FlightGroup=FlightGroup managedgroup.IsPlayer=false managedgroup.IsAI=true local callsignstring=UTILS.GetCallsignName(self.AICAPCAllName) if self.callsignTranslations and self.callsignTranslations[callsignstring]then callsignstring=self.callsignTranslations[callsignstring] end local callsignmajor=math.fmod(self.AICAPCAllNumber,9) local callsign=string.format("%s %d 1",callsignstring,callsignmajor) if self.callsignshort then callsign=string.format("%s %d",callsignstring,callsignmajor) end self:T("Assigned Callsign: "..callsign) managedgroup.CallSign=callsign managedgroup.CurrentAuftrag=AuftragsNr managedgroup.HasAssignedTask=false managedgroup.GID=self.ManagedGrpID managedgroup.LastKnownPosition=Group:GetCoordinate() self.ManagedGrps[self.ManagedGrpID]=managedgroup FlightGroup:SetDefaultRadio(self.Frequency,self.Modulation,false) FlightGroup:SwitchRadio(self.Frequency,self.Modulation) local CAPVoice=self.CAPVoice if self.PathToGoogleKey then CAPVoice=self.CapVoices[math.floor(math.random(1,10))] end FlightGroup:SetSRS(self.PathToSRS,self.CAPGender,self.CAPCulture,CAPVoice,self.Port,self.PathToGoogleKey,"FLIGHT",1,self.Provider) if self.Backend then FlightGroup.srs:SetBackend(self.Backend) end if self.CAPSpeaker then FlightGroup.srs:SetSpeakerPiper(self.CAPSpeaker) end local checkai=self.gettext:GetEntry("CHECKINAI",self.locale) text=string.format(checkai,self.callsigntxt,managedgroup.CallSign,self.CAPTimeOnStation,self.AOName) self:_NewRadioEntry(text,text,managedgroup.GID,Outcome,false,true,true) local alphacheckbulls=self:_ToStringBULLS(Group:GetCoordinate(),false,true) local alpha=self.gettext:GetEntry("ALPHACHECK",self.locale) text=string.format("%s. %s. %s. %s",managedgroup.CallSign,self.callsigntxt,alpha,alphacheckbulls) self:__CheckedIn(1,managedgroup.GID) local AW=FlightGroup.legion if AW.HasOwnStation then self:__AssignAnchor(5,managedgroup.GID,AW.HasOwnStation,AW.StationName) else self:__AssignAnchor(5,managedgroup.GID) end else local nocheckin=self.gettext:GetEntry("ALREADYCHECKEDIN",self.locale) text=string.format(nocheckin,self:_GetCallSign(Group,GID)or"Ghost 1",self.callsigntxt) end self:_NewRadioEntry(text,text,GID,Outcome,false,true,false) return self end function AWACS:_CheckOut(Group,GID,dead) self:T(self.lid.."_CheckOut") local GID,Outcome=self:_GetManagedGrpID(Group) local text="" if Outcome then local safeflight=self.gettext:GetEntry("SAFEFLIGHT",self.locale) text=string.format(safeflight,self:_GetCallSign(Group,GID)or"Ghost 1",self.callsigntxt) self:T(text) local managedgroup=self.ManagedGrps[GID] local Stack=managedgroup.AnchorStackNo local Angels=managedgroup.AnchorStackAngels local GroupName=managedgroup.GroupName if managedgroup.IsPlayer then if self.clientmenus:HasUniqueID(GroupName)then local menus=self.clientmenus:PullByID(GroupName) menus.basemenu:Remove() if self.TacticalSubscribers[GroupName]then local Freq=self.TacticalSubscribers[GroupName] self.TacticalFrequencies[Freq]=Freq self.TacticalSubscribers[GroupName]=nil end end end if managedgroup.CurrentTask and managedgroup.CurrentTask>0 then self.ManagedTasks:PullByID(managedgroup.CurrentTask) self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",false,false) end self.ManagedGrps[GID]=nil self:__CheckedOut(1,GID,Stack,Angels) else if not dead then local nocheckin=self.gettext:GetEntry("NOTCHECKEDIN",self.locale) text=string.format(nocheckin,self:_GetCallSign(Group,GID)or"Ghost 1",self.callsigntxt) end end if not dead then self:_NewRadioEntry(text,text,GID,Outcome,false,true,false) end return self end function AWACS:_SetClientMenus() self:T(self.lid.."_SetClientMenus") local clientset=self.clientset local aliveset=clientset:GetSetObjects()or{} local clientcount=0 local clientcheckedin=0 for _,_group in pairs(aliveset)do local grp=_group local cgrp=grp:GetGroup() local cgrpname=nil if cgrp and cgrp:IsAlive()then cgrpname=cgrp:GetName() self:T(cgrpname) end if self.MenuStrict then if cgrp and cgrp:IsAlive()then clientcount=clientcount+1 local GID,checkedin=self:_GetManagedGrpID(cgrp) if checkedin then clientcheckedin=clientcheckedin+1 local hasclientmenu=self.clientmenus:ReadByID(cgrpname) local basemenu=hasclientmenu.basemenu if hasclientmenu and(not hasclientmenu.menuset)then self:T(self.lid.."Setting Menus for "..cgrpname) basemenu:RemoveSubMenus() local bogeydope=MENU_GROUP_COMMAND:New(cgrp,"Bogey Dope",basemenu,self._BogeyDope,self,cgrp) local picture=MENU_GROUP_COMMAND:New(cgrp,"Picture",basemenu,self._Picture,self,cgrp) local declare=MENU_GROUP_COMMAND:New(cgrp,"Declare",basemenu,self._Declare,self,cgrp) local tasking=MENU_GROUP:New(cgrp,"Tasking",basemenu) local showtask=MENU_GROUP_COMMAND:New(cgrp,"Showtask",tasking,self._Showtask,self,cgrp) local commit local unable local abort if self.PlayerCapAssignment then commit=MENU_GROUP_COMMAND:New(cgrp,"Commit",tasking,self._Commit,self,cgrp) unable=MENU_GROUP_COMMAND:New(cgrp,"Unable",tasking,self._Unable,self,cgrp) abort=MENU_GROUP_COMMAND:New(cgrp,"Abort",tasking,self._TaskAbort,self,cgrp) end if self.AwacsROE==AWACS.ROE.POLICE or self.AwacsROE==AWACS.ROE.VID then local vid=MENU_GROUP:New(cgrp,"VID as",tasking) local hostile=MENU_GROUP_COMMAND:New(cgrp,"Hostile",vid,self._VID,self,cgrp,AWACS.IFF.ENEMY) local neutral=MENU_GROUP_COMMAND:New(cgrp,"Neutral",vid,self._VID,self,cgrp,AWACS.IFF.NEUTRAL) local friendly=MENU_GROUP_COMMAND:New(cgrp,"Friendly",vid,self._VID,self,cgrp,AWACS.IFF.FRIENDLY) end local tactical if self.TacticalMenu then tactical=MENU_GROUP:New(cgrp,"Tactical Radio",basemenu) if self.TacticalSubscribers[cgrpname]then local entry=MENU_GROUP_COMMAND:New(cgrp,"Unsubscribe",tactical,self._UnsubScribeTactRadio,self,cgrp) else for _,_freq in UTILS.spairs(self.TacticalFrequencies)do local modu=UTILS.GetModulationName(self.TacticalModulation) local text=string.format("Subscribe to %.3f %s",_freq,modu) local entry=MENU_GROUP_COMMAND:New(cgrp,text,tactical,self._SubScribeTactRadio,self,cgrp,_freq) end end end local ainfo=MENU_GROUP_COMMAND:New(cgrp,"Awacs Info",basemenu,self._ShowAwacsInfo,self,cgrp) local checkout=MENU_GROUP_COMMAND:New(cgrp,"Check Out",basemenu,self._CheckOut,self,cgrp) local menus={ groupname=cgrpname, menuset=true, basemenu=basemenu, checkout=checkout, picture=picture, bogeydope=bogeydope, declare=declare, tasking=tasking, showtask=showtask, unable=unable, abort=abort, commit=commit, tactical=tactical, } self.clientmenus:PullByID(cgrpname) self.clientmenus:Push(menus,cgrpname) end elseif not self.clientmenus:HasUniqueID(cgrpname)then local basemenu=MENU_GROUP:New(cgrp,self.Name,nil) local checkin=MENU_GROUP_COMMAND:New(cgrp,"Check In",basemenu,self._CheckIn,self,cgrp) checkin:SetTag(cgrp:GetName()) basemenu:Refresh() local menus={ groupname=cgrpname, menuset=false, basemenu=basemenu, checkin=checkin, } self.clientmenus:Push(menus,cgrpname) local GID,hasentry=self:_GetManagedGrpID(cgrp) if hasentry then self:_CheckOut(cgrp,GID,true) end end end else if cgrp and cgrp:IsAlive()and not self.clientmenus:HasUniqueID(cgrpname)then local basemenu=MENU_GROUP:New(cgrp,self.Name,nil) local picture=MENU_GROUP_COMMAND:New(cgrp,"Picture",basemenu,self._Picture,self,cgrp) local bogeydope=MENU_GROUP_COMMAND:New(cgrp,"Bogey Dope",basemenu,self._BogeyDope,self,cgrp) local declare=MENU_GROUP_COMMAND:New(cgrp,"Declare",basemenu,self._Declare,self,cgrp) local tasking=MENU_GROUP:New(cgrp,"Tasking",basemenu) local showtask=MENU_GROUP_COMMAND:New(cgrp,"Showtask",tasking,self._Showtask,self,cgrp) local commit=MENU_GROUP_COMMAND:New(cgrp,"Commit",tasking,self._Commit,self,cgrp) local unable=MENU_GROUP_COMMAND:New(cgrp,"Unable",tasking,self._Unable,self,cgrp) local abort=MENU_GROUP_COMMAND:New(cgrp,"Abort",tasking,self._TaskAbort,self,cgrp) if self.AwacsROE==AWACS.ROE.POLICE or self.AwacsROE==AWACS.ROE.VID then local vid=MENU_GROUP:New(cgrp,"VID as",tasking) local hostile=MENU_GROUP_COMMAND:New(cgrp,"Hostile",vid,self._VID,self,cgrp,AWACS.IFF.ENEMY) local neutral=MENU_GROUP_COMMAND:New(cgrp,"Neutral",vid,self._VID,self,cgrp,AWACS.IFF.NEUTRAL) local friendly=MENU_GROUP_COMMAND:New(cgrp,"Friendly",vid,self._VID,self,cgrp,AWACS.IFF.FRIENDLY) end local ainfo=MENU_GROUP_COMMAND:New(cgrp,"Awacs Info",basemenu,self._ShowAwacsInfo,self,cgrp) local checkin=MENU_GROUP_COMMAND:New(cgrp,"Check In",basemenu,self._CheckIn,self,cgrp) local checkout=MENU_GROUP_COMMAND:New(cgrp,"Check Out",basemenu,self._CheckOut,self,cgrp) basemenu:Refresh() local menus={ groupname=cgrpname, menuset=true, basemenu=basemenu, checkin=checkin, checkout=checkout, picture=picture, bogeydope=bogeydope, declare=declare, showtask=showtask, tasking=tasking, unable=unable, abort=abort, commit=commit, } self.clientmenus:Push(menus,cgrpname) end end end self.MonitoringData.Players=clientcount or 0 self.MonitoringData.PlayersCheckedin=clientcheckedin or 0 return self end function AWACS:_DeleteAnchorStackFromMarker(Name,Coord) self:T(self.lid.."_DeleteAnchorStackFromMarker") if self.AnchorStacks:HasUniqueID(Name)and self.PlayerStationName==Name then local stack=self.AnchorStacks:ReadByID(Name) local marker=stack.AnchorMarker if stack.AnchorAssignedID:Count()==0 then marker:Remove() if self.debug then stack.StationZone:UndrawZone() end self.AnchorStacks:PullByID(Name) self.PlayerStationName=nil else if self.debug then self:I(self.lid.."**** Cannot delete station, there are CAPs assigned!") local text=marker:GetText() marker:TextUpdate(text.."\nMarked for deletion") end end end return self end function AWACS:_MoveAnchorStackFromMarker(Name,Coord) self:T(self.lid.."_MoveAnchorStackFromMarker") if self.AnchorStacks:HasUniqueID(Name)and self.PlayerStationName==Name then local station=self.AnchorStacks:PullByID(Name) local stationtag=string.format("Station: %s\nCoordinate: %s",Name,Coord:ToStringLLDDM()) local marker=station.AnchorMarker local zone=station.StationZone if self.debug then zone:UndrawZone() end local radius=self.StationZone:GetRadius() if radius<10000 then radius=10000 end station.StationZone=ZONE_RADIUS:New(Name,Coord:GetVec2(),radius) marker:UpdateCoordinate(Coord) marker:UpdateText(stationtag) station.AnchorMarker=marker if self.debug then station.StationZone:DrawZone(self.coalition,{0,0,1},1,{0,0,1},0.2,5,true) end self.AnchorStacks:Push(station,Name) end return self end function AWACS:_CreateAnchorStackFromMarker(Name,Coord) self:T(self.lid.."_CreateAnchorStackFromMarker") local AnchorStackOne={} AnchorStackOne.AnchorBaseAngels=self.AnchorBaseAngels AnchorStackOne.Anchors=FIFO:New() AnchorStackOne.AnchorAssignedID=FIFO:New() local newname=Name for i=1,self.AnchorMaxStacks do AnchorStackOne.Anchors:Push((i-1)*self.AnchorStackDistance+self.AnchorBaseAngels) end local radius=self.StationZone:GetRadius() if radius<10000 then radius=10000 end AnchorStackOne.StationZone=ZONE_RADIUS:New(newname,Coord:GetVec2(),radius) AnchorStackOne.StationZoneCoordinate=Coord AnchorStackOne.StationZoneCoordinateText=Coord:ToStringLLDDM() AnchorStackOne.StationName=newname if self.debug then AnchorStackOne.StationZone:DrawZone(self.coalition,{0,0,1},1,{0,0,1},0.2,5,true) local stationtag=string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) if self.AllowMarkers then AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToCoalition(self.coalition) end else local stationtag=string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) if self.AllowMarkers then AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToCoalition(self.coalition) end end self.AnchorStacks:Push(AnchorStackOne,newname) self.PlayerStationName=newname return self end function AWACS:_CreateAnchorStack() self:T(self.lid.."_CreateAnchorStack") local stackscreated=self.AnchorStacks:GetSize() if stackscreated==self.AnchorMaxAnchors then return false,0 end local AnchorStackOne={} AnchorStackOne.AnchorBaseAngels=self.AnchorBaseAngels AnchorStackOne.Anchors=FIFO:New() AnchorStackOne.AnchorAssignedID=FIFO:New() local newname=self.StationZone:GetName() for i=1,self.AnchorMaxStacks do AnchorStackOne.Anchors:Push((i-1)*self.AnchorStackDistance+self.AnchorBaseAngels) end if stackscreated==0 then local newsubname=AWACS.AnchorNames[stackscreated+1]or tostring(stackscreated+1) newname=self.StationZone:GetName().."-"..newsubname AnchorStackOne.StationZone=self.StationZone AnchorStackOne.StationZoneCoordinate=self.StationZone:GetCoordinate() AnchorStackOne.StationZoneCoordinateText=self.StationZone:GetCoordinate():ToStringLLDDM() AnchorStackOne.StationName=newname if self.debug then AnchorStackOne.StationZone:DrawZone(self.coalition,{0,0,1},1,{0,0,1},0.2,5,true) local stationtag=string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) if self.AllowMarkers then AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToCoalition(self.coalition) end else local stationtag=string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) if self.AllowMarkers then AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToCoalition(self.coalition) end end self.AnchorStacks:Push(AnchorStackOne,newname) else local newsubname=AWACS.AnchorNames[stackscreated+1]or tostring(stackscreated+1) newname=self.StationZone:GetName().."-"..newsubname local anchorbasecoord=self.OpsZone:GetCoordinate() local anchorradius=anchorbasecoord:Get2DDistance(self.StationZone:GetCoordinate()) local angel=self.StationZone:GetCoordinate():GetAngleDegrees(self.OpsZone:GetVec3()) self:T("Angel Radians= "..angel) local turn=math.fmod(self.AnchorTurn*stackscreated,360) if self.AnchorTurn<0 then turn=-turn end local newanchorbasecoord=anchorbasecoord:Translate(anchorradius,turn+angel) local radius=self.StationZone:GetRadius() if radius<10000 then radius=10000 end AnchorStackOne.StationZone=ZONE_RADIUS:New(newname,newanchorbasecoord:GetVec2(),radius) AnchorStackOne.StationZoneCoordinate=newanchorbasecoord AnchorStackOne.StationZoneCoordinateText=newanchorbasecoord:ToStringLLDDM() AnchorStackOne.StationName=newname if self.debug then AnchorStackOne.StationZone:DrawZone(self.coalition,{0,0,1},1,{0,0,1},0.2,5,true) local stationtag=string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) if self.AllowMarkers then AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToCoalition(self.coalition) end else local stationtag=string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) if self.AllowMarkers then AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToCoalition(self.coalition) end end self.AnchorStacks:Push(AnchorStackOne,newname) end return true,self.AnchorStacks:GetSize() end function AWACS:_GetFreeAnchorStack() self:T(self.lid.."_GetFreeAnchorStack") local AnchorStackNo,Free=0,false local availablestacks=self.AnchorStacks:GetPointerStack()or{} for _id,_entry in pairs(availablestacks)do local entry=_entry local data=entry.data if data.Anchors:IsNotEmpty()then AnchorStackNo=_id Free=true break end end if not Free then local created,number=self:_CreateAnchorStack() if created then self:_GetFreeAnchorStack() end end return AnchorStackNo,Free end function AWACS:_AssignAnchorToID(GID,HasOwnStation,StationName) self:T(self.lid.."_AssignAnchorToID") if not HasOwnStation then local AnchorStackNo,Free=self:_GetFreeAnchorStack() if Free then local Anchor=self.AnchorStacks:PullByPointer(AnchorStackNo) local freeangels=Anchor.Anchors:Pull() Anchor.AnchorAssignedID:Push(GID) self.AnchorStacks:Push(Anchor,Anchor.StationName) self:T({Anchor,freeangels}) self:__AssignedAnchor(5,GID,Anchor,AnchorStackNo,freeangels) else self:E(self.lid.."Cannot assign free anchor stack to GID "..GID.." Trying again in 10secs.") self:__AssignAnchor(10,GID) end else local Anchor=self.AnchorStacks:PullByID(StationName) local freeangels=Anchor.Anchors:Pull()or 25 Anchor.AnchorAssignedID:Push(GID) self.AnchorStacks:Push(Anchor,StationName) self:T({Anchor,freeangels}) local StackNo=self.AnchorStacks.stackbyid[StationName].pointer self:__AssignedAnchor(5,GID,Anchor,StackNo,freeangels) end return self end function AWACS:_RemoveIDFromAnchor(GID,AnchorStackNo,Angels) local gid=GID or 0 local stack=AnchorStackNo or 0 local angels=Angels or 0 local debugstring=string.format("%s_RemoveIDFromAnchor for GID=%d Stack=%d Angels=%d",self.lid,gid,stack,angels) self:T(debugstring) if stack>0 and angels>0 then local AnchorStackNo=AnchorStackNo or 1 local Anchor=self.AnchorStacks:ReadByPointer(AnchorStackNo) local removedID=Anchor.AnchorAssignedID:PullByID(GID) Anchor.Anchors:Push(Angels) end return self end function AWACS:_StartIntel(awacs) self:T(self.lid.."_StartIntel") if self.intelstarted then return self end self.DetectionSet:AddGroup(awacs) local intel=INTEL:New(self.DetectionSet,self.coalition,self.callsigntxt) intel:SetClusterAnalysis(true,false,false) local acceptzoneset=SET_ZONE:New() acceptzoneset:AddZone(self.ControlZone) acceptzoneset:AddZone(self.OpsZone) if not self.GCI then self.OrbitZone:SetRadius(UTILS.NMToMeters(55)) acceptzoneset:AddZone(self.OrbitZone) end if self.BorderZone then acceptzoneset:AddZone(self.BorderZone) end intel:SetAcceptZones(acceptzoneset) if self.NoHelos then intel:SetFilterCategory({Unit.Category.AIRPLANE}) else intel:SetFilterCategory({Unit.Category.AIRPLANE,Unit.Category.HELICOPTER}) end if self.usecorridors==true then intel:SetCorridorZones(self.corridorzones) if self.corridorceiling or self.corridorfloor then intel:SetCorridorLimits(self.corridorfloor,self.corridorceiling) end end local function NewCluster(Cluster) self:__NewCluster(5,Cluster) end function intel:OnAfterNewCluster(From,Event,To,Cluster) NewCluster(Cluster) end local function NewContact(Contact) self:__NewContact(5,Contact) end function intel:OnAfterNewContact(From,Event,To,Contact) NewContact(Contact) end local function LostContact(Contact) self:__LostContact(5,Contact) end function intel:OnAfterLostContact(From,Event,To,Contact) LostContact(Contact) end local function LostCluster(Cluster,Mission) self:__LostCluster(5,Cluster,Mission) end function intel:OnAfterLostCluster(From,Event,To,Cluster,Mission) LostCluster(Cluster,Mission) end self.intelstarted=true intel.statusupdate=-30 intel:__Start(5) self.intel=intel return self end function AWACS:_GetBlurredSize(size) self:T(self.lid.."_GetBlurredSize") local threatsize=0 local blur=self.RadarBlur local blurmin=100-blur local blurmax=100+blur local actblur=math.random(blurmin,blurmax)/100 threatsize=math.floor(size*actblur) if threatsize==0 then threatsize=1 end if threatsize then end local threatsizetext=AWACS.Shipsize[1] if threatsize==2 then threatsizetext=AWACS.Shipsize[2] elseif threatsize==3 then threatsizetext=AWACS.Shipsize[3] elseif threatsize>3 then threatsizetext=AWACS.Shipsize[4] end return threatsize,threatsizetext end function AWACS:_GetThreatLevelText(threatlevel) self:T(self.lid.."_GetThreatLevelText") local threattext="GREEN" if threatlevel<=AWACS.THREATLEVEL.GREEN then threattext="GREEN" elseif threatlevel<=AWACS.THREATLEVEL.AMBER then threattext="AMBER" else threattext="RED" end return threattext end function AWACS:_ToStringBR(FromCoordinate,ToCoordinate) self:T(self.lid.."_ToStringBR") local BRText="" local BRTextTTS="" local DirectionVec3=FromCoordinate:GetDirectionVec3(ToCoordinate) local AngleRadians=FromCoordinate:GetAngleRadians(DirectionVec3) local AngleDegrees=UTILS.Round(UTILS.ToDegree(AngleRadians),0) local AngleDegText=string.format("%03d",AngleDegrees) local AngleDegTextTTS="" local zero=self.gettext:GetEntry("ZERO",self.locale) local miles=self.gettext:GetEntry("MILES",self.locale) AngleDegText=string.gsub(AngleDegText,"%d","%1 ") AngleDegText=string.gsub(AngleDegText," $","") AngleDegTextTTS=string.gsub(AngleDegText,"0",zero) local Distance=ToCoordinate:Get2DDistance(FromCoordinate) local distancenm=UTILS.Round(UTILS.MetersToNM(Distance),0) BRText=string.format("%03d, %d %s",AngleDegrees,distancenm,miles) BRTextTTS=string.format("%s, %d %s",AngleDegText,distancenm,miles) if self.PathToGoogleKey then BRTextTTS=string.format("%s, %d %s",AngleDegTextTTS,distancenm,miles) end self:T(BRText,BRTextTTS) return BRText,BRTextTTS end function AWACS:_ToStringBRA(FromCoordinate,ToCoordinate,Altitude) self:T(self.lid.."_ToStringBRA") local BRText="" local BRTextTTS="" local altitude=UTILS.Round(UTILS.MetersToFeet(Altitude)/1000,0) local DirectionVec3=FromCoordinate:GetDirectionVec3(ToCoordinate) local AngleRadians=FromCoordinate:GetAngleRadians(DirectionVec3) local AngleDegrees=UTILS.Round(UTILS.ToDegree(AngleRadians),0) local AngleDegText=string.format("%03d",AngleDegrees) AngleDegText=string.gsub(AngleDegText,"%d","%1 ") AngleDegText=string.gsub(AngleDegText," $","") local AngleDegTextTTS=string.gsub(AngleDegText,"0","zero") local Distance=ToCoordinate:Get2DDistance(FromCoordinate) local distancenm=UTILS.Round(UTILS.MetersToNM(Distance),0) local zero=self.gettext:GetEntry("ZERO",self.locale) local miles=self.gettext:GetEntry("MILES",self.locale) local thsd=self.gettext:GetEntry("THOUSAND",self.locale) local vlow=self.gettext:GetEntry("VERYLOW",self.locale) if altitude>=1 then BRText=string.format("%03d, %d %s, %d %s",AngleDegrees,distancenm,miles,altitude,thsd) BRTextTTS=string.format("%s, %d %s, %d %s",AngleDegText,distancenm,miles,altitude,thsd) if self.PathToGoogleKey then BRTextTTS=string.format("%s, %d %s, %d %s",AngleDegTextTTS,distancenm,miles,altitude,thsd) end else BRText=string.format("%03d, %d %s, %s",AngleDegrees,distancenm,miles,vlow) BRTextTTS=string.format("%s, %d %s, %s",AngleDegText,distancenm,miles,vlow) if self.PathToGoogleKey then BRTextTTS=string.format("%s, %d %s, %s",AngleDegTextTTS,distancenm,miles,vlow) end end self:T(BRText,BRTextTTS) return BRText,BRTextTTS end function AWACS:_GetBRAfromBullsOrAO(clustercoordinate) self:T(self.lid.."_GetBRAfromBullsOrAO") local refcoord=self.AOCoordinate local BRAText="" local BRATextTTS="" local bullsname=self.AOName or"Rock" local stringbr,stringbrtts=self:_ToStringBR(refcoord,clustercoordinate) BRAText=string.format("%s %s",bullsname,stringbr) BRATextTTS=string.format("%s %s",bullsname,stringbrtts) self:T(BRAText,BRATextTTS) return BRAText,BRATextTTS end function AWACS:_CreateTaskForGroup(GroupID,Description,ScreenText,Object,TaskStatus,Auftrag,Cluster,Contact) self:T(self.lid.."_CreateTaskForGroup "..GroupID.." Description: "..Description) local managedgroup=self.ManagedGrps[GroupID] local task={} self.ManagedTaskID=self.ManagedTaskID+1 task.TID=self.ManagedTaskID task.AssignedGroupID=GroupID task.Status=TaskStatus or AWACS.TaskStatus.ASSIGNED task.ToDo=Description task.Auftrag=Auftrag task.Cluster=Cluster task.Contact=Contact task.IsPlayerTask=managedgroup.IsPlayer task.IsUnassigned=TaskStatus==AWACS.TaskStatus.UNASSIGNED and false or true if Object and Object:IsInstanceOf("TARGET")then task.Target=Object else task.Target=TARGET:New(Object) end task.ScreenText=ScreenText if Description==AWACS.TaskDescription.ANCHOR or Description==AWACS.TaskDescription.REANCHOR then task.Target.Type=TARGET.ObjectType.ZONE end task.RequestedTimestamp=timer.getTime() self.ManagedTasks:Push(task,task.TID) managedgroup.HasAssignedTask=true managedgroup.CurrentTask=task.TID self:T({managedgroup}) self.ManagedGrps[GroupID]=managedgroup return task.TID end function AWACS:_ReadAssignedTaskFromGID(GroupID) self:T(self.lid.."_GetAssignedTaskFromGID "..GroupID) local managedgroup=self.ManagedGrps[GroupID] if managedgroup and managedgroup.HasAssignedTask and managedgroup.CurrentTask~=0 then local TaskID=managedgroup.CurrentTask if self.ManagedTasks:HasUniqueID(TaskID)then return self.ManagedTasks:ReadByID(TaskID) end end return nil end function AWACS:_ReadAssignedGroupFromTID(TaskID) self:T(self.lid.."_ReadAssignedGroupFromTID "..TaskID) if self.ManagedTasks:HasUniqueID(TaskID)then local task=self.ManagedTasks:ReadByID(TaskID) if task and task.AssignedGroupID and task.AssignedGroupID>0 then return self.ManagedGrps[task.AssignedGroupID] end end return nil end function AWACS:_MessageAIReadyForTasking(GID) self:T(self.lid.."_MessageAIReadyForTasking") if GID>0 and self.ManagedGrps[GID]then local managedgroup=self.ManagedGrps[GID] local GFCallsign=self:_GetCallSign(managedgroup.Group) local aionst=self.gettext:GetEntry("AIONSTATION",self.locale) local TextTTS=string.format(aionst,GFCallsign,self.callsigntxt,managedgroup.AnchorStackNo or 1,managedgroup.AnchorStackAngels or 25) self:_NewRadioEntry(TextTTS,TextTTS,GID,false,false,true,true) end return self end function AWACS:_UpdateContactEngagementTag(CID,Text,TAC,MELD,TaskStatus) self:T(self.lid.."_UpdateContactEngagementTag") local text=Text or"" local contact=self.Contacts:PullByID(CID) if contact then contact.EngagementTag=text contact.TACCallDone=TAC or false contact.MeldCallDone=MELD or false contact.Status=TaskStatus or AWACS.TaskStatus.UNASSIGNED self.Contacts:Push(contact,CID) end return self end function AWACS:_CheckTaskQueue() self:T(self.lid.."_CheckTaskQueue") local opentasks=0 local assignedtasks=0 for _id,_managedgroup in pairs(self.ManagedGrps)do local group=_managedgroup if group.Group and group.Group:IsAlive()then local coordinate=group.Group:GetCoordinate() if coordinate then local NewCoordinate=COORDINATE:New(0,0,0) group.LastKnownPosition=group.LastKnownPosition:UpdateFromCoordinate(coordinate) self.ManagedGrps[_id]=group end end end if self.ManagedTasks:IsNotEmpty()then opentasks=self.ManagedTasks:GetSize() self:T("Assigned Tasks: "..opentasks) local taskstack=self.ManagedTasks:GetPointerStack() for _id,_entry in pairs(taskstack)do local data=_entry local entry=data.data local target=entry.Target local description=entry.ToDo if description==AWACS.TaskDescription.ANCHOR or description==AWACS.TaskDescription.REANCHOR then self:T("Open Task ANCHOR/REANCHOR") local managedgroup=self.ManagedGrps[entry.AssignedGroupID] if managedgroup then local group=managedgroup.Group if group and group:IsAlive()then local groupcoord=group:GetCoordinate() local zone=target:GetObject() self:T({zone}) if group:IsInZone(zone)then self:T("Open Task ANCHOR/REANCHOR success for GroupID "..entry.AssignedGroupID) target:Stop() if managedgroup.IsAI then self:_MessageAIReadyForTasking(managedgroup.GID) end managedgroup.HasAssignedTask=false self.ManagedGrps[entry.AssignedGroupID]=managedgroup self.ManagedTasks:PullByID(entry.TID) else self:T("Open Task ANCHOR/REANCHOR executing for GroupID "..entry.AssignedGroupID) end else self.ManagedTasks:PullByID(entry.TID) end end elseif description==AWACS.TaskDescription.INTERCEPT then self:T("Open Tasks INTERCEPT") local taskstatus=entry.Status local targetstatus=entry.Target:GetState() if taskstatus==AWACS.TaskStatus.UNASSIGNED then self.ManagedTasks:PullByID(entry.TID) break end local managedgroup=self.ManagedGrps[entry.AssignedGroupID] local auftrag=entry.Auftrag local auftragstatus="Not Known" if auftrag then auftragstatus=auftrag:GetState() end local text=string.format("ID=%d | Status=%s | TargetState=%s | AuftragState=%s",entry.TID,taskstatus,targetstatus,auftragstatus) self:T(text) if auftrag then if auftrag:IsExecuting()then entry.Status=AWACS.TaskStatus.EXECUTING elseif auftrag:IsSuccess()then entry.Status=AWACS.TaskStatus.SUCCESS elseif auftrag:GetState()==AUFTRAG.Status.FAILED then entry.Status=AWACS.TaskStatus.FAILED end if targetstatus=="Dead"then entry.Status=AWACS.TaskStatus.SUCCESS elseif targetstatus=="Alive"and auftrag:IsOver()then entry.Status=AWACS.TaskStatus.FAILED end elseif entry.IsPlayerTask then if entry.Target:IsDead()or entry.Target:IsDestroyed()or entry.Target:CountTargets()==0 then entry.Status=AWACS.TaskStatus.SUCCESS elseif entry.Target:IsAlive()then local targetpos=entry.Target:GetCoordinate() local outofzones=false self.RejectZoneSet:ForEachZone( function(Zone,Position) local zone=Zone local pos=Position if pos and zone:IsVec2InZone(pos)then outofzones=true end end, targetpos:GetVec2() ) if not outofzones then outofzones=true self.ZoneSet:ForEachZone( function(Zone,Position) local zone=Zone local pos=Position if pos and zone:IsVec2InZone(pos)then outofzones=false end end, targetpos:GetVec2() ) end if outofzones then entry.Status=AWACS.TaskStatus.SUCCESS end end end if entry.Status==AWACS.TaskStatus.SUCCESS then self:T("Open Tasks INTERCEPT success for GroupID "..entry.AssignedGroupID) if managedgroup then self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",true,true,AWACS.TaskStatus.SUCCESS) managedgroup.HasAssignedTask=false managedgroup.ContactCID=0 managedgroup.LastTasking=timer.getTime() if managedgroup.IsAI then managedgroup.CurrentAuftrag=0 else managedgroup.CurrentTask=0 end self.ManagedGrps[entry.AssignedGroupID]=managedgroup self.ManagedTasks:PullByID(entry.TID) self:__InterceptSuccess(1) self:__ReAnchor(5,managedgroup.GID) end elseif entry.Status==AWACS.TaskStatus.FAILED then self:T("Open Tasks INTERCEPT failed for GroupID "..entry.AssignedGroupID) if managedgroup then managedgroup.HasAssignedTask=false self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",false,false,AWACS.TaskStatus.UNASSIGNED) managedgroup.ContactCID=0 managedgroup.LastTasking=timer.getTime() if managedgroup.IsAI then managedgroup.CurrentAuftrag=0 else managedgroup.CurrentTask=0 end if managedgroup.IsPlayer then entry.IsPlayerTask=false end self.ManagedGrps[entry.AssignedGroupID]=managedgroup if managedgroup.Group:IsAlive()or(managedgroup.FlightGroup and managedgroup.FlightGroup:IsAlive())then self:__ReAnchor(5,managedgroup.GID) end end self.ManagedTasks:PullByID(entry.TID) self:__InterceptFailure(1) elseif entry.Status==AWACS.TaskStatus.REQUESTED then self:T("Open Tasks INTERCEPT REQUESTED for GroupID "..entry.AssignedGroupID) local created=entry.RequestedTimestamp or timer.getTime()-120 local Tnow=timer.getTime() local Trunning=(Tnow-created)/60 local text=string.format("Task TID %s Requested %d minutes ago.",entry.TID,Trunning) if Trunning>self.ReassignmentPause then entry.Status=AWACS.TaskStatus.UNASSIGNED self.ManagedTasks:PullByID(entry.TID) end self:T(text) end elseif description==AWACS.TaskDescription.VID then local managedgroup=self.ManagedGrps[entry.AssignedGroupID] if(not managedgroup)or(not managedgroup.Group:IsAlive())then self.ManagedTasks:PullByID(entry.TID) return self end if entry.Target:IsDead()or entry.Target:IsDestroyed()or entry.Target:CountTargets()==0 then entry.Status=AWACS.TaskStatus.SUCCESS elseif entry.Target:IsAlive()then self:T("Checking VID target out of bounds") local targetpos=entry.Target:GetCoordinate() local outofzones=false self.RejectZoneSet:ForEachZone( function(Zone,Position) local zone=Zone local pos=Position if pos and zone:IsVec2InZone(pos)then outofzones=true end end, targetpos:GetVec2() ) if not outofzones then outofzones=true self.ZoneSet:ForEachZone( function(Zone,Position) local zone=Zone local pos=Position if pos and zone:IsVec2InZone(pos)then outofzones=false end end, targetpos:GetVec2() ) end if outofzones then entry.Status=AWACS.TaskStatus.SUCCESS self:T("Out of bounds - SUCCESS") end end if entry.Status==AWACS.TaskStatus.REQUESTED then self:T("Open Tasks VID REQUESTED for GroupID "..entry.AssignedGroupID) local created=entry.RequestedTimestamp or timer.getTime()-120 local Tnow=timer.getTime() local Trunning=(Tnow-created)/60 local text=string.format("Task TID %s Requested %d minutes ago.",entry.TID,Trunning) if Trunning>self.ReassignmentPause then entry.Status=AWACS.TaskStatus.UNASSIGNED self.ManagedTasks:PullByID(entry.TID) end self:T(text) elseif entry.Status==AWACS.TaskStatus.ASSIGNED then self:T("Open Tasks VID ASSIGNED for GroupID "..entry.AssignedGroupID) elseif entry.Status==AWACS.TaskStatus.SUCCESS then self:T("Open Tasks VID success for GroupID "..entry.AssignedGroupID) self.ManagedTasks:PullByID(entry.TID) local Contact=self.Contacts:ReadByID(entry.Contact.CID) if Contact and(Contact.IFF==AWACS.IFF.FRIENDLY or Contact.IFF==AWACS.IFF.NEUTRAL)then self:T("IFF outcome friendly/neutral for GroupID "..entry.AssignedGroupID) if managedgroup then managedgroup.HasAssignedTask=false self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",false,false,AWACS.TaskStatus.UNASSIGNED) managedgroup.ContactCID=0 managedgroup.LastTasking=timer.getTime() if managedgroup.IsAI then managedgroup.CurrentAuftrag=0 else managedgroup.CurrentTask=0 end if managedgroup.IsPlayer then entry.IsPlayerTask=false end self.ManagedGrps[entry.AssignedGroupID]=managedgroup self:__ReAnchor(5,managedgroup.GID) end elseif Contact and Contact.IFF==AWACS.IFF.ENEMY then self:T("IFF outcome hostile for GroupID "..entry.AssignedGroupID) entry.ToDo=AWACS.TaskDescription.INTERCEPT entry.Status=AWACS.TaskStatus.ASSIGNED local cname=Contact.TargetGroupNaming entry.ScreenText=string.format("Engage hostile %s group.",cname) self.ManagedTasks:Push(entry,entry.TID) local TextTTS=string.format("%s, %s. Engage hostile target!",managedgroup.CallSign,self.callsigntxt) self:_NewRadioEntry(TextTTS,TextTTS,managedgroup.GID,true,self.debug,true,false,true) elseif not Contact then self:T("IFF outcome target DEAD for GroupID "..entry.AssignedGroupID) if managedgroup then managedgroup.HasAssignedTask=false self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",false,false,AWACS.TaskStatus.UNASSIGNED) managedgroup.ContactCID=0 managedgroup.LastTasking=timer.getTime() if managedgroup.IsAI then managedgroup.CurrentAuftrag=0 else managedgroup.CurrentTask=0 end if managedgroup.IsPlayer then entry.IsPlayerTask=false end self.ManagedGrps[entry.AssignedGroupID]=managedgroup if managedgroup.Group:IsAlive()or managedgroup.FlightGroup:IsAlive()then self:__ReAnchor(5,managedgroup.GID) end end end elseif entry.Status==AWACS.TaskStatus.FAILED then self:T("Open Tasks VID failed for GroupID "..entry.AssignedGroupID) if managedgroup then managedgroup.HasAssignedTask=false self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",false,false,AWACS.TaskStatus.UNASSIGNED) managedgroup.ContactCID=0 managedgroup.LastTasking=timer.getTime() if managedgroup.IsAI then managedgroup.CurrentAuftrag=0 else managedgroup.CurrentTask=0 end if managedgroup.IsPlayer then entry.IsPlayerTask=false end self.ManagedGrps[entry.AssignedGroupID]=managedgroup if managedgroup.Group:IsAlive()or managedgroup.FlightGroup:IsAlive()then self:__ReAnchor(5,managedgroup.GID) end end self.ManagedTasks:PullByID(entry.TID) self:__InterceptFailure(1) end end end end return self end function AWACS:_LogStatistics() self:T(self.lid.."_LogStatistics") local text=string.gsub(UTILS.OneLineSerialize(self.MonitoringData),",","\n") local text=string.gsub(text,"{","\n") local text=string.gsub(text,"}","") local text=string.gsub(text,"="," = ") self:T(text) if self.MonitoringOn then MESSAGE:New(text,20,"AWACS",false):ToAll() end return self end function AWACS:AddCAPAirWing(AirWing,Zone) self:T(self.lid.."AddCAPAirWing") if AirWing then AirWing:SetUsingOpsAwacs(self) local distance=self.AOCoordinate:Get2DDistance(AirWing:GetCoordinate()) if Zone then local stackscreated=self.AnchorStacks:GetSize() if stackscreated==self.AnchorMaxAnchors then self:E(self.lid.."Max number of stacks already created!") else local AnchorStackOne={} AnchorStackOne.AnchorBaseAngels=self.AnchorBaseAngels AnchorStackOne.Anchors=FIFO:New() AnchorStackOne.AnchorAssignedID=FIFO:New() local newname=Zone:GetName() for i=1,self.AnchorMaxStacks do AnchorStackOne.Anchors:Push((i-1)*self.AnchorStackDistance+self.AnchorBaseAngels) end local newsubname=AWACS.AnchorNames[stackscreated+1]or tostring(stackscreated+1) newname=Zone:GetName().."-"..newsubname AnchorStackOne.StationZone=Zone AnchorStackOne.StationZoneCoordinate=Zone:GetCoordinate() AnchorStackOne.StationZoneCoordinateText=Zone:GetCoordinate():ToStringLLDDM() AnchorStackOne.StationName=newname if self.debug then AnchorStackOne.StationZone:DrawZone(self.coalition,{0,0,1},1,{0,0,1},0.2,5,true) local stationtag=string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) if self.AllowMarkers then AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToCoalition(self.coalition) end else local stationtag=string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) if self.AllowMarkers then AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToCoalition(self.coalition) end end self.AnchorStacks:Push(AnchorStackOne,newname) AirWing.HasOwnStation=true AirWing.StationName=newname end end self.CAPAirwings:Push(AirWing,distance) end return self end function AWACS:_AnnounceContact(Contact,IsNew,Group,IsBogeyDope,Tag,IsPopup,ReportingName,Tactical) self:T(self.lid.."_AnnounceContact") local tag="" local Tag=Tag local CID=0 if not Tag then CID=Contact.CID or 0 Tag=Contact.TargetGroupNaming or"" end if self.NoGroupTags then Tag=nil end local isGroup=false local GID=0 local grpcallsign="Ghost 1" if Group and Group:IsAlive()then GID,isGroup,grpcallsign=self:_GetManagedGrpID(Group) self:T("GID="..GID.." CheckedIn = "..tostring(isGroup)) end local cluster=Contact.Cluster local intel=self.intel local size=self.intel:ClusterCountUnits(cluster) local threatsize,threatsizetext=self:_GetBlurredSize(size) local clustercoordinate=Contact.Cluster.coordinate or Contact.Contact.position local heading=Contact.Contact.group:GetHeading()or self.intel:CalcClusterDirection(cluster) clustercoordinate:SetHeading(Contact.Contact.group:GetHeading()) local BRAfromBulls,BRAfromBullsTTS=self:_GetBRAfromBullsOrAO(clustercoordinate) self:T(BRAfromBulls) self:T(BRAfromBullsTTS) BRAfromBulls=BRAfromBulls.."." BRAfromBullsTTS=BRAfromBullsTTS.."." if isGroup then BRAfromBulls=clustercoordinate:ToStringBRAANATO(Group:GetCoordinate(),true,true) BRAfromBullsTTS=string.gsub(BRAfromBulls,"BRAA","brah") BRAfromBullsTTS=string.gsub(BRAfromBullsTTS,"BRA","brah") if self.PathToGoogleKey then BRAfromBullsTTS=clustercoordinate:ToStringBRAANATO(Group:GetCoordinate(),true,true,true,false,true) end end local BRAText="" local TextScreen="" if isGroup then BRAText=string.format("%s, %s.",grpcallsign,self.callsigntxt) TextScreen=string.format("%s, %s.",grpcallsign,self.callsigntxt) else BRAText=string.format("%s.",self.callsigntxt) TextScreen=string.format("%s.",self.callsigntxt) end local newgrp=self.gettext:GetEntry("NEWGROUP",self.locale) local grptxt=self.gettext:GetEntry("GROUP",self.locale) local GRPtxt=self.gettext:GetEntry("GROUPCAP",self.locale) local popup=self.gettext:GetEntry("POPUP",self.locale) if IsNew and self.PlayerGuidance then BRAText=string.format("%s %s.",BRAText,newgrp) TextScreen=string.format("%s %s.",TextScreen,newgrp) elseif IsPopup then BRAText=string.format("%s %s %s.",BRAText,popup,grptxt) TextScreen=string.format("%s %s %s.",TextScreen,popup,grptxt) elseif IsBogeyDope and Tag and Tag~=""then BRAText=string.format("%s %s %s.",BRAText,Tag,grptxt) TextScreen=string.format("%s %s %s.",TextScreen,Tag,grptxt) else BRAText=string.format("%s %s.",BRAText,GRPtxt) TextScreen=string.format("%s %s.",TextScreen,GRPtxt) end if not IsBogeyDope then if Tag and Tag~=""then BRAText=BRAText.." "..Tag.."." TextScreen=TextScreen.." "..Tag.."." end end if threatsize>1 then BRAText=BRAText.." "..BRAfromBullsTTS.." "..threatsizetext.."." TextScreen=TextScreen.." "..BRAfromBulls.." "..threatsizetext.."." else BRAText=BRAText.." "..BRAfromBullsTTS TextScreen=TextScreen.." "..BRAfromBulls end if self.ModernEra then local high=self.gettext:GetEntry("HIGH",self.locale) local vfast=self.gettext:GetEntry("VERYFAST",self.locale) local fast=self.gettext:GetEntry("FAST",self.locale) if ReportingName and ReportingName~="Bogey"then ReportingName=string.gsub(ReportingName,"_"," ") BRAText=BRAText.." "..ReportingName.."." TextScreen=TextScreen.." "..ReportingName.."." end local height=Contact.Contact.group:GetHeight() local height=UTILS.Round(UTILS.MetersToFeet(height)/1000,0) if height>=40 then BRAText=BRAText..high TextScreen=TextScreen..high end local speed=Contact.Contact.group:GetVelocityKNOTS() if speed>900 then BRAText=BRAText..vfast TextScreen=TextScreen..vfast elseif speed>=600 and speed<=900 then BRAText=BRAText..fast TextScreen=TextScreen..fast end end BRAText=string.gsub(BRAText,"BRAA","brah") BRAText=string.gsub(BRAText,"BRA","brah") local prio=IsNew or IsBogeyDope self:_NewRadioEntry(BRAText,TextScreen,GID,isGroup,true,IsNew,false,prio,Tactical) return self end function AWACS:_GetAliveOpsGroupFromTable(OpsGroups) self:T(self.lid.."_GetAliveOpsGroupFromTable") local handback=nil for _,_OG in pairs(OpsGroups or{})do local OG=_OG if OG and OG:IsAlive()then handback=OG break end end return handback end function AWACS:_CleanUpAIMissionStack() self:T(self.lid.."_CleanUpAIMissionStack") local CAPMissions=0 local Alert5Missions=0 local InterceptMissions=0 local MissionStack=FIFO:New() self:T("Checking MissionStack") for _,_mission in pairs(self.CatchAllMissions)do local mission=_mission local type=mission:GetType() if type==AUFTRAG.Type.ALERT5 and mission:IsNotOver()then MissionStack:Push(mission,mission.auftragsnummer) Alert5Missions=Alert5Missions+1 elseif type==AUFTRAG.Type.CAP and mission:IsNotOver()then MissionStack:Push(mission,mission.auftragsnummer) CAPMissions=CAPMissions+1 elseif type==AUFTRAG.Type.INTERCEPT and mission:IsNotOver()then MissionStack:Push(mission,mission.auftragsnummer) InterceptMissions=InterceptMissions+1 end end self.AICAPMissions=nil self.AICAPMissions=MissionStack return CAPMissions,Alert5Missions,InterceptMissions end function AWACS:_ConsistencyCheck() self:T(self.lid.."_ConsistencyCheck") if self.debug then self:T("CatchAllMissions") local catchallm={} local report1=REPORT:New("CatchAll") report1:Add("====================") report1:Add("CatchAllMissions") report1:Add("====================") for _,_mission in pairs(self.CatchAllMissions)do local mission=_mission local nummer=mission.auftragsnummer or 0 local type=mission:GetType() local state=mission:GetState() local FG=mission:GetOpsGroups() local OG=self:_GetAliveOpsGroupFromTable(FG) local OGName="UnknownFromMission" if OG then OGName=OG:GetName() end report1:Add(string.format("Auftrag Nr %d Type %s State %s FlightGroup %s",nummer,type,state,OGName)) if mission:IsNotOver()then catchallm[#catchallm+1]=mission end end self.CatchAllMissions=nil self.CatchAllMissions=catchallm local catchallfg={} self:T("CatchAllFGs") report1:Add("====================") report1:Add("CatchAllFGs") report1:Add("====================") for _,_fg in pairs(self.CatchAllFGs)do local FG=_fg local mission=FG:GetMissionCurrent() local OGName=FG:GetName()or"UnknownFromFG" local nummer=0 local type="No Type" local state="None" if mission then type=mission:GetType() nummer=mission.auftragsnummer or 0 state=mission:GetState() end report1:Add(string.format("Auftrag Nr %d Type %s State %s FlightGroup %s",nummer,type,state,OGName)) if FG:IsAlive()then catchallfg[#catchallfg+1]=FG end end report1:Add("====================") self:T(report1:Text()) self.CatchAllFGs=nil self.CatchAllFGs=catchallfg end return self end function AWACS:_CheckAICAPOnStation() self:T(self.lid.."_CheckAICAPOnStation") self:_ConsistencyCheck() local capmissions,alert5missions,interceptmissions=self:_CleanUpAIMissionStack() self:T("CAP="..capmissions.." ALERT5="..alert5missions.." Requested="..self.AIRequested) if self.MaxAIonCAP>0 then local onstation=capmissions+alert5missions if capmissions>self.MaxAIonCAP then self:T(string.format("*** Onstation %d > MaxAIOnCAP %d",onstation,self.MaxAIonCAP)) local mission=self.AICAPMissions:Pull() local Groups=mission:GetOpsGroups() local OpsGroup=self:_GetAliveOpsGroupFromTable(Groups) local GID,checkedin=self:_GetManagedGrpID(OpsGroup) mission:__Cancel(30) self.AIRequested=self.AIRequested-1 if checkedin then self:_CheckOut(OpsGroup,GID) end end if capmissions0 then local report=REPORT:New("CAP Mission Status") report:Add("===============") local missions=self.AICAPMissions:GetDataTable() local i=1 for _,_Mission in pairs(missions)do local mission=_Mission if mission then i=i+1 report:Add(string.format("Entry %d",i)) report:Add(string.format("Mission No %d",mission.auftragsnummer)) report:Add(string.format("Mission Type %s",mission:GetType())) report:Add(string.format("Mission State %s",mission:GetState())) local OpsGroups=mission:GetOpsGroups() local OpsGroup=self:_GetAliveOpsGroupFromTable(OpsGroups) if OpsGroup then local OpsName=OpsGroup:GetName()or"Unknown" local found,GID,OpsCallSign=self:_GetGIDFromGroupOrName(OpsGroup) report:Add(string.format("Mission FG %s",OpsName)) report:Add(string.format("Callsign %s",OpsCallSign)) report:Add(string.format("Mission FG State %s",OpsGroup:GetState())) else report:Add("***** Cannot obtain (yet) this missions OpsGroup!") end report:Add(string.format("Target Type %s",mission:GetTargetType())) end report:Add("===============") end if self.debug then self:I(report:Text()) end end end return self end function AWACS:_SetAIROE(FlightGroup,Group) self:T(self.lid.."_SetAIROE") local ROE=self.AwacsROE or AWACS.ROE.POLICE local ROT=self.AwacsROT or AWACS.ROT.PASSIVE Group:OptionAlarmStateGreen() Group:OptionECM_OnlyLockByRadar() Group:OptionROEHoldFire() Group:OptionROTEvadeFire() Group:OptionRTBBingoFuel(true) Group:OptionKeepWeaponsOnThreat() local callname=self.AICAPCAllName or CALLSIGN.Aircraft.Colt self.AICAPCAllNumber=self.AICAPCAllNumber+1 Group:CommandSetCallsign(callname,math.fmod(self.AICAPCAllNumber,9)) FlightGroup:SetDefaultAlarmstate(AI.Option.Ground.val.ALARM_STATE.GREEN) FlightGroup:SetDefaultCallsign(callname,math.fmod(self.AICAPCAllNumber,9)) if ROE==AWACS.ROE.POLICE or ROE==AWACS.ROE.VID then FlightGroup:SetDefaultROE(ENUMS.ROE.WeaponHold) elseif ROE==AWACS.ROE.IFF then FlightGroup:SetDefaultROE(ENUMS.ROE.ReturnFire) elseif ROE==AWACS.ROE.BVR then FlightGroup:SetDefaultROE(ENUMS.ROE.OpenFire) end if ROT==AWACS.ROT.BYPASSESCAPE or ROT==AWACS.ROT.PASSIVE then FlightGroup:SetDefaultROT(ENUMS.ROT.PassiveDefense) elseif ROT==AWACS.ROT.OPENFIRE or ROT==AWACS.ROT.RETURNFIRE then FlightGroup:SetDefaultROT(ENUMS.ROT.BypassAndEscape) elseif ROT==AWACS.ROT.EVADE then FlightGroup:SetDefaultROT(ENUMS.ROT.EvadeFire) end FlightGroup:SetFuelLowRTB(true) FlightGroup:SetFuelLowThreshold(0.2) FlightGroup:SetEngageDetectedOff() FlightGroup:SetOutOfAAMRTB(true) return self end function AWACS:_TACRangeCall(GID,Contact) self:T(self.lid.."_TACRangeCall") if not Contact then return self end local pilotcallsign=self:_GetCallSign(nil,GID) local managedgroup=self.ManagedGrps[GID] local contact=Contact.Contact local contacttag=Contact.TargetGroupNaming local name=managedgroup.GroupName if contact then local position=contact.position if position then local distance=position:Get2DDistance(managedgroup.Group:GetCoordinate()) distance=UTILS.Round(UTILS.MetersToNM(distance)) local grptxt=self.gettext:GetEntry("GROUP",self.locale) local miles=self.gettext:GetEntry("MILES",self.locale) local text=string.format("%s. %s. %s %s, %d %s.",self.callsigntxt,pilotcallsign,contacttag,grptxt,distance,miles) if not self.TacticalSubscribers[name]then self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true) end self:_UpdateContactEngagementTag(Contact.CID,Contact.EngagementTag,true,false,AWACS.TaskStatus.EXECUTING) if GID and GID~=0 then if managedgroup and managedgroup.Group and managedgroup.Group:IsAlive()then if self.TacticalSubscribers[name]then self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true,true) end end end end end return self end function AWACS:_MeldRangeCall(GID,Contact) self:T(self.lid.."_MeldRangeCall") if not Contact then return self end local pilotcallsign=self:_GetCallSign(nil,GID) local managedgroup=self.ManagedGrps[GID] local flightpos=managedgroup.Group:GetCoordinate() local contact=Contact.Contact local contacttag=Contact.TargetGroupNaming or"Bogey" local name=managedgroup.GroupName if contact then local position=contact.position if position then local BRATExt="" if self.PathToGoogleKey then BRATExt=position:ToStringBRAANATO(flightpos,false,false,true,false,true) else BRATExt=position:ToStringBRAANATO(flightpos,false,false) end local grptxt=self.gettext:GetEntry("GROUP",self.locale) local text=string.format("%s. %s. %s %s, %s",self.callsigntxt,pilotcallsign,contacttag,grptxt,BRATExt) if not self.TacticalSubscribers[name]then self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true) end self:_UpdateContactEngagementTag(Contact.CID,Contact.EngagementTag,true,true,AWACS.TaskStatus.EXECUTING) if GID and GID~=0 then if managedgroup and managedgroup.Group and managedgroup.Group:IsAlive()then local name=managedgroup.GroupName if self.TacticalSubscribers[name]then self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true,true) end end end end end return self end function AWACS:_ThreatRangeCall(GID,Contact) self:T(self.lid.."_ThreatRangeCall") if not Contact then return self end local pilotcallsign=self:_GetCallSign(nil,GID) local managedgroup=self.ManagedGrps[GID] local flightpos=managedgroup.Group:GetCoordinate()or managedgroup.LastKnownPosition local contact=Contact.Contact local contacttag=Contact.TargetGroupNaming or"Bogey" local name=managedgroup.GroupName local IsSub=self.TacticalSubscribers[name]and true or false if contact then local position=contact.position or contact.group:GetCoordinate() if position then local BRATExt="" if self.PathToGoogleKey then BRATExt=position:ToStringBRAANATO(flightpos,false,false,true,false,true) else BRATExt=position:ToStringBRAANATO(flightpos,false,false) end local grptxt=self.gettext:GetEntry("GROUP",self.locale) local thrt=self.gettext:GetEntry("THREAT",self.locale) local text=string.format("%s. %s. %s %s, %s. %s",self.callsigntxt,pilotcallsign,contacttag,grptxt,thrt,BRATExt) if string.find(text,"BRAA",1,true)then text=string.gsub(text,"BRAA","brah") elseif string.find(text,"BRA",1,true)then text=string.gsub(text,"BRA","brah") end if IsSub==false then self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true) end if GID and GID~=0 then if managedgroup and managedgroup.Group and managedgroup.Group:IsAlive()then local name=managedgroup.GroupName if self.TacticalSubscribers[name]then self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true,true) end end end end end return self end function AWACS:_MergedCall(GID) self:T(self.lid.."_MergedCall") local pilotcallsign=self:_GetCallSign(nil,GID) local merge=self.gettext:GetEntry("MERGED",self.locale) local text=string.format("%s. %s. %s.",self.callsigntxt,pilotcallsign,merge) local managedgroup=self.ManagedGrps[GID] local name if managedgroup then name=managedgroup.GroupName or"none" end if not self.TacticalSubscribers[name]then self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true) end if GID and GID~=0 then local managedgroup=self.ManagedGrps[GID] if managedgroup and managedgroup.Group and managedgroup.Group:IsAlive()then if self.TacticalSubscribers[name]then self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true,true) end end end return self end function AWACS:_AssignPilotToTarget(Pilots,Targets) self:T(self.lid.."_AssignPilotToTarget") local inreach=false local Pilot=nil local closest=UTILS.NMToMeters(self.maxassigndistance+1) local targets=Targets:GetDataTable() local Target=nil for _,_target in pairs(targets)do local targetgroupcoord=_target.Contact.position for _,_Pilot in pairs(Pilots)do local pilotcoord=_Pilot.Group:GetCoordinate() local targetdist=targetgroupcoord:Get2DDistance(pilotcoord) if UTILS.MetersToNM(targetdist) "..self.maxassigndistance.."NM! No Assignment!") end end end if inreach and Pilot and Pilot.IsPlayer then local callsign=Pilot.CallSign self.ManagedTasks:PullByID(Pilot.CurrentTask) Pilot.HasAssignedTask=true local TargetPosition=Target.Target:GetCoordinate() local PlayerPositon=Pilot.LastKnownPosition local TargetAlt=Target.Contact.altitude or Target.Cluster.altitude or Target.Contact.group:GetAltitude() local TargetDirections,TargetDirectionsTTS=self:_ToStringBRA(PlayerPositon,TargetPosition,TargetAlt) local ScreenText="" local TaskType=AWACS.TaskDescription.INTERCEPT if self.AwacsROE==AWACS.ROE.POLICE or self.AwacsROE==AWACS.ROE.VID then local interc=self.gettext:GetEntry("SCREENVID",self.locale) ScreenText=string.format(interc,Target.TargetGroupNaming) TaskType=AWACS.TaskDescription.VID else local interc=self.gettext:GetEntry("SCREENINTER",self.locale) ScreenText=string.format(interc,Target.TargetGroupNaming) end Pilot.CurrentTask=self:_CreateTaskForGroup(Pilot.GID,TaskType,ScreenText,Target.Target,AWACS.TaskStatus.REQUESTED,nil,Target.Cluster,Target.Contact) Pilot.ContactCID=Target.CID self.ManagedGrps[Pilot.GID]=Pilot Target.LinkedTask=Pilot.CurrentTask Target.LinkedGroup=Pilot.GID Target.Status=AWACS.TaskStatus.REQUESTED local targeted=self.gettext:GetEntry("ENGAGETAG",self.locale) Target.EngagementTag=string.format(targeted,Pilot.CallSign) self.Contacts:PullByID(Target.CID) self.Contacts:Push(Target,Target.CID) local reqcomm=self.gettext:GetEntry("REQCOMMIT",self.locale) local text=string.format(reqcomm,self.callsigntxt,Target.TargetGroupNaming,TargetDirectionsTTS,Pilot.CallSign) local textScreen=string.format(reqcomm,self.callsigntxt,Target.TargetGroupNaming,TargetDirections,Pilot.CallSign) self:_NewRadioEntry(text,textScreen,Pilot.GID,true,self.debug,true,false,true) elseif inreach and Pilot and Pilot.IsAI then local callsign=Pilot.CallSign local FGStatus=Pilot.FlightGroup:GetState() self:T("Pilot AI Callsign: "..callsign) self:T("Pilot FG State: "..FGStatus) local targetstatus=Target.Target:GetState() self:T("Target State: "..targetstatus) local currmission=Pilot.FlightGroup:GetMissionCurrent() if currmission then self:T("Current Mission: "..currmission:GetType()) end local ZoneSet=self.ZoneSet local RejectZoneSet=self.RejectZoneSet local intercept=AUFTRAG:NewINTERCEPT(Target.Target) intercept:SetWeaponExpend(AI.Task.WeaponExpend.ALL) intercept:SetWeaponType(ENUMS.WeaponFlag.Auto) intercept:SetMissionRange(self.MaxMissionRange) intercept:AddConditionSuccess( function(target,zoneset,rzoneset) local success=true local target=target if target:IsDestroyed()or target:IsDead()or target:CountTargets()==0 then return true end local tgtcoord=target:GetCoordinate() local tgtvec2=nil if tgtcoord then tgtvec2=tgtcoord:GetVec2() end local zones=zoneset local rzones=rzoneset if tgtvec2 then zones:ForEachZone( function(zone) if zone:IsVec2InZone(tgtvec2)then success=false end end ) rzones:ForEachZone( function(zone) if zone:IsVec2InZone(tgtvec2)then success=true end end ) end return success end, Target.Target, ZoneSet, RejectZoneSet ) Pilot.FlightGroup:AddMission(intercept) local Angels=Pilot.AnchorStackAngels or 25 Angels=Angels*1000 local AnchorSpeed=self.CapSpeedBase or 270 AnchorSpeed=UTILS.KnotsToAltKIAS(AnchorSpeed,Angels) local Anchor=self.AnchorStacks:ReadByPointer(Pilot.AnchorStackNo) local capauftrag=AUFTRAG:NewCAP(Anchor.StationZone,Angels,AnchorSpeed,Anchor.StationZoneCoordinate,0,15,{}) capauftrag:SetMissionRange(self.MaxMissionRange) capauftrag:SetTime(nil,((self.CAPTimeOnStation*3600)+(15*60))) Pilot.FlightGroup:AddMission(capauftrag) if currmission then currmission:__Cancel(3) end self.CatchAllMissions[#self.CatchAllMissions+1]=intercept self.CatchAllMissions[#self.CatchAllMissions+1]=capauftrag self.ManagedTasks:PullByID(Pilot.CurrentTask) Pilot.HasAssignedTask=true Pilot.CurrentTask=self:_CreateTaskForGroup(Pilot.GID,AWACS.TaskDescription.INTERCEPT,"Intercept Task",Target.Target,AWACS.TaskStatus.ASSIGNED,intercept,Target.Cluster,Target.Contact) Pilot.CurrentAuftrag=intercept.auftragsnummer Pilot.ContactCID=Target.CID self.ManagedGrps[Pilot.GID]=Pilot Target.LinkedTask=Pilot.CurrentTask Target.LinkedGroup=Pilot.GID Target.Status=AWACS.TaskStatus.ASSIGNED local targeted=self.gettext:GetEntry("ENGAGETAG",self.locale) Target.EngagementTag=string.format(targeted,Pilot.CallSign) self.Contacts:PullByID(Target.CID) self.Contacts:Push(Target,Target.CID) local altitude=Target.Contact.altitude or Target.Contact.group:GetAltitude() local position=Target.Cluster.coordinate or Target.Contact.position if not position then self.intel:GetClusterCoordinate(Target.Cluster,true) end local bratext,bratexttts=self:_ToStringBRA(Pilot.Group:GetCoordinate(),position,altitude or 8000) local aicomm=self.gettext:GetEntry("AICOMMIT",self.locale) local text=string.format(aicomm,self.callsigntxt,Target.TargetGroupNaming,bratexttts,Pilot.CallSign) local textScreen=string.format(aicomm,self.callsigntxt,Target.TargetGroupNaming,bratext,Pilot.CallSign) self:_NewRadioEntry(text,textScreen,Pilot.GID,true,self.debug,true,false,true) local comm=self.gettext:GetEntry("COMMIT",self.locale) local text=string.format("%s. %s.",Pilot.CallSign,comm) self:_NewRadioEntry(text,text,Pilot.GID,true,self.debug,true,true,true) self:__Intercept(2) end return self end function AWACS:onbeforeStart(From,Event,To) self:T({From,Event,To}) if self.IncludeHelicopters then self.clientset:FilterCategories("helicopter") end return self end function AWACS:onafterStart(From,Event,To) self:T({From,Event,To}) local controlzonename="FEZ-"..self.AOName self.ControlZone=ZONE_RADIUS:New(controlzonename,self.OpsZone:GetVec2(),UTILS.NMToMeters(self.ControlZoneRadius)) if self.debug then self.ControlZone:DrawZone(self.coalition,{0,1,0},1,{1,0,0},0.05,3,true) self.OpsZone:DrawZone(self.coalition,{1,0,0},1,{1,0,0},0.2,5,true) local AOCoordString=self.AOCoordinate:ToStringLLDDM() local Rocktag=string.format("FEZ: %s\nBulls Coordinate: %s",self.AOName,AOCoordString) if self.AllowMarkers then MARKER:New(self.AOCoordinate,Rocktag):ToCoalition(self.coalition) end self.StationZone:DrawZone(self.coalition,{0,0,1},1,{0,0,1},0.2,5,true) local stationtag=string.format("Station: %s\nCoordinate: %s",self.StationZoneName,self.StationZone:GetCoordinate():ToStringLLDDM()) if not self.GCI then if self.AllowMarkers then MARKER:New(self.StationZone:GetCoordinate(),stationtag):ToCoalition(self.coalition) end self.OrbitZone:DrawZone(self.coalition,{0,1,0},1,{0,1,0},0.2,5,true) if self.AllowMarkers then MARKER:New(self.OrbitZone:GetCoordinate(),"AIC Orbit Zone"):ToCoalition(self.coalition) end end else local AOCoordString=self.AOCoordinate:ToStringLLDDM() local Rocktag=string.format("FEZ: %s\nBulls Coordinate: %s",self.AOName,AOCoordString) if self.AllowMarkers then MARKER:New(self.AOCoordinate,Rocktag):ToCoalition(self.coalition) end if not self.GCI then if self.AllowMarkers then MARKER:New(self.OrbitZone:GetCoordinate(),"AIC Orbit Zone"):ToCoalition(self.coalition) end end local stationtag=string.format("Station: %s\nCoordinate: %s",self.StationZoneName,self.StationZone:GetCoordinate():ToStringLLDDM()) if self.AllowMarkers then MARKER:New(self.StationZone:GetCoordinate(),stationtag):ToCoalition(self.coalition) end end if not self.GCI then local AwacsAW=self.AirWing local mission=AUFTRAG:NewORBIT_RACETRACK(self.OrbitZone:GetCoordinate(),self.AwacsAngels*1000,self.Speed,self.Heading,self.Leg) mission:SetMissionRange(self.MaxMissionRange) mission:SetRequiredAttribute({GROUP.Attribute.AIR_AWACS}) local timeonstation=(self.AwacsTimeOnStation+self.ShiftChangeTime)*3600 mission:SetTime(nil,timeonstation) self.CatchAllMissions[#self.CatchAllMissions+1]=mission AwacsAW:AddMission(mission) self.AwacsMission=mission self.AwacsInZone=false self.AwacsReady=false else self.AwacsInZone=true self.AwacsReady=true self:_StartIntel(self.GCIGroup) if self.GCIGroup:IsGround()then self.AwacsFG=ARMYGROUP:New(self.GCIGroup) self.AwacsFG:SetDefaultRadio(self.Frequency,self.Modulation) self.AwacsFG:SwitchRadio(self.Frequency,self.Modulation) elseif self.GCIGroup:IsShip()then self.AwacsFG=NAVYGROUP:New(self.GCIGroup) self.AwacsFG:SetDefaultRadio(self.Frequency,self.Modulation) self.AwacsFG:SwitchRadio(self.Frequency,self.Modulation) else self:E(self.lid.."**** Group unsuitable for GCI ops! Needs to be a GROUND or SHIP type group!") self:Stop() return self end self.callsigntxt=string.format("%s",self.CallSignClear[self.CallSign]) self:__CheckRadioQueue(-10) local sunrise=self.gettext:GetEntry("SUNRISE",self.locale) local text=string.format(sunrise,self.callsigntxt,self.callsigntxt) self:_NewRadioEntry(text,text,0,false,false,false,false,true) self:T(self.lid..text) self.sunrisedone=true end local ZoneSet=SET_ZONE:New() ZoneSet:AddZone(self.ControlZone) if not self.GCI then ZoneSet:AddZone(self.OrbitZone) end if self.BorderZone then ZoneSet:AddZone(self.BorderZone) end local RejectZoneSet=SET_ZONE:New() if self.RejectZone then RejectZoneSet:AddZone(self.RejectZone) end self.ZoneSet=ZoneSet self.RejectZoneSet=RejectZoneSet if self.AllowMarkers then local MarkerOps=MARKEROPS_BASE:New("AWACS",{"Station","Delete","Move"}) local function Handler(Keywords,Coord,Text) self:T(Text) for _,_word in pairs(Keywords)do if string.lower(_word)=="station"then local Name=string.match(Text," ([%a]+)$") self:_CreateAnchorStackFromMarker(Name,Coord) break elseif string.lower(_word)=="delete"then local Name=string.match(Text," ([%a]+)$") self:_DeleteAnchorStackFromMarker(Name,Coord) break elseif string.lower(_word)=="move"then local Name=string.match(Text," ([%a]+)$") self:_MoveAnchorStackFromMarker(Name,Coord) break end end end function MarkerOps:OnAfterMarkAdded(From,Event,To,Text,Keywords,Coord) Handler(Keywords,Coord,Text) end function MarkerOps:OnAfterMarkChanged(From,Event,To,Text,Keywords,Coord) Handler(Keywords,Coord,Text) end function MarkerOps:OnAfterMarkDeleted(From,Event,To) end self.MarkerOps=MarkerOps end if self.GCI then self:__Started(-5) end if self.TacticalMenu then self:__CheckTacticalQueue(55) end self:__Status(-30) return self end function AWACS:_CheckAwacsStatus() self:T(self.lid.."_CheckAwacsStatus") local awacs=nil if self.AwacsFG then awacs=self.AwacsFG:GetGroup() local unit=awacs:GetUnit(1) if unit then self.STN=tostring(unit:GetSTN()) end end local monitoringdata=self.MonitoringData if not self.GCI then if awacs and awacs:IsAlive()and not self.AwacsInZone then local orbitzone=self.OrbitZone if awacs:IsInZone(orbitzone)then self.AwacsInZone=true self:T(self.lid.."Arrived in Orbit Zone: "..orbitzone:GetName()) local onstationtxt=self.gettext:GetEntry("AWONSTATION",self.locale) local text=string.format(onstationtxt,self.callsigntxt,self.AOName or"Rock") local textScreen=text self:_NewRadioEntry(text,textScreen,0,false,true,true,false,true) end end end if(awacs and awacs:IsAlive())then if not self.intelstarted then local alt=UTILS.Round(UTILS.MetersToFeet(awacs:GetAltitude())/1000,0) if alt>=10 then self:_StartIntel(awacs) end end if self.intelstarted and not self.sunrisedone then local alt=UTILS.Round(UTILS.MetersToFeet(awacs:GetAltitude())/1000,0) if alt>=10 then local sunrise=self.gettext:GetEntry("SUNRISE",self.locale) local text=string.format(sunrise,self.callsigntxt,self.callsigntxt) self:_NewRadioEntry(text,text,0,false,false,false,false,true) self:T(self.lid..text) self.sunrisedone=true end end local AWmission=self.AwacsMission local awstatus=AWmission:GetState() local AWmissiontime=(timer.getTime()-self.AwacsTimeStamp) local AWTOSLeft=UTILS.Round((((self.AwacsTimeOnStation+self.ShiftChangeTime)*3600)-AWmissiontime),0) AWTOSLeft=UTILS.Round(AWTOSLeft/60,0) local ChangeTime=UTILS.Round(((self.ShiftChangeTime*3600)/60),0) local Changedue="No" if not self.ShiftChangeAwacsFlag and(AWTOSLeft<=ChangeTime or AWmission:IsOver())then Changedue="Yes" self.ShiftChangeAwacsFlag=true self:__AwacsShiftChange(2) end local report=REPORT:New("AWACS:") report:Add("====================") report:Add("AWACS:") report:Add(string.format("Auftrag Status: %s",awstatus)) report:Add(string.format("TOS Left: %d min",AWTOSLeft)) report:Add(string.format("Needs ShiftChange: %s",Changedue)) local OpsGroups=AWmission:GetOpsGroups() local OpsGroup=self:_GetAliveOpsGroupFromTable(OpsGroups) if OpsGroup then local OpsName=OpsGroup:GetName()or"Unknown" local OpsCallSign=OpsGroup:GetCallsignName()or"Unknown" report:Add(string.format("Mission FG %s",OpsName)) report:Add(string.format("Callsign %s",OpsCallSign)) report:Add(string.format("Mission FG State %s",OpsGroup:GetState())) else report:Add("***** Cannot obtain (yet) this missions OpsGroup!") end if self.ShiftChangeAwacsFlag and self.ShiftChangeAwacsRequested then AWmission=self.AwacsMissionReplacement local esstatus=AWmission:GetState() local ESmissiontime=(timer.getTime()-self.AwacsTimeStamp) local ESTOSLeft=UTILS.Round((((self.AwacsTimeOnStation+self.ShiftChangeTime)*3600)-ESmissiontime),0) ESTOSLeft=UTILS.Round(ESTOSLeft/60,0) local ChangeTime=UTILS.Round(((self.ShiftChangeTime*3600)/60),0) report:Add("AWACS REPLACEMENT:") report:Add(string.format("Auftrag Status: %s",esstatus)) report:Add(string.format("TOS Left: %d min",ESTOSLeft)) local OpsGroups=AWmission:GetOpsGroups() local OpsGroup=self:_GetAliveOpsGroupFromTable(OpsGroups) if OpsGroup then local OpsName=OpsGroup:GetName()or"Unknown" local OpsCallSign=OpsGroup:GetCallsignName()or"Unknown" report:Add(string.format("Mission FG %s",OpsName)) report:Add(string.format("Callsign %s",OpsCallSign)) report:Add(string.format("Mission FG State %s",OpsGroup:GetState())) else report:Add("***** Cannot obtain (yet) this missions OpsGroup!") end if AWmission:IsExecuting()then self.ShiftChangeAwacsFlag=false self.ShiftChangeAwacsRequested=false self.sunrisedone=false if self.AwacsMission and self.AwacsMission:IsNotOver()then self.AwacsMission:Cancel() end self.AwacsMission=self.AwacsMissionReplacement self.AwacsMissionReplacement=nil self.AwacsTimeStamp=timer.getTime() report:Add("*** Replacement DONE ***") end report:Add("====================") end if self.HasEscorts then for i=1,self.EscortNumber do local ESmission=self.EscortMission[i] if not ESmission then break end local esstatus=ESmission:GetState() local ESmissiontime=(timer.getTime()-self.EscortsTimeStamp) local ESTOSLeft=UTILS.Round((((self.EscortsTimeOnStation+self.ShiftChangeTime)*3600)-ESmissiontime),0) ESTOSLeft=UTILS.Round(ESTOSLeft/60,0) local ChangeTime=UTILS.Round(((self.ShiftChangeTime*3600)/60),0) local Changedue="No" if(ESTOSLeft<=ChangeTime and not self.ShiftChangeEscortsFlag)or(ESmission:IsOver()and not self.ShiftChangeEscortsFlag)then Changedue="Yes" self.ShiftChangeEscortsFlag=true self:__EscortShiftChange(2) end report:Add("====================") report:Add("ESCORTS:") report:Add(string.format("Auftrag Status: %s",esstatus)) report:Add(string.format("TOS Left: %d min",ESTOSLeft)) report:Add(string.format("Needs ShiftChange: %s",Changedue)) local OpsGroups=ESmission:GetOpsGroups() local OpsGroup=self:_GetAliveOpsGroupFromTable(OpsGroups) if OpsGroup then local OpsName=OpsGroup:GetName()or"Unknown" local OpsCallSign=OpsGroup:GetCallsignName()or"Unknown" report:Add(string.format("Mission FG %s",OpsName)) report:Add(string.format("Callsign %s",OpsCallSign)) report:Add(string.format("Mission FG State %s",OpsGroup:GetState())) monitoringdata.EscortsStateMission[i]=esstatus monitoringdata.EscortsStateFG[i]=OpsGroup:GetState() else report:Add("***** Cannot obtain (yet) this missions OpsGroup!") end report:Add("====================") local RESMission if self.ShiftChangeEscortsFlag and self.ShiftChangeEscortsRequested then RESMission=self.EscortMissionReplacement[i] local esstatus=RESMission:GetState() local RESMissiontime=(timer.getTime()-self.EscortsTimeStamp) local ESTOSLeft=UTILS.Round((((self.EscortsTimeOnStation+self.ShiftChangeTime)*3600)-RESMissiontime),0) ESTOSLeft=UTILS.Round(ESTOSLeft/60,0) local ChangeTime=UTILS.Round(((self.ShiftChangeTime*3600)/60),0) report:Add("ESCORTS REPLACEMENT:") report:Add(string.format("Auftrag Status: %s",esstatus)) report:Add(string.format("TOS Left: %d min",ESTOSLeft)) local OpsGroups=RESMission:GetOpsGroups() local OpsGroup=self:_GetAliveOpsGroupFromTable(OpsGroups) if OpsGroup then local OpsName=OpsGroup:GetName()or"Unknown" local OpsCallSign=OpsGroup:GetCallsignName()or"Unknown" report:Add(string.format("Mission FG %s",OpsName)) report:Add(string.format("Callsign %s",OpsCallSign)) report:Add(string.format("Mission FG State %s",OpsGroup:GetState())) else report:Add("***** Cannot obtain (yet) this missions OpsGroup!") end if RESMission and RESMission:IsExecuting()then self.ShiftChangeEscortsFlag=false self.ShiftChangeEscortsRequested=false if ESmission and ESmission:IsNotOver()then ESmission:__Cancel(1) end self.EscortMission[i]=self.EscortMissionReplacement[i] self.EscortMissionReplacement[i]=nil self.EscortsTimeStamp=timer.getTime() report:Add("*** Replacement DONE ***") end report:Add("====================") end end end if self.debug then self:T(report:Text()) end else local AWmission=self.AwacsMission local awstatus=AWmission:GetState() if AWmission:IsOver()then self:I(self.lid.."*****AWACS is dead!*****") self.ShiftChangeAwacsFlag=true self:__AwacsShiftChange(2) end end return monitoringdata end function AWACS:onafterStatus(From,Event,To) self:T({From,Event,To}) self:_SetClientMenus() local monitoringdata=self.MonitoringData if not self.GCI then monitoringdata=self:_CheckAwacsStatus() end local awacsalive=false if self.AwacsFG then local awacs=self.AwacsFG:GetGroup() if awacs and awacs:IsAlive()then awacsalive=true end end if self:Is("Running")and(awacsalive or self.AwacsInZone)then if self.AwacsSRS then self.AwacsSRS:SetCoordinate(self.AwacsFG:GetCoordinate()) if self.TacticalSRS then self.TacticalSRS:SetCoordinate(self.AwacsFG:GetCoordinate()) end end self:_CheckAICAPOnStation() self:_CleanUpContacts() self:_CheckMerges() self:_CheckSubscribers() local outcome,targets=self:_TargetSelectionProcess(true) self:_CheckTaskQueue() local AI,Humans=self:_GetIdlePilots() if outcome and#Humans>0 and self.PlayerCapAssignment then self:_AssignPilotToTarget(Humans,targets) end if outcome and#AI>0 then self:_AssignPilotToTarget(AI,targets) end end if not self.GCI then monitoringdata.AwacsShiftChange=self.ShiftChangeAwacsFlag if self.AwacsFG then monitoringdata.AwacsStateFG=self.AwacsFG:GetState() end monitoringdata.AwacsStateMission=self.AwacsMission:GetState() monitoringdata.EscortsShiftChange=self.ShiftChangeEscortsFlag end monitoringdata.AICAPCurrent=self.AICAPMissions:Count() monitoringdata.AICAPMax=self.MaxAIonCAP monitoringdata.Airwings=self.CAPAirwings:Count() self.MonitoringData=monitoringdata if self.debug then self:_LogStatistics() end local picturetime=timer.getTime()-self.PictureTimeStamp if self.AwacsInZone and picturetime>self.PictureInterval then self.PictureTimeStamp=timer.getTime() self:_Picture(nil,true) end self:__Status(30) return self end function AWACS:onafterStop(From,Event,To) self:T({From,Event,To}) self.intel:Stop() local AWFiFo=self.CAPAirwings local AWStack=AWFiFo:GetPointerStack() for _ID,_AWID in pairs(AWStack)do local SubAW=self.CAPAirwings:ReadByPointer(_ID) if SubAW then SubAW:RemoveUsingOpsAwacs() end end self:UnHandleEvent(EVENTS.PlayerEnterAircraft) self:UnHandleEvent(EVENTS.PlayerEnterUnit) self:UnHandleEvent(EVENTS.PlayerLeaveUnit) self:UnHandleEvent(EVENTS.Ejection) self:UnHandleEvent(EVENTS.Crash) self:UnHandleEvent(EVENTS.Dead) self:UnHandleEvent(EVENTS.UnitLost) self:UnHandleEvent(EVENTS.BDA) self:UnHandleEvent(EVENTS.PilotDead) self:UnHandleEvent(EVENTS.Shot) return self end function AWACS:onafterAssignAnchor(From,Event,To,GID,HasOwnStation,StationName) self:T({From,Event,To,"GID = "..GID}) self:_AssignAnchorToID(GID,HasOwnStation,StationName) return self end function AWACS:onafterCheckedOut(From,Event,To,GID,AnchorStackNo,Angels) self:T({From,Event,To,"GID = "..GID}) self:_RemoveIDFromAnchor(GID,AnchorStackNo,Angels) return self end function AWACS:onafterAssignedAnchor(From,Event,To,GID,Anchor,AnchorStackNo,AnchorAngels) self:T({From,Event,To,"GID="..GID,"Stack="..AnchorStackNo}) local managedgroup=self.ManagedGrps[GID] if not managedgroup then self:E(self.lid.."**** GID "..GID.." Not Registered!") return self end managedgroup.AnchorStackNo=AnchorStackNo managedgroup.AnchorStackAngels=AnchorAngels managedgroup.Blocked=false local isPlayer=managedgroup.IsPlayer local isAI=managedgroup.IsAI local Group=managedgroup.Group local CallSign=managedgroup.CallSign or"Ghost 1" local AnchorName=Anchor.StationName or"unknown" local AnchorCoordTxt=Anchor.StationZoneCoordinateText or"unknown" local Angels=AnchorAngels or 25 local AnchorSpeed=self.CapSpeedBase or 270 local AuftragsNr=managedgroup.CurrentAuftrag local textTTS="" if self.PikesSpecialSwitch then local stationtxt=self.gettext:GetEntry("STATIONAT",self.locale) textTTS=string.format(stationtxt,CallSign,self.callsigntxt,AnchorName,Angels) else local stationtxt=self.gettext:GetEntry("STATIONATLONG",self.locale) textTTS=string.format(stationtxt,CallSign,self.callsigntxt,AnchorName,Angels,AnchorSpeed) end local ROEROT=self.AwacsROE..", "..self.AwacsROT local stationtxtsc=self.gettext:GetEntry("STATIONSCREEN",self.locale) local stationtxtta=self.gettext:GetEntry("STATIONTASK",self.locale) local textScreen=string.format(stationtxtsc,CallSign,self.callsigntxt,AnchorName,Angels,AnchorSpeed,AnchorCoordTxt,ROEROT) local TextTasking=string.format(stationtxtta,AnchorName,Angels,AnchorSpeed,AnchorCoordTxt,ROEROT) self:_NewRadioEntry(textTTS,textScreen,GID,isPlayer,isPlayer,true,false) managedgroup.CurrentTask=self:_CreateTaskForGroup(GID,AWACS.TaskDescription.ANCHOR,TextTasking,Anchor.StationZone) if isAI then local auftrag=managedgroup.FlightGroup:GetMissionCurrent() if auftrag then local auftragtype=auftrag:GetType() if auftragtype==AUFTRAG.Type.ALERT5 then local capauftrag=AUFTRAG:NewCAP(Anchor.StationZone,Angels*1000,AnchorSpeed,Anchor.StationZone:GetCoordinate(),0,15,{}) capauftrag:SetMissionRange(self.MaxMissionRange) capauftrag:SetTime(nil,((self.CAPTimeOnStation*3600)+(15*60))) capauftrag:AddAsset(managedgroup.FlightGroup) self.CatchAllMissions[#self.CatchAllMissions+1]=capauftrag managedgroup.FlightGroup:AddMission(capauftrag) auftrag:Cancel() else self:E("**** AssignedAnchor but Auftrag NOT ALERT5!") end else self:E("**** AssignedAnchor but NO Auftrag!") end end self.ManagedGrps[GID]=managedgroup return self end function AWACS:onafterNewCluster(From,Event,To,Cluster) self:T({From,Event,To,Cluster.index}) self.CID=self.CID+1 self.Countactcounter=self.Countactcounter+1 local ContactTable=Cluster.Contacts or{} local function GetFirstAliveContact(table) for _,_contact in pairs(table)do local contact=_contact if contact and contact.group and contact.group:IsAlive()then return contact,contact.group end end return nil end local Contact,Group=GetFirstAliveContact(ContactTable) if not Contact then return self end if Group and not Group:IsAirborne()then return self end local targetset=SET_GROUP:New() for _,_grp in pairs(ContactTable)do local grp=_grp targetset:AddGroup(grp.group,true) end local managedcontact={} managedcontact.CID=self.CID managedcontact.Contact=Contact managedcontact.Cluster=Cluster managedcontact.IFF=AWACS.IFF.BOGEY managedcontact.Target=TARGET:New(targetset) managedcontact.LinkedGroup=0 managedcontact.LinkedTask=0 managedcontact.Status=AWACS.TaskStatus.IDLE local phoneid=math.fmod(self.Countactcounter,27) if phoneid==0 then phoneid=1 end managedcontact.TargetGroupNaming=AWACS.Phonetic[phoneid] managedcontact.ReportingName=Contact.group:GetNatoReportingName() managedcontact.TACCallDone=false managedcontact.MeldCallDone=false managedcontact.EngagementTag="" local IsPopup=false if self.OpsZone:IsVec2InZone(Contact.position:GetVec2())then IsPopup=true end Contact.CID=managedcontact.CID Contact.TargetGroupNaming=managedcontact.TargetGroupNaming Cluster.CID=managedcontact.CID Cluster.TargetGroupNaming=managedcontact.TargetGroupNaming self.Contacts:Push(managedcontact,self.CID) local ContactCoordinate=Contact.position:GetVec2() local incontrolzone=self.ControlZone:IsVec2InZone(ContactCoordinate) local distance=1000000 if not self.GCI then distance=Contact.position:Get2DDistance(self.OrbitZone:GetCoordinate()) end local inborderzone=false if self.BorderZone then inborderzone=self.BorderZone:IsVec2InZone(ContactCoordinate) end if incontrolzone or inborderzone or(distance<=UTILS.NMToMeters(55))or IsPopup then self:_AnnounceContact(managedcontact,true,nil,false,managedcontact.TargetGroupNaming,IsPopup,managedcontact.ReportingName) end return self end function AWACS:onafterNewContact(From,Event,To,Contact) self:T({From,Event,To,Contact}) local tdist=self.ThreatDistance for _gid,_mgroup in pairs(self.ManagedGrps)do local managedgroup=_mgroup local group=managedgroup.Group if group and group:IsAlive()and group:IsAirborne()then local cpos=Contact.position or Contact.group:GetCoordinate() local mpos=group:GetCoordinate() local dist=cpos:Get2DDistance(mpos) dist=UTILS.Round(UTILS.MetersToNM(dist),0) if dist<=tdist then self:_ThreatRangeCall(_gid,Contact) end end end return self end function AWACS:onafterLostContact(From,Event,To,Contact) self:T({From,Event,To,Contact}) return self end function AWACS:onafterLostCluster(From,Event,To,Cluster,Mission) self:T({From,Event,To}) return self end function AWACS:onafterCheckTacticalQueue(From,Event,To) self:T({From,Event,To}) if self.clientset:CountAlive()==0 then self:T(self.lid.."No player connected.") self:__CheckTacticalQueue(-5) return self end for _name,_freq in pairs(self.TacticalSubscribers)do local Group=nil if _name then Group=GROUP:FindByName(_name) end if Group and Group:IsAlive()then self:_BogeyDope(Group,true) end end if(self.TacticalQueue:IsNotEmpty())then while self.TacticalQueue:Count()>0 do local RadioEntry=self.TacticalQueue:Pull() self:T({RadioEntry}) local frequency=self.TacticalBaseFreq if RadioEntry.GroupID and RadioEntry.GroupID~=0 then local managedgroup=self.ManagedGrps[RadioEntry.GroupID] if managedgroup and managedgroup.Group and managedgroup.Group:IsAlive()then local name=managedgroup.GroupName frequency=self.TacticalSubscribers[name] end end local gtext=RadioEntry.TextTTS if self.PathToGoogleKey and self.Backend~=MSRS.Backend.HOUND then gtext=string.format("%s",gtext) end self.TacticalSRSQ:NewTransmission(gtext,nil,self.TacticalSRS,nil,0.5,nil,nil,nil,frequency,self.TacticalModulation) self:T(RadioEntry.TextTTS) if RadioEntry.ToScreen and RadioEntry.TextScreen and(not self.SuppressScreenOutput)then if RadioEntry.GroupID and RadioEntry.GroupID~=0 then local managedgroup=self.ManagedGrps[RadioEntry.GroupID] if managedgroup and managedgroup.Group and managedgroup.Group:IsAlive()then MESSAGE:New(RadioEntry.TextScreen,20,"AWACS"):ToGroup(managedgroup.Group) self:T(RadioEntry.TextScreen) end else MESSAGE:New(RadioEntry.TextScreen,20,"AWACS"):ToCoalition(self.coalition) end end end end if not self:Is("Stopped")then self:__CheckTacticalQueue(-self.TacticalInterval) end return self end function AWACS:onafterCheckRadioQueue(From,Event,To) self:T({From,Event,To}) local nextcall=10 if(self.RadioQueue:IsNotEmpty()or self.PrioRadioQueue:IsNotEmpty())then local RadioEntry=nil if self.PrioRadioQueue:IsNotEmpty()then RadioEntry=self.PrioRadioQueue:Pull() else RadioEntry=self.RadioQueue:Pull() end self:T({RadioEntry}) if self.clientset:CountAlive()==0 then self:T(self.lid.."No player connected.") self:__CheckRadioQueue(-5) return self end if not RadioEntry.FromAI then if self.PathToGoogleKey and self.Backend~=MSRS.Backend.HOUND then local gtext=RadioEntry.TextTTS gtext=string.format("%s",gtext) self.AwacsSRS:PlayTextExt(gtext,nil,self.MultiFrequency,self.MultiModulation,self.Gender,self.Culture,self.Voice,self.Volume,"AWACS") else self.AwacsSRS:PlayTextExt(RadioEntry.TextTTS,nil,self.MultiFrequency,self.MultiModulation,self.Gender,self.Culture,self.Voice,self.Volume,"AWACS") end self:T(RadioEntry.TextTTS) else if RadioEntry.GroupID and RadioEntry.GroupID~=0 then local managedgroup=self.ManagedGrps[RadioEntry.GroupID] if managedgroup and managedgroup.FlightGroup and managedgroup.FlightGroup:IsAlive()then if self.PathToGoogleKey and self.Backend~=MSRS.Backend.HOUND then local gtext=RadioEntry.TextTTS gtext=string.format("%s",gtext) managedgroup.FlightGroup:RadioTransmission(gtext,1,false) else managedgroup.FlightGroup:RadioTransmission(RadioEntry.TextTTS,1,false) end self:T(RadioEntry.TextTTS) end end end if RadioEntry.Duration then nextcall=RadioEntry.Duration end if RadioEntry.ToScreen and RadioEntry.TextScreen and(not self.SuppressScreenOutput)then if RadioEntry.GroupID and RadioEntry.GroupID~=0 then local managedgroup=self.ManagedGrps[RadioEntry.GroupID] if managedgroup and managedgroup.Group and managedgroup.Group:IsAlive()then MESSAGE:New(RadioEntry.TextScreen,20,"AWACS"):ToGroup(managedgroup.Group) self:T(RadioEntry.TextScreen) end else MESSAGE:New(RadioEntry.TextScreen,20,"AWACS"):ToCoalition(self.coalition) end end end if self:Is("Running")then if self.PathToGoogleKey then nextcall=nextcall+self.GoogleTTSPadding else nextcall=nextcall+self.WindowsTTSPadding end self:__CheckRadioQueue(-nextcall) end return self end function AWACS:onafterEscortShiftChange(From,Event,To) self:T({From,Event,To}) if self.AwacsFG and self.ShiftChangeEscortsFlag and not self.ShiftChangeEscortsRequested then local awacs=self.AwacsFG:GetGroup() if awacs and awacs:IsAlive()then self.ShiftChangeEscortsRequested=true self.EscortsTimeStamp=timer.getTime() self:_StartEscorts(true) else self:E("**** AWACS group dead at onafterEscortShiftChange!") end end return self end function AWACS:onafterAwacsShiftChange(From,Event,To) self:T({From,Event,To}) if self.AwacsFG and self.ShiftChangeAwacsFlag and not self.ShiftChangeAwacsRequested then self.ShiftChangeAwacsRequested=true self.AwacsTimeStamp=timer.getTime() local AwacsAW=self.AirWing local mission=AUFTRAG:NewORBIT_RACETRACK(self.OrbitZone:GetCoordinate(),self.AwacsAngels*1000,self.Speed,self.Heading,self.Leg) self.CatchAllMissions[#self.CatchAllMissions+1]=mission local timeonstation=(self.AwacsTimeOnStation+self.ShiftChangeTime)*3600 mission:SetTime(nil,timeonstation) mission:SetMissionRange(self.MaxMissionRange) AwacsAW:AddMission(mission) self.AwacsMissionReplacement=mission end return self end function AWACS:onafterFlightOnMission(From,Event,To,FlightGroup,Mission) self:T({From,Event,To}) self:T("FlightGroup "..FlightGroup:GetName().." Mission "..Mission:GetName().." Type "..Mission:GetType()) self.CatchAllFGs[#self.CatchAllFGs+1]=FlightGroup if not self:Is("Stopped")then if not self.AwacsReady or self.ShiftChangeAwacsFlag or self.ShiftChangeEscortsFlag then self:_StartSettings(FlightGroup,Mission) elseif Mission and(Mission:GetType()==AUFTRAG.Type.CAP or Mission:GetType()==AUFTRAG.Type.ALERT5 or Mission:GetType()==AUFTRAG.Type.ORBIT)then if not self.FlightGroups:HasUniqueID(FlightGroup:GetName())then self:T("Pushing FG "..FlightGroup:GetName().." to stack!") self.FlightGroups:Push(FlightGroup,FlightGroup:GetName()) end end end return self end function AWACS:onafterReAnchor(From,Event,To,GID) self:T({From,Event,To,GID}) local managedgroup=self.ManagedGrps[GID] if managedgroup then if managedgroup.IsAI then local AIFG=managedgroup.FlightGroup if AIFG and AIFG:IsAlive()then if AIFG:IsFuelLow()or AIFG:IsOutOfMissiles()or AIFG:IsOutOfAmmo()then local destbase=AIFG.homebase if not destbase then destbase=self.Airbase end AIFG:RTB(destbase) self:_CheckOut(AIFG:GetGroup(),GID) self.AIRequested=self.AIRequested-1 else local Anchor=self.AnchorStacks:ReadByPointer(managedgroup.AnchorStackNo) local StationZone=Anchor.StationZone managedgroup.CurrentTask=self:_CreateTaskForGroup(GID,AWACS.TaskDescription.ANCHOR,"Re-Station AI",StationZone) managedgroup.HasAssignedTask=true local mission=AIFG:GetMissionCurrent() if mission then managedgroup.CurrentAuftrag=mission.auftragsnummer or 0 else managedgroup.CurrentAuftrag=0 end managedgroup.ContactCID=0 self.ManagedGrps[GID]=managedgroup local tostation=self.gettext:GetEntry("VECTORSTATION",self.locale) self:_MessageVector(GID,tostation,Anchor.StationZoneCoordinate,managedgroup.AnchorStackAngels) end else local savedcallsign=managedgroup.CallSign local textoptions={} textoptions[1]=self.gettext:GetEntry("TEXTOPTIONS1",self.locale) textoptions[2]=self.gettext:GetEntry("TEXTOPTIONS2",self.locale) textoptions[3]=self.gettext:GetEntry("TEXTOPTIONS3",self.locale) textoptions[4]=self.gettext:GetEntry("TEXTOPTIONS4",self.locale) local allstations=self.gettext:GetEntry("ALLSTATIONS",self.locale) local milestxt=self.gettext:GetEntry("MILES",self.locale) if managedgroup.LastKnownPosition then local lastknown=UTILS.DeepCopy(managedgroup.LastKnownPosition) local faded=textoptions[math.random(1,4)] local text=string.format("%s. %s. %s %s.",allstations,self.callsigntxt,faded,savedcallsign) local textScreen=string.format("%s, %s. %s %s.",allstations,self.callsigntxt,faded,savedcallsign) local brtext=self:_ToStringBULLS(lastknown) local brtexttts=self:_ToStringBULLS(lastknown,false,true) text=text.." "..brtexttts.." "..milestxt.."." textScreen=textScreen.." "..brtext.." "..milestxt.."." self:_NewRadioEntry(text,textScreen,0,false,self.debug,true,false,true) end self.ManagedGrps[GID]=nil end elseif managedgroup.IsPlayer then local PLFG=managedgroup.Group if PLFG and PLFG:IsAlive()then local Anchor=self.AnchorStacks:ReadByPointer(managedgroup.AnchorStackNo) local AnchorName=Anchor.StationName or"unknown" local AnchorCoordTxt=Anchor.StationZoneCoordinateText or"unknown" local Angels=managedgroup.AnchorStackAngels or 25 local AnchorSpeed=self.CapSpeedBase or 270 local StationZone=Anchor.StationZone local ROEROT=self.AwacsROE.." "..self.AwacsROT local stationtxt=self.gettext:GetEntry("STATIONTASK",self.locale) local TextTasking=string.format(stationtxt,AnchorName,Angels,AnchorSpeed,AnchorCoordTxt,ROEROT) managedgroup.CurrentTask=self:_CreateTaskForGroup(GID,AWACS.TaskDescription.ANCHOR,TextTasking,StationZone) managedgroup.HasAssignedTask=true managedgroup.ContactCID=0 self.ManagedGrps[GID]=managedgroup local vectortxt=self.gettext:GetEntry("VECTORSTATION",self.locale) self:_MessageVector(GID,vectortxt,Anchor.StationZoneCoordinate,managedgroup.AnchorStackAngels) else local savedcallsign=managedgroup.CallSign local textoptions={} textoptions[1]=self.gettext:GetEntry("TEXTOPTIONS1",self.locale) textoptions[2]=self.gettext:GetEntry("TEXTOPTIONS2",self.locale) textoptions[3]=self.gettext:GetEntry("TEXTOPTIONS3",self.locale) textoptions[4]=self.gettext:GetEntry("TEXTOPTIONS4",self.locale) local allstations=self.gettext:GetEntry("ALLSTATIONS",self.locale) local milestxt=self.gettext:GetEntry("MILES",self.locale) local faded=textoptions[math.random(1,4)] local text=string.format("%s. %s. %s %s.",allstations,self.callsigntxt,faded,savedcallsign) local textScreen=string.format("%s, %s. %s %s.",allstations,self.callsigntxt,faded,savedcallsign) if managedgroup.LastKnownPosition then local lastknown=UTILS.DeepCopy(managedgroup.LastKnownPosition) local brtext=self:_ToStringBULLS(lastknown) local brtexttts=self:_ToStringBULLS(lastknown,false,true) text=text.." "..brtexttts.." "..milestxt.."." textScreen=textScreen.." "..brtext.." "..milestxt.."." self:_NewRadioEntry(text,textScreen,0,false,self.debug,true,false,true) end self.ManagedGrps[GID]=nil end end end end end BRIGADE={ ClassName="BRIGADE", verbose=0, rearmingZones={}, refuellingZones={}, } BRIGADE.version="0.1.1" function BRIGADE:New(WarehouseName,BrigadeName) local self=BASE:Inherit(self,LEGION:New(WarehouseName,BrigadeName)) if not self then BASE:E(string.format("ERROR: Could not find warehouse %s!",WarehouseName)) return nil end self.lid=string.format("BRIGADE %s | ",self.alias) self:SetRetreatZones() if self:IsShip()then local wh=self.warehouse local group=wh:GetGroup() self.warehouseOpsGroup=NAVYGROUP:New(group) self.warehouseOpsElement=self.warehouseOpsGroup:GetElementByName(wh:GetName()) end self:AddTransition("*","ArmyOnMission","*") return self end function BRIGADE:AddPlatoon(Platoon) table.insert(self.cohorts,Platoon) self:AddAssetToPlatoon(Platoon,Platoon.Ngroups) Platoon:SetBrigade(self) if Platoon:IsStopped()then Platoon:Start() end return self end function BRIGADE:AddAssetToPlatoon(Platoon,Nassets) if Platoon then local Group=GROUP:FindByName(Platoon.templatename) if Group then local text=string.format("Adding asset %s to platoon %s",Group:GetName(),Platoon.name) self:T(self.lid..text) self:AddAsset(Group,Nassets,nil,nil,nil,nil,Platoon.skill,Platoon.livery,Platoon.name) else self:E(self.lid.."ERROR: Group does not exist!") end else self:E(self.lid.."ERROR: Platoon does not exit!") end return self end function BRIGADE:SetRetreatZones(RetreatZoneSet) self.retreatZones=RetreatZoneSet or SET_ZONE:New() return self end function BRIGADE:AddRetreatZone(RetreatZone) self.retreatZones:AddZone(RetreatZone) return self end function BRIGADE:GetRetreatZones() return self.retreatZones end function BRIGADE:AddRearmingZone(RearmingZone) local rearmingzone={} rearmingzone.zone=RearmingZone rearmingzone.mission=nil rearmingzone.marker=MARKER:New(rearmingzone.zone:GetCoordinate(),"Rearming Zone"):ToCoalition(self:GetCoalition()) table.insert(self.rearmingZones,rearmingzone) return rearmingzone end function BRIGADE:AddRefuellingZone(RefuellingZone) local supplyzone={} supplyzone.zone=RefuellingZone supplyzone.mission=nil supplyzone.marker=MARKER:New(supplyzone.zone:GetCoordinate(),"Refuelling Zone"):ToCoalition(self:GetCoalition()) table.insert(self.refuellingZones,supplyzone) return supplyzone end function BRIGADE:GetPlatoon(PlatoonName) local platoon=self:_GetCohort(PlatoonName) return platoon end function BRIGADE:GetPlatoonOfAsset(Asset) local platoon=self:GetPlatoon(Asset.squadname) return platoon end function BRIGADE:RemoveAssetFromPlatoon(Asset) local platoon=self:GetPlatoonOfAsset(Asset) if platoon then platoon:DelAsset(Asset) end end function BRIGADE:LoadBackAssetInPosition(Templatename,Position) self:T(self.lid.."LoadBackAssetInPosition: "..tostring(Templatename)) local nametbl=UTILS.Split(Templatename,"_") local name=nametbl[1] self:T(string.format("*** Target Platoon = %s ***",name)) local cohorts=self.cohorts or{} local thisasset=nil local found=false for _,_cohort in pairs(cohorts)do local asset=_cohort:GetName() self:T(string.format("*** Looking at Platoon = %s ***",asset)) if asset==name then self:T("**** Found Platoon ****") local cohassets=_cohort.assets or{} for _,_zug in pairs(cohassets)do local zug=_zug if zug.assignment==name and zug.requested==false then self:T("**** Found Asset ****") found=true thisasset=zug break end end end end if found then thisasset.rid=thisasset.uid thisasset.requested=false thisasset.score=100 thisasset.missionTask="CAS" thisasset.spawned=true local template=thisasset.templatename local alias=thisasset.spawngroupname local spawnasset=SPAWN:NewWithAlias(template,alias) :InitDelayOff() :SpawnFromCoordinate(Position) local request={} request.assignment=name request.warehouse=self request.assets={thisasset} request.ntransporthome=0 request.ndelivered=0 request.ntransport=0 request.cargoattribute=thisasset.attribute request.category=thisasset.category request.cargoassets={thisasset} request.assetdesc=WAREHOUSE.Descriptor.ASSETLIST request.cargocategory=thisasset.category request.toself=true request.transporttype=WAREHOUSE.TransportType.SELFPROPELLED request.assetproblem={} request.born=true request.prio=50 request.uid=thisasset.uid request.airbase=nil request.timestamp=timer.getAbsTime() request.assetdescval={thisasset} request.nasset=1 request.cargogroupset=SET_GROUP:New() request.cargogroupset:AddGroup(spawnasset) request.iscargo=true self:__AssetSpawned(2,spawnasset,thisasset,request) end return self end function BRIGADE:onafterStart(From,Event,To) self:GetParent(self,BRIGADE).onafterStart(self,From,Event,To) self:I(self.lid..string.format("Starting BRIGADE v%s",BRIGADE.version)) end function BRIGADE:onafterStatus(From,Event,To) self:GetParent(self).onafterStatus(self,From,Event,To) local fsmstate=self:GetState() self:CheckTransportQueue() self:CheckMissionQueue() for _,_rearmingzone in pairs(self.rearmingZones)do local rearmingzone=_rearmingzone if(not rearmingzone.mission)or rearmingzone.mission:IsOver()then rearmingzone.mission=AUFTRAG:NewAMMOSUPPLY(rearmingzone.zone) self:AddMission(rearmingzone.mission) end end for _,_supplyzone in pairs(self.refuellingZones)do local supplyzone=_supplyzone if(not supplyzone.mission)or supplyzone.mission:IsOver()then supplyzone.mission=AUFTRAG:NewFUELSUPPLY(supplyzone.zone) self:AddMission(supplyzone.mission) end end self:_TacticalOverview() if self.verbose>=1 then local Nmissions=self:CountMissionsInQueue() local Npq,Np,Nq=self:CountAssetsOnMission() local assets=string.format("%d [OnMission: Total=%d, Active=%d, Queued=%d]",self:CountAssets(),Npq,Np,Nq) local text=string.format("%s: Missions=%d, Platoons=%d, Assets=%s",fsmstate,Nmissions,#self.cohorts,assets) self:I(self.lid..text) end if self.verbose>=2 then local text=string.format("Missions Total=%d:",#self.missionqueue) for i,_mission in pairs(self.missionqueue)do local mission=_mission local prio=string.format("%d/%s",mission.prio,tostring(mission.importance));if mission.urgent then prio=prio.." (!)"end local assets=string.format("%d/%d",mission:CountOpsGroups(),mission.Nassets or 0) local target=string.format("%d/%d Damage=%.1f",mission:CountMissionTargets(),mission:GetTargetInitialNumber(),mission:GetTargetDamage()) text=text..string.format("\n[%d] %s %s: Status=%s, Prio=%s, Assets=%s, Targets=%s",i,mission.name,mission.type,mission.status,prio,assets,target) end self:I(self.lid..text) end if self.verbose>=2 then local text=string.format("Transports Total=%d:",#self.transportqueue) for i,_transport in pairs(self.transportqueue)do local transport=_transport local prio=string.format("%d/%s",transport.prio,tostring(transport.importance));if transport.urgent then prio=prio.." (!)"end local carriers=string.format("Ncargo=%d/%d, Ncarriers=%d",transport.Ncargo,transport.Ndelivered,transport.Ncarrier) text=text..string.format("\n[%d] UID=%d: Status=%s, Prio=%s, Cargo: %s",i,transport.uid,transport:GetState(),prio,carriers) end self:I(self.lid..text) end if self.verbose>=3 then local text="Platoons:" for i,_platoon in pairs(self.cohorts)do local platoon=_platoon local callsign=platoon.callsignName and UTILS.GetCallsignName(platoon.callsignName)or"N/A" local modex=platoon.modex and platoon.modex or-1 local skill=platoon.skill and tostring(platoon.skill)or"N/A" text=text..string.format("\n* %s %s: %s*%d/%d, Callsign=%s, Modex=%d, Skill=%s",platoon.name,platoon:GetState(),platoon.aircrafttype,platoon:CountAssets(true),#platoon.assets,callsign,modex,skill) end self:I(self.lid..text) end if self.verbose>=4 then local text="Rearming Zones:" for i,_rearmingzone in pairs(self.rearmingZones)do local rearmingzone=_rearmingzone text=text..string.format("\n* %s: Mission status=%s, suppliers=%d",rearmingzone.zone:GetName(),rearmingzone.mission:GetState(),rearmingzone.mission:CountOpsGroups()) end self:I(self.lid..text) end if self.verbose>=4 then local text="Refuelling Zones:" for i,_refuellingzone in pairs(self.refuellingZones)do local refuellingzone=_refuellingzone text=text..string.format("\n* %s: Mission status=%s, suppliers=%d",refuellingzone.zone:GetName(),refuellingzone.mission:GetState(),refuellingzone.mission:CountOpsGroups()) end self:I(self.lid..text) end if self.verbose>=5 then local text="Assets in stock:" for i,_asset in pairs(self.stock)do local asset=_asset text=text..string.format("\n* %s: spawned=%s",asset.spawngroupname,tostring(asset.spawned)) end self:I(self.lid..text) end if self.verbose>=3 then local Ntotal=0 local Nspawned=0 local Nrequested=0 local Nreserved=0 local Nstock=0 local text="\n===========================================\n" text=text.."Assets:" local legion=self for _,_cohort in pairs(legion.cohorts)do local cohort=_cohort for _,_asset in pairs(cohort.assets)do local asset=_asset local state="In Stock" if asset.flightgroup then state=asset.flightgroup:GetState() local mission=legion:GetAssetCurrentMission(asset) if mission then state=state..string.format(", Mission \"%s\" [%s]",mission:GetName(),mission:GetType()) end else if asset.spawned then env.info("FF ERROR: asset has opsgroup but is NOT spawned!") end if asset.requested and asset.isReserved then env.info("FF ERROR: asset is requested and reserved. Should not be both!") state="Reserved+Requested!" elseif asset.isReserved then state="Reserved" elseif asset.requested then state="Requested" end end text=text..string.format("\n[UID=%03d] %s Legion=%s [%s]: State=%s [RID=%s]", asset.uid,asset.spawngroupname,legion.alias,cohort.name,state,tostring(asset.rid)) if asset.spawned then Nspawned=Nspawned+1 end if asset.requested then Nrequested=Nrequested+1 end if asset.isReserved then Nreserved=Nreserved+1 end if not(asset.spawned or asset.requested or asset.isReserved)then Nstock=Nstock+1 end Ntotal=Ntotal+1 end end text=text.."\n-------------------------------------------" text=text..string.format("\nNstock = %d",Nstock) text=text..string.format("\nNreserved = %d",Nreserved) text=text..string.format("\nNrequested = %d",Nrequested) text=text..string.format("\nNspawned = %d",Nspawned) text=text..string.format("\nNtotal = %d (=%d)",Ntotal,Nstock+Nspawned+Nrequested+Nreserved) text=text.."\n===========================================" self:I(self.lid..text) end end function BRIGADE:onafterArmyOnMission(From,Event,To,ArmyGroup,Mission) self:T(self.lid..string.format("Group %s on %s mission %s",ArmyGroup:GetName(),Mission:GetType(),Mission:GetName())) end CHIEF={ ClassName="CHIEF", verbose=0, lid=nil, targetqueue={}, zonequeue={}, borderzoneset=nil, yellowzoneset=nil, engagezoneset=nil, tacview=false, Nsuccess=0, Nfailure=0, LegionRecruitMinRange={} } CHIEF.DEFCON={ GREEN="Green", YELLOW="Yellow", RED="Red", } CHIEF.Strategy={ PASSIVE="Passive", DEFENSIVE="Defensive", OFFENSIVE="Offensive", AGGRESSIVE="Aggressive", TOTALWAR="Total War" } CHIEF.version="0.7.1" function CHIEF:New(Coalition,AgentSet,Alias) Alias=Alias or"CHIEF" if type(Coalition)=="string"then if string.lower(Coalition)=="blue"then Coalition=coalition.side.BLUE elseif string.lower(Coalition)=="red"then Coalition=coalition.side.RED else Coalition=coalition.side.NEUTRAL end end local self=BASE:Inherit(self,INTEL:New(AgentSet,Coalition,Alias)) self:SetBorderZones() self:SetConflictZones() self:SetAttackZones() self:SetCorridorZones() self:SetRejectZones() self:SetThreatLevelRange() self.Defcon=CHIEF.DEFCON.GREEN self.strategy=CHIEF.Strategy.DEFENSIVE self.TransportCategories={Group.Category.HELICOPTER} self.commander=COMMANDER:New(Coalition,Alias) self:AddTransition("*","MissionAssign","*") self:AddTransition("*","MissionCancel","*") self:AddTransition("*","TransportCancel","*") self:AddTransition("*","OpsOnMission","*") self:AddTransition("*","ZoneCaptured","*") self:AddTransition("*","ZoneLost","*") self:AddTransition("*","ZoneEmpty","*") self:AddTransition("*","ZoneAttacked","*") self:AddTransition("*","DefconChange","*") self:AddTransition("*","StrategyChange","*") self:AddTransition("*","LegionLost","*") return self end function CHIEF:SetAirToAny() self:SetFilterCategory({}) return self end function CHIEF:SetAirToAir() self:SetFilterCategory({Unit.Category.AIRPLANE,Unit.Category.HELICOPTER}) return self end function CHIEF:SetAirToGround() self:SetFilterCategory({Unit.Category.GROUND_UNIT}) return self end function CHIEF:SetAirToSea() self:SetFilterCategory({Unit.Category.SHIP}) return self end function CHIEF:SetAirToSurface() self:SetFilterCategory({Unit.Category.GROUND_UNIT,Unit.Category.SHIP}) return self end function CHIEF:SetThreatLevelRange(ThreatLevelMin,ThreatLevelMax) self.threatLevelMin=ThreatLevelMin or 1 self.threatLevelMax=ThreatLevelMax or 10 return self end function CHIEF:SetDefcon(Defcon) local gotit=false for _,defcon in pairs(CHIEF.DEFCON)do if defcon==Defcon then gotit=true end end if not gotit then self:E(self.lid..string.format("ERROR: Unknown DEFCON specified! Dont know defcon=%s",tostring(Defcon))) return self end if Defcon~=self.Defcon then self:DefconChange(Defcon) end self.Defcon=Defcon return self end function CHIEF:CreateResource(MissionType,Nmin,Nmax,Attributes,Properties,Categories) local resources={} local resource=self:AddToResource(resources,MissionType,Nmin,Nmax,Attributes,Properties,Categories) return resources,resource end function CHIEF:AddToResource(Resource,MissionType,Nmin,Nmax,Attributes,Properties,Categories) local resource={} resource.MissionType=MissionType resource.Nmin=Nmin or 1 resource.Nmax=Nmax or Nmin resource.Attributes=UTILS.EnsureTable(Attributes,true) resource.Properties=UTILS.EnsureTable(Properties,true) resource.Categories=UTILS.EnsureTable(Categories,true) resource.carrierNmin=nil resource.carrierNmax=nil resource.carrierAttributes=nil resource.carrierProperties=nil resource.carrierCategories=nil table.insert(Resource,resource) if self.verbose>10 then local text="Resource:" for _,_r in pairs(Resource)do local r=_r text=text..string.format("\nmission=%s, Nmin=%d, Nmax=%d, attribute=%s, properties=%s",r.MissionType,r.Nmin,r.Nmax,tostring(r.Attributes[1]),tostring(r.Properties[1])) end self:I(self.lid..text) end return resource end function CHIEF:AddTransportToResource(Resource,Nmin,Nmax,CarrierAttributes,CarrierProperties,CarrierCategories) Resource.carrierNmin=Nmin or 1 Resource.carrierNmax=Nmax or Nmin Resource.carrierCategories=UTILS.EnsureTable(CarrierCategories,true) Resource.carrierAttributes=UTILS.EnsureTable(CarrierAttributes,true) Resource.carrierProperties=UTILS.EnsureTable(CarrierProperties,true) return self end function CHIEF:DeleteFromResource(Resource,MissionType) for i=#Resource,1,-1 do local resource=Resource[i] if resource.MissionType==MissionType then if resource.mission and resource.mission:IsNotOver()then resource.mission:Cancel() end table.remove(Resource,i) end end return self end function CHIEF:SetResponseOnTarget(NassetsMin,NassetsMax,ThreatLevel,TargetCategory,MissionType,Nunits,Defcon,Strategy) local bla={} bla.nAssetMin=NassetsMin or 1 bla.nAssetMax=NassetsMax or bla.nAssetMin bla.threatlevel=ThreatLevel or 0 bla.targetCategory=TargetCategory bla.missionType=MissionType bla.nUnits=Nunits or 1 bla.defcon=Defcon bla.strategy=Strategy self.assetNumbers=self.assetNumbers or{} table.insert(self.assetNumbers,bla) end function CHIEF:_GetAssetsForTarget(Target,MissionType) local threatlevel=Target:GetThreatLevelMax() local nUnits=Target.N0 local targetcategory=Target:GetCategory() self:T(self.lid..string.format("Getting number of assets for target with TL=%d, Category=%s, nUnits=%s, MissionType=%s",threatlevel,targetcategory,nUnits,tostring(MissionType))) local candidates={} local threatlevelMatch=nil for _,_assetnumber in pairs(self.assetNumbers or{})do local assetnumber=_assetnumber if(threatlevelMatch==nil and threatlevel>=assetnumber.threatlevel)or(threatlevelMatch~=nil and threatlevelMatch==threatlevel)then if threatlevelMatch==nil then threatlevelMatch=threatlevel end local nMatch=0 local cand=true if assetnumber.targetCategory~=nil then if assetnumber.targetCategory==targetcategory then nMatch=nMatch+1 else cand=false end end if MissionType and assetnumber.missionType~=nil then if assetnumber.missionType==MissionType then nMatch=nMatch+1 else cand=false end end if assetnumber.nUnits~=nil then if assetnumber.nUnits>=nUnits then nMatch=nMatch+1 else cand=false end end if assetnumber.defcon~=nil then if assetnumber.defcon==self.Defcon then nMatch=nMatch+1 else cand=false end end if assetnumber.strategy~=nil then if assetnumber.strategy==self.strategy then nMatch=nMatch+1 else cand=false end end if cand then table.insert(candidates,{assetnumber=assetnumber,nMatch=nMatch}) end end end if#candidates>0 then local function _sort(a,b) return a.nMatch>b.nMatch end table.sort(candidates,_sort) local candidate=candidates[1] local an=candidate.assetnumber self:T(self.lid..string.format("Picking candidate with %d matches: NassetsMin=%d, NassetsMax=%d, ThreatLevel=%d, TargetCategory=%s, MissionType=%s, Defcon=%s, Strategy=%s", candidate.nMatch,an.nAssetMin,an.nAssetMax,an.threatlevel,tostring(an.targetCategory),tostring(an.missionType),tostring(an.defcon),tostring(an.strategy))) return an.nAssetMin,an.nAssetMax else return 1,1 end end function CHIEF:GetDefcon(Defcon) return self.Defcon end function CHIEF:SetLimitMission(Limit,MissionType) self.commander:SetLimitMission(Limit,MissionType) return self end function CHIEF:SetTacticalOverviewOn() self.tacview=true return self end function CHIEF:SetTacticalOverviewOff() self.tacview=false return self end function CHIEF:SetStrategy(Strategy) if Strategy~=self.strategy then self:StrategyChange(Strategy) end self.strategy=Strategy return self end function CHIEF:GetStrategy() return self.strategy end function CHIEF:GetDefcon(Defcon) return self.Defcon end function CHIEF:GetCommander() return self.commander end function CHIEF:AddAirwing(Airwing) self:AddLegion(Airwing) return self end function CHIEF:AddBrigade(Brigade) self:AddLegion(Brigade) return self end function CHIEF:AddFleet(Fleet) self:AddLegion(Fleet) return self end function CHIEF:AddLegion(Legion) Legion.chief=self self.commander:AddLegion(Legion) return self end function CHIEF:RemoveLegion(Legion) Legion.chief=nil self.commander:RemoveLegion(Legion) return self end function CHIEF:AddMission(Mission) Mission.chief=self Mission.statusChief=AUFTRAG.Status.PLANNED self:I(self.lid..string.format("Adding mission #%d",Mission.auftragsnummer)) self.commander:AddMission(Mission) return self end function CHIEF:RemoveMission(Mission) Mission.chief=nil self.commander:RemoveMission(Mission) return self end function CHIEF:AddOpsTransport(Transport) Transport.chief=self self.commander:AddOpsTransport(Transport) return self end function CHIEF:RemoveTransport(Transport) Transport.chief=nil self.commander:RemoveTransport(Transport) return self end function CHIEF:AddTarget(Target) if not self:IsTarget(Target)then Target.chief=self table.insert(self.targetqueue,Target) end return self end function CHIEF:IsTarget(Target) for _,_target in pairs(self.targetqueue)do local target=_target if target.uid==Target.uid or target:GetName()==Target:GetName()then return true end end return false end function CHIEF:RemoveTarget(Target) for i,_target in pairs(self.targetqueue)do local target=_target if target.uid==Target.uid then self:T(self.lid..string.format("Removing target %s from queue",Target.name)) table.remove(self.targetqueue,i) break end end return self end function CHIEF:AddStrategicZone(OpsZone,Priority,Importance,ResourceOccupied,ResourceEmpty) local stratzone={} stratzone.opszone=OpsZone stratzone.prio=Priority or 50 stratzone.importance=Importance stratzone.missions={} if OpsZone:IsStopped()then OpsZone:Start() end if ResourceOccupied then stratzone.resourceOccup=UTILS.DeepCopy(ResourceOccupied) else stratzone.resourceOccup=self:CreateResource(AUFTRAG.Type.ARTY,1,2) self:AddToResource(stratzone.resourceOccup,AUFTRAG.Type.CASENHANCED,1,2) end if ResourceEmpty then stratzone.resourceEmpty=UTILS.DeepCopy(ResourceEmpty) else local resourceEmpty,resourceInfantry=self:CreateResource(AUFTRAG.Type.ONGUARD,1,3,GROUP.Attribute.GROUND_INFANTRY) self:AddToResource(resourceEmpty,AUFTRAG.Type.ONGUARD,0,1,GROUP.Attribute.GROUND_TANK) self:AddToResource(resourceEmpty,AUFTRAG.Type.ONGUARD,0,1,GROUP.Attribute.GROUND_IFV) self:AddTransportToResource(resourceInfantry,0,1,{GROUP.Attribute.AIR_TRANSPORTHELO,GROUP.Attribute.GROUND_APC}) stratzone.resourceEmpty=resourceEmpty end table.insert(self.zonequeue,stratzone) OpsZone:_AddChief(self) return stratzone end function CHIEF:SetStrategicZoneResourceEmpty(StrategicZone,Resource,NoCopy) if NoCopy then StrategicZone.resourceEmpty=Resource else StrategicZone.resourceEmpty=UTILS.DeepCopy(Resource) end return self end function CHIEF:SetStrategicZoneResourceOccupied(StrategicZone,Resource,NoCopy) if NoCopy then StrategicZone.resourceOccup=Resource else StrategicZone.resourceOccup=UTILS.DeepCopy(Resource) end return self end function CHIEF:GetStrategicZoneResourceEmpty(StrategicZone) return StrategicZone.resourceEmpty end function CHIEF:GetStrategicZoneResourceOccupied(StrategicZone) return StrategicZone.resourceOccup end function CHIEF:RemoveStrategicZone(OpsZone,Delay) if Delay and Delay>0 then self:ScheduleOnce(Delay,CHIEF.RemoveStrategicZone,self,OpsZone) else for i=#self.zonequeue,1,-1 do local stratzone=self.zonequeue[i] if OpsZone.zoneName==stratzone.opszone.zoneName then self:T(self.lid..string.format("Removing OPS zone \"%s\" from queue! All running missions will be cancelled",OpsZone.zoneName)) for _,_resource in pairs(stratzone.resourceEmpty)do local resource=_resource if resource.mission and resource.mission:IsNotOver()then resource.mission:Cancel() end end for _,_resource in pairs(stratzone.resourceOccup)do local resource=_resource if resource.mission and resource.mission:IsNotOver()then resource.mission:Cancel() end end table.remove(self.zonequeue,i) return self end end end return self end function CHIEF:AddRearmingZone(RearmingZone) local supplyzone=self.commander:AddRearmingZone(RearmingZone) return supplyzone end function CHIEF:AddRefuellingZone(RefuellingZone) local supplyzone=self.commander:AddRefuellingZone(RefuellingZone) return supplyzone end function CHIEF:AddCapZone(Zone,Altitude,Speed,Heading,Leg) local zone=self.commander:AddCapZone(Zone,Altitude,Speed,Heading,Leg) return zone end function CHIEF:AddGciCapZone(Zone,Altitude,Speed,Heading,Leg) local zone=self.commander:AddGciCapZone(Zone,Altitude,Speed,Heading,Leg) return zone end function CHIEF:RemoveGciCapZone(Zone) local zone=self.commander:RemoveGciCapZone(Zone) return zone end function CHIEF:AddAwacsZone(Zone,Altitude,Speed,Heading,Leg) local zone=self.commander:AddAwacsZone(Zone,Altitude,Speed,Heading,Leg) return zone end function CHIEF:RemoveAwacsZone(Zone) local zone=self.commander:RemoveAwacsZone(Zone) return zone end function CHIEF:AddTankerZone(Zone,Altitude,Speed,Heading,Leg,RefuelSystem) local zone=self.commander:AddTankerZone(Zone,Altitude,Speed,Heading,Leg,RefuelSystem) return zone end function CHIEF:RemoveTankerZone(Zone) local zone=self.commander:RemoveTankerZone(Zone) return zone end function CHIEF:SetBorderZones(BorderZoneSet) self.borderzoneset=BorderZoneSet or SET_ZONE:New() return self end function CHIEF:AddBorderZone(Zone) self.borderzoneset:AddZone(Zone) return self end function CHIEF:RemoveBorderZone(Zone) self.borderzoneset:Remove(Zone:GetName()) return self end function CHIEF:SetConflictZones(ZoneSet) self.yellowzoneset=ZoneSet or SET_ZONE:New() return self end function CHIEF:AddConflictZone(Zone) self.yellowzoneset:AddZone(Zone) return self end function CHIEF:RemoveConflictZone(Zone) self.yellowzoneset:Remove(Zone:GetName()) return self end function CHIEF:SetAttackZones(ZoneSet) self.engagezoneset=ZoneSet or SET_ZONE:New() return self end function CHIEF:AddAttackZone(Zone) self.engagezoneset:AddZone(Zone) return self end function CHIEF:RemoveAttackZone(Zone) self.engagezoneset:Remove(Zone:GetName()) return self end function CHIEF:AllowGroundTransport() env.warning("WARNING: CHIEF:AllowGroundTransport() is deprecated and will be removed in the future!") self.TransportCategories={Group.Category.GROUND,Group.Category.HELICOPTER} return self end function CHIEF:ForbidGroundTransport() env.warning("WARNING: CHIEF:ForbidGroundTransport() is deprecated and will be removed in the future!") self.TransportCategories={Group.Category.HELICOPTER} return self end function CHIEF:IsPassive() return self.strategy==CHIEF.Strategy.PASSIVE end function CHIEF:IsDefensive() return self.strategy==CHIEF.Strategy.DEFENSIVE end function CHIEF:IsOffensive() return self.strategy==CHIEF.Strategy.OFFENSIVE end function CHIEF:IsAgressive() return self.strategy==CHIEF.Strategy.AGGRESSIVE end function CHIEF:IsTotalWar() return self.strategy==CHIEF.Strategy.TOTALWAR end function CHIEF:onafterStart(From,Event,To) local text=string.format("Starting Chief of Staff") self:I(self.lid..text) self:GetParent(self).onafterStart(self,From,Event,To) if self.commander then if self.commander:GetState()=="NotReadyYet"then self.commander:Start() end end end function CHIEF:onafterStatus(From,Event,To) self:GetParent(self).onafterStatus(self,From,Event,To) local fsmstate=self:GetState() for _,_contact in pairs(self.ContactsLost)do local contact=_contact if contact.mission and contact.mission:IsNotOver()then local text=string.format("Lost contact to target %s! %s mission %s will be cancelled.",contact.groupname,contact.mission.type:upper(),contact.mission.name) MESSAGE:New(text,120,"CHIEF"):ToAll() self:T(self.lid..text) contact.mission:Cancel() end if contact.target then self:RemoveTarget(contact.target) end end self.Nborder=0;self.Nconflict=0;self.Nattack=0 for _,_contact in pairs(self.Contacts)do local contact=_contact local group=contact.group local inred=self:CheckGroupInBorder(group) if inred then self.Nborder=self.Nborder+1 end local inyellow=self:CheckGroupInConflict(group) if inyellow then self.Nconflict=self.Nconflict+1 end local inattack=self:CheckGroupInAttack(group) if inattack then self.Nattack=self.Nattack+1 end if not contact.target then local Target=TARGET:New(contact.group) contact.target=Target Target.contact=contact self:AddTarget(Target) end end if self.Nborder>0 then self:SetDefcon(CHIEF.DEFCON.RED) elseif self.Nconflict>0 then self:SetDefcon(CHIEF.DEFCON.YELLOW) else self:SetDefcon(CHIEF.DEFCON.GREEN) end self:CheckTargetQueue() for _,_target in pairs(self.targetqueue)do local target=_target if target and target:IsAlive()and target.chief and target.mission and target.mission:IsNotOver()then local inborder=self:CheckTargetInZones(target,self.borderzoneset) local inyellow=self:CheckTargetInZones(target,self.yellowzoneset) local inattack=self:CheckTargetInZones(target,self.engagezoneset) if self.strategy==CHIEF.Strategy.PASSIVE then self:T(self.lid..string.format("Cancelling mission for target %s as strategy is PASSIVE",target:GetName())) target.mission:Cancel() elseif self.strategy==CHIEF.Strategy.DEFENSIVE then if not inborder then self:T(self.lid..string.format("Cancelling mission for target %s as strategy is DEFENSIVE and not inside border",target:GetName())) target.mission:Cancel() end elseif self.strategy==CHIEF.Strategy.OFFENSIVE then if not(inborder or inyellow)then self:T(self.lid..string.format("Cancelling mission for target %s as strategy is OFFENSIVE and not inside border or conflict",target:GetName())) target.mission:Cancel() end elseif self.strategy==CHIEF.Strategy.AGGRESSIVE then if not(inborder or inyellow or inattack)then self:T(self.lid..string.format("Cancelling mission for target %s as strategy is AGGRESSIVE and not inside border, conflict or attack",target:GetName())) target.mission:Cancel() end elseif self.strategy==CHIEF.Strategy.TOTALWAR then end end end self:CheckOpsZoneQueue() self:_TacticalOverview() if self.verbose>=1 then local Nassets=self.commander:CountAssets() local Ncontacts=#self.Contacts local Nmissions=#self.commander.missionqueue local Ntargets=#self.targetqueue local text=string.format("Defcon=%s Strategy=%s: Assets=%d, Contacts=%d [Border=%d, Conflict=%d, Attack=%d], Targets=%d, Missions=%d", self.Defcon,self.strategy,Nassets,Ncontacts,self.Nborder,self.Nconflict,self.Nattack,Ntargets,Nmissions) self:I(self.lid..text) end if self.verbose>=2 and#self.Contacts>0 then local text="Contacts:" for i,_contact in pairs(self.Contacts)do local contact=_contact local mtext="N/A" if contact.mission then mtext=string.format("\"%s\" [%s] %s",contact.mission:GetName(),contact.mission:GetType(),contact.mission.status:upper()) end text=text..string.format("\n[%d] %s Type=%s (%s): Threat=%d Mission=%s",i,contact.groupname,contact.categoryname,contact.typename,contact.threatlevel,mtext) end self:I(self.lid..text) end if self.verbose>=3 and#self.targetqueue>0 then local text="Targets:" for i,_target in pairs(self.targetqueue)do local target=_target local mtext="N/A" if target.mission then mtext=string.format("\"%s\" [%s] %s",target.mission:GetName(),target.mission:GetType(),target.mission.status:upper()) end text=text..string.format("\n[%d] %s: Category=%s, prio=%d, importance=%d, alive=%s [%.1f/%.1f], Mission=%s", i,target:GetName(),target.category,target.prio,target.importance or-1,tostring(target:IsAlive()),target:GetLife(),target:GetLife0(),mtext) end self:I(self.lid..text) end if self.verbose>=4 and#self.commander.missionqueue>0 then local text="Mission queue:" for i,_mission in pairs(self.commander.missionqueue)do local mission=_mission local target=mission:GetTargetName()or"unknown" text=text..string.format("\n[%d] %s (%s): status=%s, target=%s",i,mission.name,mission.type,mission.status,target) end self:I(self.lid..text) end if self.verbose>=4 and#self.zonequeue>0 then local text="Zone queue:" for i,_stratzone in pairs(self.zonequeue)do local stratzone=_stratzone local opszone=stratzone.opszone local owner=UTILS.GetCoalitionName(opszone.ownerCurrent) local prevowner=UTILS.GetCoalitionName(opszone.ownerPrevious) text=text..string.format("\n[%d] %s [%s]: owner=%s [%s] (prio=%d, importance=%s): Blue=%d, Red=%d, Neutral=%d", i,opszone.zone:GetName(),opszone:GetState(),owner,prevowner,stratzone.prio,tostring(stratzone.importance),opszone.Nblu,opszone.Nred,opszone.Nnut) end self:I(self.lid..text) end if self.verbose>=5 then local text="Assets:" for _,missiontype in pairs(AUFTRAG.Type)do local N=self.commander:CountAssets(nil,missiontype) if N>0 then text=text..string.format("\n- %s: %d",missiontype,N) end end self:I(self.lid..text) local text="Assets:" for _,attribute in pairs(WAREHOUSE.Attribute)do local N=self.commander:CountAssets(nil,nil,attribute) if N>0 or self.verbose>=10 then text=text..string.format("\n- %s: %d",attribute,N) end end self:T(self.lid..text) end end function CHIEF:onafterMissionAssign(From,Event,To,Mission,Legions) if self.commander then self:T(self.lid..string.format("Assigning mission %s (%s) to COMMANDER",Mission.name,Mission.type)) Mission.chief=self Mission.statusChief=AUFTRAG.Status.QUEUED self.commander:MissionAssign(Mission,Legions) else self:E(self.lid..string.format("Mission cannot be assigned as no COMMANDER is defined!")) end end function CHIEF:onafterMissionCancel(From,Event,To,Mission) self:T(self.lid..string.format("Cancelling mission %s (%s) in status %s",Mission.name,Mission.type,Mission.status)) Mission.statusChief=AUFTRAG.Status.CANCELLED if Mission:IsPlanned()then self:RemoveMission(Mission) else if Mission.commander then Mission.commander:MissionCancel(Mission) end end end function CHIEF:onafterTransportCancel(From,Event,To,Transport) self:T(self.lid..string.format("Cancelling transport UID=%d in status %s",Transport.uid,Transport:GetState())) if Transport:IsPlanned()then self:RemoveTransport(Transport) else if Transport.commander then Transport.commander:TransportCancel(Transport) end end end function CHIEF:onafterDefconChange(From,Event,To,Defcon) self:T(self.lid..string.format("Changing Defcon from %s --> %s",self.Defcon,Defcon)) end function CHIEF:onafterStrategyChange(From,Event,To,Strategy) self:T(self.lid..string.format("Changing Strategy from %s --> %s",self.strategy,Strategy)) end function CHIEF:onafterOpsOnMission(From,Event,To,OpsGroup,Mission) self:T(self.lid..string.format("Group %s on mission %s [%s]",OpsGroup:GetName(),Mission:GetName(),Mission:GetType())) end function CHIEF:onafterZoneCaptured(From,Event,To,OpsZone) self:T(self.lid..string.format("Zone %s captured!",OpsZone:GetName())) end function CHIEF:onafterZoneLost(From,Event,To,OpsZone) self:T(self.lid..string.format("Zone %s lost!",OpsZone:GetName())) end function CHIEF:onafterZoneEmpty(From,Event,To,OpsZone) self:T(self.lid..string.format("Zone %s empty!",OpsZone:GetName())) end function CHIEF:onafterZoneAttacked(From,Event,To,OpsZone) self:T(self.lid..string.format("Zone %s attacked!",OpsZone:GetName())) end function CHIEF:_TacticalOverview() if self.tacview then local NassetsTotal=self.commander:CountAssets() local NassetsStock=self.commander:CountAssets(true) local Ncontacts=#self.Contacts local NmissionsTotal=#self.commander.missionqueue local NmissionsRunni=self.commander:CountMissions(AUFTRAG.Type,true) local Ntargets=#self.targetqueue local Nzones=#self.zonequeue local Nagents=self.detectionset:CountAlive() local text=string.format("Tactical Overview\n") text=text..string.format("=================\n") text=text..string.format("Strategy: %s - Defcon: %s - Agents=%s\n",self.strategy,self.Defcon,Nagents) text=text..string.format("Contacts: %d [Border=%d, Conflict=%d, Attack=%d]\n",Ncontacts,self.Nborder,self.Nconflict,self.Nattack) text=text..string.format("Assets: %d [Active=%d, Stock=%d]\n",NassetsTotal,NassetsTotal-NassetsStock,NassetsStock) text=text..string.format("Targets: %d\n",Ntargets) text=text..string.format("Missions: %d [Running=%d/%d - Success=%d, Failure=%d]\n",NmissionsTotal,NmissionsRunni,self:GetMissionLimit("Total"),self.Nsuccess,self.Nfailure) for _,mtype in pairs(AUFTRAG.Type)do local n=self.commander:CountMissions(mtype) if n>0 then local N=self.commander:CountMissions(mtype,true) local limit=self:GetMissionLimit(mtype) text=text..string.format(" - %s: %d [Running=%d/%d]\n",mtype,n,N,limit) end end text=text..string.format("Strategic Zones: %d\n",Nzones) for _,_stratzone in pairs(self.zonequeue)do local stratzone=_stratzone local owner=stratzone.opszone:GetOwnerName() text=text..string.format(" - %s: %s - %s [I=%d, P=%d]\n",stratzone.opszone:GetName(),owner,stratzone.opszone:GetState(),stratzone.importance or 0,stratzone.prio or 0) end local Ntransports=#self.commander.transportqueue if Ntransports>0 then text=text..string.format("Transports: %d\n",Ntransports) for _,_transport in pairs(self.commander.transportqueue)do local transport=_transport text=text..string.format(" - %s",transport:GetState()) end end MESSAGE:New(text,60,nil,true):ToCoalition(self.coalition) if self.verbose>=4 then self:I(self.lid..text) end end end function CHIEF:CheckTargetQueue() local Ntargets=#self.targetqueue if Ntargets==0 then return nil end local NoLimit=self:_CheckMissionLimit("Total") if NoLimit==false then return nil end local function _sort(a,b) local taskA=a local taskB=b return(taskA.priotaskB.threatlevel0) end table.sort(self.targetqueue,_sort) local vip=math.huge for _,_target in pairs(self.targetqueue)do local target=_target if target:IsAlive()and target.importance and target.importance=self.threatLevelMin and threatlevel<=self.threatLevelMax if target.category==TARGET.Category.AIRBASE or target.category==TARGET.Category.ZONE or target.Category==TARGET.Category.COORDINATE then isThreat=true end local text=string.format("Target %s: Alive=%s, Threat=%s, Important=%s",target:GetName(),tostring(isAlive),tostring(isThreat),tostring(isImportant)) if target.mission then text=text..string.format(", Mission \"%s\" (%s) [%s]",target.mission:GetName(),target.mission:GetState(),target.mission:GetType()) if target.mission:IsOver()then text=text..string.format(" - DONE ==> removing mission") target.mission=nil end else text=text..string.format(", NO mission yet") end self:T2(self.lid..text) if isAlive and isThreat and isImportant and not target.mission then local valid=false if self.strategy==CHIEF.Strategy.PASSIVE then valid=false elseif self.strategy==CHIEF.Strategy.DEFENSIVE then if self:CheckTargetInZones(target,self.borderzoneset)then valid=true end elseif self.strategy==CHIEF.Strategy.OFFENSIVE then if self:CheckTargetInZones(target,self.borderzoneset)or self:CheckTargetInZones(target,self.yellowzoneset)then valid=true end elseif self.strategy==CHIEF.Strategy.AGGRESSIVE then if self:CheckTargetInZones(target,self.borderzoneset)or self:CheckTargetInZones(target,self.yellowzoneset)or self:CheckTargetInZones(target,self.engagezoneset)then valid=true end elseif self.strategy==CHIEF.Strategy.TOTALWAR then valid=true end if valid then self:T(self.lid..string.format("Got valid target %s: category=%s, threatlevel=%d",target:GetName(),target.category,threatlevel)) local MissionPerformances=self:_GetMissionPerformanceFromTarget(target) local mission=nil local Legions=nil if#MissionPerformances>0 then for _,_mp in pairs(MissionPerformances)do local mp=_mp local notlimited=self:_CheckMissionLimit(mp.MissionType) if notlimited then local NassetsMin,NassetsMax=self:_GetAssetsForTarget(target,mp.MissionType) self:T2(self.lid..string.format("Recruiting assets for mission type %s [performance=%d] of target %s",mp.MissionType,mp.Performance,target:GetName())) local minRange=self.LegionRecruitMinRange[target.category] local recruited,assets,legions=self.commander:RecruitAssetsForTarget(target,mp.MissionType,NassetsMin,NassetsMax,minRange) if recruited then self:T(self.lid..string.format("Recruited %d assets for mission type %s [performance=%d] of target %s",#assets,mp.MissionType,mp.Performance,target:GetName())) mission=AUFTRAG:NewFromTarget(target,mp.MissionType) if mission then mission:_AddAssets(assets) Legions=legions break end else self:T(self.lid..string.format("Could NOT recruit assets for mission type %s [performance=%d] of target %s",mp.MissionType,mp.Performance,target:GetName())) end end end end if mission and Legions then target.mission=mission mission.prio=target.prio mission.importance=target.importance self:MissionAssign(mission,Legions) return end end end end end function CHIEF:_CheckMissionLimit(MissionType) return self.commander:_CheckMissionLimit(MissionType) end function CHIEF:GetMissionLimit(MissionType) local l=self.commander.limitMission[MissionType] if not l then l=999 end return l end function CHIEF:CheckOpsZoneQueue() local Nzones=#self.zonequeue if Nzones==0 then return nil end for i=Nzones,1,-1 do local stratzone=self.zonequeue[i] if stratzone.opszone:IsStopped()then self:RemoveStrategicZone(stratzone.opszone) end end for _,_startzone in pairs(self.zonequeue)do local stratzone=_startzone local ownercoalition=stratzone.opszone:GetOwner() if ownercoalition==self.coalition or stratzone.opszone:IsEmpty()then for _,_resource in pairs(stratzone.resourceOccup or{})do local resource=_resource if resource.mission then resource.mission:Cancel() end end end end if self:IsPassive()then return end local NoLimit=self:_CheckMissionLimit("Total") if NoLimit==false then return nil end local function _sort(a,b) local taskA=a local taskB=b return(taskA.prio Recruiting for mission type %s: Nmin=%d, Nmax=%d",zoneName,missionType,resource.Nmin,resource.Nmax)) local recruited=self:RecruitAssetsForZone(stratzone,resource) if recruited then self:T(self.lid..string.format("Successfully recruited assets for empty zone \"%s\" [mission type=%s]",zoneName,missionType)) else self:T(self.lid..string.format("Could not recruited assets for empty zone \"%s\" [mission type=%s]",zoneName,missionType)) end end end else for _,_resource in pairs(stratzone.resourceOccup or{})do local resource=_resource local missionType=resource.MissionType if(not resource.mission)or resource.mission:IsOver()then self:T2(self.lid..string.format("Zone %s is NOT empty ==> Recruiting for mission type %s: Nmin=%d, Nmax=%d",zoneName,missionType,resource.Nmin,resource.Nmax)) local recruited=self:RecruitAssetsForZone(stratzone,resource) if recruited then self:T(self.lid..string.format("Successfully recruited assets for occupied zone %s, mission type=%s",zoneName,missionType)) else self:T(self.lid..string.format("Could not recruited assets for occupied zone %s, mission type=%s",zoneName,missionType)) end end end end end end end function CHIEF:CheckGroupInBorder(group) local inside=self:CheckGroupInZones(group,self.borderzoneset) return inside end function CHIEF:CheckGroupInConflict(group) local inside=self:CheckGroupInZones(group,self.yellowzoneset) return inside end function CHIEF:CheckGroupInAttack(group) local inside=self:CheckGroupInZones(group,self.engagezoneset) return inside end function CHIEF:CheckGroupInZones(group,zoneset) for _,_zone in pairs(zoneset.Set or{})do local zone=_zone if group:IsInZone(zone)then return true end end return false end function CHIEF:CheckTargetInZones(target,zoneset) for _,_zone in pairs(zoneset.Set or{})do local zone=_zone if zone:IsCoordinateInZone(target:GetCoordinate())then return true end end return false end function CHIEF:_CreateMissionPerformance(MissionType,Performance) local mp={} mp.MissionType=MissionType mp.Performance=Performance return mp end function CHIEF:_GetMissionPerformanceFromTarget(Target) local group=nil local airbase=nil local scenery=nil local static=nil local coordinate=nil local target=Target:GetObject() if target:IsInstanceOf("GROUP")then group=target elseif target:IsInstanceOf("UNIT")then group=target:GetGroup() elseif target:IsInstanceOf("AIRBASE")then airbase=target elseif target:IsInstanceOf("STATIC")then static=target elseif target:IsInstanceOf("SCENERY")then scenery=target end local TargetCategory=Target:GetCategory() local missionperf={} if group then local category=group:GetCategory() local attribute=group:GetAttribute() if category==Group.Category.AIRPLANE or category==Group.Category.HELICOPTER then table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.INTERCEPT,100)) elseif category==Group.Category.GROUND or category==Group.Category.TRAIN then if attribute==GROUP.Attribute.GROUND_SAM then table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.SEAD,100)) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.GROUNDATTACK,50)) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.ARTY,30)) elseif attribute==GROUP.Attribute.GROUND_EWR then table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.BAI,100)) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.GROUNDATTACK,50)) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.ARTY,30)) elseif attribute==GROUP.Attribute.GROUND_AAA then table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.BAI,100)) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.GROUNDATTACK,50)) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.ARMORATTACK,40)) elseif attribute==GROUP.Attribute.GROUND_ARTILLERY then table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.BAI,100)) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.GROUNDATTACK,75)) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.ARMORATTACK,70)) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.BOMBING,70)) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.ARTY,30)) elseif attribute==GROUP.Attribute.GROUND_INFANTRY then table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.BAI,100)) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.GROUNDATTACK,50)) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.ARMORATTACK,40)) elseif attribute==GROUP.Attribute.GROUND_TANK then table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.BAI,100)) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.CAS,90)) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.CASENHANCED,90)) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.GROUNDATTACK,50)) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.ARMORATTACK,40)) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.ARTY,30)) else table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.BAI,100)) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.GROUNDATTACK,50)) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.ARTY,30)) end elseif category==Group.Category.SHIP then table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.ANTISHIP,100)) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.NAVALENGAGEMENT,50)) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.ARTY,30)) else self:E(self.lid.."ERROR: Unknown Group category!") end elseif airbase then table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.BOMBRUNWAY,100)) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.ARTY,30)) elseif static then table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.BAI,100)) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.BOMBING,70)) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.BOMBCARPET,50)) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.ARTY,30)) elseif scenery then table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.STRIKE,100)) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.BOMBING,70)) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.BOMBCARPET,50)) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.ARTY,30)) elseif coordinate then table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.BOMBING,100)) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.BOMBCARPET,50)) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.ARTY,30)) end return missionperf end function CHIEF:_GetMissionTypeForGroupAttribute(Attribute) local missionperf={} if Attribute==GROUP.Attribute.AIR_ATTACKHELO then table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.INTERCEPT),100) elseif Attribute==GROUP.Attribute.GROUND_AAA then table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.BAI),100) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.BOMBING),80) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.BOMBCARPET),70) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.ARTY),30) elseif Attribute==GROUP.Attribute.GROUND_SAM then table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.SEAD),100) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.BAI),90) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.ARTY),50) elseif Attribute==GROUP.Attribute.GROUND_EWR then table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.SEAD),100) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.BAI),100) table.insert(missionperf,self:_CreateMissionPerformance(AUFTRAG.Type.ARTY),50) end return missionperf end function CHIEF:RecruitAssetsForZone(StratZone,Resource) local Cohorts=self.commander:_GetCohorts() local MissionType=Resource.MissionType local NassetsMin=Resource.Nmin local NassetsMax=Resource.Nmax local Categories=Resource.Categories local Attributes=Resource.Attributes local Properties=Resource.Properties local TargetVec2=StratZone.opszone.zone:GetVec2() local RangeMax=nil if MissionType==AUFTRAG.Type.PATROLZONE or MissionType==AUFTRAG.Type.ONGUARD then RangeMax=UTILS.NMToMeters(250) end if MissionType==AUFTRAG.Type.ARMOREDGUARD then RangeMax=UTILS.NMToMeters(50) end self:T(self.lid..string.format("Recruiting assets for zone %s",StratZone.opszone:GetName())) self:T(self.lid.."Missiontype="..MissionType) self:T({categories=Categories}) self:T({attributes=Attributes}) self:T({properties=Properties}) local recruited,assets,legions=LEGION.RecruitCohortAssets(Cohorts,MissionType,nil,NassetsMin,NassetsMax,TargetVec2,nil,RangeMax,nil,nil,nil,nil,Categories,Attributes,Properties) if recruited then local mission=nil self:T2(self.lid..string.format("Recruited %d assets for %s mission STRATEGIC zone %s",#assets,MissionType,tostring(StratZone.opszone.zoneName))) local TargetZone=StratZone.opszone.zone local TargetCoord=TargetZone:GetCoordinate() local transport=nil local Ntransports=0 if Resource.carrierNmin and Resource.carrierNmax and Resource.carrierNmax>0 then self:T(self.lid..string.format("Recruiting carrier assets: Nmin=%s, Nmax=%s",tostring(Resource.carrierNmin),tostring(Resource.carrierNmax))) local cargoassets=CHIEF._FilterAssets(assets,Resource.Categories,Resource.Attributes,Resource.Properties) if#cargoassets>0 then recruited,transport=LEGION.AssignAssetsForTransport(self.commander,self.commander.legions,cargoassets, Resource.carrierNmin,Resource.carrierNmax,TargetZone,nil,Resource.carrierCategories,Resource.carrierAttributes,Resource.carrierProperties) Ntransports=transport~=nil and#transport.assets or 0 self:T(self.lid..string.format("Recruited %d transport carrier assets success=%s",Ntransports,tostring(recruited))) end end if not recruited then self:T(self.lid..string.format("Could not allocate assets or transport of OPSZONE!")) LEGION.UnRecruitAssets(assets) return false end self:T2(self.lid..string.format("Recruited %d assets for mission %s",#assets,MissionType)) if MissionType==AUFTRAG.Type.PATROLZONE or MissionType==AUFTRAG.Type.ONGUARD then if MissionType==AUFTRAG.Type.PATROLZONE then mission=AUFTRAG:NewPATROLZONE(TargetZone) elseif MissionType==AUFTRAG.Type.ONGUARD then mission=AUFTRAG:NewONGUARD(TargetZone:GetRandomCoordinate(nil,nil,{land.SurfaceType.LAND})) end mission:SetEngageDetected(25,{"Ground Units","Light armed ships","Helicopters"}) elseif MissionType==AUFTRAG.Type.CAPTUREZONE then mission=AUFTRAG:NewCAPTUREZONE(StratZone.opszone,self.coalition) elseif MissionType==AUFTRAG.Type.CASENHANCED then local height=UTILS.MetersToFeet(TargetCoord:GetLandHeight())+2500 local Speed=200 if assets[1]then if assets[1].speedmax then Speed=UTILS.KmphToKnots(assets[1].speedmax*0.7)or 200 end end mission=AUFTRAG:NewCASENHANCED(TargetZone,height,Speed) elseif MissionType==AUFTRAG.Type.CAS then local height=UTILS.MetersToFeet(TargetCoord:GetLandHeight())+2500 local Speed=200 if assets[1]then if assets[1].speedmax then Speed=UTILS.KmphToKnots(assets[1].speedmax*0.7)or 200 end end TargetZone=StratZone.opszone.zoneCircular local Leg=TargetZone:GetRadius()<=10000 and 5 or UTILS.MetersToNM(TargetZone:GetRadius()) mission=AUFTRAG:NewCAS(TargetZone,height,Speed,TargetCoord,math.random(0,359),Leg) elseif MissionType==AUFTRAG.Type.ARTY then local Radius=TargetZone:GetRadius() mission=AUFTRAG:NewARTY(TargetCoord,120,Radius) elseif MissionType==AUFTRAG.Type.ARMOREDGUARD then mission=AUFTRAG:NewARMOREDGUARD(TargetCoord) elseif MissionType==AUFTRAG.Type.BOMBCARPET then mission=AUFTRAG:NewBOMBCARPET(TargetCoord,nil,1000) elseif MissionType==AUFTRAG.Type.BOMBING then local coord=TargetZone:GetRandomCoordinate() mission=AUFTRAG:NewBOMBING(TargetCoord) elseif MissionType==AUFTRAG.Type.RECON then mission=AUFTRAG:NewRECON(TargetZone,nil,5000) elseif MissionType==AUFTRAG.Type.BARRAGE then mission=AUFTRAG:NewBARRAGE(TargetZone) elseif MissionType==AUFTRAG.Type.AMMOSUPPLY then mission=AUFTRAG:NewAMMOSUPPLY(TargetZone) end if mission then mission:_AddAssets(assets) self:MissionAssign(mission,legions) StratZone.opszone:_AddMission(self.coalition,MissionType,mission) mission:SetName(string.format("Stratzone %s-%d",StratZone.opszone:GetName(),mission.auftragsnummer)) Resource.mission=mission if transport and Ntransports>0 then mission.opstransport=transport transport.opszone=StratZone.opszone transport.chief=self transport.commander=self.commander end return true else self:E(self.lid..string.format("ERROR: Mission type %s not supported for OPSZONE! Unrecruiting assets...",tostring(MissionType))) LEGION.UnRecruitAssets(assets) return false end end self:T2(self.lid..string.format("Could NOT recruit assets for %s mission of STRATEGIC zone %s",MissionType,tostring(StratZone.opszone.zoneName))) return false end function CHIEF._FilterAssets(Assets,Categories,Attributes,Properties) local filtered={} for _,_asset in pairs(Assets)do local asset=_asset local hasCat=CHIEF._CheckAssetCategories(asset,Categories) local hasAtt=CHIEF._CheckAssetAttributes(asset,Attributes) local hasPro=CHIEF._CheckAssetProperties(asset,Properties) if hasAtt and hasCat and hasPro then table.insert(filtered,asset) end end return filtered end function CHIEF._CheckAssetAttributes(Asset,Attributes) if not Attributes then return true end for _,attribute in pairs(UTILS.EnsureTable(Attributes))do if attribute==Asset.attribute then return true end end return false end function CHIEF._CheckAssetCategories(Asset,Categories) if not Categories then return true end for _,attribute in pairs(UTILS.EnsureTable(Categories))do if attribute==Asset.category then return true end end return false end function CHIEF._CheckAssetProperties(Asset,Properties) if not Properties then return true end for _,attribute in pairs(UTILS.EnsureTable(Properties))do if attribute==Asset.DCSdesc then return true end end return false end function CHIEF:CanMission(Mission) return self.commander and self.commander:CanMission(Mission) end function CHIEF:AddLegionRecruitMinRange(TargetCategory,MinRange) self.LegionRecruitMinRange[TargetCategory]=MinRange end COHORT={ ClassName="COHORT", verbose=0, lid=nil, name=nil, templatename=nil, assets={}, missiontypes={}, repairtime=0, maintenancetime=0, livery=nil, skill=nil, legion=nil, Ngroups=0, engageRange=nil, tacanChannel={}, weightAsset=99999, cargobayLimit=0, descriptors={}, properties={}, operations={}, } COHORT.version="0.3.7" _COHORTNAMES={} function COHORT:New(TemplateGroupName,Ngroups,CohortName) local name=tostring(CohortName or TemplateGroupName) if UTILS.IsAnyInTable(_COHORTNAMES,name)then env.error(string.format('ERROR: cannot create cohort "%s" because another cohort with that name already exists. Names must be unique!',name)) return nil else table.insert(_COHORTNAMES,name) end local self=BASE:Inherit(self,FSM:New()) self.templatename=TemplateGroupName self.name=name self.lid=string.format("COHORT %s | ",self.name) self.templategroup=GROUP:FindByName(self.templatename) if not self.templategroup then self:E(self.lid..string.format("ERROR: Template group %s does not exist!",tostring(self.templatename))) return nil end self.attribute=self.templategroup:GetAttribute() self.category=self.templategroup:GetCategory() self.aircrafttype=self.templategroup:GetTypeName() self.descriptors=self.templategroup:GetUnit(1):GetDesc() self.properties=self.descriptors.attributes self.Ngroups=Ngroups or 3 self:SetSkill(AI.Skill.GOOD) if self.category==Group.Category.AIRPLANE then self:SetMissionRange(200) elseif self.category==Group.Category.HELICOPTER then self:SetMissionRange(150) elseif self.category==Group.Category.GROUND then self:SetMissionRange(75) elseif self.category==Group.Category.SHIP then self:SetMissionRange(100) elseif self.category==Group.Category.TRAIN then self:SetMissionRange(100) else self:SetMissionRange(150) end local units=self.templategroup:GetUnits() self.weightAsset=0 for i,_unit in pairs(units)do local unit=_unit local desc=unit:GetDesc() local mass=666 if desc then mass=desc.massMax or desc.massEmpty end self.weightAsset=self.weightAsset+(mass or 666) if i==1 then self.cargobayLimit=unit:GetCargoBayFreeWeight() end end self:SetStartState("Stopped") self:AddTransition("Stopped","Start","OnDuty") self:AddTransition("*","Status","*") self:AddTransition("OnDuty","Pause","Paused") self:AddTransition("Paused","Unpause","OnDuty") self:AddTransition("OnDuty","Relocate","Relocating") self:AddTransition("Relocating","Relocated","OnDuty") self:AddTransition("*","Stop","Stopped") return self end function COHORT:SetLivery(LiveryName) self.livery=LiveryName return self end function COHORT:SetSkill(Skill) self.skill=Skill return self end function COHORT:SetVerbosity(VerbosityLevel) self.verbose=VerbosityLevel or 0 return self end function COHORT:SetTurnoverTime(MaintenanceTime,RepairTime) self.maintenancetime=MaintenanceTime and MaintenanceTime*60 or 0 self.repairtime=RepairTime and RepairTime*60 or 0 return self end function COHORT:SetRadio(Frequency,Modulation) self.radioFreq=Frequency or 251 self.radioModu=Modulation or radio.modulation.AM return self end function COHORT:SetGrouping(nunits) self.ngrouping=nunits or 2 return self end function COHORT:AddMissionCapability(MissionTypes,Performance) if MissionTypes and type(MissionTypes)~="table"then MissionTypes={MissionTypes} end self.missiontypes=self.missiontypes or{} for _,missiontype in pairs(MissionTypes)do local Capability=self:GetMissionCapability(missiontype) if Capability then self:E(self.lid.."WARNING: Mission capability already present! No need to add it twice. Will update the performance though!") Capability.Performance=Performance or 50 else local capability={} capability.MissionType=missiontype capability.Performance=Performance or 50 table.insert(self.missiontypes,capability) self:T(self.lid..string.format("Adding mission capability %s, performance=%d",tostring(capability.MissionType),capability.Performance)) end end self:T2(self.missiontypes) return self end function COHORT:GetMissionCapability(MissionType) for _,_capability in pairs(self.missiontypes)do local capability=_capability if capability.MissionType==MissionType then return capability end end return nil end function COHORT:HasProperty(Property) for _,property in pairs(self.properties)do if Property==property then return true end end return false end function COHORT:GetMissionTypes() local missiontypes={} for _,Capability in pairs(self.missiontypes)do local capability=Capability table.insert(missiontypes,capability.MissionType) end return missiontypes end function COHORT:GetMissionCapabilities() return self.missiontypes end function COHORT:GetMissionPeformance(MissionType) for _,Capability in pairs(self.missiontypes)do local capability=Capability if capability.MissionType==MissionType then return capability.Performance end end return-1 end function COHORT:SetMissionRange(Range) self.engageRange=UTILS.NMToMeters(Range or 150) return self end function COHORT:SetCallsign(Callsign,Index,CallsignString) self.callsignName=Callsign self.callsignIndex=Index self.callsignClearName=CallsignString self.callsign={} self.callsign.NumberSquad=Callsign self.callsign.NumberGroup=Index return self end function COHORT:SetAttribute(Attribute) self.attribute=Attribute return self end function COHORT:GetAttribute() return self.attribute end function COHORT:GetCategory() return self.category end function COHORT:GetProperties() return self.properties end function COHORT:SetModex(Modex,Prefix,Suffix) self.modex=Modex self.modexPrefix=Prefix self.modexSuffix=Suffix return self end function COHORT:SetLegion(Legion) self.legion=Legion return self end function COHORT:AddAsset(Asset) self:T(self.lid..string.format("Adding asset %s of type %s",Asset.spawngroupname,Asset.unittype)) Asset.squadname=self.name Asset.legion=self.legion Asset.cohort=self table.insert(self.assets,Asset) return self end function COHORT:DelAsset(Asset) for i,_asset in pairs(self.assets)do local asset=_asset if Asset.uid==asset.uid then self:T2(self.lid..string.format("Removing asset %s",asset.spawngroupname)) table.remove(self.assets,i) break end end return self end function COHORT:DelGroup(GroupName) for i,_asset in pairs(self.assets)do local asset=_asset if GroupName==asset.spawngroupname then self:T2(self.lid..string.format("Removing asset %s",asset.spawngroupname)) table.remove(self.assets,i) break end end return self end function COHORT:RemoveAssets(N,Delay) self:T2(self.lid..string.format("Remove %d assets of Cohort",N)) if Delay and Delay>0 then self:ScheduleOnce(Delay,COHORT.RemoveAssets,self,N,0) else N=N or 1 local n=0 for i=#self.assets,1,-1 do local asset=self.assets[i] self:T2(self.lid..string.format("Checking removing asset %s",asset.spawngroupname)) if not(asset.requested or asset.spawned or asset.isReserved)then self:T2(self.lid..string.format("Removing asset %s",asset.spawngroupname)) asset.legion:_DeleteStockItem(asset) table.remove(self.assets,i) n=n+1 else self:T2(self.lid..string.format("Could NOT Remove asset %s",asset.spawngroupname)) end if n>=N then break end end self:T(self.lid..string.format("Removed %d/%d assets. New asset count=%d",n,N,#self.assets)) end return self end function COHORT:GetName() return self.name end function COHORT:GetRadio() return self.radioFreq,self.radioModu end function COHORT:GetCallsign(Asset) if self.callsignName then Asset.callsign={} for i=1,Asset.nunits do local callsign={} callsign[1]=self.callsignName callsign[2]=math.floor(self.callsigncounter/10) callsign[3]=self.callsigncounter%10 if callsign[3]==0 then callsign[3]=1 self.callsigncounter=self.callsigncounter+2 else self.callsigncounter=self.callsigncounter+1 end callsign["name"]=self.callsignClearName or UTILS.GetCallsignName(self.callsignName)or"None" callsign["name"]=string.format("%s%d%d",callsign["name"],callsign[2],callsign[3]) callsign[4]=callsign["name"] Asset.callsign[i]=callsign self:T3({callsign=callsign}) end end end function COHORT:GetModex(Asset) if self.modex then Asset.modex={} for i=1,Asset.nunits do Asset.modex[i]=string.format("%03d",self.modex+self.modexcounter) self.modexcounter=self.modexcounter+1 self:T3({modex=Asset.modex[i]}) end end end function COHORT:AddTacanChannel(ChannelMin,ChannelMax) ChannelMax=ChannelMax or ChannelMin if ChannelMin>126 then self:E(self.lid.."ERROR: TACAN Channel must be <= 126! Will not add to available channels") return self end if ChannelMax>126 then self:E(self.lid.."WARNING: TACAN Channel must be <= 126! Adjusting ChannelMax to 126") ChannelMax=126 end for i=ChannelMin,ChannelMax do self.tacanChannel[i]=true end return self end function COHORT:FetchTacan() local freechannel=nil for channel,free in pairs(self.tacanChannel)do if free then if freechannel==nil or channel=2 then local text="Weapon data:" for _,_weapondata in pairs(self.weaponData)do local weapondata=_weapondata text=text..string.format("\n- Bit=%s, Rmin=%d m, Rmax=%d m",tostring(weapondata.BitType),weapondata.RangeMin,weapondata.RangeMax) end self:I(self.lid..text) end return self end function COHORT:GetWeaponData(BitType) return self.weaponData[tostring(BitType)] end function COHORT:IsOnDuty() return self:Is("OnDuty") end function COHORT:IsStopped() return self:Is("Stopped") end function COHORT:IsPaused() return self:Is("Paused") end function COHORT:IsRelocating() return self:Is("Relocating") end function COHORT:onafterStart(From,Event,To) local text=string.format("Starting %s v%s %s [%s]",self.ClassName,self.version,self.name,self.attribute) self:I(self.lid..text) self:__Status(-1) end function COHORT:_CheckAssetStatus() if self.verbose>=2 and#self.assets>0 then local text="" for j,_asset in pairs(self.assets)do local asset=_asset text=text..string.format("\n[%d] %s (%s*%d): ",j,asset.spawngroupname,asset.unittype,asset.nunits) if asset.spawned then local mission=self.legion and self.legion:GetAssetCurrentMission(asset)or false if mission then local distance=asset.flightgroup and UTILS.MetersToNM(mission:GetTargetDistance(asset.flightgroup.group:GetCoordinate()))or 0 text=text..string.format("Mission %s - %s: Status=%s, Dist=%.1f NM",mission.name,mission.type,mission.status,distance) else text=text.."Mission None" end text=text..", Flight: " if asset.flightgroup and asset.flightgroup:IsAlive()then local status=asset.flightgroup:GetState() text=text..string.format("%s",status) if asset.flightgroup:IsFlightgroup()then local fuelmin=asset.flightgroup:GetFuelMin() local fuellow=asset.flightgroup:IsFuelLow() local fuelcri=asset.flightgroup:IsFuelCritical() text=text..string.format("Fuel=%d",fuelmin) if fuelcri then text=text.." (Critical!)" elseif fuellow then text=text.." (Low)" end end local lifept,lifept0=asset.flightgroup:GetLifePoints() text=text..string.format(", Life=%d/%d",lifept,lifept0) local ammo=asset.flightgroup:GetAmmoTot() text=text..string.format(", Ammo=%d [G=%d, R=%d, B=%d, M=%d]",ammo.Total,ammo.Guns,ammo.Rockets,ammo.Bombs,ammo.Missiles) else text=text.."N/A" end if asset.flightgroup:IsFlightgroup()then local payload=asset.payload and table.concat(self.legion:GetPayloadMissionTypes(asset.payload),", ")or"None" text=text..", Payload={"..payload.."}" end else text=text..string.format("In Stock") if self:IsRepaired(asset)then text=text..", Combat Ready" else text=text..string.format(", Repaired in %d sec",self:GetRepairTime(asset)) if asset.damage then text=text..string.format(" (Damage=%.1f)",asset.damage) end end if asset.Treturned then local T=timer.getAbsTime()-asset.Treturned text=text..string.format(", Returned for %d sec",T) end end end self:T(self.lid..text) end end function COHORT:onafterStop(From,Event,To) self:T(self.lid.."STOPPING Cohort and removing all assets!") for i=#self.assets,1,-1 do local asset=self.assets[i] self:DelAsset(asset) end self.CallScheduler:Clear() end function COHORT:CanMission(Mission) local cando=true if not self:IsOnDuty()then self:T(self.lid..string.format("Cohort in not OnDuty but in state %s. Cannot do mission %s with target %s",self:GetState(),Mission.name,Mission:GetTargetName())) return false end if not AUFTRAG.CheckMissionType(Mission.type,self:GetMissionTypes())then self:T(self.lid..string.format("INFO: Cohort cannot do mission type %s (%s, %s)",Mission.type,Mission.name,Mission:GetTargetName())) return false end if Mission.type==AUFTRAG.Type.TANKER then if Mission.refuelSystem and Mission.refuelSystem==self.tankerSystem then self:T(self.lid..string.format("INFO: Correct refueling system requested=%s != %s=available",tostring(Mission.refuelSystem),tostring(self.tankerSystem))) else self:T(self.lid..string.format("INFO: Wrong refueling system requested=%s != %s=available",tostring(Mission.refuelSystem),tostring(self.tankerSystem))) return false end end local TargetDistance=Mission:GetTargetDistance(self.legion:GetCoordinate()) local engagerange=Mission.engageRange and math.max(self.engageRange,Mission.engageRange)or self.engageRange if TargetDistance>engagerange then self:T(self.lid..string.format("INFO: Cohort is not in range. Target dist=%d > %d NM max mission Range",UTILS.MetersToNM(TargetDistance),UTILS.MetersToNM(engagerange))) return false end return true end function COHORT:CountAssets(InStock,MissionTypes,Attributes) local N=0 for _,_asset in pairs(self.assets)do local asset=_asset if MissionTypes==nil or AUFTRAG.CheckMissionCapability(MissionTypes,self.missiontypes)then if Attributes==nil or self:CheckAttribute(Attributes)then if asset.spawned then if InStock==false or InStock==nil then N=N+1 end else if InStock==true or InStock==nil then N=N+1 end end end end end return N end function COHORT:GetOpsGroups(MissionTypes,Attributes) local set=SET_OPSGROUP:New() for _,_asset in pairs(self.assets)do local asset=_asset if MissionTypes==nil or AUFTRAG.CheckMissionCapability(MissionTypes,self.missiontypes)then if Attributes==nil or self:CheckAttribute(Attributes)then if asset.flightgroup and asset.flightgroup:IsAlive()then set:AddGroup(asset.flightgroup) end end end end return set end function COHORT:RecruitAssets(MissionType,Npayloads) self:T2(self.lid..string.format("Recruiting asset for Mission type=%s",MissionType)) local assets={} for _,_asset in pairs(self.assets)do local asset=_asset local isRequested=asset.requested local isReserved=asset.isReserved local isSpawned=asset.spawned local isOnMission=self.legion:IsAssetOnMission(asset) local opsgroup=asset.flightgroup self:T(self.lid..string.format("Asset %s: requested=%s, reserved=%s, spawned=%s, onmission=%s", asset.spawngroupname,tostring(isRequested),tostring(isReserved),tostring(isSpawned),tostring(isOnMission))) if not(isRequested or isReserved)then if self.legion:IsAssetOnMission(asset)then if MissionType==AUFTRAG.Type.RELOCATECOHORT then table.insert(assets,asset) elseif self.legion:IsAssetOnMission(asset,AUFTRAG.Type.NOTHING)then table.insert(assets,asset) elseif self.legion:IsAssetOnMission(asset,{AUFTRAG.Type.GCICAP,AUFTRAG.Type.PATROLRACETRACK})and MissionType==AUFTRAG.Type.INTERCEPT then self:T(self.lid..string.format("Adding asset on GCICAP mission for an INTERCEPT mission")) table.insert(assets,asset) elseif self.legion:IsAssetOnMission(asset,AUFTRAG.Type.ONGUARD)and(MissionType==AUFTRAG.Type.ARTY or MissionType==AUFTRAG.Type.GROUNDATTACK)then if not opsgroup:IsOutOfAmmo()then self:T(self.lid..string.format("Adding asset on ONGUARD mission for an XXX mission")) table.insert(assets,asset) end elseif self.legion:IsAssetOnMission(asset,AUFTRAG.Type.PATROLZONE)and(MissionType==AUFTRAG.Type.ARTY or MissionType==AUFTRAG.Type.GROUNDATTACK)then if not opsgroup:IsOutOfAmmo()then self:T(self.lid..string.format("Adding asset on PATROLZONE mission for an XXX mission")) table.insert(assets,asset) end elseif self.legion:IsAssetOnMission(asset,AUFTRAG.Type.ALERT5)and AUFTRAG.CheckMissionCapability(MissionType,asset.payload.capabilities)and MissionType~=AUFTRAG.Type.ALERT5 then self:T(self.lid..string.format("Adding asset on ALERT 5 mission for %s mission",MissionType)) table.insert(assets,asset) end else if asset.spawned then local flightgroup=asset.flightgroup if flightgroup and flightgroup:IsAlive()and not(flightgroup:IsDead()or flightgroup:IsStopped())then local combatready=true if flightgroup:IsFlightgroup()then if flightgroup:IsFuelLow()then combatready=false end if MissionType==AUFTRAG.Type.INTERCEPT and not flightgroup:CanAirToAir()then combatready=false else local excludeguns=MissionType==AUFTRAG.Type.BOMBING or MissionType==AUFTRAG.Type.BOMBRUNWAY or MissionType==AUFTRAG.Type.BOMBCARPET or MissionType==AUFTRAG.Type.SEAD or MissionType==AUFTRAG.Type.ANTISHIP if excludeguns and not flightgroup:CanAirToGround(excludeguns)then combatready=false end end if flightgroup:IsHolding()or flightgroup:IsLanding()or flightgroup:IsLanded()or flightgroup:IsArrived()then combatready=false end if asset.payload and not AUFTRAG.CheckMissionCapability(MissionType,asset.payload.capabilities)then combatready=false end else if flightgroup:IsArmygroup()then if asset.attribute==WAREHOUSE.Attribute.GROUND_ARTILLERY or asset.attribute==WAREHOUSE.Attribute.GROUND_TANK or asset.attribute==WAREHOUSE.Attribute.GROUND_INFANTRY or asset.attribute==WAREHOUSE.Attribute.GROUND_AAA or asset.attribute==WAREHOUSE.Attribute.GROUND_SAM then combatready=true end else combatready=false end if flightgroup:IsRearming()or flightgroup:IsRetreating()or flightgroup:IsReturning()then combatready=false end end if flightgroup:IsLoading()or flightgroup:IsTransporting()or flightgroup:IsUnloading()or flightgroup:IsPickingup()or flightgroup:IsCarrier()then combatready=false end if flightgroup:IsCargo()or flightgroup:IsBoarding()or flightgroup:IsAwaitingLift()then combatready=false end if combatready then self:T(self.lid.."Adding SPAWNED asset to ANOTHER mission as it is COMBATREADY") table.insert(assets,asset) end end else if Npayloads>0 and self:IsRepaired(asset)then table.insert(assets,asset) Npayloads=Npayloads-1 end end end end end self:T2(self.lid..string.format("Recruited %d assets for Mission type=%s",#assets,MissionType)) return assets,Npayloads end function COHORT:GetRepairTime(Asset) if Asset.Treturned then local t=self.maintenancetime t=t+Asset.damage*self.repairtime local dt=timer.getAbsTime()-Asset.Treturned local T=t-dt return T else return 0 end end function COHORT:GetMissionRange(WeaponTypes) if WeaponTypes and type(WeaponTypes)~="table"then WeaponTypes={WeaponTypes} end local function checkWeaponType(Weapon) local weapon=Weapon if WeaponTypes and#WeaponTypes>0 then for _,weapontype in pairs(WeaponTypes)do if weapontype==weapon.BitType then return true end end return false end return true end local WeaponRange=0 for _,_weapon in pairs(self.weaponData or{})do local weapon=_weapon if weapon.RangeMax>WeaponRange and checkWeaponType(weapon)then WeaponRange=weapon.RangeMax end end return self.engageRange+WeaponRange end function COHORT:IsRepaired(Asset) if Asset.Treturned then local Tnow=timer.getAbsTime() local Trepaired=Asset.Treturned+self.maintenancetime if Tnow>=Trepaired then return true else return false end else return true end end function COHORT:CheckAttribute(Attributes) if type(Attributes)~="table"then Attributes={Attributes} end for _,attribute in pairs(Attributes)do if attribute==self.attribute then return true end end return false end function COHORT:_CheckAmmo() local units=self.templategroup:GetUnits() local nammo=0 local nguns=0 local nshells=0 local nrockets=0 local nmissiles=0 local nmissilesAA=0 local nmissilesAG=0 local nmissilesAS=0 local nmissilesSA=0 local nmissilesBM=0 local nmissilesCR=0 local ntorps=0 local nbombs=0 for _,_unit in pairs(units)do local unit=_unit local text=string.format("Unit %s:\n",unit:GetName()) local ammotable=unit:GetAmmo() if ammotable then self:T3(ammotable) for w=1,#ammotable do local weapon=ammotable[w] local Desc=weapon["desc"] local Warhead=Desc["warhead"] local Nammo=weapon["count"] local Category=Desc["category"] local MissileCategory=(Category==Weapon.Category.MISSILE)and Desc.missileCategory or nil local TypeName=Desc["typeName"] local weaponString=UTILS.Split(TypeName,"%.") local WeaponName=weaponString[#weaponString] local Rmin=Desc["rangeMin"]or 0 local Rmax=Desc["rangeMaxAltMin"]or 0 local Caliber=Warhead and Warhead["caliber"]or 0 if Category==Weapon.Category.SHELL then if Caliber<70 then nguns=nguns+Nammo else nshells=nshells+Nammo end text=text..string.format("- %d shells [%s]: caliber=%d mm, range=%d - %d meters\n",Nammo,WeaponName,Caliber,Rmin,Rmax) elseif Category==Weapon.Category.ROCKET then nrockets=nrockets+Nammo text=text..string.format("- %d rockets [%s]: caliber=%d mm, range=%d - %d meters\n",Nammo,WeaponName,Caliber,Rmin,Rmax) elseif Category==Weapon.Category.BOMB then nbombs=nbombs+Nammo text=text..string.format("- %d bombs [%s]: caliber=%d mm, range=%d - %d meters\n",Nammo,WeaponName,Caliber,Rmin,Rmax) elseif Category==Weapon.Category.MISSILE then if MissileCategory==Weapon.MissileCategory.AAM then nmissiles=nmissiles+Nammo nmissilesAA=nmissilesAA+Nammo if Rmax>0 then self:AddWeaponRange(UTILS.MetersToNM(Rmin),UTILS.MetersToNM(Rmax),ENUMS.WeaponFlag.AnyAA) end elseif MissileCategory==Weapon.MissileCategory.SAM then nmissiles=nmissiles+Nammo nmissilesSA=nmissilesSA+Nammo if Rmax>0 then end elseif MissileCategory==Weapon.MissileCategory.ANTI_SHIP then nmissiles=nmissiles+Nammo nmissilesAS=nmissilesAS+Nammo if Rmax>0 then self:AddWeaponRange(UTILS.MetersToNM(Rmin),UTILS.MetersToNM(Rmax),ENUMS.WeaponFlag.AntiShipMissile) end elseif MissileCategory==Weapon.MissileCategory.BM then nmissiles=nmissiles+Nammo nmissilesBM=nmissilesBM+Nammo if Rmax>0 then end elseif MissileCategory==Weapon.MissileCategory.CRUISE then nmissiles=nmissiles+Nammo nmissilesCR=nmissilesCR+Nammo if Rmax>0 then self:AddWeaponRange(UTILS.MetersToNM(Rmin),UTILS.MetersToNM(Rmax),ENUMS.WeaponFlag.CruiseMissile) end elseif MissileCategory==Weapon.MissileCategory.OTHER then nmissiles=nmissiles+Nammo nmissilesAG=nmissilesAG+Nammo end text=text..string.format("- %d %s missiles [%s]: caliber=%d mm, range=%d - %d meters\n",Nammo,self:_MissileCategoryName(MissileCategory),WeaponName,Caliber,Rmin,Rmax) elseif Category==Weapon.Category.TORPEDO then ntorps=ntorps+Nammo text=text..string.format("- %d torpedos [%s]: caliber=%d mm, range=%d - %d meters\n",Nammo,WeaponName,Caliber,Rmin,Rmax) else text=text..string.format("- %d unknown ammo of type %s (category=%d, missile category=%s)\n",Nammo,TypeName,Category,tostring(MissileCategory)) end end end if self.verbose>=5 then self:I(self.lid..text) else self:T2(self.lid..text) end end nammo=nguns+nshells+nrockets+nmissiles+nbombs+ntorps local ammo={} ammo.Total=nammo ammo.Guns=nguns ammo.Shells=nshells ammo.Rockets=nrockets ammo.Bombs=nbombs ammo.Torpedos=ntorps ammo.Missiles=nmissiles ammo.MissilesAA=nmissilesAA ammo.MissilesAG=nmissilesAG ammo.MissilesAS=nmissilesAS ammo.MissilesCR=nmissilesCR ammo.MissilesBM=nmissilesBM ammo.MissilesSA=nmissilesSA return ammo end function COHORT:_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 COHORT:_AddOperation(Operation) self.operations[Operation.name]=Operation end COMMANDER={ ClassName="COMMANDER", verbose=0, coalition=nil, legions={}, missionqueue={}, transportqueue={}, targetqueue={}, opsqueue={}, rearmingZones={}, refuellingZones={}, capZones={}, gcicapZones={}, awacsZones={}, tankerZones={}, limitMission={}, maxMissionsAssignPerCycle=1, } COMMANDER.version="0.1.4" function COMMANDER:New(Coalition,Alias) local self=BASE:Inherit(self,FSM:New()) if Coalition==nil then env.error("ERROR: Coalition parameter is nil in COMMANDER:New() call!") return nil end self.coalition=Coalition self.alias=Alias if self.alias==nil then if Coalition==coalition.side.BLUE then self.alias="George S. Patton" elseif Coalition==coalition.side.RED then self.alias="Georgy Zhukov" elseif Coalition==coalition.side.NEUTRAL then self.alias="Mahatma Gandhi" end end self.lid=string.format("COMMANDER %s [%s] | ",self.alias,UTILS.GetCoalitionName(self.coalition)) self:SetStartState("NotReadyYet") self:AddTransition("NotReadyYet","Start","OnDuty") self:AddTransition("*","Status","*") self:AddTransition("*","Stop","Stopped") self:AddTransition("*","MissionAssign","*") self:AddTransition("*","MissionCancel","*") self:AddTransition("*","TransportAssign","*") self:AddTransition("*","TransportCancel","*") self:AddTransition("*","OpsOnMission","*") self:AddTransition("*","LegionLost","*") return self end function COMMANDER:SetVerbosity(VerbosityLevel) self.verbose=VerbosityLevel or 0 return self end function COMMANDER:SetLimitMission(Limit,MissionType) MissionType=MissionType or"Total" if MissionType then self.limitMission[MissionType]=Limit or 10 else self:E(self.lid.."ERROR: No mission type given for setting limit!") end return self end function COMMANDER:GetCoalition() return self.coalition end function COMMANDER:AddAirwing(Airwing) self:AddLegion(Airwing) return self end function COMMANDER:AddBrigade(Brigade) self:AddLegion(Brigade) return self end function COMMANDER:AddFleet(Fleet) self:AddLegion(Fleet) return self end function COMMANDER:AddLegion(Legion) Legion.commander=self table.insert(self.legions,Legion) return self end function COMMANDER:RemoveLegion(Legion) for i,_legion in pairs(self.legions)do local legion=_legion if legion.alias==Legion.alias then table.remove(self.legions,i) Legion.commander=nil end end return self end function COMMANDER:AddMission(Mission) if not self:IsMission(Mission)then Mission.commander=self Mission.statusCommander=AUFTRAG.Status.PLANNED table.insert(self.missionqueue,Mission) end return self end function COMMANDER:AddOpsTransport(Transport) Transport.commander=self Transport.statusCommander=OPSTRANSPORT.Status.PLANNED table.insert(self.transportqueue,Transport) return self end function COMMANDER:RemoveMission(Mission) for i,_mission in pairs(self.missionqueue)do local mission=_mission if mission.auftragsnummer==Mission.auftragsnummer then self:T(self.lid..string.format("Removing mission %s (%s) status=%s from queue",Mission.name,Mission.type,Mission.status)) mission.commander=nil table.remove(self.missionqueue,i) break end end return self end function COMMANDER:RemoveTransport(Transport) for i,_transport in pairs(self.transportqueue)do local transport=_transport if transport.uid==Transport.uid then self:T(self.lid..string.format("Removing transport UID=%d status=%s from queue",transport.uid,transport:GetState())) transport.commander=nil table.remove(self.transportqueue,i) break end end return self end function COMMANDER:AddTarget(Target) if not self:IsTarget(Target)then table.insert(self.targetqueue,Target) end return self end function COMMANDER:AddOperation(Operation) table.insert(self.opsqueue,Operation) return self end function COMMANDER:IsTarget(Target) for _,_target in pairs(self.targetqueue)do local target=_target if target.uid==Target.uid or target:GetName()==Target:GetName()then return true end end return false end function COMMANDER:RemoveTarget(Target) for i,_target in pairs(self.targetqueue)do local target=_target if target.uid==Target.uid then self:T(self.lid..string.format("Removing target %s from queue",Target.name)) table.remove(self.targetqueue,i) break end end return self end function COMMANDER:AddRearmingZone(RearmingZone) local rearmingzone={} rearmingzone.zone=RearmingZone rearmingzone.mission=nil table.insert(self.rearmingZones,rearmingzone) return rearmingzone end function COMMANDER:AddRefuellingZone(RefuellingZone) local rearmingzone={} rearmingzone.zone=RefuellingZone rearmingzone.mission=nil table.insert(self.refuellingZones,rearmingzone) return rearmingzone end function COMMANDER:AddCapZone(Zone,Altitude,Speed,Heading,Leg) local patrolzone={} patrolzone.zone=Zone patrolzone.altitude=Altitude or 12000 patrolzone.heading=Heading or 270 patrolzone.speed=Speed or 350 patrolzone.leg=Leg or 30 patrolzone.mission=nil table.insert(self.capZones,patrolzone) return patrolzone end function COMMANDER:AddGciCapZone(Zone,Altitude,Speed,Heading,Leg) local patrolzone={} patrolzone.zone=Zone patrolzone.altitude=Altitude or 12000 patrolzone.heading=Heading or 270 patrolzone.speed=Speed or 350 patrolzone.leg=Leg or 30 patrolzone.mission=nil table.insert(self.gcicapZones,patrolzone) return patrolzone end function COMMANDER:RemoveGciCapZone(Zone) local patrolzone={} patrolzone.zone=Zone for i,_patrolzone in pairs(self.gcicapZones)do if _patrolzone.zone==patrolzone.zone then if _patrolzone.mission and _patrolzone.mission:IsNotOver()then _patrolzone.mission:Cancel() end table.remove(self.gcicapZones,i) break end end return patrolzone end function COMMANDER:AddAwacsZone(Zone,Altitude,Speed,Heading,Leg) local awacszone={} awacszone.zone=Zone awacszone.altitude=Altitude or 12000 awacszone.heading=Heading or 270 awacszone.speed=Speed or 350 awacszone.speed=Speed or 350 awacszone.leg=Leg or 30 awacszone.mission=nil table.insert(self.awacsZones,awacszone) return awacszone end function COMMANDER:RemoveAwacsZone(Zone) local awacszone={} awacszone.zone=Zone for i,_awacszone in pairs(self.awacsZones)do if _awacszone.zone==awacszone.zone then if _awacszone.mission and _awacszone.mission:IsNotOver()then _awacszone.mission:Cancel() end table.remove(self.awacsZones,i) break end end return awacszone end function COMMANDER:AddTankerZone(Zone,Altitude,Speed,Heading,Leg,RefuelSystem) local tankerzone={} tankerzone.zone=Zone tankerzone.altitude=Altitude or 12000 tankerzone.heading=Heading or 270 tankerzone.speed=Speed or 350 tankerzone.leg=Leg or 30 tankerzone.refuelsystem=RefuelSystem tankerzone.mission=nil tankerzone.marker=MARKER:New(tankerzone.zone:GetCoordinate(),"Tanker Zone"):ToCoalition(self:GetCoalition()) table.insert(self.tankerZones,tankerzone) return tankerzone end function COMMANDER:RemoveTankerZone(Zone) local tankerzone={} tankerzone.zone=Zone for i,_tankerzone in pairs(self.tankerZones)do if _tankerzone.zone==tankerzone.zone then if _tankerzone.mission and _tankerzone.mission:IsNotOver()then _tankerzone.mission:Cancel() end table.remove(self.tankerZones,i) break end end return tankerzone end function COMMANDER:IsMission(Mission) for _,_mission in pairs(self.missionqueue)do local mission=_mission if mission.auftragsnummer==Mission.auftragsnummer then return true end end return false end function COMMANDER:RelocateCohort(Cohort,Legion,Delay,NcarriersMin,NcarriersMax,TransportLegions) if Delay and Delay>0 then self:ScheduleOnce(Delay,COMMANDER.RelocateCohort,self,Cohort,Legion,0,NcarriersMin,NcarriersMax,TransportLegions) else if Legion:IsCohort(Cohort.name)then self:E(self.lid..string.format("ERROR: Cohort %s is already part of new legion %s ==> CANNOT Relocate!",Cohort.name,Legion.alias)) return self else table.insert(Legion.cohorts,Cohort) end local LegionOld=Cohort.legion if not LegionOld:IsCohort(Cohort.name)then self:E(self.lid..string.format("ERROR: Cohort %s is NOT part of this legion %s ==> CANNOT Relocate!",Cohort.name,self.alias)) return self end if LegionOld.alias==Legion.alias then self:E(self.lid..string.format("ERROR: old legion %s is same as new legion %s ==> CANNOT Relocate!",LegionOld.alias,Legion.alias)) return self end Cohort:Relocate() local mission=AUFTRAG:_NewRELOCATECOHORT(Legion,Cohort) mission:AssignCohort(Cohort) mission:SetRequiredAssets(#Cohort.assets) if NcarriersMin and NcarriersMin>0 then mission:SetRequiredTransport(Legion.spawnzone,NcarriersMin,NcarriersMax) end if TransportLegions then for _,legion in pairs(TransportLegions)do mission:AssignTransportLegion(legion) end else for _,legion in pairs(self.legions)do mission:AssignTransportLegion(legion) end end mission:SetMissionRange(10000) self:AddMission(mission) end return self end function COMMANDER:onafterStart(From,Event,To) local text=string.format("Starting Commander") self:I(self.lid..text) for _,_legion in pairs(self.legions)do local legion=_legion if legion:GetState()=="NotReadyYet"then legion:Start() end end self:__Status(-1) end function COMMANDER:onafterStatus(From,Event,To) local fsmstate=self:GetState() if self.verbose>=1 then local text=string.format("Status %s: Legions=%d, Missions=%d, Targets=%d, Transports=%d",fsmstate,#self.legions,#self.missionqueue,#self.targetqueue,#self.transportqueue) self:T(self.lid..text) end self:CheckOpsQueue() self:CheckTargetQueue() self:CheckMissionQueue() self:CheckTransportQueue() for _,_rearmingzone in pairs(self.rearmingZones)do local rearmingzone=_rearmingzone if(not rearmingzone.mission)or rearmingzone.mission:IsOver()then rearmingzone.mission=AUFTRAG:NewAMMOSUPPLY(rearmingzone.zone) self:AddMission(rearmingzone.mission) end end for _,_supplyzone in pairs(self.refuellingZones)do local supplyzone=_supplyzone if(not supplyzone.mission)or supplyzone.mission:IsOver()then supplyzone.mission=AUFTRAG:NewFUELSUPPLY(supplyzone.zone) self:AddMission(supplyzone.mission) end end for _,_patrolzone in pairs(self.capZones)do local patrolzone=_patrolzone if(not patrolzone.mission)or patrolzone.mission:IsOver()then local Coordinate=patrolzone.zone:GetCoordinate() patrolzone.mission=AUFTRAG:NewCAP(patrolzone.zone,patrolzone.altitude,patrolzone.speed,Coordinate,patrolzone.heading,patrolzone.leg) self:AddMission(patrolzone.mission) end end for _,_patrolzone in pairs(self.gcicapZones)do local patrolzone=_patrolzone if(not patrolzone.mission)or patrolzone.mission:IsOver()then local Coordinate=patrolzone.zone:GetCoordinate() patrolzone.mission=AUFTRAG:NewGCICAP(Coordinate,patrolzone.altitude,patrolzone.speed,patrolzone.heading,patrolzone.leg) self:AddMission(patrolzone.mission) end end for _,_awacszone in pairs(self.awacsZones)do local awacszone=_awacszone if(not awacszone.mission)or awacszone.mission:IsOver()then local Coordinate=awacszone.zone:GetCoordinate() awacszone.mission=AUFTRAG:NewAWACS(Coordinate,awacszone.altitude,awacszone.speed,awacszone.heading,awacszone.leg) self:AddMission(awacszone.mission) end end for _,_tankerzone in pairs(self.tankerZones)do local tankerzone=_tankerzone if(not tankerzone.mission)or tankerzone.mission:IsOver()then local Coordinate=tankerzone.zone:GetCoordinate() tankerzone.mission=AUFTRAG:NewTANKER(Coordinate,tankerzone.altitude,tankerzone.speed,tankerzone.heading,tankerzone.leg,tankerzone.refuelsystem) self:AddMission(tankerzone.mission) end end if self.verbose>=2 and#self.legions>0 then local text="Legions:" for _,_legion in pairs(self.legions)do local legion=_legion local Nassets=legion:CountAssets() local Nastock=legion:CountAssets(true) text=text..string.format("\n* %s [%s]: Assets=%s stock=%s",legion.alias,legion:GetState(),Nassets,Nastock) for _,aname in pairs(AUFTRAG.Type)do local na=legion:CountAssets(true,{aname}) local np=legion:CountPayloadsInStock({aname}) local nm=legion:CountAssetsOnMission({aname}) if na>0 or np>0 then text=text..string.format("\n - %s: assets=%d, payloads=%d, on mission=%d",aname,na,np,nm) end end end self:T(self.lid..text) if self.verbose>=3 then local Ntotal=0 local Nspawned=0 local Nrequested=0 local Nreserved=0 local Nstock=0 local text="\n===========================================\n" text=text.."Assets:" for _,_legion in pairs(self.legions)do local legion=_legion for _,_cohort in pairs(legion.cohorts)do local cohort=_cohort for _,_asset in pairs(cohort.assets)do local asset=_asset local state="In Stock" if asset.flightgroup then state=asset.flightgroup:GetState() local mission=legion:GetAssetCurrentMission(asset) if mission then state=state..string.format(", Mission \"%s\" [%s]",mission:GetName(),mission:GetType()) end else if asset.spawned then env.info("FF ERROR: asset has opsgroup but is NOT spawned!") end if asset.requested and asset.isReserved then env.info("FF ERROR: asset is requested and reserved. Should not be both!") state="Reserved+Requested!" elseif asset.isReserved then state="Reserved" elseif asset.requested then state="Requested" end end text=text..string.format("\n[UID=%03d] %s Legion=%s [%s]: State=%s [RID=%s]", asset.uid,asset.spawngroupname,legion.alias,cohort.name,state,tostring(asset.rid)) if asset.spawned then Nspawned=Nspawned+1 end if asset.requested then Nrequested=Nrequested+1 end if asset.isReserved then Nreserved=Nreserved+1 end if not(asset.spawned or asset.requested or asset.isReserved)then Nstock=Nstock+1 end Ntotal=Ntotal+1 end end end text=text.."\n-------------------------------------------" text=text..string.format("\nNstock = %d",Nstock) text=text..string.format("\nNreserved = %d",Nreserved) text=text..string.format("\nNrequested = %d",Nrequested) text=text..string.format("\nNspawned = %d",Nspawned) text=text..string.format("\nNtotal = %d (=%d)",Ntotal,Nstock+Nspawned+Nrequested+Nreserved) text=text.."\n===========================================" self:I(self.lid..text) end end if self.verbose>=2 and#self.missionqueue>0 then local text="Mission queue:" for i,_mission in pairs(self.missionqueue)do local mission=_mission local target=mission:GetTargetName()or"unknown" text=text..string.format("\n[%d] %s (%s): status=%s, target=%s",i,mission.name,mission.type,mission.status,target) end self:I(self.lid..text) end if self.verbose>=2 and#self.targetqueue>0 then local text="Target queue:" for i,_target in pairs(self.targetqueue)do local target=_target text=text..string.format("\n[%d] %s: status=%s, life=%d",i,target:GetName(),target:GetState(),target:GetLife()) end self:I(self.lid..text) end if self.verbose>=2 and#self.transportqueue>0 then local text="Transport queue:" for i,_transport in pairs(self.transportqueue)do local transport=_transport text=text..string.format("\n[%d] UID=%d: status=%s",i,transport.uid,transport:GetState()) end self:I(self.lid..text) end self:__Status(-30) end function COMMANDER:onafterMissionAssign(From,Event,To,Mission,Legions) self:AddMission(Mission) Mission.statusCommander=AUFTRAG.Status.QUEUED for _,_Legion in pairs(Legions)do local Legion=_Legion self:T(self.lid..string.format("Assigning mission \"%s\" [%s] to legion \"%s\"",Mission.name,Mission.type,Legion.alias)) Legion:AddMission(Mission) Legion:MissionRequest(Mission) end end function COMMANDER:onafterMissionCancel(From,Event,To,Mission) self:T(self.lid..string.format("Cancelling mission \"%s\" [%s] in status %s",Mission.name,Mission.type,Mission.status)) Mission.statusCommander=AUFTRAG.Status.CANCELLED if Mission:IsPlanned()then self:RemoveMission(Mission) else if#Mission.legions>0 then for _,_legion in pairs(Mission.legions)do local legion=_legion legion:MissionCancel(Mission) end end end end function COMMANDER:onafterTransportAssign(From,Event,To,Transport,Legions) Transport.statusCommander=OPSTRANSPORT.Status.QUEUED for _,_Legion in pairs(Legions)do local Legion=_Legion self:T(self.lid..string.format("Assigning transport UID=%d to legion \"%s\"",Transport.uid,Legion.alias)) Legion:AddOpsTransport(Transport) Legion:TransportRequest(Transport) end end function COMMANDER:onafterTransportCancel(From,Event,To,Transport) self:T(self.lid..string.format("Cancelling Transport UID=%d in status %s",Transport.uid,Transport:GetState())) Transport.statusCommander=OPSTRANSPORT.Status.CANCELLED if Transport:IsPlanned()then self:RemoveTransport(Transport) else if#Transport.legions>0 then for _,_legion in pairs(Transport.legions)do local legion=_legion legion:TransportCancel(Transport) end end end end function COMMANDER:onafterOpsOnMission(From,Event,To,OpsGroup,Mission) self:T2(self.lid..string.format("Group \"%s\" on mission \"%s\" [%s]",OpsGroup:GetName(),Mission:GetName(),Mission:GetType())) end function COMMANDER:CheckOpsQueue() local Nops=#self.opsqueue if Nops==0 then return nil end for _,_ops in pairs(self.opsqueue)do local operation=_ops if operation:IsRunning()then for _,_mission in pairs(operation.missions or{})do local mission=_mission if mission.phase==nil or(mission.phase and mission.phase==operation.phase)and mission:IsPlanned()then self:AddMission(mission) end end for _,_target in pairs(operation.targets or{})do local target=_target if(target.phase==nil or(target.phase and target.phase==operation.phase))and(not self:IsTarget(target))then self:AddTarget(target) end end end end end function COMMANDER:CheckTargetQueue() local Ntargets=#self.targetqueue if Ntargets==0 then return nil end for i=#self.targetqueue,1,-1 do local target=self.targetqueue[i] if(not target:IsAlive())or target:EvalConditionsAny(target.conditionStop)then for _,_resource in pairs(target.resources)do local resource=_resource if resource.mission and resource.mission:IsNotOver()then self:MissionCancel(resource.mission) end end table.remove(self.targetqueue,i) end end local NoLimit=self:_CheckMissionLimit("Total") if NoLimit==false then return nil end local function _sort(a,b) local taskA=a local taskB=b return(taskA.priotaskB.threatlevel0) end table.sort(self.targetqueue,_sort) local vip=math.huge for _,_target in pairs(self.targetqueue)do local target=_target if target:IsAlive()and target.importance and target.importance Creating mission type %s: Nmin=%d, Nmax=%d",target:GetName(),missionType,resource.Nmin,resource.Nmax)) local mission=AUFTRAG:NewFromTarget(target,missionType) if mission then mission:SetRequiredAssets(resource.Nmin,resource.Nmax) mission:SetRequiredAttribute(resource.Attributes) mission:SetRequiredProperty(resource.Properties) mission.operation=target.operation resource.mission=mission self:AddMission(resource.mission) end end end end end end function COMMANDER:CheckMissionQueue() local Nmissions=#self.missionqueue if Nmissions==0 then return nil end local NoLimit=self:_CheckMissionLimit("Total") if NoLimit==false then return nil end local function _sort(a,b) local taskA=a local taskB=b return(taskA.prio=(self.maxMissionsAssignPerCycle or 1)then return end end else end end end function COMMANDER:SetMaxMissionsAssignPerCycle(MaxMissionsAssignPerCycle) self.maxMissionsAssignPerCycle=MaxMissionsAssignPerCycle or 1 return self end function COMMANDER:_GetCohorts(Legions,Cohorts,Operation) local function CheckOperation(LegionOrCohort) if#self.opsqueue==0 then return true end local isAvail=true if Operation then isAvail=false end for _,_operation in pairs(self.opsqueue)do local operation=_operation local isOps=operation:IsAssignedCohortOrLegion(LegionOrCohort) if isOps and operation:IsRunning()then isAvail=false if Operation==nil then return false else if Operation.uid==operation.uid then return true end end end end return isAvail end local cohorts={} if(Legions and#Legions>0)or(Cohorts and#Cohorts>0)then for _,_legion in pairs(Legions or{})do local legion=_legion local Runway=true if legion:IsAirwing()then Runway=legion:IsRunwayOperational()and legion.airbase and legion.airbase:GetCoalition()==legion:GetCoalition() end if legion:IsRunning()and Runway then for _,_cohort in pairs(legion.cohorts)do local cohort=_cohort if CheckOperation(cohort.legion)or CheckOperation(cohort)then table.insert(cohorts,cohort) end end end end for _,_cohort in pairs(Cohorts or{})do local cohort=_cohort if CheckOperation(cohort)then table.insert(cohorts,cohort) end end else for _,_legion in pairs(self.legions)do local legion=_legion local Runway=true if legion:IsAirwing()then Runway=legion:IsRunwayOperational()and legion.airbase and legion.airbase:GetCoalition()==legion:GetCoalition() end if legion:IsRunning()and Runway then for _,_cohort in pairs(legion.cohorts)do local cohort=_cohort if CheckOperation(cohort.legion)or CheckOperation(cohort)then table.insert(cohorts,cohort) end end end end end return cohorts end function COMMANDER:CanMission(Mission) local commander=self local TargetVec2=Mission:GetTargetVec2() local MaxWeight=nil if Mission.NcarriersMin then local legions=commander.legions local cohorts=nil if Mission.transportLegions or Mission.transportCohorts then legions=Mission.transportLegions cohorts=Mission.transportCohorts end local Cohorts=LEGION._GetCohorts(legions,cohorts) local transportcohorts={} for _,_cohort in pairs(Cohorts)do local cohort=_cohort local can=LEGION._CohortCan(cohort,AUFTRAG.Type.OPSTRANSPORT,Mission.carrierCategories,Mission.carrierAttributes,Mission.carrierProperties,nil,TargetVec2) if can and(MaxWeight==nil or cohort.cargobayLimit>MaxWeight)then MaxWeight=cohort.cargobayLimit end end end local legions=commander.legions local cohorts=nil if Mission.specialLegions or Mission.specialCohorts then legions=Mission.specialLegions cohorts=Mission.specialCohorts end local Cohorts=LEGION._GetCohorts(legions,cohorts,Mission.operation,commander.opsqueue) for _,_cohort in pairs(Cohorts)do local cohort=_cohort local can=LEGION._CohortCan(cohort,Mission.type,nil,Mission.attributes,Mission.properties,{Mission.engageWeaponType},TargetVec2,Mission.engageRange,Mission.refuelSystem,nil,MaxWeight) if can then return true end end return false end function COMMANDER:RecruitAssetsForMission(Mission) self:T2(self.lid..string.format("Recruiting assets for mission \"%s\" [%s]",Mission:GetName(),Mission:GetType())) local NreqMin,NreqMax=Mission:GetRequiredAssets() local TargetVec2=Mission:GetTargetVec2() local Payloads=Mission.payloads local MaxWeight=nil if Mission.NcarriersMin then local legions=self.legions local cohorts=nil if Mission.transportLegions or Mission.transportCohorts then legions=Mission.transportLegions cohorts=Mission.transportCohorts end local Cohorts=LEGION._GetCohorts(legions,cohorts) local transportcohorts={} for _,_cohort in pairs(Cohorts)do local cohort=_cohort local can=LEGION._CohortCan(cohort,AUFTRAG.Type.OPSTRANSPORT,Mission.carrierCategories,Mission.carrierAttributes,Mission.carrierProperties,nil,TargetVec2) if can and(MaxWeight==nil or cohort.cargobayLimit>MaxWeight)then MaxWeight=cohort.cargobayLimit end end if MaxWeight then self:T(self.lid..string.format("Largest cargo bay available=%.1f",MaxWeight)) end end local legions=self.legions local cohorts=nil if Mission.specialLegions or Mission.specialCohorts then legions=Mission.specialLegions cohorts=Mission.specialCohorts end local Cohorts=LEGION._GetCohorts(legions,cohorts,Mission.operation,self.opsqueue) self:T(self.lid..string.format("Found %d cohort candidates for mission",#Cohorts)) local recruited,assets,legions=LEGION.RecruitCohortAssets(Cohorts,Mission.type,Mission.alert5MissionType,NreqMin,NreqMax,TargetVec2,Payloads, Mission.engageRange,Mission.refuelSystem,nil,nil,MaxWeight,nil,Mission.attributes,Mission.properties,{Mission.engageWeaponType}) return recruited,assets,legions end function COMMANDER:RecruitAssetsForEscort(Mission,Assets) if Mission.NescortMin and Mission.NescortMax and(Mission.NescortMin>0 or Mission.NescortMax>0)then local Cohorts=self:_GetCohorts(Mission.escortLegions,Mission.escortCohorts,Mission.operation) local assigned=LEGION.AssignAssetsForEscort(self,Cohorts,Assets,Mission.NescortMin,Mission.NescortMax,Mission.escortMissionType,Mission.escortTargetTypes,Mission.escortEngageRange) return assigned end return true end function COMMANDER:RecruitAssetsForTarget(Target,MissionType,NassetsMin,NassetsMax,RangeMin) local Cohorts=self:_GetCohorts() local TargetVec2=Target:GetVec2() local recruited,assets,legions=LEGION.RecruitCohortAssets(Cohorts,MissionType,nil,NassetsMin,NassetsMax,TargetVec2,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,RangeMin) return recruited,assets,legions end function COMMANDER:CheckTransportQueue() local Ntransports=#self.transportqueue if Ntransports==0 then return nil end local function _sort(a,b) local taskA=a local taskB=b return(taskA.prio0 then for _,_opsgroup in pairs(cargoOpsGroups)do local opsgroup=_opsgroup local weight=opsgroup:GetWeightTotal() if weight>weightGroup then weightGroup=weight end TotalWeight=TotalWeight+weight end end if weightGroup>0 then local recruited,assets,legions=self:RecruitAssetsForTransport(transport,weightGroup,TotalWeight) if recruited then for _,_asset in pairs(assets)do local asset=_asset transport:AddAsset(asset) end self:TransportAssign(transport,legions) return else LEGION.UnRecruitAssets(assets) end end else end end end function COMMANDER:RecruitAssetsForTransport(Transport,CargoWeight,TotalWeight) if CargoWeight==0 then return false,{},{} end local Cohorts=self:_GetCohorts() local TargetVec2=Transport:GetDeployZone():GetVec2() local NreqMin,NreqMax=Transport:GetRequiredCarriers() local recruited,assets,legions=LEGION.RecruitCohortAssets(Cohorts,AUFTRAG.Type.OPSTRANSPORT,nil,NreqMin,NreqMax,TargetVec2,nil,nil,nil,CargoWeight,TotalWeight) return recruited,assets,legions end function COMMANDER:_CheckMissionLimit(MissionType) local limit=self.limitMission[MissionType] if limit then if MissionType=="Total"then MissionType=AUFTRAG.Type end local N=self:CountMissions(MissionType,true) if N>=limit then return false end end return true end function COMMANDER:CountAssets(InStock,MissionTypes,Attributes) local N=0 for _,_legion in pairs(self.legions)do local legion=_legion N=N+legion:CountAssets(InStock,MissionTypes,Attributes) end return N end function COMMANDER:CountMissions(MissionTypes,OnlyRunning) local N=0 for _,_mission in pairs(self.missionqueue)do local mission=_mission if(not OnlyRunning)or(mission.statusCommander~=AUFTRAG.Status.PLANNED)then if AUFTRAG.CheckMissionType(mission.type,MissionTypes)then N=N+1 end end end return N end function COMMANDER:GetAssets(InStock,Legions,MissionTypes,Attributes) local assets={} for _,_legion in pairs(Legions or self.legions)do local legion=_legion for _,_cohort in pairs(legion.cohorts)do local cohort=_cohort for _,_asset in pairs(cohort.assets)do local asset=_asset if not(asset.spawned or asset.isReserved or asset.requested)then table.insert(assets,asset) end end end end return assets end function COMMANDER:GetLegionsForMission(Mission) local legions={} for _,_legion in pairs(self.legions)do local legion=_legion local Nassets=0 if legion:IsAirwing()then Nassets=legion:CountAssetsWithPayloadsInStock(Mission.payloads,{Mission.type},Attributes) else Nassets=legion:CountAssets(true,{Mission.type},Attributes) end if Nassets>0 and false then local coord=Mission:GetTargetCoordinate() if coord then local distance=UTILS.MetersToNM(coord:Get2DDistance(legion:GetCoordinate())) local dist=UTILS.Round(distance/10,0) self:T(self.lid..string.format("Got legion %s with Nassets=%d and dist=%.1f NM, rounded=%.1f",legion.alias,Nassets,distance,dist)) table.insert(legions,{airwing=legion,distance=distance,dist=dist,targetcoord=coord,nassets=Nassets}) end end if Nassets>0 then table.insert(legions,legion) end end return legions end function COMMANDER:GetAssetsOnMission(MissionTypes) local assets={} for _,_mission in pairs(self.missionqueue)do local mission=_mission if AUFTRAG.CheckMissionType(mission.type,MissionTypes)then for _,_asset in pairs(mission.assets or{})do local asset=_asset table.insert(assets,asset) end end end return assets end FLEET={ ClassName="FLEET", verbose=0, pathfinding=false, } FLEET.version="0.0.1" function FLEET:New(WarehouseName,FleetName) local self=BASE:Inherit(self,LEGION:New(WarehouseName,FleetName)) if not self then BASE:E(string.format("ERROR: Could not find warehouse %s!",WarehouseName)) return nil end self.lid=string.format("FLEET %s | ",self.alias) self:SetRetreatZones() if self:IsShip()then local wh=self.warehouse local group=wh:GetGroup() self.warehouseOpsGroup=NAVYGROUP:New(group) self.warehouseOpsElement=self.warehouseOpsGroup:GetElementByName(wh:GetName()) end self:AddTransition("*","NavyOnMission","*") return self end function FLEET:AddFlotilla(Flotilla) table.insert(self.cohorts,Flotilla) self:AddAssetToFlotilla(Flotilla,Flotilla.Ngroups) Flotilla:SetFleet(self) if Flotilla:IsStopped()then Flotilla:Start() end return self end function FLEET:AddAssetToFlotilla(Flotilla,Nassets) if Flotilla then local Group=GROUP:FindByName(Flotilla.templatename) if Group then local text=string.format("Adding asset %s to flotilla %s",Group:GetName(),Flotilla.name) self:T(self.lid..text) self:AddAsset(Group,Nassets,nil,nil,nil,nil,Flotilla.skill,Flotilla.livery,Flotilla.name) else self:E(self.lid.."ERROR: Group does not exist!") end else self:E(self.lid.."ERROR: Flotilla does not exit!") end return self end function FLEET:SetPathfinding(Switch) self.pathfinding=Switch return self end function FLEET:SetRetreatZones(RetreatZoneSet) self.retreatZones=RetreatZoneSet or SET_ZONE:New() return self end function FLEET:AddRetreatZone(RetreatZone) self.retreatZones:AddZone(RetreatZone) return self end function FLEET:GetRetreatZones() return self.retreatZones end function FLEET:GetFlotilla(FlotillaName) local flotilla=self:_GetCohort(FlotillaName) return flotilla end function FLEET:GetFlotillaOfAsset(Asset) local flotilla=self:GetFlotilla(Asset.squadname) return flotilla end function FLEET:RemoveAssetFromFlotilla(Asset) local flotilla=self:GetFlotillaOfAsset(Asset) if flotilla then flotilla:DelAsset(Asset) end end function FLEET:onafterStart(From,Event,To) self:GetParent(self,FLEET).onafterStart(self,From,Event,To) self:I(self.lid..string.format("Starting FLEET v%s",FLEET.version)) end function FLEET:onafterStatus(From,Event,To) self:GetParent(self).onafterStatus(self,From,Event,To) local fsmstate=self:GetState() self:CheckTransportQueue() self:CheckMissionQueue() self:_TacticalOverview() if self.verbose>=1 then local Nmissions=self:CountMissionsInQueue() local Npq,Np,Nq=self:CountAssetsOnMission() local assets=string.format("%d [OnMission: Total=%d, Active=%d, Queued=%d]",self:CountAssets(),Npq,Np,Nq) local text=string.format("%s: Missions=%d, Flotillas=%d, Assets=%s",fsmstate,Nmissions,#self.cohorts,assets) self:I(self.lid..text) end if self.verbose>=2 then local text=string.format("Missions Total=%d:",#self.missionqueue) for i,_mission in pairs(self.missionqueue)do local mission=_mission local prio=string.format("%d/%s",mission.prio,tostring(mission.importance));if mission.urgent then prio=prio.." (!)"end local assets=string.format("%d/%d",mission:CountOpsGroups(),mission.Nassets or 0) local target=string.format("%d/%d Damage=%.1f",mission:CountMissionTargets(),mission:GetTargetInitialNumber(),mission:GetTargetDamage()) text=text..string.format("\n[%d] %s %s: Status=%s, Prio=%s, Assets=%s, Targets=%s",i,mission.name,mission.type,mission.status,prio,assets,target) end self:I(self.lid..text) end if self.verbose>=2 then local text=string.format("Transports Total=%d:",#self.transportqueue) for i,_transport in pairs(self.transportqueue)do local transport=_transport local prio=string.format("%d/%s",transport.prio,tostring(transport.importance));if transport.urgent then prio=prio.." (!)"end local carriers=string.format("Ncargo=%d/%d, Ncarriers=%d",transport.Ncargo,transport.Ndelivered,transport.Ncarrier) text=text..string.format("\n[%d] UID=%d: Status=%s, Prio=%s, Cargo: %s",i,transport.uid,transport:GetState(),prio,carriers) end self:I(self.lid..text) end if self.verbose>=3 then local text="Flotillas:" for i,_flotilla in pairs(self.cohorts)do local flotilla=_flotilla local callsign=flotilla.callsignName and UTILS.GetCallsignName(flotilla.callsignName)or"N/A" local modex=flotilla.modex and flotilla.modex or-1 local skill=flotilla.skill and tostring(flotilla.skill)or"N/A" text=text..string.format("\n* %s %s: %s*%d/%d, Callsign=%s, Modex=%d, Skill=%s",flotilla.name,flotilla:GetState(),flotilla.aircrafttype,flotilla:CountAssets(true),#flotilla.assets,callsign,modex,skill) end self:I(self.lid..text) end end function FLEET:onafterNavyOnMission(From,Event,To,NavyGroup,Mission) self:T(self.lid..string.format("Group %s on %s mission %s",NavyGroup:GetName(),Mission:GetType(),Mission:GetName())) end FLIGHTCONTROL={ ClassName="FLIGHTCONTROL", verbose=0, lid=nil, theatre=nil, airbasename=nil, airbase=nil, airbasetype=nil, zoneAirbase=nil, parking={}, runways={}, flights={}, clients={}, atis=nil, Nlanding=nil, dTlanding=nil, Nparkingspots=nil, holdingpatterns={}, hpcounter=0, nosubs=false, Nplayers=0, } FLIGHTCONTROL.FlightStatus={ UNKNOWN="Unknown", PARKING="Parking", READYTX="Ready To Taxi", TAXIOUT="Taxi To Runway", READYTO="Ready For Takeoff", TAKEOFF="Takeoff", INBOUND="Inbound", HOLDING="Holding", LANDING="Landing", TAXIINB="Taxi To Parking", ARRIVED="Arrived", } FLIGHTCONTROL.version="0.7.7" function FLIGHTCONTROL:New(AirbaseName,Frequency,Modulation,PathToSRS,Port,GoogleKey,Provider) local self=BASE:Inherit(self,FSM:New()) self.airbase=AIRBASE:FindByName(AirbaseName) self.airbasename=AirbaseName self.lid=string.format("FLIGHTCONTROL %s | ",AirbaseName) if not self.airbase then self:E(string.format("ERROR: Could not find airbase %s!",tostring(AirbaseName))) return nil end if self.airbase:GetAirbaseCategory()~=Airbase.Category.AIRDROME then self:E(string.format("ERROR: Airbase %s is not an AIRDROME! Script does not handle FARPS or ships.",tostring(AirbaseName))) return nil end self.airbasetype=self.airbase:GetAirbaseCategory() self.theatre=env.mission.theatre self.zoneAirbase=ZONE_RADIUS:New("FC",self:GetCoordinate():GetVec2(),UTILS.NMToMeters(5)) self:_AddHoldingPatternBackup() self.alias=self.airbasename.." Tower" self:SetLimitLanding(2,0) self:SetLimitTaxi(2,false,0) self:SetLandingInterval() self:SetFrequency(Frequency,Modulation) self:SetMarkHoldingPattern(true) self:SetRunwayRepairtime() self.nosubs=false self:SetCallSignOptions(true,true) self.msrsqueue=MSRSQUEUE:New(self.alias) self:SetTransmitOnlyWithPlayers(true) local path=PathToSRS or MSRS.path local port=Port or MSRS.port or 5002 self:SetSRSPort(port) self.msrsTower=MSRS:New(path,Frequency,Modulation) self.msrsTower:SetPort(port) if GoogleKey then self.msrsTower:SetProviderOptionsGoogle(GoogleKey,GoogleKey) self.msrsTower:SetProvider(MSRS.Provider.GOOGLE) end if Provider then self.msrsTower:SetProvider(Provider) end self.msrsTower:SetCoordinate(self:GetCoordinate()) self:SetSRSTower() self.msrsPilot=MSRS:New(PathToSRS,Frequency,Modulation) self.msrsPilot:SetPort(self.Port) if GoogleKey then self.msrsPilot:SetProviderOptionsGoogle(GoogleKey,GoogleKey) self.msrsPilot:SetProvider(MSRS.Provider.GOOGLE) end if Provider then self.msrsPilot:SetProvider(Provider) end self.msrsTower:SetCoordinate(self:GetCoordinate()) self:SetSRSPilot() self.dTmessage=10 self:SetStartState("Stopped") self:AddTransition("Stopped","Start","Running") self:AddTransition("*","StatusUpdate","*") self:AddTransition("*","PlayerKilledGuard","*") self:AddTransition("*","PlayerSpeeding","*") self:AddTransition("*","RunwayDestroyed","*") self:AddTransition("*","RunwayRepaired","*") self:AddTransition("*","Stop","Stopped") _DATABASE:AddFlightControl(self) return self end function FLIGHTCONTROL:SetVerbosity(VerbosityLevel) self.verbose=VerbosityLevel or 0 return self end function FLIGHTCONTROL:SetRadioOnlyIfPlayers(Switch) if Switch==nil or Switch==true then self.radioOnlyIfPlayers=true else self.radioOnlyIfPlayers=false end return self end function FLIGHTCONTROL:SetTransmitOnlyWithPlayers(Switch) self.msrsqueue:SetTransmitOnlyWithPlayers(Switch) return self end function FLIGHTCONTROL:SwitchSubtitlesOn() self.nosubs=false return self end function FLIGHTCONTROL:SwitchSubtitlesOff() self.nosubs=true return self end function FLIGHTCONTROL:SetFrequency(Frequency,Modulation) self.frequency=Frequency or 305 self.modulation=Modulation or radio.modulation.AM if self.msrsPilot then self.msrsPilot:SetFrequencies(Frequency) self.msrsPilot:SetModulations(Modulation) end if self.msrsTower then self.msrsTower:SetFrequencies(Frequency) self.msrsTower:SetModulations(Modulation) end return self end function FLIGHTCONTROL:SetSRSPort(Port) self.Port=Port or 5002 return self end function FLIGHTCONTROL:_SetSRSOptions(msrs,Gender,Culture,Voice,Volume,Label,PathToGoogleCredentials,Port,Speaker) Gender=Gender or"female" Culture=Culture or"en-GB" Volume=Volume or 1.0 if msrs then msrs:SetGender(Gender) msrs:SetCulture(Culture) msrs:SetVoice(Voice) if Speaker then msrs:SetSpeakerPiper(Speaker) end msrs:SetVolume(Volume) msrs:SetLabel(Label) msrs:SetCoalition(self:GetCoalition()) msrs:SetPort(Port or self.Port or 5002) end return self end function FLIGHTCONTROL:SetSRSTower(Gender,Culture,Voice,Volume,Label,Speaker) if self.msrsTower then self:_SetSRSOptions(self.msrsTower,Gender or"female",Culture or"en-GB",Voice,Volume,Label or self.alias,nil,nil,Speaker) end return self end function FLIGHTCONTROL:SetSRSPilot(Gender,Culture,Voice,Volume,Label,Speaker) if self.msrsPilot then self:_SetSRSOptions(self.msrsPilot,Gender or"male",Culture or"en-US",Voice,Volume,Label or"Pilot",nil,nil,Speaker) end return self end function FLIGHTCONTROL:SetLimitLanding(Nlanding,Ntakeoff) self.NlandingTot=Nlanding or 2 self.NlandingTakeoff=Ntakeoff or 0 return self end function FLIGHTCONTROL:SetLandingInterval(dt) self.dTlanding=dt or 180 return self end function FLIGHTCONTROL:SetLimitTaxi(Ntaxi,IncludeInbound,Nlanding) self.NtaxiTot=Ntaxi or 2 self.NtaxiInbound=IncludeInbound self.NtaxiLanding=Nlanding or 0 return self end function FLIGHTCONTROL:AddHoldingPattern(ArrivalZone,Heading,Length,FlightlevelMin,FlightlevelMax,Prio) if type(ArrivalZone)=="string"then ArrivalZone=ZONE:New(ArrivalZone) end self.hpcounter=self.hpcounter+1 local hp={} hp.uid=self.hpcounter hp.arrivalzone=ArrivalZone hp.name=string.format("%s-%d",ArrivalZone:GetName(),hp.uid) hp.pos0=ArrivalZone:GetCoordinate() hp.pos1=hp.pos0:Translate(UTILS.NMToMeters(Length or 15),Heading) hp.angelsmin=FlightlevelMin or 5 hp.angelsmax=FlightlevelMax or 15 hp.prio=Prio or 50 hp.stacks={} for i=hp.angelsmin,hp.angelsmax do local stack={} stack.angels=i stack.flightgroup=nil stack.pos0=UTILS.DeepCopy(hp.pos0) stack.pos0:SetAltitude(UTILS.FeetToMeters(i*1000)) stack.pos1=UTILS.DeepCopy(hp.pos1) stack.pos1:SetAltitude(UTILS.FeetToMeters(i*1000)) stack.heading=Heading table.insert(hp.stacks,stack) end table.insert(self.holdingpatterns,hp) local function _sort(a,b) return a.prio0 then local text=string.format("Still got %d messages in the radio queue. Will call status again in %.1f sec",#self.msrsqueue,Tqueue) self:T(self.lid..text) self:__StatusUpdate(-Tqueue) return false end return true end function FLIGHTCONTROL:onafterStatusUpdate() self:T2(self.lid.."Status update") self:_CheckMarkHoldingPatterns() if self:IsRunwayOperational()==false then local Trepair=self:GetRunwayRepairtime() if Trepair==0 then self:RunwayRepaired() else self:I(self.lid..string.format("Runway still destroyed! Will be repaired in %d sec",Trepair)) end end self:_CheckFlights() self:_CheckQueues() local rwyLanding=self:GetActiveRunwayText() local rwyTakeoff=self:GetActiveRunwayText(true) local Nflights=self:CountFlights() local NQparking=self:CountFlights(FLIGHTCONTROL.FlightStatus.PARKING) local NQreadytx=self:CountFlights(FLIGHTCONTROL.FlightStatus.READYTX) local NQtaxiout=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAXIOUT) local NQreadyto=self:CountFlights(FLIGHTCONTROL.FlightStatus.READYTO) local NQtakeoff=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAKEOFF) local NQinbound=self:CountFlights(FLIGHTCONTROL.FlightStatus.INBOUND) local NQholding=self:CountFlights(FLIGHTCONTROL.FlightStatus.HOLDING) local NQlanding=self:CountFlights(FLIGHTCONTROL.FlightStatus.LANDING) local NQtaxiinb=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAXIINB) local NQarrived=self:CountFlights(FLIGHTCONTROL.FlightStatus.ARRIVED) local Nqueues=(NQparking+NQreadytx+NQtaxiout+NQreadyto+NQtakeoff)+(NQinbound+NQholding+NQlanding+NQtaxiinb+NQarrived) local nfree=self.Nparkingspots-NQarrived-NQparking local Nfree=self:CountParking(AIRBASE.SpotStatus.FREE) local Noccu=self:CountParking(AIRBASE.SpotStatus.OCCUPIED) local Nresv=self:CountParking(AIRBASE.SpotStatus.RESERVED) if Nfree+Noccu+Nresv~=self.Nparkingspots then self:E(self.lid..string.format("WARNING: Number of parking spots does not match! Nfree=%d, Noccu=%d, Nreserved=%d != %d total",Nfree,Noccu,Nresv,self.Nparkingspots)) end if self.verbose>=1 then local text=string.format("State %s - Runway Landing=%s, Takeoff=%s - Parking F=%d/O=%d/R=%d of %d - Flights=%s: Qpark=%d Qtxout=%d Qready=%d Qto=%d | Qinbound=%d Qhold=%d Qland=%d Qtxinb=%d Qarr=%d", self:GetState(),rwyLanding,rwyTakeoff,Nfree,Noccu,Nresv,self.Nparkingspots,Nflights,NQparking,NQtaxiout,NQreadyto,NQtakeoff,NQinbound,NQholding,NQlanding,NQtaxiinb,NQarrived) self:I(self.lid..text) end if Nflights==Nqueues then else self:E(string.format("WARNING: Number of total flights %d!=%d number of flights in all queues!",Nflights,Nqueues)) end if self.verbose>=2 then local text="Holding Patterns:" for i,_pattern in pairs(self.holdingpatterns)do local pattern=_pattern text=text..string.format("\n[%d] Pattern %s [Prio=%d, UID=%d]: Stacks=%d, Angels %d - %d",i,pattern.name,pattern.prio,pattern.uid,#pattern.stacks,pattern.angelsmin,pattern.angelsmax) if self.verbose>=4 then for _,_stack in pairs(pattern.stacks)do local stack=_stack local text=string.format("",stack.angels,stack) end end end self:I(self.lid..text) end self:__StatusUpdate(-30) end function FLIGHTCONTROL:onafterStop() self:UnHandleEvent(EVENTS.Birth) self:UnHandleEvent(EVENTS.EngineStartup) self:UnHandleEvent(EVENTS.Takeoff) self:UnHandleEvent(EVENTS.Land) self:UnHandleEvent(EVENTS.EngineShutdown) self:UnHandleEvent(EVENTS.Crash) self:UnHandleEvent(EVENTS.Kill) end function FLIGHTCONTROL:OnEventBirth(EventData) self:F3({EvendData=EventData}) if EventData and EventData.IniGroupName and EventData.IniUnit then self:T3(self.lid..string.format("BIRTH: unit = %s",tostring(EventData.IniUnitName))) self:T3(self.lid..string.format("BIRTH: group = %s",tostring(EventData.IniGroupName))) local unit=EventData.IniUnit if unit:IsAir()then local bornhere=EventData.Place and EventData.Place:GetName()==self.airbasename or false local playerunit,playername=self:_GetPlayerUnitAndName(EventData.IniUnitName) if playername or bornhere then self:ScheduleOnce(0.5,self._CreateFlightGroup,self,EventData.IniGroup) end if bornhere then self:SpawnParkingGuard(unit) end end end end function FLIGHTCONTROL:OnEventCrashOrDead(EventData) if EventData then if EventData.IniUnitName then if self.airbase and self.airbasename and self.airbasename==EventData.IniUnitName then self:RunwayDestroyed() end end end end function FLIGHTCONTROL:OnEventLand(EventData) self:F3({EvendData=EventData}) self:T2(self.lid..string.format("LAND: unit = %s",tostring(EventData.IniUnitName))) self:T3(self.lid..string.format("LAND: group = %s",tostring(EventData.IniGroupName))) end function FLIGHTCONTROL:OnEventTakeoff(EventData) self:F3({EvendData=EventData}) self:T2(self.lid..string.format("TAKEOFF: unit = %s",tostring(EventData.IniUnitName))) self:T3(self.lid..string.format("TAKEOFF: group = %s",tostring(EventData.IniGroupName))) local airbase=EventData.Place local unit=EventData.IniUnit if not(airbase or unit)then self:E(self.lid.."WARNING: Airbase or IniUnit is nil in takeoff event!") return end end function FLIGHTCONTROL:OnEventEngineStartup(EventData) self:F3({EvendData=EventData}) self:T2(self.lid..string.format("ENGINESTARTUP: unit = %s",tostring(EventData.IniUnitName))) self:T3(self.lid..string.format("ENGINESTARTUP: group = %s",tostring(EventData.IniGroupName))) end function FLIGHTCONTROL:OnEventEngineShutdown(EventData) self:F3({EvendData=EventData}) self:T2(self.lid..string.format("ENGINESHUTDOWN: unit = %s",tostring(EventData.IniUnitName))) self:T3(self.lid..string.format("ENGINESHUTDOWN: group = %s",tostring(EventData.IniGroupName))) end function FLIGHTCONTROL:OnEventKill(EventData) self:F3({EvendData=EventData}) self:T2(self.lid..string.format("KILL: ini unit = %s",tostring(EventData.IniUnitName))) self:T3(self.lid..string.format("KILL: ini group = %s",tostring(EventData.IniGroupName))) self:T2(self.lid..string.format("KILL: tgt unit = %s",tostring(EventData.TgtUnitName))) self:T3(self.lid..string.format("KILL: tgt group = %s",tostring(EventData.TgtGroupName))) local guardPrefix=string.format("Parking Guard %s",self.airbasename) local victimName=EventData.IniUnitName local killerName=EventData.TgtUnitName if victimName and victimName:find(guardPrefix)then env.info(string.format("Parking guard %s killed!",victimName)) for _,_flight in pairs(self.flights)do local flight=_flight local element=flight:GetElementByName(killerName) if element then env.info(string.format("Parking guard %s killed by %s!",victimName,killerName)) return end end end end function FLIGHTCONTROL:onafterRunwayDestroyed(From,Event,To) self:T(self.lid..string.format("Runway destoyed!")) self.runwaydestroyed=timer.getAbsTime() self:TransmissionTower("All flights, our runway was destroyed. All operations are suspended for one hour.",Flight,Delay) end function FLIGHTCONTROL:onafterRunwayRepaired(From,Event,To) self:T(self.lid..string.format("Runway repaired!")) self.runwaydestroyed=nil end function FLIGHTCONTROL:_CheckQueues() if self.verbose>=2 then self:_PrintQueue(self.flights,"All flights") end local flight,isholding,parking=self:_GetNextFlight() if flight then if isholding then if self:_CheckFlightLanding(flight)then local dTlanding=99999 if self.Tlanding then dTlanding=timer.getAbsTime()-self.Tlanding end if parking and dTlanding>=self.dTlanding then local callsign=self:_GetCallsignName(flight) local runway=self:GetActiveRunwayText() local text=string.format("%s, %s, you are cleared to land, runway %s",callsign,self.alias,runway) self:TransmissionTower(text,flight) if flight.isAI then local text=string.format("Runway %s, cleared to land, %s",runway,callsign) self:TransmissionPilot(text,flight,10) self:_LandAI(flight,parking) else self:SetFlightStatus(flight,FLIGHTCONTROL.FlightStatus.LANDING) end self.Tlanding=timer.getAbsTime() end else self:T3(self.lid..string.format("FYI: Landing clearance for flight %s denied",flight.groupname)) end else if self:_CheckFlightTakeoff(flight)then local callsign=self:_GetCallsignName(flight) local runway=self:GetActiveRunwayText(true) local text=string.format("%s, %s, taxi to runway %s, hold short",callsign,self.alias,runway) if self:GetFlightStatus(flight)==FLIGHTCONTROL.FlightStatus.READYTO then text=string.format("%s, %s, cleared for take-off, runway %s",callsign,self.alias,runway) end self:TransmissionTower(text,flight) if flight.isAI then local text="Wilco, " if flight:IsUncontrolled()then text=text..string.format("starting engines, ") flight:StartUncontrolled() end text=text..string.format("runway %s, %s",runway,callsign) self:TransmissionPilot(text,flight,10) for _,_element in pairs(flight.elements)do local element=_element if element and element.parking then local spot=self:GetParkingSpotByID(element.parking.TerminalID) self:RemoveParkingGuard(spot) end end self:SetFlightStatus(flight,FLIGHTCONTROL.FlightStatus.TAKEOFF) else if self:GetFlightStatus(flight)==FLIGHTCONTROL.FlightStatus.READYTO then self:SetFlightStatus(flight,FLIGHTCONTROL.FlightStatus.TAKEOFF) else for _,_element in pairs(flight.elements)do local element=_element if element.parking then local spot=self:GetParkingSpotByID(element.parking.TerminalID) if element.ai then self:RemoveParkingGuard(spot,15) else self:RemoveParkingGuard(spot,10) end end end end end else self:T3(self.lid..string.format("FYI: Take off for flight %s denied",flight.groupname)) end end else self:T2(self.lid..string.format("FYI: No flight in queue for takeoff or landing")) end end function FLIGHTCONTROL:_CheckFlightTakeoff(flight) local nlanding=self:CountFlights(FLIGHTCONTROL.FlightStatus.LANDING) local ntakeoff=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAKEOFF,nil,true) local status=self:GetFlightStatus(flight) if flight.isAI then if nlanding>self.NtaxiLanding then self:T(self.lid..string.format("AI flight %s [status=%s] NOT cleared for taxi/takeoff as %d>%d flight(s) landing",flight.groupname,status,nlanding,self.NtaxiLanding)) return false end local ninbound=0 if self.NtaxiInbound then ninbound=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAXIINB,nil,true) end if ntakeoff+ninbound>=self.NtaxiTot then self:T(self.lid..string.format("AI flight %s [status=%s] NOT cleared for taxi/takeoff as %d>=%d flight(s) taxi/takeoff",flight.groupname,status,ntakeoff,self.NtaxiTot)) return false end self:T(self.lid..string.format("AI flight %s [status=%s] cleared for taxi/takeoff! nLanding=%d, nTakeoff=%d",flight.groupname,status,nlanding,ntakeoff)) return true else if status==FLIGHTCONTROL.FlightStatus.READYTO then if nlanding>self.NtaxiLanding then self:T(self.lid..string.format("Player flight %s [status=%s] not cleared for taxi/takeoff as %d>%d flight(s) landing",flight.groupname,status,nlanding,self.NtaxiLanding)) return false end end self:T(self.lid..string.format("Player flight %s [status=%s] cleared for taxi/takeoff",flight.groupname,status)) return true end end function FLIGHTCONTROL:_CheckFlightLanding(flight) local nlanding=self:CountFlights(FLIGHTCONTROL.FlightStatus.LANDING) local ntakeoff=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAKEOFF,nil,true) local status=self:GetFlightStatus(flight) if flight.isAi then if ntakeoff<=self.NlandingTakeoff and nlandingTholdingMin then return fg end end local function _sortByFuel(a,b) local flightA=a local flightB=b local fuelA=flightA.group:GetFuelMin() local fuelB=flightB.group:GetFuelMin() return fuelATholdingMin then return fg end return nil end function FLIGHTCONTROL:_GetNextFightParking() local OnlyAI=nil if self:IsRunwayDestroyed()then OnlyAI=false end local QreadyTO=self:GetFlights(FLIGHTCONTROL.FlightStatus.READYTO,OPSGROUP.GroupStatus.TAXIING,OnlyAI) if#QreadyTO>0 then return QreadyTO[1] end local QreadyTX=self:GetFlights(FLIGHTCONTROL.FlightStatus.READYTX,OPSGROUP.GroupStatus.PARKING,OnlyAI) if#QreadyTX>0 then return QreadyTX[1] end if self:IsRunwayDestroyed()then return nil end local Qparking=self:GetFlights(FLIGHTCONTROL.FlightStatus.PARKING,nil,true) local Nparking=#Qparking if Nparking==0 then return nil end local function _sortByTparking(a,b) local flightA=a local flightB=b return flightA.Tparking=2 then local text="Parking flights:" for i,_flight in pairs(Qparking)do local flight=_flight text=text..string.format("\n[%d] %s [%s], state=%s [%s]: Tparking=%.1f sec",i,flight.groupname,tostring(flight.actype),flight:GetState(),self:GetFlightStatus(flight),flight:GetParkingTime()) end self:I(self.lid..text) end for i,_flight in pairs(Qparking)do local flight=_flight if flight.isAI and flight.isReadyTO then return flight end end return nil end function FLIGHTCONTROL:_PrintQueue(queue,name) local text=string.format("%s Queue N=%d:",name,#queue) if#queue==0 then text=text.." empty." else local time=timer.getAbsTime() for i,_flight in ipairs(queue)do local flight=_flight local fuel=flight.group:GetFuelMin()*100 local ai=tostring(flight.isAI) local actype=tostring(flight.actype) local holding=flight.Tholding and UTILS.SecondsToClock(time-flight.Tholding,true)or"X" local parking=flight.Tparking and UTILS.SecondsToClock(time-flight.Tparking,true)or"X" local holding=flight:GetHoldingTime() if holding>=0 then holding=UTILS.SecondsToClock(holding,true) else holding="X" end local parking=flight:GetParkingTime() if parking>=0 then parking=UTILS.SecondsToClock(parking,true) else parking="X" end local nunits=flight:CountElements() local state=flight:GetState() local status=self:GetFlightStatus(flight) text=text..string.format("\n[%d] %s (%s*%d): status=%s | %s, ai=%s, fuel=%d, holding=%s, parking=%s", i,flight.groupname,actype,nunits,state,status,ai,fuel,holding,parking) for j,_element in pairs(flight.elements)do local element=_element local life=element.unit:GetLife() local life0=element.unit:GetLife0() local park=element.parking and tostring(element.parking.TerminalID)or"N/A" text=text..string.format("\n (%d) %s (%s): status=%s, ai=%s, airborne=%s life=%d/%d spot=%s", j,tostring(element.modex),element.name,tostring(element.status),tostring(element.ai),tostring(element.unit:InAir()),life,life0,park) end end end self:I(self.lid..text) return text end function FLIGHTCONTROL:SetFlightStatus(flight,status) self:T(self.lid..string.format("New status %s-->%s for flight %s",flight.controlstatus or"unknown",status,flight:GetName())) if flight.controlstatus~=status and not flight.isAI then self:T(self.lid.."Updating menu in 0.2 sec after flight status change") flight:_UpdateMenu(0.2) end flight.controlstatus=status end function FLIGHTCONTROL:GetFlightStatus(flight) if flight then return flight.controlstatus or"unkonwn" end return"unknown" end function FLIGHTCONTROL:IsControlling(flight) local is=flight.flightcontrol and flight.flightcontrol.airbasename==self.airbasename or false return is end function FLIGHTCONTROL:_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 FLIGHTCONTROL:GetFlights(Status,GroupStatus,AI) if Status~=nil or GroupStatus~=nil or AI~=nil then local flights={} for _,_flight in pairs(self.flights)do local flight=_flight local status=self:GetFlightStatus(flight,Status) if status==Status then if AI==nil or AI==flight.isAI then if GroupStatus==nil or GroupStatus==flight:GetState()then table.insert(flights,flight) end end end end return flights else return self.flights end end function FLIGHTCONTROL:CountFlights(Status,GroupStatus,AI) if Status~=nil or GroupStatus~=nil or AI~=nil then local flights=self:GetFlights(Status,GroupStatus,AI) return#flights else return#self.flights end end function FLIGHTCONTROL:GetActiveRunway() local rwy=self.airbase:GetActiveRunway() return rwy end function FLIGHTCONTROL:GetActiveRunwayLanding() local rwy=self.airbase:GetActiveRunwayLanding() return rwy end function FLIGHTCONTROL:GetActiveRunwayTakeoff() local rwy=self.airbase:GetActiveRunwayTakeoff() return rwy end function FLIGHTCONTROL:GetActiveRunwayText(Takeoff) local runway if Takeoff then runway=self:GetActiveRunwayTakeoff() else runway=self:GetActiveRunwayLanding() end local name=self.airbase:GetRunwayName(runway,true) return name or"XX" end function FLIGHTCONTROL:_InitParkingSpots() local parkingdata=self.airbase:GetParkingSpotsTable() self.parking={} self.Nparkingspots=0 for _,_spot in pairs(parkingdata)do local spot=_spot local text=string.format("Parking ID=%d, Terminal=%d: Free=%s, Client=%s, Dist=%.1f",spot.TerminalID,spot.TerminalType,tostring(spot.Free),tostring(spot.ClientName),spot.DistToRwy) self:T3(self.lid..text) self.parking[spot.TerminalID]=spot if spot.Free then self:SetParkingFree(spot) else local unit=spot.Coordinate:FindClosestUnit(20) if unit then local unitname=unit and unit:GetName()or"unknown" local isalive=unit:IsAlive() self:T2(self.lid..string.format("FF parking spot %d is occupied by unit %s alive=%s",spot.TerminalID,unitname,tostring(isalive))) if isalive then self:SetParkingOccupied(spot,unitname) self:SpawnParkingGuard(unit) else self:SetParkingFree(spot) end else self:E(self.lid..string.format("ERROR: Parking spot is NOT FREE but no unit could be found there!")) end end self.Nparkingspots=self.Nparkingspots+1 end end function FLIGHTCONTROL:GetParkingSpotByID(TerminalID) return self.parking[TerminalID] end function FLIGHTCONTROL:_UpdateSpotStatus(spot,status,unitname) self:T2(self.lid..string.format("Updating parking spot %d status: %s --> %s (unit=%s)",spot.TerminalID,tostring(spot.Status),status,tostring(unitname))) spot.Status=status end function FLIGHTCONTROL:SetParkingFree(spot) local spot=self:GetParkingSpotByID(spot.TerminalID) self:_UpdateSpotStatus(spot,AIRBASE.SpotStatus.FREE,spot.OccupiedBy or spot.ReservedBy) spot.OccupiedBy=nil spot.ReservedBy=nil self:RemoveParkingGuard(spot) self:UpdateParkingMarker(spot) end function FLIGHTCONTROL:SetParkingReserved(spot,unitname) local spot=self:GetParkingSpotByID(spot.TerminalID) self:_UpdateSpotStatus(spot,AIRBASE.SpotStatus.RESERVED,unitname) spot.ReservedBy=unitname or"unknown" self:UpdateParkingMarker(spot) end function FLIGHTCONTROL:SetParkingOccupied(spot,unitname) local spot=self:GetParkingSpotByID(spot.TerminalID) self:_UpdateSpotStatus(spot,AIRBASE.SpotStatus.OCCUPIED,unitname) spot.OccupiedBy=unitname or"unknown" self:UpdateParkingMarker(spot) end function FLIGHTCONTROL:UpdateParkingMarker(spot) if self.markerParking then local spot=self:GetParkingSpotByID(spot.TerminalID) if spot.Status==AIRBASE.SpotStatus.FREE then if spot.Marker then spot.Marker:Remove() end else local text=string.format("Spot %d (type %d): %s",spot.TerminalID,spot.TerminalType,spot.Status:upper()) if spot.OccupiedBy then text=text..string.format("\nOccupied by %s",tostring(spot.OccupiedBy)) end if spot.ReservedBy then text=text..string.format("\nReserved for %s",tostring(spot.ReservedBy)) end if spot.ClientSpot then text=text..string.format("\nClient %s",tostring(spot.ClientName)) end if spot.Marker then if text~=spot.Marker.text or not spot.Marker.shown then spot.Marker:UpdateText(text) end else spot.Marker=MARKER:New(spot.Coordinate,text):ToAll() end end end end function FLIGHTCONTROL:IsParkingFree(spot) return spot.Status==AIRBASE.SpotStatus.FREE end function FLIGHTCONTROL:IsParkingOccupied(spot) if spot.Status==AIRBASE.SpotStatus.OCCUPIED then return tostring(spot.OccupiedBy) else return false end end function FLIGHTCONTROL:IsParkingReserved(spot) if spot.Status==AIRBASE.SpotStatus.RESERVED then return tostring(spot.ReservedBy) else return false end end function FLIGHTCONTROL:_GetFreeParkingSpots(terminal) local freespots={} local n=0 for _,_parking in pairs(self.parking)do local parking=_parking if self:IsParkingFree(parking)then if terminal==nil or terminal==parking.terminal then n=n+1 table.insert(freespots,parking) end end end return n,freespots end function FLIGHTCONTROL:GetClosestParkingSpot(Coordinate,TerminalType,Status) local distmin=math.huge local spotmin=nil for TerminalID,Spot in pairs(self.parking)do local spot=Spot if(Status==nil or Status==spot.Status)and AIRBASE._CheckTerminalType(spot.TerminalType,TerminalType)then local dist=Coordinate:Get2DDistance(spot.Coordinate) if dist0 then text=text..string.format("\n- Parking %d",NQparking) end if NQreadytx>0 then text=text..string.format("\n- Ready to taxi %d",NQreadytx) end if NQtaxiout>0 then text=text..string.format("\n- Taxi to runway %d",NQtaxiout) end if NQreadyto>0 then text=text..string.format("\n- Ready for takeoff %d",NQreadyto) end if NQtakeoff>0 then text=text..string.format("\n- Taking off %d",NQtakeoff) end if NQinbound>0 then text=text..string.format("\n- Inbound %d",NQinbound) end if NQholding>0 then text=text..string.format("\n- Holding pattern %d",NQholding) end if NQlanding>0 then text=text..string.format("\n- Landing %d",NQlanding) end if NQtaxiinb>0 then text=text..string.format("\n- Taxi to parking %d",NQtaxiinb) end if NQarrived>0 then text=text..string.format("\n- Arrived at parking %d",NQarrived) end self:TextMessageToFlight(text,flight,15,true) else self:E(self.lid..string.format("Cannot find flight group %s.",tostring(groupname))) end end function FLIGHTCONTROL:_PlayerRequestInbound(groupname) local flight=_DATABASE:GetOpsGroup(groupname) if flight then if flight:IsAirborne()then local callsign=self:_GetCallsignName(flight) local player=flight:GetPlayerElement() local text=string.format("%s, %s, inbound for landing",self.alias,callsign) self:TransmissionPilot(text,flight) local flightcoord=flight:GetCoordinate(nil,player.name) local dist=flightcoord:Get2DDistance(self:GetCoordinate()) if distself.NlandingTakeoff then local text=string.format("%s, negative! We have currently traffic taking off!",callsign) self:TransmissionTower(text,flight,10) else local runway=self:GetActiveRunwayText() local text=string.format("%s, affirmative, runway %s. Confirm approach!",callsign,runway) self:TransmissionTower(text,flight,10) self:SetFlightStatus(flight,FLIGHTCONTROL.FlightStatus.LANDING) end else local text=string.format("Negative, you must be INBOUND and CONTROLLED by us!") self:TextMessageToFlight(text,flight,10) end else self:E(self.lid..string.format("Cannot find flight group %s.",tostring(groupname))) end end function FLIGHTCONTROL:_PlayerRequestTaxi(groupname) local flight=_DATABASE:GetOpsGroup(groupname) if flight then local callsign=self:_GetCallsignName(flight) local text=string.format("%s, %s, request taxi to runway.",self.alias,callsign) self:TransmissionPilot(text,flight) if flight:IsParking()then local text=string.format("%s, %s, hold position until further notice.",callsign,self.alias) self:TransmissionTower(text,flight,10) self:SetFlightStatus(flight,FLIGHTCONTROL.FlightStatus.READYTX) elseif flight:IsTaxiing()then local runway=self:GetActiveRunwayText(true) local text=string.format("%s, %s, taxi to runway %s, hold short.",callsign,self.alias,runway) self:TransmissionTower(text,flight,10) self:SetFlightStatus(flight,FLIGHTCONTROL.FlightStatus.TAXIOUT) local playerElement=flight:GetPlayerElement() if playerElement and playerElement.parking then self:SetParkingFree(playerElement.parking) end else self:TextMessageToFlight(string.format("Negative, you must be PARKING to request TAXI!"),flight) end else self:E(self.lid..string.format("Cannot find flight group %s.",tostring(groupname))) end end function FLIGHTCONTROL:_PlayerAbortTaxi(groupname) local flight=_DATABASE:GetOpsGroup(groupname) if flight then local callsign=self:_GetCallsignName(flight) local text=string.format("%s, %s, cancel my taxi request.",self.alias,callsign) self:TransmissionPilot(text,flight) if flight:IsParking()then local text=string.format("%s, %s, roger, remain on your parking position.",callsign,self.alias) self:TransmissionTower(text,flight,10) self:SetFlightStatus(flight,FLIGHTCONTROL.FlightStatus.PARKING) local playerElement=flight:GetPlayerElement() if playerElement then self:SpawnParkingGuard(playerElement.unit) end elseif flight:IsTaxiing()then local text=string.format("%s, %s, roger, return to your parking position.",callsign,self.alias) self:TransmissionTower(text,flight,10) self:SetFlightStatus(flight,FLIGHTCONTROL.FlightStatus.TAXIINB) else self:TextMessageToFlight(string.format("Negative, you must be PARKING or TAXIING to abort TAXI!"),flight) end else self:E(self.lid..string.format("Cannot find flight group %s.",tostring(groupname))) end end function FLIGHTCONTROL:_PlayerRequestTakeoff(groupname) local flight=_DATABASE:GetOpsGroup(groupname) if flight then if flight:IsTaxiing()then local callsign=self:_GetCallsignName(flight) local text=string.format("%s, %s, ready for departure. Request takeoff.",self.alias,callsign) self:TransmissionPilot(text,flight) local Nlanding=self:CountFlights(FLIGHTCONTROL.FlightStatus.LANDING) local Ntakeoff=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAKEOFF) local text=string.format("%s, %s, ",callsign,self.alias) if Nlanding==0 then text=text.."no current traffic. You are cleared for takeoff." self:SetFlightStatus(flight,FLIGHTCONTROL.FlightStatus.TAKEOFF) elseif Nlanding>0 then if Nlanding==1 then text=text..string.format("negative, we got %d flight inbound before it's your turn. Hold position until futher notice.",Nlanding) else text=text..string.format("negative, we got %d flights inbound. Hold positon until futher notice.",Nlanding) end end self:TransmissionTower(text,flight,10) else self:TextMessageToFlight(string.format("Negative, you must request TAXI before you can request TAKEOFF!"),flight) end else self:E(self.lid..string.format("Cannot find flight group %s.",tostring(groupname))) end end function FLIGHTCONTROL:_PlayerAbortTakeoff(groupname) local flight=_DATABASE:GetOpsGroup(groupname) if flight then local status=self:GetFlightStatus(flight) if status==FLIGHTCONTROL.FlightStatus.TAKEOFF or status==FLIGHTCONTROL.FlightStatus.READYTO then local callsign=self:_GetCallsignName(flight) local text=string.format("%s, %s, abort takeoff.",self.alias,callsign) self:TransmissionPilot(text,flight) if flight:IsParking()then text=string.format("%s, %s, affirm, remain on your parking position.",callsign,self.alias) self:SetFlightStatus(flight,FLIGHTCONTROL.FlightStatus.PARKING) local playerElement=flight:GetPlayerElement() if playerElement then self:SpawnParkingGuard(playerElement.unit) end elseif flight:IsTaxiing()then text=string.format("%s, %s, roger, report whether you want to taxi back or takeoff later.",callsign,self.alias) self:SetFlightStatus(flight,FLIGHTCONTROL.FlightStatus.TAXIOUT) else env.info(self.lid.."ERROR") end self:TransmissionTower(text,flight,10) else self:TextMessageToFlight("Negative, You are NOT in the takeoff queue",flight) end else self:E(self.lid..string.format("Cannot find flight group %s.",tostring(groupname))) end end function FLIGHTCONTROL:_PlayerRequestParking(groupname) local flight=_DATABASE:GetOpsGroup(groupname) if flight then local callsign=self:_GetCallsignName(flight) local player=flight:GetPlayerElement() local TerminalType=AIRBASE.TerminalType.FighterAircraft if flight.isHelo then TerminalType=AIRBASE.TerminalType.HelicopterUsable end local coord=flight:GetCoordinate(nil,player.name) local spot=self:_GetPlayerSpot(player.name) if not spot then spot=self:GetClosestParkingSpot(coord,TerminalType,AIRBASE.SpotStatus.FREE) end if spot then local text=string.format("%s, your assigned parking position is terminal ID %d.",callsign,spot.TerminalID) self:TransmissionTower(text,flight) if player.parking then self:SetParkingFree(player.parking) end player.parking=spot self:SetParkingReserved(spot,player.name) flight:_UpdateMenu(0.2) else local text=string.format("%s, no free parking spot available. Try again later.",callsign) self:TransmissionTower(text,flight) end else self:E(self.lid..string.format("Cannot find flight group %s.",tostring(groupname))) end end function FLIGHTCONTROL:_PlayerCancelParking(groupname) local flight=_DATABASE:GetOpsGroup(groupname) if flight then local callsign=self:_GetCallsignName(flight) local player=flight:GetPlayerElement() if player.parking then self:SetParkingFree(player.parking) player.parking=nil self:TextMessageToFlight(string.format("%s, your parking spot reservation at terminal ID %d was cancelled.",callsign,player.parking.TerminalID),flight) else self:TextMessageToFlight("You did not have a valid parking spot reservation.",flight) end flight:_UpdateMenu(0.2) else self:E(self.lid..string.format("Cannot find flight group %s.",tostring(groupname))) end end function FLIGHTCONTROL:_PlayerArrived(groupname) local flight=_DATABASE:GetOpsGroup(groupname) if flight then local player=flight:GetPlayerElement() local coord=flight:GetCoordinate(nil,player.name) local spot=self:_GetPlayerSpot(player.name) if player.parking then spot=self:GetParkingSpotByID(player.parking.TerminalID) else if not spot then spot=self:GetClosestParkingSpot(coord) end end if spot then local callsign=self:_GetCallsignName(flight) local dist=coord:Get2DDistance(spot.Coordinate) if dist<12 then local text=string.format("%s, %s, arrived at parking position. Terminal ID %d.",self.alias,callsign,spot.TerminalID) self:TransmissionPilot(text,flight) local text="" if spot.ReservedBy and spot.ReservedBy~=player.name then text=string.format("%s, this spot is already reserved for %s. Find yourself a different parking position.",callsign,self.alias,spot.ReservedBy) else text=string.format("%s, %s, roger. Enjoy a cool bevarage in the officers' club.",callsign,self.alias) flight:ElementParking(player,spot) self:SetFlightStatus(flight,FLIGHTCONTROL.FlightStatus.PARKING) if player then self:SpawnParkingGuard(player.unit) end end self:TransmissionTower(text,flight,10) else local text=string.format("%s, %s, arrived at parking position.",self.alias,callsign) self:TransmissionPilot(text,flight) local text="" if spot.ReservedBy then if spot.ReservedBy==player.name then text=string.format("%s, %s, you are still %d meters away from your reserved parking position at terminal ID %d. Continue taxiing!",callsign,self.alias,dist,spot.TerminalID) else text=string.format("%s, %s, the closest parking spot is already reserved. Continue taxiing to a free spot!",callsign,self.alias) end else text=string.format("%s, %s, you are still %d meters away from the closest parking position. Continue taxiing to a proper spot!",callsign,self.alias,dist) end self:TransmissionTower(text,flight,10) end else end else self:E(self.lid..string.format("Cannot find flight group %s.",tostring(groupname))) end end function FLIGHTCONTROL:_CreateFlightGroup(group) if self:_InQueue(self.flights,group)then self:E(self.lid..string.format("WARNING: Flight group %s does already exist!",group:GetName())) return end self:T(self.lid..string.format("Creating new flight for group %s of aircraft type %s.",group:GetName(),group:GetTypeName())) local flight=_DATABASE:GetOpsGroup(group:GetName()) if not flight then flight=FLIGHTGROUP:New(group:GetName()) end if flight.homebase and flight.homebase:GetName()==self.airbasename then flight:SetFlightControl(self) end return flight end function FLIGHTCONTROL:_RemoveFlight(Flight) for i,_flight in pairs(self.flights)do local flight=_flight if flight.groupname==Flight.groupname then self:T(self.lid..string.format("Removing flight group %s",flight.groupname)) table.remove(self.flights,i) Flight.flightcontrol=nil self:SetFlightStatus(Flight,FLIGHTCONTROL.FlightStatus.UNKNOWN) return true end end self:E(self.lid..string.format("WARNING: Could NOT remove flight group %s",Flight.groupname)) end function FLIGHTCONTROL:_GetFlightFromGroup(group) if group then local name=group:GetName() for i,_flight in pairs(self.flights)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 FLIGHTCONTROL:_GetFlightElement(unitname) local unit=UNIT:FindByName(unitname) if unit then local flight=self:_GetFlightFromGroup(unit:GetGroup()) 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 FLIGHTCONTROL:_CheckFlights() for i=#self.flights,1,-1 do local flight=self.flights[i] if flight:IsDead()then self:T(self.lid..string.format("Removing DEAD flight %s",tostring(flight.groupname))) self:_RemoveFlight(flight) end end self.Nplayers=0 for _,_flight in pairs(self.flights)do local flight=_flight if not flight.isAI then self.Nplayers=self.Nplayers+1 end end if self.speedLimitTaxi then for _,_flight in pairs(self.flights)do local flight=_flight if not flight.isAI then local playerElement=flight:GetPlayerElement() local flightstatus=self:GetFlightStatus(flight) if playerElement then if(flightstatus==FLIGHTCONTROL.FlightStatus.TAXIINB or flightstatus==FLIGHTCONTROL.FlightStatus.TAXIOUT)and self.speedLimitTaxi then local speed=playerElement.unit:GetVelocityMPS() local coord=playerElement.unit:GetCoord() local onRunway=self:IsCoordinateRunway(coord) self:T(self.lid..string.format("Player %s speed %.1f knots (max=%.1f) onRunway=%s",playerElement.playerName,UTILS.MpsToKnots(speed),UTILS.MpsToKnots(self.speedLimitTaxi),tostring(onRunway))) if speed and speed>self.speedLimitTaxi and not onRunway then local callsign=self:_GetCallsignName(flight) local text=string.format("%s, slow down, you are taxiing too fast!",callsign) self:TransmissionTower(text,flight) local PlayerData=flight:_GetPlayerData() self:PlayerSpeeding(PlayerData) end end end end end end end function FLIGHTCONTROL:_CheckParking() for TerminalID,_spot in pairs(self.parking)do local spot=_spot if spot.Reserved then if spot.MarkerID then spot.Coordinate:RemoveMark(spot.MarkerID) end spot.MarkerID=spot.Coordinate:MarkToCoalition(string.format("Parking reserved for %s",tostring(spot.Reserved)),self:GetCoalition()) end for i=1,#self.flights do local flight=self.flights[i] for _,_element in pairs(flight.elements)do local element=_element if element.parking and element.parking.TerminalID==TerminalID then if spot.MarkerID then spot.Coordinate:RemoveMark(spot.MarkerID) end spot.MarkerID=spot.Coordinate:MarkToCoalition(string.format("Parking spot occupied by %s",tostring(element.name)),self:GetCoalition()) end end end end end function FLIGHTCONTROL:_LandAI(flight,parking) self:T(self.lid..string.format("Landing AI flight %s.",flight.groupname)) local respawn=false if respawn then local Template=flight.group:GetTemplate() Template.route.points=wp for i,unit in pairs(Template.units)do local spot=parking[i] local element=flight:GetElementByName(unit.name) if element then unit.parking_landing=spot.TerminalID local text=string.format("Reserving parking spot %d for unit %s",spot.TerminalID,tostring(unit.name)) self:T(self.lid..text) self:SetParkingReserved(spot,element.name) else env.info("FF error could not get element to assign parking!") end end self:TextMessageToFlight(string.format("Respawning group %s",flight.groupname),flight) flight:Respawn(Template) else flight:ClearToLand() end end function FLIGHTCONTROL:_GetHoldingStack(flight) self:T(self.lid..string.format("Getting holding point for flight %s",flight:GetName())) for i,_hp in pairs(self.holdingpatterns)do local holdingpattern=_hp self:T(self.lid..string.format("Checking holding point %s",holdingpattern.name)) for j,_stack in pairs(holdingpattern.stacks)do local stack=_stack local name=stack.flightgroup and stack.flightgroup:GetName()or"empty" self:T(self.lid..string.format("Stack %d: %s",j,name)) if not stack.flightgroup then return stack end end end return nil end function FLIGHTCONTROL:_CountFlightsInPattern(Pattern) local N=0 for _,_stack in pairs(Pattern.stacks)do local stack=_stack if stack.flightgroup then N=N+1 end end return N end function FLIGHTCONTROL:_FlightOnFinal(flight) local callsign=self:_GetCallsignName(flight) local text=string.format("%s, final",callsign) self:TransmissionPilot(text,flight) return self end function FLIGHTCONTROL:TransmissionTower(Text,Flight,Delay) if self.radioOnlyIfPlayers==true and self.Nplayers==0 then self:T(self.lid.."No players ==> skipping TOWER radio transmission") return end local text=self:_GetTextForSpeech(Text) local subgroups=nil if Flight and not Flight.isAI then local playerData=Flight:_GetPlayerData() if playerData.subtitles and(not self.nosubs)then subgroups=subgroups or{} table.insert(subgroups,Flight.group) end end local transmission=self.msrsqueue:NewTransmission(text,nil,self.msrsTower,nil,1,subgroups,Text) self.Tlastmessage=timer.getAbsTime()+(Delay or 0) self:T(self.lid..string.format("Radio Tower: %s",Text)) end function FLIGHTCONTROL:TransmissionPilot(Text,Flight,Delay) if self.radioOnlyIfPlayers==true and self.Nplayers==0 then self:T(self.lid.."No players ==> skipping PILOT radio transmission") return end local playerData=Flight:_GetPlayerData() if playerData==nil or playerData.myvoice then local text=self:_GetTextForSpeech(Text) local msrs=self.msrsPilot if Flight.useSRS and Flight.msrs then msrs=Flight.msrs end local subgroups=nil if Flight and not Flight.isAI then local playerData=Flight:_GetPlayerData() if playerData.subtitles and(not self.nosubs)then subgroups=subgroups or{} table.insert(subgroups,Flight.group) end end local coordinate=Flight:GetCoordinate(true) msrs:SetCoordinate() self.msrsqueue:NewTransmission(text,nil,msrs,nil,1,subgroups,Text,nil,self.frequency,self.modulation) end self.Tlastmessage=timer.getAbsTime()+(Delay or 0) self:T(self.lid..string.format("Radio Pilot: %s",Text)) end function FLIGHTCONTROL:TextMessageToFlight(Text,Flight,Duration,Clear,Delay) if Delay and Delay>0 then self:ScheduleOnce(Delay,FLIGHTCONTROL.TextMessageToFlight,self,Text,Flight,Duration,Clear,0) else if Flight and Flight.group and Flight.group:IsAlive()then local gid=Flight.group:GetID() trigger.action.outTextForGroup(gid,self:_CleanText(Text),Duration or 5,Clear) end end end function FLIGHTCONTROL:_CleanText(Text) local text=Text:gsub("\n$",""):gsub("\n$","") return text end function FLIGHTCONTROL:_SpawnParkingGuard(unit) local coordinate=unit:GetCoordinate() local spot=self:GetClosestParkingSpot(coordinate) if not spot.ParkingGuard then local heading=unit:GetHeading() local size,x,y,z=unit:GetObjectSize() local xdiff=3 if AIRBASE._CheckTerminalType(spot.TerminalType,AIRBASE.TerminalType.Shelter)then xdiff=27-(x*0.5) end if(AIRBASE._CheckTerminalType(spot.TerminalType,AIRBASE.TerminalType.OpenMed)or AIRBASE._CheckTerminalType(spot.TerminalType,AIRBASE.TerminalType.Shelter))and self.airbasename==AIRBASE.Sinai.Ramon_Airbase then xdiff=12 end self:T2(self.lid..string.format("Parking guard for %s: heading=%d, length x=%.1f m, xdiff=%.1f m",unit:GetName(),heading,x,xdiff)) local Coordinate=coordinate:Translate(x*0.5+xdiff,heading) local lookat=heading-180 self.parkingGuard:InitHeading(lookat) if self.parkingGuard:IsInstanceOf("SPAWN")then end spot.ParkingGuard=self.parkingGuard:SpawnFromCoordinate(Coordinate) else self:E(self.lid.."ERROR: Parking Guard already exists!") end end function FLIGHTCONTROL:SpawnParkingGuard(unit) if unit and self.parkingGuard then self:ScheduleOnce(1,FLIGHTCONTROL._SpawnParkingGuard,self,unit) end end function FLIGHTCONTROL:RemoveParkingGuard(spot,delay) if delay and delay>0 then self:ScheduleOnce(delay,FLIGHTCONTROL.RemoveParkingGuard,self,spot) else if spot.ParkingGuard then spot.ParkingGuard:Destroy() spot.ParkingGuard=nil end end end function FLIGHTCONTROL:_IsFlightOnRunway(flight) for _,_runway in pairs(self.airbase.runways)do local runway=_runway local inzone=flight:IsInZone(runway.zone) if inzone then return runway end end return nil end function FLIGHTCONTROL: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 FLIGHTCONTROL:_GetCallsignName(flight) local callsign=flight:GetCallsignName(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations) return callsign end function FLIGHTCONTROL:_GetTextForSpeech(text) local function space(text) local res="" for i=1,#text do local char=text:sub(i,i) res=res..char.." " end return res end local t=text:gsub("(%d+)",space) return t end function FLIGHTCONTROL:_GetPlayerUnitAndName(unitName) if unitName then local DCSunit=Unit.getByName(unitName) if DCSunit then local playername=DCSunit:getPlayerName() local unit=UNIT:Find(DCSunit) if DCSunit and unit and playername then 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 FLIGHTCONTROL:_CheckMarkHoldingPatterns() for _,pattern in pairs(self.holdingpatterns)do local Pattern=pattern if self.markPatterns then self:_MarkHoldingPattern(Pattern) else self:_UnMarkHoldingPattern(Pattern) end end end function FLIGHTCONTROL:_MarkHoldingPattern(Pattern) if not Pattern.markArrow then Pattern.markArrow=Pattern.pos0:ArrowToAll(Pattern.pos1,nil,{1,0,0},1,{1,1,0},0.5,2,true) end if not Pattern.markArrival then Pattern.markArrival=Pattern.arrivalzone:DrawZone() end end function FLIGHTCONTROL:_UnMarkHoldingPattern(Pattern) if Pattern.markArrow then UTILS.RemoveMark(Pattern.markArrow) Pattern.markArrow=nil end if Pattern.markArrival then UTILS.RemoveMark(Pattern.markArrival) Pattern.markArrival=nil end end function FLIGHTCONTROL:_AddHoldingPatternBackup() local runway=self:GetActiveRunway() local heading=runway.heading local vec2=self.airbase:GetVec2() local Vec2=UTILS.Vec2Translate(vec2,UTILS.NMToMeters(5),heading+90) local ArrivalZone=ZONE_RADIUS:New("Arrival Zone",Vec2,5000) self.holdingBackup=self:AddHoldingPattern(ArrivalZone,heading,15,5,25,999) return self end FLIGHTGROUP={ ClassName="FLIGHTGROUP", homebase=nil, destbase=nil, homezone=nil, destzone=nil, actype=nil, speedMax=nil, rangemax=nil, ceiling=nil, fuellow=false, fuellowthresh=nil, fuellowrtb=nil, fuelcritical=nil, fuelcriticalthresh=nil, fuelcriticalrtb=false, outofAAMrtb=false, outofAGMrtb=false, flightcontrol=nil, flaghold=nil, Tholding=nil, Tparking=nil, Twaiting=nil, menu=nil, isHelo=nil, RTBRecallCount=0, playerSettings={}, playerWarnings={}, prohibitAB=false, jettisonEmptyTanks=true, jettisonWeapons=true, } FLIGHTGROUP.Attribute={ TRANSPORTPLANE="TransportPlane", AWACS="AWACS", FIGHTER="Fighter", BOMBER="Bomber", TANKER="Tanker", TRANSPORTHELO="TransportHelo", ATTACKHELO="AttackHelo", UAV="UAV", OTHER="Other", } FLIGHTGROUP.RadioMessage={ AIRBORNE={normal="Airborn",enhanced="Airborn"}, TAXIING={normal="Taxiing",enhanced="Taxiing"}, } FLIGHTGROUP.PlayerSkill={ STUDENT="Student", AVIATOR="Aviator", GRADUATE="Graduate", INSTRUCTOR="Instructor", } FLIGHTGROUP.Players={} FLIGHTGROUP.version="1.0.4" function FLIGHTGROUP:New(group) local og=_DATABASE:GetOpsGroup(group) if og then og:I(og.lid..string.format("WARNING: OPS group already exists in data base!")) return og end local self=BASE:Inherit(self,OPSGROUP:New(group)) self.lid=string.format("FLIGHTGROUP %s | ",self.groupname or"N/A") self:SetDefaultROE() self:SetDefaultROT() self:SetDefaultEPLRS(self.isEPLRS) self:SetDetection() self:SetFuelLowThreshold() self:SetFuelLowRTB() self:SetFuelCriticalThreshold() self:SetFuelCriticalRTB() self.flaghold=USERFLAG:New(string.format("%s_FlagHold",self.groupname)) self.flaghold:Set(0) self.holdtime=2*60 self:AddTransition("*","LandAtAirbase","Inbound") self:AddTransition("*","RTB","Inbound") self:AddTransition("*","RTZ","Inbound") self:AddTransition("Inbound","Holding","Holding") self:AddTransition("*","Refuel","Going4Fuel") self:AddTransition("Going4Fuel","Refueled","Cruising") self:AddTransition("*","LandAt","LandingAt") self:AddTransition("LandingAt","LandedAt","LandedAt") self:AddTransition("*","FuelLow","*") self:AddTransition("*","FuelCritical","*") self:AddTransition("Cruising","EngageTarget","Engaging") self:AddTransition("Engaging","Disengage","Cruising") self:AddTransition("*","ElementParking","*") self:AddTransition("*","ElementEngineOn","*") self:AddTransition("*","ElementTaxiing","*") self:AddTransition("*","ElementTakeoff","*") self:AddTransition("*","ElementAirborne","*") self:AddTransition("*","ElementLanded","*") self:AddTransition("*","ElementArrived","*") self:AddTransition("*","ElementOutOfAmmo","*") self:AddTransition("*","Parking","Parking") self:AddTransition("*","Taxiing","Taxiing") self:AddTransition("*","Takeoff","Airborne") self:AddTransition("*","Airborne","Airborne") self:AddTransition("*","Cruise","Cruising") self:AddTransition("*","Landing","Landing") self:AddTransition("*","Landed","Landed") self:AddTransition("*","Arrived","Arrived") self:HandleEvent(EVENTS.Birth,self.OnEventBirth) self:HandleEvent(EVENTS.EngineStartup,self.OnEventEngineStartup) self:HandleEvent(EVENTS.Takeoff,self.OnEventTakeOff) self:HandleEvent(EVENTS.Land,self.OnEventLanding) self:HandleEvent(EVENTS.EngineShutdown,self.OnEventEngineShutdown) self:HandleEvent(EVENTS.PilotDead,self.OnEventPilotDead) self:HandleEvent(EVENTS.Ejection,self.OnEventEjection) self:HandleEvent(EVENTS.Crash,self.OnEventCrash) self:HandleEvent(EVENTS.RemoveUnit,self.OnEventRemoveUnit) self:HandleEvent(EVENTS.UnitLost,self.OnEventUnitLost) self:HandleEvent(EVENTS.Kill,self.OnEventKill) self:HandleEvent(EVENTS.PlayerLeaveUnit,self.OnEventPlayerLeaveUnit) self:_InitGroup() self:_InitWaypoints() self.timerStatus=TIMER:New(self.Status,self):Start(1,30) self.timerQueueUpdate=TIMER:New(self._QueueUpdate,self):Start(2,5) self.timerCheckZone=TIMER:New(self._CheckInZones,self):Start(3,10) _DATABASE:AddOpsGroup(self) return self end function FLIGHTGROUP:AddTaskEnrouteEngageTargetsInZone(ZoneRadius,TargetTypes,Priority) local Task=self.group:EnRouteTaskEngageTargetsInZone(ZoneRadius:GetVec2(),ZoneRadius:GetRadius(),TargetTypes,Priority) self:AddTaskEnroute(Task) end function FLIGHTGROUP:GetAirwing() return self.legion end function FLIGHTGROUP:GetAirwingName() local name=self.legion and self.legion.alias or"None" return name end function FLIGHTGROUP:GetSquadron() return self.cohort end function FLIGHTGROUP:GetSquadronName() local name=self.cohort and self.cohort:GetName()or"None" return name end function FLIGHTGROUP:SetVTOL() self.isVTOL=true return self end function FLIGHTGROUP:SetProhibitAfterburner() self.prohibitAB=true if self:GetGroup():IsAlive()then self:GetGroup():SetOption(AI.Option.Air.id.PROHIBIT_AB,true) end return self end function FLIGHTGROUP:SetAllowAfterburner() self.prohibitAB=false if self:GetGroup():IsAlive()then self:GetGroup():SetOption(AI.Option.Air.id.PROHIBIT_AB,false) end return self end function FLIGHTGROUP:SetJettisonEmptyTanks(Switch) self.jettisonEmptyTanks=Switch if self:GetGroup():IsAlive()then self:GetGroup():SetOption(AI.Option.Air.id.JETT_TANKS_IF_EMPTY,Switch) end return self end function FLIGHTGROUP:SetJettisonWeapons(Switch) self.jettisonWeapons=not Switch if self:GetGroup():IsAlive()then self:GetGroup():SetOption(AI.Option.Air.id.PROHIBIT_JETT,not Switch) end return self end function FLIGHTGROUP:SetOptionLandingStraightIn() self.OptionLandingStraightIn=true if self:GetGroup():IsAlive()then self:GetGroup():SetOptionLandingStraightIn() end return self end function FLIGHTGROUP:SetOptionLandingForcePair() self.OptionLandingForcePair=true if self:GetGroup():IsAlive()then self:GetGroup():SetOptionLandingForcePair() end return self end function FLIGHTGROUP:SetOptionLandingRestrictPair() self.OptionLandingRestrictPair=true if self:GetGroup():IsAlive()then self:GetGroup():SetOptionLandingRestrictPair() end return self end function FLIGHTGROUP:SetOptionLandingOverheadBreak() self.OptionLandingOverheadBreak=true if self:GetGroup():IsAlive()then self:GetGroup():SetOptionLandingOverheadBreak() end return self end function FLIGHTGROUP:SetOptionPreferVertical() self.OptionPreferVertical=true if self:GetGroup():IsAlive()then self:GetGroup():OptionPreferVerticalLanding() end return self end function FLIGHTGROUP:SetReadyForTakeoff(ReadyTO,Delay) if Delay and Delay>0 then self:ScheduleOnce(Delay,FLIGHTGROUP.SetReadyForTakeoff,self,ReadyTO,0) else self:T(self.lid.."Set Ready for Takeoff switch for flightcontrol") self.isReadyTO=ReadyTO end return self end function FLIGHTGROUP:SetFlightControl(flightcontrol) if self.flightcontrol then if self.flightcontrol:IsControlling(self)then return else self.flightcontrol:_RemoveFlight(self) end end self:T(self.lid..string.format("Setting FLIGHTCONTROL to airbase %s",flightcontrol.airbasename)) self.flightcontrol=flightcontrol if not flightcontrol:IsFlight(self)then table.insert(flightcontrol.flights,self) end return self end function FLIGHTGROUP:GetFlightControl() return self.flightcontrol end function FLIGHTGROUP:SetHomebase(HomeAirbase) if type(HomeAirbase)=="string"then HomeAirbase=AIRBASE:FindByName(HomeAirbase) end self.homebase=HomeAirbase return self end function FLIGHTGROUP:SetDestinationbase(DestinationAirbase) if type(DestinationAirbase)=="string"then DestinationAirbase=AIRBASE:FindByName(DestinationAirbase) end self.destbase=DestinationAirbase return self end function FLIGHTGROUP:SetAirboss(airboss) self.airboss=airboss return self end function FLIGHTGROUP:SetFuelLowThreshold(threshold) self.fuellowthresh=threshold or 25 return self end function FLIGHTGROUP:SetFuelLowRTB(switch) if switch==false then self.fuellowrtb=false else self.fuellowrtb=true end return self end function FLIGHTGROUP:SetOutOfAAMRTB(switch) if switch==false then self.outofAAMrtb=false else self.outofAAMrtb=true end return self end function FLIGHTGROUP:SetOutOfAGMRTB(switch) if switch==false then self.outofAGMrtb=false else self.outofAGMrtb=true end return self end function FLIGHTGROUP:SetFuelLowRefuel(switch) if switch==false then self.fuellowrefuel=false else self.fuellowrefuel=true end return self end function FLIGHTGROUP:SetFuelCriticalThreshold(threshold) self.fuelcriticalthresh=threshold or 10 return self end function FLIGHTGROUP:SetFuelCriticalRTB(switch) if switch==false then self.fuelcriticalrtb=false else self.fuelcriticalrtb=true end return self end function FLIGHTGROUP:SetDespawnAfterLanding() self.despawnAfterLanding=true return self end function FLIGHTGROUP:SetDespawnAfterHolding() self.despawnAfterHolding=true return self end function FLIGHTGROUP:IsParking(Element) local is=self:Is("Parking") if Element then is=Element.status==OPSGROUP.ElementStatus.PARKING end return is end function FLIGHTGROUP:IsTaxiing(Element) local is=self:Is("Taxiing") if Element then is=Element.status==OPSGROUP.ElementStatus.TAXIING end return is end function FLIGHTGROUP:IsAirborne(Element) local is=self:Is("Airborne")or self:Is("Cruising") if Element then is=Element.status==OPSGROUP.ElementStatus.AIRBORNE end return is end function FLIGHTGROUP:IsCruising() local is=self:Is("Cruising") return is end function FLIGHTGROUP:IsLanding(Element) local is=self:Is("Landing") if Element then is=Element.status==OPSGROUP.ElementStatus.LANDING end return is end function FLIGHTGROUP:IsLanded(Element) local is=self:Is("Landed") if Element then is=Element.status==OPSGROUP.ElementStatus.LANDED end return is end function FLIGHTGROUP:IsArrived(Element) local is=self:Is("Arrived") if Element then is=Element.status==OPSGROUP.ElementStatus.ARRIVED end return is end function FLIGHTGROUP:IsInbound() local is=self:Is("Inbound") return is end function FLIGHTGROUP:IsHolding() local is=self:Is("Holding") return is end function FLIGHTGROUP:IsGoing4Fuel() local is=self:Is("Going4Fuel") return is end function FLIGHTGROUP:IsLandingAt() local is=self:Is("LandingAt") return is end function FLIGHTGROUP:IsLandedAt() local is=self:Is("LandedAt") return is end function FLIGHTGROUP:IsFuelLow() return self.fuellow end function FLIGHTGROUP:IsFuelCritical() return self.fuelcritical end function FLIGHTGROUP:IsFuelGood() local isgood=not(self.fuellow or self.fuelcritical) return isgood end function FLIGHTGROUP:CanAirToGround(ExcludeGuns) local ammo=self:GetAmmoTot() if ExcludeGuns then return ammo.MissilesAG+ammo.Rockets+ammo.Bombs>0 else return ammo.MissilesAG+ammo.Rockets+ammo.Bombs+ammo.Guns>0 end end function FLIGHTGROUP:CanAirToAir(ExcludeGuns) local ammo=self:GetAmmoTot() if ExcludeGuns then return ammo.MissilesAA>0 else return ammo.MissilesAA+ammo.Guns>0 end end function FLIGHTGROUP:StartUncontrolled(delay) if delay and delay>0 then self:T2(self.lid..string.format("Starting uncontrolled group in %d seconds",delay)) self:ScheduleOnce(delay,FLIGHTGROUP.StartUncontrolled,self) else local alive=self:IsAlive() if alive~=nil then local _delay=0 if alive==false then self:Activate() _delay=1 end self:T(self.lid.."Starting uncontrolled group") self.group:StartUncontrolled(_delay) self.isUncontrolled=false else self:T(self.lid.."ERROR: Could not start uncontrolled group as it is NOT alive!") end end return self end function FLIGHTGROUP:ClearToLand(Delay) if Delay and Delay>0 then self:ScheduleOnce(Delay,FLIGHTGROUP.ClearToLand,self) else if self:IsHolding()then self:T(self.lid..string.format("Clear to land ==> setting holding flag to 1 (true)")) self.flaghold:Set(1) self.Tholding=nil if self.stack then self.stack.flightgroup=nil self.stack=nil end end end return self end function FLIGHTGROUP:GetFuelMin() local fuelmin=math.huge for i,_element in pairs(self.elements)do local element=_element local unit=element.unit local life=unit:GetLife() if unit and unit:IsAlive()and life>1 then local fuel=unit:GetFuel() if fuelself.Twaiting+self.dTwait then end end end if mission and mission.missionHoldingCoord and self.isHoldingAtHoldingPoint==true then self:T(self.lid.."...yes") if mission:IsReadyToPush()then self.flaghold:Set(1) self.Twaiting=nil self.dTwait=nil self.isHoldingAtHoldingPoint=false end end if mission and mission.updateDCSTask then local mtype=mission:GetType() if(mtype==AUFTRAG.Type.ORBIT or mtype==AUFTRAG.Type.RECOVERYTANKER or mtype==AUFTRAG.Type.CAP or mtype==AUFTRAG.Type.AWACS)and mission.orbitVec2 then local vec2=mission:GetTargetVec2() local hdg=mission:GetTargetHeading() local hdgchange=false if mission.orbitLeg then if UTILS.HdgDiff(hdg,mission.targetHeading)>0 then hdgchange=true end end local dist=UTILS.VecDist2D(vec2,mission.orbitVec2) local distchange=dist>mission.orbitDeltaR self:T3(self.lid..string.format("Checking orbit mission dist=%d meters",dist)) if distchange or hdgchange then self:T3(self.lid..string.format("Updating orbit!")) local DCSTask=mission:GetDCSMissionTask() local Task=mission:GetGroupWaypointTask(self) self.controller:resetTask() self:_SandwitchDCSTask(DCSTask,Task,false,1) end elseif mission.type==AUFTRAG.Type.CAPTUREZONE then local Task=mission:GetGroupWaypointTask(self) if mission:GetGroupStatus(self)==AUFTRAG.GroupStatus.EXECUTING or mission:GetGroupStatus(self)==AUFTRAG.GroupStatus.STARTED then self:_UpdateTask(Task,mission) end end end if self:IsParking()then for _,_element in pairs(self.elements)do local element=_element if element.parking then local dist=self:_GetDistToParking(element.parking,element.unit:GetCoord()) self:T(self.lid..string.format("Distance to parking spot %d = %.1f meters",element.parking.TerminalID,dist)) if dist>12 and element.engineOn then self:ElementTaxiing(element) end else end end end else self:_CheckDamage() end if self.verbose>=1 then local nelem=self:CountElements() local Nelem=#self.elements local nTaskTot,nTaskSched,nTaskWP=self:CountRemainingTasks() local nMissions=self:CountRemainingMissison() local roe=self:GetROE()or-1 local rot=self:GetROT()or-1 local wpidxCurr=self.currentwp local wpuidCurr=self:GetWaypointUIDFromIndex(wpidxCurr)or 0 local wpidxNext=self:GetWaypointIndexNext()or 0 local wpuidNext=self:GetWaypointUIDFromIndex(wpidxNext)or 0 local wpN=#self.waypoints or 0 local wpF=tostring(self.passedfinalwp) local speed=UTILS.MpsToKnots(self.velocity or 0) local speedEx=UTILS.MpsToKnots(self:GetExpectedSpeed()) local alt=self.position and self.position.y or 0 local hdg=self.heading or 0 local formation=self.option.Formation or"unknown" local life=self.life or 0 local ammo=self:GetAmmoTot().Total local ndetected=self.detectionOn and tostring(self.detectedunits:Count())or"Off" local cargo=0 for _,_element in pairs(self.elements)do local element=_element cargo=cargo+element.weightCargo end local home=self.homebase and self.homebase:GetName()or"unknown" local dest=self.destbase and self.destbase:GetName()or"unknown" local curr=self.currbase and self.currbase:GetName()or"N/A" local text=string.format("%s [%d/%d]: ROE/ROT=%d/%d | T/M=%d/%d | Wp=%d[%d]-->%d[%d]/%d [%s] | Life=%.1f | v=%.1f (%d) | Hdg=%03d | Ammo=%d | Detect=%s | Cargo=%.1f | Base=%s [%s-->%s]", fsmstate,nelem,Nelem,roe,rot,nTaskTot,nMissions,wpidxCurr,wpuidCurr,wpidxNext,wpuidNext,wpN,wpF,life,speed,speedEx,hdg,ammo,ndetected,cargo,curr,home,dest) self:I(self.lid..text) end if self.verbose>=2 then local text="Elements:" for i,_element in pairs(self.elements)do local element=_element local name=element.name local status=element.status local unit=element.unit local fuel=unit:GetFuel()or 0 local life=unit:GetLifeRelative()or 0 local lp=unit:GetLife() local lp0=unit:GetLife0() local parking=element.parking and tostring(element.parking.TerminalID)or"X" local ammo=self:GetAmmoElement(element) text=text..string.format("\n[%d] %s: status=%s, fuel=%.1f, life=%.1f [%.1f/%.1f], guns=%d, rockets=%d, bombs=%d, missiles=%d (AA=%d, AG=%d, AS=%s), parking=%s", i,name,status,fuel*100,life*100,lp,lp0,ammo.Guns,ammo.Rockets,ammo.Bombs,ammo.Missiles,ammo.MissilesAA,ammo.MissilesAG,ammo.MissilesAS,parking) end if#self.elements==0 then text=text.." none!" end self:I(self.lid..text) end if self.verbose>=4 and alive then local ds=self.travelds local dt=self.dTpositionUpdate local v=ds/dt local TmaxFuel=math.huge for _,_element in pairs(self.elements)do local element=_element local fuel=element.unit:GetFuel()or 0 local dFrel=element.fuelrel-fuel local dFreldt=dFrel/dt local Tfuel=fuel/dFreldt if Tfuel Tfuel=%.1f min",element.name,fuel*100,dFrel*100,dFreldt*100*60,Tfuel/60)) element.fuelrel=fuel end self:T(self.lid..string.format("Travelled ds=%.1f km dt=%.1f s ==> v=%.1f knots. Fuel left for %.1f min",self.traveldist/1000,dt,UTILS.MpsToKnots(v),TmaxFuel/60)) end if false then for _,_element in pairs(self.elements)do local element=_element local unit=element.unit if unit and unit:IsAlive()then local vec3=unit:GetVec3() if vec3 and element.pos then local id=UTILS.GetMarkID() trigger.action.lineToAll(-1,id,vec3,element.pos,{1,1,1,0.5},1) end element.pos=vec3 end end end if alive and self.group:IsAirborne(true)then local fuelmin=self:GetFuelMin() self:T2(self.lid..string.format("Fuel state=%d",fuelmin)) if fuelmin>=self.fuellowthresh then self.fuellow=false end if fuelmin>=self.fuelcriticalthresh then self.fuelcritical=false end if fuelmin taxiing (if AI)",element.name)) self:ElementEngineOn(element) element.engineOn=true end end end end function FLIGHTGROUP:OnEventTakeOff(EventData) self:T3(self.lid.."EVENT: TakeOff") if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then local unit=EventData.IniUnit local group=EventData.IniGroup local unitname=EventData.IniUnitName local element=self:GetElementByName(unitname) if element then self:T2(self.lid..string.format("EVENT: Element %s took off ==> airborne",element.name)) self:ElementTakeoff(element,EventData.Place) end end end function FLIGHTGROUP:OnEventLanding(EventData) if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then local unit=EventData.IniUnit local group=EventData.IniGroup local unitname=EventData.IniUnitName local element=self:GetElementByName(unitname) local airbase=EventData.Place local airbasename="unknown" if airbase then airbasename=tostring(airbase:GetName()) end if element then self:T3(self.lid..string.format("EVENT: Element %s landed at %s ==> landed",element.name,airbasename)) self:ElementLanded(element,airbase) end end end function FLIGHTGROUP:OnEventEngineShutdown(EventData) if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then local unit=EventData.IniUnit local group=EventData.IniGroup local unitname=EventData.IniUnitName local element=self:GetElementByName(unitname) if element then element.engineOn=false if element.unit and element.unit:IsAlive()then local airbase=self:GetClosestAirbase() local parking=self:GetParkingSpot(element,100,airbase) if airbase and parking then self:ElementArrived(element,airbase,parking) self:T3(self.lid..string.format("EVENT: Element %s shut down engines ==> arrived",element.name)) else self:T3(self.lid..string.format("EVENT: Element %s shut down engines but is not parking. Is it dead?",element.name)) end else end end end end function FLIGHTGROUP:OnEventCrash(EventData) if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then local unit=EventData.IniUnit local group=EventData.IniGroup local unitname=EventData.IniUnitName local element=self:GetElementByName(unitname) if element and element.status~=OPSGROUP.ElementStatus.DEAD then self:T(self.lid..string.format("EVENT: Element %s crashed ==> destroyed",element.name)) self:ElementDestroyed(element) end end end function FLIGHTGROUP:OnEventUnitLost(EventData) if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then self:T2(self.lid..string.format("EVENT: Unit %s lost at t=%.3f",EventData.IniUnitName,timer.getTime())) local unit=EventData.IniUnit local group=EventData.IniGroup local unitname=EventData.IniUnitName local element=self:GetElementByName(unitname) if element and element.status~=OPSGROUP.ElementStatus.DEAD then self:T(self.lid..string.format("EVENT: Element %s unit lost ==> destroyed t=%.3f",element.name,timer.getTime())) self:ElementDestroyed(element) end end end function FLIGHTGROUP:onafterElementSpawned(From,Event,To,Element) self:T(self.lid..string.format("Element spawned %s",Element.name)) if Element.playerName then self:_InitPlayerData(Element.playerName) end self:_UpdateStatus(Element,OPSGROUP.ElementStatus.SPAWNED) if Element.unit:InAir(not self.isHelo)then self:__ElementAirborne(0.11,Element) else local spot=self:GetParkingSpot(Element,10) if spot then self:__ElementParking(0.11,Element,spot) else self:T(self.lid..string.format("Element spawned not in air but not on any parking spot.")) self:__ElementParking(0.11,Element) end end end function FLIGHTGROUP:onafterElementParking(From,Event,To,Element,Spot) if Spot then self:_SetElementParkingAt(Element,Spot) end self:T(self.lid..string.format("Element parking %s at spot %s",Element.name,Element.parking and tostring(Element.parking.TerminalID)or"N/A")) self:_UpdateStatus(Element,OPSGROUP.ElementStatus.PARKING) if self:IsTakeoffCold()then elseif self:IsTakeoffHot()then self:__ElementEngineOn(0.5,Element) Element.engineOn=true elseif self:IsTakeoffRunway()then self:__ElementEngineOn(0.5,Element) Element.engineOn=true end end function FLIGHTGROUP:onafterElementEngineOn(From,Event,To,Element) self:T(self.lid..string.format("Element %s started engines",Element.name)) self:_UpdateStatus(Element,OPSGROUP.ElementStatus.ENGINEON) end function FLIGHTGROUP:onafterElementTaxiing(From,Event,To,Element) local TerminalID=Element.parking and tostring(Element.parking.TerminalID)or"N/A" self:T(self.lid..string.format("Element taxiing %s. Parking spot %s is now free",Element.name,TerminalID)) self:_SetElementParkingFree(Element) self:_UpdateStatus(Element,OPSGROUP.ElementStatus.TAXIING) end function FLIGHTGROUP:onafterElementTakeoff(From,Event,To,Element,airbase) self:T(self.lid..string.format("Element takeoff %s at %s airbase.",Element.name,airbase and airbase:GetName()or"unknown")) if Element.parking then self:_SetElementParkingFree(Element) end self:_UpdateStatus(Element,OPSGROUP.ElementStatus.TAKEOFF,airbase) self:__ElementAirborne(0.01,Element) end function FLIGHTGROUP:onafterElementAirborne(From,Event,To,Element) self:T2(self.lid..string.format("Element airborne %s",Element.name)) self:_SetElementParkingFree(Element) self:_UpdateStatus(Element,OPSGROUP.ElementStatus.AIRBORNE) end function FLIGHTGROUP:onafterElementLanded(From,Event,To,Element,airbase) self:T2(self.lid..string.format("Element landed %s at %s airbase",Element.name,airbase and airbase:GetName()or"unknown")) self:_UpdateStatus(Element,OPSGROUP.ElementStatus.LANDED,airbase) if self.isHelo then local Spot=self:GetParkingSpot(Element,10,airbase) if Spot then self:_SetElementParkingAt(Element,Spot) self:_UpdateStatus(Element,OPSGROUP.ElementStatus.ARRIVED) end end if self.despawnAfterLanding then if self.legion then if airbase and self.legion.airbase and airbase.AirbaseName==self.legion.airbase.AirbaseName then if self:IsLanded()then self:ReturnToLegion() else self:DespawnElement(Element) end end else self:DespawnElement(Element) end end end function FLIGHTGROUP:onafterElementArrived(From,Event,To,Element,airbase,Parking) self:T(self.lid..string.format("Element arrived %s at %s airbase using parking spot %d",Element.name,airbase and airbase:GetName()or"unknown",Parking and Parking.TerminalID or-99)) self:_SetElementParkingAt(Element,Parking) self:_UpdateStatus(Element,OPSGROUP.ElementStatus.ARRIVED) end function FLIGHTGROUP:onafterElementDestroyed(From,Event,To,Element) self:GetParent(self).onafterElementDestroyed(self,From,Event,To,Element) end function FLIGHTGROUP:onafterElementDead(From,Event,To,Element) if self.flightcontrol and Element.parking then self.flightcontrol:SetParkingFree(Element.parking) end self:GetParent(self).onafterElementDead(self,From,Event,To,Element) Element.parking=nil end function FLIGHTGROUP:onafterSpawned(From,Event,To) self:T(self.lid..string.format("Flight spawned")) if self.verbose>=1 then local text=string.format("Initialized Flight Group %s:\n",self.groupname) text=text..string.format("Unit type = %s\n",tostring(self.actype)) text=text..string.format("Speed max = %.1f Knots\n",UTILS.KmphToKnots(self.speedMax)) text=text..string.format("Range max = %.1f km\n",self.rangemax/1000) text=text..string.format("Ceiling = %.1f feet\n",UTILS.MetersToFeet(self.ceiling)) text=text..string.format("Weight = %.1f kg\n",self:GetWeightTotal()) text=text..string.format("Cargo bay = %.1f kg\n",self:GetFreeCargobay()) text=text..string.format("Tanker type = %s\n",tostring(self.tankertype)) text=text..string.format("Refuel type = %s\n",tostring(self.refueltype)) text=text..string.format("AI = %s\n",tostring(self.isAI)) text=text..string.format("Has EPLRS = %s\n",tostring(self.isEPLRS)) text=text..string.format("Helicopter = %s\n",tostring(self.isHelo)) text=text..string.format("Elements = %d\n",#self.elements) text=text..string.format("Waypoints = %d\n",#self.waypoints) text=text..string.format("Radio = %.1f MHz %s %s\n",self.radio.Freq,UTILS.GetModulationName(self.radio.Modu),tostring(self.radio.On)) text=text..string.format("Ammo = %d (G=%d/R=%d/B=%d/M=%d)\n",self.ammo.Total,self.ammo.Guns,self.ammo.Rockets,self.ammo.Bombs,self.ammo.Missiles) text=text..string.format("FSM state = %s\n",self:GetState()) text=text..string.format("Is alive = %s\n",tostring(self.group:IsAlive())) text=text..string.format("LateActivate = %s\n",tostring(self:IsLateActivated())) text=text..string.format("Uncontrolled = %s\n",tostring(self:IsUncontrolled())) text=text..string.format("Start Air = %s\n",tostring(self:IsTakeoffAir())) text=text..string.format("Start Cold = %s\n",tostring(self:IsTakeoffCold())) text=text..string.format("Start Hot = %s\n",tostring(self:IsTakeoffHot())) text=text..string.format("Start Rwy = %s\n",tostring(self:IsTakeoffRunway())) text=text..string.format("Elements:") for i,_element in pairs(self.elements)do local element=_element text=text..string.format("\n[%d] %s: callsign=%s, modex=%s, player=%s",i,element.name,tostring(element.callsign),tostring(element.modex),tostring(element.playerName)) end self:I(self.lid..text) end self:_UpdatePosition() self.isDead=false self.isDestroyed=false if self.isAI then self:SwitchROE(self.option.ROE) self:SwitchROT(self.option.ROT) self:SwitchEPLRS(self.option.EPLRS) self:SwitchInvisible(self.option.Invisible) self:SwitchImmortal(self.option.Immortal) self:SwitchFormation(self.option.Formation) self:_SwitchTACAN() if self.radioDefault then self:SwitchRadio() else self:SetDefaultRadio(self.radio.Freq,self.radio.Modu,self.radio.On) end if self.callsignDefault then self:SwitchCallsign(self.callsignDefault.NumberSquad,self.callsignDefault.NumberGroup) else self:SetDefaultCallsign(self.callsign.NumberSquad,self.callsign.NumberGroup) end self:GetGroup():SetOption(AI.Option.Air.id.PROHIBIT_JETT,self.jettisonWeapons) self:GetGroup():SetOption(AI.Option.Air.id.PROHIBIT_AB,self.prohibitAB) self:GetGroup():SetOption(AI.Option.Air.id.RTB_ON_BINGO,false) self:GetGroup():SetOption(AI.Option.Air.id.JETT_TANKS_IF_EMPTY,self.jettisonEmptyTanks) self:__UpdateRoute(-0.5) else if self.currbase then local flightcontrol=_DATABASE:GetFlightControl(self.currbase:GetName()) if flightcontrol then self:SetFlightControl(flightcontrol) else self:_UpdateMenu(0.5) end else self:_UpdateMenu(0.5) end end end function FLIGHTGROUP:onafterParking(From,Event,To) local airbase=self:GetClosestAirbase() local airbasename=airbase:GetName()or"unknown" self:T(self.lid..string.format("Flight is parking at airbase %s",airbasename)) self.currbase=airbase if not self.homebase then self.homebase=airbase end self.Tparking=timer.getAbsTime() local flightcontrol=_DATABASE:GetFlightControl(airbasename) if flightcontrol then self:SetFlightControl(flightcontrol) if self.flightcontrol then self.flightcontrol:SetFlightStatus(self,FLIGHTCONTROL.FlightStatus.PARKING) end else self:T3(self.lid.."INFO: No flight control in onAfterParking!") end end function FLIGHTGROUP:onafterTaxiing(From,Event,To) self:T(self.lid..string.format("Flight is taxiing")) self.Tparking=nil if self.flightcontrol and self.flightcontrol:IsControlling(self)then if self.isAI then self.flightcontrol:SetFlightStatus(self,FLIGHTCONTROL.FlightStatus.TAKEOFF) else self.flightcontrol:SetFlightStatus(self,FLIGHTCONTROL.FlightStatus.TAXIOUT) end end end function FLIGHTGROUP:onafterTakeoff(From,Event,To,airbase) self:T(self.lid..string.format("Flight takeoff from %s",airbase and airbase:GetName()or"unknown airbase")) if self.flightcontrol and airbase and self.flightcontrol.airbasename==airbase:GetName()then self.flightcontrol:_RemoveFlight(self) self.flightcontrol=nil end end function FLIGHTGROUP:onafterAirborne(From,Event,To) self:T(self.lid..string.format("Flight airborne")) self.currbase=nil self:__Cruise(-0.01) end function FLIGHTGROUP:onafterCruise(From,Event,To) self:T(self.lid..string.format("Flight cruising")) self.Twaiting=nil self.dTwait=nil if self.isAI then self:_CheckGroupDone(nil,120) else self:_UpdateMenu(0.1) end end function FLIGHTGROUP:onafterLanding(From,Event,To) self:T(self.lid..string.format("Flight is landing")) self:_SetElementStatusAll(OPSGROUP.ElementStatus.LANDING) if self.flightcontrol and self.flightcontrol:IsControlling(self)then self.flightcontrol:SetFlightStatus(self,FLIGHTCONTROL.FlightStatus.LANDING) end self.Tholding=nil if self.stack then self.stack.flightgroup=nil self.stack=nil end end function FLIGHTGROUP:onafterLanded(From,Event,To,airbase) self:T(self.lid..string.format("Flight landed at %s",airbase and airbase:GetName()or"unknown place")) if self.flightcontrol and self.flightcontrol:IsControlling(self)then self.flightcontrol:SetFlightStatus(self,FLIGHTCONTROL.FlightStatus.TAXIINB) end end function FLIGHTGROUP:onafterLandedAt(From,Event,To) self:T(self.lid..string.format("Flight landed at")) if self:IsPickingup()then self:__Loading(-1) elseif self:IsTransporting()then self:__Unloading(-1) end end function FLIGHTGROUP:onafterArrived(From,Event,To) self:T(self.lid..string.format("Flight arrived")) if self.flightcontrol then self.flightcontrol:SetFlightStatus(self,FLIGHTCONTROL.FlightStatus.ARRIVED) end if not self.isAI then return end local airwing=self:GetAirwing() if airwing and not(self:IsPickingup()or self:IsTransporting())then self:T(self.lid..string.format("Airwing asset group %s arrived ==> Adding asset back to stock of airwing %s",self.groupname,airwing.alias)) self:ReturnToLegion(1) elseif self.isLandingAtAirbase then local Template=UTILS.DeepCopy(self.template) self.isLateActivated=false Template.lateActivation=self.isLateActivated self.isUncontrolled=true Template.uncontrolled=self.isUncontrolled local SpawnPoint=Template.route.points[1] SpawnPoint.linkUnit=nil SpawnPoint.helipadId=nil SpawnPoint.airdromeId=nil local airbase=self.isLandingAtAirbase local AirbaseID=airbase:GetID() if airbase:IsShip()then SpawnPoint.linkUnit=AirbaseID SpawnPoint.helipadId=AirbaseID elseif airbase:IsHelipad()then SpawnPoint.linkUnit=AirbaseID SpawnPoint.helipadId=AirbaseID elseif airbase:IsAirdrome()then SpawnPoint.airdromeId=AirbaseID end SpawnPoint.alt=0 SpawnPoint.type=COORDINATE.WaypointType.TakeOffParking SpawnPoint.action=COORDINATE.WaypointAction.FromParkingArea local units=Template.units for i=#units,1,-1 do local unit=units[i] local element=self:GetElementByName(unit.name) if element and element.status~=OPSGROUP.ElementStatus.DEAD then unit.parking=element.parking and element.parking.TerminalID or nil unit.parking_id=nil local vec3=element.unit:GetVec3() local heading=element.unit:GetHeading() unit.x=vec3.x unit.y=vec3.z unit.alt=vec3.y unit.heading=math.rad(heading) unit.psi=-unit.heading else table.remove(units,i) end end self:_Respawn(0,Template) self.isLandingAtAirbase=nil if self:IsPickingup()then self:__Loading(-1) elseif self:IsTransporting()then self:__Unloading(-1) end else self:T(self.lid..string.format("Despawning group in 5 minutes after arrival!")) self:Despawn(5*60) end end function FLIGHTGROUP:onafterDead(From,Event,To) if self.flightcontrol then self.flightcontrol:_RemoveFlight(self) self.flightcontrol=nil end self:GetParent(self).onafterDead(self,From,Event,To) end function FLIGHTGROUP:onbeforeUpdateRoute(From,Event,To,n,N) local allowed=true local trepeat=nil if self:IsAlive()then self:T3(self.lid.."Update route possible. Group is ALIVE") elseif self:IsDead()then self:T(self.lid.."Update route denied. Group is DEAD!") allowed=false elseif self:IsInUtero()then self:T(self.lid.."Update route denied. Group is INUTERO!") allowed=false else self:T(self.lid.."Update route denied ==> checking back in 5 sec") trepeat=-5 allowed=false end if allowed and self:IsUncontrolled()then self:T(self.lid.."Update route denied. Group is UNCONTROLLED!") local mission=self:GetMissionCurrent() if mission and mission.type==AUFTRAG.Type.ALERT5 then trepeat=nil else trepeat=-5 end allowed=false end if n and n<1 then self:T(self.lid.."Update route denied because waypoint n<1!") allowed=false end if not self.currentwp then self:T(self.lid.."Update route denied because self.currentwp=nil!") allowed=false end local Nn=n or self.currentwp+1 if not Nn or Nn<1 then self:T(self.lid.."Update route denied because N=nil or N<1") trepeat=-5 allowed=false end if self.taskcurrent>0 then local task=self:GetTaskByID(self.taskcurrent) if task then if task.dcstask.id==AUFTRAG.SpecialTask.PATROLZONE then self:T2(self.lid.."Allowing update route for Task: PatrolZone") elseif task.dcstask.id==AUFTRAG.SpecialTask.CAPTUREZONE then self:T2(self.lid.."Allowing update route for Task: CaptureZone") elseif task.dcstask.id==AUFTRAG.SpecialTask.RECON then self:T2(self.lid.."Allowing update route for Task: ReconMission") elseif task.dcstask.id==AUFTRAG.SpecialTask.PATROLRACETRACK then self:T2(self.lid.."Allowing update route for Task: Patrol Race Track") elseif task.dcstask.id==AUFTRAG.SpecialTask.HOVER then self:T2(self.lid.."Allowing update route for Task: Hover") elseif task.dcstask.id==AUFTRAG.SpecialTask.RELOCATECOHORT then self:T2(self.lid.."Allowing update route for Task: Relocate Cohort") elseif task.description and task.description=="Task_Land_At"then self:T2(self.lid.."Allowing update route for Task: Task_Land_At") else local taskname=task and task.description or"No description" self:T(self.lid..string.format("WARNING: Update route denied because taskcurrent=%d>0! Task description = %s",self.taskcurrent,tostring(taskname))) allowed=false end else self:T(self.lid..string.format("WARNING: before update route taskcurrent=%d (>0!) but no task?!",self.taskcurrent)) allowed=false end end if not self.isAI then allowed=false end self:T2(self.lid..string.format("Onbefore Updateroute in state %s: allowed=%s (repeat in %s)",self:GetState(),tostring(allowed),tostring(trepeat))) if trepeat then self:__UpdateRoute(trepeat,n) end return allowed end function FLIGHTGROUP:onafterUpdateRoute(From,Event,To,n,N) n=n or self.currentwp+1 N=N or#self.waypoints N=math.min(N,#self.waypoints) local wp={} local speed=self.group and self.group:GetVelocityKMH()or 100 local waypointType=COORDINATE.WaypointType.TurningPoint local waypointAction=COORDINATE.WaypointAction.TurningPoint if self:IsLanded()or self:IsLandedAt()or self:IsAirborne()==false then waypointType=COORDINATE.WaypointType.TakeOff end local current=self:GetCoordinate():WaypointAir(COORDINATE.WaypointAltType.BARO,waypointType,waypointAction,speed,true,nil,{},"Current") table.insert(wp,current) for i=n,N do table.insert(wp,self.waypoints[i]) end if wp[2]then self.speedWp=wp[2].speed end local hb=self.homebase and self.homebase:GetName()or"unknown" local db=self.destbase and self.destbase:GetName()or"unknown" self:T(self.lid..string.format("Updating route for WP #%d-%d [%s], homebase=%s destination=%s",n,#wp,self:GetState(),hb,db)) if#wp>1 then self:Route(wp) else if self:IsAirborne()then self:T(self.lid.."No waypoints left ==> CheckGroupDone") self:_CheckGroupDone() end end end function FLIGHTGROUP:onafterOutOfMissilesAA(From,Event,To) self:T(self.lid.."Group is out of AA Missiles!") if self.outofAAMrtb then local airbase=self.destbase or self.homebase self:T(self.lid.."Calling RTB in onafterOutOfMissilesAA") self:__RTB(-5,airbase) end end function FLIGHTGROUP:onafterOutOfMissilesAG(From,Event,To) self:T(self.lid.."Group is out of AG Missiles!") if self.outofAGMrtb then local airbase=self.destbase or self.homebase self:T(self.lid.."Calling RTB in onafterOutOfMissilesAG") self:__RTB(-5,airbase) end end function FLIGHTGROUP:_CheckGroupDone(delay,waittime) local fsmstate=self:GetState() if self:IsAlive()and self.isAI then if delay and delay>0 then self:T(self.lid..string.format("Check FLIGHTGROUP [state=%s] done in %.3f seconds... (t=%.4f)",fsmstate,delay,timer.getTime())) self:ScheduleOnce(delay,FLIGHTGROUP._CheckGroupDone,self) else self:T(self.lid..string.format("Check FLIGHTGROUP [state=%s] done? (t=%.4f)",fsmstate,timer.getTime())) if self:IsEngaging()then self:T(self.lid.."Engaging! Group NOT done...") return end if self:IsGoing4Fuel()then self:T(self.lid.."Going for FUEL! Group NOT done...") return end local nTasks=self:CountRemainingTasks() local nMissions=self:CountRemainingMissison() local nTransports=self:CountRemainingTransports() local nPaused=self:_CountPausedMissions() if nPaused>0 and nPaused==nMissions then local missionpaused=self:_GetPausedMission() self:T(self.lid..string.format("Found paused mission %s [%s]. Unpausing mission...",missionpaused.name,missionpaused.type)) self:UnpauseMission() return end if self.isLandingAtAirbase then self:T(self.lid..string.format("Landing at airbase %s! Group NOT done...",self.isLandingAtAirbase:GetName())) return end if self:IsWaiting()then self:T(self.lid.."Waiting! Group NOT done...") return end self:T(self.lid..string.format("Remaining (final=%s): missions=%d, tasks=%d, transports=%d",tostring(self.passedfinalwp),nMissions,nTasks,nTransports)) if self:HasPassedFinalWaypoint()or self:GetWaypointIndexNext()==1 then if self.currentmission==nil and self.taskcurrent==0 and(self.cargoTransport==nil or self.cargoTransport:GetCarrierTransportStatus(self)==OPSTRANSPORT.Status.DELIVERED)then if nTasks==0 and nMissions==0 and nTransports==0 then local destbase=self.destbase or self.homebase local destzone=self.destzone or self.homezone if waittime then self:T(self.lid..string.format("Passed Final WP and No current and/or future missions/tasks/transports. Waittime given ==> Waiting for %d sec!",waittime)) self:Wait(waittime) elseif destbase then if self.currbase and self.currbase.AirbaseName==destbase.AirbaseName and self:IsParking()then self:T(self.lid.."Passed Final WP and No current and/or future missions/tasks/transports AND parking at destination airbase ==> Arrived!") self:Arrived() else if self.currbase==nil or self.currbase.AirbaseName~=destbase.AirbaseName then self:T(self.lid.."Passed Final WP and No current and/or future missions/tasks/transports ==> RTB!") self:__RTB(-0.1,destbase) end end elseif destzone then self:T(self.lid.."Passed Final WP and No current and/or future missions/tasks/transports ==> RTZ!") self:__RTZ(-0.1,destzone) else self:T(self.lid.."Passed Final WP and NO Tasks/Missions left. No DestBase or DestZone ==> Wait!") self:__Wait(-1) end else if not self:IsParking()then self:T(self.lid..string.format("Passed Final WP but Tasks=%d or Missions=%d left in the queue. Wait!",nTasks,nMissions)) self:__Wait(-1) end end else self:T(self.lid..string.format("Passed Final WP but still have current Task (#%s) or Mission (#%s) left to do",tostring(self.taskcurrent),tostring(self.currentmission))) end else self:T(self.lid..string.format("Flight (status=%s) did NOT pass the final waypoint yet ==> update route in -0.01 sec",self:GetState())) self:__UpdateRoute(-0.01) end end end end function FLIGHTGROUP:onbeforeRTB(From,Event,To,airbase,SpeedTo,SpeedHold) self:T(self.lid..string.format("RTB: before event=%s: %s --> %s to %s",Event,From,To,airbase and airbase:GetName()or"None")) if self:IsAlive()then local allowed=true local Tsuspend=nil if airbase==nil then self:T(self.lid.."ERROR: Airbase is nil in RTB() call!") allowed=false end if airbase and airbase:GetCoalition()~=self.group:GetCoalition()and airbase:GetCoalition()>0 then self:T(self.lid..string.format("ERROR: Wrong airbase coalition %d in RTB() call! We allow only same as group %d or neutral airbases 0",airbase:GetCoalition(),self.group:GetCoalition())) return false end if self.currbase and self.currbase:GetName()==airbase:GetName()then self:T(self.lid.."WARNING: Currbase is already same as RTB airbase. RTB canceled!") return false end if self:IsLanded()then self:T(self.lid.."WARNING: Flight has already landed. RTB canceled!") return false end if not self.group:IsAirborne(true)then self:T(self.lid..string.format("WARNING: Group [%s] is not AIRBORNE ==> RTB event is suspended for 20 sec",self:GetState())) allowed=false Tsuspend=-20 local groupspeed=self.group:GetVelocityMPS() if groupspeed<=1 and not self:IsParking()then self.RTBRecallCount=self.RTBRecallCount+1 end if self.RTBRecallCount>6 then self:T(self.lid..string.format("WARNING: Group [%s] is not moving and was called RTB %d times. Assuming a problem and despawning!",self:GetState(),self.RTBRecallCount)) self.RTBRecallCount=0 self:Despawn(5) return end end if self:IsFuelGood()then local Ntot,Nsched,Nwp=self:CountRemainingTasks() if self.taskcurrent>0 then self:T(self.lid..string.format("WARNING: Got current task ==> RTB event is suspended for 10 sec")) Tsuspend=-10 allowed=false end if Nsched>0 then self:T(self.lid..string.format("WARNING: Still got %d SCHEDULED tasks in the queue ==> RTB event is suspended for 10 sec",Nsched)) Tsuspend=-10 allowed=false end if Nwp>0 then self:T(self.lid..string.format("WARNING: Still got %d WAYPOINT tasks in the queue ==> RTB event is suspended for 10 sec",Nwp)) Tsuspend=-10 allowed=false end if self.Twaiting and self.dTwait then self:T(self.lid..string.format("WARNING: Group is Waiting for a specific duration ==> RTB event is canceled",Nwp)) allowed=false end end if Tsuspend and not allowed then self:T(self.lid.."Calling RTB in onbeforeRTB") self:__RTB(Tsuspend,airbase,SpeedTo,SpeedHold) end return allowed else self:T(self.lid.."WARNING: Group is not alive! RTB call not allowed.") return false end end function FLIGHTGROUP:onafterRTB(From,Event,To,airbase,SpeedTo,SpeedHold,SpeedLand) self:T(self.lid..string.format("RTB: event=%s: %s --> %s to %s",Event,From,To,airbase:GetName())) self.destbase=airbase self:CancelAllMissions() self:_LandAtAirbase(airbase,SpeedTo,SpeedHold,SpeedLand) end function FLIGHTGROUP:onbeforeLandAtAirbase(From,Event,To,airbase) if self:IsAlive()then local allowed=true local Tsuspend=nil if airbase==nil then self:T(self.lid.."ERROR: Airbase is nil in LandAtAirbase() call!") allowed=false end if airbase and airbase:GetCoalition()~=self.group:GetCoalition()and airbase:GetCoalition()>0 then self:T(self.lid..string.format("ERROR: Wrong airbase coalition %d in LandAtAirbase() call! We allow only same as group %d or neutral airbases 0",airbase:GetCoalition(),self.group:GetCoalition())) return false end if self.currbase and self.currbase:GetName()==airbase:GetName()then self:T(self.lid.."WARNING: Currbase is already same as LandAtAirbase airbase. LandAtAirbase canceled!") return false end if self:IsLanded()then self:T(self.lid.."WARNING: Flight has already landed. LandAtAirbase canceled!") return false end if self:IsParking()then allowed=false Tsuspend=-30 self:T(self.lid.."WARNING: Flight is parking. LandAtAirbase call delayed by 30 sec") elseif self:IsTaxiing()then allowed=false Tsuspend=-1 self:T(self.lid.."WARNING: Flight is parking. LandAtAirbase call delayed by 1 sec") end if Tsuspend and not allowed then self:__LandAtAirbase(Tsuspend,airbase) end return allowed else self:T(self.lid.."WARNING: Group is not alive! LandAtAirbase call not allowed") return false end end function FLIGHTGROUP:onafterLandAtAirbase(From,Event,To,airbase) self.isLandingAtAirbase=airbase self:_LandAtAirbase(airbase) end function FLIGHTGROUP:_LandAtAirbase(airbase,SpeedTo,SpeedHold,SpeedLand) self.currbase=airbase self:_PassedFinalWaypoint(true,"_LandAtAirbase") self.Twaiting=nil self.dTwait=nil SpeedTo=SpeedTo or UTILS.KmphToKnots(self.speedCruise) SpeedHold=SpeedHold or(self.isHelo and 80 or 250) SpeedLand=SpeedLand or(self.isHelo and 40 or 170) self.Tholding=nil local text=string.format("Flight group set to hold at airbase %s. SpeedTo=%d, SpeedHold=%d, SpeedLand=%d",airbase:GetName(),SpeedTo,SpeedHold,SpeedLand) self:T(self.lid..text) local althold=self.isHelo and 1000+math.random(10)*100 or math.random(4,10)*1000 local c0=self:GetCoordinate() local p0=airbase:GetZone():GetRandomCoordinate():SetAltitude(UTILS.FeetToMeters(althold)) local p1=nil local wpap=nil local fc=_DATABASE:GetFlightControl(airbase:GetName()) if fc and self.isAI then local stack=fc:_GetHoldingStack(self) if stack then stack.flightgroup=self self.stack=stack p0=stack.pos0 p1=stack.pos1 if false then p0:MarkToAll(string.format("%s: Holding stack P0, alt=%d meters",self:GetName(),p0.y)) p1:MarkToAll(string.format("%s: Holding stack P1, alt=%d meters",self:GetName(),p0.y)) end else end self:SetFlightControl(fc) self.flightcontrol:SetFlightStatus(self,FLIGHTCONTROL.FlightStatus.INBOUND) local callsign=self:GetCallsignName() local text=string.format("%s, %s, inbound for landing",fc.alias,callsign) fc:TransmissionPilot(text,self) local text=string.format("%s, %s, roger, hold at angels %d. Report entering the pattern.",callsign,fc.alias,stack.angels) fc:TransmissionTower(text,self,10) end local c1=c0:GetIntermediateCoordinate(p0,0.25):SetAltitude(self.altitudeCruise,true) local c2=c0:GetIntermediateCoordinate(p0,0.75):SetAltitude(self.altitudeCruise,true) local x1=self.isHelo and UTILS.NMToMeters(2.0)or UTILS.NMToMeters(10) local x2=self.isHelo and UTILS.NMToMeters(1.0)or UTILS.NMToMeters(5) local alpha=math.rad(3) local h1=x1*math.tan(alpha) local h2=x2*math.tan(alpha) local runway=airbase:GetActiveRunwayLanding() self.flaghold:Set(0) local holdtime=self.holdtime if fc or self.airboss then holdtime=nil end local TaskArrived=self.group:TaskFunction("FLIGHTGROUP._ReachedHolding",self) local TaskOrbit=self.group:TaskOrbit(p0,nil,UTILS.KnotsToMps(SpeedHold),p1) local TaskLand=self.group:TaskCondition(nil,self.flaghold.UserFlagName,1,nil,holdtime) local TaskHold=self.group:TaskControlled(TaskOrbit,TaskLand) local TaskKlar=self.group:TaskFunction("FLIGHTGROUP._ClearedToLand",self) local wp={} wp[#wp+1]=c1:WaypointAir("BARO",COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.TurningPoint,UTILS.KnotsToKmph(SpeedTo),true,nil,{},"Climb") wp[#wp+1]=c2:WaypointAir("BARO",COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.TurningPoint,UTILS.KnotsToKmph(SpeedTo),true,nil,{},"Descent") wp[#wp+1]=p0:WaypointAir("BARO",COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.TurningPoint,UTILS.KnotsToKmph(SpeedTo),true,nil,{TaskArrived,TaskHold,TaskKlar},"Holding Point") if airbase:IsAirdrome()then local TaskFinal=self.group:TaskFunction("FLIGHTGROUP._OnFinal",self) local rheading if runway then rheading=runway.heading-180 else local wind=airbase:GetCoordinate():GetWind() rheading=-wind end local papp=airbase:GetCoordinate():Translate(x1,rheading):SetAltitude(h1) wp[#wp+1]=papp:WaypointAirTurningPoint("BARO",UTILS.KnotsToKmph(SpeedLand),{TaskFinal},"Final Approach") local pland=airbase:GetCoordinate():Translate(x2,rheading):SetAltitude(h2) wp[#wp+1]=pland:WaypointAirLanding(UTILS.KnotsToKmph(SpeedLand),airbase,{},"Landing") elseif airbase:IsShip()or airbase:IsHelipad()then local pland=airbase:GetCoordinate() wp[#wp+1]=pland:WaypointAirLanding(UTILS.KnotsToKmph(SpeedLand),airbase,{},"Landing") end if self.isAI then self:Route(wp,1.0) end end function FLIGHTGROUP:onbeforeWait(From,Event,To,Duration,Altitude,Speed) local allowed=true local Tsuspend=nil if self.taskcurrent>0 and not self:IsLandedAt()then self:T(self.lid..string.format("WARNING: Got current task ==> WAIT event is suspended for 30 sec!")) Tsuspend=-30 allowed=false end if self.cargoTransport and not self:IsLandedAt()then end if Tsuspend and not allowed then self:__Wait(Tsuspend,Duration,Altitude,Speed) end return allowed end function FLIGHTGROUP:onafterWait(From,Event,To,Duration,Altitude,Speed) local Coord=self:GetCoordinate() if Altitude then Altitude=UTILS.FeetToMeters(Altitude) else Altitude=self.altitudeCruise end Speed=Speed or(self.isHelo and 20 or 250) local text=string.format("Group set to wait/orbit at altitude %d m and speed %.1f km/h for %s seconds",Altitude,Speed,tostring(Duration)) self:T(self.lid..text) self.flaghold:Set(0) local TaskOrbit=self.group:TaskOrbit(Coord,Altitude,UTILS.KnotsToMps(Speed)) local TaskStop=self.group:TaskCondition(nil,self.flaghold.UserFlagName,1,nil,Duration) local TaskCntr=self.group:TaskControlled(TaskOrbit,TaskStop) local TaskOver=self.group:TaskFunction("FLIGHTGROUP._FinishedWaiting",self) local DCSTasks if Duration or true then DCSTasks=self.group:TaskCombo({TaskCntr,TaskOver}) else DCSTasks=self.group:TaskCombo({TaskOrbit,TaskOver}) end self:PushTask(DCSTasks) self.Twaiting=timer.getAbsTime() self.dTwait=Duration end function FLIGHTGROUP:onafterRefuel(From,Event,To,Coordinate) local text=string.format("Flight group set to refuel at the nearest tanker") self:T(self.lid..text) self:PauseMission() local TaskRefuel=self.group:TaskRefueling() local TaskFunction=self.group:TaskFunction("FLIGHTGROUP._FinishedRefuelling",self) local DCSTasks={TaskRefuel,TaskFunction} local Speed=self.speedCruise local coordinate=self:GetCoordinate() Coordinate=Coordinate or coordinate:Translate(UTILS.NMToMeters(5),self.group:GetHeading(),true) local wp0=coordinate:WaypointAir("BARO",COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.TurningPoint,Speed,true) local wp9=Coordinate:WaypointAir("BARO",COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.TurningPoint,Speed,true,nil,DCSTasks,"Refuel") self:Route({wp0,wp9},1) self.group:SetOption(AI.Option.Air.id.RTB_ON_BINGO,true) end function FLIGHTGROUP:onafterRefueled(From,Event,To) local text=string.format("Flight group finished refuelling") self:T(self.lid..text) self.group:SetOption(AI.Option.Air.id.RTB_ON_BINGO,false) self:_CheckGroupDone(1) end function FLIGHTGROUP:onafterHolding(From,Event,To) self.flaghold:Set(0) if self.despawnAfterHolding then if self.legion then self:ReturnToLegion(1) else self:Despawn(1) end return end self.Tholding=timer.getAbsTime() local text=string.format("Flight group %s is HOLDING now",self.groupname) self:T(self.lid..text) if self.flightcontrol then self.flightcontrol:SetFlightStatus(self,FLIGHTCONTROL.FlightStatus.HOLDING) if self.isAI then local callsign=self:GetCallsignName() local text=string.format("%s, %s, arrived at holding pattern",self.flightcontrol.alias,callsign) if self.stack then text=text..string.format(", angels %d.",self.stack.angels) end self.flightcontrol:TransmissionPilot(text,self) local text=string.format("%s, roger, fly heading %d and wait for landing clearance",callsign,self.stack.heading) self.flightcontrol:TransmissionTower(text,self,10) end elseif self.airboss then if self.isHelo then local carrierpos=self.airboss:GetCoordinate() local carrierheading=self.airboss:GetHeading() local Distance=UTILS.NMToMeters(5) local Angle=carrierheading+90 local altitude=math.random(12,25)*100 local oc=carrierpos:Translate(Distance,Angle):SetAltitude(altitude,true) local TaskOrbit=self.group:TaskOrbit(oc,nil,UTILS.KnotsToMps(50)) local TaskLand=self.group:TaskCondition(nil,self.flaghold.UserFlagName,1) local TaskHold=self.group:TaskControlled(TaskOrbit,TaskLand) local TaskKlar=self.group:TaskFunction("FLIGHTGROUP._ClearedToLand",self) local DCSTask=self.group:TaskCombo({TaskOrbit,TaskHold,TaskKlar}) self:SetTask(DCSTask) end end end function FLIGHTGROUP:onafterEngageTarget(From,Event,To,Target) local DCStask=nil if Target:IsInstanceOf("UNIT")or Target:IsInstanceOf("STATIC")then DCStask=self:GetGroup():TaskAttackUnit(Target,true) elseif Target:IsInstanceOf("GROUP")then DCStask=self:GetGroup():TaskAttackGroup(Target,nil,nil,nil,nil,nil,nil,true) elseif Target:IsInstanceOf("SET_UNIT")then local DCSTasks={} for _,_unit in pairs(Target:GetSet())do local unit=_unit local task=self:GetGroup():TaskAttackUnit(unit,true) table.insert(DCSTasks) end DCStask=self:GetGroup():TaskCombo(DCSTasks) elseif Target:IsInstanceOf("SET_GROUP")then local DCSTasks={} for _,_unit in pairs(Target:GetSet())do local unit=_unit local task=self:GetGroup():TaskAttackGroup(Target,nil,nil,nil,nil,nil,nil,true) table.insert(DCSTasks) end DCStask=self:GetGroup():TaskCombo(DCSTasks) else self:T("ERROR: unknown Target in EngageTarget! Needs to be a UNIT, STATIC, GROUP, SET_UNIT or SET_GROUP") return end local Task=self:NewTaskScheduled(DCStask,1,"Engage_Target",0) Task.backupROE=self:GetROE() self:SwitchROE(ENUMS.ROE.OpenFire) local mission=self:GetMissionCurrent() if mission then self:PauseMission() end self:TaskExecute(Task) end function FLIGHTGROUP:onafterDisengage(From,Event,To) self:T(self.lid.."Disengage target") end function FLIGHTGROUP:onbeforeLandAt(From,Event,To,Coordinate,Duration) return self.isHelo end function FLIGHTGROUP:onafterLandAt(From,Event,To,Coordinate,Duration) self:T(self.lid..string.format("Landing at Coordinate for %s seconds",tostring(Duration))) Coordinate=Coordinate or self:GetCoordinate() local DCStask=self.group:TaskLandAtVec2(Coordinate:GetVec2(),Duration) local Task=self:NewTaskScheduled(DCStask,1,"Task_Land_At",0) self:TaskExecute(Task) end function FLIGHTGROUP:onafterFuelLow(From,Event,To) local fuel=self:GetFuelMin()or 0 local text=string.format("Low fuel %d for flight group %s",fuel,self.groupname) self:T(self.lid..text) self.fuellow=true local airbase=self.destbase or self.homebase if self.fuellowrefuel and self.refueltype then local tanker=self:FindNearestTanker(50) if tanker then self:T(self.lid..string.format("Send to refuel at tanker %s",tanker:GetName())) local coordinate=self:GetCoordinate():GetIntermediateCoordinate(tanker:GetCoordinate(),0.75) self:Refuel(coordinate) return end end if airbase and self.fuellowrtb then self:T(self.lid.."Calling RTB in onafterFuelLow") self:RTB(airbase) end end function FLIGHTGROUP:onafterFuelCritical(From,Event,To) local text=string.format("Critical fuel for flight group %s",self.groupname) self:T(self.lid..text) self.fuelcritical=true local airbase=self.destbase or self.homebase if airbase and self.fuelcriticalrtb and not self:IsGoing4Fuel()then self:T(self.lid.."Calling RTB in onafterFuelCritical") self:RTB(airbase) end end function FLIGHTGROUP._ReachedHolding(group,flightgroup) flightgroup:T2(flightgroup.lid..string.format("Group reached holding point")) flightgroup:__Holding(-1) end function FLIGHTGROUP._ClearedToLand(group,flightgroup) flightgroup:T2(flightgroup.lid..string.format("Group was cleared to land")) flightgroup:__Landing(-1) end function FLIGHTGROUP._OnFinal(group,flightgroup) flightgroup:T2(flightgroup.lid..string.format("Group on final approach")) local fc=flightgroup.flightcontrol if fc and fc:IsControlling(flightgroup)then fc:_FlightOnFinal(flightgroup) end end function FLIGHTGROUP._FinishedRefuelling(group,flightgroup) flightgroup:T2(flightgroup.lid..string.format("Group finished refueling")) flightgroup:__Refueled(-1) end function FLIGHTGROUP._FinishedWaiting(group,flightgroup) flightgroup:T(flightgroup.lid..string.format("Group finished waiting")) flightgroup.Twaiting=nil flightgroup.dTwait=nil flightgroup:_CheckGroupDone(0.1) end function FLIGHTGROUP:_InitGroup(Template,Delay) if Delay and Delay>0 then self:ScheduleOnce(Delay,FLIGHTGROUP._InitGroup,self,Template,0) else if self.groupinitialized then self:T(self.lid.."WARNING: Group was already initialized! Will NOT do it again!") return end local group=self.group self.isHelo=group:IsHelicopter() self.speedMax=group:GetSpeedMax() if self.speedMax and self.speedMax>3.6 then self.isMobile=true else self.isMobile=false self.speedMax=0 end local speedCruiseLimit=self.isHelo and UTILS.KnotsToKmph(110)or UTILS.KnotsToKmph(380) self.speedCruise=math.min(self.speedMax*0.7,speedCruiseLimit) self.ammo=self:GetAmmoTot() local template=Template or self:_GetTemplate() self.isUncontrolled=template~=nil and template.uncontrolled or false self.isLateActivated=template~=nil and template.lateActivation or false if template then self.radio.Freq=tonumber(template.frequency) self.radio.Modu=tonumber(template.modulation) self.radio.On=template.communication local callsign=template.units[1].callsign if type(callsign)=="number"then local cs=tostring(callsign) callsign={} callsign[1]=cs:sub(1,1) callsign[2]=cs:sub(2,2) callsign[3]=cs:sub(3,3) end self.callsign.NumberSquad=tonumber(callsign[1]) self.callsign.NumberGroup=tonumber(callsign[2]) self.callsign.NameSquad=UTILS.GetCallsignName(self.callsign.NumberSquad) end if self.isHelo then self.optionDefault.Formation=ENUMS.Formation.RotaryWing.EchelonLeft.D300 else self.optionDefault.Formation=ENUMS.Formation.FixedWing.EchelonLeft.Group end if not self.tacanDefault then self:SetDefaultTACAN(nil,nil,nil,nil,true) end if not self.tacan then self.tacan=UTILS.DeepCopy(self.tacanDefault) end self.isAI=not self:_IsHuman(group) if not self.isAI then self.menu=self.menu or{} self.menu.atc=self.menu.atc or{} self.menu.atc.root=self.menu.atc.root or MENU_GROUP:New(self.group,"ATC") self.menu.atc.help=self.menu.atc.help or MENU_GROUP:New(self.group,"Help",self.menu.atc.root) end local units=self.group:GetUnits() local dcsgroup=Group.getByName(self.groupname) local size0=dcsgroup:getInitialSize() if#units~=size0 then self:T(self.lid..string.format("ERROR: Got #units=%d but group consists of %d units!",#units,size0)) end for _,unit in pairs(units)do self:_AddElementByName(unit:GetName()) end self.groupinitialized=true end return self end function FLIGHTGROUP:GetHomebaseFromWaypoints() local wp=self.waypoints0 and self.waypoints0[1]or nil if wp then if wp and wp.action and wp.action==COORDINATE.WaypointAction.FromParkingArea or wp.action==COORDINATE.WaypointAction.FromParkingAreaHot or wp.action==COORDINATE.WaypointAction.FromRunway then local airbaseID=nil if wp.airdromeId then airbaseID=wp.airdromeId else airbaseID=-wp.helipadId end local airbase=AIRBASE:FindByID(airbaseID) return airbase end end return nil end function FLIGHTGROUP:FindNearestAirbase(Radius) local coord=self:GetCoordinate() local dmin=math.huge local airbase=nil for _,_airbase in pairs(AIRBASE.GetAllAirbases())do local ab=_airbase local coalitionAB=ab:GetCoalition() if coalitionAB==self:GetCoalition()or coalitionAB==coalition.side.NEUTRAL then if airbase then local d=ab:GetCoordinate():Get2DDistance(coord) if dself.currentwp then self:_PassedFinalWaypoint(false,"AddWaypointLanding") end Speed=Speed or self.speedCruise local Coordinate=Airbase:GetCoordinate() local wp=Coordinate:WaypointAir(COORDINATE.WaypointAltType.BARO,COORDINATE.WaypointType.Land,COORDINATE.WaypointAction.Landing,Speed,nil,Airbase,{},"Landing Temp",nil) local waypoint=self:_CreateWaypoint(wp) if Altitude then waypoint.alt=UTILS.FeetToMeters(Altitude) end self:_AddWaypoint(waypoint,wpnumber) self:T(self.lid..string.format("Adding AIR waypoint #%d, speed=%.1f knots. Last waypoint passed was #%s. Total waypoints #%d",wpnumber,Speed,self.currentwp,#self.waypoints)) if Updateroute==nil or Updateroute==true then self:__UpdateRoute(-1) end return waypoint end function FLIGHTGROUP:GetPlayerElement() for _,_element in pairs(self.elements)do local element=_element if not element.ai then return element end end return nil end function FLIGHTGROUP:GetPlayerName() local playerElement=self:GetPlayerElement() if playerElement then return playerElement.playerName end return nil end function FLIGHTGROUP:_SetElementParkingAt(Element,Spot) Element.parking=Spot if Spot then self:T(self.lid..string.format("Element %s is parking on spot %d",Element.name,Spot.TerminalID)) local fc=_DATABASE:GetFlightControl(Spot.AirbaseName) if fc and not self.flightcontrol then self:SetFlightControl(fc) end if self.flightcontrol then self.flightcontrol:SetParkingOccupied(Element.parking,Element.name) end end end function FLIGHTGROUP:_SetElementParkingFree(Element) if Element.parking then if self.flightcontrol then self.flightcontrol:SetParkingFree(Element.parking) end Element.parking=nil end end function FLIGHTGROUP:_GetOnboardNumber(unitname) local group=UNIT:FindByName(unitname):GetGroup() local units=group:GetTemplate().units local numbers={} for _,unit in pairs(units)do if unitname==unit.name then return tostring(unit.onboard_num) end end return nil end function FLIGHTGROUP:_IsHumanUnit(unit) local playerunit=self:_GetPlayerUnitAndName(unit:GetName()) if playerunit then return true else return false end end function FLIGHTGROUP:_IsHuman(group) local units=group:GetUnits() for _,_unit in pairs(units)do local human=self:_IsHumanUnit(_unit) if human then return true end end return false end function FLIGHTGROUP:_GetPlayerUnitAndName(_unitName) self:F2(_unitName) if _unitName~=nil then local DCSunit=Unit.getByName(_unitName) if DCSunit then local playername=DCSunit:getPlayerName() local unit=UNIT:Find(DCSunit) if DCSunit and unit and playername then return unit,playername end end end return nil,nil end function FLIGHTGROUP:GetParkingSpot(element,maxdist,airbase) local coord=element.unit:GetCoordinate() airbase=airbase or self:GetClosestAirbase() if airbase==nil then self:T(self.lid.."No airbase found for element "..element.name) return nil end local parking=airbase.parking if airbase and airbase:IsShip()then if#parking>1 then coord=airbase:GetRelativeCoordinate(coord.x,coord.y,coord.z) else coord.x=0 coord.z=0 maxdist=500 end end local spot=nil local dist=nil local distmin=math.huge for _,_parking in pairs(parking)do local parking=_parking dist=coord:Get2DDistance(parking.Coordinate) if distsafedist) return safe end local function _clients() local clients=_DATABASE.CLIENTS local coords={} for clientname,client in pairs(clients)do local template=_DATABASE:GetGroupTemplateFromUnitName(clientname) 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 return coords end local airbasecategory=airbase:GetAirbaseCategory() local parkingdata=airbase:GetParkingSpotsTable() local obstacles={} for _,_parkingspot in pairs(parkingdata)do local parkingspot=_parkingspot local _,_,_,_units,_statics,_sceneries=parkingspot.Coordinate:ScanObjects(scanradius,scanunits,scanstatics,scanscenery) for _,_unit in pairs(_units)do local unit=_unit local _coord=unit:GetCoordinate() local _size=self:_GetObjectSize(unit:GetDCSObject()) local _name=unit:GetName() table.insert(obstacles,{coord=_coord,size=_size,name=_name,type="unit"}) end local clientcoords=_clients() for clientname,_coord in pairs(clientcoords)do table.insert(obstacles,{coord=_coord,size=15,name=clientname,type="client"}) end for _,static in pairs(_statics)do local _vec3=static:getPoint() local _coord=COORDINATE:NewFromVec3(_vec3) local _name=static:getName() local _size=self:_GetObjectSize(static) table.insert(obstacles,{coord=_coord,size=_size,name=_name,type="static"}) end for _,scenery in pairs(_sceneries)do local _vec3=scenery:getPoint() local _coord=COORDINATE:NewFromVec3(_vec3) local _name=scenery:getTypeName() local _size=self:_GetObjectSize(scenery) table.insert(obstacles,{coord=_coord,size=_size,name=_name,type="scenery"}) end end local parking={} local terminaltype=self:_GetTerminal(self.attribute,airbase:GetAirbaseCategory()) for i,_element in pairs(self.elements)do local element=_element local gotit=false for _,_parkingspot in pairs(parkingdata)do local parkingspot=_parkingspot if AIRBASE._CheckTerminalType(parkingspot.TerminalType,terminaltype)then local free=true local problem=nil if verysafe and parkingspot.TOAC then free=false self:T2(self.lid..string.format("Parking spot %d is occupied by other aircraft taking off (TOAC).",parkingspot.TerminalID)) end for _,obstacle in pairs(obstacles)do local dist=parkingspot.Coordinate:Get2DDistance(obstacle.coord) local safe=_overlap(element.size,obstacle.size,dist) if not safe then free=false problem=obstacle problem.dist=dist break end end if self.flightcontrol and self.flightcontrol.airbasename==airbase:GetName()then local problem=self.flightcontrol:IsParkingReserved(parkingspot)or self.flightcontrol:IsParkingOccupied(parkingspot) if problem then free=false end end if free then table.insert(parking,parkingspot) self:T2(self.lid..string.format("Parking spot %d is free for element %s!",parkingspot.TerminalID,element.name)) table.insert(obstacles,{coord=parkingspot.Coordinate,size=element.size,name=element.name,type="element"}) gotit=true break else self:T2(self.lid..string.format("Parking spot %d is occupied or not big enough!",parkingspot.TerminalID)) end end end if not gotit then self:T(self.lid..string.format("WARNING: No free parking spot for element %s",element.name)) return nil end end return parking end function FLIGHTGROUP:_GetObjectSize(DCSobject) local DCSdesc=DCSobject:getDesc() if DCSdesc.box then local x=DCSdesc.box.max.x+math.abs(DCSdesc.box.min.x) local y=DCSdesc.box.max.y+math.abs(DCSdesc.box.min.y) local z=DCSdesc.box.max.z+math.abs(DCSdesc.box.min.z) return math.max(x,z),x,y,z end return 0,0,0,0 end function FLIGHTGROUP:_GetAttribute() local attribute=FLIGHTGROUP.Attribute.OTHER local group=self.group if group then local transportplane=group:HasAttribute("Transports")and group:HasAttribute("Planes") local awacs=group:HasAttribute("AWACS") local fighter=group:HasAttribute("Fighters")or group:HasAttribute("Interceptors")or group:HasAttribute("Multirole fighters")or(group:HasAttribute("Bombers")and not group:HasAttribute("Strategic bombers")) local bomber=group:HasAttribute("Strategic bombers") local tanker=group:HasAttribute("Tankers") local uav=group:HasAttribute("UAVs") local transporthelo=group:HasAttribute("Transport helicopters") local attackhelicopter=group:HasAttribute("Attack helicopters") if transportplane then attribute=FLIGHTGROUP.Attribute.AIR_TRANSPORTPLANE elseif awacs then attribute=FLIGHTGROUP.Attribute.AIR_AWACS elseif fighter then attribute=FLIGHTGROUP.Attribute.AIR_FIGHTER elseif bomber then attribute=FLIGHTGROUP.Attribute.AIR_BOMBER elseif tanker then attribute=FLIGHTGROUP.Attribute.AIR_TANKER elseif transporthelo then attribute=FLIGHTGROUP.Attribute.AIR_TRANSPORTHELO elseif attackhelicopter then attribute=FLIGHTGROUP.Attribute.AIR_ATTACKHELO elseif uav then attribute=FLIGHTGROUP.Attribute.AIR_UAV end end return attribute end function FLIGHTGROUP:_GetTerminal(_attribute,_category) local _terminal=AIRBASE.TerminalType.OpenBig if _attribute==FLIGHTGROUP.Attribute.AIR_FIGHTER or _attribute==FLIGHTGROUP.Attribute.AIR_UAV then _terminal=AIRBASE.TerminalType.FighterAircraft elseif _attribute==FLIGHTGROUP.Attribute.AIR_BOMBER or _attribute==FLIGHTGROUP.Attribute.AIR_TRANSPORTPLANE or _attribute==FLIGHTGROUP.Attribute.AIR_TANKER or _attribute==FLIGHTGROUP.Attribute.AIR_AWACS then _terminal=AIRBASE.TerminalType.OpenBig elseif _attribute==FLIGHTGROUP.Attribute.AIR_TRANSPORTHELO or _attribute==FLIGHTGROUP.Attribute.AIR_ATTACKHELO then _terminal=AIRBASE.TerminalType.HelicopterUsable else end if _category==Airbase.Category.SHIP then if not(_attribute==FLIGHTGROUP.Attribute.AIR_TRANSPORTHELO or _attribute==FLIGHTGROUP.Attribute.AIR_ATTACKHELO)then _terminal=AIRBASE.TerminalType.OpenMedOrBig end end return _terminal end function FLIGHTGROUP:_CheckStuck(Despawn) if not self:IsTaxiing()then return nil end local Tnow=timer.getTime() local ExpectedSpeed=5 local speed=self:GetVelocity() if speed<0.1 then if ExpectedSpeed>0 and not self.stuckTimestamp then self:T2(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected",speed,ExpectedSpeed)) self.stuckTimestamp=Tnow self.stuckVec3=self:GetVec3() end else self.stuckTimestamp=nil end local holdtime=nil if self.stuckTimestamp then holdtime=Tnow-self.stuckTimestamp self:Stuck(holdtime) if holdtime>=5*60 and holdtime<15*60 then self:T(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected for %d sec",speed,ExpectedSpeed,holdtime)) elseif holdtime>=15*60 then self:T(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected for %d sec",speed,ExpectedSpeed,holdtime)) local mission=self:GetMissionCurrent() if mission then self:T(self.lid..string.format("WARNING: Cancelling mission %s [%s] due to being stuck",mission:GetName(),mission:GetType())) self:MissionCancel(mission) end if self.stuckDespawn then if self.legion then self:T(self.lid..string.format("Asset is returned to its legion after being stuck!")) self:ReturnToLegion() else self:T(self.lid..string.format("Despawning group after being stuck!")) self:Despawn() end end end end return holdtime end function FLIGHTGROUP:_UpdateMenu(delay) if delay and delay>0 then self:ScheduleOnce(delay,FLIGHTGROUP._UpdateMenu,self) else local player=self:GetPlayerElement() if player and player.status~=OPSGROUP.ElementStatus.DEAD then if self.verbose>=2 then local text=string.format("Updating MENU: State=%s, ATC=%s [%s]",self:GetState(), self.flightcontrol and self.flightcontrol.airbasename or"None",self.flightcontrol and self.flightcontrol:GetFlightStatus(self)or"Unknown") MESSAGE:New(text,5):ToGroup(self.group) self:T(self.lid..text) end local position=self:GetCoordinate(nil,player.name) local fc={} for airbasename,_flightcontrol in pairs(_DATABASE.FLIGHTCONTROLS)do local flightcontrol=_flightcontrol local coord=flightcontrol:GetCoordinate() local dist=coord:Get2DDistance(position) table.insert(fc,{airbasename=airbasename,dist=dist}) end local function _sort(a,b) return a.dist=1 then local fsmstate=self:GetState() local callsign=self.callsignName and UTILS.GetCallsignName(self.callsignName)or"N/A" local skill=self.skill and tostring(self.skill)or"N/A" local NassetsTot=#self.assets local NassetsInS=self:CountAssets(true) local NassetsQP=0;local NassetsP=0;local NassetsQ=0 if self.legion then NassetsQP,NassetsP,NassetsQ=self.legion:CountAssetsOnMission(nil,self) end local text=string.format("%s [Type=%s, Call=%s, Skill=%s]: Assets Total=%d, Stock=%d, Mission=%d [Active=%d, Queue=%d]", fsmstate,self.aircrafttype,callsign,skill,NassetsTot,NassetsInS,NassetsQP,NassetsP,NassetsQ) self:T(self.lid..text) if self.verbose>=3 and self.weaponData then local text="Weapon Data:" for bit,_weapondata in pairs(self.weaponData)do local weapondata=_weapondata text=text..string.format("\n- Bit=%s: Rmin=%.1f km, Rmax=%.1f km",bit,weapondata.RangeMin/1000,weapondata.RangeMax/1000) end self:I(self.lid..text) end self:_CheckAssetStatus() end if not self:IsStopped()then self:__Status(-60) end end INTEL={ ClassName="INTEL", verbose=0, lid=nil, alias=nil, filterCategory={}, detectionset=nil, Contacts={}, ContactsLost={}, ContactsUnknown={}, Clusters={}, clustercounter=1, clusterradius=15000, clusteranalysis=true, clustermarkers=false, clusterarrows=false, prediction=300, detectStatics=false, DetectAccoustic=false, DetectAccousticRadius=1000, DetectAccousticUnitTypes={Unit.Category.HELICOPTER}, DopplerRadar=false, DopplerMinAltAGL=500, DopplerNotchSin=math.sin(math.rad(15)), DopplerMinSpeedMps=50, DopplerRCS=true, DopplerRadarRangeM=200*1000, } INTEL.Ctype={ GROUND="Ground", NAVAL="Naval", AIRCRAFT="Aircraft", STRUCTURE="Structure" } INTEL.version="0.3.10" INTEL.RCS_Table={ ["A-10C"]=8.0, ["A-10C_2"]=8.0, ["F-14A-135-GR"]=6.0, ["F-14B"]=6.0, ["F-15C"]=5.0, ["F-15E"]=5.0, ["F-15ESE"]=5.0, ["F-16A"]=1.2, ["F-16C bl.50"]=1.2, ["F-16C bl.52d"]=1.2, ["F/A-18C"]=1.5, ["FA-18C_hornet"]=1.5, ["F/A-18C_hornet"]=1.5, ["F/A-18F"]=2.0, ["F-117A"]=0.003, ["F-22A"]=0.0001, ["F-35A"]=0.001, ["B-52H"]=100.0, ["B-1B"]=0.75, ["B-2A"]=0.001, ["AV8BNA"]=2.0, ["Harrier"]=2.0, ["A-4E-C"]=3.0, ["Tornado_IDS"]=5.0, ["Tornado_GR4"]=5.0, ["F-111F"]=5.0, ["F-4E"]=6.0, ["F-5E"]=1.0, ["F-5E-3"]=1.0, ["Mirage-F1CE"]=2.5, ["Mirage-F1EE"]=2.5, ["M-2000C"]=2.0, ["M-2000-5"]=2.0, ["C-17A"]=50.0, ["C-130"]=40.0, ["KC-130"]=40.0, ["KC-135"]=50.0, ["IL-76MD"]=45.0, ["E-3A"]=50.0, ["MiG-15bis"]=4.0, ["MiG-19P"]=3.5, ["MiG-21Bis"]=2.5, ["MiG-23MLD"]=7.0, ["MiG-25PD"]=14.0, ["MiG-25RBT"]=14.0, ["MiG-29A"]=5.0, ["MiG-29S"]=5.0, ["MiG-29G"]=5.0, ["MiG-29K"]=4.0, ["MiG-31"]=14.0, ["Su-7B"]=6.0, ["Su-17M4"]=7.0, ["Su-24M"]=6.0, ["Su-24MR"]=6.0, ["Su-25"]=10.0, ["Su-25T"]=10.0, ["Su-25TM"]=10.0, ["Su-27"]=15.0, ["Su-30"]=15.0, ["Su-33"]=15.0, ["Su-34"]=10.0, ["Su-57"]=0.01, ["Tu-22M3"]=20.0, ["Tu-95MS"]=80.0, ["Tu-142"]=80.0, ["Tu-160"]=12.0, ["An-26B"]=30.0, ["An-30M"]=30.0, ["IL-78M"]=45.0, ["A-50"]=50.0, ["Mi-8MT"]=5.0, ["Mi-8MSB"]=5.0, ["Mi-8MSB-V"]=5.0, ["Mi-8AMTSh"]=5.0, ["Mi-24V"]=3.5, ["Mi-24P"]=3.5, ["Mi-28N"]=2.5, ["Ka-50"]=2.0, ["Ka-52"]=2.0, ["AH-64D"]=3.5, ["AH-64D_BLK_II"]=3.5, ["UH-1H"]=3.0, ["UH-60L"]=3.0, ["CH-47D"]=8.0, ["OH-58D"]=0.8, ["SA342M"]=0.8, ["SA342L"]=0.8, } INTEL.RCS_CategoryDefault={ [Group.Category.AIRPLANE]=5.0, [Group.Category.HELICOPTER]=2.5, } INTEL.RCS_Reference=5.0 INTEL.RCS_NoseOnFraction=0.15 function INTEL:New(DetectionSet,Coalition,Alias) local self=BASE:Inherit(self,FSM:New()) self.detectionset=DetectionSet or SET_GROUP:New() if Coalition and type(Coalition)=="string"then if Coalition=="blue"then Coalition=coalition.side.BLUE elseif Coalition=="red"then Coalition=coalition.side.RED elseif Coalition=="neutral"then Coalition=coalition.side.NEUTRAL else self:E("ERROR: Unknown coalition in INTEL!") end end self.coalition=Coalition or DetectionSet:CountAlive()>0 and DetectionSet:GetFirst():GetCoalition()or nil if self.coalition then local coalitionname=UTILS.GetCoalitionName(self.coalition):lower() self.detectionset:FilterCoalitions(coalitionname) end self.detectionset:FilterOnce() if Alias then self.alias=tostring(Alias) else self.alias="INTEL SPECTRE" if self.coalition then if self.coalition==coalition.side.RED then self.alias="INTEL KGB" elseif self.coalition==coalition.side.BLUE then self.alias="INTEL CIA" end end end self.DetectVisual=true self.DetectOptical=true self.DetectRadar=true self.DetectIRST=true self.DetectRWR=true self.DetectDLINK=true self.statusupdate=-60 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("*","Stop","Stopped") self:AddTransition("*","Detect","*") self:AddTransition("*","NewContact","*") self:AddTransition("*","LostContact","*") self:AddTransition("*","NewCluster","*") self:AddTransition("*","LostCluster","*") self:SetForgetTime() self:SetAcceptZones() self:SetRejectZones() self:SetCorridorZones() self:SetConflictZones() self.DopplerRadar=false return self end function INTEL:SetAcceptZones(AcceptZoneSet) self.acceptzoneset=AcceptZoneSet or SET_ZONE:New() return self end function INTEL:SetAccousticDetectionOn(Radius,UnitCategories) self.DetectAccoustic=true self.DetectAccousticRadius=Radius or 1000 self.DetectAccousticUnitTypes=UnitCategories or{Unit.Category.HELICOPTER} return self end function INTEL:SetAccousticDetectionOff() self.DetectAccoustic=false return self end function INTEL:AddAcceptZone(AcceptZone) self.acceptzoneset:AddZone(AcceptZone) return self end function INTEL:RemoveAcceptZone(AcceptZone) self.acceptzoneset:Remove(AcceptZone:GetName(),true) return self end function INTEL:SetRejectZones(RejectZoneSet) self.rejectzoneset=RejectZoneSet or SET_ZONE:New() return self end function INTEL:AddRejectZone(RejectZone) self.rejectzoneset:AddZone(RejectZone) return self end function INTEL:RemoveRejectZone(RejectZone) self.rejectzoneset:Remove(RejectZone:GetName(),true) return self end function INTEL:SetConflictZones(ConflictZoneSet) self.conflictzoneset=ConflictZoneSet or SET_ZONE:New() return self end function INTEL:AddConflictZone(ConflictZone) self.conflictzoneset:AddZone(ConflictZone) return self end function INTEL:RemoveConflictZone(ConflictZone) self.conflictzoneset:Remove(ConflictZone:GetName(),true) return self end function INTEL:SetCorridorZones(CorridorZoneSet) self.corridorzoneset=CorridorZoneSet or SET_ZONE:New() return self end function INTEL:AddCorridorZone(CorridorZone) self.corridorzoneset:AddZone(CorridorZone) return self end function INTEL:RemoveCorridorZone(CorridorZone) self.corridorzoneset:Remove(CorridorZone:GetName(),true) return self end function INTEL:SetCorridorLimits(Floor,Ceiling) self.corridorceiling=Ceiling or 10000 self.corridorfloor=Floor or 1 return self end function INTEL:SetCorridorLimitsFeet(Floor,Ceiling) local Ceiling=Ceiling or 25000 local Floor=Floor or 15000 self.corridorceiling=UTILS.FeetToMeters(Ceiling) self.corridorfloor=UTILS.FeetToMeters(Floor) return self end function INTEL:SetForgetTime(TimeInterval) return self end function INTEL:SetFilterCategory(Categories) if type(Categories)~="table"then Categories={Categories} end self.filterCategory=Categories local text="Filter categories: " for _,category in pairs(self.filterCategory)do text=text..string.format("%d,",category) end self:T(self.lid..text) return self end function INTEL: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 function INTEL:SetAcceptRange(Range) self.RadarAcceptRange=true self.RadarAcceptRangeKilometers=Range or 75 return self end function INTEL:FilterCategoryGroup(GroupCategories) if type(GroupCategories)~="table"then GroupCategories={GroupCategories} end self.filterCategoryGroup=GroupCategories local text="Filter group categories: " for _,category in pairs(self.filterCategoryGroup)do text=text..string.format("%d,",category) end self:T(self.lid..text) return self end function INTEL:AddAgent(AgentGroup) if AgentGroup:IsInstanceOf("OPSGROUP")then AgentGroup=AgentGroup:GetGroup() end self.detectionset:AddGroup(AgentGroup,true) return self end function INTEL:SetClusterAnalysis(Switch,Markers,Arrows) self.clusteranalysis=Switch self.clustermarkers=Markers self.clusterarrows=Arrows return self end function INTEL:SetDetectStatics(Switch) if Switch and Switch==true then self.detectStatics=true else self.detectStatics=false end return self end function INTEL:SetVerbosity(Verbosity) self.verbose=Verbosity or 2 return self end function INTEL:AddMissionToContact(Contact,Mission) if Mission and Contact then Contact.mission=Mission end return self end function INTEL:AddMissionToCluster(Cluster,Mission) if Mission and Cluster then Cluster.mission=Mission end return self end function INTEL:SetClusterRadius(radius) self.clusterradius=(radius or 15)*1000 return self end function INTEL:SetDetectionTypes(DetectVisual,DetectOptical,DetectRadar,DetectIRST,DetectRWR,DetectDLINK) self.DetectVisual=DetectVisual and true self.DetectOptical=DetectOptical and true self.DetectRadar=DetectRadar and true self.DetectIRST=DetectIRST and true self.DetectRWR=DetectRWR and true self.DetectDLINK=DetectDLINK and true return self end function INTEL:GetContactTable() if self:Is("Running")then return self.Contacts else return nil end end function INTEL:GetClusterTable() if self:Is("Running")and self.clusteranalysis then return self.Clusters else return nil end end function INTEL:GetContactName(Contact) return Contact.groupname end function INTEL:GetContactGroup(Contact) return Contact.group end function INTEL:GetContactThreatlevel(Contact) return Contact.threatlevel end function INTEL:GetContactTypeName(Contact) return Contact.typename end function INTEL:GetContactCategoryName(Contact) return Contact.categoryname end function INTEL:GetContactCoordinate(Contact) return Contact.position end function INTEL:onafterStart(From,Event,To) local text=string.format("Starting INTEL v%s",self.version) self:I(self.lid..text) self:__Status(-math.random(10)) return self end function INTEL:onafterStatus(From,Event,To) local fsmstate=self:GetState() self.ContactsLost={} self.ContactsUnknown={} self:UpdateIntel() local Ncontacts=#self.Contacts local Nclusters=#self.Clusters if self.verbose>=1 then local text=string.format("Status %s [Agents=%s]: Contacts=%d, Clusters=%d, New=%d, Lost=%d",fsmstate,self.detectionset:CountAlive(),Ncontacts,Nclusters,#self.ContactsUnknown,#self.ContactsLost) self:I(self.lid..text) end if self.verbose>=2 and Ncontacts>0 then local text="Detected Contacts:" for _,_contact in pairs(self.Contacts)do local contact=_contact local dT=timer.getAbsTime()-contact.Tdetected text=text..string.format("\n- %s (%s): %s, units=%d, T=%d sec",contact.categoryname,contact.attribute,contact.groupname,contact.isStatic and 1 or contact.group:CountAliveUnits(),dT) if contact.mission then local mission=contact.mission text=text..string.format(" mission name=%s type=%s target=%s",mission.name,mission.type,mission:GetTargetName()or"unknown") end end self:I(self.lid..text) end self:__Status(self.statusupdate) return self end function INTEL:UpdateIntel() local DetectedUnits={} local RecceDetecting={} for _,_group in pairs(self.detectionset.Set or{})do local group=_group if group and group:IsAlive()then for _,_recce in pairs(group:GetUnits())do local recce=_recce if self.DopplerRadar==true then self:GetDetectedUnitsDoppler(recce,DetectedUnits,RecceDetecting,self.DetectVisual,self.DetectOptical,self.DetectRadar,self.DetectIRST,self.DetectRWR,self.DetectDLINK) else self:GetDetectedUnits(recce,DetectedUnits,RecceDetecting,self.DetectVisual,self.DetectOptical,self.DetectRadar,self.DetectIRST,self.DetectRWR,self.DetectDLINK) end end if self.DetectAccoustic then local recce=group:GetFirstUnitAlive() local detectionzone=group:GetProperty("INTEL_DETECT_ACCZONE") if not detectionzone then detectionzone=ZONE_GROUP:New(group.IdentifiableName.."INTEL_DETECT_ACCZONE",group,self.DetectAccousticRadius or 2000) group:SetProperty("INTEL_DETECT_ACCZONE",detectionzone) end if recce and recce:IsGround()then self:GetDetectedUnitsAccoustic(recce,DetectedUnits,RecceDetecting,detectionzone) end end end end local remove={} for unitname,_unit in pairs(DetectedUnits)do local unit=_unit local inconflictzone=false if self.conflictzoneset:Count()>0 then for _,_zone in pairs(self.conflictzoneset.Set)do local zone=_zone if unit:IsInZone(zone)then inconflictzone=true break end end end if self.acceptzoneset:Count()>0 then local inzone=false for _,_zone in pairs(self.acceptzoneset.Set)do local zone=_zone if unit:IsInZone(zone)then inzone=true break end end if(not inzone)and(not inconflictzone)then table.insert(remove,unitname) end end if self.rejectzoneset:Count()>0 then local inzone=false for _,_zone in pairs(self.rejectzoneset.Set)do local zone=_zone if unit:IsInZone(zone)then inzone=true break end end if inzone and(not inconflictzone)then table.insert(remove,unitname) end end if self.corridorzoneset:Count()>0 then self:T("Corridorzone Check for unit "..unit:GetName()) local inzone=false for _,_zone in pairs(self.corridorzoneset.Set)do local zone=_zone if unit:IsInZone(zone)then local corridorfloor=zone:GetProperty("CorridorFloor")or self.corridorfloor local corridorceiling=zone:GetProperty("CorridorCeiling")or self.corridorceiling local debugtext="Corridorzone Check for unit "..unit:GetName().."\n" debugtext=debugtext..string.format("IsAir %s | Alt %dft | Floor %dft | Ceil %dft",tostring(unit:IsAir()),tonumber(UTILS.MetersToFeet(unit:GetAltitude())), tonumber(UTILS.MetersToFeet(corridorfloor)),tonumber(UTILS.MetersToFeet(corridorceiling))) MESSAGE:New(debugtext,15,"INTEL"):ToAllIf(self.verbose>1):ToLogIf(self.verbose>1) if unit:IsAir()and(corridorfloor~=nil or corridorceiling~=nil)then local alt=unit:GetAltitude() if corridorfloor and alt>corridorfloor then inzone=true end if corridorceiling and(inzone==true or corridorfloor==nil)and alt0 and unit:IsInstanceOf("UNIT")then local unitcategory=unit:GetUnitCategory() local keepit=false for _,filtercategory in pairs(self.filterCategory)do if unitcategory==filtercategory then keepit=true break end end if not keepit then self:T(self.lid..string.format("Removing unit %s category=%d",unitname,unit:GetCategory())) table.insert(remove,unitname) end end end for _,unitname in pairs(remove)do DetectedUnits[unitname]=nil end local DetectedGroups={} local DetectedStatics={} local RecceGroups={} for unitname,_unit in pairs(DetectedUnits)do local unit=_unit if unit:IsInstanceOf("UNIT")then local group=unit:GetGroup() if group then local groupname=group:GetName() DetectedGroups[groupname]=group RecceGroups[groupname]=RecceDetecting[unitname] end else if self.detectStatics then DetectedStatics[unitname]=unit RecceGroups[unitname]=RecceDetecting[unitname] end end end self:CreateDetectedItems(DetectedGroups,DetectedStatics,RecceGroups) if self.clusteranalysis then self:PaintPicture() end return self end function INTEL:_UpdateContact(Contact) if Contact.isStatic then else if Contact.group and Contact.group:IsAlive()then Contact.Tdetected=timer.getAbsTime() Contact.position=Contact.group:GetCoordinate() Contact.velocity=Contact.group:GetVelocityVec3() Contact.speed=Contact.group:GetVelocityMPS() if Contact.group:IsAir()then Contact.altitude=Contact.group:GetAltitude() local oldheading=Contact.heading or 1 local newheading=Contact.group:GetHeading() if newheading==0 then newheading=1 end local changeh=math.abs(((oldheading-newheading)+360)%360) Contact.heading=newheading if changeh>10 then Contact.maneuvering=true else Contact.maneuvering=false end local typename=Contact.group:GetTypeName() local base_rcs=INTEL.RCS_Table[typename] if not base_rcs then local cat=Contact.group:GetCategory() base_rcs=(cat and INTEL.RCS_CategoryDefault[cat])or INTEL.RCS_Reference end Contact.rcs=base_rcs end end end return self end function INTEL:_CreateContact(Positionable,RecceName) if Positionable and Positionable:IsAlive()then local item={} if Positionable:IsInstanceOf("GROUP")then local group=Positionable item.groupname=group:GetName() item.group=group item.Tdetected=timer.getAbsTime() item.typename=group:GetTypeName() item.attribute=group:GetAttribute() item.category=group:GetCategory() item.categoryname=group:GetCategoryName() item.threatlevel=group:GetThreatLevel() item.position=group:GetCoordinate() item.velocity=group:GetVelocityVec3() item.speed=group:GetVelocityMPS() item.recce=RecceName item.isground=group:IsGround()or false item.isship=group:IsShip()or false item.isStatic=false if group:IsAir()then item.platform=group:GetNatoReportingName() item.heading=group:GetHeading() item.maneuvering=false item.altitude=group:GetAltitude() else item.platform="Unknown" item.altitude=group:GetAltitude(true) end if item.category==Group.Category.AIRPLANE or item.category==Group.Category.HELICOPTER then item.ctype=INTEL.Ctype.AIRCRAFT elseif item.category==Group.Category.GROUND or item.category==Group.Category.TRAIN then item.ctype=INTEL.Ctype.GROUND elseif item.category==Group.Category.SHIP then item.ctype=INTEL.Ctype.NAVAL end return item elseif Positionable:IsInstanceOf("STATIC")then local static=Positionable item.groupname=static:GetName() item.group=static item.Tdetected=timer.getAbsTime() item.typename=static:GetTypeName()or"Unknown" item.attribute="Static" item.category=3 item.categoryname=static:GetCategoryName()or"Unknown" item.threatlevel=static:GetThreatLevel()or 0 item.position=static:GetCoord() item.velocity=static:GetVelocityVec3() item.speed=0 item.recce=RecceName item.isground=true item.isship=false item.isStatic=true item.ctype=INTEL.Ctype.STRUCTURE return item else self:E(self.lid..string.format("ERROR: object needs to be a GROUP or STATIC!")) end end return nil end function INTEL:CreateDetectedItems(DetectedGroups,DetectedStatics,RecceDetecting) self:F({RecceDetecting=RecceDetecting}) local Tnow=timer.getAbsTime() for groupname,_group in pairs(DetectedGroups)do local group=_group self:KnowObject(group,RecceDetecting[groupname]) end for staticname,_static in pairs(DetectedStatics)do local static=_static self:KnowObject(static,RecceDetecting[staticname]) end for i=#self.Contacts,1,-1 do local item=self.Contacts[i] if self:_CheckContactLost(item)then self:LostContact(item) self:RemoveContact(item) end end return self end function INTEL:GetDetectedUnits(Unit,DetectedUnits,RecceDetecting,DetectVisual,DetectOptical,DetectRadar,DetectIRST,DetectRWR,DetectDLINK) local reccename=Unit:GetName() local detectedtargets=Unit:GetDetectedTargets(DetectVisual,DetectOptical,DetectRadar,DetectIRST,DetectRWR,DetectDLINK) for DetectionObjectID,Detection in pairs(detectedtargets or{})do local DetectedObject=Detection.object if DetectedObject and DetectedObject:isExist()and DetectedObject.id_<50000000 then local status,name=pcall( function() local name=DetectedObject:getName() return name end) if status then local unit=UNIT:FindByName(name) if unit and unit:IsAlive()then local DetectionAccepted=true if self.RadarAcceptRange then local reccecoord=Unit:GetCoord() local coord=unit:GetCoord() local dist=math.floor(coord:Get2DDistance(reccecoord)/1000) if dist>self.RadarAcceptRangeKilometers then DetectionAccepted=false end end if self.RadarBlur then local reccecoord=Unit:GetCoord() local coord=unit:GetCoord() local dist=math.floor(coord:Get2DDistance(reccecoord)/1000) local AGL=unit:GetAltitude(true) local minheight=self.RadarBlurMinHeight or 250 local thresheight=self.RadarBlurThresHeight or 90 local thresblur=self.RadarBlurThresBlur or 85 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) if fblur>thresblur then DetectionAccepted=false end if AGL<=minheight and fheight1 then MESSAGE:New("Radar Blur",10):ToLogIf(self.debug):ToAllIf(self.verbose>1) MESSAGE:New("Unit "..name.." is at "..math.floor(AGL).."m. Distance "..math.floor(dist).."km.",10):ToLogIf(self.debug):ToAllIf(self.verbose>1) MESSAGE:New(string.format("fheight = %d/%d | fblur = %d/%d",fheight,thresheight,fblur,thresblur),10):ToLogIf(self.debug):ToAllIf(self.verbose>1) MESSAGE:New("Detection Accepted = "..tostring(DetectionAccepted),10):ToLogIf(self.debug):ToAllIf(self.verbose>1) end end if DetectionAccepted then DetectedUnits[name]=unit RecceDetecting[name]=reccename self:T(string.format("Unit %s detect by %s",name,reccename)) end else if self.detectStatics then local static=STATIC:FindByName(name,false) if static then DetectedUnits[name]=static RecceDetecting[name]=reccename end end end else self:T(self.lid..string.format("WARNING: Could not get name of detected object ID=%s! Detected by %s",DetectedObject.id_,reccename)) end end end end function INTEL:GetDetectedUnitsAccoustic(Recce,DetectedUnits,RecceDetecting,detectionzone) local othercoalition=self.coalition==coalition.side.BLUE and coalition.side.RED or coalition.side.BLUE self:T("Other coalition = "..othercoalition) if detectionzone then local reccename=Recce:GetName() local DetectAccousticUnitTypes=self.DetectAccousticUnitTypes or{Unit.Category.HELICOPTER} detectionzone:Scan({Object.Category.UNIT},DetectAccousticUnitTypes) local unitset=detectionzone:GetScannedSetUnit(othercoalition) self:T("Accoustic detection found #Units "..unitset:CountAlive()) for _,_unit in pairs(unitset.Set or{})do if _unit and _unit:IsAlive()and _unit:GetCoalition()~=self.coalition then local name=_unit:GetName()or"none" DetectedUnits[name]=_unit RecceDetecting[name]=reccename self:T("Unit name = "..name) end end end end function INTEL:onafterNewContact(From,Event,To,Contact) self:F(self.lid..string.format("NEW contact %s",Contact.groupname)) table.insert(self.ContactsUnknown,Contact) return self end function INTEL:onafterLostContact(From,Event,To,Contact) self:F(self.lid..string.format("LOST contact %s",Contact.groupname)) table.insert(self.ContactsLost,Contact) return self end function INTEL:onafterNewCluster(From,Event,To,Cluster) self:F(self.lid..string.format("NEW cluster #%d [%s] of size %d",Cluster.index,Cluster.ctype,Cluster.size)) self:_AddCluster(Cluster) return self end function INTEL:onafterLostCluster(From,Event,To,Cluster,Mission) local text=self.lid..string.format("LOST cluster #%d [%s]",Cluster.index,Cluster.ctype) if Mission then local mission=Mission text=text..string.format(" mission name=%s type=%s target=%s",mission.name,mission.type,mission:GetTargetName()or"unknown") end self:T(text) return self end function INTEL:KnowObject(Positionable,RecceName,Tdetected) local Tnow=timer.getAbsTime() Tdetected=Tdetected or Tnow if Positionable and Positionable:IsAlive()then if Tdetected>Tnow then self:ScheduleOnce(Tdetected-Tnow,self.KnowObject,self,Positionable,RecceName) else local name=Positionable:GetName() local contact=self:GetContactByName(name) if contact then self:_UpdateContact(contact) else contact=self:_CreateContact(Positionable,RecceName) if contact then self:T(string.format("%s contact detected by %s",contact.groupname,RecceName or"unknown")) self:AddContact(contact) self:NewContact(contact) end end end end return self end function INTEL:GetContactByName(groupname) for i,_contact in pairs(self.Contacts)do local contact=_contact if contact.groupname==groupname then return contact end end return nil end function INTEL:_IsContactKnown(Contact) for i,_contact in pairs(self.Contacts)do local contact=_contact if contact.groupname==Contact.groupname then return true end end return false end function INTEL:AddContact(Contact) if self:_IsContactKnown(Contact)then self:E(self.lid..string.format("WARNING: Contact %s is already in the contact table!",tostring(Contact.groupname))) else self:T(self.lid..string.format("Adding new Contact %s to table",tostring(Contact.groupname))) table.insert(self.Contacts,Contact) end return self end function INTEL:RemoveContact(Contact) for i,_contact in pairs(self.Contacts)do local contact=_contact if contact.groupname==Contact.groupname then table.remove(self.Contacts,i) end end return self end function INTEL:_CheckContactLost(Contact) if Contact.group==nil or not Contact.group:IsAlive()then return true end if Contact.isStatic then return false end local dT=timer.getAbsTime()-Contact.Tdetected local dTforget=nil if Contact.category==Group.Category.GROUND then dTforget=60*60*2 elseif Contact.category==Group.Category.AIRPLANE then dTforget=60*10 elseif Contact.category==Group.Category.HELICOPTER then dTforget=60*20 elseif Contact.category==Group.Category.SHIP then dTforget=60*60 elseif Contact.category==Group.Category.TRAIN then dTforget=60*60 end if dT>dTforget then return true else return false end end function INTEL:PaintPicture() self:F(self.lid.."Painting Picture!") for _,_contact in pairs(self.ContactsLost)do local contact=_contact local cluster=self:GetClusterOfContact(contact) if cluster then self:RemoveContactFromCluster(contact,cluster) end end local ClusterSet={} for _i,_cluster in pairs(self.Clusters)do local cluster=_cluster if cluster.size>0 and self:ClusterCountUnits(cluster)>0 then table.insert(ClusterSet,_cluster) else if cluster.marker then cluster.marker:Remove() end if cluster.markerID then COORDINATE:RemoveMark(cluster.markerID) end self:LostCluster(cluster,cluster.mission) end end self.Clusters=ClusterSet self:_UpdateClusterPositions() for _,_contact in pairs(self.Contacts)do local contact=_contact self:T(string.format("Paint Picture: checking for %s",contact.groupname)) local currentcluster=self:GetClusterOfContact(contact) if currentcluster then local isconnected=self:IsContactConnectedToCluster(contact,currentcluster) if isconnected then else self:RemoveContactFromCluster(contact,currentcluster) local cluster=self:_GetClosestClusterOfContact(contact) if cluster then self:AddContactToCluster(contact,cluster) else local newcluster=self:_CreateClusterFromContact(contact) self:NewCluster(newcluster) end end else self:T(self.lid..string.format("Paint Picture: contact %s has NO current cluster",contact.groupname)) local cluster=self:_GetClosestClusterOfContact(contact) if cluster then self:T(self.lid..string.format("Paint Picture: contact %s has closest cluster #%d",contact.groupname,cluster.index)) self:AddContactToCluster(contact,cluster) else self:T(self.lid..string.format("Paint Picture: contact %s has no closest cluster ==> Create new cluster",contact.groupname)) local newcluster=self:_CreateClusterFromContact(contact) self:NewCluster(newcluster) end end end self:_UpdateClusterPositions() if self.clustermarkers then for _,_cluster in pairs(self.Clusters)do local cluster=_cluster if self.verbose>=1 then BASE:I("Updating cluster marker and future position") end self:UpdateClusterMarker(cluster) self:CalcClusterFuturePosition(cluster,300) end end return self end function INTEL:_CreateCluster() local cluster={} cluster.index=self.clustercounter cluster.coordinate=COORDINATE:New(0,0,0) cluster.threatlevelSum=0 cluster.threatlevelMax=0 cluster.size=0 cluster.Contacts={} cluster.altitude=0 self.clustercounter=self.clustercounter+1 return cluster end function INTEL:_CreateClusterFromContact(Contact) local cluster=self:_CreateCluster() self:T(self.lid..string.format("Created NEW cluster #%d with first contact %s",cluster.index,Contact.groupname)) cluster.coordinate:UpdateFromCoordinate(Contact.position) cluster.ctype=Contact.ctype self:AddContactToCluster(Contact,cluster) return cluster end function INTEL:_AddCluster(Cluster) table.insert(self.Clusters,Cluster) return self end function INTEL:AddContactToCluster(contact,cluster) if contact and cluster then table.insert(cluster.Contacts,contact) cluster.threatlevelSum=cluster.threatlevelSum+contact.threatlevel cluster.size=cluster.size+1 self:GetClusterAltitude(cluster,true) self:T(self.lid..string.format("Adding contact %s to cluster #%d [%s] ==> New size=%d",contact.groupname,cluster.index,cluster.ctype,cluster.size)) end return self end function INTEL:RemoveContactFromCluster(contact,cluster) if contact and cluster then for i=#cluster.Contacts,1,-1 do local Contact=cluster.Contacts[i] if Contact.groupname==contact.groupname then cluster.threatlevelSum=cluster.threatlevelSum-contact.threatlevel cluster.size=cluster.size-1 table.remove(cluster.Contacts,i) self:T(self.lid..string.format("Removing contact %s from cluster #%d ==> New cluster size=%d",contact.groupname,cluster.index,cluster.size)) return self end end end return self end function INTEL:CalcClusterThreatlevelSum(cluster) local threatlevel=0 for _,_contact in pairs(cluster.Contacts)do local contact=_contact threatlevel=threatlevel+contact.threatlevel end cluster.threatlevelSum=threatlevel return threatlevel end function INTEL:CalcClusterThreatlevelAverage(cluster) local threatlevel=self:CalcClusterThreatlevelSum(cluster) threatlevel=threatlevel/cluster.size cluster.threatlevelAve=threatlevel return threatlevel end function INTEL:CalcClusterThreatlevelMax(cluster) local threatlevel=0 for _,_contact in pairs(cluster.Contacts)do local contact=_contact if contact.threatlevel>threatlevel then threatlevel=contact.threatlevel end end cluster.threatlevelMax=threatlevel return threatlevel end function INTEL:CalcClusterDirection(cluster) local direction=0 local speedsum=0 local n=0 for _,_contact in pairs(cluster.Contacts)do local contact=_contact if(not contact.isStatic)and contact.group:IsAlive()then local speed=contact.group:GetVelocityKNOTS() direction=direction+(contact.group:GetHeading()*speed) n=n+1 speedsum=speedsum+speed end end if n==0 then return 0 else return math.floor(direction/(speedsum*n)) end end function INTEL:CalcClusterSpeed(cluster) local velocity=0;local n=0 for _,_contact in pairs(cluster.Contacts)do local contact=_contact if(not contact.isStatic)and contact.group:IsAlive()then velocity=velocity+contact.group:GetVelocityMPS() n=n+1 end end if n==0 then return 0 else return math.floor(velocity/n) end end function INTEL:CalcClusterVelocityVec3(cluster) local v={x=0,y=0,z=0} for _,_contact in pairs(cluster.Contacts)do local contact=_contact if(not contact.isStatic)and contact.group:IsAlive()then local vec=contact.group:GetVelocityVec3() v.x=v.x+vec.x v.y=v.y+vec.y v.z=v.y+vec.z end end return v end function INTEL:CalcClusterFuturePosition(cluster,seconds) local p=self:GetClusterCoordinate(cluster) local v=self:CalcClusterVelocityVec3(cluster) local t=seconds or self.prediction local Vec3={x=p.x+v.x*t,y=p.y+v.y*t,z=p.z+v.z*t} local futureposition=COORDINATE:NewFromVec3(Vec3) if self.clustermarkers and self.clusterarrows then if cluster.markerID then COORDINATE:RemoveMark(cluster.markerID) end cluster.markerID=p:ArrowToAll(futureposition,self.coalition,{1,0,0},1,{1,1,0},0.5,2,true,"Position Calc") end return futureposition end function INTEL:CheckContactInClusters(contact) for _,_cluster in pairs(self.Clusters)do local cluster=_cluster for _,_contact in pairs(cluster.Contacts)do local Contact=_contact if Contact.groupname==contact.groupname then return true end end end return false end function INTEL:IsContactConnectedToCluster(contact,cluster) if contact.ctype~=cluster.ctype then return false,math.huge end for _,_contact in pairs(cluster.Contacts)do local Contact=_contact if Contact.groupname~=contact.groupname or cluster.size==1 then local dist=Contact.position:DistanceFromPointVec2(contact.position) local airprox=true if contact.ctype==INTEL.Ctype.AIRCRAFT then self:T(string.format("Cluster Alt=%d | Contact Alt=%d",cluster.altitude,contact.altitude)) local adist=math.abs(cluster.altitude-contact.altitude) if adist>UTILS.FeetToMeters(10000)then airprox=false end end if distUTILS.FeetToMeters(10000)then airprox=false end end if dist0 then avgalt=newalt/n end Cluster.altitude=avgalt self:T(string.format("Updating Cluster Altitude: %d",Cluster.altitude)) return Cluster.altitude end function INTEL:GetClusterCoordinate(Cluster,Update) local x=0;local y=0;local z=0;local n=0 for _,_contact in pairs(Cluster.Contacts)do local contact=_contact local vec3=nil if Update and contact.group and contact.group:IsAlive()then vec3=contact.group:GetVec3() end if not vec3 then vec3=contact.position 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} Cluster.coordinate:UpdateFromVec3(Vec3) end return Cluster.coordinate end function INTEL:_CheckClusterCoordinateChanged(Cluster,Coordinate,Threshold) Threshold=Threshold or 100 Coordinate=Coordinate or Cluster.coordinate local a=Coordinate:GetVec3() local b=self:GetClusterCoordinate(Cluster,true):GetVec3() local dist=UTILS.VecDist3D(a,b) if dist>Threshold then return true else return false end end function INTEL:_UpdateClusterPositions() for _,_cluster in pairs(self.Clusters)do local cluster=_cluster local coord=self:GetClusterCoordinate(cluster,true) local alt=self:GetClusterAltitude(cluster,true) self:T(self.lid..string.format("Updating Cluster position size: %s",cluster.size)) end return self end function INTEL:ContactCountUnits(Contact) if Contact.isStatic then if Contact.group and Contact.group:IsAlive()then return 1 else return 0 end else if Contact.group then local n=Contact.group:CountAliveUnits() return n else return 0 end end end function INTEL:ClusterCountUnits(Cluster) local unitcount=0 for _,_contact in pairs(Cluster.Contacts)do local contact=_contact unitcount=unitcount+self:ContactCountUnits(contact) end return unitcount end function INTEL:UpdateClusterMarker(cluster) local unitcount=self:ClusterCountUnits(cluster) local text=string.format("Cluster #%d: %s\nSize %d\nUnits %d\nTLsum=%d",cluster.index,cluster.ctype,cluster.size,unitcount,cluster.threatlevelSum) if not cluster.marker then cluster.marker=MARKER:New(cluster.coordinate,text):ToCoalition(self.coalition) else local refresh=false if cluster.marker.text~=text then cluster.marker.text=text refresh=true end local coordchange=self:_CheckClusterCoordinateChanged(cluster,cluster.marker.coordinate) if coordchange then cluster.marker.coordinate:UpdateFromCoordinate(cluster.coordinate) refresh=true end if refresh then cluster.marker:Refresh() end end return self end function INTEL:GetHighestThreatContact(Cluster) local threatlevel=-1 local rcontact=nil for _,_contact in pairs(Cluster.Contacts)do local contact=_contact if contact.threatlevel>threatlevel then threatlevel=contact.threatlevel rcontact=contact end end return rcontact end function INTEL:SetDopplerRadar(MinAltAGL,NotchHalfDeg,MinSpeedMps,RadarRangeKm,RCS) self:T(self.lid.."SetDopplerRadar") self.DopplerRadar=true self.DopplerMinAltAGL=MinAltAGL or 500 self.DopplerNotchSin=math.sin(math.rad(NotchHalfDeg or 15)) self.DopplerMinSpeedMps=MinSpeedMps or 50 self.DopplerRCS=(RCS~=false) self.DopplerRadarRangeM=(RadarRangeKm or 200)*1000 return self end function INTEL:SetDopplerRadarOff() self:T(self.lid.."SetDopplerRadarOff") self.DopplerRadar=false return self end function INTEL:SetTypeRCS(TypeName,RCS_m2) self:T(self.lid.."SetTypeRCS") INTEL.RCS_Table[TypeName]=RCS_m2 return self end function INTEL:_GetAspectRCS(TargetUnit,rpos,spd,tvel) self:T(self.lid.."_GetAspectRCS") local typename=TargetUnit:GetTypeName() local base_rcs=INTEL.RCS_Table[typename] if not base_rcs then local cat=TargetUnit:GetGroup()and TargetUnit:GetGroup():GetCategory() base_rcs=(cat and INTEL.RCS_CategoryDefault[cat])or INTEL.RCS_Reference end if spd<1 then return base_rcs end local tpos=TargetUnit:GetVec3() local dx=rpos.x-tpos.x local dz=rpos.z-tpos.z local d=math.sqrt(dx*dx+dz*dz) if d<1 then return base_rcs end local cos_a=(tvel.x*dx+tvel.z*dz)/(spd*d) local sin2_a=1.0-cos_a*cos_a local f=INTEL.RCS_NoseOnFraction return base_rcs*(f+(1.0-f)*sin2_a) end function INTEL:_CheckDopplerDetection(TargetUnit,RadarUnit) self:T(self.lid.."_CheckDopplerDetection") local spd=TargetUnit:GetVelocityMPS() local rpos=RadarUnit:GetVec3() local tpos=TargetUnit:GetVec3() local tvel=TargetUnit:GetVelocityVec3() local dx=tpos.x-rpos.x local dz=tpos.z-rpos.z local slant=math.sqrt(dx*dx+dz*dz) if spd(agl/self.DopplerMinAltAGL)then return false,"clutter" end end if slant>1 then local nx=dx/slant local nz=dz/slant local vr=tvel.x*nx+tvel.z*nz local vr_frac=math.abs(vr)/math.max(spd,1) if vr_frac1 then local sigma=self:_GetAspectRCS(TargetUnit,rpos,spd,tvel) local scale=(sigma/INTEL.RCS_Reference)^0.25 local R_max=self.DopplerRadarRangeM*scale if slant>R_max then return false,"rcs" end local fade_start=R_max*0.80 if slant>fade_start then local p=(R_max-slant)/(R_max-fade_start) if math.random()>p then return false,"rcs" end end end return true end function INTEL:GetDetectedUnitsDoppler(Unit,DetectedUnits,RecceDetecting,DetectVisual,DetectOptical,DetectRadar,DetectIRST,DetectRWR,DetectDLINK) self:T(self.lid.."GetDetectedUnitsDoppler") self:GetDetectedUnits(Unit,DetectedUnits,RecceDetecting,DetectVisual,DetectOptical,DetectRadar,DetectIRST,DetectRWR,DetectDLINK) if self.DopplerRadar==false then return end if DetectRadar==false then return end local remove={} for name,unit in pairs(DetectedUnits)do if unit:IsInstanceOf("UNIT")and unit:IsAir()then local ok,reason=self:_CheckDopplerDetection(unit,Unit) if not ok then table.insert(remove,name) self:T(string.format("%sDoppler: suppressed %s [%s] by %s",self.lid,name,reason,Unit:GetName())) end end end for _,name in ipairs(remove)do DetectedUnits[name]=nil RecceDetecting[name]=nil end end INTEL_DLINK={ ClassName="INTEL_DLINK", verbose=0, lid=nil, alias=nil, cachetime=120, interval=20, contacts={}, clusters={}, contactcoords={}, } INTEL_DLINK.version="0.0.2" function INTEL_DLINK:New(Intels,Alias,Interval,Cachetime) local self=BASE:Inherit(self,FSM:New()) self.intels=Intels or{} self.contacts={} self.clusters={} self.contactcoords={} if Alias then self.alias=tostring(Alias) else self.alias="SPECTRE" end self.interval=Interval or 20 self.lid=string.format("INTEL_DLINK %s | ",self.alias) self:SetDLinkCacheTime(Cachetime or 120) self:SetStartState("Stopped") self:AddTransition("Stopped","Start","Running") self:AddTransition("*","Collect","*") self:AddTransition("*","Collected","*") self:AddTransition("*","Stop","Stopped") return self end function INTEL_DLINK:AddIntel(Intel) self:T(self.lid.."AddIntel") if Intel then table.insert(self.intels,Intel) end return self end function INTEL_DLINK:onafterStart(From,Event,To) self:T({From,Event,To}) local text=string.format("Version %s started.",self.version) self:I(self.lid..text) self:__Collect(-math.random(1,10)) return self end function INTEL_DLINK:SetDLinkCacheTime(seconds) self.cachetime=math.abs(seconds or 120) self:T(self.lid.."Caching for "..self.cachetime.." seconds.") return self end function INTEL_DLINK:onbeforeCollect(From,Event,To) self:T({From,Event,To}) self:T("Contacts Data Gathering") local newcontacts={} local intels=self.intels for _,_intel in pairs(intels)do _intel=_intel if _intel:Is("Running")then local ctable=_intel:GetContactTable()or{} for _,_contact in pairs(ctable)do local _ID=string.format("%s-%d",_contact.groupname,_contact.Tdetected) self:T(string.format("Adding %s",_ID)) newcontacts[_ID]=_contact end end end self:T("Cleanup") local contacttable={} local coordtable={} local TNow=timer.getAbsTime() local Tcache=self.cachetime for _ind,_contact in pairs(newcontacts)do if TNow-_contact.Tdetected0 then self:ScheduleOnce(Delay,LEGION.RelocateCohort,self,Cohort,Legion,0,NcarriersMin,NcarriersMax,TransportLegions) else if Legion:IsCohort(Cohort.name)then self:E(self.lid..string.format("ERROR: Cohort %s is already part of new legion %s ==> CANNOT Relocate!",Cohort.name,Legion.alias)) return self else table.insert(Legion.cohorts,Cohort) end if not self:IsCohort(Cohort.name)then self:E(self.lid..string.format("ERROR: Cohort %s is NOT part of this legion %s ==> CANNOT Relocate!",Cohort.name,self.alias)) return self end if self.alias==Legion.alias then self:E(self.lid..string.format("ERROR: old legion %s is same as new legion %s ==> CANNOT Relocate!",self.alias,Legion.alias)) return self end Cohort:Relocate() local mission=AUFTRAG:_NewRELOCATECOHORT(Legion,Cohort) if false then mission:_AddAssets(Cohort.assets) self:I(self.lid..string.format("Relocating Cohort %s [nassets=%d] to legion %s",Cohort.name,#Cohort.assets,Legion.alias)) self:MissionAssign(mission,{self}) else mission:AssignCohort(Cohort) mission:SetRequiredAssets(#Cohort.assets) if NcarriersMin and NcarriersMin>0 then mission:SetRequiredTransport(Legion.spawnzone,NcarriersMin,NcarriersMax) end if TransportLegions then for _,legion in pairs(TransportLegions)do mission:AssignTransportLegion(legion) end end mission:SetMissionRange(10000) self:AddMission(mission) end end return self end function LEGION:_GetCohort(CohortName) for _,_cohort in pairs(self.cohorts)do local cohort=_cohort if cohort.name==CohortName then return cohort end end return nil end function LEGION:IsCohort(CohortName) for _,_cohort in pairs(self.cohorts)do local cohort=_cohort if cohort.name==CohortName then return true end end return false end function LEGION:GetName() return self.alias end function LEGION:_GetCohortOfAsset(Asset) local cohort=self:_GetCohort(Asset.squadname) return cohort end function LEGION:IsBrigade() local is=self.ClassName==BRIGADE.ClassName return is end function LEGION:IsAirwing() local is=self.ClassName==AIRWING.ClassName return is end function LEGION:IsFleet() local is=self.ClassName==FLEET.ClassName return is end function LEGION:onafterStart(From,Event,To) self:GetParent(self,LEGION).onafterStart(self,From,Event,To) self:T3(self.lid..string.format("Starting LEGION v%s",LEGION.version)) end function LEGION:CheckMissionQueue() local Nmissions=#self.missionqueue if Nmissions==0 then return nil end for _,_mission in pairs(self.missionqueue)do local mission=_mission if mission:IsNotOver()and mission:IsReadyToCancel()then mission:Cancel() end local TNow=timer.getTime() if mission:IsOver()and mission:IsNotRepeatable()and mission.DeletionTimstamp==nil then mission.DeletionTimstamp=TNow end if mission.DeletionTimstamp~=nil and TNow-mission.DeletionTimstamp>1800 then mission=nil end end if self:IsAirwing()then if self:IsRunwayOperational()==false then return nil end local airboss=self.airboss if airboss then if not airboss:IsIdle()then return nil end end end local function _sort(a,b) local taskA=a local taskB=b return(taskA.prio0 then local N=mission.Nassigned-mission.Ndead if N Reinforce=%s", mission.reinforce,N,mission.Nassigned,mission.Ndead,mission.NassetsMin,tostring(reinforce))) end if(mission:IsQueued(self)or reinforce)and mission:IsReadyToGo()and(mission.importance==nil or mission.importance<=vip)then local recruited,assets,legions=self:RecruitAssetsForMission(mission) if recruited then local EscortAvail=self:RecruitAssetsForEscort(mission,assets) local TransportAvail=true if EscortAvail then local Transport=nil if mission.NcarriersMin then local Legions=mission.transportLegions or{self} TransportAvail,Transport=self:AssignAssetsForTransport(Legions,assets,mission.NcarriersMin,mission.NcarriersMax,mission.transportDeployZone,mission.transportDisembarkZone,mission.carrierCategories,mission.carrierAttributes,mission.carrierProperties) end if TransportAvail and Transport then mission.opstransport=Transport end end if EscortAvail and TransportAvail then self:MissionRequest(mission,assets) if reinforce then mission.reinforce=mission.reinforce-#assets self:T(self.lid..string.format("Reinforced with N=%d Nreinforce=%d",#assets,mission.reinforce)) end return true else LEGION.UnRecruitAssets(assets,mission) end end end end return nil end function LEGION:CheckTransportQueue() local Ntransports=#self.transportqueue if Ntransports==0 then return nil end local function _sort(a,b) local taskA=a local taskB=b return(taskA.prio0 then for i,_asset in pairs(Assetlist)do local asset=_asset asset.requested=true if asset.spawned then asset.requested=false end asset.isReserved=false if Mission.missionTask then asset.missionTask=Mission.missionTask end if Mission.type==AUFTRAG.Type.ALERT5 then asset.takeoffType=COORDINATE.WaypointType.TakeOffParking end Mission:AddAsset(asset) end local assignment=string.format("Mission-%d",Mission.auftragsnummer) local request=self:_AddRequest(WAREHOUSE.Descriptor.ASSETLIST,Assetlist,#Assetlist,Mission.prio,assignment) self:T(self.lid..string.format("Added request=%d for Nasssets=%d",request.uid,#Assetlist)) Mission:_SetRequestID(self,self.queueid) self:T(self.lid..string.format("Mission %s [%s] got Request ID=%d",Mission:GetName(),Mission:GetType(),self.queueid)) if request then if self:IsShip()then self:T(self.lid.."Warehouse physical structure is SHIP. Requestes assets will be late activated!") request.lateActivation=true end end end end function LEGION:onafterTransportAssign(From,Event,To,Transport,Legions) for _,_Legion in pairs(Legions)do local Legion=_Legion self:T(self.lid..string.format("Assigning transport %d to legion %s",Transport.uid,Legion.alias)) Legion:AddOpsTransport(Transport) Legion:TransportRequest(Transport) end end function LEGION:onafterTransportRequest(From,Event,To,OpsTransport) local AssetList={} for i,_asset in pairs(OpsTransport.assets)do local asset=_asset if asset.wid==self.uid then asset.requested=true asset.isReserved=false asset.missionTask=ENUMS.MissionTask.TRANSPORT table.insert(AssetList,asset) end end if#AssetList>0 then OpsTransport:Requested() OpsTransport:SetLegionStatus(self,OPSTRANSPORT.Status.REQUESTED) local assignment=string.format("Transport-%d",OpsTransport.uid) self:_AddRequest(WAREHOUSE.Descriptor.ASSETLIST,AssetList,#AssetList,OpsTransport.prio,assignment) OpsTransport.requestID[self.alias]=self.queueid end end function LEGION:onafterTransportCancel(From,Event,To,Transport) self:T(self.lid..string.format("Cancel transport UID=%d",Transport.uid)) Transport:SetLegionStatus(self,OPSTRANSPORT.Status.CANCELLED) for i=#Transport.assets,1,-1 do local asset=Transport.assets[i] if asset.wid==self.uid then local opsgroup=asset.flightgroup if opsgroup then opsgroup:TransportCancel(Transport) end local cargos=Transport:GetCargoOpsGroups(false) for _,_cargo in pairs(cargos)do local cargo=_cargo cargo:_DelMyLift(Transport) local legion=cargo.legion if legion then legion:T(self.lid..string.format("Adding cargo group %s back to legion",cargo:GetName())) legion:__AddAsset(0.1,cargo.group,1) end end Transport:DelAsset(asset) asset.requested=nil asset.isReserved=nil end end if Transport.requestID[self.alias]then self:_DeleteQueueItemByID(Transport.requestID[self.alias],self.queue) end end function LEGION:onafterMissionCancel(From,Event,To,Mission) self:T(self.lid..string.format("Cancel mission %s",Mission.name)) Mission:SetLegionStatus(self,AUFTRAG.Status.CANCELLED) for i=#Mission.assets,1,-1 do local asset=Mission.assets[i] if asset.wid==self.uid then local opsgroup=asset.flightgroup if opsgroup then opsgroup:MissionCancel(Mission) end Mission:DelAsset(asset) asset.requested=nil asset.isReserved=nil end end local requestID=Mission:_GetRequestID(self) if requestID then self:_DeleteQueueItemByID(requestID,self.queue) end end function LEGION:onafterOpsOnMission(From,Event,To,OpsGroup,Mission) self:T2(self.lid..string.format("Group %s on mission %s [%s]",OpsGroup:GetName(),Mission:GetName(),Mission:GetType())) if self:IsAirwing()then self:FlightOnMission(OpsGroup,Mission) elseif self:IsBrigade()then self:ArmyOnMission(OpsGroup,Mission) else self:NavyOnMission(OpsGroup,Mission) end if self:IsBrigade()and self:IsShip()then OpsGroup:PauseMission() self.warehouseOpsGroup:Load(OpsGroup,self.warehouseOpsElement) end if self.chief then self.chief:OpsOnMission(OpsGroup,Mission) end if self.commander then self.commander:OpsOnMission(OpsGroup,Mission) end end function LEGION:onafterNewAsset(From,Event,To,asset,assignment) self:GetParent(self,LEGION).onafterNewAsset(self,From,Event,To,asset,assignment) local text=string.format("New asset %s with assignment %s and request assignment %s",asset.spawngroupname,tostring(asset.assignment),tostring(assignment)) self:T(self.lid..text) local cohort=self:_GetCohort(asset.assignment) if cohort then if asset.assignment==assignment then local nunits=#asset.template.units local text=string.format("Adding asset to cohort %s: assignment=%s, type=%s, attribute=%s, nunits=%d ngroup=%s",cohort.name,assignment,asset.unittype,asset.attribute,nunits,tostring(cohort.ngrouping)) self:T(self.lid..text) if cohort.ngrouping then local template=asset.template local N=math.max(#template.units,cohort.ngrouping) asset.weight=0 asset.cargobaytot=0 for i=1,N do local unit=template.units[i] if i>nunits then table.insert(template.units,UTILS.DeepCopy(template.units[1])) asset.cargobaytot=asset.cargobaytot+asset.cargobay[1] asset.weight=asset.weight+asset.weights[1] template.units[i].x=template.units[1].x+5*(i-nunits) template.units[i].y=template.units[1].y+5*(i-nunits) else if i<=cohort.ngrouping then asset.weight=asset.weight+asset.weights[i] asset.cargobaytot=asset.cargobaytot+asset.cargobay[i] end end if i>cohort.ngrouping then template.units[i]=nil end end asset.nunits=cohort.ngrouping self:T(self.lid..string.format("After regrouping: Nunits=%d, weight=%.1f cargobaytot=%.1f kg",#asset.template.units,asset.weight,asset.cargobaytot)) end asset.takeoffType=cohort.takeoffType~=nil and cohort.takeoffType or self.takeoffType asset.parkingIDs=cohort.parkingIDs cohort:GetCallsign(asset) cohort:GetModex(asset) asset.spawngroupname=string.format("%s_AID-%d",cohort.name,asset.uid) cohort:AddAsset(asset) else self:T(self.lid..string.format("Asset returned to legion ==> calling LegionAssetReturned event")) asset.takeoffType=cohort.takeoffType self:LegionAssetReturned(cohort,asset) end end end function LEGION:onafterLegionAssetReturned(From,Event,To,Cohort,Asset) self:T(self.lid..string.format("Asset %s from Cohort %s returned! asset.assignment=\"%s\"",Asset.spawngroupname,Cohort.name,tostring(Asset.assignment))) if Asset.flightgroup and not Asset.flightgroup:IsStopped()then Asset.flightgroup:Stop() end if Asset.flightgroup:IsFlightgroup()then self:ReturnPayloadFromAsset(Asset) end if Asset.tacan then Cohort:ReturnTacan(Asset.tacan) end Asset.Treturned=timer.getAbsTime() end function LEGION:onafterAssetSpawned(From,Event,To,group,asset,request) self:T({From,Event,To,group:GetName(),asset.assignment,request.assignment}) self:GetParent(self,LEGION).onafterAssetSpawned(self,From,Event,To,group,asset,request) local cohort=self:_GetCohortOfAsset(asset) if cohort then self:T(self.lid..string.format("Cohort asset spawned %s",asset.spawngroupname)) local flightgroup=self:_CreateFlightGroup(asset) asset.flightgroup=flightgroup asset.requested=nil asset.Treturned=nil local Tacan=cohort:FetchTacan() if Tacan then asset.tacan=Tacan flightgroup:SwitchTACAN(Tacan,Morse,UnitName,Band) end local radioFreq,radioModu=cohort:GetRadio() if radioFreq then flightgroup:SwitchRadio(radioFreq,radioModu) end if cohort.fuellow then flightgroup:SetFuelLowThreshold(cohort.fuellow) end if cohort.fuellowRefuel then flightgroup:SetFuelLowRefuel(cohort.fuellowRefuel) end local assignment=request.assignment if self:IsFleet()then flightgroup:SetPathfinding(self.pathfinding) end if string.find(assignment,"Mission-")then local uid=UTILS.Split(assignment,"-")[2] local mission=self:GetMissionByID(uid) local despawnLanding=cohort.despawnAfterLanding~=nil and cohort.despawnAfterLanding or self.despawnAfterLanding if despawnLanding then flightgroup:SetDespawnAfterLanding() end local despawnHolding=cohort.despawnAfterHolding~=nil and cohort.despawnAfterHolding or self.despawnAfterHolding if despawnHolding then flightgroup:SetDespawnAfterHolding() end if mission then if Tacan then end flightgroup:AddMission(mission) if self:IsBrigade()or self:IsFleet()then flightgroup:SetReturnOnOutOfAmmo() end self:__OpsOnMission(5,flightgroup,mission) else if Tacan then end end local chief=self.chief or(self.commander and self.commander.chief or nil) if chief then self:T(self.lid..string.format("Adding group %s to agents of CHIEF",group:GetName())) chief.detectionset:AddGroup(asset.flightgroup.group) end elseif string.find(assignment,"Transport-")then local uid=UTILS.Split(assignment,"-")[2] local transport=self:GetTransportByID(uid) if transport then flightgroup:AddOpsTransport(transport) end end end end function LEGION:onafterAssetDead(From,Event,To,asset,request) self:GetParent(self,LEGION).onafterAssetDead(self,From,Event,To,asset,request) if self.commander and self.commander.chief then self.commander.chief.detectionset:RemoveGroupsByName({asset.spawngroupname}) end self:DelAsset(asset) end function LEGION:onafterDestroyed(From,Event,To) self:T(self.lid.."Legion warehouse destroyed!") for _,_mission in pairs(self.missionqueue)do local mission=_mission mission:Cancel() end for _,_cohort in pairs(self.cohorts)do local cohort=_cohort cohort:Stop() end self:GetParent(self,LEGION).onafterDestroyed(self,From,Event,To) end function LEGION:onafterRequest(From,Event,To,Request) if Request.toself then local assets=Request.cargoassets local Mission=self:GetMissionByID(Request.assignment) if Mission and assets then for _,_asset in pairs(assets)do local asset=_asset end end end self:GetParent(self,LEGION).onafterRequest(self,From,Event,To,Request) end function LEGION:onafterSelfRequest(From,Event,To,groupset,request) self:GetParent(self,LEGION).onafterSelfRequest(self,From,Event,To,groupset,request) local mission=self:GetMissionByID(request.assignment) for _,_asset in pairs(request.assets)do local asset=_asset end for _,_group in pairs(groupset:GetSet())do local group=_group end end function LEGION:onafterRequestSpawned(From,Event,To,Request,CargoGroupSet,TransportGroupSet) self:GetParent(self,LEGION).onafterRequestSpawned(self,From,Event,To,Request,CargoGroupSet,TransportGroupSet) end function LEGION:onafterCaptured(From,Event,To,Coalition,Country) self:GetParent(self,LEGION).onafterCaptured(self,From,Event,To,Coalition,Country) if self.chief then self.chief.commander:LegionLost(self,Coalition,Country) self.chief:LegionLost(self,Coalition,Country) self.chief:RemoveLegion(self) elseif self.commander then self.commander:LegionLost(self,Coalition,Country) self.commander:RemoveLegion(self) end end function LEGION:_CreateFlightGroup(asset) local opsgroup=nil if self:IsAirwing()then opsgroup=FLIGHTGROUP:New(asset.spawngroupname) elseif self:IsBrigade()then opsgroup=ARMYGROUP:New(asset.spawngroupname) opsgroup:SetValidateAndRepositionGroundUnits(self.ValidateAndRepositionGroundUnits) elseif self:IsFleet()then opsgroup=NAVYGROUP:New(asset.spawngroupname) else self:E(self.lid.."ERROR: not airwing or brigade!") end opsgroup:_SetLegion(self) opsgroup.cohort=self:_GetCohortOfAsset(asset) opsgroup.homebase=self.airbase opsgroup.destbase=self.airbase opsgroup.homezone=self.spawnzone if opsgroup.cohort.weaponData then local text="Weapon data for group:" opsgroup.weaponData=opsgroup.weaponData or{} for bittype,_weapondata in pairs(opsgroup.cohort.weaponData)do local weapondata=_weapondata opsgroup.weaponData[bittype]=UTILS.DeepCopy(weapondata) text=text..string.format("\n- Bit=%s: Rmin=%.1f km, Rmax=%.1f km",bittype,weapondata.RangeMin/1000,weapondata.RangeMax/1000) end self:T3(self.lid..text) end return opsgroup end function LEGION:_TacticalOverview() if self.tacview then local NassetsTotal=self:CountAssets(nil) local NassetsStock=self:CountAssets(true) local NassetsActiv=self:CountAssets(false) local NmissionsTotal=#self.missionqueue local NmissionsRunni=self:CountMissionsInQueue() local text=string.format("Tactical Overview %s\n",self.alias) text=text..string.format("===================================\n") text=text..string.format("Assets: %d [Active=%d, Stock=%d]\n",NassetsTotal,NassetsActiv,NassetsStock) text=text..string.format("Missions: %d [Running=%d]\n",NmissionsTotal,NmissionsRunni) for _,mtype in pairs(AUFTRAG.Type)do local n=self:CountMissionsInQueue(mtype) if n>0 then local N=self:CountMissionsInQueue(mtype,true) text=text..string.format(" - %s: %d [Running=%d]\n",mtype,n,N) end end local Ntransports=#self.transportqueue if Ntransports>0 then text=text..string.format("Transports: %d\n",Ntransports) for _,_transport in pairs(self.transportqueue)do local transport=_transport text=text..string.format(" - %s",transport:GetState()) end end MESSAGE:New(text,60,nil,true):ToCoalition(self:GetCoalition()) end end function LEGION:IsAssetOnMission(asset,MissionTypes) if MissionTypes then if type(MissionTypes)~="table"then MissionTypes={MissionTypes} end else MissionTypes=AUFTRAG.Type end if asset.flightgroup and asset.flightgroup:IsAlive()then for _,_mission in pairs(asset.flightgroup.missionqueue or{})do local mission=_mission if mission:IsNotOver()then local status=mission:GetGroupStatus(asset.flightgroup) if(status==AUFTRAG.GroupStatus.STARTED or status==AUFTRAG.GroupStatus.EXECUTING)and AUFTRAG.CheckMissionType(mission.type,MissionTypes)then return true end end end end return false end function LEGION:GetAssetCurrentMission(asset) if asset.flightgroup then return asset.flightgroup:GetMissionCurrent() end return nil end function LEGION:CountPayloadsInStock(MissionTypes,UnitTypes,Payloads) if MissionTypes then if type(MissionTypes)=="string"then MissionTypes={MissionTypes} end end if UnitTypes then if type(UnitTypes)=="string"then UnitTypes={UnitTypes} end end local function _checkUnitTypes(payload) if UnitTypes then for _,unittype in pairs(UnitTypes)do if unittype==payload.aircrafttype then return true end end else return true end return false end local function _checkPayloads(payload) if Payloads then for _,Payload in pairs(Payloads)do if Payload.uid==payload.uid then return true end end else return nil end return false end local n=0 for _,_payload in pairs(self.payloads or{})do local payload=_payload for _,MissionType in pairs(MissionTypes)do local specialpayload=_checkPayloads(payload) local compatible=AUFTRAG.CheckMissionCapability(MissionType,payload.capabilities) local goforit=specialpayload or(specialpayload==nil and compatible) if goforit and _checkUnitTypes(payload)then if payload.unlimited then return 999 else n=n+payload.navail end end end end return n end function LEGION:CountMissionsInQueue(MissionTypes,OnlyRunning) MissionTypes=MissionTypes or AUFTRAG.Type local N=0 for _,_mission in pairs(self.missionqueue)do local mission=_mission if(not OnlyRunning)or(mission.statusLegion~=AUFTRAG.Status.PLANNED)then if mission:IsNotOver()and AUFTRAG.CheckMissionType(mission.type,MissionTypes)then N=N+1 end end end return N end function LEGION:CountAssets(InStock,MissionTypes,Attributes) local N=0 for _,_cohort in pairs(self.cohorts)do local cohort=_cohort N=N+cohort:CountAssets(InStock,MissionTypes,Attributes) end return N end function LEGION:GetOpsGroups(MissionTypes,Attributes) local setLegion=SET_OPSGROUP:New() for _,_cohort in pairs(self.cohorts)do local cohort=_cohort local setCohort=cohort:GetOpsGroups(MissionTypes,Attributes) self:T2(self.lid..string.format("Found %d opsgroups of cohort %s",setCohort:Count(),cohort.name)) setLegion:AddSet(setCohort) end return setLegion end function LEGION:CountAssetsWithPayloadsInStock(Payloads,MissionTypes,Attributes) local N=0 local Npayloads={} for _,_cohort in pairs(self.cohorts)do local cohort=_cohort if Npayloads[cohort.aircrafttype]==nil then Npayloads[cohort.aircrafttype]=self:CountPayloadsInStock(MissionTypes,cohort.aircrafttype,Payloads) self:T3(self.lid..string.format("Got Npayloads=%d for type=%s",Npayloads[cohort.aircrafttype],cohort.aircrafttype)) end end for _,_cohort in pairs(self.cohorts)do local cohort=_cohort local n=cohort:CountAssets(true,MissionTypes,Attributes) local p=Npayloads[cohort.aircrafttype]or 0 local m=math.min(n,p) N=N+m Npayloads[cohort.aircrafttype]=Npayloads[cohort.aircrafttype]-m end return N end function LEGION:CountAssetsOnMission(MissionTypes,Cohort) local Nq=0 local Np=0 for _,_mission in pairs(self.missionqueue)do local mission=_mission if AUFTRAG.CheckMissionType(mission.type,MissionTypes or AUFTRAG.Type)then for _,_asset in pairs(mission.assets or{})do local asset=_asset if asset.wid==self.uid then if Cohort==nil or Cohort.name==asset.squadname then local request,isqueued=self:GetRequestByID(mission.requestID[self.alias]) if isqueued then Nq=Nq+1 else Np=Np+1 end end end end end end return Np+Nq,Np,Nq end function LEGION:GetAssetsOnMission(MissionTypes) local assets={} local Np=0 for _,_mission in pairs(self.missionqueue)do local mission=_mission if AUFTRAG.CheckMissionType(mission.type,MissionTypes)then for _,_asset in pairs(mission.assets or{})do local asset=_asset if asset.wid==self.uid then table.insert(assets,asset) end end end end return assets end function LEGION:GetAircraftTypes(onlyactive,cohorts) local unittypes={} for _,_cohort in pairs(cohorts or self.cohorts)do local cohort=_cohort if(not onlyactive)or cohort:IsOnDuty()then local gotit=false for _,unittype in pairs(unittypes)do if cohort.aircrafttype==unittype then gotit=true break end end if not gotit then table.insert(unittypes,cohort.aircrafttype) end end end return unittypes end function LEGION:_CountPayloads(MissionType,Cohorts,Payloads) local Npayloads={} for _,_cohort in pairs(Cohorts)do local cohort=_cohort if Npayloads[cohort.aircrafttype]==nil then Npayloads[cohort.aircrafttype]=cohort.legion:IsAirwing()and self:CountPayloadsInStock(MissionType,cohort.aircrafttype,Payloads)or 999 self:T2(self.lid..string.format("Got N=%d payloads for mission type=%s and unit type=%s",Npayloads[cohort.aircrafttype],MissionType,cohort.aircrafttype)) end end return Npayloads end function LEGION:RecruitAssetsForMission(Mission) local NreqMin,NreqMax=Mission:GetRequiredAssets() local TargetVec2=Mission:GetTargetVec2() local Payloads=Mission.payloads local MaxWeight=nil if Mission.NcarriersMin then local legions={self} local cohorts=self.cohorts if Mission.transportLegions or Mission.transportCohorts then legions=Mission.transportLegions cohorts=Mission.transportCohorts end local Cohorts=LEGION._GetCohorts(legions,cohorts) local transportcohorts={} for _,_cohort in pairs(Cohorts)do local cohort=_cohort local can=LEGION._CohortCan(cohort,AUFTRAG.Type.OPSTRANSPORT,Mission.carrierCategories,Mission.carrierAttributes,Mission.carrierProperties,nil,TargetVec2) if can and(MaxWeight==nil or cohort.cargobayLimit>MaxWeight)then MaxWeight=cohort.cargobayLimit end end self:T(self.lid..string.format("Largest cargo bay available=%.1f",MaxWeight or 0)) end local legions={self} local cohorts=self.cohorts if Mission.specialLegions or Mission.specialCohorts then legions=Mission.specialLegions cohorts=Mission.specialCohorts end local Cohorts=LEGION._GetCohorts(legions,cohorts,Operation,OpsQueue) local recruited,assets,legions=LEGION.RecruitCohortAssets(Cohorts,Mission.type,Mission.alert5MissionType,NreqMin,NreqMax,TargetVec2,Payloads, Mission.engageRange,Mission.refuelSystem,nil,nil,MaxWeight,nil,Mission.attributes,Mission.properties,{Mission.engageWeaponType}) return recruited,assets,legions end function LEGION:RecruitAssetsForTransport(Transport) local cargoOpsGroups=Transport:GetCargoOpsGroups(false) local weightGroup=0 local TotalWeight=nil if#cargoOpsGroups>0 then TotalWeight=0 for _,_opsgroup in pairs(cargoOpsGroups)do local opsgroup=_opsgroup local weight=opsgroup:GetWeightTotal() if weight>weightGroup then weightGroup=weight end TotalWeight=TotalWeight+weight end else return false end local TargetVec2=Transport:GetDeployZone():GetVec2() local NreqMin,NreqMax=Transport:GetRequiredCarriers() local recruited,assets,legions=LEGION.RecruitCohortAssets(self.cohorts,AUFTRAG.Type.OPSTRANSPORT,nil,NreqMin,NreqMax,TargetVec2,nil,nil,nil,weightGroup,TotalWeight) return recruited,assets,legions end function LEGION:RecruitAssetsForEscort(Mission,Assets) if Mission.NescortMin and Mission.NescortMax and(Mission.NescortMin>0 or Mission.NescortMax>0)then self:T(self.lid..string.format("Requested escort for mission %s [%s]. Required assets=%d-%d",Mission:GetName(),Mission:GetType(),Mission.NescortMin,Mission.NescortMax)) local Cohorts={} for _,_legion in pairs(Mission.escortLegions or{})do local legion=_legion for _,_cohort in pairs(legion.cohorts)do local cohort=_cohort table.insert(Cohorts,cohort) end end for _,_cohort in pairs(Mission.escortCohorts or{})do local cohort=_cohort table.insert(Cohorts,cohort) end if#Cohorts==0 then Cohorts=self.cohorts end local assigned=LEGION.AssignAssetsForEscort(self,Cohorts,Assets,Mission.NescortMin,Mission.NescortMax,Mission.escortMissionType,Mission.escortTargetTypes) return assigned end return true end function LEGION._GetCohorts(Legions,Cohorts,Operation,OpsQueue) OpsQueue=OpsQueue or{} local function CheckOperation(LegionOrCohort) if#OpsQueue==0 then return true end local isAvail=true if Operation then isAvail=false end for _,_operation in pairs(OpsQueue)do local operation=_operation local isOps=operation:IsAssignedCohortOrLegion(LegionOrCohort) if isOps and operation:IsRunning()then isAvail=false if Operation==nil then return false else if Operation.uid==operation.uid then return true end end end end return isAvail end local cohorts={} if(Legions and#Legions>0)or(Cohorts and#Cohorts>0)then for _,_legion in pairs(Legions or{})do local legion=_legion local Runway=true if legion:IsAirwing()then Runway=legion:IsRunwayOperational()and legion.airbase and legion.airbase:GetCoalition()==legion:GetCoalition() end if legion:IsRunning()and Runway then for _,_cohort in pairs(legion.cohorts)do local cohort=_cohort if(CheckOperation(cohort.legion)or CheckOperation(cohort))and not UTILS.IsInTable(cohorts,cohort,"name")then table.insert(cohorts,cohort) end end end end for _,_cohort in pairs(Cohorts or{})do local cohort=_cohort if CheckOperation(cohort)and not UTILS.IsInTable(cohorts,cohort,"name")then table.insert(cohorts,cohort) end end end return cohorts end function LEGION._CohortCan(Cohort,MissionType,Categories,Attributes,Properties,WeaponTypes,TargetVec2,RangeMax,RefuelSystem,CargoWeight,MaxWeight,RangeMin) RangeMin=RangeMin or-1 local function CheckCategory(_cohort) local cohort=_cohort if Categories and#Categories>0 then for _,category in pairs(Categories)do if category==cohort.category then return true end end else return true end end local function CheckAttribute(_cohort) local cohort=_cohort if Attributes and#Attributes>0 then for _,attribute in pairs(Attributes)do if attribute==cohort.attribute then return true end end else return true end end local function CheckProperty(_cohort) local cohort=_cohort if Properties and#Properties>0 then for _,Property in pairs(Properties)do for property,value in pairs(cohort.properties)do if Property==property then return true end end end else return true end end local function CheckWeapon(_cohort) local cohort=_cohort if WeaponTypes and#WeaponTypes>0 then for _,WeaponType in pairs(WeaponTypes)do if WeaponType==ENUMS.WeaponFlag.Auto then return true else for _,_weaponData in pairs(cohort.weaponData or{})do local weaponData=_weaponData if weaponData.BitType==WeaponType then return true end end end end return false else return true end end local function CheckRange(_cohort) local cohort=_cohort local TargetDistance=TargetVec2 and UTILS.VecDist2D(TargetVec2,cohort.legion:GetVec2())or 0 local Rmax=cohort:GetMissionRange(WeaponTypes) local RangeMax=RangeMax or 0 local InRange=(RangeMax and math.max(RangeMax,Rmax)or Rmax)>=TargetDistance and TargetDistance>RangeMin return InRange end local function CheckRefueling(_cohort) local cohort=_cohort if RefuelSystem then if cohort.tankerSystem then return RefuelSystem==cohort.tankerSystem else return false end else return true end end local function CheckCargoWeight(_cohort) local cohort=_cohort if CargoWeight~=nil then return cohort.cargobayLimit>=CargoWeight else return true end end local function CheckMaxWeight(_cohort) local cohort=_cohort if MaxWeight~=nil then cohort:T(string.format("Cohort weight=%.1f | max weight=%.1f",cohort.weightAsset,MaxWeight)) return cohort.weightAsset<=MaxWeight else return true end end local can=AUFTRAG.CheckMissionCapability(MissionType,Cohort.missiontypes) if can then can=CheckCategory(Cohort) else Cohort:T(Cohort.lid..string.format("Cohort %s cannot because of mission types",Cohort.name)) return false end if can then if MissionType==AUFTRAG.Type.RELOCATECOHORT then can=Cohort:IsRelocating() else can=Cohort:IsOnDuty() end else Cohort:T(Cohort.lid..string.format("Cohort %s cannot because of category",Cohort.name)) return false end if can then can=CheckAttribute(Cohort) else Cohort:T(Cohort.lid..string.format("Cohort %s cannot because of readyiness",Cohort.name)) return false end if can then can=CheckProperty(Cohort) else Cohort:T(Cohort.lid..string.format("Cohort %s cannot because of attribute",Cohort.name)) return false end if can then can=CheckWeapon(Cohort) else Cohort:T(Cohort.lid..string.format("Cohort %s cannot because of property",Cohort.name)) return false end if can then can=CheckRange(Cohort) else Cohort:T(Cohort.lid..string.format("Cohort %s cannot because of weapon type",Cohort.name)) return false end if can then can=CheckRefueling(Cohort) else Cohort:T(Cohort.lid..string.format("Cohort %s cannot because of range",Cohort.name)) return false end if can then can=CheckCargoWeight(Cohort) else Cohort:T(Cohort.lid..string.format("Cohort %s cannot because of refueling system",Cohort.name)) return false end if can then can=CheckMaxWeight(Cohort) else Cohort:T(Cohort.lid..string.format("Cohort %s cannot because of cargo weight",Cohort.name)) return false end if can then return true else Cohort:T(Cohort.lid..string.format("Cohort %s cannot because of max weight",Cohort.name)) return false end return nil end function LEGION.RecruitCohortAssets(Cohorts,MissionTypeRecruit,MissionTypeOpt,NreqMin,NreqMax,TargetVec2,Payloads,RangeMax,RefuelSystem,CargoWeight,TotalWeight,MaxWeight,Categories,Attributes,Properties,WeaponTypes,RangeMin) local Assets={} local Legions={} if MissionTypeOpt==nil then MissionTypeOpt=MissionTypeRecruit end for _,_cohort in pairs(Cohorts)do local cohort=_cohort local can=LEGION._CohortCan(cohort,MissionTypeRecruit,Categories,Attributes,Properties,WeaponTypes,TargetVec2,RangeMax,RefuelSystem,CargoWeight,MaxWeight,RangeMin) if can then local assets,npayloads=cohort:RecruitAssets(MissionTypeRecruit,999) for _,asset in pairs(assets)do table.insert(Assets,asset) end end end if#Assets==0 then return false,{},{} end LEGION._OptimizeAssetSelection(Assets,MissionTypeOpt,TargetVec2,false,TotalWeight) for _,_asset in pairs(Assets)do local asset=_asset if asset.legion:IsAirwing()and not asset.payload then asset.payload=asset.legion:FetchPayloadFromStock(asset.unittype,MissionTypeOpt,Payloads) end end for i=#Assets,1,-1 do local asset=Assets[i] if asset.legion:IsAirwing()and not asset.payload then table.remove(Assets,i) end end LEGION._OptimizeAssetSelection(Assets,MissionTypeOpt,TargetVec2,true,TotalWeight) local Nassets=math.min(#Assets,NreqMax) if#Assets>=NreqMin then local cargobay=0 for i=1,Nassets do local asset=Assets[i] asset.isReserved=true Legions[asset.legion.alias]=asset.legion if TotalWeight then local N=math.floor(asset.cargobaytot/asset.nunits/CargoWeight)*asset.nunits cargobay=cargobay+N*CargoWeight if cargobay>=TotalWeight then Nassets=i break end end end for i=#Assets,Nassets+1,-1 do local asset=Assets[i] if asset.legion:IsAirwing()and not asset.spawned then asset.legion:T2(asset.legion.lid..string.format("Returning payload from asset %s",asset.spawngroupname)) asset.legion:ReturnPayloadFromAsset(asset) end table.remove(Assets,i) end return true,Assets,Legions else for i=1,#Assets do local asset=Assets[i] if asset.legion:IsAirwing()and not asset.spawned then asset.legion:T2(asset.legion.lid..string.format("Returning payload from asset %s",asset.spawngroupname)) asset.legion:ReturnPayloadFromAsset(asset) end end return false,{},{} end return false,{},{} end function LEGION.UnRecruitAssets(Assets,Mission) for i=1,#Assets do local asset=Assets[i] asset.isReserved=false if asset.legion:IsAirwing()and not asset.spawned then asset.legion:T2(asset.legion.lid..string.format("Returning payload from asset %s",asset.spawngroupname)) asset.legion:ReturnPayloadFromAsset(asset) end if Mission then Mission:DelAsset(asset) end end end function LEGION:AssignAssetsForEscort(Cohorts,Assets,NescortMin,NescortMax,MissionType,TargetTypes,EngageRange) if NescortMin and NescortMax and(NescortMin>0 or NescortMax>0)then self:T(self.lid..string.format("Requested escort for %d assets from %d cohorts. Required escort assets=%d-%d",#Assets,#Cohorts,NescortMin,NescortMax)) local Escorts={} local EscortAvail=true for _,_asset in pairs(Assets)do local asset=_asset local TargetVec2=asset.legion:GetVec2() local Categories={Group.Category.HELICOPTER} local targetTypes={"Ground Units"} if asset.category==Group.Category.AIRPLANE then Categories={Group.Category.AIRPLANE} targetTypes={"Air"} end TargetTypes=TargetTypes or targetTypes local Erecruited,eassets,elegions=LEGION.RecruitCohortAssets(Cohorts,AUFTRAG.Type.ESCORT,MissionType,NescortMin,NescortMax,TargetVec2,nil,nil,nil,nil,nil,nil,Categories) if Erecruited then Escorts[asset.spawngroupname]={EscortLegions=elegions,EscortAssets=eassets,ecategory=asset.category} else EscortAvail=false break end end if EscortAvail then local N=0 for groupname,value in pairs(Escorts)do local Elegions=value.EscortLegions local Eassets=value.EscortAssets local ecategory=value.ecategory for _,_legion in pairs(Elegions)do local legion=_legion local OffsetVector=nil if ecategory==Group.Category.GROUND then OffsetVector={} OffsetVector.x=0 OffsetVector.y=UTILS.FeetToMeters(1000) OffsetVector.z=0 elseif MissionType==AUFTRAG.Type.SEAD then OffsetVector={} OffsetVector.x=-100 OffsetVector.y=500 OffsetVector.z=500 end local escort=AUFTRAG:NewESCORT(groupname,OffsetVector,EngageRange,TargetTypes) if MissionType==AUFTRAG.Type.SEAD then escort.missionTask=ENUMS.MissionTask.SEAD local DCStask=CONTROLLABLE.EnRouteTaskSEAD(nil) table.insert(escort.enrouteTasks,DCStask) end for _,_asset in pairs(Eassets)do local asset=_asset escort:AddAsset(asset) N=N+1 end self:MissionAssign(escort,{legion}) end end self:T(self.lid..string.format("Recruited %d escort assets",N)) return true else self:T(self.lid..string.format("Could not get at least one escort!")) for groupname,value in pairs(Escorts)do local Eassets=value.EscortAssets LEGION.UnRecruitAssets(Eassets) end return false end else self:T(self.lid..string.format("No escort required! NescortMin=%s, NescortMax=%s",tostring(NescortMin),tostring(NescortMax))) return true end end function LEGION:AssignAssetsForTransport(Legions,CargoAssets,NcarriersMin,NcarriersMax,DeployZone,DisembarkZone,Categories,Attributes,Properties) if NcarriersMin and NcarriersMax and(NcarriersMin>0 or NcarriersMax>0)then local Cohorts=LEGION._GetCohorts(Legions) local CargoLegions={};local CargoWeight=nil;local TotalWeight=0 for _,_asset in pairs(CargoAssets)do local asset=_asset CargoLegions[asset.legion.alias]=asset.legion if CargoWeight==nil or asset.weight>CargoWeight then CargoWeight=asset.weight end TotalWeight=TotalWeight+asset.weight end self:T(self.lid..string.format("Cargo weight=%.1f",CargoWeight)) self:T(self.lid..string.format("Total weight=%.1f",TotalWeight)) local TargetVec2=DeployZone:GetVec2() local TransportAvail,CarrierAssets,CarrierLegions= LEGION.RecruitCohortAssets(Cohorts,AUFTRAG.Type.OPSTRANSPORT,nil,NcarriersMin,NcarriersMax,TargetVec2,nil,nil,nil,CargoWeight,TotalWeight,nil,Categories,Attributes,Properties) if TransportAvail then local Transport=OPSTRANSPORT:New(nil,nil,DeployZone) if DisembarkZone then Transport:SetDisembarkZone(DisembarkZone) end self:T(self.lid..string.format("Transport available with %d carrier assets",#CarrierAssets)) for _,_legion in pairs(CargoLegions)do local legion=_legion local pickupzone=legion.spawnzone local tpz=Transport:AddTransportZoneCombo(nil,pickupzone,Transport:GetDeployZone()) tpz.PickupAirbase=legion:IsRunwayOperational()and legion.airbase or nil Transport:SetEmbarkZone(legion.spawnzone,tpz) for _,_asset in pairs(CargoAssets)do local asset=_asset if asset.legion.alias==legion.alias then Transport:AddAssetCargo(asset,tpz) end end end for _,_asset in pairs(CarrierAssets)do local asset=_asset Transport:AddAsset(asset) end self:TransportAssign(Transport,CarrierLegions) return true,Transport else self:T(self.lid..string.format("Transport assets could not be allocated ==> Unrecruiting assets")) LEGION.UnRecruitAssets(CarrierAssets) return false,nil end return nil,nil end return true,nil end function LEGION.CalculateAssetMissionScore(asset,MissionType,TargetVec2,IncludePayload,TotalWeight) local score=0 if asset.skill==AI.Skill.AVERAGE then score=score+0 elseif asset.skill==AI.Skill.GOOD then score=score+10 elseif asset.skill==AI.Skill.HIGH then score=score+20 elseif asset.skill==AI.Skill.EXCELLENT then score=score+30 end score=score+asset.cohort:GetMissionPeformance(MissionType) local function scorePayload(Payload,MissionType) for _,Capability in pairs(Payload.capabilities)do local capability=Capability if capability.MissionType==MissionType then return capability.Performance end end return 0 end if IncludePayload and asset.payload then score=score+scorePayload(asset.payload,MissionType) end local OrigVec2=asset.flightgroup and asset.flightgroup:GetVec2()or asset.legion:GetVec2() local distance=0 if TargetVec2 and OrigVec2 then distance=UTILS.MetersToNM(UTILS.VecDist2D(OrigVec2,TargetVec2)) if asset.category==Group.Category.AIRPLANE or asset.category==Group.Category.HELICOPTER then distance=UTILS.Round(distance/10,0) else distance=UTILS.Round(distance,0) end end score=score-distance if asset.spawned and asset.flightgroup and asset.flightgroup:IsAlive()then local currmission=asset.flightgroup:GetMissionCurrent() if currmission then if currmission.type==AUFTRAG.Type.ALERT5 and currmission.alert5MissionType==MissionType then score=score+25 elseif(currmission.type==AUFTRAG.Type.GCICAP or currmission.type==AUFTRAG.Type.PATROLRACETRACK)and MissionType==AUFTRAG.Type.INTERCEPT then score=score+35 elseif(currmission.type==AUFTRAG.Type.ONGUARD or currmission.type==AUFTRAG.Type.PATROLZONE)and(MissionType==AUFTRAG.Type.ARTY or MissionType==AUFTRAG.Type.GROUNDATTACK)then score=score+25 elseif currmission.type==AUFTRAG.Type.NOTHING then score=score+30 end end if MissionType==AUFTRAG.Type.OPSTRANSPORT or MissionType==AUFTRAG.Type.AMMOSUPPLY or MissionType==AUFTRAG.Type.AWACS or MissionType==AUFTRAG.Type.FUELSUPPLY or MissionType==AUFTRAG.Type.TANKER then score=score-10 else if asset.flightgroup:IsOutOfAmmo()then score=score-1000 end end end if MissionType==AUFTRAG.Type.OPSTRANSPORT then if TotalWeight then if asset.cargobaymax=2 then asset.legion:I(asset.legion.lid..string.format("Asset %s [spawned=%s] score=%d",asset.spawngroupname,tostring(asset.spawned),score)) end return score end function LEGION._OptimizeAssetSelection(assets,MissionType,TargetVec2,IncludePayload,TotalWeight) for _,_asset in pairs(assets)do local asset=_asset asset.score=LEGION.CalculateAssetMissionScore(asset,MissionType,TargetVec2,IncludePayload,TotalWeight) if IncludePayload then local RandomScoreMax=asset.legion and asset.legion.RandomAssetScore or LEGION.RandomAssetScore local RandomScore=math.random(0,RandomScoreMax) asset.score=asset.score+RandomScore end end local function optimize(a,b) local assetA=a local assetB=b return(assetA.score>assetB.score) end table.sort(assets,optimize) if LEGION.verbose>0 then local text=string.format("Optimized %d assets for %s mission/transport (payload=%s):",#assets,MissionType,tostring(IncludePayload)) for i,Asset in pairs(assets)do local asset=Asset text=text..string.format("\n%d. %s [%s]: score=%d",i,asset.spawngroupname,asset.squadname,asset.score or-1) asset.score=nil end env.info(text) end end function LEGION:GetMissionByID(mid) for _,_mission in pairs(self.missionqueue)do local mission=_mission if mission.auftragsnummer==tonumber(mid)then return mission end end return nil end function LEGION:GetTransportByID(uid) for _,_transport in pairs(self.transportqueue)do local transport=_transport if transport.uid==tonumber(uid)then return transport end end return nil end function LEGION:GetMissionFromRequestID(RequestID) for _,_mission in pairs(self.missionqueue)do local mission=_mission local mid=mission.requestID[self.alias] if mid and mid==RequestID then return mission end end return nil end function LEGION:GetMissionFromRequest(Request) return self:GetMissionFromRequestID(Request.uid) end function LEGION:FetchPayloadFromStock(UnitType,MissionType,Payloads) return nil end function LEGION:ReturnPayloadFromAsset(asset) return nil end NAVYGROUP={ ClassName="NAVYGROUP", turning=false, intowind=nil, intowindcounter=0, Qintowind={}, pathCorridor=400, engage={}, } NAVYGROUP.version="1.0.4" function NAVYGROUP:New(group) local og=_DATABASE:GetOpsGroup(group) if og then og:I(og.lid..string.format("WARNING: OPS group already exists in data base!")) return og end local self=BASE:Inherit(self,OPSGROUP:New(group)) self.lid=string.format("NAVYGROUP %s | ",self.groupname) self:SetDefaultROE() self:SetDefaultAlarmstate() self:SetDefaultEPLRS(self.isEPLRS) self:SetDefaultEmission() self:SetDetection() self:SetPatrolAdInfinitum(true) self:SetPathfinding(false) self:AddTransition("*","FullStop","Holding") self:AddTransition("*","Cruise","Cruising") self:AddTransition("*","RTZ","Returning") self:AddTransition("Returning","Returned","Returned") self:AddTransition("*","Detour","Cruising") self:AddTransition("*","DetourReached","*") self:AddTransition("*","Retreat","Retreating") self:AddTransition("Retreating","Retreated","Retreated") self:AddTransition("Cruising","EngageTarget","Engaging") self:AddTransition("Holding","EngageTarget","Engaging") self:AddTransition("OnDetour","EngageTarget","Engaging") self:AddTransition("Engaging","Disengage","Cruising") self:AddTransition("*","TurnIntoWind","Cruising") self:AddTransition("*","TurnedIntoWind","*") self:AddTransition("*","TurnIntoWindStop","*") self:AddTransition("*","TurnIntoWindOver","*") self:AddTransition("*","TurningStarted","*") self:AddTransition("*","TurningStopped","*") self:AddTransition("*","CollisionWarning","*") self:AddTransition("*","ClearAhead","*") self:AddTransition("Cruising","Dive","Cruising") self:AddTransition("Engaging","Dive","Engaging") self:AddTransition("Cruising","Surface","Cruising") self:AddTransition("Engaging","Surface","Engaging") self:_InitWaypoints() self:_InitGroup() self:HandleEvent(EVENTS.Birth,self.OnEventBirth) self:HandleEvent(EVENTS.Dead,self.OnEventDead) self:HandleEvent(EVENTS.RemoveUnit,self.OnEventRemoveUnit) self:HandleEvent(EVENTS.UnitLost,self.OnEventRemoveUnit) self.timerStatus=TIMER:New(self.Status,self):Start(1,30) self.timerQueueUpdate=TIMER:New(self._QueueUpdate,self):Start(2,5) self.timerCheckZone=TIMER:New(self._CheckInZones,self):Start(2,60) _DATABASE:AddOpsGroup(self) return self end function NAVYGROUP:SetPatrolAdInfinitum(switch) if switch==false then self.adinfinitum=false else self.adinfinitum=true end return self end function NAVYGROUP:SetPathfinding(Switch,CorridorWidth) self.pathfindingOn=Switch self.pathCorridor=CorridorWidth or 400 return self end function NAVYGROUP:SetPathfindingOn(CorridorWidth) self:SetPathfinding(true,CorridorWidth) return self end function NAVYGROUP:SetPathfindingOff() self:SetPathfinding(false,self.pathCorridor) return self end function NAVYGROUP:SetIntoWindLegacy(SwitchOn) if SwitchOn==nil then SwitchOn=true end self.intowindold=SwitchOn return self end function NAVYGROUP:AddTaskFireAtPoint(Coordinate,Clock,Radius,Nshots,WeaponType,Prio) local DCStask=CONTROLLABLE.TaskFireAtPoint(nil,Coordinate:GetVec2(),Radius,Nshots,WeaponType) local task=self:AddTask(DCStask,Clock,nil,Prio) return task end function NAVYGROUP:AddTaskWaypointFireAtPoint(Coordinate,Waypoint,Radius,Nshots,WeaponType,Prio,Duration) Waypoint=Waypoint or self:GetWaypointNext() local DCStask=CONTROLLABLE.TaskFireAtPoint(nil,Coordinate:GetVec2(),Radius,Nshots,WeaponType) local task=self:AddTaskWaypoint(DCStask,Waypoint,nil,Prio,Duration) return task end function NAVYGROUP:AddTaskAttackGroup(TargetGroup,WeaponExpend,WeaponType,Clock,Prio) local DCStask=CONTROLLABLE.TaskAttackGroup(nil,TargetGroup,WeaponType,WeaponExpend,AttackQty,Direction,Altitude,AttackQtyLimit,GroupAttack) local task=self:AddTask(DCStask,Clock,nil,Prio) return task end function NAVYGROUP:_CreateTurnIntoWind(starttime,stoptime,speed,uturn,offset) local Tnow=timer.getAbsTime() if starttime and type(starttime)=="number"then starttime=UTILS.SecondsToClock(Tnow+starttime) end starttime=starttime or UTILS.SecondsToClock(Tnow) local Tstart=UTILS.ClockToSeconds(starttime) if uturn==nil then uturn=true end local Tstop=Tstart+90*60 if stoptime==nil then Tstop=Tstart+90*60 elseif type(stoptime)=="number"then Tstop=Tstart+stoptime else Tstop=UTILS.ClockToSeconds(stoptime) end if Tstart>Tstop then self:E(string.format("ERROR:Into wind stop time %s lies before start time %s. Input rejected!",UTILS.SecondsToClock(Tstart),UTILS.SecondsToClock(Tstop))) return self end if Tstop<=Tnow then self:E(string.format("WARNING: Into wind stop time %s already over. Tnow=%s! Input rejected.",UTILS.SecondsToClock(Tstop),UTILS.SecondsToClock(Tnow))) return self end self.intowindcounter=self.intowindcounter+1 local recovery={} recovery.Tstart=Tstart recovery.Tstop=Tstop recovery.Open=false recovery.Over=false recovery.Speed=speed or 20 recovery.Uturn=uturn and uturn or false recovery.Offset=offset or 0 recovery.Id=self.intowindcounter return recovery end function NAVYGROUP:AddTurnIntoWind(starttime,stoptime,speed,uturn,offset) local recovery=self:_CreateTurnIntoWind(starttime,stoptime,speed,uturn,offset) table.insert(self.Qintowind,recovery) return recovery end function NAVYGROUP:GetTurnIntoWind(TID) if TID then for _,_turn in pairs(self.Qintowind)do local turn=_turn if turn.Id==TID then return turn end end else return self.intowind end return nil end function NAVYGROUP:ExtendTurnIntoWind(Duration,TurnIntoWind) Duration=Duration or 300 local TID=TurnIntoWind and TurnIntoWind.Id or nil local turn=self:GetTurnIntoWind(TID) if turn then turn.Tstop=turn.Tstop+Duration self:T(self.lid..string.format("Extending turn into wind by %d seconds. New stop time is %s",Duration,UTILS.SecondsToClock(turn.Tstop))) else self:E(self.lid.."Could not get turn into wind to extend!") end return self end function NAVYGROUP:RemoveTurnIntoWind(IntoWindData) if self.intowind and self.intowind.Id==IntoWindData.Id then self:TurnIntoWindStop() return end for i,_tiw in pairs(self.Qintowind)do local tiw=_tiw if tiw.Id==IntoWindData.Id then table.remove(self.Qintowind,i) break end end return self end function NAVYGROUP:IsHolding() return self:Is("Holding") end function NAVYGROUP:IsCruising() return self:Is("Cruising") end function NAVYGROUP:IsOnDetour() return self:Is("OnDetour") end function NAVYGROUP:IsDiving() return self:Is("Diving") end function NAVYGROUP:IsTurning() return self.turning end function NAVYGROUP:IsSteamingIntoWind() if self.intowind then return true else return false end end function NAVYGROUP:IsRecovering() if self.intowind then if self.intowind.Recovery==true then return true else return false end else return false end end function NAVYGROUP:IsLaunching() if self.intowind then if self.intowind.Recovery==false then return true else return false end else return false end end function NAVYGROUP:Status() local fsmstate=self:GetState() local alive=self:IsAlive() local freepath=0 if alive then self:_UpdatePosition() self:_CheckDetectedUnits() self:_CheckTurning() local disttoWP=math.min(self:GetDistanceToWaypoint(),UTILS.NMToMeters(10)) freepath=disttoWP if not self:IsTurning()then freepath=self:_CheckFreePath(freepath,100) if disttoWP>1 and freepathself.Twaiting+self.dTwait then self.Twaiting=nil self.dTwait=nil if self:_CountPausedMissions()>0 then self:UnpauseMission() else self:Cruise() end end end end local mission=self:GetMissionCurrent() if mission and mission.updateDCSTask then if mission.type==AUFTRAG.Type.CAPTUREZONE then local Task=mission:GetGroupWaypointTask(self) if mission:GetGroupStatus(self)==AUFTRAG.GroupStatus.EXECUTING or mission:GetGroupStatus(self)==AUFTRAG.GroupStatus.STARTED then self:_UpdateTask(Task,mission) end end end else self:_CheckDamage() end if alive~=nil then if self.verbose>=1 then local nelem=self:CountElements() local Nelem=#self.elements local nTaskTot,nTaskSched,nTaskWP=self:CountRemainingTasks() local nMissions=self:CountRemainingMissison() local roe=self:GetROE()or-1 local als=self:GetAlarmstate()or-1 local wpidxCurr=self.currentwp local wpuidCurr=self:GetWaypointUIDFromIndex(wpidxCurr)or 0 local wpidxNext=self:GetWaypointIndexNext()or 0 local wpuidNext=self:GetWaypointUIDFromIndex(wpidxNext)or 0 local wpN=#self.waypoints or 0 local wpF=tostring(self.passedfinalwp) local speed=UTILS.MpsToKnots(self.velocity or 0) local speedEx=UTILS.MpsToKnots(self:GetExpectedSpeed()) local alt=self.position and self.position.y or 0 local hdg=self.heading or 0 local life=self.life or 0 local ammo=self:GetAmmoTot().Total local ndetected=self.detectionOn and tostring(self.detectedunits:Count())or"Off" local cargo=0 for _,_element in pairs(self.elements)do local element=_element cargo=cargo+element.weightCargo end local intowind=self:IsSteamingIntoWind()and UTILS.SecondsToClock(self.intowind.Tstop-timer.getAbsTime(),true)or"N/A" local turning=tostring(self:IsTurning()) local text=string.format("%s [%d/%d]: ROE/AS=%d/%d | T/M=%d/%d | Wp=%d[%d]-->%d[%d]/%d [%s] | Life=%.1f | v=%.1f (%d) | Hdg=%03d | Ammo=%d | Detect=%s | Cargo=%.1f | Turn=%s Collision=%d IntoWind=%s", fsmstate,nelem,Nelem,roe,als,nTaskTot,nMissions,wpidxCurr,wpuidCurr,wpidxNext,wpuidNext,wpN,wpF,life,speed,speedEx,hdg,ammo,ndetected,cargo,turning,freepath,intowind) self:I(self.lid..text) end else local text=string.format("State %s: Alive=%s",fsmstate,tostring(self:IsAlive())) self:T(self.lid..text) end if alive and self.verbose>=2 and#self.Qintowind>0 then local text=string.format(self.lid.."Turn into wind time windows:") if#self.Qintowind==0 then text=text.." none!" end for i,_recovery in pairs(self.Qintowind)do local recovery=_recovery local Cstart=UTILS.SecondsToClock(recovery.Tstart) local Cstop=UTILS.SecondsToClock(recovery.Tstop) text=text..string.format("\n[%d] ID=%d Start=%s Stop=%s Open=%s Over=%s",i,recovery.Id,Cstart,Cstop,tostring(recovery.Open),tostring(recovery.Over)) end self:I(self.lid..text) end if self.verbose>=2 then local text="Elements:" for i,_element in pairs(self.elements)do local element=_element local name=element.name local status=element.status local unit=element.unit local life,life0=self:GetLifePoints(element) local life0=element.life0 local ammo=self:GetAmmoElement(element) text=text..string.format("\n[%d] %s: status=%s, life=%.1f/%.1f, guns=%d, rockets=%d, bombs=%d, missiles=%d, cargo=%d/%d kg", i,name,status,life,life0,ammo.Guns,ammo.Rockets,ammo.Bombs,ammo.Missiles,element.weightCargo,element.weightMaxCargo) end if#self.elements==0 then text=text.." none!" end self:I(self.lid..text) end if self:IsCruising()and self.detectionOn and self.engagedetectedOn then local targetgroup,targetdist=self:_GetDetectedTarget() if targetgroup then self:I(self.lid..string.format("Engaging target group %s at distance %d meters",targetgroup:GetName(),targetdist)) self:EngageTarget(targetgroup) end end self:_CheckCargoTransport() self:_PrintTaskAndMissionStatus() end function NAVYGROUP:onafterElementSpawned(From,Event,To,Element) self:T(self.lid..string.format("Element spawned %s",Element.name)) self:_UpdateStatus(Element,OPSGROUP.ElementStatus.SPAWNED) end function NAVYGROUP:onafterSpawned(From,Event,To) self:T(self.lid..string.format("Group spawned!")) if self.verbose>=1 then local text=string.format("Initialized Navy Group %s [GID=%d]:\n",self.groupname,self.group:GetID()) text=text..string.format("Unit type = %s\n",self.actype) text=text..string.format("Speed max = %.1f Knots\n",UTILS.KmphToKnots(self.speedMax)) text=text..string.format("Speed cruise = %.1f Knots\n",UTILS.KmphToKnots(self.speedCruise)) text=text..string.format("Weight = %.1f kg\n",self:GetWeightTotal()) text=text..string.format("Cargo bay = %.1f kg\n",self:GetFreeCargobay()) text=text..string.format("Has EPLRS = %s\n",tostring(self.isEPLRS)) text=text..string.format("Is Submarine = %s\n",tostring(self.isSubmarine)) text=text..string.format("Elements = %d\n",#self.elements) text=text..string.format("Waypoints = %d\n",#self.waypoints) text=text..string.format("Radio = %.1f MHz %s %s\n",self.radio.Freq,UTILS.GetModulationName(self.radio.Modu),tostring(self.radio.On)) text=text..string.format("Ammo = %d (G=%d/C=%d/R=%d/M=%d/T=%d)\n",self.ammo.Total,self.ammo.Guns,self.ammo.Cannons,self.ammo.Rockets,self.ammo.Missiles,self.ammo.Torpedos) text=text..string.format("FSM state = %s\n",self:GetState()) text=text..string.format("Is alive = %s\n",tostring(self:IsAlive())) text=text..string.format("LateActivate = %s\n",tostring(self:IsLateActivated())) self:I(self.lid..text) end self:_UpdatePosition() self.isDead=false self.isDestroyed=false if self.isAI then self:SwitchROE(self.option.ROE) self:SwitchAlarmstate(self.option.Alarm) self:SwitchEmission(self.option.Emission) self:SwitchEPLRS(self.option.EPLRS) self:SwitchInvisible(self.option.Invisible) self:SwitchImmortal(self.option.Immortal) self:_SwitchTACAN() self:_SwitchICLS() if self.radioDefault then else self:SetDefaultRadio(self.radio.Freq,self.radio.Modu,false) end if#self.waypoints>1 then self:__Cruise(-0.1) else self:FullStop() end end end function NAVYGROUP:onbeforeUpdateRoute(From,Event,To,n,Speed,Depth) local allowed=true local trepeat=nil if self:IsWaiting()then self:T(self.lid.."Update route denied. Group is WAITING!") return false elseif self:IsInUtero()then self:T(self.lid.."Update route denied. Group is INUTERO!") return false elseif self:IsDead()then self:T(self.lid.."Update route denied. Group is DEAD!") return false elseif self:IsStopped()then self:T(self.lid.."Update route denied. Group is STOPPED!") return false elseif self:IsHolding()then self:T(self.lid.."Update route denied. Group is holding position!") return false elseif self:IsEngaging()then self:T(self.lid.."Update route allowed. Group is engaging!") return true end if self.taskcurrent>0 then local task=self:GetTaskByID(self.taskcurrent) if task then if task.dcstask.id==AUFTRAG.SpecialTask.PATROLZONE then self:T2(self.lid.."Allowing update route for Task: PatrolZone") elseif task.dcstask.id==AUFTRAG.SpecialTask.RECON then self:T2(self.lid.."Allowing update route for Task: ReconMission") elseif task.dcstask.id==AUFTRAG.SpecialTask.RELOCATECOHORT then self:T2(self.lid.."Allowing update route for Task: Relocate Cohort") elseif task.dcstask.id==AUFTRAG.SpecialTask.REARMING then self:T2(self.lid.."Allowing update route for Task: Rearming") else local taskname=task and task.description or"No description" self:T(self.lid..string.format("WARNING: Update route denied because taskcurrent=%d>0! Task description = %s",self.taskcurrent,tostring(taskname))) allowed=false end else self:T(self.lid..string.format("WARNING: before update route taskcurrent=%d (>0!) but no task?!",self.taskcurrent)) allowed=false end end if not self.isAI then allowed=false end self:T2(self.lid..string.format("Onbefore Updateroute in state %s: allowed=%s (repeat in %s)",self:GetState(),tostring(allowed),tostring(trepeat))) if trepeat then self:__UpdateRoute(trepeat,n) end return allowed end function NAVYGROUP:onafterUpdateRoute(From,Event,To,n,N,Speed,Depth) n=n or self:GetWaypointIndexNext() N=N or#self.waypoints N=math.min(N,#self.waypoints) local waypoints={} for i=n,N do local wp=UTILS.DeepCopy(self.waypoints[i]) if Speed then wp.speed=UTILS.KnotsToMps(Speed) else if wp.speed<0.1 then wp.speed=UTILS.KmphToMps(self.speedCruise) end end if Depth then wp.alt=-Depth elseif self.depth then wp.alt=-self.depth else wp.alt=wp.alt or 0 end if i==n then self.speedWp=wp.speed self.altWp=wp.alt end table.insert(waypoints,wp) end local current=self:GetCoordinate():WaypointNaval(UTILS.MpsToKmph(self.speedWp),self.altWp) table.insert(waypoints,1,current) if self:IsEngaging()or not self.passedfinalwp then if self.verbose>=10 then for i=1,#waypoints do local wp=waypoints[i] local text=string.format("%s Waypoint [%d] UID=%d speed=%d m/s",self.groupname,i-1,wp.uid or-1,wp.speed) self:I(self.lid..text) COORDINATE:NewFromWaypoint(wp):MarkToAll(text) end end self:T(self.lid..string.format("Updateing route: WP %d-->%d (%d/%d), Speed=%.1f knots, Depth=%d m",self.currentwp,n,#waypoints,#self.waypoints,UTILS.MpsToKnots(self.speedWp),self.altWp)) self:Route(waypoints) else self:E(self.lid..string.format("WARNING: Passed final WP ==> Full Stop!")) self:FullStop() end end function NAVYGROUP:onafterDetour(From,Event,To,Coordinate,Speed,Depth,ResumeRoute) Depth=Depth or 0 Speed=Speed or self:GetSpeedCruise() local uid=self:GetWaypointCurrent().uid local wp=self:AddWaypoint(Coordinate,Speed,uid,Depth,true) if ResumeRoute then wp.detour=1 else wp.detour=0 end end function NAVYGROUP:onafterDetourReached(From,Event,To) self:T(self.lid.."Group reached detour coordinate.") end function NAVYGROUP:onafterTurnIntoWind(From,Event,To,IntoWind) local heading,speed=self:GetHeadingIntoWind(IntoWind.Offset,IntoWind.Speed) IntoWind.Heading=heading IntoWind.Open=true IntoWind.Coordinate=self:GetCoordinate(true) self.intowind=IntoWind self:T(self.lid..string.format("Steaming into wind: Heading=%03d Speed=%.1f, Tstart=%d Tstop=%d",IntoWind.Heading,speed,IntoWind.Tstart,IntoWind.Tstop)) local distance=UTILS.NMToMeters(1000) local coord=self:GetCoordinate() local Coord=coord:Translate(distance,IntoWind.Heading) local uid=self:GetWaypointCurrent().uid local wptiw=self:AddWaypoint(Coord,speed,uid) wptiw.intowind=true IntoWind.waypoint=wptiw if IntoWind.Uturn and false then IntoWind.Coordinate:MarkToAll("Return coord") end end function NAVYGROUP:onbeforeTurnIntoWindStop(From,Event,To) if self.intowind then return true else return false end end function NAVYGROUP:onafterTurnIntoWindStop(From,Event,To) self:TurnIntoWindOver(self.intowind) end function NAVYGROUP:onafterTurnIntoWindOver(From,Event,To,IntoWindData) if IntoWindData and self.intowind and IntoWindData.Id==self.intowind.Id then self:T2(self.lid.."Turn Into Wind Over!") self.intowind.Over=true self.intowind.Open=false self:RemoveWaypointByID(self.intowind.waypoint.uid) if self.intowind.Uturn then self:T(self.lid.."FF Turn Into Wind Over ==> Uturn!") local uid=self:GetWaypointCurrent().uid local wp=self:AddWaypoint(self.intowind.Coordinate,self:GetSpeedCruise(),uid);wp.temp=true else local indx=self:GetWaypointIndexNext() local speed=self:GetSpeedToWaypoint(indx) self:T(self.lid..string.format("FF Turn Into Wind Over ==> Next WP Index=%d at %.1f knots via update route!",indx,speed)) self:__UpdateRoute(-1,indx,nil,speed) end self.intowind=nil self:RemoveTurnIntoWind(IntoWindData) end end function NAVYGROUP:onafterFullStop(From,Event,To) self:T(self.lid.."Full stop ==> holding") local pos=self:GetCoordinate() local wp=pos:WaypointNaval(0) self:Route({wp}) end function NAVYGROUP:onafterCruise(From,Event,To,Speed) self.Twaiting=nil self.dTwait=nil self.depth=nil self:__UpdateRoute(-0.1,nil,nil,Speed) end function NAVYGROUP:onafterDive(From,Event,To,Depth,Speed) Depth=Depth or 50 self:I(self.lid..string.format("Diving to %d meters",Depth)) self.depth=Depth self:__UpdateRoute(-1,nil,nil,Speed) end function NAVYGROUP:onafterSurface(From,Event,To,Speed) self.depth=0 self:__UpdateRoute(-1,nil,nil,Speed) end function NAVYGROUP:onafterTurningStarted(From,Event,To) self.turning=true end function NAVYGROUP:onafterTurningStopped(From,Event,To) self.turning=false self.collisionwarning=false if self:IsSteamingIntoWind()then self:TurnedIntoWind() end end function NAVYGROUP:onafterCollisionWarning(From,Event,To,Distance) self:T(self.lid..string.format("Iceberg ahead in %d meters!",Distance or-1)) self.collisionwarning=true end function NAVYGROUP:onafterEngageTarget(From,Event,To,Target,Speed,Depth) self:T(self.lid.."Engaging Target") if Target:IsInstanceOf("TARGET")then self.engage.Target=Target else self.engage.Target=TARGET:New(Target) end self.engage.Coordinate=UTILS.DeepCopy(self.engage.Target:GetCoordinate()) local intercoord=self:GetCoordinate():GetIntermediateCoordinate(self.engage.Coordinate,0.8) self.engage.roe=self:GetROE() self.engage.alarmstate=self:GetAlarmstate() self:SwitchAlarmstate(ENUMS.AlarmState.Auto) self:SwitchROE(ENUMS.ROE.OpenFire) local uid=self:GetWaypointCurrentUID() self.engage.Depth=Depth or 0 self.engage.Speed=Speed self.engage.Waypoint=self:AddWaypoint(intercoord,Speed,uid,Depth,true) self.engage.Waypoint.detour=1 end function NAVYGROUP:_UpdateEngageTarget() if self.engage.Target and self.engage.Target:IsAlive()then local vec3=self.engage.Target:GetVec3() if vec3 then local dist=UTILS.VecDist3D(vec3,self.engage.Coordinate:GetVec3()) if dist>100 then self.engage.Coordinate:UpdateFromVec3(vec3) local uid=self:GetWaypointCurrentUID() self:RemoveWaypointByID(self.engage.Waypoint.uid) local intercoord=self:GetCoordinate():GetIntermediateCoordinate(self.engage.Coordinate,0.8) self.engage.Waypoint=self:AddWaypoint(intercoord,self.engage.Speed,uid,self.engage.Depth,true) self.engage.Waypoint.detour=1 end else self:Disengage() end else self:Disengage() end end function NAVYGROUP:onafterDisengage(From,Event,To) self:T(self.lid.."Disengage Target") self:SwitchROE(self.engage.roe) self:SwitchAlarmstate(self.engage.alarmstate) local task=self:GetTaskCurrent() if task and(task.dcstask.id==AUFTRAG.SpecialTask.GROUNDATTACK or task.dcstask.id==AUFTRAG.SpecialTask.NAVALENGAGEMENT)then self:T(self.lid.."Disengage with current task GROUNDATTACK/NAVALENGAGEMENT ==> Task Done!") self:TaskDone(task) end if self.engage.Waypoint then self:RemoveWaypointByID(self.engage.Waypoint.uid) end self:_CheckGroupDone(1) end function NAVYGROUP:onafterOutOfAmmo(From,Event,To) self:T(self.lid..string.format("Group is out of ammo at t=%.3f",timer.getTime())) if self.retreatOnOutOfAmmo then self:__Retreat(-1) return end if self.rtzOnOutOfAmmo then self:__RTZ(-1) end local task=self:GetTaskCurrent() if task then if task.dcstask.id=="FireAtPoint"or task.dcstask.id==AUFTRAG.SpecialTask.BARRAGE then self:T(self.lid..string.format("Cancelling current %s task because out of ammo!",task.dcstask.id)) self:TaskCancel(task) end end end function NAVYGROUP:onafterRTZ(From,Event,To,Zone,Formation) local zone=Zone or self.homezone self:CancelAllMissions() if zone then if self:IsInZone(zone)then self:Returned() else self:T(self.lid..string.format("RTZ to Zone %s",zone:GetName())) local Coordinate=zone:GetRandomCoordinate() local uid=self:GetWaypointCurrentUID() local wp=self:AddWaypoint(Coordinate,nil,uid,Formation,true) wp.detour=0 end else self:T(self.lid.."ERROR: No RTZ zone given!") end end function NAVYGROUP:onafterReturned(From,Event,To) self:T(self.lid..string.format("Group returned")) if self.legion then self:T(self.lid..string.format("Adding group back to warehouse stock")) self.legion:__AddAsset(10,self.group,1) end end function NAVYGROUP:AddWaypoint(Coordinate,Speed,AfterWaypointWithID,Depth,Updateroute) local coordinate=self:_CoordinateFromObject(Coordinate) local wpnumber=self:GetWaypointIndexAfterID(AfterWaypointWithID) Speed=Speed or self:GetSpeedCruise() local wp=coordinate:WaypointNaval(UTILS.KnotsToKmph(Speed),Depth) local waypoint=self:_CreateWaypoint(wp) if Depth then waypoint.alt=UTILS.FeetToMeters(Depth) end self:_AddWaypoint(waypoint,wpnumber) self:T(self.lid..string.format("Adding NAVAL waypoint index=%d uid=%d, speed=%.1f knots. Last waypoint passed was #%d. Total waypoints #%d",wpnumber,waypoint.uid,Speed,self.currentwp,#self.waypoints)) if Updateroute==nil or Updateroute==true then self:__UpdateRoute(-0.01) end return waypoint end function NAVYGROUP:_InitGroup(Template,Delay) if Delay and Delay>0 then self:ScheduleOnce(Delay,NAVYGROUP._InitGroup,self,Template,0) else if self.groupinitialized then self:T(self.lid.."WARNING: Group was already initialized! Will NOT do it again!") return end local template=Template or self:_GetTemplate() self.isAI=true self.isLateActivated=template.lateActivation self.isUncontrolled=false self.speedMax=self.group:GetSpeedMax() if self.speedMax and self.speedMax>3.6 then self.isMobile=true else self.isMobile=false self.speedMax=0 end self.speedCruise=self.speedMax*0.7 self.ammo=self:GetAmmoTot() self.radio.On=true self.radio.Freq=tonumber(template.units[1].frequency)/1000000 self.radio.Modu=tonumber(template.units[1].modulation) self.optionDefault.Formation="Off Road" self.option.Formation=self.optionDefault.Formation if not self.tacanDefault then self:SetDefaultTACAN(nil,nil,nil,nil,true) end if not self.tacan then self.tacan=UTILS.DeepCopy(self.tacanDefault) end if not self.iclsDefault then self:SetDefaultICLS(nil,nil,nil,true) end if not self.icls then self.icls=UTILS.DeepCopy(self.iclsDefault) end local units=self.group:GetUnits() local dcsgroup=Group.getByName(self.groupname) local size0=dcsgroup:getInitialSize() if#units~=size0 then self:E(self.lid..string.format("ERROR: Got #units=%d but group consists of %d units!",#units,size0)) end for _,unit in pairs(units)do self:_AddElementByName(unit:GetName()) end self.groupinitialized=true end return self end function NAVYGROUP:_CheckFreePath(DistanceMax,dx) local distance=DistanceMax or 5000 local dx=dx or 100 if self:IsTurning()then return distance end local offsetY=0.1 if UTILS.GetDCSMap()==DCSMAP.Caucasus then offsetY=5.01 end local vec3=self:GetVec3() vec3.y=offsetY local heading=self:GetHeading() local function LoS(dist) local checkvec3=UTILS.VecTranslate(vec3,dist,heading) local los=land.isVisible(vec3,checkvec3) return los end if LoS(DistanceMax)then return DistanceMax end local function check() local xmin=0 local xmax=DistanceMax local Nmax=100 local eps=100 local N=1 while N<=Nmax do local d=xmax-xmin local x=xmin+d/2 local los=LoS(x) self:T(self.lid..string.format("N=%d: xmin=%.1f xmax=%.1f x=%.1f d=%.3f los=%s",N,xmin,xmax,x,d,tostring(los))) if los and d<=eps then return x end if los then xmin=x else xmax=x end N=N+1 end return 0 end local _check=check() return _check end function NAVYGROUP:_CheckTurning() local unit=self.group:GetUnit(1) if unit and unit:IsAlive()then local vNew=self.orientX local vLast=self.orientXLast vNew.y=0;vLast.y=0 local deltaLast=math.deg(math.acos(UTILS.VecDot(vNew,vLast)/UTILS.VecNorm(vNew)/UTILS.VecNorm(vLast))) local turning=math.abs(deltaLast)>=2 if self.turning and not turning then self:TurningStopped() elseif turning and not self.turning then self:TurningStarted() end self.turning=turning end end function NAVYGROUP:_CheckTurnsIntoWind() local time=timer.getAbsTime() if self.intowind then if time>=self.intowind.Tstop then self:TurnIntoWindOver(self.intowind) end else local IntoWind=self:GetTurnIntoWindNext() if IntoWind then self:TurnIntoWind(IntoWind) end end end function NAVYGROUP:GetTurnIntoWindNext() if#self.Qintowind>0 then local time=timer.getAbsTime() table.sort(self.Qintowind,function(a,b)return a.Tstart=recovery.Tstart and time0 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 Offset=Offset or 0 local windfrom,vwind=self:GetWind() local intowind=windfrom-Offset+adjustDegreesForWindSpeed(vwind) if vwind<0.1 then intowind=self:GetHeading() end if intowind<0 then intowind=intowind+360 end local vtot=math.max(vdeck-UTILS.MpsToKnots(vwind),4) return intowind,vtot end function NAVYGROUP:GetHeadingIntoWind_new(Offset,vdeck) Offset=Offset or 0 local windfrom,vwind=self:GetWind(18) vwind=UTILS.MpsToKnots(vwind) local windto=(windfrom+180)%360 local alpha=math.rad(-Offset) local Vmin=4 local Vmax=UTILS.KmphToKnots(self.speedMax) 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 intowind=(540+(windto+math.deg(theta)))%360 self:T(self.lid..string.format("Heading into Wind: vship=%.1f, vwind=%.1f, WindTo=%03d°, Theta=%03d°, Heading=%03d",v,vwind,windto,theta,intowind)) return intowind,v end function NAVYGROUP:GetHeadingIntoWind(Offset,vdeck) if self.intowindold then return self:GetHeadingIntoWind_old(Offset,vdeck) else return self:GetHeadingIntoWind_new(Offset,vdeck) end end function NAVYGROUP:_FindPathToNextWaypoint() self:T3(self.lid.."Path finding") local astar=ASTAR:New() local position=self:GetCoordinate() local wpnext=self:GetWaypointNext() if wpnext==nil then return end local nextwp=wpnext.coordinate if wpnext.intowind then local hdg=self:GetHeading() nextwp=position:Translate(UTILS.NMToMeters(20),hdg,true) end local speed=UTILS.MpsToKnots(wpnext.speed) astar:SetStartCoordinate(position) astar:SetEndCoordinate(nextwp) local dist=position:Get2DDistance(nextwp) if dist<5 then return end local boxwidth=dist*2 local spacex=dist*0.1 local delta=dist/10 astar:CreateGrid({land.SurfaceType.WATER},boxwidth,spacex,delta,delta,self.verbose>10) astar:SetValidNeighbourLoS(self.pathCorridor) local function findpath() local path=astar:GetPath(true,true) if path then local uid=self:GetWaypointCurrent().uid for i,_node in ipairs(path)do local node=_node local wp=self:AddWaypoint(node.coordinate,speed,uid) wp.astar=true uid=wp.uid if self.verbose>=10 then node.coordinate:MarkToAll(string.format("Path node #%d",i)) end end return#path>0 else return false end end return findpath() end OPERATION={ ClassName="OPERATION", verbose=0, branches={}, counterPhase=0, counterBranch=0, counterEdge=0, cohorts={}, legions={}, targets={}, missions={}, } _OPERATIONID=0 OPERATION.PhaseStatus={ PLANNED="Planned", ACTIVE="Active", OVER="Over", } OPERATION.version="0.2.0" function OPERATION:New(Name) local self=BASE:Inherit(self,FSM:New()) _OPERATIONID=_OPERATIONID+1 self.uid=_OPERATIONID self.name=Name or string.format("Operation-%02d",_OPERATIONID) self.lid=string.format("%s | ",self.name) self:SetStartState("Planned") self.branchMaster=self:AddBranch("Master") self.conditionStart=CONDITION:New("Operation %s start",self.name) self.conditionStart:SetNoneResult(false) self.conditionStart:SetDefaultPersistence(false) self.conditionOver=CONDITION:New("Operation %s over",self.name) self.conditionOver:SetNoneResult(false) self.conditionOver:SetDefaultPersistence(false) self.branchActive=self.branchMaster self:AddTransition("*","Start","Running") self:AddTransition("*","StatusUpdate","*") self:AddTransition("Running","Pause","Paused") self:AddTransition("Paused","Unpause","Running") self:AddTransition("*","PhaseOver","*") self:AddTransition("*","PhaseNext","*") self:AddTransition("*","PhaseChange","*") self:AddTransition("*","BranchSwitch","*") self:AddTransition("*","Over","Over") self:AddTransition("*","Stop","Stopped") self:__StatusUpdate(-1) return self end function OPERATION:SetVerbosity(VerbosityLevel) self.verbose=VerbosityLevel or 0 return self end function OPERATION:SetTime(ClockStart,ClockStop) local Tnow=timer.getAbsTime() local Tstart=Tnow+5 if ClockStart and type(ClockStart)=="number"then Tstart=Tnow+ClockStart elseif ClockStart and type(ClockStart)=="string"then Tstart=UTILS.ClockToSeconds(ClockStart) end local Tstop=nil if ClockStop and type(ClockStop)=="number"then Tstop=Tnow+ClockStop elseif ClockStop and type(ClockStop)=="string"then Tstop=UTILS.ClockToSeconds(ClockStop) end self.Tstart=Tstart self.Tstop=Tstop if Tstop then self.duration=self.Tstop-self.Tstart end return self end function OPERATION:AddConditonOverAll(Function,...) local cf=self.conditionOver:AddFunctionAll(Function,...) return cf end function OPERATION:AddConditonOverAny(Phase,Function,...) local cf=self.conditionOver:AddFunctionAny(Function,...) return cf end function OPERATION:AddPhase(Name,Branch,Duration) Branch=Branch or self.branchMaster local phase=self:_CreatePhase(Name) phase.branch=Branch phase.duration=Duration self:T(self.lid..string.format("Adding phase %s to branch %s",phase.name,Branch.name)) table.insert(Branch.phases,phase) return phase end function OPERATION:InsertPhaseAfter(PhaseAfter,Name) for i=1,#self.phases do local phase=self.phases[i] if PhaseAfter.uid==phase.uid then local phase=self:_CreatePhase(Name) end end return nil end function OPERATION:GetName() return self.name or"Unknown" end function OPERATION:GetPhaseByName(Name) for _,_branch in pairs(self.branches)do local branch=_branch for _,_phase in pairs(branch.phases or{})do local phase=_phase if phase.name==Name then return phase end end end return nil end function OPERATION:SetPhaseStatus(Phase,Status) if Phase then self:T(self.lid..string.format("Phase %s status: %s-->%s",tostring(Phase.name),tostring(Phase.status),tostring(Status))) Phase.status=Status if Phase.status==OPERATION.PhaseStatus.ACTIVE then Phase.Tstart=timer.getAbsTime() Phase.nActive=Phase.nActive+1 elseif Phase.status==OPERATION.PhaseStatus.OVER then self:PhaseOver(Phase) end end return self end function OPERATION:GetPhaseStatus(Phase) return Phase.status end function OPERATION:SetPhaseConditonOver(Phase,Condition) if Phase then self:T(self.lid..string.format("Setting phase %s conditon over %s",self:GetPhaseName(Phase),Condition and Condition.name or"None")) Phase.conditionOver=Condition end return self end function OPERATION:AddPhaseConditonOverAll(Phase,Function,...) if Phase then local cf=Phase.conditionOver:AddFunctionAll(Function,...) return cf end return nil end function OPERATION:AddPhaseConditonOverAny(Phase,Function,...) if Phase then local cf=Phase.conditionOver:AddFunctionAny(Function,...) return cf end return nil end function OPERATION:SetConditionFunctionPersistence(ConditionFunction,IsPersistent) ConditionFunction.persistence=IsPersistent return self end function OPERATION:AddPhaseConditonRepeatAll(Phase,Function,...) if Phase then Phase.conditionRepeat:AddFunctionAll(Function,...) end return self end function OPERATION:GetPhaseConditonOver(Phase,Condition) return Phase.conditionOver end function OPERATION:GetPhaseNactive(Phase) return Phase.nActive end function OPERATION:GetPhaseName(Phase) Phase=Phase or self.phase if Phase then return Phase.name end return"None" end function OPERATION:GetPhaseActive() return self.phase end function OPERATION:GetPhaseIndex(Phase) local branch=Phase.branch for i,_phase in pairs(branch.phases)do local phase=_phase if phase.uid==Phase.uid then return i,branch end end return nil end function OPERATION:GetPhaseNext(Branch,PhaseStatus) Branch=Branch or self:GetBranchActive() local phases=Branch.phases or{} local phase=nil if self.phase and self.phase.branch.uid==Branch.uid then phase=self.phase end local N=#phases self:T(self.lid..string.format("Getting next phase! Branch=%s, Phases=%d, Status=%s",Branch.name,N,tostring(PhaseStatus))) if N>0 then if phase==nil and PhaseStatus==nil then return phases[1] end local n=1 if phase then n=self:GetPhaseIndex(phase)+1 end for i=n,N do local phase=phases[i] if PhaseStatus==nil or PhaseStatus==phase.status then return phase end end end return nil end function OPERATION:CountPhases(Status,Branch) Branch=Branch or self.branchActive local N=0 for _,_phase in pairs(Branch.phases)do local phase=_phase if Status==nil or Status==phase.status then N=N+1 end end return N end function OPERATION:AddBranch(Name) local branch=self:_CreateBranch(Name) table.insert(self.branches,branch) return branch end function OPERATION:GetBranchMaster() return self.branchMaster end function OPERATION:GetBranchActive() return self.branchActive or self.branchMaster end function OPERATION:GetBranchName(Branch) Branch=Branch or self:GetBranchActive() if Branch then return Branch.name end return"None" end function OPERATION:AddEdge(PhaseFrom,PhaseTo,ConditionSwitch) local edge={} edge.phaseFrom=PhaseFrom edge.phaseTo=PhaseTo edge.branchFrom=PhaseFrom.branch edge.branchTo=PhaseTo.branch if ConditionSwitch then edge.conditionSwitch=ConditionSwitch else edge.conditionSwitch=CONDITION:New("Edge") edge.conditionSwitch:SetNoneResult(true) end table.insert(edge.branchFrom.edges,edge) return edge end function OPERATION:AddEdgeConditonSwitchAll(Edge,Function,...) if Edge then local cf=Edge.conditionSwitch:AddFunctionAll(Function,...) return cf end return nil end function OPERATION:AddMission(Mission,Phase) Mission.phase=Phase Mission.operation=self table.insert(self.missions,Mission) return self end function OPERATION:AddTarget(Target,Phase) Target.phase=Phase Target.operation=self table.insert(self.targets,Target) return self end function OPERATION:GetTargets(Phase) local N={} for _,_target in pairs(self.targets)do local target=_target if target:IsAlive()and(Phase==nil or target.phase==Phase)then table.insert(N,target) end end return N end function OPERATION:CountTargets(Phase) local N=0 for _,_target in pairs(self.targets)do local target=_target if target:IsAlive()and(Phase==nil or target.phase==Phase)then N=N+1 end end return N end function OPERATION:AssignCohort(Cohort) self:T(self.lid..string.format("Assiging Cohort %s to operation",Cohort.name)) self.cohorts[Cohort.name]=Cohort end function OPERATION:AssignLegion(Legion) self.legions[Legion.alias]=Legion end function OPERATION:IsAssignedLegion(Legion) local legion=self.legions[Legion.alias] if legion then self:T(self.lid..string.format("Legion %s is assigned to this operation",Legion.alias)) return true else self:T(self.lid..string.format("Legion %s is NOT assigned to this operation",Legion.alias)) return false end end function OPERATION:IsAssignedCohort(Cohort) local cohort=self.cohorts[Cohort.name] if cohort then self:T(self.lid..string.format("Cohort %s is assigned to this operation",Cohort.name)) return true else local Legion=Cohort.legion if Legion and self:IsAssignedLegion(Legion)then self:T(self.lid..string.format("Legion %s of Cohort %s is assigned to this operation",Legion.alias,Cohort.name)) return true end self:T(self.lid..string.format("Cohort %s is NOT assigned to this operation",Cohort.name)) return false end return nil end function OPERATION:IsAssignedCohortOrLegion(Object) local isAssigned=nil if Object:IsInstanceOf("COHORT")then isAssigned=self:IsAssignedCohort(Object) elseif Object:IsInstanceOf("LEGION")then isAssigned=self:IsAssignedLegion(Object) else self:E(self.lid.."ERROR: Unknown Object!") end return isAssigned end function OPERATION:IsPlanned() local is=self:is("Planned") return is end function OPERATION:IsRunning() local is=self:is("Running") return is end function OPERATION:IsPaused() local is=self:is("Paused") return is end function OPERATION:IsOver() local is=self:is("Over") return is end function OPERATION:IsStopped() local is=self:is("Stopped") return is end function OPERATION:IsNotOver() local is=not(self:IsOver()or self:IsStopped()) return is end function OPERATION:IsPhaseActive(Phase) if Phase and Phase.status and Phase.status==OPERATION.PhaseStatus.ACTIVE then return true end return false end function OPERATION:IsPhaseActive(Phase) local phase=self:GetPhaseActive() if phase and phase.uid==Phase.uid then return true else return false end return nil end function OPERATION:IsPhasePlanned(Phase) if Phase and Phase.status and Phase.status==OPERATION.PhaseStatus.PLANNED then return true end return false end function OPERATION:IsPhaseOver(Phase) if Phase and Phase.status and Phase.status==OPERATION.PhaseStatus.OVER then return true end return false end function OPERATION:onafterStart(From,Event,To) self:T(self.lid..string.format("Starting Operation!")) return self end function OPERATION:onafterStatusUpdate(From,Event,To) local Tnow=timer.getAbsTime() local fsmstate=self:GetState() if self:IsPlanned()then if(self.Tstart and Tnow>self.Tstart or self.Tstart==nil)and(self.conditionStart==nil or self.conditionStart:Evaluate())then self:Start() end elseif self:IsNotOver()then if(self.Tstop and Tnow>self.Tstop or self.Tstop==nil)and(self.conditionOver==nil or self.conditionOver:Evaluate())then self:Over() end end if self:IsRunning()then self:_CheckPhases() end if self.verbose>=1 then local phaseName=self:GetPhaseName() local branchName=self:GetBranchName() local NphaseTot=self:CountPhases() local NphaseAct=self:CountPhases(OPERATION.PhaseStatus.ACTIVE) local NphasePla=self:CountPhases(OPERATION.PhaseStatus.PLANNED) local NphaseOvr=self:CountPhases(OPERATION.PhaseStatus.OVER) local text=string.format("State=%s: Phase=%s [%s], Phases=%d [Active=%d, Planned=%d, Over=%d]",fsmstate,phaseName,branchName,NphaseTot,NphaseAct,NphasePla,NphaseOvr) self:I(self.lid..text) end if self.verbose>=2 then local text="Phases:" for i,_phase in pairs(self.branchActive.phases)do local phase=_phase text=text..string.format("\n[%d] %s [uid=%d]: status=%s Nact=%d",i,phase.name,phase.uid,tostring(phase.status),phase.nActive) end if text=="Phases:"then text=text.." None"end self:I(self.lid..text) end self:__StatusUpdate(-30) return self end function OPERATION:onafterPhaseNext(From,Event,To) local Phase=self:GetPhaseNext() if Phase then self:PhaseChange(Phase) else self:Over() end return self end function OPERATION:onafterPhaseChange(From,Event,To,Phase) local oldphase="None" if self.phase then if self.phase.status~=OPERATION.PhaseStatus.OVER then self:SetPhaseStatus(self.phase,OPERATION.PhaseStatus.OVER) end oldphase=self.phase.name end self:I(self.lid..string.format("Phase change: %s --> %s",oldphase,Phase.name)) self.phase=Phase self:SetPhaseStatus(Phase,OPERATION.PhaseStatus.ACTIVE) return self end function OPERATION:onafterPhaseOver(From,Event,To,Phase) Phase.conditionOver:RemoveNonPersistant() end function OPERATION:onafterBranchSwitch(From,Event,To,Branch,Phase) self:T(self.lid..string.format("Switching to branch %s",Branch.name)) self.branchActive=Branch self:PhaseChange(Phase) return self end function OPERATION:onafterOver(From,Event,To) self:T(self.lid..string.format("Operation is over!")) self.phase=nil for _,_branch in pairs(self.branches)do local branch=_branch for _,_phase in pairs(branch.phases)do local phase=_phase if not self:IsPhaseOver(phase)then self:SetPhaseStatus(phase,OPERATION.PhaseStatus.OVER) end end end return self end function OPERATION:_CheckPhases() local phase=self:GetPhaseActive() if phase and phase.conditionOver then local isOver=phase.conditionOver:Evaluate() local Tnow=timer.getAbsTime() if phase.duration and phase.Tstart and Tnow-phase.Tstart>phase.duration then isOver=true end if isOver then self:SetPhaseStatus(phase,OPERATION.PhaseStatus.OVER) end end if phase==nil or phase.status==OPERATION.PhaseStatus.OVER then for _,_edge in pairs(self.branchActive.edges)do local edge=_edge if phase then end if(edge.phaseFrom==nil)or(phase and edge.phaseFrom.uid==phase.uid)then local switch=edge.conditionSwitch:Evaluate() if switch then local phaseTo=edge.phaseTo or self:GetPhaseNext(edge.branchTo,nil) if phaseTo then self:BranchSwitch(edge.branchTo,phaseTo) else self:Over() end return end end end self:PhaseNext() end end function OPERATION:_CreatePhase(Name) self.counterPhase=self.counterPhase+1 local phase={} phase.uid=self.counterPhase phase.name=Name or string.format("Phase-%02d",self.counterPhase) phase.conditionOver=CONDITION:New(Name.." Over") phase.conditionOver:SetDefaultPersistence(false) phase.status=OPERATION.PhaseStatus.PLANNED phase.nActive=0 return phase end function OPERATION:_CreateBranch(Name) self.counterBranch=self.counterBranch+1 local branch={} branch.uid=self.counterBranch branch.name=Name or string.format("Branch-%02d",self.counterBranch) branch.phases={} branch.edges={} return branch end OPSGROUP={ ClassName="OPSGROUP", verbose=0, lid=nil, groupname=nil, group=nil, template=nil, isLateActivated=nil, waypoints=nil, waypoints0=nil, currentwp=1, elements={}, taskqueue={}, taskcounter=nil, taskcurrent=nil, taskenroute=nil, taskpaused={}, missionqueue={}, currentmission=nil, detectedunits={}, detectedgroups={}, attribute=nil, checkzones=nil, inzones=nil, groupinitialized=nil, wpcounter=1, radio={}, option={}, optionDefault={}, tacan={}, icls={}, callsign={}, Ndestroyed=0, Nkills=0, Nhit=0, weaponData={}, cargoqueue={}, cargoBay={}, mycarrier={}, carrierLoader={}, carrierUnloader={}, useMEtasks=false, pausedmissions={}, } OPSGROUP.ElementStatus={ INUTERO="InUtero", SPAWNED="Spawned", PARKING="Parking", ENGINEON="Engine On", TAXIING="Taxiing", TAKEOFF="Takeoff", AIRBORNE="Airborne", LANDING="Landing", LANDED="Landed", ARRIVED="Arrived", DEAD="Dead", } OPSGROUP.GroupStatus={ INUTERO="InUtero", PARKING="Parking", TAXIING="Taxiing", AIRBORNE="Airborne", INBOUND="Inbound", LANDING="Landing", LANDED="Landed", ARRIVED="Arrived", DEAD="Dead", } OPSGROUP.TaskStatus={ SCHEDULED="scheduled", EXECUTING="executing", PAUSED="paused", DONE="done", } OPSGROUP.TaskType={ SCHEDULED="scheduled", WAYPOINT="waypoint", } OPSGROUP.CarrierStatus={ NOTCARRIER="not carrier", PICKUP="pickup", LOADING="loading", LOADED="loaded", TRANSPORTING="transporting", UNLOADING="unloading", } OPSGROUP.CargoStatus={ AWAITING="Awaiting carrier", NOTCARGO="not cargo", ASSIGNED="assigned to carrier", BOARDING="boarding", LOADED="loaded", } OPSGROUP.version="1.0.6" function OPSGROUP:New(group) local self=BASE:Inherit(self,FSM:New()) if type(group)=="string"then self.groupname=group self.group=GROUP:FindByName(self.groupname) else self.group=group self.groupname=group:GetName() end self.lid=string.format("OPSGROUP %s | ",tostring(self.groupname)) if self.group then if not self:IsExist()then self:E(self.lid.."ERROR: GROUP does not exist! Returning nil") return nil end end if UTILS.IsInstanceOf(group,"OPSGROUP")then self:E(self.lid.."ERROR: GROUP is already an OPSGROUP: "..tostring(self.groupname).."!") return group end self:_SetTemplate() self.dcsgroup=self:GetDCSGroup() self.controller=self.dcsgroup:getController() self.category=self.dcsgroup:getCategory() if self.category==Group.Category.GROUND then self.isArmygroup=true elseif self.category==Group.Category.TRAIN then self.isArmygroup=true self.isTrain=true elseif self.category==Group.Category.SHIP then self.isNavygroup=true elseif self.category==Group.Category.AIRPLANE then self.isFlightgroup=true elseif self.category==Group.Category.HELICOPTER then self.isFlightgroup=true self.isHelo=true else end self.attribute=self.group:GetAttribute() local units=self.group:GetUnits() if units then local masterunit=units[1] if masterunit then self.descriptors=masterunit:GetDesc() self.actype=masterunit:GetTypeName() self.isSubmarine=masterunit:HasAttribute("Submarines") self.isEPLRS=masterunit:HasAttribute("Datalink") if self:IsFlightgroup()then self.rangemax=self.descriptors.range and self.descriptors.range*1000 or 500*1000 self.ceiling=self.descriptors.Hmax self.tankertype=select(2,masterunit:IsTanker()) self.refueltype=select(2,masterunit:IsRefuelable()) end end end self.detectedunits=SET_UNIT:New() self.detectedgroups=SET_GROUP:New() self.inzones=SET_ZONE:New() self:SetDefaultAltitude() self:SetReturnToLegion() self.spot={} self.spot.On=false self.spot.timer=TIMER:New(self._UpdateLaser,self) self.spot.Coordinate=COORDINATE:New(0,0,0) self:SetLaser(1688,true,false,0.5) self.cargoStatus=OPSGROUP.CargoStatus.NOTCARGO self.carrierStatus=OPSGROUP.CarrierStatus.NOTCARRIER self:SetCarrierLoaderAllAspect() self:SetCarrierUnloaderAllAspect() self.taskcurrent=0 self.taskcounter=0 self:SetStartState("InUtero") self:AddTransition("InUtero","Spawned","Spawned") self:AddTransition("*","Respawn","InUtero") self:AddTransition("*","Dead","InUtero") self:AddTransition("*","InUtero","InUtero") self:AddTransition("*","Stop","Stopped") self:AddTransition("*","Hit","*") self:AddTransition("*","Damaged","*") self:AddTransition("*","Destroyed","*") self:AddTransition("*","UpdateRoute","*") self:AddTransition("*","PassingWaypoint","*") self:AddTransition("*","PassedFinalWaypoint","*") self:AddTransition("*","GotoWaypoint","*") self:AddTransition("*","Wait","*") self:AddTransition("*","Stuck","*") self:AddTransition("*","DetectedUnit","*") self:AddTransition("*","DetectedUnitNew","*") self:AddTransition("*","DetectedUnitKnown","*") self:AddTransition("*","DetectedUnitLost","*") self:AddTransition("*","DetectedGroup","*") self:AddTransition("*","DetectedGroupNew","*") self:AddTransition("*","DetectedGroupKnown","*") self:AddTransition("*","DetectedGroupLost","*") self:AddTransition("*","OutOfAmmo","*") self:AddTransition("*","OutOfGuns","*") self:AddTransition("*","OutOfRockets","*") self:AddTransition("*","OutOfBombs","*") self:AddTransition("*","OutOfMissiles","*") self:AddTransition("*","OutOfTorpedos","*") self:AddTransition("*","OutOfMissilesAA","*") self:AddTransition("*","OutOfMissilesAG","*") self:AddTransition("*","OutOfMissilesAS","*") self:AddTransition("*","EnterZone","*") self:AddTransition("*","LeaveZone","*") self:AddTransition("*","LaserOn","*") self:AddTransition("*","LaserOff","*") self:AddTransition("*","LaserCode","*") self:AddTransition("*","LaserPause","*") self:AddTransition("*","LaserResume","*") self:AddTransition("*","LaserLostLOS","*") self:AddTransition("*","LaserGotLOS","*") self:AddTransition("*","TaskExecute","*") self:AddTransition("*","TaskPause","*") self:AddTransition("*","TaskCancel","*") self:AddTransition("*","TaskDone","*") self:AddTransition("*","MissionStart","*") self:AddTransition("*","MissionExecute","*") self:AddTransition("*","MissionCancel","*") self:AddTransition("*","PauseMission","*") self:AddTransition("*","UnpauseMission","*") self:AddTransition("*","MissionDone","*") self:AddTransition("*","ElementInUtero","*") self:AddTransition("*","ElementSpawned","*") self:AddTransition("*","ElementDestroyed","*") self:AddTransition("*","ElementDead","*") self:AddTransition("*","ElementDamaged","*") self:AddTransition("*","ElementHit","*") self:AddTransition("*","Board","*") self:AddTransition("*","Embarked","*") self:AddTransition("*","Disembarked","*") self:AddTransition("*","Pickup","*") self:AddTransition("*","Loading","*") self:AddTransition("*","Load","*") self:AddTransition("*","Loaded","*") self:AddTransition("*","LoadingDone","*") self:AddTransition("*","Transport","*") self:AddTransition("*","Unloading","*") self:AddTransition("*","Unload","*") self:AddTransition("*","Unloaded","*") self:AddTransition("*","UnloadingDone","*") self:AddTransition("*","Delivered","*") self:AddTransition("*","TransportCancel","*") self:AddTransition("*","HoverStart","*") self:AddTransition("*","HoverEnd","*") return self end function OPSGROUP:GetCoalition() return self.group:GetCoalition() end function OPSGROUP:GetLifePoints(Element) local life=0 local life0=0 if Element then local unit=Element.unit if unit then life=unit:GetLife() life0=unit:GetLife0() life=math.min(life,life0) end else for _,element in pairs(self.elements)do local l,l0=self:GetLifePoints(element) life=life+l life0=life0+l0 end end return life,life0 end function OPSGROUP:GetAttribute() return self.attribute end function OPSGROUP:SetVerbosity(VerbosityLevel) self.verbose=VerbosityLevel or 0 return self end function OPSGROUP:_SetLegion(Legion) self:T2(self.lid..string.format("Adding opsgroup to legion %s",Legion.alias)) self.legion=Legion return self end function OPSGROUP:SetReturnToLegion(Switch) if Switch==false then self.legionReturn=false else self.legionReturn=true end self:T(self.lid..string.format("Setting ReturnToLegion=%s",tostring(self.legionReturn))) return self end function OPSGROUP:SetDefaultSpeed(Speed) if Speed then self.speedCruise=UTILS.KnotsToKmph(Speed) end return self end function OPSGROUP:GetSpeedCruise() local speed=UTILS.KmphToKnots(self.speedCruise or self.speedMax*0.7) return speed end function OPSGROUP:SetDefaultAltitude(Altitude) if Altitude then self.altitudeCruise=UTILS.FeetToMeters(Altitude) else if self:IsFlightgroup()then if self.isHelo then self.altitudeCruise=UTILS.FeetToMeters(1500) else self.altitudeCruise=UTILS.FeetToMeters(10000) end else self.altitudeCruise=0 end end return self end function OPSGROUP:GetCruiseAltitude() local alt=UTILS.MetersToFeet(self.altitudeCruise) return alt end function OPSGROUP:SetAltitude(Altitude,Keep,RadarAlt) if Altitude then Altitude=UTILS.FeetToMeters(Altitude) else if self:IsFlightgroup()then if self.isHelo then Altitude=UTILS.FeetToMeters(1500) else Altitude=UTILS.FeetToMeters(10000) end else Altitude=0 end end local AltType="BARO" if RadarAlt then AltType="RADIO" end if self.controller then self.controller:setAltitude(Altitude,Keep,AltType) end return self end function OPSGROUP:GetAltitude() local alt=0 if self.group then alt=self.group:GetAltitude() alt=UTILS.MetersToFeet(alt) end return alt end function OPSGROUP:SetSpeed(Speed,Keep,AltCorrected) if Speed then else Speed=UTILS.KmphToKnots(self.speedMax) end if AltCorrected then local altitude=self:GetAltitude() Speed=UTILS.KnotsToAltKIAS(Speed,altitude) end Speed=UTILS.KnotsToMps(Speed) if self.controller then self.controller:setSpeed(Speed,Keep) end return self end function OPSGROUP:SetDetection(Switch) self:T(self.lid..string.format("Detection is %s",tostring(Switch))) self.detectionOn=Switch return self end function OPSGROUP:GetDCSObject() return self.dcsgroup end function OPSGROUP:KnowTarget(TargetObject,KnowType,KnowDist,Delay) if Delay and Delay>0 then self:ScheduleOnce(Delay,OPSGROUP.KnowTarget,self,TargetObject,KnowType,KnowDist,0) else if TargetObject:IsInstanceOf("GROUP")then TargetObject=TargetObject:GetUnit(1) elseif TargetObject:IsInstanceOf("OPSGROUP")then TargetObject=TargetObject.group:GetUnit(1) end local object=TargetObject:GetDCSObject() for _,_element in pairs(self.elements)do local element=_element if element.controller then element.controller:knowTarget(object,true,true) end end self:T(self.lid..string.format("We should now know target %s",TargetObject:GetName())) end return self end function OPSGROUP:IsTargetDetected(TargetObject) local objects={} if TargetObject:IsInstanceOf("GROUP")then for _,unit in pairs(TargetObject:GetUnits())do table.insert(objects,unit:GetDCSObject()) end elseif TargetObject:IsInstanceOf("OPSGROUP")then for _,unit in pairs(TargetObject.group:GetUnits())do table.insert(objects,unit:GetDCSObject()) end elseif TargetObject:IsInstanceOf("UNIT")or TargetObject:IsInstanceOf("STATIC")then table.insert(objects,TargetObject:GetDCSObject()) end for _,object in pairs(objects or{})do local detected,visible,lastTime,type,distance,lastPos,lastVel=self.controller:isTargetDetected(object,1,2,4,8,16,32) if detected then return true end for _,_element in pairs(self.elements)do local element=_element if element.controller then local detected,visible,lastTime,type,distance,lastPos,lastVel= element.controller:isTargetDetected(object,1,2,4,8,16,32) if detected then return true end end end end return false end function OPSGROUP:InWeaponRange(TargetCoord,WeaponBitType,RefCoord) RefCoord=RefCoord or self:GetCoordinate() local dist=TargetCoord:Get2DDistance(RefCoord) if WeaponBitType then local weapondata=self:GetWeaponData(WeaponBitType) if weapondata then if dist>=weapondata.RangeMin and dist<=weapondata.RangeMax then return true else return false end end else for _,_weapondata in pairs(self.weaponData or{})do local weapondata=_weapondata if dist>=weapondata.RangeMin and dist<=weapondata.RangeMax then return true end end return false end return nil end function OPSGROUP:GetCoordinateInRange(TargetCoord,WeaponBitType,RefCoord,SurfaceTypes) local coordInRange=nil RefCoord=RefCoord or self:GetCoordinate() local weapondata=self:GetWeaponData(WeaponBitType) local dh={0,-5,5,-10,10,-15,15,-20,20,-25,25,-30,30,-35,35,-40,40,-45,45,-50,50,-55,55,-60,60,-65,65,-70,70,-75,75,-80,80} local function _checkSurface(point) if SurfaceTypes then local stype=point:GetSurfaceType() for _,sf in pairs(SurfaceTypes)do if sf==stype then return true end end return false else return true end end if weapondata then local heading=TargetCoord:HeadingTo(RefCoord) local dist=RefCoord:Get2DDistance(TargetCoord) local range=nil if dist>weapondata.RangeMax then range=weapondata.RangeMax self:T(self.lid..string.format("Out of max range = %.1f km by %.1f km for weapon %s",weapondata.RangeMax/1000,(weapondata.RangeMax-dist)/1000,tostring(WeaponBitType))) elseif dist=ThreatLevelMin and threatlevel<=ThreatLevelMax then if threatlevellevelmax then threat=unit levelmax=threatlevel end end return threat,levelmax end function OPSGROUP:SetEngageDetectedOn(RangeMax,TargetTypes,EngageZoneSet,NoEngageZoneSet) if TargetTypes then if type(TargetTypes)~="table"then TargetTypes={TargetTypes} end else TargetTypes={"All"} end if EngageZoneSet and EngageZoneSet:IsInstanceOf("ZONE_BASE")then local zoneset=SET_ZONE:New():AddZone(EngageZoneSet) EngageZoneSet=zoneset end if NoEngageZoneSet and NoEngageZoneSet:IsInstanceOf("ZONE_BASE")then local zoneset=SET_ZONE:New():AddZone(NoEngageZoneSet) NoEngageZoneSet=zoneset end self.engagedetectedOn=true self.engagedetectedRmax=UTILS.NMToMeters(RangeMax or 25) self.engagedetectedTypes=TargetTypes self.engagedetectedEngageZones=EngageZoneSet self.engagedetectedNoEngageZones=NoEngageZoneSet self:T(self.lid..string.format("Engage detected ON: Rmax=%d NM",UTILS.MetersToNM(self.engagedetectedRmax))) self:SetDetection(true) return self end function OPSGROUP:SetEngageDetectedOff() self:T(self.lid..string.format("Engage detected OFF")) self.engagedetectedOn=false return self end function OPSGROUP:SetRearmOnOutOfAmmo() self.rearmOnOutOfAmmo=true return self end function OPSGROUP:SetRetreatOnOutOfAmmo() self.retreatOnOutOfAmmo=true return self end function OPSGROUP:SetReturnOnOutOfAmmo() self.rtzOnOutOfAmmo=true return self end function OPSGROUP:SetCargoBayLimit(Weight,UnitName) for _,_element in pairs(self.elements)do local element=_element if UnitName==nil or UnitName==element.name then element.weightMaxCargo=Weight if element.unit then element.unit:SetCargoBayWeightLimit(Weight) end end end return self end function OPSGROUP:HasLoS(Coordinate,Element,OffsetElement,OffsetCoordinate) if Coordinate then local Vec3={x=Coordinate.x,y=Coordinate.y,z=Coordinate.z} if OffsetCoordinate then Vec3=UTILS.VecAdd(Vec3,OffsetCoordinate) end local function checklos(vec3) if vec3 then if OffsetElement then vec3=UTILS.VecAdd(vec3,OffsetElement) end local _los=land.isVisible(vec3,Vec3) return _los end return nil end if Element then if Element.unit and Element.unit:IsAlive()then local vec3=Element.unit:GetVec3() local los=checklos(vec3) return los end else local gotit=false for _,_element in pairs(self.elements)do local element=_element if element and element.unit and element.unit:IsAlive()then gotit=true local vec3=element.unit:GetVec3() local los=checklos(vec3) if los then return true end end end if gotit then return false end end end return nil end function OPSGROUP:GetGroup() return self.group end function OPSGROUP:GetName() return self.groupname end function OPSGROUP:GetDCSGroup() local DCSGroup=Group.getByName(self.groupname) return DCSGroup end function OPSGROUP:GetUnit(UnitNumber) local DCSUnit=self:GetDCSUnit(UnitNumber) if DCSUnit then local unit=UNIT:Find(DCSUnit) return unit end return nil end function OPSGROUP:GetDCSUnit(UnitNumber) local DCSGroup=self:GetDCSGroup() if DCSGroup then local unit=DCSGroup:getUnit(UnitNumber or 1) return unit else self:E(self.lid..string.format("ERROR: DCS group does not exist! Cannot get unit")) end return nil end function OPSGROUP:GetDCSUnits() local DCSGroup=self:GetDCSGroup() if DCSGroup then local units=DCSGroup:getUnits() return units end return nil end function OPSGROUP:GetVec2(UnitName) local vec3=self:GetVec3(UnitName) if vec3 then local vec2={x=vec3.x,y=vec3.z} return vec2 end return nil end function OPSGROUP:GetVec3(UnitName) local vec3=nil local carrier=self:_GetMyCarrierElement() if carrier and carrier.status~=OPSGROUP.ElementStatus.DEAD and self:IsLoaded()then local unit=carrier.unit if unit and unit:IsExist()then vec3=unit:GetVec3() return vec3 end end if self:IsExist()then local unit=nil if UnitName then unit=Unit.getByName(UnitName) else unit=self:GetDCSUnit() end if unit then local vec3=unit:getPoint() return vec3 end end if self.position then return self.position end return nil end function OPSGROUP:GetCoordinate(NewObject,UnitName) local vec3=self:GetVec3(UnitName)or self.position if vec3 then self.coordinate=self.coordinate or COORDINATE:New(0,0,0) self.coordinate.x=vec3.x self.coordinate.y=vec3.y self.coordinate.z=vec3.z if NewObject then local coord=COORDINATE:NewFromCoordinate(self.coordinate) return coord else return self.coordinate end else self:T(self.lid.."WARNING: Cannot get coordinate!") end return nil end function OPSGROUP:GetVelocity(UnitName) if self:IsExist()then local unit=nil if UnitName then unit=Unit.getByName(UnitName) else unit=self:GetDCSUnit() end if unit then local velvec3=unit:getVelocity() local vel=UTILS.VecNorm(velvec3) return vel else self:T(self.lid.."WARNING: Unit does not exist. Cannot get velocity!") end else self:T(self.lid.."WARNING: Group does not exist. Cannot get velocity!") end return nil end function OPSGROUP:GetHeading(UnitName) if self:IsExist()then local unit=nil if UnitName then unit=Unit.getByName(UnitName) else unit=self:GetDCSUnit() end if unit then local pos=unit:getPosition() local heading=math.atan2(pos.x.z,pos.x.x) if heading<0 then heading=heading+2*math.pi end heading=math.deg(heading) return heading end else self:T(self.lid.."WARNING: Group does not exist. Cannot get heading!") end return nil end function OPSGROUP:GetOrientation(UnitName) if self:IsExist()then local unit=nil if UnitName then unit=Unit.getByName(UnitName) else unit=self:GetDCSUnit() end if unit then local pos=unit:getPosition() return pos.x,pos.y,pos.z end else self:T(self.lid.."WARNING: Group does not exist. Cannot get orientation!") end return nil end function OPSGROUP:GetOrientationX(UnitName) local X,Y,Z=self:GetOrientation(UnitName) return X end function OPSGROUP:CheckTaskDescriptionUnique(description) for _,_task in pairs(self.taskqueue)do local task=_task if task.description==description then return false end end return true end function OPSGROUP:DespawnUnit(UnitName,Delay,NoEventRemoveUnit) self:T(self.lid.."Despawn element "..tostring(UnitName)) local element=self:GetElementByName(UnitName) if element then local DCSunit=Unit.getByName(UnitName) if DCSunit then DCSunit:destroy() self:ElementInUtero(element) if not NoEventRemoveUnit then self:CreateEventRemoveUnit(timer.getTime(),DCSunit) end end end end function OPSGROUP:DespawnElement(Element,Delay,NoEventRemoveUnit) if Delay and Delay>0 then self:ScheduleOnce(Delay,OPSGROUP.DespawnElement,self,Element,0,NoEventRemoveUnit) else if Element then local DCSunit=Unit.getByName(Element.name) if DCSunit then DCSunit:destroy() if not NoEventRemoveUnit then self:CreateEventRemoveUnit(timer.getTime(),DCSunit) end end end end return self end function OPSGROUP:Despawn(Delay,NoEventRemoveUnit) if Delay and Delay>0 then self.scheduleIDDespawn=self:ScheduleOnce(Delay,OPSGROUP.Despawn,self,0,NoEventRemoveUnit) else self:T(self.lid..string.format("Despawning Group!")) local DCSGroup=self:GetDCSGroup() if DCSGroup then local units=self:GetDCSUnits() for i=1,#units do local unit=units[i] if unit then local name=unit:getName() if name then self:DespawnUnit(name,0,NoEventRemoveUnit) end end end end end return self end function OPSGROUP:ReturnToLegion(Delay) if Delay and Delay>0 then self:ScheduleOnce(Delay,OPSGROUP.ReturnToLegion,self) else if self.legion then self:T(self.lid..string.format("Adding asset back to LEGION")) self.legion:AddAsset(self.group,1) else self:E(self.lid..string.format("ERROR: Group does not belong to a LEGION!")) end end return self end function OPSGROUP:DestroyUnit(UnitName,Delay) if Delay and Delay>0 then self:ScheduleOnce(Delay,OPSGROUP.DestroyUnit,self,UnitName,0) else local unit=Unit.getByName(UnitName) if unit then local EventTime=timer.getTime() if self:IsFlightgroup()then self:CreateEventUnitLost(EventTime,unit) else self:CreateEventDead(EventTime,unit) end unit:destroy() end end end function OPSGROUP:Destroy(Delay) if Delay and Delay>0 then self:ScheduleOnce(Delay,OPSGROUP.Destroy,self,0) else self:T(self.lid.."Destroying group!") local units=self:GetDCSUnits() if units then for _,unit in pairs(units)do if unit then self:DestroyUnit(unit:getName()) end end end end return self end function OPSGROUP:Activate(delay) if delay and delay>0 then self:T2(self.lid..string.format("Activating late activated group in %d seconds",delay)) self:ScheduleOnce(delay,OPSGROUP.Activate,self) else if self:IsAlive()==false then self:T(self.lid.."Activating late activated group") self.group:Activate() self.isLateActivated=false elseif self:IsAlive()==true then self:T(self.lid.."WARNING: Activating group that is already activated") else self:T(self.lid.."ERROR: Activating group that is does not exist!") end end return self end function OPSGROUP:Deactivate(delay) if delay and delay>0 then self:ScheduleOnce(delay,OPSGROUP.Deactivate,self) else if self:IsAlive()==true then self.template.lateActivation=true local template=UTILS.DeepCopy(self.template) self:_Respawn(0,template) end end return self end function OPSGROUP:SelfDestruction(Delay,ExplosionPower,ElementName) if Delay and Delay>0 then self:ScheduleOnce(Delay,OPSGROUP.SelfDestruction,self,0,ExplosionPower,ElementName) else for i,_element in pairs(self.elements)do local element=_element if ElementName==nil or ElementName==element.name then local unit=element.unit if unit and unit:IsAlive()then unit:Explode(ExplosionPower or 100) end end end end return self end function OPSGROUP:SetSRS(PathToSRS,Gender,Culture,Voice,Port,PathToGoogleKey,Label,Volume,Provider,Speaker) self.useSRS=true local path=PathToSRS or MSRS.path local port=Port or MSRS.port self.msrs=MSRS:New(path,self.frequency,self.modulation) self.msrs:SetGender(Gender) self.msrs:SetCulture(Culture) self.msrs:SetVoice(Voice) if Speaker then self.msrs:SetSpeakerPiper(Speaker) end self.msrs:SetPort(port) self.msrs:SetLabel(Label) if PathToGoogleKey then self.msrs:SetProviderOptionsGoogle(PathToGoogleKey,PathToGoogleKey) self.msrs:SetProvider(MSRS.Provider.GOOGLE) end if Provider then self.msrs:SetProvider(Provider) end self.msrs:SetCoalition(self:GetCoalition()) self.msrs:SetVolume(Volume) return self end function OPSGROUP:RadioTransmission(Text,Delay,SayCallsign,Frequency) if Delay and Delay>0 then self:ScheduleOnce(Delay,OPSGROUP.RadioTransmission,self,Text,0,SayCallsign) else if self.useSRS and self.msrs then local freq,modu,radioon=self:GetRadio() local coord=self:GetCoordinate() self.msrs:SetCoordinate(coord) if Frequency then self.msrs:SetFrequencies(Frequency) else self.msrs:SetFrequencies(freq) end self.msrs:SetModulations(modu) if SayCallsign then local callsign=self:GetCallsignName() Text=string.format("%s, %s",callsign,Text) end self:T(self.lid..string.format("Radio transmission on %.3f MHz %s: %s",freq,UTILS.GetModulationName(modu),Text)) self.msrs:PlayText(Text) end end return self end function OPSGROUP:SetCarrierLoaderAllAspect(Length,Width) self.carrierLoader.type="front" self.carrierLoader.length=Length or 50 self.carrierLoader.width=Width or 20 return self end function OPSGROUP:SetCarrierLoaderFront(Length,Width) self.carrierLoader.type="front" self.carrierLoader.length=Length or 50 self.carrierLoader.width=Width or 20 return self end function OPSGROUP:SetCarrierLoaderBack(Length,Width) self.carrierLoader.type="back" self.carrierLoader.length=Length or 50 self.carrierLoader.width=Width or 20 return self end function OPSGROUP:SetCarrierLoaderStarboard(Length,Width) self.carrierLoader.type="right" self.carrierLoader.length=Length or 50 self.carrierLoader.width=Width or 20 return self end function OPSGROUP:SetCarrierLoaderPort(Length,Width) self.carrierLoader.type="left" self.carrierLoader.length=Length or 50 self.carrierLoader.width=Width or 20 return self end function OPSGROUP:SetCarrierUnloaderAllAspect(Length,Width) self.carrierUnloader.type="front" self.carrierUnloader.length=Length or 50 self.carrierUnloader.width=Width or 20 return self end function OPSGROUP:SetCarrierUnloaderFront(Length,Width) self.carrierUnloader.type="front" self.carrierUnloader.length=Length or 50 self.carrierUnloader.width=Width or 20 return self end function OPSGROUP:SetCarrierUnloaderBack(Length,Width) self.carrierUnloader.type="back" self.carrierUnloader.length=Length or 50 self.carrierUnloader.width=Width or 20 return self end function OPSGROUP:SetCarrierUnloaderStarboard(Length,Width) self.carrierUnloader.type="right" self.carrierUnloader.length=Length or 50 self.carrierUnloader.width=Width or 20 return self end function OPSGROUP:SetCarrierUnloaderPort(Length,Width) self.carrierUnloader.type="left" self.carrierUnloader.length=Length or 50 self.carrierUnloader.width=Width or 20 return self end function OPSGROUP:IsInZone(Zone) local vec2=self:GetVec2() local is=false if vec2 then is=Zone:IsVec2InZone(vec2) else self:T3(self.lid.."WARNING: Cannot get vec2 at IsInZone()!") end return is end function OPSGROUP:Get2DDistance(Coordinate) local a=self:GetVec2() local b={} if Coordinate.z then b.x=Coordinate.x b.y=Coordinate.z else b.x=Coordinate.x b.y=Coordinate.y end local dist=UTILS.VecDist2D(a,b) return dist end function OPSGROUP:IsFlightgroup() return self.isFlightgroup end function OPSGROUP:IsArmygroup() return self.isArmygroup end function OPSGROUP:IsNavygroup() return self.isNavygroup end function OPSGROUP:IsExist() local DCSGroup=self:GetDCSGroup() if DCSGroup then local exists=DCSGroup:isExist() return exists end return nil end function OPSGROUP:IsActive() if self.group then local active=self.group:IsActive() return active end return nil end function OPSGROUP:IsAlive() if self.group then local alive=self.group:IsAlive() return alive end return nil end function OPSGROUP:IsLateActivated() return self.isLateActivated end function OPSGROUP:IsInUtero() local is=self:Is("InUtero")and not self:IsDead() return is end function OPSGROUP:IsSpawned() local is=self:Is("Spawned") return is end function OPSGROUP:IsDead() return self.isDead end function OPSGROUP:IsDestroyed() return self.isDestroyed end function OPSGROUP:IsStopped() local is=self:Is("Stopped") return is end function OPSGROUP:IsUncontrolled() return self.isUncontrolled end function OPSGROUP:HasPassedFinalWaypoint() return self.passedfinalwp end function OPSGROUP:IsRearming() local rearming=self:Is("Rearming")or self:Is("Rearm") return rearming end function OPSGROUP:IsOutOfAmmo() return self.outofAmmo end function OPSGROUP:IsOutOfBombs() return self.outofBombs end function OPSGROUP:IsOutOfGuns() return self.outofGuns end function OPSGROUP:IsOutOfMissiles() return self.outofMissiles end function OPSGROUP:IsOutOfTorpedos() return self.outofTorpedos end function OPSGROUP:IsOutOfA2GAmmo() if(self.outofMissilesAG and self.outofBombs and self.outofGuns)or self.outofAmmo then return true end return false end function OPSGROUP:IsLasing() return self.spot.On end function OPSGROUP:IsRetreating() local is=self:is("Retreating")or self:is("Retreated") return is end function OPSGROUP:IsRetreated() local is=self:is("Retreated") return is end function OPSGROUP:IsReturning() local is=self:is("Returning") return is end function OPSGROUP:IsEngaging() local is=self:is("Engaging") return is end function OPSGROUP:IsWaiting() if self.Twaiting then return true end return false end function OPSGROUP:IsNotCarrier() return self.carrierStatus==OPSGROUP.CarrierStatus.NOTCARRIER end function OPSGROUP:IsCarrier() return not self:IsNotCarrier() end function OPSGROUP:IsPickingup() return self.carrierStatus==OPSGROUP.CarrierStatus.PICKUP end function OPSGROUP:IsLoading() return self.carrierStatus==OPSGROUP.CarrierStatus.LOADING end function OPSGROUP:IsTransporting() return self.carrierStatus==OPSGROUP.CarrierStatus.TRANSPORTING end function OPSGROUP:IsUnloading() return self.carrierStatus==OPSGROUP.CarrierStatus.UNLOADING end function OPSGROUP:IsCargo(CheckTransport) return not self:IsNotCargo(CheckTransport) end function OPSGROUP:IsNotCargo(CheckTransport) local notcargo=self.cargoStatus==OPSGROUP.CargoStatus.NOTCARGO if notcargo then return true else if CheckTransport then if self.cargoTransportUID==nil then return true else return false end else return false end end return notcargo end function OPSGROUP:_AddMyLift(Transport) self.mylifts=self.mylifts or{} self.mylifts[Transport.uid]=true return self end function OPSGROUP:_DelMyLift(Transport) if self.mylifts then self.mylifts[Transport.uid]=nil end return self end function OPSGROUP:IsAwaitingLift(Transport) if self.mylifts then for uid,iswaiting in pairs(self.mylifts)do if Transport==nil or Transport.uid==uid then if iswaiting==true then return true end end end end return false end function OPSGROUP:_GetPausedMission() if self.pausedmissions and#self.pausedmissions>0 then for _,mid in pairs(self.pausedmissions)do if mid then local mission=self:GetMissionByID(mid) if mission and mission:IsNotOver()then return mission end end end end return nil end function OPSGROUP:_CountPausedMissions() local N=0 if self.pausedmissions and#self.pausedmissions>0 then for _,mid in pairs(self.pausedmissions)do local mission=self:GetMissionByID(mid) if mission and mission:IsNotOver()then N=N+1 end end end return N end function OPSGROUP:_RemovePausedMission(AuftragsNummer) if self.pausedmissions and#self.pausedmissions>0 then for i=#self.pausedmissions,1,-1 do local mid=self.pausedmissions[i] if mid==AuftragsNummer then table.remove(self.pausedmissions,i) return self end end end return self end function OPSGROUP:IsBoarding(CarrierGroupName) if CarrierGroupName then local carrierGroup=self:_GetMyCarrierGroup() if carrierGroup and carrierGroup.groupname~=CarrierGroupName then return false end end return self.cargoStatus==OPSGROUP.CargoStatus.BOARDING end function OPSGROUP:IsLoaded(CarrierGroupName) local isloaded=self.cargoStatus==OPSGROUP.CargoStatus.LOADED if not isloaded then return false end if CarrierGroupName then if type(CarrierGroupName)~="table"then CarrierGroupName={CarrierGroupName} end for _,CarrierName in pairs(CarrierGroupName)do local carrierGroup=self:_GetMyCarrierGroup() if carrierGroup and carrierGroup.groupname==CarrierName then return isloaded end end return false end return isloaded end function OPSGROUP:IsBusy() if self:IsBoarding()then return true end if self:IsRearming()then return true end if self:IsReturning()then return true end if self:IsPickingup()or self:IsLoading()or self:IsTransporting()or self:IsUnloading()then return true end if self:IsEngaging()then return true end return false end function OPSGROUP:GetWaypoints() return self.waypoints end function OPSGROUP:MarkWaypoints(Duration) for i,_waypoint in pairs(self.waypoints or{})do local waypoint=_waypoint local text=string.format("Waypoint ID=%d of %s",waypoint.uid,self.groupname) text=text..string.format("\nSpeed=%.1f kts, Alt=%d ft (%s)",UTILS.MpsToKnots(waypoint.speed),UTILS.MetersToFeet(waypoint.alt or 0),"BARO") if waypoint.marker then if waypoint.marker.text~=text then waypoint.marker.text=text end else waypoint.marker=MARKER:New(waypoint.coordinate,text):ToCoalition(self:GetCoalition()) end end if Duration then self:RemoveWaypointMarkers(Duration) end return self end function OPSGROUP:RemoveWaypointMarkers(Delay) if Delay and Delay>0 then self:ScheduleOnce(Delay,OPSGROUP.RemoveWaypointMarkers,self) else for i,_waypoint in pairs(self.waypoints or{})do local waypoint=_waypoint if waypoint.marker then waypoint.marker:Remove() end end end return self end function OPSGROUP:GetWaypointByID(uid) for _,_waypoint in pairs(self.waypoints or{})do local waypoint=_waypoint if waypoint.uid==uid then return waypoint end end return nil end function OPSGROUP:GetWaypointByIndex(index) for i,_waypoint in pairs(self.waypoints)do local waypoint=_waypoint if i==index then return waypoint end end return nil end function OPSGROUP:GetWaypointUIDFromIndex(index) for i,_waypoint in pairs(self.waypoints)do local waypoint=_waypoint if i==index then return waypoint.uid end end return nil end function OPSGROUP:GetWaypointIndex(uid) if uid then for i,_waypoint in pairs(self.waypoints or{})do local waypoint=_waypoint if waypoint.uid==uid then return i end end end return nil end function OPSGROUP:GetWaypointIndexNext(cyclic,i) if cyclic==nil then cyclic=self.adinfinitum end local N=#self.waypoints i=i or self.currentwp local n=math.min(i+1,N) if cyclic and i==N then n=1 end return n end function OPSGROUP:GetWaypointIndexCurrent() return self.currentwp or 1 end function OPSGROUP:GetWaypointIndexAfterID(uid) local index=self:GetWaypointIndex(uid) if index then return index+1 else return#self.waypoints+1 end end function OPSGROUP:GetWaypoint(indx) return self.waypoints[indx] end function OPSGROUP:GetWaypointFinal() return self.waypoints[#self.waypoints] end function OPSGROUP:GetWaypointNext(cyclic) local n=self:GetWaypointIndexNext(cyclic) return self.waypoints[n] end function OPSGROUP:GetWaypointCurrent() return self.waypoints[self.currentwp] end function OPSGROUP:GetWaypointCurrentUID() local wp=self:GetWaypointCurrent() if wp then return wp.uid end return nil end function OPSGROUP:GetNextWaypointCoordinate(cyclic) local waypoint=self:GetWaypointNext(cyclic) return waypoint.coordinate end function OPSGROUP:GetWaypointCoordinate(index) local waypoint=self:GetWaypoint(index) if waypoint then return waypoint.coordinate end return nil end function OPSGROUP:GetWaypointSpeed(indx) local waypoint=self:GetWaypoint(indx) if waypoint then return UTILS.MpsToKnots(waypoint.speed) end return nil end function OPSGROUP:GetWaypointUID(waypoint) return waypoint.uid end function OPSGROUP:GetWaypointID(indx) local waypoint=self:GetWaypoint(indx) if waypoint then return waypoint.uid end return nil end function OPSGROUP:GetSpeedToWaypoint(indx) local speed=self:GetWaypointSpeed(indx) if speed<=0.01 then speed=self:GetSpeedCruise() end return speed end function OPSGROUP:GetDistanceToWaypoint(indx) local dist=0 if#self.waypoints>0 then indx=indx or self:GetWaypointIndexNext() local wp=self:GetWaypoint(indx) if wp then local coord=self:GetCoordinate() dist=coord:Get2DDistance(wp.coordinate) end end return dist end function OPSGROUP:GetTimeToWaypoint(indx) local s=self:GetDistanceToWaypoint(indx) local v=self:GetVelocity() local t=s/v if t==math.inf then return 365*24*60*60 elseif t==math.nan then return 0 else return t end end function OPSGROUP:GetExpectedSpeed() if self:IsHolding()or self:Is("Rearming")or self:IsWaiting()or self:IsRetreated()then return 0 else return self.speedWp or 0 end end function OPSGROUP:RemoveWaypointByID(uid) local index=self:GetWaypointIndex(uid) if index then self:RemoveWaypoint(index) end return self end function OPSGROUP:RemoveWaypoint(wpindex) if self.waypoints then local wp=self:GetWaypoint(wpindex) local istemp=wp.temp or wp.detour or wp.astar or wp.missionUID local N=#self.waypoints if N==1 then self:T(self.lid..string.format("ERROR: Cannot remove waypoint with index=%d! It is the only waypoint and a group needs at least ONE waypoint",wpindex)) return self end if wpindex>N then self:T(self.lid..string.format("ERROR: Cannot remove waypoint with index=%d as there are only N=%d waypoints!",wpindex,N)) return self end if wp and wp.marker then wp.marker:Remove() end table.remove(self.waypoints,wpindex) local n=#self.waypoints self:T(self.lid..string.format("Removing waypoint UID=%d [temp=%s]: index=%d [currentwp=%d]. N %d-->%d",wp.uid,tostring(istemp),wpindex,self.currentwp,N,n)) if wpindex>self.currentwp then if self.currentwp>=n and not(self.adinfinitum or istemp)then self:_PassedFinalWaypoint(true,"Removed FUTURE waypoint we are currently moving to and that was the LAST waypoint") end self:_CheckGroupDone(1) else if self.currentwp==1 then if self.adinfinitum then self.currentwp=#self.waypoints else self.currentwp=1 end else self.currentwp=self.currentwp-1 end if(self.adinfinitum or istemp)then self:_PassedFinalWaypoint(false,"Removed PASSED temporary waypoint") end end end return self end function OPSGROUP:OnEventBirth(EventData) if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then local unit=EventData.IniUnit local group=EventData.IniGroup local unitname=EventData.IniUnitName if self.isFlightgroup then if EventData.Place then self.homebase=self.homebase or EventData.Place self.currbase=EventData.Place else self.currbase=nil end if self.homebase and not self.destbase then self.destbase=self.homebase end self:T(self.lid..string.format("EVENT: Element %s born at airbase %s ==> spawned",unitname,self.currbase and self.currbase:GetName()or"unknown")) else self:T3(self.lid..string.format("EVENT: Element %s born ==> spawned",unitname)) end local element=self:GetElementByName(unitname) if element and element.status~=OPSGROUP.ElementStatus.SPAWNED then self:T(self.lid..string.format("EVENT: Element %s born ==> spawned",unitname)) self:T2(self.lid..string.format("DCS unit=%s isExist=%s",tostring(EventData.IniDCSUnit:getName()),tostring(EventData.IniDCSUnit:isExist()))) self:ElementSpawned(element) end end end function OPSGROUP:OnEventHit(EventData) if EventData and EventData.TgtGroup and EventData.TgtUnit and EventData.TgtGroupName and EventData.TgtGroupName==self.groupname then self:T2(self.lid..string.format("EVENT: Unit %s hit!",EventData.TgtUnitName)) local unit=EventData.TgtUnit local group=EventData.TgtGroup local unitname=EventData.TgtUnitName local element=self:GetElementByName(unitname) self.Nhit=self.Nhit or 0 self.Nhit=self.Nhit+1 if element and element.status~=OPSGROUP.ElementStatus.DEAD then self:ElementHit(element,EventData.IniUnit) end end end function OPSGROUP:OnEventDead(EventData) if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then self:T2(self.lid..string.format("EVENT: Unit %s dead!",EventData.IniUnitName)) local unit=EventData.IniUnit local group=EventData.IniGroup local unitname=EventData.IniUnitName local element=self:GetElementByName(unitname) if element and element.status~=OPSGROUP.ElementStatus.DEAD then self:T(self.lid..string.format("EVENT: Element %s dead ==> destroyed",element.name)) self:ElementDestroyed(element) end end end function OPSGROUP:OnEventRemoveUnit(EventData) if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then self:T2(self.lid..string.format("EVENT: Unit %s removed!",EventData.IniUnitName)) local unit=EventData.IniUnit local group=EventData.IniGroup local unitname=EventData.IniUnitName local element=self:GetElementByName(unitname) if element and element.status~=OPSGROUP.ElementStatus.DEAD then self:T(self.lid..string.format("EVENT: Element %s removed ==> dead",element.name)) self:ElementDead(element) end end end function OPSGROUP:OnEventPlayerLeaveUnit(EventData) if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then self:T2(self.lid..string.format("EVENT: Player left Unit %s!",EventData.IniUnitName)) local unit=EventData.IniUnit local group=EventData.IniGroup local unitname=EventData.IniUnitName local element=self:GetElementByName(unitname) if element and element.status~=OPSGROUP.ElementStatus.DEAD then self:T(self.lid..string.format("EVENT: Player left Element %s ==> dead",element.name)) self:ElementDead(element) end end end function OPSGROUP:OnEventKill(EventData) if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then local targetname=tostring(EventData.TgtUnitName) self:T2(self.lid..string.format("EVENT: Unit %s killed object %s!",tostring(EventData.IniUnitName),targetname)) local target=UNIT:FindByName(targetname) if not target then target=STATIC:FindByName(targetname,false) end if target then self:T(self.lid..string.format("EVENT: Unit %s killed unit/static %s!",tostring(EventData.IniUnitName),targetname)) self.Nkills=self.Nkills+1 local mission=self:GetMissionCurrent() if mission then mission.Nkills=mission.Nkills+1 end end end end function OPSGROUP:SetTask(DCSTask) if self:IsAlive()then if self.taskenroute and#self.taskenroute>0 then if tostring(DCSTask.id)=="ComboTask"then for _,task in pairs(self.taskenroute)do table.insert(DCSTask.params.tasks,1,task) end else local tasks=UTILS.DeepCopy(self.taskenroute) table.insert(tasks,DCSTask) DCSTask=self.group.TaskCombo(self,tasks) end end self.controller:setTask(DCSTask) local text=string.format("SETTING Task %s",tostring(DCSTask.id)) if tostring(DCSTask.id)=="ComboTask"then for i,task in pairs(DCSTask.params.tasks)do text=text..string.format("\n[%d] %s",i,tostring(task.id)) end end self:T(self.lid..text) end return self end function OPSGROUP:PushTask(DCSTask) if self:IsAlive()then if self.taskenroute and#self.taskenroute>0 then if tostring(DCSTask.id)=="ComboTask"then for _,task in pairs(self.taskenroute)do table.insert(DCSTask.params.tasks,1,task) end else local tasks=UTILS.DeepCopy(self.taskenroute) table.insert(tasks,DCSTask) DCSTask=self.group.TaskCombo(self,tasks) end end self.controller:pushTask(DCSTask) local text=string.format("PUSHING Task %s",tostring(DCSTask.id)) if tostring(DCSTask.id)=="ComboTask"then for i,task in pairs(DCSTask.params.tasks)do text=text..string.format("\n[%d] %s",i,tostring(task.id)) end end self:T(self.lid..text) end return self end function OPSGROUP:HasTaskController() local hastask=nil if self.controller then hastask=self.controller:hasTask() end self:T3(self.lid..string.format("Controller hasTask=%s",tostring(hastask))) return hastask end function OPSGROUP:ClearTasks() local hastask=self:HasTaskController() if self:IsAlive()and self.controller and self:HasTaskController()then self:T(self.lid..string.format("CLEARING Tasks")) self.controller:resetTask() end return self end function OPSGROUP:AddTask(task,clock,description,prio,duration) local newtask=self:NewTaskScheduled(task,clock,description,prio,duration) table.insert(self.taskqueue,newtask) self:T(self.lid..string.format("Adding SCHEDULED task %s starting at %s",newtask.description,UTILS.SecondsToClock(newtask.time,true))) self:T3({newtask=newtask}) return newtask end function OPSGROUP:NewTaskScheduled(task,clock,description,prio,duration) self.taskcounter=self.taskcounter+1 local time=timer.getAbsTime()+5 if clock then if type(clock)=="string"then time=UTILS.ClockToSeconds(clock) elseif type(clock)=="number"then time=timer.getAbsTime()+clock end end local newtask={} newtask.status=OPSGROUP.TaskStatus.SCHEDULED newtask.dcstask=task newtask.description=description or task.id newtask.prio=prio or 50 newtask.time=time newtask.id=self.taskcounter newtask.duration=duration newtask.waypoint=-1 newtask.type=OPSGROUP.TaskType.SCHEDULED newtask.stopflag=USERFLAG:New(string.format("%s StopTaskFlag %d",self.groupname,newtask.id)) newtask.stopflag:Set(0) return newtask end function OPSGROUP:AddTaskWaypoint(task,Waypoint,description,prio,duration) Waypoint=Waypoint or self:GetWaypointNext() if Waypoint then self.taskcounter=self.taskcounter+1 local newtask={} newtask.description=description or string.format("Task #%d",self.taskcounter) newtask.status=OPSGROUP.TaskStatus.SCHEDULED newtask.dcstask=task newtask.prio=prio or 50 newtask.id=self.taskcounter newtask.duration=duration newtask.time=0 newtask.waypoint=Waypoint.uid newtask.type=OPSGROUP.TaskType.WAYPOINT newtask.stopflag=USERFLAG:New(string.format("%s StopTaskFlag %d",self.groupname,newtask.id)) newtask.stopflag:Set(0) table.insert(self.taskqueue,newtask) self:T(self.lid..string.format("Adding WAYPOINT task %s at WP ID=%d",newtask.description,newtask.waypoint)) self:T3({newtask=newtask}) return newtask end return nil end function OPSGROUP:AddTaskEnroute(task) if not self.taskenroute then self.taskenroute={} end local gotit=false for _,Task in pairs(self.taskenroute)do if Task.id==task.id then gotit=true break end end if not gotit then self:T(self.lid..string.format("Adding enroute task")) table.insert(self.taskenroute,task) end end function OPSGROUP:GetTasksWaypoint(id) local tasks={} self:_SortTaskQueue() for _,_task in pairs(self.taskqueue)do local task=_task if task.type==OPSGROUP.TaskType.WAYPOINT and task.status==OPSGROUP.TaskStatus.SCHEDULED and task.waypoint==id then table.insert(tasks,task) end end return tasks end function OPSGROUP:CountTasksWaypoint(id) local n=0 for _,_task in pairs(self.taskqueue)do local task=_task if task.type==OPSGROUP.TaskType.WAYPOINT and task.status==OPSGROUP.TaskStatus.SCHEDULED and task.waypoint==id then n=n+1 end end return n end function OPSGROUP:_SortTaskQueue() local function _sort(a,b) local taskA=a local taskB=b return(taskA.prio=task.time then return task end end return nil end function OPSGROUP:GetTaskCurrent() local task=self:GetTaskByID(self.taskcurrent,OPSGROUP.TaskStatus.EXECUTING) return task end function OPSGROUP:GetTaskByID(id,status) for _,_task in pairs(self.taskqueue)do local task=_task if task.id==id then if status==nil or status==task.status then return task end end end return nil end function OPSGROUP:onbeforeTaskExecute(From,Event,To,Task) local Mission=self:GetMissionByTaskID(Task.id) if Mission and(Mission.Tpush or#Mission.conditionPush>0)then if Mission:IsReadyToPush()then if self:IsWaiting()then self.Twaiting=nil self.dTwait=nil if self:IsFlightgroup()then self.flaghold:Set(1) end end else if self:IsWaiting()then else local alt=Mission.missionAltitude and UTILS.MetersToFeet(Mission.missionAltitude)or nil self:Wait(nil,alt) end local dt=Mission.Tpush and Mission.Tpush-timer.getAbsTime()or 20 self:T(self.lid..string.format("Mission %s task execute suspended for %d seconds",Mission.name,dt)) self:__TaskExecute(-dt,Task) return false end end if Mission and Mission.opstransport then local delivered=Mission.opstransport:IsCargoDelivered(self.groupname) if not delivered then local dt=30 self:T(self.lid..string.format("Mission %s task execute suspended for %d seconds because we were not delivered",Mission.name,dt)) self:__TaskExecute(-dt,Task) if(self:IsArmygroup()or self:IsNavygroup())and self:IsCruising()then self:FullStop() end return false end end return true end function OPSGROUP:onafterTaskExecute(From,Event,To,Task) local text=string.format("Task %s ID=%d execute",tostring(Task.description),Task.id) self:T(self.lid..text) self:T2({Task}) if self.taskcurrent>0 then self:TaskCancel() end self.taskcurrent=Task.id Task.timestamp=timer.getAbsTime() Task.status=OPSGROUP.TaskStatus.EXECUTING if self:GetTaskCurrent()==nil then table.insert(self.taskqueue,Task) end local Mission=self:GetMissionByTaskID(self.taskcurrent) self:_UpdateTask(Task,Mission) if Mission then self:MissionExecute(Mission) end end function OPSGROUP:_UpdateTask(Task,Mission) Mission=Mission or self:GetMissionByTaskID(self.taskcurrent) if Task.dcstask.id==AUFTRAG.SpecialTask.FORMATION then if Mission.type==AUFTRAG.Type.RESCUEHELO then self:T("**********") self:T("** RESCUEHELO USED") self:T("**********") local param=Task.dcstask.params local followUnit=UNIT:FindByName(param.unitname) local helogroupname=self:GetGroup():GetName() Task.formation=RESCUEHELO:New(followUnit,helogroupname) Task.formation:SetRespawnOnOff(false) Task.formation.respawninair=false Task.formation:SetTakeoffCold() Task.formation:SetHomeBase(followUnit) Task.formation.helo=self:GetGroup() Task.formation:Start() if self:IsFlightgroup()then self:SetDespawnAfterLanding() end else local followSet=SET_GROUP:New():AddGroup(self.group) local param=Task.dcstask.params local followUnit=UNIT:FindByName(param.unitname) Task.formation=FORMATION:New(followUnit,followSet,AUFTRAG.SpecialTask.FORMATION) Task.formation:FormationCenterWing(-param.offsetX,50,math.abs(param.altitude),50,param.offsetZ,50) Task.formation:SetFollowTimeInterval(param.dtFollow) Task.formation:Start() end elseif Task.dcstask.id==AUFTRAG.SpecialTask.PATROLZONE then local zone=Task.dcstask.params.zone local surfacetypes=nil if self:IsArmygroup()then surfacetypes={land.SurfaceType.LAND,land.SurfaceType.ROAD} elseif self:IsNavygroup()then surfacetypes={land.SurfaceType.WATER,land.SurfaceType.SHALLOW_WATER} end local Coordinate=zone:GetRandomCoordinate(nil,nil,surfacetypes) local Speed=Task.dcstask.params.speed and UTILS.MpsToKnots(Task.dcstask.params.speed)or UTILS.KmphToKnots(self.speedCruise) local Altitude=Task.dcstask.params.altitude and UTILS.MetersToFeet(Task.dcstask.params.altitude)or nil local currUID=self:GetWaypointCurrent().uid local wp=nil if self.isFlightgroup then wp=FLIGHTGROUP.AddWaypoint(self,Coordinate,Speed,currUID,Altitude) elseif self.isArmygroup then wp=ARMYGROUP.AddWaypoint(self,Coordinate,Speed,currUID,Task.dcstask.params.formation) elseif self.isNavygroup then wp=NAVYGROUP.AddWaypoint(self,Coordinate,Speed,currUID,Altitude) end wp.missionUID=Mission and Mission.auftragsnummer or nil elseif Task.dcstask.id==AUFTRAG.SpecialTask.RECON then local target=Task.dcstask.params.target self.reconindecies={} for i=1,#target.targets do table.insert(self.reconindecies,i) end local n=1 if Task.dcstask.params.randomly then n=UTILS.GetRandomTableElement(self.reconindecies) else table.remove(self.reconindecies,n) end local object=target.targets[n] local zone=object.Object local Coordinate=zone:GetRandomCoordinate() local Speed=Task.dcstask.params.speed and UTILS.MpsToKnots(Task.dcstask.params.speed)or UTILS.KmphToKnots(self.speedCruise) local Altitude=Task.dcstask.params.altitude and UTILS.MetersToFeet(Task.dcstask.params.altitude)or nil local currUID=self:GetWaypointCurrent().uid local wp=nil if self.isFlightgroup then wp=FLIGHTGROUP.AddWaypoint(self,Coordinate,Speed,currUID,Altitude) elseif self.isArmygroup then wp=ARMYGROUP.AddWaypoint(self,Coordinate,Speed,currUID,Task.dcstask.params.formation) elseif self.isNavygroup then wp=NAVYGROUP.AddWaypoint(self,Coordinate,Speed,currUID,Altitude) end wp.missionUID=Mission and Mission.auftragsnummer or nil elseif Task.dcstask.id==AUFTRAG.SpecialTask.AMMOSUPPLY or Task.dcstask.id==AUFTRAG.SpecialTask.FUELSUPPLY then elseif Task.dcstask.id==AUFTRAG.SpecialTask.REARMING then local rearmed=self:_CheckAmmoFull() if rearmed then self:T2(self.lid.."Ammo already full ==> reaming task done!") self:TaskDone(Task) else self:T2(self.lid.."Ammo not full ==> Rearm()") self:Rearm() end elseif Task.dcstask.id==AUFTRAG.SpecialTask.ALERT5 then elseif Task.dcstask.id==AUFTRAG.SpecialTask.ONGUARD or Task.dcstask.id==AUFTRAG.SpecialTask.ARMOREDGUARD then if self:IsArmygroup()or self:IsNavygroup()then self:FullStop() else end elseif Task.dcstask.id==AUFTRAG.SpecialTask.NOTHING then if self:IsArmygroup()or self:IsNavygroup()then self:__FullStop(0.1) else end elseif Task.dcstask.id==AUFTRAG.SpecialTask.AIRDEFENSE or Task.dcstask.id==AUFTRAG.SpecialTask.EWR then if self:IsArmygroup()or self:IsNavygroup()then self:FullStop() else end elseif Task.dcstask.id==AUFTRAG.SpecialTask.GROUNDATTACK or Task.dcstask.id==AUFTRAG.SpecialTask.ARMORATTACK then local target=Task.dcstask.params.target local speed=self.speedMax and UTILS.KmphToKnots(self.speedMax)or nil if Task.dcstask.params.speed then speed=UTILS.MpsToKnots(Task.dcstask.params.speed) end if target then self:EngageTarget(target,speed,Task.dcstask.params.formation) end elseif Task.dcstask.id==AUFTRAG.SpecialTask.NAVALENGAGEMENT then local target=Task.dcstask.params.target local speed=self.speedMax and UTILS.KmphToKnots(self.speedMax)or nil if Task.dcstask.params.speed then speed=UTILS.MpsToKnots(Task.dcstask.params.speed) end if target then self:EngageTarget(target,speed,Task.dcstask.params.altitude) end elseif Task.dcstask.id==AUFTRAG.SpecialTask.PATROLRACETRACK then if self.isFlightgroup then self:T("We are Special Auftrag Patrol Race Track, starting now ...") local aircraft=self:GetGroup() aircraft:PatrolRaceTrack(Task.dcstask.params.TrackPoint1,Task.dcstask.params.TrackPoint2,Task.dcstask.params.TrackAltitude,Task.dcstask.params.TrackSpeed,Task.dcstask.params.TrackFormation,false,1) end elseif Task.dcstask.id==AUFTRAG.SpecialTask.HOVER then if self.isFlightgroup then self:T("We are Special Auftrag HOVER, hovering now ...") local alt=Task.dcstask.params.hoverAltitude local time=Task.dcstask.params.hoverTime local mSpeed=Task.dcstask.params.missionSpeed or self.speedCruise or 150 local Speed=UTILS.KmphToKnots(mSpeed) local CruiseAlt=UTILS.FeetToMeters(Task.dcstask.params.missionAltitude or 1000) local helo=self:GetGroup() helo:SetSpeed(0.01,true) helo:SetAltitude(alt,true,"BARO") self:HoverStart() local function FlyOn(Helo,Speed,CruiseAlt,Task) if Helo then Helo:SetSpeed(Speed,true) Helo:SetAltitude(CruiseAlt,true,"BARO") self:T("We are Special Auftrag HOVER, end of hovering now ...") self:TaskDone(Task) self:HoverEnd() end end local timer=TIMER:New(FlyOn,helo,Speed,CruiseAlt,Task) timer:Start(time) end elseif Task.dcstask.id==AUFTRAG.SpecialTask.RELOCATECOHORT then self:T(self.lid.."Executing task for relocation mission") local legion=Task.dcstask.params.legion local Coordinate=legion.spawnzone:GetRandomCoordinate() local currUID=self:GetWaypointCurrent().uid local wp=nil if self.isArmygroup then self:T2(self.lid.."Routing group to spawn zone of new legion") wp=ARMYGROUP.AddWaypoint(self,Coordinate,UTILS.KmphToKnots(self.speedCruise),currUID,Mission.optionFormation) elseif self.isFlightgroup then self:T2(self.lid.."Routing group to intermediate point near new legion") Coordinate=self:GetCoordinate():GetIntermediateCoordinate(Coordinate,0.8) wp=FLIGHTGROUP.AddWaypoint(self,Coordinate,UTILS.KmphToKnots(self.speedCruise),currUID,UTILS.MetersToFeet(self.altitudeCruise)) elseif self.isNavygroup then self:T2(self.lid.."Routing group to spawn zone of new legion") wp=NAVYGROUP.AddWaypoint(self,Coordinate,UTILS.KmphToKnots(self.speedCruise),currUID) else end wp.missionUID=Mission and Mission.auftragsnummer or nil elseif Task.dcstask.id==AUFTRAG.SpecialTask.CAPTUREZONE then if self:IsEngaging()then self:T2(self.lid..string.format("CaptureZone: Engaging currently!")) else local Coalitions=UTILS.GetCoalitionEnemy(self:GetCoalition(),false) local zoneCurr=Task.target if zoneCurr then self:T(self.lid..string.format("Current target zone=%s owner=%s",zoneCurr:GetName(),zoneCurr:GetOwnerName())) if zoneCurr:GetOwner()==self:GetCoalition()then self:T(self.lid..string.format("Zone %s captured ==> Task DONE!",zoneCurr:GetName())) if Task.StayInZoneTime then local stay=Task.StayInZoneTime self:__TaskDone(stay,Task) else self:TaskDone(Task) end else self:T(self.lid..string.format("Zone %s NOT captured!",zoneCurr:GetName())) if Mission:GetGroupStatus(self)==AUFTRAG.GroupStatus.EXECUTING then self:T(self.lid..string.format("Zone %s NOT captured and EXECUTING ==> Find target",zoneCurr:GetName())) local targetgroup=zoneCurr:GetScannedGroupSet():GetClosestGroup(self.coordinate,Coalitions) if targetgroup then self:T(self.lid..string.format("Zone %s NOT captured: engaging target %s",zoneCurr:GetName(),targetgroup:GetName())) self:EngageTarget(targetgroup) else if self:IsFlightgroup()then self:T(self.lid..string.format("Zone %s not captured but no target group could be found ==> TaskDone as FLIGHTGROUPS cannot capture zones",zoneCurr:GetName())) self:TaskDone(Task) else self:T(self.lid..string.format("Zone %s not captured but no target group could be found. Should be captured in the next zone evaluation.",zoneCurr:GetName())) end end else self:T(self.lid..string.format("Zone %s NOT captured and NOT EXECUTING",zoneCurr:GetName())) end end else self:T(self.lid..string.format("NO Current target zone=%s")) end end else if Task.type==OPSGROUP.TaskType.SCHEDULED or Task.ismission then local DCSTask=nil if Task.dcstask.id==AUFTRAG.SpecialTask.BARRAGE then local vec2=self:GetVec2() local param=Task.dcstask.params local heading=param.heading or math.random(1,360) local Altitude=param.altitude or 500 local Alpha=param.angle or math.random(45,85) local distance=Altitude/math.tan(math.rad(Alpha)) local tvec2=UTILS.Vec2Translate(vec2,distance,heading) self:T(self.lid..string.format("Barrage: Shots=%s, Altitude=%d m, Angle=%d°, heading=%03d°, distance=%d m",tostring(param.shots),Altitude,Alpha,heading,distance)) DCSTask=CONTROLLABLE.TaskFireAtPoint(nil,tvec2,param.radius,param.shots,param.weaponType,Altitude) elseif Task.ismission and Task.dcstask.id=='FireAtPoint'then DCSTask=UTILS.DeepCopy(Task.dcstask) local ammo=self:GetAmmoTot() local nAmmo=ammo.Total local weaponType=DCSTask.params.weaponType or-1 if weaponType==ENUMS.WeaponFlag.CruiseMissile then nAmmo=ammo.MissilesCR elseif weaponType==ENUMS.WeaponFlag.AnyRocket then nAmmo=ammo.Rockets elseif weaponType==ENUMS.WeaponFlag.Cannons then nAmmo=ammo.Cannons end local nShots=DCSTask.params.expendQty or 1 self:T(self.lid..string.format("Fire at point with nshots=%d of %d",nShots,nAmmo)) if nShots==-1 then nShots=nAmmo self:T(self.lid..string.format("Fire at point taking max amount of ammo = %d",nShots)) elseif nShots<1 then local p=nShots nShots=UTILS.Round(p*nAmmo,0) self:T(self.lid..string.format("Fire at point taking %.1f percent amount of ammo = %d",p,nShots)) else nShots=math.min(nShots,nAmmo) end DCSTask.params.expendQty=nShots else DCSTask=Task.dcstask end self:_SandwitchDCSTask(DCSTask,Task) elseif Task.type==OPSGROUP.TaskType.WAYPOINT then else self:T(self.lid.."ERROR: Unknown task type: ") end end end function OPSGROUP:_SandwitchDCSTask(DCSTask,Task,SetTask,Delay) if Delay and Delay>0 then self:ScheduleOnce(Delay,OPSGROUP._SandwitchDCSTask,self,DCSTask,Task,SetTask) else local DCStasks={} if DCSTask.id=='ComboTask'then for TaskID,Task in ipairs(DCSTask.params.tasks)do table.insert(DCStasks,Task) end else table.insert(DCStasks,DCSTask) end local TaskCombo=self.group:TaskCombo(DCStasks) local TaskCondition=self.group:TaskCondition(nil,Task.stopflag:GetName(),1,nil,Task.duration) local TaskControlled=self.group:TaskControlled(TaskCombo,TaskCondition) local TaskDone=self.group:TaskFunction("OPSGROUP._TaskDone",self,Task) local TaskFinal=self.group:TaskCombo({TaskControlled,TaskDone}) if SetTask then self:SetTask(TaskFinal) else self:PushTask(TaskFinal) end end end function OPSGROUP:onafterTaskCancel(From,Event,To,Task) local currenttask=self:GetTaskCurrent() Task=Task or currenttask if Task then if currenttask and Task.id==currenttask.id then local stopflag=Task.stopflag:Get() local text=string.format("Current task %s ID=%d cancelled (flag %s=%d)",Task.description,Task.id,Task.stopflag:GetName(),stopflag) self:T(self.lid..text) Task.stopflag:Set(1) local done=false if Task.dcstask.id==AUFTRAG.SpecialTask.FORMATION then Task.formation:Stop() done=true elseif Task.dcstask.id==AUFTRAG.SpecialTask.PATROLZONE then done=true elseif Task.dcstask.id==AUFTRAG.SpecialTask.RECON then done=true elseif Task.dcstask.id==AUFTRAG.SpecialTask.AMMOSUPPLY then done=true elseif Task.dcstask.id==AUFTRAG.SpecialTask.FUELSUPPLY then done=true elseif Task.dcstask.id==AUFTRAG.SpecialTask.REARMING then done=true elseif Task.dcstask.id==AUFTRAG.SpecialTask.ALERT5 then done=true elseif Task.dcstask.id==AUFTRAG.SpecialTask.ONGUARD or Task.dcstask.id==AUFTRAG.SpecialTask.ARMOREDGUARD then done=true elseif Task.dcstask.id==AUFTRAG.SpecialTask.GROUNDATTACK or Task.dcstask.id==AUFTRAG.SpecialTask.ARMORATTACK or Task.dcstask.id==AUFTRAG.SpecialTask.NAVALENGAGEMENT then done=true elseif Task.dcstask.id==AUFTRAG.SpecialTask.NOTHING then done=true elseif stopflag==1 or(not self:IsAlive())or self:IsDead()or self:IsStopped()then done=true end if done then self:TaskDone(Task) end else self:T(self.lid..string.format("TaskCancel: Setting task %s ID=%d to DONE",Task.description,Task.id)) self:TaskDone(Task) end else local text=string.format("WARNING: No (current) task to cancel!") self:T(self.lid..text) end end function OPSGROUP:onbeforeTaskDone(From,Event,To,Task) local allowed=true if Task.status==OPSGROUP.TaskStatus.PAUSED then allowed=false end return allowed end function OPSGROUP:onafterTaskDone(From,Event,To,Task) local text=string.format("Task done: %s ID=%d",Task.description,Task.id) self:T(self.lid..text) if Task.id==self.taskcurrent then self.taskcurrent=0 end Task.status=OPSGROUP.TaskStatus.DONE if Task.backupROE then self:SwitchROE(Task.backupROE) end local Mission=self:GetMissionByTaskID(Task.id) if Mission and Mission:IsNotOver()then local status=Mission:GetGroupStatus(self) if status~=AUFTRAG.GroupStatus.PAUSED then if Mission.type==AUFTRAG.Type.CAPTUREZONE and Mission:CountMissionTargets()>0 then self:T(self.lid.."Remove mission waypoints") self:_RemoveMissionWaypoints(Mission,false) if self:IsFlightgroup()then else self:T(self.lid.."Task done ==> Route to mission for next opszone") self:MissionStart(Mission) return end end local EgressUID=Mission:GetGroupEgressWaypointUID(self) if EgressUID then self:T(self.lid..string.format("Task Done but Egress waypoint defined ==> Will call Mission Done once group passed waypoint UID=%d!",EgressUID)) else self:T(self.lid.."Task Done ==> Mission Done!") self:MissionDone(Mission) end else if self:IsOnMission(Mission.auftragsnummer)then self.currentmission=nil end self:T(self.lid.."Remove mission waypoints") self:_RemoveMissionWaypoints(Mission,false) end else if Task.description=="Engage_Target"then self:T(self.lid.."Task DONE Engage_Target ==> Cruise") self:Disengage() end if Task.description==AUFTRAG.SpecialTask.ONGUARD or Task.description==AUFTRAG.SpecialTask.ARMOREDGUARD or Task.description==AUFTRAG.SpecialTask.NOTHING then self:T(self.lid.."Task DONE OnGuard ==> Cruise") self:Cruise() end if Task.description=="Task_Land_At"then self:T(self.lid.."Taske DONE Task_Land_At ==> Wait") self:Wait(20,100) else self:T(self.lid.."Task Done but NO mission found ==> _CheckGroupDone in 1 sec") self:_CheckGroupDone(1) end end end function OPSGROUP:AddMission(Mission) Mission:AddOpsGroup(self) Mission:SetGroupStatus(self,AUFTRAG.GroupStatus.SCHEDULED) Mission:Scheduled() Mission.Nelements=Mission.Nelements+#self.elements Mission.Ngroups=Mission.Ngroups+1 table.insert(self.missionqueue,Mission) self.adinfinitum=Mission.DCStask.params.adinfinitum and Mission.DCStask.params.adinfinitum or false local text=string.format("Added %s mission %s starting at %s, stopping at %s", tostring(Mission.type),tostring(Mission.name),UTILS.SecondsToClock(Mission.Tstart,true),Mission.Tstop and UTILS.SecondsToClock(Mission.Tstop,true)or"INF") self:T(self.lid..text) return self end function OPSGROUP:RemoveMission(Mission) for i=#self.missionqueue,1,-1 do local mission=self.missionqueue[i] if mission.auftragsnummer==Mission.auftragsnummer then local Task=Mission:GetGroupWaypointTask(self) if Task then self:RemoveTask(Task) end for j=#self.pausedmissions,1,-1 do local mid=self.pausedmissions[j] if Mission.auftragsnummer==mid then table.remove(self.pausedmissions,j) end end table.remove(self.missionqueue,i) return self end end return self end function OPSGROUP:CancelAllMissions() self:T(self.lid.."Cancelling ALL missions!") for _,_mission in pairs(self.missionqueue)do local mission=_mission local mystatus=mission:GetGroupStatus(self) if not(mystatus==AUFTRAG.GroupStatus.DONE or mystatus==AUFTRAG.GroupStatus.CANCELLED)then self:T(self.lid.."Cancelling mission "..tostring(mission:GetName())) self:MissionCancel(mission) end end end function OPSGROUP:CountRemainingMissison() local N=0 for _,_mission in pairs(self.missionqueue)do local mission=_mission if mission and mission:IsNotOver()then local status=mission:GetGroupStatus(self) if status~=AUFTRAG.GroupStatus.DONE and status~=AUFTRAG.GroupStatus.CANCELLED then N=N+1 end end end return N end function OPSGROUP:CountRemainingTransports() local N=0 for _,_transport in pairs(self.cargoqueue)do local transport=_transport local mystatus=transport:GetCarrierTransportStatus(self) local status=transport:GetState() self:T(self.lid..string.format("Transport my status=%s [%s]",mystatus,status)) if transport and mystatus==OPSTRANSPORT.Status.SCHEDULED and status~=OPSTRANSPORT.Status.DELIVERED and status~=OPSTRANSPORT.Status.CANCELLED then N=N+1 end end if N==0 and self.cargoTransport and self.cargoTransport:GetState()~=OPSTRANSPORT.Status.DELIVERED and self.cargoTransport:GetCarrierTransportStatus(self)~=OPSTRANSPORT.Status.DELIVERED and self.cargoTransport:GetState()~=OPSTRANSPORT.Status.CANCELLED and self.cargoTransport:GetCarrierTransportStatus(self)~=OPSTRANSPORT.Status.CANCELLED then N=1 end return N end function OPSGROUP:_GetNextMission() if self:IsPickingup()or self:IsLoading()or self:IsTransporting()or self:IsUnloading()or self:IsLoaded()then return nil end local Nmissions=#self.missionqueue if Nmissions==0 then return nil end local function _sort(a,b) local taskA=a local taskB=b return(taskA.prio3.6 or true then self:RouteToMission(Mission,3) else self:T(self.lid.."Immobile GROUP!") local Clock=Mission.Tpush and UTILS.SecondsToClock(Mission.Tpush)or 5 local Task=self:AddTask(Mission.DCStask,Clock,Mission.name,Mission.prio,Mission.duration) Task.ismission=true Mission:SetGroupWaypointTask(self,Task) self:__TaskExecute(3,Task) end end function OPSGROUP:onafterMissionExecute(From,Event,To,Mission) local text=string.format("Executing %s Mission %s, target %s",Mission.type,tostring(Mission.name),Mission:GetTargetName()) self:T(self.lid..text) Mission:SetGroupStatus(self,AUFTRAG.GroupStatus.EXECUTING) Mission:Executing() if self:IsHolding()and not self:HasPassedFinalWaypoint()then self:Cruise() end if Mission.engagedetectedOn then self:SetEngageDetectedOn(UTILS.MetersToNM(Mission.engagedetectedRmax),Mission.engagedetectedTypes,Mission.engagedetectedEngageZones,Mission.engagedetectedNoEngageZones) end if self.isFlightgroup then if Mission.prohibitABExecute==true then self:SetProhibitAfterburner() self:T(self.lid.."Set prohibit AB") elseif Mission.prohibitABExecute==false then self:SetAllowAfterburner() self:T2(self.lid.."Set allow AB") end end end function OPSGROUP:onafterPauseMission(From,Event,To) local Mission=self:GetMissionCurrent() if Mission then Mission:SetGroupStatus(self,AUFTRAG.GroupStatus.PAUSED) local Task=Mission:GetGroupWaypointTask(self) self:T(self.lid..string.format("Pausing current mission %s. Task=%s",tostring(Mission.name),tostring(Task and Task.description or"WTF"))) self:TaskCancel(Task) self:_RemoveMissionWaypoints(Mission) table.insert(self.pausedmissions,1,Mission.auftragsnummer) end end function OPSGROUP:onafterUnpauseMission(From,Event,To) local mission=self:_GetPausedMission() if mission then self:T(self.lid..string.format("Unpausing mission %s [%s]",mission:GetName(),mission:GetType())) mission.unpaused=true self:MissionStart(mission) for i,mid in pairs(self.pausedmissions)do if mid==mission.auftragsnummer then self:T(self.lid..string.format("Removing paused mission id=%d",mid)) table.remove(self.pausedmissions,i) break end end else self:T(self.lid.."ERROR: No mission to unpause!") end end function OPSGROUP:onafterMissionCancel(From,Event,To,Mission) if self:IsOnMission(Mission.auftragsnummer)then local Task=Mission:GetGroupWaypointTask(self) if Task then self:T(self.lid..string.format("Cancel current mission %s. Task=%s",tostring(Mission.name),tostring(Task and Task.description or"WTF"))) self:TaskCancel(Task) else self:MissionDone(Mission) end else Mission:SetGroupStatus(self,AUFTRAG.GroupStatus.CANCELLED) self:RemoveMission(Mission) self:_CheckGroupDone(1) end end function OPSGROUP:_RemoveMissionWaypoints(Mission,Silently) for i=#self.waypoints,1,-1 do local wp=self.waypoints[i] if wp.missionUID==Mission.auftragsnummer then if Silently then table.remove(self.waypoints,i) else self:RemoveWaypoint(i) end end end end function OPSGROUP:onafterMissionDone(From,Event,To,Mission) local text=string.format("Mission DONE %s!",Mission.name) self:T(self.lid..text) Mission:SetGroupStatus(self,AUFTRAG.GroupStatus.DONE) if self:IsOnMission(Mission.auftragsnummer)then self.currentmission=nil end self:_RemoveMissionWaypoints(Mission) if Mission.patroldata then Mission.patroldata.noccupied=Mission.patroldata.noccupied-1 AIRWING.UpdatePatrolPointMarker(self,Mission.patroldata) end if Mission.engagedetectedOn then self:SetEngageDetectedOff() end if Mission.optionROE then self:SwitchROE() end if self:IsFlightgroup()and Mission.optionROT then self:SwitchROT() end if Mission.optionAlarm then self:SwitchAlarmstate() end if Mission.optionEPLRS then self:SwitchEPLRS() end if Mission.optionEmission then self:SwitchEmission() end if Mission.optionInvisible then self:SwitchInvisible() end if Mission.optionImmortal then self:SwitchImmortal() end if Mission.optionFormation and self:IsFlightgroup()then self:SwitchFormation() end if Mission.radio then self:SwitchRadio() end if Mission.tacan then self:_SwitchTACAN() local cohort=self.cohort if cohort then cohort:ReturnTacan(Mission.tacan.Channel) end local asset=Mission:GetAssetByName(self.groupname) if asset then asset.tacan=nil end end if Mission.icls then self:_SwitchICLS() end if self.legion and Mission.legionReturn~=nil then self:SetReturnToLegion(Mission.legionReturn) end local delay=1 if Mission.type==AUFTRAG.Type.ARTY then delay=60 elseif Mission.type==AUFTRAG.Type.RELOCATECOHORT then local legion=Mission.DCStask.params.legion self:T(self.lid..string.format("Asset relocated to new legion=%s",tostring(legion.alias))) local asset=Mission:GetAssetByName(self.groupname) if asset then asset.wid=legion.uid end self.legion=legion if self.isArmygroup then self:T2(self.lid.."Adding asset via ReturnToLegion()") self:ReturnToLegion() elseif self.isFlightgroup then self:T2(self.lid.."Adding asset via RTB to new legion airbase") self:RTB(self.legion.airbase) end return end if self.isFlightgroup then if Mission.prohibitAB==true then self:T2("Setting prohibit AB") self:SetProhibitAfterburner() elseif Mission.prohibitAB==false then self:T2("Setting allow AB") self:SetAllowAfterburner() end end if self.legion and self.legionReturn==false and self.waypoints and#self.waypoints==1 then local Coordinate=self:GetCoordinate() if self.isArmygroup then ARMYGROUP.AddWaypoint(self,Coordinate,0,nil,nil,false) elseif self.isNavygroup then NAVYGROUP.AddWaypoint(self,Coordinate,0,nil,nil,false) end self:RemoveWaypoint(1) self:_PassedFinalWaypoint(true,"Passed final waypoint as group is done with mission but should NOT return to its legion") end self:_CheckGroupDone(delay) end function OPSGROUP:RouteToMission(mission,delay) if delay and delay>0 then self:ScheduleOnce(delay,OPSGROUP.RouteToMission,self,mission) else self:T(self.lid..string.format("Route To Mission")) local delayGo=-1 if self:IsDead()or self:IsStopped()then self:T(self.lid..string.format("Route To Mission: I am DEAD or STOPPED! Ooops...")) return end if self:IsCargo()then self:T(self.lid..string.format("Route To Mission: I am CARGO! You cannot route me...")) return end if mission.type==AUFTRAG.Type.OPSTRANSPORT then self:T(self.lid..string.format("Route To Mission: I am OPSTRANSPORT! Add transport and return...")) self:AddOpsTransport(mission.opstransport) return end if mission.type==AUFTRAG.Type.ALERT5 then self:T(self.lid..string.format("Route To Mission: I am ALERT5! Go right to MissionExecute()...")) self:MissionExecute(mission) return end local uid=self:GetWaypointCurrentUID() local waypointcoord=nil local currentcoord=self:GetCoordinate() local roadcoord=currentcoord:GetClosestPointToRoad() local roaddist=nil if roadcoord then roaddist=currentcoord:Get2DDistance(roadcoord) end local targetzone=nil local randomradius=mission.missionWaypointRadius or 1000 local surfacetypes=nil if self:IsArmygroup()then surfacetypes={land.SurfaceType.LAND,land.SurfaceType.ROAD} elseif self:IsNavygroup()then surfacetypes={land.SurfaceType.WATER,land.SurfaceType.SHALLOW_WATER} end local targetobject=mission:GetObjective(currentcoord,UTILS.GetCoalitionEnemy(self:GetCoalition(),true)) if targetobject then self:T(self.lid..string.format("Route to mission target object %s",targetobject:GetName())) end if mission.opstransport and not mission.opstransport:IsCargoDelivered(self.groupname)then local tzc=mission.opstransport:GetTZCofCargo(self.groupname) local pickupzone=tzc.PickupZone if self:IsInZone(pickupzone)then self:PauseMission() self:FullStop() return else waypointcoord=pickupzone:GetRandomCoordinate() end elseif mission.type==AUFTRAG.Type.PATROLZONE or mission.type==AUFTRAG.Type.BARRAGE or mission.type==AUFTRAG.Type.AMMOSUPPLY or mission.type==AUFTRAG.Type.FUELSUPPLY or mission.type==AUFTRAG.Type.REARMING or mission.type==AUFTRAG.Type.AIRDEFENSE or mission.type==AUFTRAG.Type.EWR then targetzone=targetobject waypointcoord=targetzone:GetRandomCoordinate(nil,nil,surfacetypes) elseif mission.type==AUFTRAG.Type.ONGUARD or mission.type==AUFTRAG.Type.ARMOREDGUARD then waypointcoord=mission:GetMissionWaypointCoord(self.group,nil,surfacetypes) elseif mission.type==AUFTRAG.Type.NOTHING then targetzone=targetobject waypointcoord=targetzone:GetRandomCoordinate(nil,nil,surfacetypes) elseif mission.type==AUFTRAG.Type.HOVER then local zone=targetobject waypointcoord=zone:GetCoordinate() elseif mission.type==AUFTRAG.Type.RELOCATECOHORT then local ToCoordinate=mission.DCStask.params.legion:GetCoordinate() if self.isFlightgroup then waypointcoord=currentcoord:GetIntermediateCoordinate(ToCoordinate,0.2):SetAltitude(self.altitudeCruise) elseif self.isArmygroup then if roadcoord then waypointcoord=roadcoord else waypointcoord=currentcoord:GetIntermediateCoordinate(ToCoordinate,100) end else waypointcoord=currentcoord:GetIntermediateCoordinate(ToCoordinate,0.05) end elseif mission.type==AUFTRAG.Type.CAPTUREZONE then targetzone=targetobject:GetZone() waypointcoord=targetzone:GetRandomCoordinate(nil,nil,surfacetypes) else waypointcoord=mission:GetMissionWaypointCoord(self.group,randomradius,surfacetypes) end for _,task in pairs(mission.enrouteTasks)do self:AddTaskEnroute(task) end local SpeedToMission=mission.missionSpeed and UTILS.KmphToKnots(mission.missionSpeed)or self:GetSpeedCruise() if mission.type==AUFTRAG.Type.TROOPTRANSPORT then mission.DCStask=mission:GetDCSMissionTask(self.group) local pradius=mission.transportPickupRadius local pickupZone=ZONE_RADIUS:New("Pickup Zone",mission.transportPickup:GetVec2(),pradius) for _,_group in pairs(mission.transportGroupSet.Set)do local group=_group if group and group:IsAlive()then local pcoord=pickupZone:GetRandomCoordinate(20,pradius,{land.SurfaceType.LAND,land.SurfaceType.ROAD}) local DCSTask=group:TaskEmbarkToTransport(pcoord,pradius) group:SetTask(DCSTask,5) end end elseif mission.type==AUFTRAG.Type.FREIGHTTRANSPORT then local destination=mission.DCStask.params.destination local cargo=mission.DCStask.params.cargo waypointcoord=destination:GetCoordinate() mission.DCStask.params.destination=destination mission.DCStask.params.cargo=cargo local unit=self.group:GetFirstUnit() local unitIdTransport=unit:GetID() local vec2=unit:GetVec2() local tasks={} for StaticName,StaticObject in pairs(cargo:GetSet())do local static=StaticObject local TaskCargoTransportation={ id="CargoTransportationPlane", params={ x=vec2.x, y=vec2.y, unitIdTransport=unitIdTransport, groupId=static:GetID(), unitId=static:GetID(), } } table.insert(tasks,TaskCargoTransportation) end local TaskCargo=nil if#tasks==1 then TaskCargo=tasks[1] else TaskCargo=CONTROLLABLE.TaskCombo(nil,tasks) end self:_ClearFSMEvent("UpdateRoute") delayGo=-50 self.group:SetTask(TaskCargo) elseif mission.type==AUFTRAG.Type.ARTY then local targetcoord=mission:GetTargetCoordinate() local inRange=self:InWeaponRange(targetcoord,mission.engageWeaponType,waypointcoord) if inRange then else local coordInRange=self:GetCoordinateInRange(targetcoord,mission.engageWeaponType,waypointcoord,surfacetypes) if coordInRange then local waypoint=nil if self:IsFlightgroup()then waypoint=FLIGHTGROUP.AddWaypoint(self,waypointcoord,SpeedToMission,uid,UTILS.MetersToFeet(mission.missionAltitude or self.altitudeCruise),false) elseif self:IsArmygroup()then waypoint=ARMYGROUP.AddWaypoint(self,waypointcoord,SpeedToMission,uid,mission.optionFormation,false) elseif self:IsNavygroup()then waypoint=NAVYGROUP.AddWaypoint(self,waypointcoord,SpeedToMission,uid,UTILS.MetersToFeet(mission.missionAltitude or self.altitudeCruise),false) end waypoint.missionUID=mission.auftragsnummer waypointcoord=coordInRange uid=waypoint.uid end end end local d=currentcoord:Get2DDistance(waypointcoord) self:T(self.lid..string.format("Distance to ingress waypoint=%.1f m",d)) local waypoint=nil if self:IsFlightgroup()then local ingresscoord=mission:GetMissionIngressCoord() local holdingcoord=mission:GetMissionHoldingCoord() if holdingcoord then waypoint=FLIGHTGROUP.AddWaypoint(self,holdingcoord,mission.missionHoldingCoordSpeed or SpeedToMission,uid,UTILS.MetersToFeet(mission.missionHoldingCoordAlt or self.altitudeCruise),false) uid=waypoint.uid self.flaghold:Set(0) local TaskOrbit=self.group:TaskOrbit(holdingcoord,mission.missionHoldingCoordAlt,UTILS.KnotsToMps(mission.missionHoldingCoordSpeed or SpeedToMission)) local TaskStop=self.group:TaskCondition(nil,self.flaghold.UserFlagName,1,nil,mission.missionHoldingDuration or 900) local TaskCntr=self.group:TaskControlled(TaskOrbit,TaskStop) local TaskOver=self.group:TaskFunction("FLIGHTGROUP._FinishedWaiting",self) local DCSTasks=self.group:TaskCombo({TaskCntr,TaskOver}) local waypointtask=self:AddTaskWaypoint(DCSTasks,waypoint,"Holding") waypointtask.ismission=false self.isHoldingAtHoldingPoint=true end if ingresscoord then waypoint=FLIGHTGROUP.AddWaypoint(self,ingresscoord,mission.missionIngressCoordSpeed or SpeedToMission,uid,UTILS.MetersToFeet(mission.missionIngressCoordAlt or self.altitudeCruise),false) uid=waypoint.uid end waypoint=FLIGHTGROUP.AddWaypoint(self,waypointcoord,SpeedToMission,uid,UTILS.MetersToFeet(mission.missionAltitude or self.altitudeCruise),false) elseif self:IsArmygroup()then local formation=mission.optionFormation if d<1000 or mission.type==AUFTRAG.Type.RELOCATECOHORT then formation=ENUMS.Formation.Vehicle.OffRoad end waypoint=ARMYGROUP.AddWaypoint(self,waypointcoord,SpeedToMission,uid,formation,false) elseif self:IsNavygroup()then waypoint=NAVYGROUP.AddWaypoint(self,waypointcoord,SpeedToMission,uid,UTILS.MetersToFeet(mission.missionAltitude or self.altitudeCruise),false) end waypoint.missionUID=mission.auftragsnummer local waypointtask=self:AddTaskWaypoint(mission.DCStask,waypoint,mission.name,mission.prio,mission.duration) waypointtask.ismission=true waypointtask.target=targetobject mission:SetGroupWaypointTask(self,waypointtask) mission:SetGroupWaypointIndex(self,waypoint.uid) local egresscoord=mission:GetMissionEgressCoord() if egresscoord then local Ewaypoint=nil if self:IsFlightgroup()then Ewaypoint=FLIGHTGROUP.AddWaypoint(self,egresscoord,mission.missionEgressCoordSpeed or SpeedToMission,waypoint.uid,UTILS.MetersToFeet(mission.missionEgressCoordAlt or self.altitudeCruise),false) elseif self:IsArmygroup()then Ewaypoint=ARMYGROUP.AddWaypoint(self,egresscoord,SpeedToMission,waypoint.uid,mission.optionFormation,false) elseif self:IsNavygroup()then Ewaypoint=NAVYGROUP.AddWaypoint(self,egresscoord,SpeedToMission,waypoint.uid,UTILS.MetersToFeet(mission.missionAltitude or self.altitudeCruise),false) end Ewaypoint.missionUID=mission.auftragsnummer mission:SetGroupEgressWaypointUID(self,Ewaypoint.uid) end if targetzone and self:IsInZone(targetzone)then self:T(self.lid.."Already in mission zone ==> TaskExecute()") self:TaskExecute(waypointtask) self:PassingWaypoint(waypoint) return elseif d<25 then self:T(self.lid.."Already within 25 meters of mission waypoint ==> TaskExecute()") self:TaskExecute(waypointtask) self:PassingWaypoint(waypoint) return end if(self.speedMax<=3.6 or mission.teleport)and not mission.unpaused then self:Teleport(waypointcoord,nil,true) self:__TaskExecute(-1,waypointtask) else if self:IsArmygroup()then self:Cruise(SpeedToMission) elseif self:IsNavygroup()then self:Cruise(SpeedToMission) elseif self:IsFlightgroup()then self:__UpdateRoute(delayGo) end end self:_SetMissionOptions(mission) end end function OPSGROUP:_SetMissionOptions(mission) if mission.optionROE then self:SwitchROE(mission.optionROE) end if mission.optionROT then self:SwitchROT(mission.optionROT) end if mission.optionAlarm then self:SwitchAlarmstate(mission.optionAlarm) end if mission.optionEPLRS then self:SwitchEPLRS(mission.optionEPLRS) end if mission.optionEmission then self:SwitchEmission(mission.optionEmission) end if mission.optionInvisible then self:SwitchInvisible(mission.optionInvisible) end if mission.optionImmortal then self:SwitchImmortal(mission.optionImmortal) end if mission.optionFormation and self:IsFlightgroup()then self:SwitchFormation(mission.optionFormation) end if mission.radio then self:SwitchRadio(mission.radio.Freq,mission.radio.Modu) end if mission.tacan then self:SwitchTACAN(mission.tacan.Channel,mission.tacan.Morse,mission.tacan.BeaconName,mission.tacan.Band) end if mission.icls then self:SwitchICLS(mission.icls.Channel,mission.icls.Morse,mission.icls.UnitName) end if self.isFlightgroup then if mission.prohibitAB==true then self:SetProhibitAfterburner() self:T2("Set prohibit AB") elseif mission.prohibitAB==false then self:SetAllowAfterburner() self:T2("Set allow AB") end end return self end function OPSGROUP:_QueueUpdate() if self:IsExist()then local mission=self:_GetNextMission() if mission then local currentmission=self:GetMissionCurrent() if currentmission then if mission.urgent and mission.prio0 then self:T(self.lid..string.format("WARNING: Got current task ==> WAIT event is suspended for 30 sec!")) Tsuspend=-30 allowed=false end if self.cargoTransport then self:T(self.lid..string.format("WARNING: Got current TRANSPORT assignment ==> WAIT event is suspended for 30 sec!")) Tsuspend=-30 allowed=false end if Tsuspend and not allowed then self:__Wait(Tsuspend,Duration) end return allowed end function OPSGROUP:onafterWait(From,Event,To,Duration) self:FullStop() self.Twaiting=timer.getAbsTime() self.dTwait=Duration end function OPSGROUP:onafterPassingWaypoint(From,Event,To,Waypoint) local task=self:GetTaskCurrent() local mission=nil if task then mission=self:GetMissionByTaskID(task.id) end if task and task.dcstask.id==AUFTRAG.SpecialTask.PATROLZONE then self:RemoveWaypointByID(Waypoint.uid) local zone=task.dcstask.params.zone local surfacetypes=nil if self:IsArmygroup()then surfacetypes={land.SurfaceType.LAND,land.SurfaceType.ROAD} elseif self:IsNavygroup()then surfacetypes={land.SurfaceType.WATER,land.SurfaceType.SHALLOW_WATER} end local Coordinate=zone:GetRandomCoordinate(nil,nil,surfacetypes) local Speed=task.dcstask.params.speed and UTILS.MpsToKnots(task.dcstask.params.speed)or UTILS.KmphToKnots(self.speedCruise) local Altitude=UTILS.MetersToFeet(task.dcstask.params.altitude or self.altitudeCruise) local currUID=self:GetWaypointCurrent().uid local wp=nil if self.isFlightgroup then wp=FLIGHTGROUP.AddWaypoint(self,Coordinate,Speed,currUID,Altitude) elseif self.isArmygroup then wp=ARMYGROUP.AddWaypoint(self,Coordinate,Speed,currUID,task.dcstask.params.formation) elseif self.isNavygroup then wp=NAVYGROUP.AddWaypoint(self,Coordinate,Speed,currUID,Altitude) end wp.missionUID=mission and mission.auftragsnummer or nil elseif task and task.dcstask.id==AUFTRAG.SpecialTask.RECON then local target=task.dcstask.params.target if self.adinfinitum and#self.reconindecies==0 then self.reconindecies={} for i=1,#target.targets do table.insert(self.reconindecies,i) end end if#self.reconindecies>0 then local n=1 if task.dcstask.params.randomly then n=UTILS.GetRandomTableElement(self.reconindecies) else n=self.reconindecies[1] table.remove(self.reconindecies,1) end local object=target.targets[n] local zone=object.Object local Coordinate=zone:GetRandomCoordinate() local Speed=task.dcstask.params.speed and UTILS.MpsToKnots(task.dcstask.params.speed)or UTILS.KmphToKnots(self.speedCruise) local Altitude=task.dcstask.params.altitude and UTILS.MetersToFeet(task.dcstask.params.altitude)or nil local currUID=self:GetWaypointCurrent().uid local wp=nil if self.isFlightgroup then wp=FLIGHTGROUP.AddWaypoint(self,Coordinate,Speed,currUID,Altitude) elseif self.isArmygroup then wp=ARMYGROUP.AddWaypoint(self,Coordinate,Speed,currUID,task.dcstask.params.formation) elseif self.isNavygroup then wp=NAVYGROUP.AddWaypoint(self,Coordinate,Speed,currUID,Altitude) end wp.missionUID=mission and mission.auftragsnummer or nil else local wpindex=self:GetWaypointIndex(Waypoint.uid) if wpindex==nil or wpindex==#self.waypoints then if not self.adinfinitum or#self.waypoints<=1 then self:_PassedFinalWaypoint(true,"Passing waypoint and NOT adinfinitum and #self.waypoints<=1") end end self:TaskDone(task) end elseif task and task.dcstask.id==AUFTRAG.SpecialTask.RELOCATECOHORT then local legion=task.dcstask.params.legion self:T(self.lid..string.format("Asset arrived at relocation task waypoint ==> Task Done!")) self:TaskDone(task) elseif task and task.dcstask.id==AUFTRAG.SpecialTask.REARMING then self:T(self.lid..string.format("FF Rearming Mission ==> Rearm()")) self:Rearm() else local ntasks=self:_SetWaypointTasks(Waypoint) local wpindex=self:GetWaypointIndex(Waypoint.uid) if wpindex==nil or wpindex==#self.waypoints then if self.adinfinitum then if Waypoint.missionUID then else if#self.waypoints<=1 then self:_PassedFinalWaypoint(true,"PassingWaypoint: adinfinitum but only ONE WAYPOINT left") else self:__UpdateRoute(-0.01,1,1) end end else self:_PassedFinalWaypoint(true,"PassingWaypoint: wpindex=#self.waypoints (or wpindex=nil)") end elseif wpindex==1 then if self.adinfinitum then if#self.waypoints<=1 then self:_PassedFinalWaypoint(true,"PassingWaypoint: adinfinitum but only ONE WAYPOINT left") else if not Waypoint.missionUID then self:__UpdateRoute(-0.01,2) end end end end local isEgress=false if Waypoint.missionUID then self:T2(self.lid..string.format("Passing mission waypoint UID=%s",tostring(Waypoint.uid))) local mission=self:GetMissionByID(Waypoint.missionUID) local EgressUID=mission and mission:GetGroupEgressWaypointUID(self)or nil isEgress=EgressUID and Waypoint.uid==EgressUID if isEgress and mission:GetGroupStatus(self)~=AUFTRAG.GroupStatus.DONE then self:MissionDone(mission) end end if ntasks==0 and self:HasPassedFinalWaypoint()and not isEgress then self:_CheckGroupDone(0.01) end local text=string.format("Group passed waypoint %s/%d ID=%d: final=%s detour=%s astar=%s", tostring(wpindex),#self.waypoints,Waypoint.uid,tostring(self.passedfinalwp),tostring(Waypoint.detour),tostring(Waypoint.astar)) self:T(self.lid..text) end local wpnext=self:GetWaypointNext() if wpnext then self.speedWp=wpnext.speed self:T(self.lid..string.format("Expected/waypoint speed=%.1f m/s",self.speedWp)) end end function OPSGROUP:_SetWaypointTasks(Waypoint) local tasks=self:GetTasksWaypoint(Waypoint.uid) local text=string.format("WP uid=%d tasks:",Waypoint.uid) local missiontask=nil if#tasks>0 then for i,_task in pairs(tasks)do local task=_task text=text..string.format("\n[%d] %s",i,task.description) if task.ismission then missiontask=task end end else text=text.." None" end self:T(self.lid..text) if missiontask then self:T(self.lid.."Executing mission task") local mission=self:GetMissionByTaskID(missiontask.id) if mission then if mission.opstransport and not mission.opstransport:IsCargoDelivered(self.groupname)then self:PauseMission() return end end self:TaskExecute(missiontask) return 1 end local taskswp={} for _,task in pairs(tasks)do local Task=task table.insert(taskswp,self.group:TaskFunction("OPSGROUP._TaskExecute",self,Task)) local TaskCondition=self.group:TaskCondition(nil,Task.stopflag:GetName(),1,nil,Task.duration) table.insert(taskswp,self.group:TaskControlled(Task.dcstask,TaskCondition)) table.insert(taskswp,self.group:TaskFunction("OPSGROUP._TaskDone",self,Task)) end if#taskswp>0 then self:SetTask(self.group:TaskCombo(taskswp)) end return#taskswp end function OPSGROUP:onafterPassedFinalWaypoint(From,Event,To) self:T(self.lid..string.format("Group passed final waypoint")) end function OPSGROUP:onafterGotoWaypoint(From,Event,To,UID,Speed) local n=self:GetWaypointIndex(UID) if n then Speed=Speed or self:GetSpeedToWaypoint(n) self:T(self.lid..string.format("Goto Waypoint UID=%d index=%d from %d at speed %.1f knots",UID,n,self.currentwp,Speed)) self:__UpdateRoute(0.1,n,nil,Speed) end end function OPSGROUP:onafterDetectedUnit(From,Event,To,Unit) local unitname=Unit and Unit:GetName()or"unknown" self:T2(self.lid..string.format("Detected unit %s",unitname)) if self.detectedunits:FindUnit(unitname)then self:DetectedUnitKnown(Unit) else self:DetectedUnitNew(Unit) end end function OPSGROUP:onafterDetectedUnitNew(From,Event,To,Unit) self:T(self.lid..string.format("Detected New unit %s",Unit:GetName())) self.detectedunits:AddUnit(Unit) end function OPSGROUP:onafterDetectedGroup(From,Event,To,Group) local groupname=Group and Group:GetName()or"unknown" self:T(self.lid..string.format("Detected group %s",groupname)) if self.detectedgroups:FindGroup(groupname)then self:DetectedGroupKnown(Group) else self:DetectedGroupNew(Group) end end function OPSGROUP:onafterDetectedGroupNew(From,Event,To,Group) self:T(self.lid..string.format("Detected New group %s",Group:GetName())) self.detectedgroups:AddGroup(Group) end function OPSGROUP:onafterEnterZone(From,Event,To,Zone) local zonename=Zone and Zone:GetName()or"unknown" self:T2(self.lid..string.format("Entered Zone %s",zonename)) self.inzones:Add(Zone:GetName(),Zone) end function OPSGROUP:onafterLeaveZone(From,Event,To,Zone) local zonename=Zone and Zone:GetName()or"unknown" self:T2(self.lid..string.format("Left Zone %s",zonename)) self.inzones:Remove(zonename,true) end function OPSGROUP:onbeforeLaserOn(From,Event,To,Target) if self.spot.On then return false end if Target then self:SetLaserTarget(Target) else self:T(self.lid.."ERROR: No target provided for LASER!") return false end local element=self:GetElementAlive() if element then self.spot.element=element local offsetY=2 if self.isFlightgroup or self.isNavygroup then offsetY=element.height end self.spot.offset={x=0,y=offsetY,z=0} if self.spot.CheckLOS then local los=self:HasLoS(self.spot.Coordinate,self.spot.element,self.spot.offset) if los then self:LaserGotLOS() else self:T(self.lid.."LASER got no LOS currently. Trying to switch the laser on again in 10 sec") self:__LaserOn(-10,Target) return false end end else self:T(self.lid.."ERROR: No element alive for lasing") return false end return true end function OPSGROUP:onafterLaserOn(From,Event,To,Target) if not self.spot.timer:IsRunning()then self.spot.timer:Start(nil,self.spot.dt) end local DCSunit=self.spot.element.unit:GetDCSObject() self.spot.Laser=Spot.createLaser(DCSunit,self.spot.offset,self.spot.vec3,self.spot.Code or 1688) if self.spot.IRon then self.spot.IR=Spot.createInfraRed(DCSunit,self.spot.offset,self.spot.vec3) end self.spot.On=true self.spot.Paused=false self:T(self.lid.."Switching LASER on") end function OPSGROUP:onbeforeLaserOff(From,Event,To) return self.spot.On or self.spot.Paused end function OPSGROUP:onafterLaserOff(From,Event,To) self:T(self.lid.."Switching LASER off") if self.spot.On then self.spot.Laser:destroy() self.spot.IR:destroy() self.spot.Laser=nil self.spot.IR=nil end self.spot.timer:Stop() self.spot.TargetUnit=nil self.spot.On=false self.spot.Paused=false end function OPSGROUP:onafterLaserPause(From,Event,To) self:T(self.lid.."Switching LASER off temporarily") self.spot.Laser:destroy() self.spot.IR:destroy() self.spot.Laser=nil self.spot.IR=nil self.spot.On=false self.spot.Paused=true end function OPSGROUP:onbeforeLaserResume(From,Event,To) return self.spot.Paused end function OPSGROUP:onafterLaserResume(From,Event,To) self:T(self.lid.."Resuming LASER") self.spot.Paused=false local target=nil if self.spot.TargetType==0 then target=self.spot.Coordinate elseif self.spot.TargetType==1 or self.spot.TargetType==2 then target=self.spot.TargetUnit elseif self.spot.TargetType==3 then target=self.spot.TargetGroup end if target then self:T(self.lid.."Switching LASER on again") self:LaserOn(target) end end function OPSGROUP:onafterLaserCode(From,Event,To,Code) self.spot.Code=Code or 1688 self:T2(self.lid..string.format("Setting LASER Code to %d",self.spot.Code)) if self.spot.On then self:T(self.lid..string.format("New LASER Code is %d",self.spot.Code)) self.spot.Laser:setCode(self.spot.Code) end end function OPSGROUP:onafterLaserLostLOS(From,Event,To) self.spot.LOS=false self.spot.lostLOS=true if self.spot.On then self:LaserPause() end end function OPSGROUP:onafterLaserGotLOS(From,Event,To) self.spot.LOS=true if self.spot.lostLOS then self.spot.lostLOS=false if self.spot.Paused then self:LaserResume() end end end function OPSGROUP:SetLaserTarget(Target) if Target then if Target:IsInstanceOf("SCENERY")then self.spot.TargetType=0 self.spot.offsetTarget={x=0,y=3,z=0} elseif Target:IsInstanceOf("POSITIONABLE")then local target=Target if target:IsAlive()then if target:IsInstanceOf("GROUP")then self.spot.TargetGroup=target self.spot.TargetUnit=target:GetHighestThreat() self.spot.TargetType=3 else self.spot.TargetUnit=target if target:IsInstanceOf("STATIC")then self.spot.TargetType=1 elseif target:IsInstanceOf("UNIT")then self.spot.TargetType=2 end end local size,x,y,z=self.spot.TargetUnit:GetObjectSize() if y then self.spot.offsetTarget={x=0,y=y*0.75,z=0} else self.spot.offsetTarget={x=0,2,z=0} end else self:T("WARNING: LASER target is not alive!") return end elseif Target:IsInstanceOf("COORDINATE")then self.spot.TargetType=0 self.spot.offsetTarget={x=0,y=0,z=0} else self:T(self.lid.."ERROR: LASER target should be a POSITIONABLE (GROUP, UNIT or STATIC) or a COORDINATE object!") return end self.spot.vec3=UTILS.VecAdd(Target:GetVec3(),self.spot.offsetTarget) self.spot.Coordinate:UpdateFromVec3(self.spot.vec3) end end function OPSGROUP:_UpdateLaser() if self.spot.TargetUnit then if self.spot.TargetUnit:IsAlive()then local vec3=self.spot.TargetUnit:GetVec3() vec3=UTILS.VecAdd(vec3,self.spot.offsetTarget) local dist=UTILS.VecDist3D(vec3,self.spot.vec3) self.spot.vec3=vec3 self.spot.Coordinate:UpdateFromVec3(vec3) if dist>1 then if self.spot.On then self.spot.Laser:setPoint(vec3) if self.spot.IRon then self.spot.IR:setPoint(vec3) end end end else if self.spot.TargetGroup and self.spot.TargetGroup:IsAlive()then local unit=self.spot.TargetGroup:GetHighestThreat() if unit then self:T(self.lid..string.format("Switching to target unit %s in the group",unit:GetName())) self.spot.TargetUnit=unit return else self:T(self.lid.."Target is not alive any more ==> switching LASER off") self:LaserOff() return end else self:T(self.lid.."Target is not alive any more ==> switching LASER off") self:LaserOff() return end end end if self.spot.CheckLOS then local los=self:HasLoS(self.spot.Coordinate,self.spot.element,self.spot.offset) if los then if self.spot.lostLOS then self:LaserGotLOS() end else if not self.spot.lostLOS then self:LaserLostLOS() end end end end function OPSGROUP:onbeforeElementSpawned(From,Event,To,Element) if Element and Element.status==OPSGROUP.ElementStatus.SPAWNED then self:T2(self.lid..string.format("Element %s is already spawned ==> Transition denied!",Element.name)) return false end return true end function OPSGROUP:onafterElementInUtero(From,Event,To,Element) self:T(self.lid..string.format("Element in utero %s",Element.name)) self:_UpdateStatus(Element,OPSGROUP.ElementStatus.INUTERO) end function OPSGROUP:onafterElementDamaged(From,Event,To,Element) self:T(self.lid..string.format("Element damaged %s",Element.name)) if Element and(Element.status~=OPSGROUP.ElementStatus.DEAD and Element.status~=OPSGROUP.ElementStatus.INUTERO)then local lifepoints=0 if Element.DCSunit and Element.DCSunit:isExist()then lifepoints=Element.DCSunit:getLife() self:T(self.lid..string.format("Element life %s: %.2f/%.2f",Element.name,lifepoints,Element.life0)) else self:T(self.lid..string.format("Element.DCSunit %s does not exist!",Element.name)) end if lifepoints<=1.0 then self:T(self.lid..string.format("Element %s life %.2f <= 1.0 ==> Destroyed!",Element.name,lifepoints)) self:ElementDestroyed(Element) end end end function OPSGROUP:onafterElementHit(From,Event,To,Element,Enemy) Element.Nhit=Element.Nhit+1 self:T(self.lid..string.format("Element hit %s by %s [n=%d, N=%d]",Element.name,Enemy and Enemy:GetName()or"unknown",Element.Nhit,self.Nhit)) self:__Hit(-3,Enemy) end function OPSGROUP:onafterHit(From,Event,To,Enemy) self:T(self.lid..string.format("Group hit by %s",Enemy and Enemy:GetName()or"unknown")) end function OPSGROUP:onafterElementDestroyed(From,Event,To,Element) self:T(self.lid..string.format("Element destroyed %s",Element.name)) for _,_mission in pairs(self.missionqueue)do local mission=_mission mission:ElementDestroyed(self,Element) end self.Ndestroyed=self.Ndestroyed+1 self:ElementDead(Element) end function OPSGROUP:onafterElementDead(From,Event,To,Element) self:T(self.lid..string.format("Element dead %s at t=%.3f",Element.name,timer.getTime())) self:_UpdateStatus(Element,OPSGROUP.ElementStatus.DEAD) if self.spot.On and self.spot.element.name==Element.name then self:LaserOff() if self:GetNelements()>0 then local target=nil if self.spot.TargetType==0 then target=self.spot.Coordinate elseif self.spot.TargetType==1 or self.spot.TargetType==2 then if self.spot.TargetUnit and self.spot.TargetUnit:IsAlive()then target=self.spot.TargetUnit end elseif self.spot.TargetType==3 then if self.spot.TargetGroup and self.spot.TargetGroup:IsAlive()then target=self.spot.TargetGroup end end if target then self:__LaserOn(-1,target) end end end for i=#Element.cargoBay,1,-1 do local mycargo=Element.cargoBay[i] if mycargo.group then self:_DelCargobay(mycargo.group) if mycargo.group and not(mycargo.group:IsDead()or mycargo.group:IsStopped())then mycargo.group:_RemoveMyCarrier() if mycargo.reserved then mycargo.group:_NewCargoStatus(OPSGROUP.CargoStatus.NOTCARGO) else for _,cargoelement in pairs(mycargo.group.elements)do self:T2(self.lid.."Cargo element dead "..cargoelement.name) mycargo.group:ElementDead(cargoelement) end end end else if self.cargoTZC then for _,_cargo in pairs(self.cargoTZC.Cargos)do local cargo=_cargo if cargo.uid==mycargo.cargoUID then cargo.storage.cargoLost=cargo.storage.cargoLost+mycargo.storageAmount end end end self:_DelCargobayElement(Element,mycargo) end end end function OPSGROUP:onafterRespawn(From,Event,To,Template) self:T(self.lid.."Respawning group!") local template=UTILS.DeepCopy(Template or self.template) template.lateActivation=false self:_Respawn(0,template) end function OPSGROUP:Teleport(Coordinate,Delay,NoPauseMission) if Delay and Delay>0 then self:ScheduleOnce(Delay,OPSGROUP.Teleport,self,Coordinate,0,NoPauseMission) else self:T(self.lid.."FF Teleporting...") if self:IsOnMission()and not NoPauseMission then self:T(self.lid.."Pausing current mission for telport") self:PauseMission() end local Template=UTILS.DeepCopy(self.template) Template.lateActivation=self:IsLateActivated() Template.uncontrolled=false if self:IsFlightgroup()then Template.route.points[1]=Coordinate:WaypointAir("BARO",COORDINATE.WaypointType.TurningPoint,COORDINATE.WaypointAction.TurningPoint,300,true,nil,nil,"Spawnpoint") elseif self:IsArmygroup()then Template.route.points[1]=Coordinate:WaypointGround(0) elseif self:IsNavygroup()then Template.route.points[1]=Coordinate:WaypointNaval(0) end local units=Template.units local d={} for i=1,#units do local unit=units[i] d[i]={x=Coordinate.x+(units[i].x-units[1].x),y=Coordinate.z+units[i].y-units[1].y} end for i=#units,1,-1 do local unit=units[i] local element=self:GetElementByName(unit.name) if element and element.status~=OPSGROUP.ElementStatus.DEAD then unit.parking=nil unit.parking_id=nil local vec3=element.unit:GetVec3() local heading=element.unit:GetHeading() unit.x=d[i].x unit.y=d[i].y unit.alt=Coordinate.y unit.heading=math.rad(heading) unit.psi=-unit.heading else table.remove(units,i) end end self:_Respawn(0,Template,true) end end function OPSGROUP:_Respawn(Delay,Template,Reset) if Delay and Delay>0 then self:ScheduleOnce(Delay,OPSGROUP._Respawn,self,0,Template,Reset) else self:T2(self.lid.."FF _Respawn") Template=Template or self:_GetTemplate(true) self.Ndestroyed=0 self.Nhit=0 if self:IsAlive()then local units=Template.units for i=#units,1,-1 do local unit=units[i] local element=self:GetElementByName(unit.name) if element and element.status~=OPSGROUP.ElementStatus.DEAD then if not Reset then unit.parking=element.parking and element.parking.TerminalID or unit.parking unit.parking_id=nil local vec3=element.unit:GetVec3() local heading=element.unit:GetHeading() unit.x=vec3.x unit.y=vec3.z unit.alt=vec3.y unit.heading=math.rad(heading) unit.psi=-unit.heading end else table.remove(units,i) self.Ndestroyed=self.Ndestroyed+1 end end self:Despawn(0,true) end for _,_element in pairs(self.elements)do local element=_element if element and element.status~=OPSGROUP.ElementStatus.DEAD then self:ElementInUtero(element) end end self:_Spawn(0.01,Template) end return self end function OPSGROUP:_Spawn(Delay,Template) if Delay and Delay>0 then self:ScheduleOnce(Delay,OPSGROUP._Spawn,self,0,Template) else if self:IsArmygroup()and self.ValidateAndRepositionGroundUnits then UTILS.ValidateAndRepositionGroundUnits(Template.units) end self.group=_DATABASE:Spawn(Template) self.group:SetValidateAndRepositionGroundUnits(self.ValidateAndRepositionGroundUnits) self.dcsgroup=self:GetDCSGroup() self.controller=self.dcsgroup:getController() self.isLateActivated=Template.lateActivation self.isUncontrolled=Template.uncontrolled self.isDead=false self.isDestroyed=false self.groupinitialized=false self.wpcounter=1 self.currentwp=1 self:_InitWaypoints() self:_InitGroup(Template,0.001) end end function OPSGROUP:onafterInUtero(From,Event,To) self:T(self.lid..string.format("Group inutero at t=%.3f",timer.getTime())) end function OPSGROUP:onafterDamaged(From,Event,To) self:T(self.lid..string.format("Group damaged at t=%.3f",timer.getTime())) end function OPSGROUP:onafterDestroyed(From,Event,To) self:T(self.lid..string.format("Group destroyed at t=%.3f",timer.getTime())) self.isDestroyed=true end function OPSGROUP:onbeforeDead(From,Event,To) if self.Ndestroyed==#self.elements then self:Destroyed() end end function OPSGROUP:onafterDead(From,Event,To) self:T(self.lid..string.format("Group dead at t=%.3f",timer.getTime())) self.isDead=true for _,_mission in pairs(self.missionqueue)do local mission=_mission self:T(self.lid.."Cancelling mission because group is dead! Mission name "..tostring(mission:GetName())) self:MissionCancel(mission) mission:GroupDead(self) end self:ClearWaypoints() self.groupinitialized=false self.cargoStatus=OPSGROUP.CargoStatus.NOTCARGO self.carrierStatus=OPSGROUP.CarrierStatus.NOTCARRIER local mycarrier=self:_GetMyCarrierGroup() if mycarrier and not mycarrier:IsDead()then mycarrier:_DelCargobay(self) self:_RemoveMyCarrier() end for i,_transport in pairs(self.cargoqueue)do local transport=_transport transport:__DeadCarrierGroup(1,self) end self.cargoqueue={} self.cargoTransport=nil self.cargoTZC=nil if self.Ndestroyed==#self.elements then if self.cohort then self.cohort:DelGroup(self.groupname) end else end if self.legion then if not self:IsInUtero()then local asset=self.legion:GetAssetByName(self.groupname) if asset then local request=self.legion:GetRequestByID(asset.rid) self.legion:AssetDead(asset,request) end end self:__Stop(-5) elseif not self.isAI then self:__Stop(-1) end end function OPSGROUP:onbeforeStop(From,Event,To) if self:IsAlive()then self:T(self.lid..string.format("WARNING: Group is still alive! Will not stop the FSM. Use :Despawn() instead")) return false end return true end function OPSGROUP:onafterStop(From,Event,To) self:UnHandleEvent(EVENTS.Birth) self:UnHandleEvent(EVENTS.Dead) self:UnHandleEvent(EVENTS.RemoveUnit) if self.isFlightgroup then self:UnHandleEvent(EVENTS.EngineStartup) self:UnHandleEvent(EVENTS.Takeoff) self:UnHandleEvent(EVENTS.Land) self:UnHandleEvent(EVENTS.EngineShutdown) self:UnHandleEvent(EVENTS.PilotDead) self:UnHandleEvent(EVENTS.Ejection) self:UnHandleEvent(EVENTS.Crash) self.currbase=nil elseif self.isArmygroup then self:UnHandleEvent(EVENTS.Hit) end for _,_mission in pairs(self.missionqueue)do local mission=_mission self:MissionCancel(mission) end self.timerCheckZone:Stop() self.timerQueueUpdate:Stop() self.timerStatus:Stop() self.CallScheduler:Clear() if self.Scheduler then self.Scheduler:Clear() end if self.flightcontrol then for _,_element in pairs(self.elements)do local element=_element if element.parking then self.flightcontrol:SetParkingFree(element.parking) end end self.flightcontrol:_RemoveFlight(self) end if self:IsAlive()and not(self:IsDead()or self:IsStopped())then local life,life0=self:GetLifePoints() local state=self:GetState() local text=string.format("WARNING: Group is still alive! Current state=%s. Life points=%d/%d. Use OPSGROUP:Destroy() or OPSGROUP:Despawn() for a clean stop",state,life,life0) self:T(self.lid..text) end _DATABASE.FLIGHTGROUPS[self.groupname]=nil self:T(self.lid.."STOPPED! Unhandled events, cleared scheduler and removed from _DATABASE") end function OPSGROUP:onafterOutOfAmmo(From,Event,To) self:T(self.lid..string.format("Group is out of ammo at t=%.3f",timer.getTime())) end function OPSGROUP:_CheckCargoTransport() local Time=timer.getAbsTime() if self.verbose>=1 then local text="" for _,_element in pairs(self.elements)do local element=_element for _,_cargo in pairs(element.cargoBay)do local cargo=_cargo if cargo.group then text=text..string.format("\n- %s in carrier %s, reserved=%s",tostring(cargo.group:GetName()),tostring(element.name),tostring(cargo.reserved)) else text=text..string.format("\n- storage %s=%d kg in carrier %s [UID=%s]", tostring(cargo.storageType),tostring(cargo.storageAmount*cargo.storageWeight),tostring(element.name),tostring(cargo.cargoUID)) end end end if text==""then text=" empty" end self:T(self.lid.."Cargo bay:"..text) end if self.verbose>=3 then local text="" for i,_transport in pairs(self.cargoqueue)do local transport=_transport local pickupzone=transport:GetPickupZone() local deployzone=transport:GetDeployZone() local pickupname=pickupzone and pickupzone:GetName()or"unknown" local deployname=deployzone and deployzone:GetName()or"unknown" text=text..string.format("\n[%d] UID=%d Status=%s: %s --> %s",i,transport.uid,transport:GetState(),pickupname,deployname) for j,_cargo in pairs(transport:GetCargos())do local cargo=_cargo if cargo.type==OPSTRANSPORT.CargoType.OPSGROUP then local state=cargo.opsgroup:GetState() local status=cargo.opsgroup.cargoStatus local name=cargo.opsgroup.groupname local carriergroup,carrierelement,reserved=cargo.opsgroup:_GetMyCarrier() local carrierGroupname=carriergroup and carriergroup.groupname or"none" local carrierElementname=carrierelement and carrierelement.name or"none" text=text..string.format("\n (%d) %s [%s]: %s, carrier=%s(%s), delivered=%s",j,name,state,status,carrierGroupname,carrierElementname,tostring(cargo.delivered)) else end end end if text~=""then self:T(self.lid.."Cargo queue:"..text) end end if self.cargoTransport and self.cargoTransport:GetCarrierTransportStatus(self)==OPSTRANSPORT.Status.DELIVERED then self:DelOpsTransport(self.cargoTransport) self.cargoTransport=nil self.cargoTZC=nil end local mission=self:GetMissionCurrent() if(not self.cargoTransport)and(mission==nil or mission.type==AUFTRAG.Type.NOTHING)then self.cargoTransport=self:_GetNextCargoTransport() if self.cargoTransport and mission then self:MissionCancel(mission) end if self.cargoTransport and not self:IsActive()then self:Activate() end end if self.cargoTransport then if self:IsNotCarrier()then self.Tpickingup=nil self.Tloading=nil self.Ttransporting=nil self.Tunloading=nil self.cargoTZC=self.cargoTransport:_GetTransportZoneCombo(self) if self.cargoTZC then self:T(self.lid..string.format("Not carrier ==> pickup at %s [TZC UID=%d]",self.cargoTZC.PickupZone and self.cargoTZC.PickupZone:GetName()or"unknown",self.cargoTZC.uid)) self:__Pickup(-1) else self:T2(self.lid.."Not carrier ==> No TZC found") end elseif self:IsPickingup()then self.Tpickingup=self.Tpickingup or Time local tpickingup=Time-self.Tpickingup self:T(self.lid..string.format("Picking up at %s [TZC UID=%d] for %s sec...",self.cargoTZC.PickupZone and self.cargoTZC.PickupZone:GetName()or"unknown",self.cargoTZC.uid,tpickingup)) elseif self:IsLoading()then self.Tloading=self.Tloading or Time local tloading=Time-self.Tloading self:T(self.lid..string.format("Loading at %s [TZC UID=%d] for %.1f sec...",self.cargoTZC.PickupZone and self.cargoTZC.PickupZone:GetName()or"unknown",self.cargoTZC.uid,tloading)) local boarding=false local gotcargo=false for _,_cargo in pairs(self.cargoTZC.Cargos)do local cargo=_cargo if cargo.type==OPSTRANSPORT.CargoType.OPSGROUP then if cargo.opsgroup and cargo.opsgroup:IsBoarding(self.groupname)then boarding=true end if cargo.opsgroup and cargo.opsgroup:IsLoaded(self.groupname)then gotcargo=true end else local mycargo=self:_GetMyCargoBayFromUID(cargo.uid) if mycargo and mycargo.storageAmount>0 then gotcargo=true end end end local required=self.cargoTransport:_CheckRequiredCargos(self.cargoTZC,self) if gotcargo and required and not boarding then self:T(self.lid.."Boarding/loading finished ==> Loaded") self.Tloading=nil self:LoadingDone() else self:Loading() end elseif self:IsTransporting()then self.Ttransporting=self.Ttransporting or Time local ttransporting=Time-self.Ttransporting self:T(self.lid.."Transporting (nothing to do)") elseif self:IsUnloading()then self.Tunloading=self.Tunloading or Time local tunloading=Time-self.Tunloading self:T(self.lid.."Unloading ==> Checking if all cargo was delivered") local delivered=true for _,_cargo in pairs(self.cargoTZC.Cargos)do local cargo=_cargo if cargo.type==OPSTRANSPORT.CargoType.OPSGROUP then local carrierGroup=cargo.opsgroup:_GetMyCarrierGroup() if(carrierGroup and carrierGroup:GetName()==self:GetName())and not cargo.delivered then delivered=false break end else local mycargo=self:_GetMyCargoBayFromUID(cargo.uid) if mycargo and not cargo.delivered then delivered=false break end end end if delivered then self:T(self.lid.."Unloading finished ==> UnloadingDone") self:UnloadingDone() else self:Unloading() end end if self.verbose>=2 and self.cargoTransport then local pickupzone=self.cargoTransport:GetPickupZone(self.cargoTZC) local deployzone=self.cargoTransport:GetDeployZone(self.cargoTZC) local pickupname=pickupzone and pickupzone:GetName()or"unknown" local deployname=deployzone and deployzone:GetName()or"unknown" local text=string.format("Carrier [%s]: %s --> %s",self.carrierStatus,pickupname,deployname) for _,_cargo in pairs(self.cargoTransport:GetCargos(self.cargoTZC))do local cargo=_cargo if cargo.type==OPSTRANSPORT.CargoType.OPSGROUP then local name=cargo.opsgroup:GetName() local gstatus=cargo.opsgroup:GetState() local cstatus=cargo.opsgroup.cargoStatus local weight=cargo.opsgroup:GetWeightTotal() local carriergroup,carrierelement,reserved=cargo.opsgroup:_GetMyCarrier() local carrierGroupname=carriergroup and carriergroup.groupname or"none" local carrierElementname=carrierelement and carrierelement.name or"none" text=text..string.format("\n- %s (%.1f kg) [%s]: %s, carrier=%s (%s), delivered=%s",name,weight,gstatus,cstatus,carrierElementname,carrierGroupname,tostring(cargo.delivered)) else end end self:I(self.lid..text) end end return self end function OPSGROUP:_IsInCargobay(OpsGroup) for _,_element in pairs(self.elements)do local element=_element for _,_cargo in pairs(element.cargoBay)do local cargo=_cargo if cargo.group.groupname==OpsGroup.groupname then return true end end end return false end function OPSGROUP:_AddCargobay(CargoGroup,CarrierElement,Reserved) local cargo=self:_GetCargobay(CargoGroup) if cargo then cargo.reserved=Reserved else cargo={} cargo.group=CargoGroup cargo.reserved=Reserved table.insert(CarrierElement.cargoBay,cargo) end CargoGroup:_SetMyCarrier(self,CarrierElement,Reserved) self.cargoBay[CargoGroup.groupname]=CarrierElement.name if not Reserved then local weight=CargoGroup:GetWeightTotal() self:AddWeightCargo(CarrierElement.name,weight) end return self end function OPSGROUP:_AddCargobayStorage(CarrierElement,CargoUID,StorageType,StorageAmount,StorageWeight) local MyCargo=self:_CreateMyCargo(CargoUID,nil,StorageType,StorageAmount,StorageWeight) self:_AddMyCargoBay(MyCargo,CarrierElement) end function OPSGROUP:_CreateMyCargo(CargoUID,OpsGroup,StorageType,StorageAmount,StorageWeight) local cargo={} cargo.cargoUID=CargoUID cargo.group=OpsGroup cargo.storageType=StorageType cargo.storageAmount=StorageAmount cargo.storageWeight=StorageWeight cargo.reserved=false return cargo end function OPSGROUP:_AddMyCargoBay(MyCargo,CarrierElement) table.insert(CarrierElement.cargoBay,MyCargo) if not MyCargo.reserved then local weight=0 if MyCargo.group then weight=MyCargo.group:GetWeightTotal() else weight=MyCargo.storageAmount*MyCargo.storageWeight end self:AddWeightCargo(CarrierElement.name,weight) end end function OPSGROUP:_GetMyCargoBayFromUID(uid) for _,_element in pairs(self.elements)do local element=_element for i,_mycargo in pairs(element.cargoBay)do local mycargo=_mycargo if mycargo.cargoUID and mycargo.cargoUID==uid then return mycargo,element,i end end end return nil,nil,nil end function OPSGROUP:GetCargoGroups(CarrierName) local cargos={} for _,_element in pairs(self.elements)do local element=_element if CarrierName==nil or element.name==CarrierName then for _,_cargo in pairs(element.cargoBay)do local cargo=_cargo if not cargo.reserved then table.insert(cargos,cargo.group) end end end end return cargos end function OPSGROUP:_GetCargobay(CargoGroup) local CarrierElement=nil local cargobayIndex=nil local reserved=nil for i,_element in pairs(self.elements)do local element=_element for j,_cargo in pairs(element.cargoBay)do local cargo=_cargo if cargo.group and cargo.group.groupname==CargoGroup.groupname then return cargo,j,element end end end return nil,nil,nil end function OPSGROUP:_GetCargobayElement(Element,CargoUID) self:T3({Element=Element,CargoUID=CargoUID}) for i,_mycargo in pairs(Element.cargoBay)do local mycargo=_mycargo if mycargo.cargoUID and mycargo.cargoUID==CargoUID then return mycargo end end return nil end function OPSGROUP:_DelCargobayElement(Element,MyCargo) for i,_mycargo in pairs(Element.cargoBay)do local mycargo=_mycargo if mycargo.cargoUID and MyCargo.cargoUID and mycargo.cargoUID==MyCargo.cargoUID then if MyCargo.group then self:RedWeightCargo(Element.name,MyCargo.group:GetWeightTotal()) else self:RedWeightCargo(Element.name,MyCargo.storageAmount*MyCargo.storageWeight) end table.remove(Element.cargoBay,i) return true end end return false end function OPSGROUP:_DelCargobay(CargoGroup) if self.cargoBay[CargoGroup.groupname]then self.cargoBay[CargoGroup.groupname]=nil end local cargoBayItem,cargoBayIndex,CarrierElement=self:_GetCargobay(CargoGroup) if cargoBayItem and cargoBayIndex then self:T(self.lid..string.format("Removing cargo group %s from cargo bay (index=%d) of carrier %s",CargoGroup:GetName(),cargoBayIndex,CarrierElement.name)) table.remove(CarrierElement.cargoBay,cargoBayIndex) if not cargoBayItem.reserved then local weight=CargoGroup:GetWeightTotal() self:RedWeightCargo(CarrierElement.name,weight) end return true end self:T(self.lid.."ERROR: Group is not in cargo bay. Cannot remove it!") return false end function OPSGROUP:_GetNextCargoTransport() local coord=self:GetCoordinate() local function _sort(a,b) local transportA=a local transportB=b return(transportA.priomaxweight then maxweight=weight end end end return maxweight end function OPSGROUP:GetWeightCargo(UnitName,IncludeReserved) local weight=0 for _,_element in pairs(self.elements)do local element=_element if(UnitName==nil or UnitName==element.name)and element.status~=OPSGROUP.ElementStatus.DEAD then weight=weight+element.weightCargo or 0 end end local gewicht=0 for _,_element in pairs(self.elements)do local element=_element if(UnitName==nil or UnitName==element.name)and(element and element.status~=OPSGROUP.ElementStatus.DEAD)then for _,_cargo in pairs(element.cargoBay)do local cargo=_cargo if(not cargo.reserved)or(cargo.reserved==true and(IncludeReserved==true or IncludeReserved==nil))then if cargo.group then gewicht=gewicht+cargo.group:GetWeightTotal() else gewicht=gewicht+cargo.storageAmount*cargo.storageWeight end end end end end self:T3(self.lid..string.format("Unit=%s (reserved=%s): weight=%d, gewicht=%d",tostring(UnitName),tostring(IncludeReserved),weight,gewicht)) if IncludeReserved==false and gewicht~=weight then self:T(self.lid..string.format("ERROR: FF weight!=gewicht: weight=%.1f, gewicht=%.1f",weight,gewicht)) end return gewicht end function OPSGROUP:GetWeightCargoMax(UnitName) local weight=0 for _,_element in pairs(self.elements)do local element=_element if(UnitName==nil or UnitName==element.name)and element.status~=OPSGROUP.ElementStatus.DEAD then weight=weight+element.weightMaxCargo end end return weight end function OPSGROUP:GetCargoOpsGroups() local opsgroups={} for _,_element in pairs(self.elements)do local element=_element for _,_cargo in pairs(element.cargoBay)do local cargo=_cargo table.insert(opsgroups,cargo.group) end end return opsgroups end function OPSGROUP:AddWeightCargo(UnitName,Weight) local element=self:GetElementByName(UnitName) if element then element.weightCargo=element.weightCargo+Weight self:T(self.lid..string.format("%s: Adding %.1f kg cargo weight. New cargo weight=%.1f kg",UnitName,Weight,element.weightCargo)) if self.isFlightgroup and element.unit and element.unit:IsAlive()then trigger.action.setUnitInternalCargo(element.name,element.weightCargo) end end return self end function OPSGROUP:RedWeightCargo(UnitName,Weight) self:AddWeightCargo(UnitName,-Weight) return self end function OPSGROUP:_GetWeightStorage(Storage,Total,Reserved,Amount) local weight=Storage.cargoAmount if not Total then weight=weight-Storage.cargoLost-Storage.cargoLoaded-Storage.cargoDelivered end if Reserved then weight=weight-Storage.cargoReserved end if not Amount then weight=weight*Storage.cargoWeight end return weight end function OPSGROUP:CanCargo(Cargo) if Cargo then local weight=math.huge if Cargo.type==OPSTRANSPORT.CargoType.OPSGROUP then local weight=Cargo.opsgroup:GetWeightTotal() for _,_element in pairs(self.elements)do local element=_element if element and element.status~=OPSGROUP.ElementStatus.DEAD and element.weightMaxCargo>=weight then return true end end else weight=Cargo.storage.cargoWeight end local bay=0 for _,_element in pairs(self.elements)do local element=_element if element and element.status~=OPSGROUP.ElementStatus.DEAD then bay=bay+element.weightMaxCargo end end if bay>=weight then return true end end return false end function OPSGROUP:FindCarrierForCargo(Weight) for _,_element in pairs(self.elements)do local element=_element local free=self:GetFreeCargobay(element.name) if free>=Weight then return element else self:T3(self.lid..string.format("%s: Weight %d>%d free cargo bay",element.name,Weight,free)) end end return nil end function OPSGROUP:_SetMyCarrier(CarrierGroup,CarrierElement,Reserved) self:T(self.lid..string.format("Setting My Carrier: %s (%s), reserved=%s",CarrierGroup:GetName(),tostring(CarrierElement.name),tostring(Reserved))) self.mycarrier.group=CarrierGroup self.mycarrier.element=CarrierElement self.mycarrier.reserved=Reserved self.cargoTransportUID=CarrierGroup.cargoTransport and CarrierGroup.cargoTransport.uid or nil end function OPSGROUP:_GetMyCarrierGroup() if self.mycarrier and self.mycarrier.group then return self.mycarrier.group end return nil end function OPSGROUP:_GetMyCarrierElement() if self.mycarrier and self.mycarrier.element then return self.mycarrier.element end return nil end function OPSGROUP:_IsMyCarrierReserved() if self.mycarrier then return self.mycarrier.reserved end return nil end function OPSGROUP:_GetMyCarrier() return self.mycarrier.group,self.mycarrier.element,self.mycarrier.reserved end function OPSGROUP:_RemoveMyCarrier() self:T(self.lid..string.format("Removing my carrier!")) self.mycarrier.group=nil self.mycarrier.element=nil self.mycarrier.reserved=nil self.mycarrier={} self.cargoTransportUID=nil return self end function OPSGROUP:onafterPickup(From,Event,To) local oldstatus=self.carrierStatus self:_NewCarrierStatus(OPSGROUP.CarrierStatus.PICKUP) local TZC=self.cargoTZC local Zone=TZC.PickupZone local inzone=self:IsInZone(Zone) local airbasePickup=TZC.PickupAirbase local ready4loading=false if self:IsArmygroup()or self:IsNavygroup()then ready4loading=inzone else ready4loading=self.currbase and airbasePickup and self.currbase:GetName()==airbasePickup:GetName()and self:IsParking() if ready4loading==false and self.isHelo and self:IsLandedAt()and inzone then ready4loading=true end end if ready4loading then if(self:IsArmygroup()or self:IsNavygroup())and not self:IsHolding()then self:FullStop() end self:__Loading(-5) else local surfacetypes=nil if self:IsArmygroup()or self:IsFlightgroup()then surfacetypes={land.SurfaceType.LAND} elseif self:IsNavygroup()then surfacetypes={land.SurfaceType.WATER} end local Coordinate=Zone:GetRandomCoordinate(nil,nil,surfacetypes) local uid=self:GetWaypointCurrentUID() if self:IsFlightgroup()then if self:IsParking()and self:IsUncontrolled()then self:StartUncontrolled() end if airbasePickup then local path=self.cargoTransport:_GetPathTransport(self.category,self.cargoTZC) if path and oldstatus~=OPSGROUP.CarrierStatus.NOTCARRIER then for i=#path.waypoints,1,-1 do local wp=path.waypoints[i] local coordinate=COORDINATE:NewFromWaypoint(wp) local waypoint=FLIGHTGROUP.AddWaypoint(self,coordinate,nil,uid,nil,false);waypoint.temp=true uid=waypoint.uid if i==1 then waypoint.temp=false waypoint.detour=1 end end else local coordinate=self:GetCoordinate():GetIntermediateCoordinate(Coordinate,0.5) local waypoint=FLIGHTGROUP.AddWaypoint(self,coordinate,nil,uid,UTILS.MetersToFeet(self.altitudeCruise),true);waypoint.detour=1 end elseif self.isHelo then local waypoint=FLIGHTGROUP.AddWaypoint(self,Coordinate,nil,uid,UTILS.MetersToFeet(self.altitudeCruise),false);waypoint.detour=1 else self:T(self.lid.."ERROR: Transportcarrier aircraft cannot land in Pickup zone! Specify a ZONE_AIRBASE as pickup zone") end if self.isHelo and self:IsLandedAt()then local Task=self:GetTaskCurrent() if Task then self:TaskCancel(Task) else self:T(self.lid.."ERROR: No current task but landed at?!") end end if self:IsWaiting()then self:__Cruise(-2) end elseif self:IsNavygroup()then local path=self.cargoTransport:_GetPathTransport(self.category,self.cargoTZC) if path then for i=#path.waypoints,1,-1 do local wp=path.waypoints[i] local coordinate=COORDINATE:NewFromWaypoint(wp) local waypoint=NAVYGROUP.AddWaypoint(self,coordinate,nil,uid,nil,false);waypoint.temp=true uid=waypoint.uid end end local waypoint=NAVYGROUP.AddWaypoint(self,Coordinate,nil,uid,self.altitudeCruise,false);waypoint.detour=1 self:__Cruise(-2) elseif self:IsArmygroup()then local path=self.cargoTransport:_GetPathTransport(self.category,self.cargoTZC) local Formation=self.cargoTransport:_GetFormationPickup(self.cargoTZC,self) if path and oldstatus~=OPSGROUP.CarrierStatus.NOTCARRIER then for i=#path.waypoints,1,-1 do local wp=path.waypoints[i] local coordinate=COORDINATE:NewFromWaypoint(wp) local waypoint=ARMYGROUP.AddWaypoint(self,coordinate,nil,uid,wp.action,false);waypoint.temp=true uid=waypoint.uid end end local waypoint=ARMYGROUP.AddWaypoint(self,Coordinate,nil,uid,Formation,false);waypoint.detour=1 self:__Cruise(-2,nil,Formation) end end end function OPSGROUP:_SortCargo(Cargos) local function _sort(a,b) local cargoA=a local cargoB=b local weightA=0 local weightB=0 if cargoA.opsgroup then weightA=cargoA.opsgroup:GetWeightTotal() else weightA=self:_GetWeightStorage(cargoA.storage) end if cargoB.opsgroup then weightB=cargoB.opsgroup:GetWeightTotal() else weightB=self:_GetWeightStorage(cargoB.storage) end return weightA>weightB end table.sort(Cargos,_sort) return Cargos end function OPSGROUP:onafterLoading(From,Event,To) self:_NewCarrierStatus(OPSGROUP.CarrierStatus.LOADING) local cargos={} for _,_cargo in pairs(self.cargoTZC.Cargos)do local cargo=_cargo local canCargo=self:CanCargo(cargo) local isCarrier=false local isNotCargo=true local isHolding=cargo.type==OPSTRANSPORT.CargoType.OPSGROUP and(cargo.opsgroup:IsHolding()or cargo.opsgroup:IsLoaded())or true local inZone=cargo.type==OPSTRANSPORT.CargoType.OPSGROUP and(cargo.opsgroup:IsInZone(self.cargoTZC.EmbarkZone)or cargo.opsgroup:IsInUtero())or true local isOnMission=cargo.type==OPSTRANSPORT.CargoType.OPSGROUP and cargo.opsgroup:IsOnMission()or false if isOnMission then local mission=cargo.opsgroup:GetMissionCurrent() if mission and((mission.opstransport and mission.opstransport.uid==self.cargoTransport.uid)or mission.type==AUFTRAG.Type.NOTHING)then isOnMission=not isHolding end end local isAvail=true if cargo.type==OPSTRANSPORT.CargoType.STORAGE then local nAvail=cargo.storage.storageFrom:GetAmount(cargo.storage.cargoType) if nAvail>0 then isAvail=true else isAvail=false end else isCarrier=cargo.opsgroup:IsPickingup()or cargo.opsgroup:IsLoading()or cargo.opsgroup:IsTransporting()or cargo.opsgroup:IsUnloading() isNotCargo=cargo.opsgroup:IsNotCargo(true) end local isDead=cargo.type==OPSTRANSPORT.CargoType.OPSGROUP and cargo.opsgroup:IsDead()or false self:T(self.lid..string.format("Loading: canCargo=%s, isCarrier=%s, isNotCargo=%s, isHolding=%s, isOnMission=%s", tostring(canCargo),tostring(isCarrier),tostring(isNotCargo),tostring(isHolding),tostring(isOnMission))) if canCargo and inZone and isNotCargo and isHolding and isAvail and(not(cargo.delivered or isDead or isCarrier or isOnMission))then table.insert(cargos,cargo) end end self:_SortCargo(cargos) for _,_cargo in pairs(cargos)do local cargo=_cargo local weight=nil if cargo.type==OPSTRANSPORT.CargoType.OPSGROUP then weight=cargo.opsgroup:GetWeightTotal() local carrier=self:FindCarrierForCargo(weight) if carrier then cargo.opsgroup:Board(self,carrier) end else weight=self:_GetWeightStorage(cargo.storage,false) local Amount=cargo.storage.storageFrom:GetAmount(cargo.storage.cargoType) local Weight=Amount*cargo.storage.cargoWeight weight=math.min(weight,Weight) self:T(self.lid..string.format("Loading storage weight=%d kg (warehouse has %d kg)!",weight,Weight)) for _,_element in pairs(self.elements)do local element=_element local free=self:GetFreeCargobay(element.name) local w=math.min(weight,free) if w>=cargo.storage.cargoWeight then local amount=math.floor(w/cargo.storage.cargoWeight) cargo.storage.storageFrom:RemoveAmount(cargo.storage.cargoType,amount) cargo.storage.cargoLoaded=cargo.storage.cargoLoaded+amount self:_AddCargobayStorage(element,cargo.uid,cargo.storage.cargoType,amount,cargo.storage.cargoWeight) weight=weight-amount*cargo.storage.cargoWeight local text=string.format("Element %s: loaded amount=%d (weight=%d) ==> left=%d kg",element.name,amount,amount*cargo.storage.cargoWeight,weight) self:T(self.lid..text) if weight<=0 then break end end end end end end function OPSGROUP:_NewCargoStatus(Status) if self.verbose>=2 then self:I(self.lid..string.format("New cargo status: %s --> %s",tostring(self.cargoStatus),tostring(Status))) end self.cargoStatus=Status end function OPSGROUP:_NewCarrierStatus(Status) if self.verbose>=2 then self:I(self.lid..string.format("New carrier status: %s --> %s",tostring(self.carrierStatus),tostring(Status))) end self.carrierStatus=Status end function OPSGROUP:_TransferCargo(CargoGroup,CarrierGroup,CarrierElement) self:T(self.lid..string.format("Transferring cargo %s to new carrier group %s",CargoGroup:GetName(),CarrierGroup:GetName())) self:Unload(CargoGroup) CarrierGroup:Load(CargoGroup,CarrierElement) end function OPSGROUP:onafterLoad(From,Event,To,CargoGroup,Carrier) self:T(self.lid..string.format("Loading group %s",tostring(CargoGroup.groupname))) local carrier=Carrier or CargoGroup:_GetMyCarrierElement() if not carrier then local weight=CargoGroup:GetWeightTotal() carrier=self:FindCarrierForCargo(weight) end if carrier then CargoGroup:_NewCargoStatus(OPSGROUP.CargoStatus.LOADED) CargoGroup:ClearWaypoints() self:_AddCargobay(CargoGroup,carrier,false) if CargoGroup:IsAlive()then CargoGroup:Despawn(0,true) end CargoGroup:Embarked(self,carrier) self:Loaded(CargoGroup) if self.cargoTransport then CargoGroup:_DelMyLift(self.cargoTransport) self.cargoTransport:Loaded(CargoGroup,self,carrier) else self:T(self.lid..string.format("WARNING: Loaded cargo but no current OPSTRANSPORT assignment!")) end else self:T(self.lid.."ERROR: Cargo has no carrier on Load event!") end end function OPSGROUP:onafterLoadingDone(From,Event,To) self:T(self.lid.."Carrier Loading Done ==> Transport") self:__Transport(1) end function OPSGROUP:onbeforeTransport(From,Event,To) if self.cargoTransport==nil then return false elseif self.cargoTransport:IsDelivered()then return false end return true end function OPSGROUP:onafterTransport(From,Event,To) self:_NewCarrierStatus(OPSGROUP.CarrierStatus.TRANSPORTING) local Zone=self.cargoTZC.DeployZone local inzone=self:IsInZone(Zone) local airbaseDeploy=self.cargoTZC.DeployAirbase local ready2deploy=false if self:IsArmygroup()or self:IsNavygroup()then ready2deploy=inzone else ready2deploy=self.currbase and airbaseDeploy and self.currbase:GetName()==airbaseDeploy:GetName()and self:IsParking() if ready2deploy==false and(self.isHelo or self.isVTOL)and self:IsLandedAt()and inzone then ready2deploy=true end end if inzone then if(self:IsArmygroup()or self:IsNavygroup())and not self:IsHolding()then self:FullStop() end self:__Unloading(-5) else local surfacetypes=nil if self:IsArmygroup()or self:IsFlightgroup()then surfacetypes={land.SurfaceType.LAND} elseif self:IsNavygroup()then surfacetypes={land.SurfaceType.WATER,land.SurfaceType.SHALLOW_WATER} end local Coordinate=Zone:GetRandomCoordinate(nil,nil,surfacetypes) local uid=self:GetWaypointCurrentUID() if self:IsFlightgroup()then if self:IsParking()and self:IsUncontrolled()then self:StartUncontrolled() end if airbaseDeploy then local path=self.cargoTransport:_GetPathTransport(self.category,self.cargoTZC) if path then for i=1,#path.waypoints do local wp=path.waypoints[i] local coordinate=COORDINATE:NewFromWaypoint(wp) local waypoint=FLIGHTGROUP.AddWaypoint(self,coordinate,nil,uid,nil,false);waypoint.temp=true uid=waypoint.uid if i==#path.waypoints then waypoint.temp=false waypoint.detour=1 end end else local coordinate=self:GetCoordinate():GetIntermediateCoordinate(Coordinate,0.5) local waypoint=FLIGHTGROUP.AddWaypoint(self,coordinate,nil,uid,UTILS.MetersToFeet(self.altitudeCruise),true);waypoint.detour=1 end elseif self.isHelo then local waypoint=FLIGHTGROUP.AddWaypoint(self,Coordinate,nil,uid,UTILS.MetersToFeet(self.altitudeCruise),false);waypoint.detour=1 else self:T(self.lid.."ERROR: Aircraft (cargo carrier) cannot land in Deploy zone! Specify a ZONE_AIRBASE as deploy zone") end if self.isHelo and self:IsLandedAt()then local Task=self:GetTaskCurrent() if Task then self:TaskCancel(Task) else self:T(self.lid.."ERROR: No current task but landed at?!") end end if self:IsWaiting()then self:__Cruise(-10) end elseif self:IsArmygroup()then local path=self.cargoTransport:_GetPathTransport(self.category,self.cargoTZC) local Formation=self.cargoTransport:_GetFormationTransport(self.cargoTZC,self) if path then for i=1,#path.waypoints do local wp=path.waypoints[i] local coordinate=COORDINATE:NewFromWaypoint(wp) local waypoint=ARMYGROUP.AddWaypoint(self,coordinate,nil,uid,wp.action,false);waypoint.temp=true uid=waypoint.uid end end local waypoint=ARMYGROUP.AddWaypoint(self,Coordinate,nil,uid,Formation,false);waypoint.detour=1 self:Cruise(nil,Formation) elseif self:IsNavygroup()then local path=self.cargoTransport:_GetPathTransport(self.category,self.cargoTZC) if path then for i=1,#path.waypoints do local wp=path.waypoints[i] local coordinate=COORDINATE:NewFromWaypoint(wp) local waypoint=NAVYGROUP.AddWaypoint(self,coordinate,nil,uid,nil,false);waypoint.temp=true uid=waypoint.uid end end local waypoint=NAVYGROUP.AddWaypoint(self,Coordinate,nil,uid,self.altitudeCruise,false);waypoint.detour=1 self:Cruise() end end end function OPSGROUP:onafterUnloading(From,Event,To) self:_NewCarrierStatus(OPSGROUP.CarrierStatus.UNLOADING) self:T(self.lid.."Unloading..") local zone=self.cargoTZC.DisembarkZone or self.cargoTZC.DeployZone for _,_cargo in pairs(self.cargoTZC.Cargos)do local cargo=_cargo if cargo.type==OPSTRANSPORT.CargoType.OPSGROUP then if cargo.opsgroup:IsLoaded(self.groupname)and not cargo.opsgroup:IsDead()then local carrier=nil local carrierGroup=nil local disembarkToCarriers=cargo.disembarkCarriers~=nil or self.cargoTZC.disembarkToCarriers if cargo.disembarkZone then zone=cargo.disembarkZone end self:T(self.lid..string.format("Unloading cargo %s to zone %s",cargo.opsgroup:GetName(),zone and zone:GetName()or"No Zone Found!")) if zone and zone:IsInstanceOf("ZONE_AIRBASE")and zone:GetAirbase():IsShip()then local shipname=zone:GetAirbase():GetName() local ship=UNIT:FindByName(shipname) local group=ship:GetGroup() carrierGroup=_DATABASE:GetOpsGroup(group:GetName()) carrier=carrierGroup:GetElementByName(shipname) end if disembarkToCarriers then self:T(self.lid..string.format("Trying to find disembark carriers in zone %s",zone:GetName())) local disembarkCarriers=cargo.disembarkCarriers or self.cargoTZC.DisembarkCarriers carrier,carrierGroup=self.cargoTransport:FindTransferCarrierForCargo(cargo.opsgroup,zone,disembarkCarriers,self.cargoTZC.DeployAirbase) end if(disembarkToCarriers and carrier and carrierGroup)or(not disembarkToCarriers)then cargo.delivered=true self.cargoTransport.Ndelivered=self.cargoTransport.Ndelivered+1 if carrier and carrierGroup then self:_TransferCargo(cargo.opsgroup,carrierGroup,carrier) elseif zone and zone:IsInstanceOf("ZONE_AIRBASE")and zone:GetAirbase():IsShip()then self:T(self.lid.."ERROR: Deploy/disembark zone is a ZONE_AIRBASE of a ship! Where to put the cargo? Dumping into the sea, sorry!") self:Unload(cargo.opsgroup) else if self.cargoTransport:GetDisembarkInUtero(self.cargoTZC)then self:Unload(cargo.opsgroup) else local DisembarkZone=cargo.disembarkZone or self.cargoTransport:GetDisembarkZone(self.cargoTZC) local Coordinate=nil if DisembarkZone then Coordinate=DisembarkZone:GetRandomCoordinate() else local element=cargo.opsgroup:_GetMyCarrierElement() if element then local zoneCarrier=self:GetElementZoneUnload(element.name) Coordinate=zoneCarrier:GetRandomCoordinate() else self:E(self.lid..string.format("ERROR carrier element nil!")) end end local Heading=math.random(0,359) local activation=self.cargoTransport:GetDisembarkActivation(self.cargoTZC) if cargo.disembarkActivation~=nil then activation=cargo.disembarkActivation end self:Unload(cargo.opsgroup,Coordinate,activation,Heading) end self.cargoTransport:Unloaded(cargo.opsgroup,self) end else self:T(self.lid.."Cargo needs carrier but no carrier is avaiable (yet)!") end else end else if not cargo.delivered then for _,_element in pairs(self.elements)do local element=_element local mycargo=self:_GetCargobayElement(element,cargo.uid) if mycargo then cargo.storage.storageTo:AddAmount(mycargo.storageType,mycargo.storageAmount) cargo.storage.cargoDelivered=cargo.storage.cargoDelivered+mycargo.storageAmount cargo.storage.cargoLoaded=cargo.storage.cargoLoaded-mycargo.storageAmount self:_DelCargobayElement(element,mycargo) self:T2(self.lid..string.format("Cargo loaded=%d, delivered=%d, lost=%d",cargo.storage.cargoLoaded,cargo.storage.cargoDelivered,cargo.storage.cargoLost)) end end local amountToDeliver=self:_GetWeightStorage(cargo.storage,false,false,true) local amountTotal=self:_GetWeightStorage(cargo.storage,true,false,true) local text=string.format("Amount delivered=%d, total=%d",amountToDeliver,amountTotal) self:T(self.lid..text) if amountToDeliver<=0 then cargo.delivered=true self.cargoTransport.Ndelivered=self.cargoTransport.Ndelivered+1 local text=string.format("Ndelivered=%d delivered=%s",self.cargoTransport.Ndelivered,tostring(cargo.delivered)) self:T(self.lid..text) end end end end end function OPSGROUP:onbeforeUnload(From,Event,To,OpsGroup,Coordinate,Heading) local removed=self:_DelCargobay(OpsGroup) return removed end function OPSGROUP:onafterUnload(From,Event,To,OpsGroup,Coordinate,Activated,Heading) OpsGroup:_NewCargoStatus(OPSGROUP.CargoStatus.NOTCARGO) if Coordinate then local Template=UTILS.DeepCopy(OpsGroup.template) if Activated==false then Template.lateActivation=true else Template.lateActivation=false end for _,Unit in pairs(Template.units)do local element=OpsGroup:GetElementByName(Unit.name) if element then local vec3=element.vec3 local rvec2={x=Unit.x-Template.x,y=Unit.y-Template.y} local cvec2={x=Coordinate.x,y=Coordinate.z} Unit.x=cvec2.x+rvec2.x Unit.y=cvec2.y+rvec2.y Unit.alt=land.getHeight({x=Unit.x,y=Unit.y}) Unit.heading=Heading and math.rad(Heading)or Unit.heading Unit.psi=-Unit.heading end end OpsGroup:_Respawn(0,Template) if OpsGroup:IsNavygroup()then OpsGroup:ClearWaypoints() OpsGroup.currentwp=1 OpsGroup.passedfinalwp=true NAVYGROUP.AddWaypoint(OpsGroup,Coordinate,nil,nil,nil,false) elseif OpsGroup:IsArmygroup()then OpsGroup:ClearWaypoints() OpsGroup.currentwp=1 OpsGroup.passedfinalwp=true ARMYGROUP.AddWaypoint(OpsGroup,Coordinate,nil,nil,nil,false) end else OpsGroup.position=self:GetVec3() end OpsGroup:Disembarked(OpsGroup:_GetMyCarrierGroup(),OpsGroup:_GetMyCarrierElement()) self:Unloaded(OpsGroup) OpsGroup:_RemoveMyCarrier() end function OPSGROUP:onafterUnloaded(From,Event,To,OpsGroupCargo) self:T(self.lid..string.format("Unloaded OPSGROUP %s",OpsGroupCargo:GetName())) if OpsGroupCargo.legion and OpsGroupCargo:IsInZone(OpsGroupCargo.legion.spawnzone)then self:T(self.lid..string.format("Unloaded group %s returned to legion",OpsGroupCargo:GetName())) OpsGroupCargo:Returned() end local paused=OpsGroupCargo:_CountPausedMissions()>0 if paused then OpsGroupCargo:UnpauseMission() end end function OPSGROUP:onafterUnloadingDone(From,Event,To) self:T(self.lid.."Cargo unloading done..") if self:IsFlightgroup()and self:IsLandedAt()then local Task=self:GetTaskCurrent() self:__TaskCancel(5,Task) end local delivered=self:_CheckGoPickup(self.cargoTransport) if not delivered then self.cargoTZC=self.cargoTransport:_GetTransportZoneCombo(self) if self.cargoTZC then self:T(self.lid.."Unloaded: Still cargo left ==> Pickup") self:Pickup() else self:T(self.lid..string.format("WARNING: Not all cargo was delivered but could not get a transport zone combo ==> setting carrier state to NOT CARRIER")) self:_NewCarrierStatus(OPSGROUP.CarrierStatus.NOTCARRIER) end else self:T(self.lid.."Unloaded: ALL cargo unloaded ==> Delivered (current)") self:Delivered(self.cargoTransport) end end function OPSGROUP:onafterDelivered(From,Event,To,CargoTransport) if self.cargoTransport and self.cargoTransport.uid==CargoTransport.uid then if self:IsPickingup()then local wpindex=self:GetWaypointIndexNext(false) if wpindex then self:RemoveWaypoint(wpindex) end self.isLandingAtAirbase=nil elseif self:IsLoading()then elseif self:IsTransporting()then elseif self:IsUnloading()then end self:_NewCarrierStatus(OPSGROUP.CarrierStatus.NOTCARRIER) if self:IsFlightgroup()then local function atbase(_airbase) local airbase=_airbase if airbase and self.currbase then if airbase.AirbaseName==self.currbase.AirbaseName then return true end end return false end if self:IsUncontrolled()and not atbase(self.destbase)then self:StartUncontrolled() end if self:IsLandedAt()then local Task=self:GetTaskCurrent() self:TaskCancel(Task) end else self:__Cruise(-0.1) end self.cargoTransport:SetCarrierTransportStatus(self,OPSTRANSPORT.Status.DELIVERED) self:T(self.lid..string.format("All cargo of transport UID=%d delivered ==> check group done in 0.2 sec",self.cargoTransport.uid)) self:_CheckGroupDone(0.2) end end function OPSGROUP:onafterTransportCancel(From,Event,To,Transport) if self.cargoTransport and self.cargoTransport.uid==Transport.uid then self:T(self.lid..string.format("Cancel current transport %d",Transport.uid)) local calldelivered=false if self:IsPickingup()then calldelivered=true elseif self:IsLoading()then local cargos=Transport:GetCargoOpsGroups(false) for _,_opsgroup in pairs(cargos)do local opsgroup=_opsgroup if opsgroup:IsBoarding(self.groupname)then opsgroup:RemoveWaypoint(self.currentwp+1) self:_DelCargobay(opsgroup) opsgroup:_RemoveMyCarrier() opsgroup:_NewCargoStatus(OPSGROUP.CargoStatus.NOTCARGO) elseif opsgroup:IsLoaded(self.groupname)then local zoneCarrier=self:GetElementZoneUnload(opsgroup:_GetMyCarrierElement().name) local Coordinate=zoneCarrier and zoneCarrier:GetRandomCoordinate()or self.cargoTransport:GetEmbarkZone(self.cargoTZC):GetRandomCoordinate() local Heading=math.random(0,359) self:Unload(opsgroup,Coordinate,self.cargoTransport:GetDisembarkActivation(self.cargoTZC),Heading) self.cargoTransport:Unloaded(opsgroup,self) end end calldelivered=true elseif self:IsTransporting()then elseif self:IsUnloading()then else end if calldelivered then self:__Delivered(-2,Transport) end else Transport:SetCarrierTransportStatus(self,AUFTRAG.GroupStatus.CANCELLED) self:DelOpsTransport(Transport) self:_CheckGroupDone(1) end end function OPSGROUP:onbeforeBoard(From,Event,To,CarrierGroup,Carrier) if self:IsDead()then self:T(self.lid.."Group DEAD ==> Deny Board transition!") return false elseif CarrierGroup:IsDead()then self:T(self.lid.."Carrier Group DEAD ==> Deny Board transition!") self:_NewCargoStatus(OPSGROUP.CargoStatus.NOTCARGO) return false elseif Carrier.status==OPSGROUP.ElementStatus.DEAD then self:T(self.lid.."Carrier Element DEAD ==> Deny Board transition!") self:_NewCargoStatus(OPSGROUP.CargoStatus.NOTCARGO) return false end return true end function OPSGROUP:onafterBoard(From,Event,To,CarrierGroup,Carrier) local CarrierIsArmyOrNavy=CarrierGroup:IsArmygroup()or CarrierGroup:IsNavygroup() local CargoIsArmyOrNavy=self:IsArmygroup()or self:IsNavygroup() if(CarrierIsArmyOrNavy and(CarrierGroup:GetVelocity(Carrier.name)<=1))or(CarrierGroup:IsFlightgroup()and(CarrierGroup:IsParking()or CarrierGroup:IsLandedAt()))then local board=self.speedMax>0 and CargoIsArmyOrNavy and self:IsAlive()and CarrierGroup:IsAlive() if self:IsArmygroup()and CarrierGroup:IsNavygroup()then board=false end if self:IsLoaded()then self:T(self.lid..string.format("Group is loaded currently ==> Moving directly to new carrier - No Unload(), Disembart() events triggered!")) self:_RemoveMyCarrier() CarrierGroup:Load(self) elseif board then self:_NewCargoStatus(OPSGROUP.CargoStatus.BOARDING) self:T(self.lid..string.format("Boarding group=%s [%s], carrier=%s",CarrierGroup:GetName(),CarrierGroup:GetState(),tostring(Carrier.name))) local Coordinate=Carrier.unit:GetCoordinate() self:ClearWaypoints(self.currentwp+1) if self.isArmygroup then local waypoint=ARMYGROUP.AddWaypoint(self,Coordinate,nil,nil,ENUMS.Formation.Vehicle.Diamond);waypoint.detour=1 self:Cruise() else local waypoint=NAVYGROUP.AddWaypoint(self,Coordinate);waypoint.detour=1 self:Cruise() end CarrierGroup:_AddCargobay(self,Carrier,true) else self:T(self.lid..string.format("Board [loaded=%s] with direct load to carrier group=%s, element=%s",tostring(self:IsLoaded()),CarrierGroup:GetName(),tostring(Carrier.name))) local mycarriergroup=self:_GetMyCarrierGroup() if mycarriergroup then self:T(self.lid..string.format("Current carrier group %s",mycarriergroup:GetName())) end if mycarriergroup and mycarriergroup:GetName()~=CarrierGroup:GetName()then self:T(self.lid.."Unloading from mycarrier") mycarriergroup:Unload(self) end CarrierGroup:Load(self) end else self:T(self.lid.."Carrier not ready for boarding yet ==> repeating boarding call in 10 sec") self:__Board(-10,CarrierGroup,Carrier) CarrierGroup:_AddCargobay(self,Carrier,true) end end function OPSGROUP:_CheckInZones() if self.checkzones and self:IsAlive()then local Ncheck=self.checkzones:Count() local Ninside=self.inzones:Count() self:T(self.lid..string.format("Check if group is in %d zones. Currently it is in %d zones.",self.checkzones:Count(),self.inzones:Count())) local leftzones={} for inzonename,inzone in pairs(self.inzones:GetSet())do local isstillinzone=self.group:IsInZone(inzone) if not isstillinzone then table.insert(leftzones,inzone) end end for _,leftzone in pairs(leftzones)do self:LeaveZone(leftzone) end local enterzones={} for checkzonename,_checkzone in pairs(self.checkzones:GetSet())do local checkzone=_checkzone local isincheckzone=self.group:IsInZone(checkzone) if isincheckzone and not self.inzones:_Find(checkzonename)then table.insert(enterzones,checkzone) end end for _,enterzone in pairs(enterzones)do self:EnterZone(enterzone) end end end function OPSGROUP:_CheckDetectedUnits() if self.detectionOn and self.group and not self:IsDead()then local detectedtargets=self.group:GetDetectedTargets() local detected={} local groups={} for DetectionObjectID,Detection in pairs(detectedtargets or{})do local DetectedObject=Detection.object if DetectedObject and DetectedObject:isExist()and DetectedObject.id_<50000000 then local unit=UNIT:Find(DetectedObject) if unit and unit:IsAlive()then local unitname=unit:GetName() table.insert(detected,unit) self:DetectedUnit(unit) local group=unit:GetGroup() if group then groups[group:GetName()]=group end end end end for groupname,group in pairs(groups)do self:DetectedGroup(group) end local lost={} for _,_unit in pairs(self.detectedunits:GetSet())do local unit=_unit local gotit=false for _,_du in pairs(detected)do local du=_du if unit:GetName()==du:GetName()then gotit=true end end if not gotit then table.insert(lost,unit:GetName()) self:DetectedUnitLost(unit) end end self.detectedunits:RemoveUnitsByName(lost) local lost={} for _,_group in pairs(self.detectedgroups:GetSet())do local group=_group local gotit=false for _,_du in pairs(groups)do local du=_du if group:GetName()==du:GetName()then gotit=true end end if not gotit then table.insert(lost,group:GetName()) self:DetectedGroupLost(group) end end self.detectedgroups:RemoveGroupsByName(lost) end end function OPSGROUP:_CheckGroupDone(delay) local fsmstate=self:GetState() if self:IsAlive()and self.isAI then if delay and delay>0 then self:T(self.lid..string.format("Check OPSGROUP done? [state=%s] in %.3f seconds...",fsmstate,delay)) self:ScheduleOnce(delay,self._CheckGroupDone,self) else self:T(self.lid..string.format("Check OSGROUP done? [state=%s]",fsmstate)) if self:IsEngaging()then self:T(self.lid.."Engaging! Group NOT done ==> UpdateRoute()") self:UpdateRoute() return end if self:IsReturning()then self:T(self.lid.."Returning! Group NOT done...") return end if self:IsRearming()then self:T(self.lid.."Rearming! Group NOT done...") return end if self:IsRetreating()then self:T(self.lid.."Retreating! Group NOT done...") return end if self:IsBoarding()then self:T(self.lid.."Boarding! Group NOT done...") return end if self:IsWaiting()then self:T(self.lid.."Waiting! Group NOT done...") return end local nTasks=self:CountRemainingTasks() local nMissions=self:CountRemainingMissison() local nTransports=self:CountRemainingTransports() local nPaused=self:_CountPausedMissions() if nPaused>0 and nPaused==nMissions then local missionpaused=self:_GetPausedMission() self:T(self.lid..string.format("Found paused mission %s [%s]. Unpausing mission...",missionpaused.name,missionpaused.type)) self:UnpauseMission() return end if nTasks>0 or nMissions>0 or nTransports>0 then self:T(self.lid..string.format("Group still has tasks, missions or transports ==> NOT DONE")) return end local waypoint=self:GetWaypoint(self.currentwp) if waypoint then local ntasks=self:CountTasksWaypoint(waypoint.uid) if ntasks>0 then self:T(self.lid..string.format("Still got %d tasks for the current waypoint UID=%d ==> RETURN (no action)",ntasks,waypoint.uid)) return end end if self.adinfinitum then if#self.waypoints>0 then local i=self:GetWaypointIndexNext(true) local speed=self:GetSpeedToWaypoint(i) self:Cruise(speed) self:T(self.lid..string.format("Adinfinitum=TRUE ==> Goto WP index=%d at speed=%d knots",i,speed)) else self:T(self.lid..string.format("WARNING: No waypoints left! Commanding a Full Stop")) self:__FullStop(-1) end else if self:HasPassedFinalWaypoint()then if self.legion and self.legionReturn then self:T(self.lid..string.format("Passed final WP, adinfinitum=FALSE, LEGION set ==> RTZ")) if self.isArmygroup then self:T2(self.lid.."RTZ to legion spawn zone") self:RTZ(self.legion.spawnzone) elseif self.isNavygroup then self:T2(self.lid.."RTZ to legion port zone") self:RTZ(self.legion.portzone) end else self:__FullStop(-1) self:T(self.lid..string.format("Passed final WP, adinfinitum=FALSE ==> Full Stop")) end else if#self.waypoints>0 then self:T(self.lid..string.format("NOT Passed final WP, #WP>0 ==> Update Route")) self:Cruise() else self:T(self.lid..string.format("WARNING: No waypoints left! Commanding a Full Stop")) self:__FullStop(-1) end end end end end end function OPSGROUP:_CheckStuck() if self:IsHolding()or self:Is("Rearming")or self:IsWaiting()or self:HasPassedFinalWaypoint()then return end local Tnow=timer.getTime() local ExpectedSpeed=self:GetExpectedSpeed() local speed=self:GetVelocity() if speed<0.1 then if ExpectedSpeed>0 and not self.stuckTimestamp then self:T2(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected",speed,ExpectedSpeed)) self.stuckTimestamp=Tnow self.stuckVec3=self:GetVec3() end else self.stuckTimestamp=nil end if self.stuckTimestamp then local holdtime=Tnow-self.stuckTimestamp if holdtime>=5*60 and holdtime<10*60 then self:T(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected for %d sec",speed,ExpectedSpeed,holdtime)) if self:IsEngaging()then self:__Disengage(1) elseif self:IsReturning()then self:T2(self.lid.."RTZ because of stuck") self:__RTZ(1) else self:__Cruise(1) end elseif holdtime>=10*60 and holdtime<30*60 then self:T(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected for %d sec",speed,ExpectedSpeed,holdtime)) local mission=self:GetMissionCurrent() if mission then self:T(self.lid..string.format("WARNING: Cancelling mission %s [%s] due to being stuck",mission:GetName(),mission:GetType())) self:MissionCancel(mission) else if self:IsReturning()then self:T2(self.lid.."RTZ because of stuck") self:__RTZ(1) else self:__Cruise(1) end end elseif holdtime>=30*60 then self:T(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected for %d sec",speed,ExpectedSpeed,holdtime)) if self.legion then self:T(self.lid..string.format("Asset is returned to its legion after being stuck!")) self:ReturnToLegion() end end end end function OPSGROUP:_CheckDamage() self:T(self.lid..string.format("Checking damage...")) self.life=0 local damaged=false for _,_element in pairs(self.elements)do local element=_element if element.status~=OPSGROUP.ElementStatus.DEAD and element.status~=OPSGROUP.ElementStatus.INUTERO then local life=element.unit:GetLife() self.life=self.life+life if life0 then local ammo=self:GetAmmoTot() if self:IsRearming()then if ammo.Total>=self.ammo.Total then self:Rearmed() end end if self.outofAmmo and ammo.Total>0 then self.outofAmmo=false end if ammo.Total==0 and not self.outofAmmo then self.outofAmmo=true self:OutOfAmmo() end if self.outofGuns and ammo.Shells>0 then self.outofGuns=false end if ammo.Shells==0 and self.ammo.Shells>0 and not self.outofGuns then self.outofGuns=true self:OutOfGuns() end if self.outofRockets and ammo.Rockets>0 then self.outofRockets=false end if ammo.Rockets==0 and self.ammo.Rockets>0 and not self.outofRockets then self.outofRockets=true self:OutOfRockets() end if self.outofBombs and ammo.Bombs>0 then self.outofBombs=false end if ammo.Bombs==0 and self.ammo.Bombs>0 and not self.outofBombs then self.outofBombs=true self:OutOfBombs() end if self.outofMissiles and ammo.Missiles>0 then self.outofMissiles=false end if ammo.Missiles==0 and self.ammo.Missiles>0 and not self.outofMissiles then self.outofMissiles=true self:OutOfMissiles() end if self.outofMissilesAA and ammo.MissilesAA>0 then self.outofMissilesAA=false end if ammo.MissilesAA==0 and self.ammo.MissilesAA>0 and not self.outofMissilesAA then self.outofMissilesAA=true self:OutOfMissilesAA() end if self.outofMissilesAG and ammo.MissilesAG>0 then self.outofMissilesAG=false end if ammo.MissilesAG==0 and self.ammo.MissilesAG>0 and not self.outofMissilesAG then self.outofMissilesAG=true self:OutOfMissilesAG() end if self.outofMissilesAS and ammo.MissilesAS>0 then self.outofMissilesAS=false end if ammo.MissilesAS==0 and self.ammo.MissilesAS>0 and not self.outofMissilesAS then self.outofMissilesAS=true self:OutOfMissilesAS() end if self.outofTorpedos and ammo.Torpedos>0 then self.outofTorpedos=false end if ammo.Torpedos==0 and self.ammo.Torpedos>0 and not self.outofTorpedos then self.outofTorpedos=true self:OutOfTorpedos() end if self:IsEngaging()and ammo.Total==0 then self:Disengage() end end end function OPSGROUP:_PrintTaskAndMissionStatus() if self.verbose>=3 and#self.taskqueue>0 then local text=string.format("Tasks #%d",#self.taskqueue) for i,_task in pairs(self.taskqueue)do local task=_task local name=task.description local taskid=task.dcstask.id or"unknown" local status=task.status local clock=UTILS.SecondsToClock(task.time,true) local eta=task.time-timer.getAbsTime() local started=task.timestamp and UTILS.SecondsToClock(task.timestamp,true)or"N/A" local duration=-1 if task.duration then duration=task.duration if task.timestamp then duration=task.duration-(timer.getAbsTime()-task.timestamp) else duration=task.duration end end if task.type==OPSGROUP.TaskType.SCHEDULED then text=text..string.format("\n[%d] %s (%s): status=%s, scheduled=%s (%d sec), started=%s, duration=%d",i,taskid,name,status,clock,eta,started,duration) elseif task.type==OPSGROUP.TaskType.WAYPOINT then text=text..string.format("\n[%d] %s (%s): status=%s, waypoint=%d, started=%s, duration=%d, stopflag=%d",i,taskid,name,status,task.waypoint,started,duration,task.stopflag:Get()) end end self:I(self.lid..text) end if self.verbose>=2 then local Mission=self:GetMissionByID(self.currentmission) local text=string.format("Missions %d, Current: %s",self:CountRemainingMissison(),Mission and Mission.name or"none") for i,_mission in pairs(self.missionqueue)do local mission=_mission local Cstart=UTILS.SecondsToClock(mission.Tstart,true) local Cstop=mission.Tstop and UTILS.SecondsToClock(mission.Tstop,true)or"INF" text=text..string.format("\n[%d] %s (%s) status=%s (%s), Time=%s-%s, prio=%d wp=%s targets=%d", i,tostring(mission.name),mission.type,mission:GetGroupStatus(self),tostring(mission.status),Cstart,Cstop,mission.prio,tostring(mission:GetGroupWaypointIndex(self)),mission:CountMissionTargets()) end self:I(self.lid..text) end end function OPSGROUP:_SimpleTaskFunction(Function,uid) local DCSScript={} DCSScript[#DCSScript+1]=string.format('local mygroup = _DATABASE:FindOpsGroup(\"%s\") ',self.groupname) DCSScript[#DCSScript+1]=string.format('%s(mygroup, %d)',Function,uid) local DCSTask=CONTROLLABLE.TaskWrappedAction(self,CONTROLLABLE.CommandDoScript(self,table.concat(DCSScript))) return DCSTask end function OPSGROUP:_CreateWaypoint(waypoint) waypoint.uid=self.wpcounter waypoint.npassed=0 waypoint.coordinate=COORDINATE:New(waypoint.x,waypoint.alt,waypoint.y) waypoint.name=string.format("Waypoint UID=%d",waypoint.uid) waypoint.patrol=false waypoint.detour=false waypoint.astar=false waypoint.temp=false local taskswp={} local TaskPassingWaypoint=self:_SimpleTaskFunction("OPSGROUP._PassingWaypoint",waypoint.uid) table.insert(taskswp,TaskPassingWaypoint) waypoint.task=self.group:TaskCombo(taskswp) self.wpcounter=self.wpcounter+1 return waypoint end function OPSGROUP:_AddWaypoint(waypoint,wpnumber) wpnumber=wpnumber or#self.waypoints+1 table.insert(self.waypoints,wpnumber,waypoint) self:T(self.lid..string.format("Adding waypoint at index=%d with UID=%d",wpnumber,waypoint.uid)) if self.currentwp and wpnumber>self.currentwp then self:_PassedFinalWaypoint(false,string.format("_AddWaypoint: wpnumber/index %d>%d self.currentwp",wpnumber,self.currentwp)) end end function OPSGROUP:_InitWaypoints(WpIndexMin,WpIndexMax) self.waypoints={} self.waypoints0={} local template=_DATABASE:GetGroupTemplate(self.groupname) if template==nil then return self end self.waypoints0=UTILS.DeepCopy(template.route.points) WpIndexMin=WpIndexMin or 1 WpIndexMax=WpIndexMax or#self.waypoints0 WpIndexMax=math.min(WpIndexMax,#self.waypoints0) for i=WpIndexMin,WpIndexMax do local wp=self.waypoints0[i] local Coordinate=COORDINATE:NewFromWaypoint(wp) wp.speed=wp.speed or 0 local speedknots=UTILS.MpsToKnots(wp.speed) if i<=2 then self.speedWp=wp.speed self:T(self.lid..string.format("Expected/waypoint speed=%.1f m/s",self.speedWp)) end local Speed=UTILS.MpsToKnots(wp.speed) local Waypoint=nil if self:IsFlightgroup()then Waypoint=FLIGHTGROUP.AddWaypoint(self,Coordinate,Speed,nil,Altitude,false) elseif self:IsArmygroup()then Waypoint=ARMYGROUP.AddWaypoint(self,Coordinate,Speed,nil,wp.action,false) elseif self:IsNavygroup()then Waypoint=NAVYGROUP.AddWaypoint(self,Coordinate,Speed,nil,Depth,false) end local DCStasks=wp.task and wp.task.params.tasks or nil if DCStasks and self.useMEtasks then for _,DCStask in pairs(DCStasks)do if DCStask.id and DCStask.id~="WrappedAction"then self:AddTaskWaypoint(DCStask,Waypoint,"ME Task") end end end end self:T(self.lid..string.format("Initializing %d waypoints",#self.waypoints)) if self:IsFlightgroup()then self.homebase=self.homebase or self:GetHomebaseFromWaypoints() local destbase=self:GetDestinationFromWaypoints() self.destbase=self.destbase or destbase self.currbase=self:GetHomebaseFromWaypoints() if destbase and#self.waypoints>1 then table.remove(self.waypoints,#self.waypoints) end if self.destbase==nil then self.destbase=self.homebase end end if#self.waypoints>0 then if#self.waypoints==1 then self:_PassedFinalWaypoint(true,"_InitWaypoints: #self.waypoints==1") end else self:T(self.lid.."WARNING: No waypoints initialized. Number of waypoints is 0!") end return self end function OPSGROUP:Route(waypoints,delay) if delay and delay>0 then self:ScheduleOnce(delay,OPSGROUP.Route,self,waypoints) else if self:IsAlive()then local DCSTask={ id='Mission', params={ airborne=self:IsFlightgroup(), route={points=waypoints}, }, } self:SetTask(DCSTask) else self:T(self.lid.."ERROR: Group is not alive! Cannot route group.") end end return self end function OPSGROUP:_UpdateWaypointTasks(n) local waypoints=self.waypoints or{} local nwaypoints=#waypoints for i,_wp in pairs(waypoints)do local wp=_wp if i>=n or nwaypoints==1 then self:T2(self.lid..string.format("Updating waypoint task for waypoint %d/%d ID=%d. Last waypoint passed %d",i,nwaypoints,wp.uid,self.currentwp)) local taskswp={} local TaskPassingWaypoint=self.group:TaskFunction("OPSGROUP._PassingWaypoint",self,wp.uid) table.insert(taskswp,TaskPassingWaypoint) wp.task=self.group:TaskCombo(taskswp) end end end function OPSGROUP._PassingWaypoint(opsgroup,uid) local text=string.format("Group passing waypoint uid=%d",uid) opsgroup:T(opsgroup.lid..text) local waypoint=opsgroup:GetWaypointByID(uid) if waypoint then waypoint.npassed=waypoint.npassed+1 local currentwp=opsgroup.currentwp opsgroup.currentwp=opsgroup:GetWaypointIndex(uid) local wpistemp=waypoint.temp or waypoint.detour or waypoint.astar if wpistemp then opsgroup:RemoveWaypointByID(uid) end local wpnext=opsgroup:GetWaypointNext() if wpnext then opsgroup:T(opsgroup.lid..string.format("Next waypoint UID=%d index=%d",wpnext.uid,opsgroup:GetWaypointIndex(wpnext.uid))) if opsgroup.isArmygroup then opsgroup.option.Formation=wpnext.action end opsgroup.speed=wpnext.speed if opsgroup.speed<0.01 then opsgroup.speed=UTILS.KmphToMps(opsgroup.speedCruise) end else opsgroup:_PassedFinalWaypoint(true,"_PassingWaypoint No next Waypoint found") end if opsgroup.currentwp==#opsgroup.waypoints and not(opsgroup.adinfinitum or wpistemp)then opsgroup:_PassedFinalWaypoint(true,"_PassingWaypoint currentwp==#waypoints and NOT adinfinitum and NOT a temporary waypoint") end if waypoint.temp then if(opsgroup:IsNavygroup()or opsgroup:IsArmygroup())and opsgroup.currentwp==#opsgroup.waypoints then opsgroup:Cruise() end elseif waypoint.astar then opsgroup:Cruise() elseif waypoint.detour then if opsgroup:IsRearming()then opsgroup:Rearming() elseif opsgroup:IsRetreating()then opsgroup:Retreated() elseif opsgroup:IsReturning()then opsgroup:Returned() elseif opsgroup:IsPickingup()then if opsgroup:IsFlightgroup()then if opsgroup.cargoTZC then if opsgroup.cargoTZC.PickupAirbase then opsgroup:LandAtAirbase(opsgroup.cargoTZC.PickupAirbase) else local coordinate=opsgroup.cargoTZC.PickupZone:GetRandomCoordinate(nil,nil,{land.SurfaceType.LAND}) opsgroup:LandAt(coordinate,60*60) end else local coordinate=opsgroup:GetCoordinate() opsgroup:LandAt(coordinate,60*60) end else opsgroup:FullStop() opsgroup:__Loading(-5) end elseif opsgroup:IsTransporting()then if opsgroup:IsFlightgroup()then if opsgroup.cargoTZC then if opsgroup.cargoTZC.DeployAirbase then opsgroup:LandAtAirbase(opsgroup.cargoTZC.DeployAirbase) else local coordinate=opsgroup.cargoTZC.DeployZone:GetRandomCoordinate(nil,nil,{land.SurfaceType.LAND}) opsgroup:LandAt(coordinate,60*60) end else local coordinate=opsgroup:GetCoordinate() opsgroup:LandAt(coordinate,60*60) end else opsgroup:FullStop() opsgroup:Unloading() end elseif opsgroup:IsBoarding()then local carrierGroup=opsgroup:_GetMyCarrierGroup() local carrier=opsgroup:_GetMyCarrierElement() if carrierGroup and carrierGroup:IsAlive()then if carrier and carrier.unit and carrier.unit:IsAlive()then carrierGroup:Load(opsgroup) else opsgroup:E(opsgroup.lid.."ERROR: Group cannot board assigned carrier UNIT as it is NOT alive!") end else opsgroup:E(opsgroup.lid.."ERROR: Group cannot board assigned carrier GROUP as it is NOT alive!") end elseif opsgroup:IsEngaging()then opsgroup:T(opsgroup.lid.."Passing engaging waypoint") else opsgroup:DetourReached() if waypoint.detour==0 then opsgroup:FullStop() elseif waypoint.detour==1 then opsgroup:Cruise() else opsgroup:E("ERROR: waypoint.detour should be 0 or 1") opsgroup:FullStop() end end else if opsgroup.ispathfinding then opsgroup.ispathfinding=false end opsgroup:PassingWaypoint(waypoint) end end end function OPSGROUP._TaskExecute(group,opsgroup,task) local text=string.format("_TaskExecute %s",task.description) opsgroup:T3(opsgroup.lid..text) if opsgroup then opsgroup:TaskExecute(task) end end function OPSGROUP._TaskDone(group,opsgroup,task) local text=string.format("_TaskDone %s",task.description) opsgroup:T(opsgroup.lid..text) if opsgroup then opsgroup:TaskDone(task) end end function OPSGROUP:SetDefaultROE(roe) self.optionDefault.ROE=roe or ENUMS.ROE.ReturnFire return self end function OPSGROUP:SwitchROE(roe) if self:IsAlive()or self:IsInUtero()then self.option.ROE=roe or self.optionDefault.ROE if self:IsInUtero()then self:T2(self.lid..string.format("Setting current ROE=%d when GROUP is SPAWNED",self.option.ROE)) else self.group:OptionROE(self.option.ROE) self:T(self.lid..string.format("Setting current ROE=%d (%s)",self.option.ROE,self:_GetROEName(self.option.ROE))) end else self:T(self.lid.."WARNING: Cannot switch ROE! Group is not alive") end return self end function OPSGROUP:_GetROEName(roe) local name="unknown" if roe==0 then name="Weapon Free" elseif roe==1 then name="Open Fire/Weapon Free" elseif roe==2 then name="Open Fire" elseif roe==3 then name="Return Fire" elseif roe==4 then name="Weapon Hold" end return name end function OPSGROUP:GetROE() return self.option.ROE or self.optionDefault.ROE end function OPSGROUP:SetDefaultROT(rot) self.optionDefault.ROT=rot or ENUMS.ROT.PassiveDefense return self end function OPSGROUP:SwitchROT(rot) if self:IsFlightgroup()then if self:IsAlive()or self:IsInUtero()then self.option.ROT=rot or self.optionDefault.ROT if self:IsInUtero()then self:T2(self.lid..string.format("Setting current ROT=%d when GROUP is SPAWNED",self.option.ROT)) else self.group:OptionROT(self.option.ROT) self:T(self.lid..string.format("Setting current ROT=%d (0=NoReaction, 1=Passive, 2=Evade, 3=ByPass, 4=AllowAbort)",self.option.ROT)) end else self:T(self.lid.."WARNING: Cannot switch ROT! Group is not alive") end end return self end function OPSGROUP:GetROT() return self.option.ROT or self.optionDefault.ROT end function OPSGROUP:SetDefaultAlarmstate(alarmstate) self.optionDefault.Alarm=alarmstate or 0 return self end function OPSGROUP:SwitchAlarmstate(alarmstate) if self:IsAlive()or self:IsInUtero()then if self.isArmygroup or self.isNavygroup then self.option.Alarm=alarmstate or self.optionDefault.Alarm if self:IsInUtero()then self:T2(self.lid..string.format("Setting current Alarm State=%d when GROUP is SPAWNED",self.option.Alarm)) else if self.option.Alarm==0 then self.group:OptionAlarmStateAuto() elseif self.option.Alarm==1 then self.group:OptionAlarmStateGreen() elseif self.option.Alarm==2 then self.group:OptionAlarmStateRed() else self:T("ERROR: Unknown Alarm State! Setting to AUTO") self.group:OptionAlarmStateAuto() self.option.Alarm=0 end self:T(self.lid..string.format("Setting current Alarm State=%d (0=Auto, 1=Green, 2=Red)",self.option.Alarm)) end end else self:T(self.lid.."WARNING: Cannot switch Alarm State! Group is not alive.") end return self end function OPSGROUP:GetAlarmstate() return self.option.Alarm or self.optionDefault.Alarm end function OPSGROUP:SetDefaultEPLRS(OnOffSwitch) if OnOffSwitch==nil then self.optionDefault.EPLRS=self.isEPLRS else self.optionDefault.EPLRS=OnOffSwitch end return self end function OPSGROUP:SwitchEPLRS(OnOffSwitch) if self:IsAlive()or self:IsInUtero()then if OnOffSwitch==nil then self.option.EPLRS=self.optionDefault.EPLRS else self.option.EPLRS=OnOffSwitch end if self:IsInUtero()then self:T2(self.lid..string.format("Setting current EPLRS=%s when GROUP is SPAWNED",tostring(self.option.EPLRS))) else self.group:CommandEPLRS(self.option.EPLRS) self:T(self.lid..string.format("Setting current EPLRS=%s",tostring(self.option.EPLRS))) end else self:E(self.lid.."WARNING: Cannot switch EPLRS! Group is not alive") end return self end function OPSGROUP:GetEPLRS() return self.option.EPLRS or self.optionDefault.EPLRS end function OPSGROUP:SetDefaultEmission(OnOffSwitch) if OnOffSwitch==nil then self.optionDefault.Emission=true else self.optionDefault.Emission=OnOffSwitch end return self end function OPSGROUP:SwitchEmission(OnOffSwitch) if self:IsAlive()or self:IsInUtero()then if OnOffSwitch==nil then self.option.Emission=self.optionDefault.Emission else self.option.Emission=OnOffSwitch end if self:IsInUtero()then self:T2(self.lid..string.format("Setting current EMISSION=%s when GROUP is SPAWNED",tostring(self.option.Emission))) else self.group:EnableEmission(self.option.Emission) self:T(self.lid..string.format("Setting current EMISSION=%s",tostring(self.option.Emission))) end else self:E(self.lid.."WARNING: Cannot switch Emission! Group is not alive") end return self end function OPSGROUP:GetEmission() return self.option.Emission or self.optionDefault.Emission end function OPSGROUP:SetDefaultInvisible(OnOffSwitch) if OnOffSwitch==nil then self.optionDefault.Invisible=true else self.optionDefault.Invisible=OnOffSwitch end return self end function OPSGROUP:SwitchInvisible(OnOffSwitch) if self:IsAlive()or self:IsInUtero()then if OnOffSwitch==nil then self.option.Invisible=self.optionDefault.Invisible else self.option.Invisible=OnOffSwitch end if self:IsInUtero()then self:T2(self.lid..string.format("Setting current INVISIBLE=%s when GROUP is SPAWNED",tostring(self.option.Invisible))) else self.group:SetCommandInvisible(self.option.Invisible) self:T(self.lid..string.format("Setting current INVISIBLE=%s",tostring(self.option.Invisible))) end else self:E(self.lid.."WARNING: Cannot switch Invisible! Group is not alive") end return self end function OPSGROUP:SetDefaultImmortal(OnOffSwitch) if OnOffSwitch==nil then self.optionDefault.Immortal=true else self.optionDefault.Immortal=OnOffSwitch end return self end function OPSGROUP:SwitchImmortal(OnOffSwitch) if self:IsAlive()or self:IsInUtero()then if OnOffSwitch==nil then self.option.Immortal=self.optionDefault.Immortal else self.option.Immortal=OnOffSwitch end if self:IsInUtero()then self:T2(self.lid..string.format("Setting current IMMORTAL=%s when GROUP is SPAWNED",tostring(self.option.Immortal))) else self.group:SetCommandImmortal(self.option.Immortal) self:T(self.lid..string.format("Setting current IMMORTAL=%s",tostring(self.option.Immortal))) end else self:E(self.lid.."WARNING: Cannot switch Immortal! Group is not alive") end return self end function OPSGROUP:SetDefaultTACAN(Channel,Morse,UnitName,Band,OffSwitch) self.tacanDefault={} self.tacanDefault.Channel=Channel or 74 self.tacanDefault.Morse=Morse or"XXX" self.tacanDefault.BeaconName=UnitName if self:IsFlightgroup()then Band=Band or"Y" else Band=Band or"X" end self.tacanDefault.Band=Band if OffSwitch then self.tacanDefault.On=false else self.tacanDefault.On=true end return self end function OPSGROUP:_SwitchTACAN(Tacan) if Tacan then self:SwitchTACAN(Tacan.Channel,Tacan.Morse,Tacan.BeaconName,Tacan.Band) else if self.tacanDefault.On then self:SwitchTACAN() else self:TurnOffTACAN() end end end function OPSGROUP:SwitchTACAN(Channel,Morse,UnitName,Band) if self:IsInUtero()then self:T(self.lid..string.format("Switching TACAN to DEFAULT when group is spawned")) self:SetDefaultTACAN(Channel,Morse,UnitName,Band) elseif self:IsAlive()then Channel=Channel or self.tacanDefault.Channel Morse=Morse or self.tacanDefault.Morse Band=Band or self.tacanDefault.Band UnitName=UnitName or self.tacanDefault.BeaconName local unit=self:GetUnit(1) if UnitName then if type(UnitName)=="number"then unit=self.group:GetUnit(UnitName) else unit=UNIT:FindByName(UnitName) end end if not unit then self:T(self.lid.."WARNING: Could not get TACAN unit. Trying first unit in the group") unit=self:GetUnit(1) end if unit and unit:IsAlive()then local UnitID=unit:GetID() local Type=BEACON.Type.TACAN local System=BEACON.System.TACAN if self:IsFlightgroup()then System=BEACON.System.TACAN_TANKER_Y end local Frequency=UTILS.TACANToFrequency(Channel,Band) unit:CommandActivateBeacon(Type,System,Frequency,UnitID,Channel,Band,true,Morse,true) self.tacan.Channel=Channel self.tacan.Morse=Morse self.tacan.Band=Band self.tacan.BeaconName=unit:GetName() self.tacan.BeaconUnit=unit self.tacan.On=true self:T(self.lid..string.format("Switching TACAN to Channel %d%s Morse %s on unit %s",self.tacan.Channel,self.tacan.Band,tostring(self.tacan.Morse),self.tacan.BeaconName)) else self:T(self.lid.."ERROR: Cound not set TACAN! Unit is not alive") end else self:T(self.lid.."ERROR: Cound not set TACAN! Group is not alive and not in utero any more") end return self end function OPSGROUP:TurnOffTACAN() if self.tacan.BeaconUnit and self.tacan.BeaconUnit:IsAlive()then self.tacan.BeaconUnit:CommandDeactivateBeacon() end self:T(self.lid..string.format("Switching TACAN OFF")) self.tacan.On=false end function OPSGROUP:GetTACAN() return self.tacan.Channel,self.tacan.Morse,self.tacan.Band,self.tacan.On,self.tacan.BeaconName end function OPSGROUP:GetBeaconTACAN() return self.tacan end function OPSGROUP:SetDefaultICLS(Channel,Morse,UnitName,OffSwitch) self.iclsDefault={} self.iclsDefault.Channel=Channel or 1 self.iclsDefault.Morse=Morse or"XXX" self.iclsDefault.BeaconName=UnitName if OffSwitch then self.iclsDefault.On=false else self.iclsDefault.On=true end return self end function OPSGROUP:_SwitchICLS(Icls) if Icls then self:SwitchICLS(Icls.Channel,Icls.Morse,Icls.BeaconName) else if self.iclsDefault.On then self:SwitchICLS() else self:TurnOffICLS() end end end function OPSGROUP:SwitchICLS(Channel,Morse,UnitName) if self:IsInUtero()then self:SetDefaultICLS(Channel,Morse,UnitName) self:T2(self.lid..string.format("Switching ICLS to Channel %d Morse %s on unit %s when GROUP is SPAWNED",self.iclsDefault.Channel,tostring(self.iclsDefault.Morse),tostring(self.iclsDefault.BeaconName))) elseif self:IsAlive()then Channel=Channel or self.iclsDefault.Channel Morse=Morse or self.iclsDefault.Morse local unit=self:GetUnit(1) if UnitName then if type(UnitName)=="number"then unit=self:GetUnit(UnitName) else unit=UNIT:FindByName(UnitName) end end if not unit then self:T(self.lid.."WARNING: Could not get ICLS unit. Trying first unit in the group") unit=self:GetUnit(1) end if unit and unit:IsAlive()then local UnitID=unit:GetID() unit:CommandActivateICLS(Channel,UnitID,Morse) self.icls.Channel=Channel self.icls.Morse=Morse self.icls.Band=nil self.icls.BeaconName=unit:GetName() self.icls.BeaconUnit=unit self.icls.On=true self:T(self.lid..string.format("Switching ICLS to Channel %d Morse %s on unit %s",self.icls.Channel,tostring(self.icls.Morse),self.icls.BeaconName)) else self:T(self.lid.."ERROR: Cound not set ICLS! Unit is not alive.") end end return self end function OPSGROUP:TurnOffICLS() if self.icls.BeaconUnit and self.icls.BeaconUnit:IsAlive()then self.icls.BeaconUnit:CommandDeactivateICLS() end self:T(self.lid..string.format("Switching ICLS OFF")) self.icls.On=false end function OPSGROUP:SetDefaultRadio(Frequency,Modulation,OffSwitch) self.radioDefault={} self.radioDefault.Freq=Frequency or 251 self.radioDefault.Modu=Modulation or radio.modulation.AM if OffSwitch then self.radioDefault.On=false else self.radioDefault.On=true end return self end function OPSGROUP:GetRadio() return self.radio.Freq,self.radio.Modu,self.radio.On end function OPSGROUP:SwitchRadio(Frequency,Modulation) if self:IsInUtero()then self:SetDefaultRadio(Frequency,Modulation) self:T2(self.lid..string.format("Switching radio to frequency %.3f MHz %s when GROUP is SPAWNED",self.radioDefault.Freq,UTILS.GetModulationName(self.radioDefault.Modu))) elseif self:IsAlive()then Frequency=Frequency or self.radioDefault.Freq Modulation=Modulation or self.radioDefault.Modu if self:IsFlightgroup()and not self.radio.On then self.group:SetOption(AI.Option.Air.id.SILENCE,false) end self.group:CommandSetFrequency(Frequency,Modulation) self.radio.Freq=Frequency self.radio.Modu=Modulation self.radio.On=true self:T(self.lid..string.format("Switching radio to frequency %.3f MHz %s",self.radio.Freq,UTILS.GetModulationName(self.radio.Modu))) else self:T(self.lid.."ERROR: Cound not set Radio! Group is not alive or not in utero any more") end return self end function OPSGROUP:TurnOffRadio() if self:IsAlive()then if self:IsFlightgroup()then self.group:SetOption(AI.Option.Air.id.SILENCE,true) self.radio.On=false self:T(self.lid..string.format("Switching radio OFF")) else self:T(self.lid.."ERROR: Radio can only be turned off for aircraft!") end end return self end function OPSGROUP:SetDefaultFormation(Formation) self.optionDefault.Formation=Formation return self end function OPSGROUP:SwitchFormation(Formation) if self:IsAlive()then Formation=Formation or self.optionDefault.Formation if self:IsFlightgroup()then self.group:SetOption(AI.Option.Air.id.FORMATION,Formation) elseif self.isArmygroup then else self:T(self.lid.."ERROR: Formation can only be set for aircraft or ground units!") return self end self.option.Formation=Formation self:T(self.lid..string.format("Switching formation to %s",tostring(self.option.Formation))) end return self end function OPSGROUP:SetDefaultCallsign(CallsignName,CallsignNumber) self:T(self.lid..string.format("Setting Default callsign %s-%s",tostring(CallsignName),tostring(CallsignNumber))) self.callsignDefault={} self.callsignDefault.NumberSquad=CallsignName self.callsignDefault.NumberGroup=CallsignNumber or 1 self.callsignDefault.NameSquad=UTILS.GetCallsignName(self.callsign.NumberSquad) return self end function OPSGROUP:SwitchCallsign(CallsignName,CallsignNumber) if self:IsInUtero()then self:SetDefaultCallsign(CallsignName,CallsignNumber) elseif self:IsAlive()then CallsignName=CallsignName or self.callsignDefault.NumberSquad CallsignNumber=CallsignNumber or self.callsignDefault.NumberGroup self.callsign.NumberSquad=CallsignName self.callsign.NumberGroup=CallsignNumber self:T(self.lid..string.format("Switching callsign to %d-%d",self.callsign.NumberSquad,self.callsign.NumberGroup)) self.group:CommandSetCallsign(self.callsign.NumberSquad,self.callsign.NumberGroup) self.callsignName=UTILS.GetCallsignName(self.callsign.NumberSquad).."-"..self.callsign.NumberGroup self.callsign.NameSquad=UTILS.GetCallsignName(self.callsign.NumberSquad) for _,_element in pairs(self.elements)do local element=_element if element.status~=OPSGROUP.ElementStatus.DEAD then element.callsign=element.unit:GetCallsign() end end else self:T(self.lid.."ERROR: Group is not alive and not in utero! Cannot switch callsign") end return self end function OPSGROUP:GetCallsignName(ShortCallsign,Keepnumber,CallsignTranslations) local element=self:GetElementAlive() if element then self:T2(self.lid..string.format("Callsign %s",tostring(element.callsign))) local name=element.callsign or"Ghostrider11" name=name:gsub("-","") if self.group:IsPlayer()or CallsignTranslations then name=self.group:GetCustomCallSign(ShortCallsign,Keepnumber,CallsignTranslations) end return name end return"Ghostrider11" end function OPSGROUP:_UpdatePosition() if self:IsExist()then self.positionLast=self.position or self:GetVec3() self.headingLast=self.heading or self:GetHeading() self.orientXLast=self.orientX or self:GetOrientationX() self.velocityLast=self.velocity or self.group:GetVelocityMPS() self.position=self:GetVec3() self.heading=self:GetHeading() self.orientX=self:GetOrientationX() self.velocity=self:GetVelocity() for _,_element in pairs(self.elements)do local element=_element element.vec3=self:GetVec3(element.name) end local Tnow=timer.getTime() self.dTpositionUpdate=self.TpositionUpdate and Tnow-self.TpositionUpdate or 0 self.TpositionUpdate=Tnow if not self.traveldist then self.traveldist=0 end self.travelds=UTILS.VecNorm(UTILS.VecSubstract(self.position,self.positionLast)) self.traveldist=self.traveldist+self.travelds end return self end function OPSGROUP:_AllSameStatus(status) for _,_element in pairs(self.elements)do local element=_element if element.status==OPSGROUP.ElementStatus.DEAD then elseif element.status~=status then return false end end return true end function OPSGROUP:_AllSimilarStatus(status) if status==OPSGROUP.ElementStatus.DEAD then for _,_element in pairs(self.elements)do local element=_element if element.status~=OPSGROUP.ElementStatus.DEAD then return false end end return true end for _,_element in pairs(self.elements)do local element=_element self:T2(self.lid..string.format("Status=%s, element %s status=%s",status,element.name,element.status)) if element.status~=OPSGROUP.ElementStatus.DEAD then if status==OPSGROUP.ElementStatus.INUTERO then if element.status~=status then return false end elseif status==OPSGROUP.ElementStatus.SPAWNED then if element.status~=status and element.status==OPSGROUP.ElementStatus.INUTERO then return false end elseif status==OPSGROUP.ElementStatus.PARKING then if element.status~=status or (element.status==OPSGROUP.ElementStatus.INUTERO or element.status==OPSGROUP.ElementStatus.SPAWNED)then return false end elseif status==OPSGROUP.ElementStatus.ENGINEON then if element.status~=status and (element.status==OPSGROUP.ElementStatus.INUTERO or element.status==OPSGROUP.ElementStatus.SPAWNED or element.status==OPSGROUP.ElementStatus.PARKING)then return false end elseif status==OPSGROUP.ElementStatus.TAXIING then if element.status~=status and (element.status==OPSGROUP.ElementStatus.INUTERO or element.status==OPSGROUP.ElementStatus.SPAWNED or element.status==OPSGROUP.ElementStatus.PARKING or element.status==OPSGROUP.ElementStatus.ENGINEON)then return false end elseif status==OPSGROUP.ElementStatus.TAKEOFF then if element.status~=status and (element.status==OPSGROUP.ElementStatus.INUTERO or element.status==OPSGROUP.ElementStatus.SPAWNED or element.status==OPSGROUP.ElementStatus.PARKING or element.status==OPSGROUP.ElementStatus.ENGINEON or element.status==OPSGROUP.ElementStatus.TAXIING)then return false end elseif status==OPSGROUP.ElementStatus.AIRBORNE then if element.status~=status and (element.status==OPSGROUP.ElementStatus.INUTERO or element.status==OPSGROUP.ElementStatus.SPAWNED or element.status==OPSGROUP.ElementStatus.PARKING or element.status==OPSGROUP.ElementStatus.ENGINEON or element.status==OPSGROUP.ElementStatus.TAXIING or element.status==OPSGROUP.ElementStatus.TAKEOFF)then return false end elseif status==OPSGROUP.ElementStatus.LANDED then if element.status~=status and (element.status==OPSGROUP.ElementStatus.AIRBORNE or element.status==OPSGROUP.ElementStatus.LANDING)then return false end elseif status==OPSGROUP.ElementStatus.ARRIVED then if element.status~=status and (element.status==OPSGROUP.ElementStatus.AIRBORNE or element.status==OPSGROUP.ElementStatus.LANDING or element.status==OPSGROUP.ElementStatus.LANDED)then return false end end else end end self:T2(self.lid..string.format("All %d elements have similar status %s ==> returning TRUE",#self.elements,status)) return true end function OPSGROUP:_UpdateStatus(element,newstatus,airbase) local oldstatus=element.status element.status=newstatus self:T3(self.lid..string.format("UpdateStatus element=%s: %s --> %s",element.name,oldstatus,newstatus)) for _,_element in pairs(self.elements)do local Element=_element self:T3(self.lid..string.format("Element %s: %s",Element.name,Element.status)) end if newstatus==OPSGROUP.ElementStatus.INUTERO then if self:_AllSimilarStatus(newstatus)then self:InUtero() end elseif newstatus==OPSGROUP.ElementStatus.SPAWNED then if self:_AllSimilarStatus(newstatus)then self:Spawned() end elseif newstatus==OPSGROUP.ElementStatus.PARKING then if self:_AllSimilarStatus(newstatus)then self:Parking() end elseif newstatus==OPSGROUP.ElementStatus.ENGINEON then elseif newstatus==OPSGROUP.ElementStatus.TAXIING then if self:_AllSimilarStatus(newstatus)then self:Taxiing() end elseif newstatus==OPSGROUP.ElementStatus.TAKEOFF then if self:_AllSimilarStatus(newstatus)then self:Takeoff(airbase) end elseif newstatus==OPSGROUP.ElementStatus.AIRBORNE then if self:_AllSimilarStatus(newstatus)then self:Airborne() end elseif newstatus==OPSGROUP.ElementStatus.LANDED then if self:_AllSimilarStatus(newstatus)then if self:IsLandingAt()then self:LandedAt() else self:Landed(airbase) end end elseif newstatus==OPSGROUP.ElementStatus.ARRIVED then if self:_AllSimilarStatus(newstatus)then if self:IsLanded()then self:Arrived() elseif self:IsAirborne()then self:Landed() self:Arrived() end end elseif newstatus==OPSGROUP.ElementStatus.DEAD then if self:_AllSimilarStatus(newstatus)then self:Dead() end end end function OPSGROUP:_SetElementStatusAll(status) for _,_element in pairs(self.elements)do local element=_element if element.status~=OPSGROUP.ElementStatus.DEAD then element.status=status end end end function OPSGROUP:GetElementByName(unitname) if unitname and type(unitname)=="string"then for _,_element in pairs(self.elements)do local element=_element if element.name==unitname then return element end end end return nil end function OPSGROUP:GetElementZoneBoundingBox(UnitName) local element=self:GetElementByName(UnitName) if element and element.status~=OPSGROUP.ElementStatus.DEAD then element.zoneBoundingbox=element.zoneBoundingbox or ZONE_POLYGON_BASE:New(element.name.." Zone Bounding Box",{}) local l=element.length local w=element.width local X=self:GetOrientationX(element.name) local heading=math.deg(math.atan2(X.z,X.x)) self:T(self.lid..string.format("Element %s bouding box: l=%d w=%d heading=%d",element.name,l,w,heading)) local b={} b[1]={x=l/2,y=-w/2} b[2]={x=l/2,y=w/2} b[3]={x=-l/2,y=w/2} b[4]={x=-l/2,y=-w/2} for i,p in pairs(b)do b[i]=UTILS.Vec2Rotate2D(p,heading) end local vec2=self:GetVec2(element.name) local d=UTILS.Vec2Norm(vec2) local h=UTILS.Vec2Hdg(vec2) for i,p in pairs(b)do b[i]=UTILS.Vec2Translate(p,d,h) end element.zoneBoundingbox:UpdateFromVec2(b) return element.zoneBoundingbox end return nil end function OPSGROUP:GetElementZoneLoad(UnitName) local element=self:GetElementByName(UnitName) if element and element.status~=OPSGROUP.ElementStatus.DEAD then element.zoneLoad=element.zoneLoad or ZONE_POLYGON_BASE:New(element.name.." Zone Load",{}) self:_GetElementZoneLoader(element,element.zoneLoad,self.carrierLoader) return element.zoneLoad end return nil end function OPSGROUP:GetElementZoneUnload(UnitName) local element=self:GetElementByName(UnitName) if element and element.status~=OPSGROUP.ElementStatus.DEAD then element.zoneUnload=element.zoneUnload or ZONE_POLYGON_BASE:New(element.name.." Zone Unload",{}) self:_GetElementZoneLoader(element,element.zoneUnload,self.carrierUnloader) return element.zoneUnload end return nil end function OPSGROUP:_GetElementZoneLoader(Element,Zone,Loader) if Element.status~=OPSGROUP.ElementStatus.DEAD then local l=Element.length local w=Element.width local X=self:GetOrientationX(Element.name) local heading=math.deg(math.atan2(X.z,X.x)) local b={} if Loader.type:lower()=="front"then table.insert(b,{x=l/2,y=-Loader.width/2}) table.insert(b,{x=l/2+Loader.length,y=-Loader.width/2}) table.insert(b,{x=l/2+Loader.length,y=Loader.width/2}) table.insert(b,{x=l/2,y=Loader.width/2}) elseif Loader.type:lower()=="back"then table.insert(b,{x=-l/2,y=-Loader.width/2}) table.insert(b,{x=-l/2-Loader.length,y=-Loader.width/2}) table.insert(b,{x=-l/2-Loader.length,y=Loader.width/2}) table.insert(b,{x=-l/2,y=Loader.width/2}) elseif Loader.type:lower()=="left"then table.insert(b,{x=Loader.length/2,y=-w/2}) table.insert(b,{x=Loader.length/2,y=-w/2-Loader.width}) table.insert(b,{x=-Loader.length/2,y=-w/2-Loader.width}) table.insert(b,{x=-Loader.length/2,y=-w/2}) elseif Loader.type:lower()=="right"then table.insert(b,{x=Loader.length/2,y=w/2}) table.insert(b,{x=Loader.length/2,y=w/2+Loader.width}) table.insert(b,{x=-Loader.length/2,y=w/2+Loader.width}) table.insert(b,{x=-Loader.length/2,y=w/2}) else b[1]={x=l/2,y=-w/2} b[2]={x=l/2,y=w/2} b[3]={x=-l/2,y=w/2} b[4]={x=-l/2,y=-w/2} table.insert(b,{x=b[1].x+Loader.length,y=b[1].y-Loader.width}) table.insert(b,{x=b[2].x+Loader.length,y=b[2].y+Loader.width}) table.insert(b,{x=b[3].x-Loader.length,y=b[3].y+Loader.width}) table.insert(b,{x=b[4].x-Loader.length,y=b[4].y-Loader.width}) end for i,p in pairs(b)do b[i]=UTILS.Vec2Rotate2D(p,heading) end local vec2=self:GetVec2(Element.name) local d=UTILS.Vec2Norm(vec2) local h=UTILS.Vec2Hdg(vec2) for i,p in pairs(b)do b[i]=UTILS.Vec2Translate(p,d,h) end Zone:UpdateFromVec2(b) return Zone end return nil end function OPSGROUP:GetElementAlive() for _,_element in pairs(self.elements)do local element=_element if element.status~=OPSGROUP.ElementStatus.DEAD then if element.unit and element.unit:IsAlive()then return element end end end return nil end function OPSGROUP:GetNelements(status) local n=0 for _,_element in pairs(self.elements)do local element=_element if element.status~=OPSGROUP.ElementStatus.DEAD then if element.unit and element.unit:IsAlive()then if status==nil or element.status==status then n=n+1 end end end end return n end function OPSGROUP:GetAmmoElement(element) return self:GetAmmoUnit(element.unit) end function OPSGROUP:GetAmmoTot() local units=self.group:GetUnits() local Ammo={} Ammo.Total=0 Ammo.Shells=0 Ammo.Guns=0 Ammo.Cannons=0 Ammo.Rockets=0 Ammo.Bombs=0 Ammo.Torpedos=0 Ammo.Missiles=0 Ammo.MissilesAA=0 Ammo.MissilesAG=0 Ammo.MissilesAS=0 Ammo.MissilesCR=0 Ammo.MissilesSA=0 for _,_unit in pairs(units or{})do local unit=_unit if unit and unit:IsExist()then local ammo=self:GetAmmoUnit(unit) Ammo.Total=Ammo.Total+ammo.Total Ammo.Shells=Ammo.Shells+ammo.Shells Ammo.Guns=Ammo.Guns+ammo.Guns Ammo.Cannons=Ammo.Cannons+ammo.Cannons Ammo.Rockets=Ammo.Rockets+ammo.Rockets Ammo.Bombs=Ammo.Bombs+ammo.Bombs Ammo.Torpedos=Ammo.Torpedos+ammo.Torpedos Ammo.Missiles=Ammo.Missiles+ammo.Missiles Ammo.MissilesAA=Ammo.MissilesAA+ammo.MissilesAA Ammo.MissilesAG=Ammo.MissilesAG+ammo.MissilesAG Ammo.MissilesAS=Ammo.MissilesAS+ammo.MissilesAS Ammo.MissilesCR=Ammo.MissilesCR+ammo.MissilesCR Ammo.MissilesSA=Ammo.MissilesSA+ammo.MissilesSA end end return Ammo end function OPSGROUP:GetAmmoUnit(unit,display) if display==nil then display=false end local nammo=0 local nshells=0 local nguns=0 local ncannons=0 local nrockets=0 local nmissiles=0 local nmissilesAA=0 local nmissilesAG=0 local nmissilesAS=0 local nmissilesSA=0 local nmissilesBM=0 local nmissilesCR=0 local ntorps=0 local nbombs=0 unit=unit or self.group:GetUnit(1) if unit and unit:IsExist()then local text=string.format("OPSGROUP group %s - unit %s:\n",self.groupname,unit:GetName()) local ammotable=unit:GetAmmo() if ammotable then local weapons=#ammotable for w=1,weapons do local Nammo=ammotable[w]["count"] local rmin=ammotable[w]["desc"]["rangeMin"]or 0 local rmax=ammotable[w]["desc"]["rangeMaxAltMin"]or 0 local Tammo=ammotable[w]["desc"]["typeName"] local _weaponString=UTILS.Split(Tammo,"%.") local _weaponName=_weaponString[#_weaponString] local Category=ammotable[w].desc.category local MissileCategory=nil if Category==Weapon.Category.MISSILE then MissileCategory=ammotable[w].desc.missileCategory end if Category==Weapon.Category.SHELL then nshells=nshells+Nammo if ammotable[w]["desc"]["warhead"]and ammotable[w]["desc"]["warhead"]["caliber"]then local caliber=ammotable[w]["desc"]["warhead"]["caliber"] if caliber<25 then nguns=nguns+Nammo else ncannons=ncannons+Nammo end end text=text..string.format("- %d shells of type %s, range=%d - %d meters\n",Nammo,_weaponName,rmin,rmax) elseif Category==Weapon.Category.ROCKET then nrockets=nrockets+Nammo text=text..string.format("- %d rockets of type %s, \n",Nammo,_weaponName,rmin,rmax) elseif Category==Weapon.Category.BOMB then nbombs=nbombs+Nammo text=text..string.format("- %d bombs of type %s\n",Nammo,_weaponName) elseif Category==Weapon.Category.MISSILE then if MissileCategory==Weapon.MissileCategory.AAM then nmissiles=nmissiles+Nammo nmissilesAA=nmissilesAA+Nammo elseif MissileCategory==Weapon.MissileCategory.SAM then nmissiles=nmissiles+Nammo nmissilesSA=nmissilesSA+Nammo elseif MissileCategory==Weapon.MissileCategory.ANTI_SHIP then nmissiles=nmissiles+Nammo nmissilesAS=nmissilesAS+Nammo elseif MissileCategory==Weapon.MissileCategory.BM then nmissiles=nmissiles+Nammo nmissilesBM=nmissilesBM+Nammo elseif MissileCategory==Weapon.MissileCategory.CRUISE then nmissiles=nmissiles+Nammo nmissilesCR=nmissilesCR+Nammo elseif MissileCategory==Weapon.MissileCategory.OTHER then nmissiles=nmissiles+Nammo nmissilesAG=nmissilesAG+Nammo end text=text..string.format("- %d %s missiles of type %s, range=%d - %d meters\n",Nammo,self:_MissileCategoryName(MissileCategory),_weaponName,rmin,rmax) elseif Category==Weapon.Category.TORPEDO then ntorps=ntorps+Nammo text=text..string.format("- %d torpedos of type %s\n",Nammo,_weaponName) else text=text..string.format("- %d unknown ammo of type %s (category=%d, missile category=%s)\n",Nammo,Tammo,Category,tostring(MissileCategory)) end end end if display then self:I(self.lid..text) else self:T3(self.lid..text) end end nammo=nshells+nrockets+nmissiles+nbombs+ntorps local ammo={} ammo.Total=nammo ammo.Shells=nshells ammo.Guns=nguns ammo.Cannons=ncannons ammo.Rockets=nrockets ammo.Bombs=nbombs ammo.Torpedos=ntorps ammo.Missiles=nmissiles ammo.MissilesAA=nmissilesAA ammo.MissilesAG=nmissilesAG ammo.MissilesAS=nmissilesAS ammo.MissilesCR=nmissilesCR ammo.MissilesBM=nmissilesBM ammo.MissilesSA=nmissilesSA return ammo end function OPSGROUP:_MissileCategoryName(categorynumber) local cat="unknown" if categorynumber==Weapon.MissileCategory.AAM then cat="air-to-air" elseif categorynumber==Weapon.MissileCategory.SAM then cat="surface-to-air" elseif categorynumber==Weapon.MissileCategory.BM then cat="ballistic" elseif categorynumber==Weapon.MissileCategory.ANTI_SHIP then cat="anti-ship" elseif categorynumber==Weapon.MissileCategory.CRUISE then cat="cruise" elseif categorynumber==Weapon.MissileCategory.OTHER then cat="other" end return cat end function OPSGROUP:_PassedFinalWaypoint(final,comment) self:T(self.lid..string.format("Passed final waypoint=%s [from %s]: comment \"%s\"",tostring(final),tostring(self.passedfinalwp),tostring(comment))) if final==true and not self.passedfinalwp then self:PassedFinalWaypoint() end self.passedfinalwp=final end function OPSGROUP:_CoordinateFromObject(Object) if Object then if Object:IsInstanceOf("COORDINATE")then return Object else if Object:IsInstanceOf("POSITIONABLE")or Object:IsInstanceOf("ZONE_BASE")then self:T(self.lid.."WARNING: Coordinate is not a COORDINATE but a POSITIONABLE or ZONE. Trying to get coordinate") local coord=Object:GetCoordinate() return coord else self:T(self.lid.."ERROR: Coordinate is neither a COORDINATE nor any POSITIONABLE or ZONE!") end end else self:T(self.lid.."ERROR: Object passed is nil!") end return nil end function OPSGROUP:_IsElement(unitname) for _,_element in pairs(self.elements)do local element=_element if element.name==unitname then return true end end return false end function OPSGROUP:CountElements(States) if States then if type(States)=="string"then States={States} end else States=OPSGROUP.ElementStatus end local IncludeDeads=true local N=0 for _,_element in pairs(self.elements)do local element=_element if element and(IncludeDeads or element.status~=OPSGROUP.ElementStatus.DEAD)then for _,state in pairs(States)do if element.status==state then N=N+1 break end end end end return N end function OPSGROUP:_AddElementByName(unitname) local unit=UNIT:FindByName(unitname) if unit then local element=self:GetElementByName(unitname) if element then else element={} element.status=OPSGROUP.ElementStatus.INUTERO table.insert(self.elements,element) end element.name=unitname element.unit=unit element.DCSunit=Unit.getByName(unitname) element.gid=element.DCSunit:getNumber() element.uid=element.DCSunit:getID() element.controller=element.DCSunit:getController() element.Nhit=0 element.opsgroup=self local unittemplate=unit:GetTemplate() if unittemplate==nil then if element.DCSunit:getPlayerName()then element.skill="Client" end else element.skill=unittemplate~=nil and unittemplate.skill or"Unknown" end if element.skill=="Client"or element.skill=="Player"then element.ai=false element.client=CLIENT:FindByName(unitname) element.playerName=element.DCSunit:getPlayerName() else element.ai=true end element.descriptors=unit:GetDesc() element.category=unit:GetUnitCategory() element.categoryname=unit:GetCategoryName() element.typename=unit:GetTypeName() element.ammo0=self:GetAmmoUnit(unit,false) element.life=unit:GetLife() element.life0=math.max(unit:GetLife0(),element.life) element.size,element.length,element.height,element.width=unit:GetObjectSize() element.weightEmpty=element.descriptors.massEmpty or 666 if self.isArmygroup then element.weightMaxTotal=element.weightEmpty+10*95 elseif self.isNavygroup then element.weightMaxTotal=element.weightEmpty+10*1000 else element.weightMaxTotal=element.descriptors.massMax or element.weightEmpty+8*95 end unit:SetCargoBayWeightLimit() element.weightMaxCargo=unit.__.CargoBayWeightLimit if element.cargoBay then element.weightCargo=self:GetWeightCargo(element.name,false) else element.cargoBay={} element.weightCargo=0 end element.weight=element.weightEmpty+element.weightCargo element.callsign=element.unit:GetCallsign() element.fuelmass=element.fuelmass0 or 99999 element.fuelrel=element.unit:GetFuel()or 1 if self.isFlightgroup and unittemplate then element.modex=unittemplate.onboard_num element.payload=unittemplate.payload element.pylons=unittemplate.payload and unittemplate.payload.pylons or nil element.fuelmass0=unittemplate.payload and unittemplate.payload.fuel or 0 else element.callsign="Peter-1-1" element.modex="000" element.payload={} element.pylons={} end local text=string.format("Adding element %s: status=%s, skill=%s, life=%.1f/%.1f category=%s (%d), type=%s, size=%.1f (L=%.1f H=%.1f W=%.1f), weight=%.1f/%.1f (cargo=%.1f/%.1f)", element.name,element.status,element.skill,element.life,element.life0,element.categoryname,element.category,element.typename, element.size,element.length,element.height,element.width,element.weight,element.weightMaxTotal,element.weightCargo,element.weightMaxCargo) self:T(self.lid..text) if unit:IsAlive()and element.status~=OPSGROUP.ElementStatus.SPAWNED then self:__ElementSpawned(0.05,element) end return element end return nil end function OPSGROUP:_SetTemplate(Template) self.template=Template or UTILS.DeepCopy(_DATABASE:GetGroupTemplate(self.groupname)) self:T3(self.lid.."Setting group template") return self end function OPSGROUP:_GetTemplate(Copy) if self.template then if Copy then local template=UTILS.DeepCopy(self.template) return template else return self.template end else self:T(self.lid..string.format("ERROR: No template was set yet!")) end return nil end function OPSGROUP:ClearWaypoints(IndexMin,IndexMax) IndexMin=IndexMin or 1 IndexMax=IndexMax or#self.waypoints for i=IndexMax,IndexMin,-1 do table.remove(self.waypoints,i) end end function OPSGROUP:_GetDetectedTarget() local targetgroup=nil local targetdist=math.huge for _,_group in pairs(self.detectedgroups:GetSet())do local group=_group if group and group:IsAlive()then local targetVec3=group:GetVec3() local distance=UTILS.VecDist3D(self.position,targetVec3) if distance<=self.engagedetectedRmax and distance=1 then local text=string.format("Added cargo groups:") local Weight=0 for _,_cargo in pairs(self:GetCargos())do local cargo=_cargo local weight=cargo.opsgroup:GetWeightTotal() Weight=Weight+weight text=text..string.format("\n- %s [%s] weight=%.1f kg",cargo.opsgroup:GetName(),cargo.opsgroup:GetState(),weight) end text=text..string.format("\nTOTAL: Ncargo=%d, Weight=%.1f kg",self.Ncargo,Weight) self:I(self.lid..text) end return self end function OPSTRANSPORT:AddCargoStorage(StorageFrom,StorageTo,CargoType,CargoAmount,CargoWeight,TransportZoneCombo) TransportZoneCombo=TransportZoneCombo or self.tzcDefault local cargo=self:_CreateCargoStorage(StorageFrom,StorageTo,CargoType,CargoAmount,CargoWeight,TransportZoneCombo) if cargo then self.Ncargo=self.Ncargo+1 table.insert(TransportZoneCombo.Cargos,cargo) end end function OPSTRANSPORT:SetPickupZone(PickupZone,TransportZoneCombo) TransportZoneCombo=TransportZoneCombo or self.tzcDefault TransportZoneCombo.PickupZone=PickupZone if PickupZone and PickupZone:IsInstanceOf("ZONE_AIRBASE")then TransportZoneCombo.PickupAirbase=PickupZone._.ZoneAirbase end return self end function OPSTRANSPORT:GetPickupZone(TransportZoneCombo) TransportZoneCombo=TransportZoneCombo or self.tzcDefault return TransportZoneCombo.PickupZone end function OPSTRANSPORT:SetDeployZone(DeployZone,TransportZoneCombo) TransportZoneCombo=TransportZoneCombo or self.tzcDefault TransportZoneCombo.DeployZone=DeployZone if DeployZone and DeployZone:IsInstanceOf("ZONE_AIRBASE")then TransportZoneCombo.DeployAirbase=DeployZone._.ZoneAirbase end return self end function OPSTRANSPORT:GetDeployZone(TransportZoneCombo) TransportZoneCombo=TransportZoneCombo or self.tzcDefault return TransportZoneCombo.DeployZone end function OPSTRANSPORT:SetEmbarkZone(EmbarkZone,TransportZoneCombo) TransportZoneCombo=TransportZoneCombo or self.tzcDefault TransportZoneCombo.EmbarkZone=EmbarkZone or TransportZoneCombo.PickupZone return self end function OPSTRANSPORT:GetEmbarkZone(TransportZoneCombo) TransportZoneCombo=TransportZoneCombo or self.tzcDefault return TransportZoneCombo.EmbarkZone end function OPSTRANSPORT:SetDisembarkZone(DisembarkZone,TransportZoneCombo) TransportZoneCombo=TransportZoneCombo or self.tzcDefault TransportZoneCombo.DisembarkZone=DisembarkZone return self end function OPSTRANSPORT:GetDisembarkZone(TransportZoneCombo) TransportZoneCombo=TransportZoneCombo or self.tzcDefault return TransportZoneCombo.DisembarkZone end function OPSTRANSPORT:SetDisembarkActivation(Active,TransportZoneCombo) TransportZoneCombo=TransportZoneCombo or self.tzcDefault if Active==true or Active==nil then TransportZoneCombo.disembarkActivation=true else TransportZoneCombo.disembarkActivation=false end return self end function OPSTRANSPORT:GetDisembarkActivation(TransportZoneCombo) TransportZoneCombo=TransportZoneCombo or self.tzcDefault return TransportZoneCombo.disembarkActivation end function OPSTRANSPORT:SetDisembarkCarriers(Carriers,TransportZoneCombo) self:T(self.lid.."Setting transfer carriers!") TransportZoneCombo=TransportZoneCombo or self.tzcDefault TransportZoneCombo.disembarkToCarriers=true self:_AddDisembarkCarriers(Carriers,TransportZoneCombo.DisembarkCarriers) return self end function OPSTRANSPORT:_AddDisembarkCarriers(Carriers,Table) if Carriers:IsInstanceOf("GROUP")or Carriers:IsInstanceOf("OPSGROUP")then local carrier=self:_GetOpsGroupFromObject(Carriers) if carrier then table.insert(Table,carrier) end elseif Carriers:IsInstanceOf("SET_GROUP")or Carriers:IsInstanceOf("SET_OPSGROUP")then for _,object in pairs(Carriers:GetSet())do local carrier=self:_GetOpsGroupFromObject(object) if carrier then table.insert(Table,carrier) end end else self:E(self.lid.."ERROR: Carriers must be a GROUP, OPSGROUP, SET_GROUP or SET_OPSGROUP object!") end end function OPSTRANSPORT:GetDisembarkCarriers(TransportZoneCombo) TransportZoneCombo=TransportZoneCombo or self.tzcDefault return TransportZoneCombo.DisembarkCarriers end function OPSTRANSPORT:SetDisembarkInUtero(InUtero,TransportZoneCombo) TransportZoneCombo=TransportZoneCombo or self.tzcDefault if InUtero==true or InUtero==nil then TransportZoneCombo.disembarkInUtero=true else TransportZoneCombo.disembarkInUtero=false end return self end function OPSTRANSPORT:GetDisembarkInUtero(TransportZoneCombo) TransportZoneCombo=TransportZoneCombo or self.tzcDefault return TransportZoneCombo.disembarkInUtero end function OPSTRANSPORT:SetFormationPickup(Formation,TransportZoneCombo) TransportZoneCombo=TransportZoneCombo or self.tzcDefault TransportZoneCombo.PickupFormation=Formation return self end function OPSTRANSPORT:_GetFormationDefault(OpsGroup) if OpsGroup.isArmygroup then return self.formationArmy elseif OpsGroup.isFlightgroup then if OpsGroup.isHelo then return self.formationHelo else return self.formationPlane end else return ENUMS.Formation.Vehicle.OffRoad end return nil end function OPSTRANSPORT:_GetFormationPickup(TransportZoneCombo,OpsGroup) TransportZoneCombo=TransportZoneCombo or self.tzcDefault local formation=TransportZoneCombo.PickupFormation or self:_GetFormationDefault(OpsGroup) return formation end function OPSTRANSPORT:SetFormationTransport(Formation,TransportZoneCombo) TransportZoneCombo=TransportZoneCombo or self.tzcDefault TransportZoneCombo.TransportFormation=Formation return self end function OPSTRANSPORT:_GetFormationTransport(TransportZoneCombo,OpsGroup) TransportZoneCombo=TransportZoneCombo or self.tzcDefault local formation=TransportZoneCombo.TransportFormation or self:_GetFormationDefault(OpsGroup) return formation end function OPSTRANSPORT:SetRequiredCargos(Cargos,TransportZoneCombo) self:T(self.lid.."Setting required cargos!") TransportZoneCombo=TransportZoneCombo or self.tzcDefault TransportZoneCombo.RequiredCargos=TransportZoneCombo.RequiredCargos or{} if Cargos:IsInstanceOf("GROUP")or Cargos:IsInstanceOf("OPSGROUP")then local cargo=self:_GetOpsGroupFromObject(Cargos) if cargo then table.insert(TransportZoneCombo.RequiredCargos,cargo) end elseif Cargos:IsInstanceOf("SET_GROUP")or Cargos:IsInstanceOf("SET_OPSGROUP")then for _,object in pairs(Cargos:GetSet())do local cargo=self:_GetOpsGroupFromObject(object) if cargo then table.insert(TransportZoneCombo.RequiredCargos,cargo) end end else self:E(self.lid.."ERROR: Required Cargos must be a GROUP, OPSGROUP, SET_GROUP or SET_OPSGROUP object!") end return self end function OPSTRANSPORT:GetRequiredCargos(TransportZoneCombo) TransportZoneCombo=TransportZoneCombo or self.tzcDefault return TransportZoneCombo.RequiredCargos end function OPSTRANSPORT:SetRequiredCarriers(NcarriersMin,NcarriersMax) self.nCarriersMin=NcarriersMin or 1 self.nCarriersMax=NcarriersMax or self.nCarriersMin if self.nCarriersMax0 then self:ScheduleOnce(Delay,OPSTRANSPORT._DelCarrier,self,CarrierGroup) else if self:IsCarrier(CarrierGroup)then for i=#self.carriers,1,-1 do local carrier=self.carriers[i] if carrier.groupname==CarrierGroup.groupname then self:T(self.lid..string.format("Removing carrier %s",CarrierGroup.groupname)) table.remove(self.carriers,i) end end end end return self end function OPSTRANSPORT:_GetCarrierNames() local names={} for _,_carrier in pairs(self.carriers)do local carrier=_carrier if carrier:IsAlive()~=nil then table.insert(names,carrier.groupname) end end return names end function OPSTRANSPORT:GetCargoOpsGroups(Delivered,Carrier,TransportZoneCombo) local cargos=self:GetCargos(TransportZoneCombo,Carrier,Delivered) local opsgroups={} for _,_cargo in pairs(cargos)do local cargo=_cargo if cargo.type=="OPSGROUP"then if cargo.opsgroup and not(cargo.opsgroup:IsDead()or cargo.opsgroup:IsStopped())then table.insert(opsgroups,cargo.opsgroup) end end end return opsgroups end function OPSTRANSPORT:GetCargoStorages(Delivered,Carrier,TransportZoneCombo) local cargos=self:GetCargos(TransportZoneCombo,Carrier,Delivered) local opsgroups={} for _,_cargo in pairs(cargos)do local cargo=_cargo if cargo.type=="STORAGE"then table.insert(opsgroups,cargo.storage) end end return opsgroups end function OPSTRANSPORT:GetCarriers() return self.carriers end function OPSTRANSPORT:GetCargos(TransportZoneCombo,Carrier,Delivered) local tczs=self.tzCombos if TransportZoneCombo then tczs={TransportZoneCombo} end local cargos={} for _,_tcz in pairs(tczs)do local tcz=_tcz for _,_cargo in pairs(tcz.Cargos)do local cargo=_cargo if Delivered==nil or cargo.delivered==Delivered then if Carrier==nil or Carrier:CanCargo(cargo)then table.insert(cargos,cargo) end end end end return cargos end function OPSTRANSPORT:GetCargoTotalWeight(Cargo,IncludeReserved) local weight=0 if Cargo.type==OPSTRANSPORT.CargoType.OPSGROUP then weight=Cargo.opsgroup:GetWeightTotal(nil,IncludeReserved) else if type(Cargo.storage.cargoType)=="number"then if IncludeReserved then return Cargo.storage.cargoAmount+Cargo.storage.cargoReserved else return Cargo.storage.cargoAmount end else if IncludeReserved then return Cargo.storage.cargoAmount*100 else return(Cargo.storage.cargoAmount+Cargo.storage.cargoReserved)*100 end end end return weight end function OPSTRANSPORT:SetTime(ClockStart,ClockStop) local Tnow=timer.getAbsTime() local Tstart=Tnow+5 if ClockStart and type(ClockStart)=="number"then Tstart=Tnow+ClockStart elseif ClockStart and type(ClockStart)=="string"then Tstart=UTILS.ClockToSeconds(ClockStart) end local Tstop=nil if ClockStop and type(ClockStop)=="number"then Tstop=Tnow+ClockStop elseif ClockStop and type(ClockStop)=="string"then Tstop=UTILS.ClockToSeconds(ClockStop) end self.Tstart=Tstart self.Tstop=Tstop if Tstop then self.duration=self.Tstop-self.Tstart end return self end function OPSTRANSPORT:SetPriority(Prio,Importance,Urgent) self.prio=Prio or 50 self.urgent=Urgent self.importance=Importance return self end function OPSTRANSPORT:SetVerbosity(Verbosity) self.verbose=Verbosity or 0 return self end function OPSTRANSPORT:AddConditionStart(ConditionFunction,...) if ConditionFunction then local condition={} condition.func=ConditionFunction condition.arg={} if arg then condition.arg=arg end table.insert(self.conditionStart,condition) end return self end function OPSTRANSPORT:AddPathTransport(PathGroup,Reversed,Radius,TransportZoneCombo) TransportZoneCombo=TransportZoneCombo or self.tzcDefault if type(PathGroup)=="string"then PathGroup=GROUP:FindByName(PathGroup) end local path={} path.category=PathGroup:GetCategory() path.radius=Radius or 0 path.waypoints=PathGroup:GetTaskRoute() table.insert(TransportZoneCombo.TransportPaths,path) return self end function OPSTRANSPORT:_GetPathTransport(Category,TransportZoneCombo) TransportZoneCombo=TransportZoneCombo or self.tzcDefault local pathsTransport=TransportZoneCombo.TransportPaths if pathsTransport and#pathsTransport>0 then local paths={} for _,_path in pairs(pathsTransport)do local path=_path if path.category==Category then table.insert(paths,path) end end if#paths>0 then local path=paths[math.random(#paths)] return path end end return nil end function OPSTRANSPORT:SetCarrierTransportStatus(CarrierGroup,Status) local oldstatus=self:GetCarrierTransportStatus(CarrierGroup) self:T(self.lid..string.format("New carrier transport status for %s: %s --> %s",CarrierGroup:GetName(),oldstatus,Status)) self.carrierTransportStatus[CarrierGroup.groupname]=Status return self end function OPSTRANSPORT:GetCarrierTransportStatus(CarrierGroup) local status=self.carrierTransportStatus[CarrierGroup.groupname]or"unknown" return status end function OPSTRANSPORT:GetUID() return self.uid end function OPSTRANSPORT:GetNcargoDelivered() return self.Ndelivered end function OPSTRANSPORT:GetNcargoTotal() return self.Ncargo end function OPSTRANSPORT:GetNcarrier() return self.Ncarrier end function OPSTRANSPORT:AddAsset(Asset,TransportZoneCombo) self:T(self.lid..string.format("Adding asset carrier \"%s\" to transport",tostring(Asset.spawngroupname))) self.assets=self.assets or{} table.insert(self.assets,Asset) return self end function OPSTRANSPORT:DelAsset(Asset) for i,_asset in pairs(self.assets or{})do local asset=_asset if asset.uid==Asset.uid then self:T(self.lid..string.format("Removing asset \"%s\" from transport",tostring(Asset.spawngroupname))) table.remove(self.assets,i) return self end end return self end function OPSTRANSPORT:AddAssetCargo(Asset,TransportZoneCombo) self:T(self.lid..string.format("Adding asset cargo \"%s\" to transport and TZC=%s",tostring(Asset.spawngroupname),TransportZoneCombo and TransportZoneCombo.uid or"N/A")) self.assetsCargo=self.assetsCargo or{} table.insert(self.assetsCargo,Asset) TransportZoneCombo.assetsCargo=TransportZoneCombo.assetsCargo or{} TransportZoneCombo.assetsCargo[Asset.spawngroupname]=Asset return self end function OPSTRANSPORT:GetTZCofCargo(GroupName) for _,_tzc in pairs(self.tzCombos)do local tzc=_tzc for _,_cargo in pairs(tzc.Cargos)do local cargo=_cargo if cargo.opsgroup:GetName()==GroupName then return tzc end end end return nil end function OPSTRANSPORT:AddLegion(Legion) self:T(self.lid..string.format("Adding legion %s",Legion.alias)) table.insert(self.legions,Legion) return self end function OPSTRANSPORT:RemoveLegion(Legion) for i=#self.legions,1,-1 do local legion=self.legions[i] if legion.alias==Legion.alias then self:T(self.lid..string.format("Removing legion %s",Legion.alias)) table.remove(self.legions,i) return self end end self:E(self.lid..string.format("ERROR: Legion %s not found and could not be removed!",Legion.alias)) return self end function OPSTRANSPORT:IsCarrier(CarrierGroup) if CarrierGroup then for _,_carrier in pairs(self.carriers)do local carrier=_carrier if carrier.groupname==CarrierGroup.groupname then return true end end end return false end function OPSTRANSPORT:IsReadyToGo() local text=self.lid.."Is ReadyToGo? " local Tnow=timer.getAbsTime() local gotzones=false for _,_tz in pairs(self.tzCombos)do local tz=_tz if tz.PickupZone and tz.DeployZone then gotzones=true break end end if not gotzones then text=text.."No, pickup/deploy zone combo not yet defined!" return false end if self.Tstart and Tnowself.Tstop then text=text.."Nope, stop time already passed!" self:T(text) return false end local startme=self:EvalConditionsAll(self.conditionStart) if not startme then text=text..("No way, at least one start condition is not true!") self:T(text) return false end text=text.."Yes!" self:T(text) return true end function OPSTRANSPORT:SetLegionStatus(Legion,Status) local status=self:GetLegionStatus(Legion) self:T(self.lid..string.format("Setting LEGION %s to status %s-->%s",Legion.alias,tostring(status),tostring(Status))) self.statusLegion[Legion.alias]=Status return self end function OPSTRANSPORT:GetLegionStatus(Legion) local status=self.statusLegion[Legion.alias]or"unknown" return status end function OPSTRANSPORT:IsPlanned() local is=self:is(OPSTRANSPORT.Status.PLANNED) return is end function OPSTRANSPORT:IsQueued(Legion) local is=self:is(OPSTRANSPORT.Status.QUEUED) if Legion then is=self:GetLegionStatus(Legion)==OPSTRANSPORT.Status.QUEUED end return is end function OPSTRANSPORT:IsRequested(Legion) local is=self:is(OPSTRANSPORT.Status.REQUESTED) if Legion then is=self:GetLegionStatus(Legion)==OPSTRANSPORT.Status.REQUESTED end return is end function OPSTRANSPORT:IsScheduled() local is=self:is(OPSTRANSPORT.Status.SCHEDULED) return is end function OPSTRANSPORT:IsExecuting() local is=self:is(OPSTRANSPORT.Status.EXECUTING) return is end function OPSTRANSPORT:IsDelivered(Nmin) local is=self:is(OPSTRANSPORT.Status.DELIVERED) if is==false and Nmin and self.Ndelivered>=math.min(self.Ncargo,Nmin)then is=true end return is end function OPSTRANSPORT:onafterStatusUpdate(From,Event,To) local fsmstate=self:GetState() if self.verbose>=1 then local text=string.format("%s: Ncargo=%d/%d, Ncarrier=%d/%d, Nlegions=%d",fsmstate:upper(),self.Ncargo,self.Ndelivered,#self.carriers,self.Ncarrier,#self.legions) if self.verbose>=2 then for i,_tz in pairs(self.tzCombos)do local tz=_tz local pickupzone=tz.PickupZone and tz.PickupZone:GetName()or"Unknown" local deployzone=tz.DeployZone and tz.DeployZone:GetName()or"Unknown" text=text..string.format("\n[%d] %s --> %s: Ncarriers=%d, Ncargo=%d (%d)",i,pickupzone,deployzone,tz.Ncarriers,#tz.Cargos,tz.Ncargo) end end if self.verbose>=3 then text=text..string.format("\nCargos:") for _,_cargo in pairs(self:GetCargos())do local cargo=_cargo if cargo.type==OPSTRANSPORT.CargoType.OPSGROUP then local carrier=cargo.opsgroup:_GetMyCarrierElement() local name=carrier and carrier.name or"none" local cstate=carrier and carrier.status or"N/A" text=text..string.format("\n- %s: %s [%s], weight=%d kg, carrier=%s [%s], delivered=%s [UID=%s]", cargo.opsgroup:GetName(),cargo.opsgroup.cargoStatus:upper(),cargo.opsgroup:GetState(),cargo.opsgroup:GetWeightTotal(),name,cstate,tostring(cargo.delivered),tostring(cargo.opsgroup.cargoTransportUID)) else local storage=cargo.storage text=text..string.format("\n- storage type=%s: amount: total=%d loaded=%d, lost=%d, delivered=%d, delivered=%s [UID=%s]", storage.cargoType,storage.cargoAmount,storage.cargoLoaded,storage.cargoLost,storage.cargoDelivered,tostring(cargo.delivered),tostring(cargo.uid)) end end text=text..string.format("\nCarriers:") for _,_carrier in pairs(self.carriers)do local carrier=_carrier text=text..string.format("\n- %s: %s [%s], Cargo Bay [current/reserved/total]=%d/%d/%d kg [free %d/%d/%d kg]", carrier:GetName(),carrier.carrierStatus:upper(),carrier:GetState(), carrier:GetWeightCargo(nil,false),carrier:GetWeightCargo(),carrier:GetWeightCargoMax(), carrier:GetFreeCargobay(nil,false),carrier:GetFreeCargobay(),carrier:GetFreeCargobayMax()) end end self:I(self.lid..text) end self:_CheckDelivered() if not self:IsDelivered()then self:__StatusUpdate(-30) end end function OPSTRANSPORT:IsCargoDelivered(GroupName) for _,_cargo in pairs(self:GetCargos())do local cargo=_cargo if cargo.opsgroup:GetName()==GroupName then return cargo.delivered end end return nil end function OPSTRANSPORT:onafterPlanned(From,Event,To) self:T(self.lid..string.format("New status: %s-->%s",From,To)) end function OPSTRANSPORT:onafterScheduled(From,Event,To) self:T(self.lid..string.format("New status: %s-->%s",From,To)) end function OPSTRANSPORT:onafterExecuting(From,Event,To) self:T(self.lid..string.format("New status: %s-->%s",From,To)) end function OPSTRANSPORT:onbeforeDelivered(From,Event,To) if From==OPSTRANSPORT.Status.DELIVERED then return false end return true end function OPSTRANSPORT:onafterDelivered(From,Event,To) self:T(self.lid..string.format("New status: %s-->%s",From,To)) for i=#self.carriers,1,-1 do local carrier=self.carriers[i] if self:GetCarrierTransportStatus(carrier)~=OPSTRANSPORT.Status.DELIVERED then carrier:Delivered(self) end end end function OPSTRANSPORT:onafterLoaded(From,Event,To,OpsGroupCargo,OpsGroupCarrier,CarrierElement) self:I(self.lid..string.format("Loaded OPSGROUP %s into carrier %s",OpsGroupCargo:GetName(),tostring(CarrierElement.name))) end function OPSTRANSPORT:onafterUnloaded(From,Event,To,OpsGroupCargo,OpsGroupCarrier) self:I(self.lid..string.format("Unloaded OPSGROUP %s",OpsGroupCargo:GetName())) end function OPSTRANSPORT:onafterDeadCarrierGroup(From,Event,To,OpsGroup) self:I(self.lid..string.format("Carrier OPSGROUP %s dead!",OpsGroup:GetName())) self.NcarrierDead=self.NcarrierDead+1 self:_DelCarrier(OpsGroup) if#self.carriers==0 then self:DeadCarrierAll() end end function OPSTRANSPORT:onafterDeadCarrierAll(From,Event,To) self:I(self.lid..string.format("ALL Carrier OPSGROUPs are dead!")) if self.opszone then self:I(self.lid..string.format("Cancelling transport on CHIEF level")) self.chief:TransportCancel(self) else self:_CheckDelivered() if not self:IsDelivered()then self:Planned() end end end function OPSTRANSPORT:onafterCancel(From,Event,To) local Ngroups=#self.carriers self:I(self.lid..string.format("CANCELLING transport in status %s. Will wait for %d carrier groups to report DONE before evaluation",self:GetState(),Ngroups)) self.Tover=timer.getAbsTime() if self.chief then self:T(self.lid..string.format("CHIEF will cancel the transport. Will wait for mission DONE before evaluation!")) self.chief:TransportCancel(self) elseif self.commander then self:T(self.lid..string.format("COMMANDER will cancel the transport. Will wait for transport DELIVERED before evaluation!")) self.commander:TransportCancel(self) elseif self.legions and#self.legions>0 then for _,_legion in pairs(self.legions or{})do local legion=_legion self:T(self.lid..string.format("LEGION %s will cancel the transport. Will wait for transport DELIVERED before evaluation!",legion.alias)) legion:TransportCancel(self) end else self:T(self.lid..string.format("No legion, commander or chief. Attached OPS groups will cancel the transport on their own. Will wait for transport DELIVERED before evaluation!")) for _,_carrier in pairs(self:GetCarriers())do local carrier=_carrier carrier:TransportCancel(self) end local cargos=self:GetCargoOpsGroups(false) for _,_cargo in pairs(cargos)do local cargo=_cargo cargo:_DelMyLift(self) end end if self:IsPlanned()or self:IsQueued()or self:IsRequested()or Ngroups==0 then self:T(self.lid..string.format("Cancelled transport was in %s stage with %d carrier groups assigned and alive. Call it DELIVERED!",self:GetState(),Ngroups)) self:Delivered() end end function OPSTRANSPORT:_CheckDelivered() if self.Ncargo>0 then local done=true local dead=true for _,_cargo in pairs(self:GetCargos())do local cargo=_cargo if cargo.delivered then dead=false elseif cargo.type==OPSTRANSPORT.CargoType.OPSGROUP and cargo.opsgroup==nil then dead=false elseif cargo.type==OPSTRANSPORT.CargoType.OPSGROUP and cargo.opsgroup:IsDestroyed()then elseif cargo.type==OPSTRANSPORT.CargoType.OPSGROUP and cargo.opsgroup:IsDead()then elseif cargo.type==OPSTRANSPORT.CargoType.OPSGROUP and cargo.opsgroup:IsStopped()then dead=false else done=false dead=false end end if dead then self:I(self.lid.."All cargo DEAD ==> Delivered!") self:Delivered() elseif done then self:I(self.lid.."All cargo DONE ==> Delivered!") self:Delivered() end end end function OPSTRANSPORT:_CheckRequiredCargos(TransportZoneCombo,CarrierGroup) TransportZoneCombo=TransportZoneCombo or self.tzcDefault local requiredCargos=TransportZoneCombo.Cargos if TransportZoneCombo.RequiredCargos and#TransportZoneCombo.RequiredCargos>0 then requiredCargos=TransportZoneCombo.RequiredCargos else requiredCargos={} for _,_cargo in pairs(TransportZoneCombo.Cargos)do local cargo=_cargo if not cargo.delivered then table.insert(requiredCargos,cargo.opsgroup) end end end if requiredCargos==nil or#requiredCargos==0 then return true end local carrierNames=self:_GetCarrierNames() local weightmin=nil for _,_cargo in pairs(requiredCargos)do local cargo=_cargo local isLoaded=cargo:IsLoaded(carrierNames) if not isLoaded then local weight=cargo:GetWeightTotal() if weightmin==nil or weight=1 then local dist=tz.PickupZone:Get2DDistance(vec2) local ncarriers=0 for _,_carrier in pairs(self.carriers)do local carrier=_carrier if carrier and carrier:IsAlive()and carrier.cargoTZC and carrier.cargoTZC.uid==tz.uid then ncarriers=ncarriers+1 end end local candidate={tzc=tz,distance=dist/1000,ncargo=ncargo,ncarriers=ncarriers} candidate.penalty=penalty(candidate) table.insert(candidates,candidate) end end end if#candidates>0 then local function optTZC(candA,candB) return candA.penalty=3 then local text="TZC optimized" for i,candidate in pairs(candidates)do text=text..string.format("\n[%d] TPZ=%d, Ncarriers=%d, Ncargo=%d, Distance=%.1f km, PENALTY=%d",i,candidate.tzc.uid,candidate.ncarriers,candidate.ncargo,candidate.distance,candidate.penalty) end self:I(self.lid..text) end return candidates[1].tzc else self:T(self.lid..string.format("Could NOT find a pickup zone (with cargo) for carrier group %s",Carrier:GetName())) end return nil end function OPSTRANSPORT:_GetOpsGroupFromObject(Object) local opsgroup=nil if Object:IsInstanceOf("OPSGROUP")then opsgroup=Object elseif Object:IsInstanceOf("GROUP")then opsgroup=_DATABASE:GetOpsGroup(Object) if not opsgroup then if Object:IsAir()then opsgroup=FLIGHTGROUP:New(Object) elseif Object:IsShip()then opsgroup=NAVYGROUP:New(Object) else opsgroup=ARMYGROUP:New(Object) end end else self:E(self.lid.."ERROR: Object must be a GROUP or OPSGROUP object!") return nil end return opsgroup end OPSZONE={ ClassName="OPSZONE", verbose=0, Nred=0, Nblu=0, Nnut=0, Ncoal={}, Tred=0, Tblu=0, Tnut=0, chiefs={}, Missions={}, UpdateSeconds=120, } OPSZONE.ZoneType={ Circular="Circular", Polygon="Polygon", } OPSZONE.version="0.6.2" function OPSZONE:New(Zone,CoalitionOwner) local self=BASE:Inherit(self,FSM:New()) if Zone then if type(Zone)=="string"then local Name=Zone Zone=ZONE:FindByName(Name) if not Zone then local airbase=AIRBASE:FindByName(Name) if airbase then Zone=ZONE_AIRBASE:New(Name,2000) end end if not Zone then self:E(string.format("ERROR: No ZONE or ZONE_AIRBASE found for name: %s",Name)) return nil end end else self:E("ERROR: First parameter Zone is nil in OPSZONE:New(Zone) call!") return nil end if Zone:IsInstanceOf("ZONE_AIRBASE")then self.airbase=Zone._.ZoneAirbase self.airbaseName=self.airbase:GetName() self.zoneType=OPSZONE.ZoneType.Circular self.zoneCircular=Zone elseif Zone:IsInstanceOf("ZONE_RADIUS")then self.zoneType=OPSZONE.ZoneType.Circular self.zoneCircular=Zone elseif Zone:IsInstanceOf("ZONE_POLYGON_BASE")then self.zoneType=OPSZONE.ZoneType.Polygon local zone=Zone self.zoneCircular=zone:GetZoneRadius(nil,true) else self:E("ERROR: OPSZONE must be a SPHERICAL zone due to DCS restrictions!") return nil end self.lid=string.format("OPSZONE %s | ",Zone:GetName()) self.zone=Zone self.zoneName=Zone:GetName() self.zoneRadius=self.zoneCircular:GetRadius() self.Missions={} self.ScanUnitSet=SET_UNIT:New():FilterZones({Zone}) self.ScanGroupSet=SET_GROUP:New():FilterZones({Zone}) _DATABASE:AddOpsZone(self) self.ownerCurrent=CoalitionOwner or coalition.side.NEUTRAL self.ownerPrevious=CoalitionOwner or coalition.side.NEUTRAL self.isContested=false self.Ncoal[coalition.side.BLUE]=0 self.Ncoal[coalition.side.RED]=0 self.Ncoal[coalition.side.NEUTRAL]=0 if self.airbase then self.ownerCurrent=self.airbase:GetCoalition() self.ownerPrevious=self.airbase:GetCoalition() end self:SetObjectCategories() self:SetUnitCategories() self:SetDrawZone() self:SetMarkZone(true) self:SetCaptureTime() self:SetCaptureNunits() self:SetCaptureThreatlevel() self.timerStatus=TIMER:New(OPSZONE.Status,self) self:SetStartState("Stopped") self:AddTransition("Stopped","Start","Empty") self:AddTransition("*","Stop","Stopped") self:AddTransition("*","Evaluated","*") self:AddTransition("*","Captured","Guarded") self:AddTransition("Empty","Guarded","Guarded") self:AddTransition("*","Empty","Empty") self:AddTransition("*","Attacked","Attacked") self:AddTransition("*","Defeated","Guarded") return self end function OPSZONE:SetVerbosity(VerbosityLevel) self.verbose=VerbosityLevel or 0 return self end function OPSZONE:SetObjectCategories(Categories) if Categories and type(Categories)~="table"then Categories={Categories} end self.ObjectCategories=Categories or{Object.Category.UNIT,Object.Category.STATIC} return self end function OPSZONE:SetUnitCategories(Categories) if Categories and type(Categories)~="table"then Categories={Categories} end self.UnitCategories=Categories or{Unit.Category.GROUND_UNIT} return self end function OPSZONE:SetCaptureThreatlevel(Threatlevel) self.threatlevelCapture=Threatlevel or 0 return self end function OPSZONE:SetCaptureNunits(Nunits) Nunits=Nunits or 1 self.nunitsCapture=Nunits return self end function OPSZONE:SetCaptureTime(Tcapture) self.TminCaptured=Tcapture or 0 return self end function OPSZONE:SetNeutralCanCapture(CanCapture) self.neutralCanCapture=CanCapture return self end function OPSZONE:SetDrawZone(Switch) if Switch==false then self.drawZone=false else self.drawZone=true end return self end function OPSZONE:SetDrawZoneForCoalition(Switch) if Switch==true then self.drawZoneForCoalition=true else self.drawZoneForCoalition=false end return self end function OPSZONE:SetMarkZone(Switch,ReadOnly) if Switch then self.markZone=true local Coordinate=self:GetCoordinate() self.markerText=self:_GetMarkerText() self.marker=self.marker or MARKER:New(Coordinate,self.markerText) if ReadOnly==false then self.marker.readonly=false else self.marker.readonly=true end self.marker:ToAll() else if self.marker then self.marker:Remove() end self.marker=nil self.markZone=false end return self end function OPSZONE:GetOwner() return self.ownerCurrent end function OPSZONE:GetOwnerName() return UTILS.GetCoalitionName(self.ownerCurrent) end function OPSZONE:GetCoordinate() local coordinate=self.zone:GetCoordinate() return coordinate end function OPSZONE:GetScannedUnitSet() return self.ScanUnitSet end function OPSZONE:GetScannedGroupSet() return self.ScanGroupSet end function OPSZONE:GetRandomCoordinate(inner,outer,surfacetypes) local zone=self:GetZone() local coord=zone:GetRandomCoordinate(inner,outer,surfacetypes) return coord end function OPSZONE:GetName() return self.zoneName end function OPSZONE:GetZone() return self.zone end function OPSZONE:GetPreviousOwner() return self.ownerPrevious end function OPSZONE:GetAttackDuration() if self:IsAttacked()and self.Tattacked then local dT=timer.getAbsTime()-self.Tattacked return dT end return nil end function OPSZONE:FindByName(ZoneName) local Found=_DATABASE:FindOpsZone(ZoneName) return Found end function OPSZONE:IsRed() local is=self.ownerCurrent==coalition.side.RED return is end function OPSZONE:IsBlue() local is=self.ownerCurrent==coalition.side.BLUE return is end function OPSZONE:IsNeutral() local is=self.ownerCurrent==coalition.side.NEUTRAL return is end function OPSZONE:IsCoalition(Coalition) local is=self.ownerCurrent==Coalition return is end function OPSZONE:IsStarted() local is=not self:IsStopped() return is end function OPSZONE:IsStopped() local is=self:is("Stopped") return is end function OPSZONE:IsGuarded() local is=self:is("Guarded") return is end function OPSZONE:IsEmpty() local is=self:is("Empty") return is end function OPSZONE:IsAttacked() local is=self:is("Attacked") return is end function OPSZONE:IsContested() return self.isContested end function OPSZONE:IsStopped() local is=self:is("Stopped") return is end function OPSZONE:onafterStart(From,Event,To) self:I(self.lid..string.format("Starting OPSZONE v%s",OPSZONE.version)) self.timerStatus=self.timerStatus or TIMER:New(OPSZONE.Status,self) local EveryUpdateIn=self.UpdateSeconds or 120 self.timerStatus:Start(1,EveryUpdateIn) if self.airbase then self:HandleEvent(EVENTS.BaseCaptured) end return self end function OPSZONE:onafterStop(From,Event,To) self:I(self.lid..string.format("Stopping OPSZONE")) self.timerStatus:Stop() self.zone:UndrawZone() if self.markZone then self.marker:Remove() end self:UnHandleEvent(EVENTS.BaseCaptured) self.CallScheduler:Clear() if self.Scheduler then self.Scheduler:Clear() end end function OPSZONE:Status() local fsmstate=self:GetState() local contested=tostring(self:IsContested()) if self.verbose>=1 then local text=string.format("State %s: Owner %d (previous %d), contested=%s, Nunits: red=%d, blue=%d, neutral=%d",fsmstate,self.ownerCurrent,self.ownerPrevious,contested,self.Nred,self.Nblu,self.Nnut) self:I(self.lid..text) end self:Scan() self:EvaluateZone() self:_UpdateMarker() if self.zone.DrawID and not self.drawZone then self.zone:UndrawZone() end end function OPSZONE:onbeforeCaptured(From,Event,To,NewOwnerCoalition) if self.ownerCurrent==NewOwnerCoalition then self:T(self.lid.."") end return true end function OPSZONE:onafterCaptured(From,Event,To,NewOwnerCoalition) self:T(self.lid..string.format("Zone captured by coalition=%d",NewOwnerCoalition)) self.ownerPrevious=self.ownerCurrent self.ownerCurrent=NewOwnerCoalition if self.drawZone then self.zone:UndrawZone() local color=self:_GetZoneColor() local coalition=nil if self.drawZoneForCoalition then coalition=self.ownerCurrent end self.zone:DrawZone(coalition,color,1.0,color,0.5) end for _,_chief in pairs(self.chiefs)do local chief=_chief if chief.coalition==self.ownerCurrent then chief:ZoneCaptured(self) else chief:ZoneLost(self) end end end function OPSZONE:onafterEmpty(From,Event,To) self:T(self.lid..string.format("Zone is empty EVENT")) end function OPSZONE:onafterAttacked(From,Event,To,AttackerCoalition) self:T(self.lid..string.format("Zone is being attacked by coalition=%s!",tostring(AttackerCoalition))) end function OPSZONE:onafterDefeated(From,Event,To,DefeatedCoalition) self:T(self.lid..string.format("Defeated attack on zone by coalition=%d",DefeatedCoalition)) self.Tattacked=nil end function OPSZONE:onenterGuarded(From,Event,To) if From~=To then self:T(self.lid..string.format("Zone is guarded")) self.Tattacked=nil if self.drawZone then self.zone:UndrawZone() local color=self:_GetZoneColor() local coalition=nil if self.drawZoneForCoalition then coalition=self.ownerCurrent end self.zone:DrawZone(coalition,color,1.0,color,0.5) end end end function OPSZONE:onenterAttacked(From,Event,To,AttackerCoalition) if From~="Attacked"then self:T(self.lid..string.format("Zone is Attacked")) self.Tattacked=timer.getAbsTime() if AttackerCoalition then for _,_chief in pairs(self.chiefs)do local chief=_chief if chief.coalition~=AttackerCoalition then chief:ZoneAttacked(self) end end end if self.drawZone then self.zone:UndrawZone() local color={1,204/255,204/255} local coalition=nil if self.drawZoneForCoalition then coalition=self.ownerCurrent end self.zone:DrawZone(coalition,color,1.0,color,0.5) end self:_CleanMissionTable() end end function OPSZONE:onenterEmpty(From,Event,To) if From~=To then self:T(self.lid..string.format("Zone is empty now")) for _,_chief in pairs(self.chiefs)do local chief=_chief chief:ZoneEmpty(self) end if self.drawZone then self.zone:UndrawZone() local color=self:_GetZoneColor() local coalition=nil if self.drawZoneForCoalition then coalition=self.ownerCurrent end self.zone:DrawZone(coalition,color,1.0,color,0.2) end end end function OPSZONE:Scan() if self.verbose>=3 then local text=string.format("Scanning zone %s R=%.1f m",self.zoneName,self.zoneRadius) self:I(self.lid..text) end local SphereSearch={id=world.VolumeType.SPHERE,params={point=self.zone:GetVec3(),radius=self.zoneRadius}} local Nred=0 local Nblu=0 local Nnut=0 local Tred=0 local Tblu=0 local Tnut=0 self.ScanGroupSet:Clear(false) self.ScanUnitSet:Clear(false) local function EvaluateZone(_ZoneObject) local ZoneObject=_ZoneObject if ZoneObject then local ObjectCategory=Object.getCategory(ZoneObject) if ObjectCategory==Object.Category.UNIT and ZoneObject:isExist()and ZoneObject:isActive()then local DCSUnit=ZoneObject local function Included() if not self.UnitCategories then return true else local CategoryDCSUnit=ZoneObject:getDesc().category for _,UnitCategory in pairs(self.UnitCategories)do if UnitCategory==CategoryDCSUnit then return true end end end return false end if Included()then local Coalition=DCSUnit:getCoalition() local tl=0 local unit=UNIT:Find(DCSUnit) if unit then local inzone=true if self.zoneType==OPSZONE.ZoneType.Polygon then inzone=unit:IsInZone(self.zone) end if inzone then tl=unit:GetThreatLevel() self.ScanUnitSet:AddUnit(unit) local group=unit:GetGroup() if group then self.ScanGroupSet:AddGroup(group,true) end if Coalition==coalition.side.RED then Nred=Nred+1 Tred=Tred+tl elseif Coalition==coalition.side.BLUE then Nblu=Nblu+1 Tblu=Tblu+tl elseif Coalition==coalition.side.NEUTRAL then Nnut=Nnut+1 Tnut=Tnut+tl end if self.verbose>=4 then self:I(self.lid..string.format("Found unit %s (coalition=%d)",DCSUnit:getName(),Coalition)) end end end end elseif ObjectCategory==Object.Category.STATIC and ZoneObject:isExist()then local DCSStatic=ZoneObject local Coalition=DCSStatic:getCoalition() local inzone=true if self.zoneType==OPSZONE.ZoneType.Polygon then local Vec3=DCSStatic:getPoint() inzone=self.zone:IsVec3InZone(Vec3) end if inzone then if Coalition==coalition.side.RED then Nred=Nred+1 elseif Coalition==coalition.side.BLUE then Nblu=Nblu+1 elseif Coalition==coalition.side.NEUTRAL then Nnut=Nnut+1 end if self.verbose>=4 then self:I(self.lid..string.format("Found static %s (coalition=%d)",DCSStatic:getName(),Coalition)) end end elseif ObjectCategory==Object.Category.SCENERY then local SceneryType=ZoneObject:getTypeName() local SceneryName=ZoneObject:getName() self:T2(self.lid..string.format("Found scenery type=%s, name=%s",SceneryType,SceneryName)) end end return true end world.searchObjects(self.ObjectCategories,SphereSearch,EvaluateZone) if self.verbose>=3 then local text=string.format("Scan result Nred=%d, Nblue=%d, Nneutral=%d",Nred,Nblu,Nnut) if self.verbose>=4 then for _,_unit in pairs(self.ScanUnitSet:GetSet())do local unit=_unit text=text..string.format("\nUnit %s coalition=%s",unit:GetName(),unit:GetCoalitionName()) end for _,_group in pairs(self.ScanGroupSet:GetSet())do local group=_group text=text..string.format("\nGroup %s coalition=%s",group:GetName(),group:GetCoalitionName()) end end self:I(self.lid..text) end self.Nred=Nred self.Nblu=Nblu self.Nnut=Nnut self.Ncoal[coalition.side.BLUE]=Nblu self.Ncoal[coalition.side.RED]=Nred self.Ncoal[coalition.side.NEUTRAL]=Nnut self.Tblu=Tblu self.Tred=Tred self.Tnut=Tnut return self end function OPSZONE:EvaluateZone() local Nred=self.Nred local Nblu=self.Nblu local Nnut=self.Nnut local Tnow=timer.getAbsTime() local function captured(coal) if not self.airbase then if not self.Tcaptured then self.Tcaptured=Tnow end if Tnow-self.Tcaptured>=self.TminCaptured then self:Captured(coal) self.Tcaptured=nil end end end if self:IsRed()then if Nred==0 then if Nblu>=self.nunitsCapture and self.Tblu>=self.threatlevelCapture then captured(coalition.side.BLUE) elseif Nnut>=self.nunitsCapture and self.Tnut>=self.threatlevelCapture and self.neutralCanCapture then captured(coalition.side.NEUTRAL) end else if Nblu>0 then if not self:IsAttacked()and self.Tblu>=self.threatlevelCapture then self:Attacked(coalition.side.BLUE) end elseif Nblu==0 then if self:IsAttacked()and self:IsContested()then self:Defeated(coalition.side.BLUE) elseif self:IsEmpty()then self:Guarded() end end end if Nblu==0 then self.isContested=false else self.isContested=true end elseif self:IsBlue()then if Nblu==0 then if Nred>=self.nunitsCapture and self.Tred>=self.threatlevelCapture then captured(coalition.side.RED) elseif Nnut>=self.nunitsCapture and self.Tnut>=self.threatlevelCapture and self.neutralCanCapture then captured(coalition.side.NEUTRAL) end else if Nred>0 then if not self:IsAttacked()and self.Tred>=self.threatlevelCapture then self:Attacked(coalition.side.RED) end elseif Nred==0 then if self:IsAttacked()and self:IsContested()then self:Defeated(coalition.side.RED) elseif self:IsEmpty()then self:Guarded() end end end if Nred==0 then self.isContested=false else self.isContested=true end elseif self:IsNeutral()then if Nred>0 and Nblu>0 then self:T(self.lid.."FF neutrals left neutral zone and red and blue are present! What to do?") if not self:IsAttacked()then self:Attacked() end self.isContested=true elseif Nred>=self.nunitsCapture and self.Tred>=self.threatlevelCapture then captured(coalition.side.RED) elseif Nblu>=self.nunitsCapture and self.Tblu>=self.threatlevelCapture then captured(coalition.side.BLUE) end else self:E(self.lid.."ERROR: Unknown coaliton!") end if Nblu==0 and Nred==0 and Nnut==0 and(not self:IsEmpty())then self:Empty() end if self.airbase then local airbasecoalition=self.airbase:GetCoalition() if airbasecoalition~=self.ownerCurrent then self:T(self.lid..string.format("Captured airbase %s: Coaltion %d-->%d",self.airbaseName,self.ownerCurrent,airbasecoalition)) self:Captured(airbasecoalition) end end self:Evaluated() end function OPSZONE:OnEventHit(EventData) if self.HitsOn then local UnitHit=EventData.TgtUnit if UnitHit and UnitHit:IsInZone(self)and UnitHit:GetCoalition()==self.ownerCurrent then self.HitTimeLast=timer.getTime() if not self:IsAttacked()then self:T3(self.lid.."Hit ==> Attack") self:Attacked() end end end end function OPSZONE:OnEventBaseCaptured(EventData) if EventData and EventData.Place and self.airbase and self.airbaseName then local airbase=EventData.Place if EventData.PlaceName==self.airbaseName then local CoalitionNew=airbase:GetCoalition() self:I(self.lid..string.format("EVENT BASE CAPTURED: New coalition of airbase %s: %d [previous=%d]",self.airbaseName,CoalitionNew,self.ownerCurrent)) if CoalitionNew~=self.ownerCurrent then self:Captured(CoalitionNew) end end end end function OPSZONE:_GetZoneColor() local color={0,0,0} if self.ownerCurrent==coalition.side.NEUTRAL then color=self.ZoneOwnerNeutral or{1,1,1} elseif self.ownerCurrent==coalition.side.BLUE then color=self.ZoneOwnerBlue or{0,0,1} elseif self.ownerCurrent==coalition.side.RED then color=self.ZoneOwnerRed or{1,0,0} else end return color end function OPSZONE:SetZoneColor(Neutral,Blue,Red) self.ZoneOwnerNeutral=Neutral or{1,1,1} self.ZoneOwnerBlue=Blue or{0,0,1} self.ZoneOwnerRed=Red or{1,0,0} return self end function OPSZONE:_UpdateMarker() if self.markZone then local text=self:_GetMarkerText() if text~=self.markerText then self.markerText=text self.marker:UpdateText(self.markerText) end end end function OPSZONE:_GetMarkerText() local owner=UTILS.GetCoalitionName(self.ownerCurrent) local prevowner=UTILS.GetCoalitionName(self.ownerPrevious) local text=string.format("%s [N=%d, TL=%d T=%d]:\nOwner=%s [%s]\nState=%s [Contested=%s]\nBlue=%d [TL=%d]\nRed=%d [TL=%d]\nNeutral=%d [TL=%d]", self.zoneName,self.nunitsCapture or 0,self.threatlevelCapture or 0,self.TminCaptured or 0, owner,prevowner,self:GetState(),tostring(self:IsContested()), self.Nblu,self.Tblu,self.Nred,self.Tred,self.Nnut,self.Tnut) return text end function OPSZONE:_AddChief(Chief) table.insert(self.chiefs,Chief) end function OPSZONE:_AddMission(Coalition,Type,Auftrag) local entry={} entry.Coalition=Coalition or coalition.side.NEUTRAL entry.Type=Type or"" entry.Mission=Auftrag or nil table.insert(self.Missions,entry) return self end function OPSZONE:_GetMissions() return self.Missions end function OPSZONE:_FindMissions(Coalition,Type) local foundmissions={} local found=false for _,_entry in pairs(self.Missions)do local entry=_entry if entry.Coalition==Coalition and entry.Type==Type and entry.Mission and entry.Mission:IsNotOver()then table.insert(foundmissions,entry.Mission) found=true end end return found,foundmissions end function OPSZONE:_CleanMissionTable() local missions={} for _,_entry in pairs(self.Missions)do local entry=_entry if entry.Mission and entry.Mission:IsNotOver()then table.insert(missions,entry) end end self.Missions=missions return self end PLATOON={ ClassName="PLATOON", verbose=0, weaponData={}, } PLATOON.version="0.1.0" function PLATOON:New(TemplateGroupName,Ngroups,PlatoonName) local self=BASE:Inherit(self,COHORT:New(TemplateGroupName,Ngroups,PlatoonName)) self:AddMissionCapability(AUFTRAG.Type.NOTHING,50) self.isGround=true self.ammo=self:_CheckAmmo() return self end function PLATOON:SetBrigade(Brigade) self.legion=Brigade return self end function PLATOON:GetBrigade() return self.legion end function PLATOON:onafterStatus(From,Event,To) if self.verbose>=1 then local fsmstate=self:GetState() local callsign=self.callsignName and UTILS.GetCallsignName(self.callsignName)or"N/A" local skill=self.skill and tostring(self.skill)or"N/A" local NassetsTot=#self.assets local NassetsInS=self:CountAssets(true) local NassetsQP=0;local NassetsP=0;local NassetsQ=0 if self.legion then NassetsQP,NassetsP,NassetsQ=self.legion:CountAssetsOnMission(nil,self) end local text=string.format("%s [Type=%s, Call=%s, Skill=%s]: Assets Total=%d, Stock=%d, Mission=%d [Active=%d, Queue=%d]", fsmstate,self.aircrafttype,callsign,skill,NassetsTot,NassetsInS,NassetsQP,NassetsP,NassetsQ) self:T(self.lid..text) if self.verbose>=3 and self.weaponData then local text="Weapon Data:" for bit,_weapondata in pairs(self.weaponData)do local weapondata=_weapondata text=text..string.format("\n- Bit=%s: Rmin=%.1f km, Rmax=%.1f km",bit,weapondata.RangeMin/1000,weapondata.RangeMax/1000) end self:I(self.lid..text) end self:_CheckAssetStatus() end if not self:IsStopped()then self:__Status(-60) end end do _PlayerTaskNr=0 PLAYERTASK={ ClassName="PLAYERTASK", verbose=false, lid=nil, PlayerTaskNr=nil, Type=nil, TTSType=nil, Target=nil, Clients=nil, Repeat=false, repeats=0, RepeatNo=1, TargetMarker=nil, SmokeColor=nil, FlareColor=nil, conditionSuccess={}, conditionFailure={}, TaskController=nil, timestamp=0, lastsmoketime=0, Freetext=nil, FreetextTTS=nil, TaskSubType=nil, NextTaskSuccess={}, NextTaskFailure={}, FinalState="none", PreviousCount=0, CanSmoke=true, ShowThreatDetails=true, PersistMe=false, } PLAYERTASK.version="0.1.31" function PLAYERTASK:New(Type,Target,Repeat,Times,TTSType) local self=BASE:Inherit(self,FSM:New()) self.Type=Type self.Repeat=false self.repeats=0 self.RepeatNo=1 self.Clients=FIFO:New() self.TargetMarker=nil self.SmokeColor=SMOKECOLOR.Red self.conditionSuccess={} self.conditionFailure={} self.TaskController=nil self.timestamp=timer.getAbsTime() self.TTSType=TTSType or"close air support" self.lastsmoketime=0 if type(Repeat)=="boolean"and Repeat==true and type(Times)=="number"and Times>1 then self.Repeat=true self.RepeatNo=Times or 1 end _PlayerTaskNr=_PlayerTaskNr+1 self.PlayerTaskNr=_PlayerTaskNr self.lid=string.format("PlayerTask #%d %s | ",self.PlayerTaskNr,tostring(self.Type)) if Target and Target.ClassName and Target.ClassName=="TARGET"then self.Target=Target elseif Target and Target.ClassName then self.Target=TARGET:New(Target) else self:E(self.lid.."*** NO VALID TARGET!") return self end self.PreviousCount=self.Target:CountTargets() self:T(self.lid.."Created.") self:SetStartState("Planned") self:AddTransition("*","Planned","Planned") self:AddTransition("*","Requested","Requested") self:AddTransition("*","ClientAdded","*") self:AddTransition("*","ClientRemoved","*") self:AddTransition("*","Executing","Executing") self:AddTransition("*","Progress","*") self:AddTransition("*","Done","Done") self:AddTransition("*","Cancel","Done") self:AddTransition("*","Success","Done") self:AddTransition("*","ClientAborted","*") self:AddTransition("*","Failed","Failed") self:AddTransition("*","Status","*") self:AddTransition("*","Stop","Stopped") self:__Status(-5) return self end function PLAYERTASK:NewFromTarget(Target,Repeat,Times,TTSType) return PLAYERTASK:New(self:_GetTaskTypeForTarget(Target),Target,Repeat,Times,TTSType) end function PLAYERTASK:_GetTaskTypeForTarget(Target) local group=nil local auftrag=nil if Target:IsInstanceOf("GROUP")then group=Target elseif Target:IsInstanceOf("SET_GROUP")then group=Target:GetFirst() elseif Target:IsInstanceOf("UNIT")then group=Target:GetGroup() elseif Target:IsInstanceOf("SET_UNIT")then group=Target:GetFirst():GetGroup() elseif Target:IsInstanceOf("AIRBASE")then auftrag=AUFTRAG.Type.BOMBRUNWAY elseif Target:IsInstanceOf("STATIC") or Target:IsInstanceOf("SET_STATIC") or Target:IsInstanceOf("SCENERY") or Target:IsInstanceOf("SET_SCENERY")then auftrag=AUFTRAG.Type.BOMBING elseif Target:IsInstanceOf("OPSZONE") or Target:IsInstanceOf("SET_OPSZONE")then auftrag=AUFTRAG.Type.CAPTUREZONE end if group then local category=group:GetCategory() local attribute=group:GetAttribute() if(category==Group.Category.AIRPLANE or category==Group.Category.HELICOPTER) and group:InAir()then auftrag=AUFTRAG.Type.INTERCEPT elseif category==Group.Category.GROUND or category==Group.Category.TRAIN then if attribute==GROUP.Attribute.GROUND_SAM or attribute==GROUP.Attribute.GROUND_EWR then auftrag=AUFTRAG.Type.SEAD elseif attribute==GROUP.Attribute.GROUND_AAA or attribute==GROUP.Attribute.GROUND_APC or attribute==GROUP.Attribute.GROUND_IFV or attribute==GROUP.Attribute.GROUND_TRUCK or attribute==GROUP.Attribute.GROUND_TRAIN then auftrag=AUFTRAG.Type.BAI elseif attribute==GROUP.Attribute.GROUND_INFANTRY or attribute==GROUP.Attribute.GROUND_ARTILLERY or attribute==GROUP.Attribute.GROUND_TANK then auftrag=AUFTRAG.Type.CAS else auftrag=AUFTRAG.Type.BAI end elseif category==Group.Category.SHIP then auftrag=AUFTRAG.Type.ANTISHIP else self:T(self.lid.."ERROR: Unknown Group category!") end end return auftrag end function PLAYERTASK:_CheckCaptureOpsZoneSuccess(OpsZone,CaptureSquadGroupNamePrefix,Coalition,CheckClientInZone) local isClientInZone=true if CheckClientInZone then isClientInZone=false for _,client in ipairs(self:GetClientObjects())do local clientCoord=client:GetCoordinate() if OpsZone.zone:IsCoordinateInZone(clientCoord)then isClientInZone=true break end end end local isCaptureGroupInZone=false OpsZone:GetScannedGroupSet():ForEachGroup(function(group) if string.find(group:GetName(),CaptureSquadGroupNamePrefix)then isCaptureGroupInZone=true end end) return OpsZone:GetOwner()==Coalition and isClientInZone and isCaptureGroupInZone end function PLAYERTASK:CanJoinTask(Group,Client) return true end function PLAYERTASK:EnablePersistance() self.PersistMe=true return self end function PLAYERTASK:_SetController(Controller) self:T(self.lid.."_SetController") self.TaskController=Controller return self end function PLAYERTASK:SetCoalition(Coalition) self:T(self.lid.."SetCoalition") self.coalition=Coalition or coalition.side.BLUE return self end function PLAYERTASK:GetCoalition() self:T(self.lid.."GetCoalition") return self.coalition end function PLAYERTASK:GetTarget() self:T(self.lid.."GetTarget") return self.Target end function PLAYERTASK:AddFreetext(Text) self:T(self.lid.."AddFreetext") self.Freetext=Text return self end function PLAYERTASK:HasFreetext() self:T(self.lid.."HasFreetext") return self.Freetext~=nil and true or false end function PLAYERTASK:HasFreetextTTS() self:T(self.lid.."HasFreetextTTS") return self.FreetextTTS~=nil and true or false end function PLAYERTASK:SetSubType(Type) self:T(self.lid.."AddSubType") self.TaskSubType=Type return self end function PLAYERTASK:SetCanSmoke(OnOff) self:T(self.lid.."AddSSetCanSmokeubType") self.CanSmoke=OnOff return self end function PLAYERTASK:SetShowThreatDetails(OnOff) self:T(self.lid.."SetShowThreatDetails") self.ShowThreatDetails=OnOff return self end function PLAYERTASK:GetSubType() self:T(self.lid.."GetSubType") return self.TaskSubType end function PLAYERTASK:GetFreetext() self:T(self.lid.."GetFreetext") return self.Freetext or self.FreetextTTS or"No Details" end function PLAYERTASK:AddFreetextTTS(TextTTS) self:T(self.lid.."AddFreetextTTS") self.FreetextTTS=TextTTS return self end function PLAYERTASK:GetFreetextTTS() self:T(self.lid.."GetFreetextTTS") return self.FreetextTTS or self.Freetext or"No Details" end function PLAYERTASK:SetMenuName(Text) self:T(self.lid.."SetMenuName") self.Target.name=Text return self end function PLAYERTASK:AddStaticObjectSuccessCondition() local task=self task:AddConditionSuccess( function(target) if target==nil then return false end local isDead=false if target:IsInstanceOf("STATIC") or target:IsInstanceOf("SCENERY") or target:IsInstanceOf("SET_SCENERY")then isDead=(not target)or target:GetLife()<1 or target:GetLife()<0.2*target:GetLife0() elseif target:IsInstanceOf("SET_STATIC")then local deadCount=0 target:ForEachStatic(function(static) if static:GetLife()<1 or static:GetLife()<0.2*static:GetLife0()then deadCount=deadCount+1 end end) if deadCount==target:Count()then isDead=true end end return isDead end,task:GetTarget() ) return self end function PLAYERTASK:AddOpsZoneCaptureSuccessCondition(CaptureSquadGroupNamePrefix,Coalition,CheckClientInZone) local task=self task:AddConditionSuccess( function(target) if target:IsInstanceOf("OPSZONE")then return task:_CheckCaptureOpsZoneSuccess(target,CaptureSquadGroupNamePrefix,Coalition,CheckClientInZone or true) elseif target:IsInstanceOf("SET_OPSZONE")then local successes=0 local isClientInZone=false target:ForEachZone(function(opszone) if task:_CheckCaptureOpsZoneSuccess(opszone,CaptureSquadGroupNamePrefix,Coalition,CheckClientInZone or true)then successes=successes+1 end for _,client in ipairs(task:GetClientObjects())do local clientCoord=client:GetCoordinate() if opszone.zone:IsCoordinateInZone(clientCoord)then isClientInZone=true break end end end) return successes==target:Count()and isClientInZone end return false end,task:GetTarget() ) return self end function PLAYERTASK:AddReconSuccessCondition(MinDistance) local task=self task:AddConditionSuccess( function(target) local targetLocation=target:GetCoordinate() local minD=MinDistance or UTILS.NMToMeters(5) for _,client in ipairs(task:GetClientObjects())do local clientCoord=client:GetCoordinate() local distance=clientCoord:Get2DDistance(targetLocation) local isLos=land.isVisible(clientCoord:GetVec3(),targetLocation:GetVec3()) if distance0 and timer.getTime()-task.StartTime>TimeLimit end) return self end function PLAYERTASK:AddNextTaskAfterSuccess(Task) self:T(self.lid.."AddNextTaskAfterSuccess") table.insert(self.NextTaskSuccess,Task) return self end function PLAYERTASK:AddNextTaskAfterFailure(Task) self:T(self.lid.."AddNextTaskAfterFailure") table.insert(self.NextTaskFailure,Task) return self end function PLAYERTASK:IsDone() self:T(self.lid.."IsDone?") local IsDone=false local state=self:GetState() if state=="Done"or state=="Stopped"then IsDone=true end return IsDone end function PLAYERTASK:IsNotDone() self:T(self.lid.."IsNotDone?") local IsNotDone=not self:IsDone() return IsNotDone end function PLAYERTASK:HasClients() self:T(self.lid.."HasClients?") local hasclients=self:CountClients()>0 and true or false return hasclients end function PLAYERTASK:GetClients() self:T(self.lid.."GetClients") local clientlist=self.Clients:GetIDStackSorted()or{} local count=self.Clients:Count() return clientlist,count end function PLAYERTASK:GetClientObjects() self:T(self.lid.."GetClientObjects") local clientlist=self.Clients:GetDataTable()or{} local count=self.Clients:Count() return clientlist,count end function PLAYERTASK:CountClients() self:T(self.lid.."CountClients") return self.Clients:Count() end function PLAYERTASK:HasPlayerName(Name) self:T(self.lid.."HasPlayerName?") return self.Clients:HasUniqueID(Name) end function PLAYERTASK:AddClient(Client) self:T(self.lid.."AddClient") local name=Client:GetPlayerName() if not self.Clients:HasUniqueID(name)then self.Clients:Push(Client,name) self:__ClientAdded(-2,Client) end if self.TaskController and self.TaskController.Scoring then self.TaskController.Scoring:_AddPlayerFromUnit(Client) end return self end function PLAYERTASK:RemoveClient(Client,Name) self:T(self.lid.."RemoveClient") local name=Name or Client:GetPlayerName() if self.Clients:HasUniqueID(name)then self.Clients:PullByID(name) if self.verbose then self.Clients:Flush() end self:__ClientRemoved(-2,Client) if self.Clients:Count()==0 then self:__Failed(-1) end end return self end function PLAYERTASK:ClientAbort(Client) self:T(self.lid.."ClientAbort") if Client and Client:IsAlive()then self:RemoveClient(Client) self:__ClientAborted(-1,Client) return self else if self.Clients:Count()==0 then self:__Failed(-1) end end return self end function PLAYERTASK:MarkTargetOnF10Map(Text,Coalition,ReadOnly) self:T(self.lid.."MarkTargetOnF10Map") if self.Target then local coordinate=self.Target:GetCoordinate() if coordinate then if self.TargetMarker then self.TargetMarker:Remove() end local text=Text or("Target of "..self.lid) self.TargetMarker=MARKER:New(coordinate,text) if ReadOnly then self.TargetMarker:ReadOnly() end if Coalition then self.TargetMarker:ToCoalition(Coalition) else self.TargetMarker:ToAll() end end end return self end function PLAYERTASK:SmokeTarget(Color) self:T(self.lid.."SmokeTarget") local color=Color or SMOKECOLOR.Red if not self.lastsmoketime then self.lastsmoketime=0 end local TDiff=timer.getAbsTime()-self.lastsmoketime if self.Target and TDiff>299 then local coordinate=self.Target:GetAverageCoordinate() if coordinate then coordinate:Smoke(color) self.lastsmoketime=timer.getAbsTime() end end return self end function PLAYERTASK:FlareTarget(Color) self:T(self.lid.."SmokeTarget") local color=Color or FLARECOLOR.Red if self.Target then local coordinate=self.Target:GetAverageCoordinate() if coordinate then coordinate:Flare(color,0) end end return self end function PLAYERTASK:IlluminateTarget(Power,Height) self:T(self.lid.."IlluminateTarget") local Power=Power or 1000 local Height=Height or 150 if self.Target then local coordinate=self.Target:GetAverageCoordinate() if coordinate then local bcoord=COORDINATE:NewFromVec2(coordinate:GetVec2(),Height) bcoord:IlluminationBomb(Power) end end return self end function PLAYERTASK:AddConditionSuccess(ConditionFunction,...) local condition={} condition.func=ConditionFunction condition.arg={} if arg then condition.arg=arg end table.insert(self.conditionSuccess,condition) return self end function PLAYERTASK:AddConditionFailure(ConditionFunction,...) local condition={} condition.func=ConditionFunction condition.arg={} if arg then condition.arg=arg end table.insert(self.conditionFailure,condition) return self end function PLAYERTASK:_EvalConditionsAny(Conditions) for _,_condition in pairs(Conditions or{})do local condition=_condition local istrue=condition.func(unpack(condition.arg)) if istrue then return true end end return false end function PLAYERTASK:onafterStatus(From,Event,To) self:T({From,Event,To}) self:T(self.lid.."onafterStatus") local status=self:GetState() if status=="Stopped"then return self end if self.TargetMarker then local coordinate=self.Target:GetCoordinate() self.TargetMarker:UpdateCoordinate(coordinate,0.5) end local targetdead=false if self.Type~=AUFTRAG.Type.CTLD and self.Type~=AUFTRAG.Type.CSAR then if self.Target:IsDead()or self.Target:IsDestroyed()or self.Target:CountTargets()==0 then targetdead=true self:__Success(-2) status="Success" return self end end local clientsalive=false if status=="Executing"then local ClientTable=self.Clients:GetDataTable() for _,_client in pairs(ClientTable)do local client=_client if client:IsAlive()then clientsalive=true end end if status=="Executing"and(not clientsalive)and(not targetdead)then self:__Failed(-2) status="Failed" end end if status~="Done"and status~="Stopped"then local successCondition=self:_EvalConditionsAny(self.conditionSuccess) local failureCondition=self:_EvalConditionsAny(self.conditionFailure) if failureCondition and status~="Failed"then self:__Failed(-2) status="Failed" elseif successCondition then self:__Success(-2) status="Success" end if status~="Failed"and status~="Success"then local targetcount=self.Target:CountTargets() if targetcount0 then for _,_client in pairs(clients)do self.TaskController.Scoring:AddGoalScore(_client,self.Type,nil,10) end end end self.TaskController:__TaskProgress(-1,self,TargetCount) end return self end function PLAYERTASK:onafterPlanned(From,Event,To) self:T({From,Event,To}) self.timestamp=timer.getAbsTime() return self end function PLAYERTASK:onafterRequested(From,Event,To) self:T({From,Event,To}) self.timestamp=timer.getAbsTime() return self end function PLAYERTASK:onafterExecuting(From,Event,To) self:T({From,Event,To}) self.timestamp=timer.getAbsTime() return self end function PLAYERTASK:onafterStop(From,Event,To) self:T({From,Event,To}) self.timestamp=timer.getAbsTime() return self end function PLAYERTASK:onafterClientAdded(From,Event,To,Client) self:T({From,Event,To}) if Client and self.verbose then local text=string.format("Player %s joined task %03d!",Client:GetPlayerName()or"Generic",self.PlayerTaskNr) self:T(self.lid..text) end self.timestamp=timer.getAbsTime() return self end function PLAYERTASK:onafterDone(From,Event,To) self:T({From,Event,To}) if self.TaskController then self.TaskController:__TaskDone(-1,self) end self.timestamp=timer.getAbsTime() self:__Stop(-1) return self end function PLAYERTASK:onafterCancel(From,Event,To,Silent) self:T({From,Event,To}) if self.TaskController then self.TaskController:__TaskCancelled(-1,self,Silent) end self.timestamp=timer.getAbsTime() self.FinalState="Cancelled" self:__Done(-1) return self end function PLAYERTASK:onafterSuccess(From,Event,To) self:T({From,Event,To}) if self.TaskController then self.TaskController:__TaskSuccess(-1,self) end if self.TargetMarker then self.TargetMarker:Remove() end if self.TaskController and self.TaskController.Scoring then local clients,count=self:GetClientObjects() if count>0 then for _,_client in pairs(clients)do local auftrag=self:GetSubType() self.TaskController.Scoring:AddGoalScore(_client,self.Type,nil,self.TaskController.Scores[self.Type]) end end end self.timestamp=timer.getAbsTime() self.FinalState="Success" self:__Done(-1) return self end function PLAYERTASK:onafterFailed(From,Event,To) self:T({From,Event,To}) self.repeats=self.repeats+1 if self.Repeat and(self.repeats<=self.RepeatNo)then if self.TaskController then self.TaskController:__TaskRepeatOnFailed(-1,self) end self:__Planned(-1) return self else if self.TargetMarker then self.TargetMarker:Remove() end self.FinalState="Failed" if self.TaskController then self.TaskController:__TaskFailed(-1,self) end self:__Done(-1.5) end if self.TaskController.Scoring then local clients,count=self:GetClientObjects() if count>0 then for _,_client in pairs(clients)do local auftrag=self:GetSubType() self.TaskController.Scoring:AddGoalScore(_client,self.Type,nil,-self.TaskController.Scores[self.Type]) end end end self.timestamp=timer.getAbsTime() return self end end do PLAYERTASKCONTROLLER={ ClassName="PLAYERTASKCONTROLLER", verbose=false, lid=nil, TargetQueue=nil, ClientSet=nil, UseGroupNames=true, PlayerMenu={}, usecluster=false, MenuName=nil, ClusterRadius=0.5, NoScreenOutput=false, TargetRadius=500, UseWhiteList=false, WhiteList={}, gettext=nil, locale="en", precisionbombing=false, taskinfomenu=false, activehasinfomenu=false, MarkerReadOnly=false, customcallsigns={}, ShortCallsign=true, Keepnumber=false, CallsignTranslations=nil, PlayerFlashMenu={}, PlayerJoinMenu={}, PlayerInfoMenu={}, PlayerMenuTag={}, noflaresmokemenu=false, illumenu=false, TransmitOnlyWithPlayers=true, buddylasing=false, PlayerRecce=nil, Coalition=nil, MenuParent=nil, ShowMagnetic=true, InfoHasLLDDM=false, InfoHasCoordinate=false, UseTypeNames=false, Scoring=nil, MenuNoTask=nil, InformationMenu=false, TaskInfoDuration=30, TaskPersistance={}, TaskPersistanceSwitch=false, TaskPersistancePath=nil, TaskPersistanceFilename=nil, TasksPersistable={}, SceneryExplosivesAmount=300, } PLAYERTASKCONTROLLER.Type={ A2A="Air-To-Air", A2G="Air-To-Ground", A2S="Air-To-Sea", A2GS="Air-To-Ground-Sea", } AUFTRAG.Type.PRECISIONBOMBING="Precision Bombing" AUFTRAG.Type.CTLD="Combat Transport" AUFTRAG.Type.CSAR="Combat Rescue" AUFTRAG.Type.CONQUER="Conquer" PLAYERTASKCONTROLLER.Scores={ [AUFTRAG.Type.PRECISIONBOMBING]=100, [AUFTRAG.Type.CTLD]=100, [AUFTRAG.Type.CSAR]=100, [AUFTRAG.Type.INTERCEPT]=100, [AUFTRAG.Type.ANTISHIP]=100, [AUFTRAG.Type.CAS]=100, [AUFTRAG.Type.BAI]=100, [AUFTRAG.Type.SEAD]=100, [AUFTRAG.Type.BOMBING]=100, [AUFTRAG.Type.BOMBRUNWAY]=100, [AUFTRAG.Type.CONQUER]=100, [AUFTRAG.Type.RECON]=100, [AUFTRAG.Type.ESCORT]=100, [AUFTRAG.Type.CAP]=100, [AUFTRAG.Type.CAPTUREZONE]=100, } PLAYERTASKCONTROLLER.TasksPersistable={ [AUFTRAG.Type.PRECISIONBOMBING]=true, [AUFTRAG.Type.BOMBING]=true, [AUFTRAG.Type.ARTY]=true, [AUFTRAG.Type.SEAD]=true, } PLAYERTASKCONTROLLER.SeadAttributes={ SAM=GROUP.Attribute.GROUND_SAM, AAA=GROUP.Attribute.GROUND_AAA, EWR=GROUP.Attribute.GROUND_EWR, } PLAYERTASKCONTROLLER.Messages={ EN={ TASKABORT="Task aborted!", NOACTIVETASK="No active task!", FREQUENCIES="frequencies ", FREQUENCY="frequency %.3f", BROADCAST="%s, %s, switch to %s for task assignment!", CASTTS="close air support", SEADTTS="suppress air defense", BOMBTTS="bombing", PRECBOMBTTS="precision bombing", BAITTS="battle field air interdiction", ANTISHIPTTS="anti-ship", INTERCEPTTS="intercept", BOMBRUNWAYTTS="bomb runway", HAVEACTIVETASK="You already have one active task! Complete it first!", PILOTJOINEDTASK="%s, %s. You have been assigned %s task %03d", TASKNAME="%s Task ID %03d", TASKNAMETTS="%s Task ID %03d", THREATHIGH="high", THREATMEDIUM="medium", THREATLOW="low", THREATTEXT="%s\nThreat: %s\nTargets left: %d\nCoord: %s", NOTHREATTEXT="%s\nNo target information available.", ELEVATION="\nTarget Elevation: %s %s", METER="meter", FEET="feet", THREATTEXTTTS="%s, %s. Target information for %s. Threat level %s. Targets left %d. Target location %s.", NOTHREATTEXTTTS="%s, %s. No target information available.", MARKTASK="%s, %s, copy, task %03d location marked on map!", SMOKETASK="%s, %s, copy, task %03d location smoked!", NOSMOKETASK="%s, %s, negative, task %03d location cannot be smoked!", FLARETASK="%s, %s, copy, task %03d location illuminated!", ABORTTASK="All stations, %s, %s has aborted %s task %03d!", UNKNOWN="Unknown", MENUTASKING=" Tasking ", MENUACTIVE="Active Task", MENUINFO="Info", MENUMARK="Mark on map", MENUSMOKE="Smoke", MENUFLARE="Flare", MENUILLU="Illuminate", MENUABORT="Abort", MENUJOIN="Join Task", MENUTASKINFO="Task Info", MENUTASKNO="TaskNo", MENUNOTASKS="Currently no tasks available.", TASKCANCELLED="Task #%03d %s is cancelled!", TASKCANCELLEDTTS="%s, task %03d %s is cancelled!", TASKSUCCESS="Task #%03d %s completed successfully!", TASKSUCCESSTTS="%s, task %03d %s completed successfully!", TASKFAILED="Task #%03d %s was a failure!", TASKFAILEDTTS="%s, task %03d %s was a failure!", TASKFAILEDREPLAN="Task #%03d %s available for reassignment!", TASKFAILEDREPLANTTS="%s, task %03d %s vailable for reassignment!", TASKADDED="%s has a new %s task available!", PILOTS="\nPilot(s): ", PILOTSTTS=". Pilot(s): ", YES="Yes", NO="No", NONE="None", POINTEROVERTARGET="%s, %s, pointer in reach for task %03d, lasing!", POINTERTARGETREPORT="\nPointer in reach: %s\nLasing: %s", RECCETARGETREPORT="\nRecce %s in reach: %s\nLasing: %s", POINTERTARGETLASINGTTS=". Pointer in reach and lasing.", TARGET="Target", FLASHON="%s - Flashing directions is now ON!", FLASHOFF="%s - Flashing directions is now OFF!", FLASHMENU="Flash Directions Switch", BRIEFING="Briefing", TARGETLOCATION="Target location", COORDINATE="Coordinate", INFANTRY="Infantry", TECHNICAL="Technical", ARTILLERY="Artillery", TANKS="Tanks", AIRDEFENSE="Airdefense", SAM="SAM", GROUP="Group", UNARMEDSHIP="Merchant", LIGHTARMEDSHIP="Light Boat", CORVETTE="Corvette", FRIGATE="Frigate", CRUISER="Cruiser", DESTROYER="Destroyer", CARRIER="Aircraft Carrier", RADIOS="Radios", INTERCEPTCOURSE="Intercept course", }, DE={ TASKABORT="Auftrag abgebrochen!", NOACTIVETASK="Kein aktiver Auftrag!", FREQUENCIES="Frequenzen ", FREQUENCY="Frequenz %.3f", BROADCAST="%s, %s, Radio %s für Aufgabenzuteilung!", CASTTS="Nahbereichsunterstützung", SEADTTS="Luftabwehr ausschalten", BOMBTTS="Bombardieren", PRECBOMBTTS="Präzisionsbombardieren", BAITTS="Luftunterstützung", ANTISHIPTTS="Anti-Schiff", INTERCEPTTS="Abfangen", BOMBRUNWAYTTS="Startbahn Bombardieren", HAVEACTIVETASK="Du hast einen aktiven Auftrag! Beende ihn zuerst!", PILOTJOINEDTASK="%s, %s hat Auftrag %s %03d angenommen", TASKNAME="%s Auftrag ID %03d", TASKNAMETTS="%s Auftrag ID %03d", THREATHIGH="hoch", THREATMEDIUM="mittel", THREATLOW="niedrig", THREATTEXT="%s\nGefahrstufe: %s\nZiele: %d\nKoord: %s", NOTHREATTEXT="%s\nKeine Zielinformation verfügbar.", ELEVATION="\nZiel Höhe: %s %s", METER="Meter", FEET="Fuss", THREATTEXTTTS="%s, %s. Zielinformation zu %s. Gefahrstufe %s. Ziele %d. Zielposition %s.", NOTHREATTEXTTTS="%s, %s. Keine Zielinformation verfügbar.", MARKTASK="%s, %s, verstanden, Zielposition %03d auf der Karte markiert!", SMOKETASK="%s, %s, verstanden, Zielposition %03d mit Rauch markiert!", NOSMOKETASK="%s, %s, negativ, Zielposition %03d kann nicht markiert werden!", FLARETASK="%s, %s, verstanden, Zielposition %03d beleuchtet!", ABORTTASK="%s, an alle, %s hat Auftrag %s %03d abgebrochen!", UNKNOWN="Unbekannt", MENUTASKING=" Aufträge ", MENUACTIVE="Aktiver Auftrag", MENUINFO="Information", MENUMARK="Kartenmarkierung", MENUSMOKE="Rauchgranate", MENUFLARE="Leuchtgranate", MENUILLU="Feldbeleuchtung", MENUABORT="Abbrechen", MENUJOIN="Auftrag annehmen", MENUTASKINFO="Auftrag Briefing", MENUTASKNO="AuftragsNr", MENUNOTASKS="Momentan keine Aufträge verfügbar.", TASKCANCELLED="Auftrag #%03d %s wurde beendet!", TASKCANCELLEDTTS="%s, Auftrag %03d %s wurde beendet!", TASKSUCCESS="Auftrag #%03d %s erfolgreich!", TASKSUCCESSTTS="%s, Auftrag %03d %s erfolgreich!", TASKFAILED="Auftrag #%03d %s gescheitert!", TASKFAILEDTTS="%s, Auftrag %03d %s gescheitert!", TASKFAILEDREPLAN="Auftrag #%03d %s gescheitert! Neuplanung!", TASKFAILEDREPLANTTS="%s, Auftrag %03d %s gescheitert! Neuplanung!", TASKADDED="%s hat einen neuen Auftrag %s erstellt!", PILOTS="\nPilot(en): ", PILOTSTTS=". Pilot(en): ", YES="Ja", NO="Nein", NONE="Keine", POINTEROVERTARGET="%s, %s, Marker im Zielbereich für %03d, Laser an!", POINTERTARGETREPORT="\nMarker im Zielbereich: %s\nLaser an: %s", RECCETARGETREPORT="\nSpäher % im Zielbereich: %s\nLasing: %s", POINTERTARGETLASINGTTS=". Marker im Zielbereich, Laser is an.", TARGET="Ziel", FLASHON="%s - Richtungsangaben einblenden ist EIN!", FLASHOFF="%s - Richtungsangaben einblenden ist AUS!", FLASHMENU="Richtungsangaben Schalter", BRIEFING="Briefing", TARGETLOCATION="Zielposition", COORDINATE="Koordinate", INFANTRY="Infantrie", TECHNICAL="Technische", ARTILLERY="Artillerie", TANKS="Panzer", AIRDEFENSE="Flak", SAM="Luftabwehr", GROUP="Einheit", UNARMEDSHIP="Handelsschiff", LIGHTARMEDSHIP="Tender", CORVETTE="Korvette", FRIGATE="Fregatte", CRUISER="Kreuzer", DESTROYER="Zerstörer", CARRIER="Flugzeugträger", RADIOS="Frequenzen", INTERCEPTCOURSE="Abfangkurs", }, } PLAYERTASKCONTROLLER.version="0.1.73" function PLAYERTASKCONTROLLER:New(Name,Coalition,Type,ClientFilter) local self=BASE:Inherit(self,FSM:New()) self.Name=Name or"CentCom" self.Coalition=Coalition or coalition.side.BLUE self.CoalitionName=UTILS.GetCoalitionName(Coalition) self.Type=Type or PLAYERTASKCONTROLLER.Type.A2G self.usecluster=false if self.Type==PLAYERTASKCONTROLLER.Type.A2A then self.usecluster=true end self.ClusterRadius=0.5 self.TargetRadius=500 self.ClientFilter=ClientFilter self.TargetQueue=FIFO:New() self.TaskQueue=FIFO:New() self.TasksPerPlayer=FIFO:New() self.PrecisionTasks=FIFO:New() self.LasingDroneSet=SET_OPSGROUP:New() self.FlashPlayer={} self.AllowFlash=false self.lasttaskcount=0 self.taskinfomenu=false self.activehasinfomenu=false self.MenuName=nil self.menuitemlimit=6 self.holdmenutime=30 self.MarkerReadOnly=false self.repeatonfailed=true self.repeattimes=5 self.UseGroupNames=true self.customcallsigns={} self.ShortCallsign=true self.Keepnumber=false self.CallsignTranslations=nil self.noflaresmokemenu=false self.illumenu=false self.ShowMagnetic=true self.UseTypeNames=false self.InformationMenu=false self.TaskInfoDuration=30 self.IsClientSet=false if ClientFilter and type(ClientFilter)=="table"and ClientFilter.ClassName and ClientFilter.ClassName=="SET_CLIENT"then self.ClientSet=ClientFilter self.IsClientSet=true end if ClientFilter and not self.IsClientSet then self.ClientSet=SET_CLIENT:New():FilterCoalitions(string.lower(self.CoalitionName)):FilterActive(true):FilterPrefixes(ClientFilter):FilterStart() elseif not self.IsClientSet then self.ClientSet=SET_CLIENT:New():FilterCoalitions(string.lower(self.CoalitionName)):FilterActive(true):FilterStart() end self.ActiveClientSet=SET_CLIENT:New() self.lid=string.format("PlayerTaskController %s %s | ",self.Name,tostring(self.Type)) self:_InitLocalization() self:SetStartState("Stopped") self:AddTransition("Stopped","Start","Running") self:AddTransition("*","Status","*") self:AddTransition("*","TaskAdded","*") self:AddTransition("*","TaskDone","*") self:AddTransition("*","TaskCancelled","*") self:AddTransition("*","TaskSuccess","*") self:AddTransition("*","TaskFailed","*") self:AddTransition("*","TaskProgress","*") self:AddTransition("*","TaskTargetSmoked","*") self:AddTransition("*","TaskTargetFlared","*") self:AddTransition("*","TaskTargetIlluminated","*") self:AddTransition("*","TaskRepeatOnFailed","*") self:AddTransition("*","PlayerJoinedTask","*") self:AddTransition("*","PlayerAbortedTask","*") self:AddTransition("*","Stop","Stopped") self:__Start(2) local starttime=math.random(10,15) self:__Status(starttime) self:I(self.lid..self.version.." Started.") return self end function PLAYERTASKCONTROLLER:EnableTaskPersistance(Path,Filename,KgsOfTNT) self.TaskPersistanceSwitch=true self.TaskPersistancePath=Path self.TaskPersistanceFilename=Filename self.SceneryExplosivesAmount=KgsOfTNT or 300 return self end function PLAYERTASKCONTROLLER:DisableTaskPersistance() self.TaskPersistanceSwitch=false self.TaskPersistancePath=nil self.TaskPersistanceFilename=nil return self end function PLAYERTASKCONTROLLER:EnableScoring(Scoring) self.Scoring=Scoring or SCORING:New(self.Name) return self end function PLAYERTASKCONTROLLER:DisableScoring() self.Scoring=nil return self end function PLAYERTASKCONTROLLER:_InitLocalization() self:T(self.lid.."_InitLocalization") self.gettext=TEXTANDSOUND:New("PLAYERTASKCONTROLLER","en") self.locale="en" for locale,table in pairs(self.Messages)do local Locale=string.lower(tostring(locale)) self:T("**** Adding locale: "..Locale) for ID,Text in pairs(table)do self:T(string.format('Adding ID %s',tostring(ID))) self.gettext:AddEntry(Locale,tostring(ID),Text) end end return self end function PLAYERTASKCONTROLLER:SetEnableUseTypeNames() self:T(self.lid.."SetEnableUseTypeNames") self.UseTypeNames=true return self end function PLAYERTASKCONTROLLER:SetDisableUseTypeNames() self:T(self.lid.."SetDisableUseTypeNames") self.UseTypeNames=false return self end function PLAYERTASKCONTROLLER:SetAllowFlashDirection(OnOff) self:T(self.lid.."SetAllowFlashDirection") self.AllowFlash=OnOff return self end function PLAYERTASKCONTROLLER:SetShowRadioInfoMenu(OnOff) self:T(self.lid.."SetAllowRadioInfoMenu") self.InformationMenu=OnOff return self end function PLAYERTASKCONTROLLER:SetDisableSmokeFlareTask() self:T(self.lid.."SetDisableSmokeFlareTask") self.noflaresmokemenu=true return self end function PLAYERTASKCONTROLLER:SetTransmitOnlyWithPlayers(Switch) self.TransmitOnlyWithPlayers=Switch if self.SRSQueue then self.SRSQueue:SetTransmitOnlyWithPlayers(Switch) end return self end function PLAYERTASKCONTROLLER:SetEnableSmokeFlareTask() self:T(self.lid.."SetEnableSmokeFlareTask") self.noflaresmokemenu=false return self end function PLAYERTASKCONTROLLER:SetEnableIlluminateTask() self:T(self.lid.."SetEnableSmokeFlareTask") self.illumenu=true return self end function PLAYERTASKCONTROLLER:SetDisableIlluminateTask() self:T(self.lid.."SetDisableIlluminateTask") self.illumenu=false return self end function PLAYERTASKCONTROLLER:SetInfoShowsCoordinate(OnOff,LLDDM) self:T(self.lid.."SetInfoShowsCoordinate") self.InfoHasCoordinate=OnOff self.InfoHasLLDDM=LLDDM return self end function PLAYERTASKCONTROLLER:SetCallSignOptions(ShortCallsign,Keepnumber,CallsignTranslations,CallsignCustomFunc,...) if not ShortCallsign or ShortCallsign==false then self.ShortCallsign=false else self.ShortCallsign=true end self.Keepnumber=Keepnumber or false self.CallsignTranslations=CallsignTranslations self.CallsignCustomFunc=CallsignCustomFunc self.CallsignCustomArgs=arg or{} return self end function PLAYERTASKCONTROLLER:_GetTextForSpeech(text) self:T(self.lid.."_GetTextForSpeech") text=string.gsub(text,"%d","%1 ") text=string.gsub(text,"^%s*","") text=string.gsub(text,"%s*$","") text=string.gsub(text," "," ") return text end function PLAYERTASKCONTROLLER:SetTaskRepetition(OnOff,Repeats) self:T(self.lid.."SetTaskRepetition") if OnOff then self.repeatonfailed=true self.repeattimes=Repeats or 5 else self.repeatonfailed=false self.repeattimes=Repeats or 5 end return self end function PLAYERTASKCONTROLLER:SetBriefingDuration(Seconds) self:T(self.lid.."SetBriefingDuration") self.TaskInfoDuration=Seconds or 30 return self end function PLAYERTASKCONTROLLER:_SendMessageToClients(Text,Seconds) self:T(self.lid.."_SendMessageToClients") local seconds=Seconds or 10 self.ClientSet:ForEachClient( function(Client) if Client~=nil and Client:IsActive()then local m=MESSAGE:New(Text,seconds,"Tasking"):ToClient(Client) end end ) return self end function PLAYERTASKCONTROLLER:EnablePrecisionBombing(FlightGroup,LaserCode,HoldingPoint,Alt,Speed,MaxTravelDist) self:T(self.lid.."EnablePrecisionBombing") if not self.LasingDroneSet then self.LasingDroneSet=SET_OPSGROUP:New() end local LasingDrone if FlightGroup then if FlightGroup.ClassName and(FlightGroup.ClassName=="FLIGHTGROUP"or FlightGroup.ClassName=="ARMYGROUP")then LasingDrone=FlightGroup self.precisionbombing=true LasingDrone.playertask={} LasingDrone.playertask.id=0 LasingDrone.playertask.busy=false LasingDrone.playertask.lasercode=LaserCode or 1688 LasingDrone:SetLaser(LasingDrone.playertask.lasercode) LasingDrone.playertask.template=LasingDrone:_GetTemplate(true) LasingDrone.playertask.alt=Alt or 10000 LasingDrone.playertask.speed=Speed or 120 LasingDrone.playertask.maxtravel=UTILS.NMToMeters(MaxTravelDist or 50) if LasingDrone:IsFlightgroup()then local BullsCoordinate=COORDINATE:NewFromVec3(coalition.getMainRefPoint(self.Coalition)) if HoldingPoint then BullsCoordinate=HoldingPoint end local Orbit=AUFTRAG:NewORBIT_CIRCLE(BullsCoordinate,Alt,Speed) Orbit:SetMissionAltitude(Alt) LasingDrone:AddMission(Orbit) elseif LasingDrone:IsArmygroup()then local BullsCoordinate=COORDINATE:NewFromVec3(coalition.getMainRefPoint(self.Coalition)) if HoldingPoint then BullsCoordinate=HoldingPoint end local Orbit=AUFTRAG:NewONGUARD(BullsCoordinate) LasingDrone:AddMission(Orbit) end self.LasingDroneSet:AddObject(FlightGroup) elseif FlightGroup.ClassName and(FlightGroup.ClassName=="SET_OPSGROUP")then FlightGroup:ForEachGroup( function(group) self:EnablePrecisionBombing(group,LaserCode,HoldingPoint,Alt,Speed,MaxTravelDist) end ) else self:E(self.lid.."No OPSGROUP/SET_OPSGROUP object passed or object is not alive!") end else self.autolase=nil self.precisionbombing=false end return self end function PLAYERTASKCONTROLLER:AddPrecisionBombingOpsGroup(FlightGroup,LaserCode,HoldingPoint,Alt,Speed) self:EnablePrecisionBombing(FlightGroup,LaserCode,HoldingPoint,Alt,Speed) return self end function PLAYERTASKCONTROLLER:EnableBuddyLasing(Recce) self:T(self.lid.."EnableBuddyLasing") self.buddylasing=true self.PlayerRecce=Recce return self end function PLAYERTASKCONTROLLER:DisableBuddyLasing() self:T(self.lid.."DisableBuddyLasing") self.buddylasing=false return self end function PLAYERTASKCONTROLLER:EnableMarkerOps(Tag) self:T(self.lid.."EnableMarkerOps") local tag=Tag or"TASK" local MarkerOps=MARKEROPS_BASE:New(tag,{"Name","Text"},true) local function Handler(Keywords,Coord,Text) if self.verbose then local m=MESSAGE:New(string.format("Target added from marker at: %s",Coord:ToStringA2G(nil,nil,self.ShowMagnetic)),15,"INFO"):ToAll() local m=MESSAGE:New(string.format("Text: %s",Text),15,"INFO"):ToAll() end local menuname=string.match(Text,"Name=(.+),") local freetext=string.match(Text,"Text=(.+)") if menuname then Coord.menuname=menuname if freetext then Coord.freetext=freetext end end self:AddTarget(Coord) end function MarkerOps:OnAfterMarkAdded(From,Event,To,Text,Keywords,Coord) Handler(Keywords,Coord,Text) end function MarkerOps:OnAfterMarkChanged(From,Event,To,Text,Keywords,Coord) Handler(Keywords,Coord,Text) end self.MarkerOps=MarkerOps return self end function PLAYERTASKCONTROLLER:_GetPlayerName(Client) self:T(self.lid.."_GetPlayerName") local playername=Client:GetPlayerName() local ttsplayername=nil if not self.customcallsigns[playername]then local playergroup=Client:GetGroup() if playergroup~=nil then ttsplayername=playergroup:GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs) local newplayername=self:_GetTextForSpeech(ttsplayername) self.customcallsigns[playername]=newplayername ttsplayername=newplayername end else ttsplayername=self.customcallsigns[playername] end return playername,ttsplayername end function PLAYERTASKCONTROLLER:DisablePrecisionBombing(FlightGroup,LaserCode) self:T(self.lid.."DisablePrecisionBombing") self.autolase=nil self.precisionbombing=false return self end function PLAYERTASKCONTROLLER:EnableTaskInfoMenu() self:T(self.lid.."EnableTaskInfoMenu") self.taskinfomenu=true return self end function PLAYERTASKCONTROLLER:DisableTaskInfoMenu() self:T(self.lid.."DisableTaskInfoMenu") self.taskinfomenu=false return self end function PLAYERTASKCONTROLLER:SetMenuOptions(InfoMenu,ItemLimit,HoldTime) self:T(self.lid.."SetMenuOptions") self.activehasinfomenu=InfoMenu or false if self.activehasinfomenu then self:EnableTaskInfoMenu() end self.menuitemlimit=ItemLimit+1 or 6 self.holdmenutime=HoldTime or 30 return self end function PLAYERTASKCONTROLLER:SetMarkerReadOnly() self:T(self.lid.."SetMarkerReadOnly") self.MarkerReadOnly=true return self end function PLAYERTASKCONTROLLER:SetMarkerDeleteable() self:T(self.lid.."SetMarkerDeleteable") self.MarkerReadOnly=false return self end function PLAYERTASKCONTROLLER:_EventHandler(EventData) self:T(self.lid.."_EventHandler: "..EventData.id) if EventData.id==EVENTS.UnitLost or EventData.id==EVENTS.PlayerLeaveUnit or EventData.id==EVENTS.Ejection or EventData.id==EVENTS.Crash or EventData.id==EVENTS.PilotDead then if EventData.IniPlayerName then self:T(self.lid.."Event for player: "..EventData.IniPlayerName) local text="" if self.TasksPerPlayer:HasUniqueID(EventData.IniPlayerName)then local task=self.TasksPerPlayer:PullByID(EventData.IniPlayerName) local Client=_DATABASE:FindClient(EventData.IniPlayerName) if Client then task:RemoveClient(Client) text=self.gettext:GetEntry("TASKABORT",self.locale) self.ActiveTaskMenuTemplate:ResetMenu(Client) self.JoinTaskMenuTemplate:ResetMenu(Client) else task:RemoveClient(nil,EventData.IniPlayerName) text=self.gettext:GetEntry("TASKABORT",self.locale) end else text=self.gettext:GetEntry("NOACTIVETASK",self.locale) end self:T(self.lid..text) end elseif EventData.id==EVENTS.PlayerEnterAircraft and EventData.IniCoalition==self.Coalition then if EventData.IniPlayerName and EventData.IniGroup then if self.IsClientSet and(not self.ClientSet:IsIncludeObject(CLIENT:FindByName(EventData.IniUnitName)))then self:T(self.lid.."Client not in SET: "..EventData.IniPlayerName) return self end self:T(self.lid.."Event for player: "..EventData.IniPlayerName) if self.UseSRS then local frequency=self.Frequency local freqtext="" if type(frequency)=="table"then freqtext=self.gettext:GetEntry("FREQUENCIES",self.locale) freqtext=freqtext..table.concat(frequency,", ") else local freqt=self.gettext:GetEntry("FREQUENCY",self.locale) freqtext=string.format(freqt,frequency) end local modulation=self.Modulation if type(modulation)=="table"then modulation=modulation[1]end modulation=UTILS.GetModulationName(modulation) local switchtext=self.gettext:GetEntry("BROADCAST",self.locale) local playername=EventData.IniPlayerName if EventData.IniGroup then if self.customcallsigns[playername]then self.customcallsigns[playername]=nil end playername=EventData.IniGroup:GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs) end playername=self:_GetTextForSpeech(playername) local text=string.format(switchtext,playername,self.MenuName or self.Name,freqtext) self.SRSQueue:NewTransmission(text,nil,self.SRS,timer.getAbsTime()+60,2,{EventData.IniGroup},text,30,self.BCFrequency,self.BCModulation) end if EventData.IniPlayerName then local player=_DATABASE:FindClient(EventData.IniUnitName) self:_SwitchMenuForClient(player,"Info") end end end return self end function PLAYERTASKCONTROLLER:SetLocale(Locale) self:T(self.lid.."SetLocale") self.locale=Locale or"en" return self end function PLAYERTASKCONTROLLER:SuppressScreenOutput(OnOff) self:T(self.lid.."SuppressScreenOutput") self.NoScreenOutput=OnOff or false return self end function PLAYERTASKCONTROLLER:SetTargetRadius(Radius) self:T(self.lid.."SetTargetRadius") self.TargetRadius=Radius or 500 return self end function PLAYERTASKCONTROLLER:SetClusterRadius(Radius) self:T(self.lid.."SetClusterRadius") self.ClusterRadius=Radius or 0.5 self.usecluster=true return self end function PLAYERTASKCONTROLLER:CancelTask(Task,Silent) self:T(self.lid.."CancelTask") Task:__Cancel(-1,Silent) return self end function PLAYERTASKCONTROLLER:SwitchUseGroupNames(OnOff) self:T(self.lid.."SwitchUseGroupNames") if OnOff then self.UseGroupNames=true else self.UseGroupNames=false end return self end function PLAYERTASKCONTROLLER:SwitchMagenticAngles(OnOff) self:T(self.lid.."SwitchMagenticAngles") if OnOff then self.ShowMagnetic=true else self.ShowMagnetic=false end return self end function PLAYERTASKCONTROLLER:_GetAvailableTaskTypes() self:T(self.lid.."_GetAvailableTaskTypes") local tasktypes={} self.TaskQueue:ForEach( function(Task) local task=Task local type=Task.Type tasktypes[type]={} end ) return tasktypes end function PLAYERTASKCONTROLLER:_GetTasksPerType() self:T(self.lid.."_GetTasksPerType") local tasktypes=self:_GetAvailableTaskTypes() local datatable=self.TaskQueue:GetDataTable() local threattable={} for _,_task in pairs(datatable)do local task=_task local threat=task.Target:GetThreatLevelMax() if not task:IsDone()then threattable[#threattable+1]={task=task,threat=threat} end end table.sort(threattable,function(k1,k2)return k1.threat>k2.threat end) for _id,_data in pairs(threattable)do local threat=_data.threat local task=_data.task local type=task.Type local name=task.Target:GetName() if not task:IsDone()then table.insert(tasktypes[type],task) end end return tasktypes end function PLAYERTASKCONTROLLER:_CheckTargetQueue() self:T(self.lid.."_CheckTargetQueue") if self.TargetQueue:Count()>0 then local object=self.TargetQueue:Pull() local target=TARGET:New(object) if object.menuname then target.menuname=object.menuname if object.freetext then target.freetext=object.freetext end end if object:IsInstanceOf("UNIT")or object:IsInstanceOf("GROUP")then if self.UseTypeNames and object:IsGround()then local threat=object:GetThreatLevel() local typekey="INFANTRY" if threat==0 or threat==2 then typekey="TECHNICAL" elseif threat==3 then typekey="ARTILLERY" elseif threat==4 or threat==5 then typekey="TANKS" elseif threat==6 or threat==7 then typekey="AIRDEFENSE" elseif threat>=8 then typekey="SAM" end local typename=self.gettext:GetEntry(typekey,self.locale) local gname=self.gettext:GetEntry("GROUP",self.locale) target.TypeName=string.format("%s %s",typename,gname) end if self.UseTypeNames and object:IsShip()then local threat=object:GetThreatLevel() local typekey="UNARMEDSHIP" if threat==1 then typekey="LIGHTARMEDSHIP" elseif threat==2 then typekey="CORVETTE" elseif threat==3 or threat==4 then typekey="FRIGATE" elseif threat==5 or threat==6 then typekey="CRUISER" elseif threat==7 or threat==8 then typekey="DESTROYER" elseif threat>=9 then typekey="CARRIER" end local typename=self.gettext:GetEntry(typekey,self.locale) target.TypeName=typename end end self:_AddTask(target) end return self end function PLAYERTASKCONTROLLER:_CheckTaskQueue() self:T(self.lid.."_CheckTaskQueue") if self.TaskQueue:Count()>0 then local tasks=self.TaskQueue:GetIDStack() for _id,_entry in pairs(tasks)do local data=_entry.data self:T("Looking at Task: "..data.PlayerTaskNr.." Type: "..data.Type.." State: "..data:GetState()) if data:GetState()=="Done"or data:GetState()=="Stopped"then local task=self.TaskQueue:ReadByID(_id) local clientsattask=task.Clients:GetIDStackSorted() for _,_id in pairs(clientsattask)do self:T("*****Removing player ".._id) self.TasksPerPlayer:PullByID(_id) end local clients=task:GetClientObjects() for _,client in pairs(clients)do self:_RemoveMenuEntriesForTask(task,client) end for _,client in pairs(clients)do self:_SwitchMenuForClient(client,"Info",5) end local nexttasks={} if task.FinalState=="Success"then nexttasks=task.NextTaskSuccess elseif task.FinalState=="Failed"then nexttasks=task.NextTaskFailure end local clientlist,count=task:GetClientObjects() if count>0 then for _,_client in pairs(clientlist)do local client=_client local group=client:GetGroup() for _,task in pairs(nexttasks)do self:_JoinTask(task,true,group,client) end end end local TNow=timer.getAbsTime() if TNow-task.timestamp>5 then self:_RemoveMenuEntriesForTask(task) local task=self.TaskQueue:PullByID(_id) task=nil end end end end return self end function PLAYERTASKCONTROLLER:_CheckPrecisionTasks() self:T(self.lid.."_CheckPrecisionTasks") self:T({count=self.PrecisionTasks:Count(),enabled=self.precisionbombing}) if self.PrecisionTasks:Count()>0 and self.precisionbombing then self.LasingDroneSet:ForEachGroup( function(LasingDrone) if not LasingDrone or LasingDrone:IsDead()then self:E(self.lid.."Lasing drone is dead ... creating a new one!") if LasingDrone then LasingDrone:_Respawn(1,nil,true) else end end end ) local function SelectDrone(coord) local selected=nil local mindist=math.huge local dist=math.huge self.LasingDroneSet:ForEachGroup( function(grp) if grp.playertask and(not grp.playertask.busy)then local gc=grp:GetCoordinate() if coord and gc then dist=coord:Get2DDistance(gc) end if dist2 and threat<7 then type=AUFTRAG.Type.PRECISIONBOMBING ttstype=self.gettext:GetEntry("PRECBOMBTTS",self.locale) end end elseif cat==TARGET.Category.NAVAL then type=AUFTRAG.Type.ANTISHIP ttstype=self.gettext:GetEntry("ANTISHIPTTS",self.locale) elseif cat==TARGET.Category.AIRCRAFT then type=AUFTRAG.Type.INTERCEPT ttstype=self.gettext:GetEntry("INTERCEPTTS",self.locale) elseif cat==TARGET.Category.AIRBASE then type=AUFTRAG.Type.BOMBRUNWAY ttstype=self.gettext:GetEntry("BOMBRUNWAYTTS",self.locale) elseif cat==TARGET.Category.COORDINATE or cat==TARGET.Category.ZONE then local zone=Target:GetObject() if cat==TARGET.Category.COORDINATE then zone=ZONE_RADIUS:New("TargetZone-"..math.random(1,10000),Target:GetVec2(),self.TargetRadius) end local enemies=self.CoalitionName=="Blue"and"red"or"blue" local enemysetg=SET_GROUP:New():FilterCoalitions(enemies):FilterCategoryGround():FilterActive(true):FilterZones({zone}):FilterOnce() local enemysets=SET_STATIC:New():FilterCoalitions(enemies):FilterZones({zone}):FilterOnce() local countg=enemysetg:Count() local counts=enemysets:Count() if countg>0 then if Target.menuname then enemysetg.menuname=Target.menuname if Target.freetext then enemysetg.freetext=Target.freetext end end self:AddTarget(enemysetg) end if counts>0 then if Target.menuname then enemysets.menuname=Target.menuname if Target.freetext then enemysets.freetext=Target.freetext end end self:AddTarget(enemysets) end return self end if self.UseWhiteList then if not self:_CheckTaskTypeAllowed(type)then return self end end if self.UseBlackList then if self:_CheckTaskTypeDisallowed(type)then return self end end local task=PLAYERTASK:New(type,Target,self.repeatonfailed,self.repeattimes,ttstype) if Target.menuname then task:SetMenuName(Target.menuname) if Target.freetext then task:AddFreetext(Target.freetext) end end task.coalition=self.Coalition task.TypeName=Target.TypeName if type==AUFTRAG.Type.BOMBRUNWAY then task:HandleEvent(EVENTS.Shot) function task:OnEventShot(EventData) local data=EventData local wcat=Object.getCategory(data.Weapon) local coord=data.IniUnit:GetCoordinate()or data.IniGroup:GetCoordinate() local vec2=coord:GetVec2()or{x=0,y=0} local coal=data.IniCoalition local afbzone=AIRBASE:FindByName(Target:GetName()):GetZone() local runways=AIRBASE:FindByName(Target:GetName()):GetRunways()or{} local inrunwayzone=false for _,_runway in pairs(runways)do local runway=_runway if runway.zone:IsVec2InZone(vec2)then inrunwayzone=true end end local inzone=afbzone:IsVec2InZone(vec2) if coal==task.coalition and(wcat==2 or wcat==3)and(inrunwayzone or inzone)then task:__Success(-20) end end end task:_SetController(self) self.TaskQueue:Push(task) self:__TaskAdded(10,task) return self end function PLAYERTASKCONTROLLER:AddPlayerTaskToQueue(PlayerTask,Silent,TaskFilter) self:T(self.lid.."AddPlayerTaskToQueue") if PlayerTask and PlayerTask.ClassName and PlayerTask.ClassName=="PLAYERTASK"then if TaskFilter then if self.UseWhiteList and(not self:_CheckTaskTypeAllowed(PlayerTask.Type))then return self end if self.UseBlackList and self:_CheckTaskTypeDisallowed(PlayerTask.Type)then return self end end PlayerTask:_SetController(self) PlayerTask:SetCoalition(self.Coalition) self.TaskQueue:Push(PlayerTask) self:__TaskAdded(10,PlayerTask,Silent) else self:E(self.lid.."***** NO valid PAYERTASK object sent!") end return self end function PLAYERTASKCONTROLLER:CanJoinTask(Task,Group,Client) return true end function PLAYERTASKCONTROLLER:_JoinTask(Task,Force,Group,Client) self:T({Force,Group,Client}) self:T(self.lid.."_JoinTask") if not self:CanJoinTask(Task,Group,Client)then return self end if not Task:CanJoinTask(Group,Client)then return self end local force=false if type(Force)=="boolean"then force=Force end local playername,ttsplayername=self:_GetPlayerName(Client) if self.TasksPerPlayer:HasUniqueID(playername)and not force then if not self.NoScreenOutput then local text=self.gettext:GetEntry("HAVEACTIVETASK",self.locale) local m=MESSAGE:New(text,"10","Tasking"):ToClient(Client) end return self end local taskstate=Task:GetState() if not Task:IsDone()then if taskstate~="Executing"then Task:__Requested(-1) Task:__Executing(-2) end Task:AddClient(Client) local joined=self.gettext:GetEntry("PILOTJOINEDTASK",self.locale) local text=string.format(joined,ttsplayername,self.MenuName or self.Name,Task.TTSType,Task.PlayerTaskNr) self:T(self.lid..text) if not self.NoScreenOutput then self:_SendMessageToClients(text) end if self.UseSRS then self:T(self.lid..text) self.SRSQueue:NewTransmission(text,nil,self.SRS,nil,2) end self.TasksPerPlayer:Push(Task,playername) self:__PlayerJoinedTask(1,Group,Client,Task) self:_SwitchMenuForClient(Client,"Active",1) end if Task.Type==AUFTRAG.Type.PRECISIONBOMBING then if not self.PrecisionTasks:HasUniqueID(Task.PlayerTaskNr)then self.PrecisionTasks:Push(Task,Task.PlayerTaskNr) end end return self end function PLAYERTASKCONTROLLER:_SwitchFlashing(Group,Client) self:T(self.lid.."_SwitchFlashing") local playername,ttsplayername=self:_GetPlayerName(Client) if(not self.FlashPlayer[playername])or(self.FlashPlayer[playername]==false)then self.FlashPlayer[playername]=Client local flashtext=self.gettext:GetEntry("FLASHON",self.locale) local text=string.format(flashtext,ttsplayername) local m=MESSAGE:New(text,10,"Tasking"):ToClient(Client) else self.FlashPlayer[playername]=false local flashtext=self.gettext:GetEntry("FLASHOFF",self.locale) local text=string.format(flashtext,ttsplayername) local m=MESSAGE:New(text,10,"Tasking"):ToClient(Client) end return self end function PLAYERTASKCONTROLLER:_ShowRadioInfo(Group,Client) self:T(self.lid.."_ShowRadioInfo") local playername,ttsplayername=self:_GetPlayerName(Client) if self.UseSRS then local frequency=self.Frequency local freqtext="" if type(frequency)=="table"then freqtext=self.gettext:GetEntry("FREQUENCIES",self.locale) freqtext=freqtext..table.concat(frequency,", ") else local freqt=self.gettext:GetEntry("FREQUENCY",self.locale) freqtext=string.format(freqt,frequency) end local switchtext=self.gettext:GetEntry("BROADCAST",self.locale) playername=ttsplayername or self:_GetTextForSpeech(playername) local text=string.format(switchtext,playername,self.MenuName or self.Name,freqtext) self.SRSQueue:NewTransmission(text,nil,self.SRS,nil,2,{Group},text,30,self.BCFrequency,self.BCModulation) end return self end function PLAYERTASKCONTROLLER:_CalcGroupFuturePosition(group,seconds) local p=group:GetCoordinate() local v=group:GetVelocityVec3() local t=seconds or self.prediction local Vec3={x=p.x+v.x*t,y=p.y+v.y*t,z=p.z+v.z*t} local futureposition=COORDINATE:NewFromVec3(Vec3) if self.verbose==true then local markerID=group:GetProperty("PLAYERTASK_ARROW") if markerID then COORDINATE:RemoveMark(markerID) end markerID=p:ArrowToAll(futureposition,self.coalition,{1,0,0},1,{1,1,0},0.5,2,true,"Position Calc") group:SetProperty("PLAYERTASK_ARROW",markerID) end return futureposition end function PLAYERTASKCONTROLLER:_FlashInfo() self:T(self.lid.."_FlashInfo") for _playername,_client in pairs(self.FlashPlayer)do if _client and _client:IsAlive()then if self.TasksPerPlayer:HasUniqueID(_playername)then local task=self.TasksPerPlayer:ReadByID(_playername) local Coordinate=task.Target:GetCoordinate() local CoordText="" if self.Type~=PLAYERTASKCONTROLLER.Type.A2A and task.Type~=AUFTRAG.Type.INTERCEPT then CoordText=Coordinate:ToStringA2G(_client,nil,self.ShowMagnetic) local targettxt=self.gettext:GetEntry("TARGET",self.locale) local text=targettxt..": "..CoordText local m=MESSAGE:New(text,10,"Tasking"):ToClient(_client) else CoordText=Coordinate:ToStringA2A(_client,nil,self.ShowMagnetic) local targettxt=self.gettext:GetEntry("TARGET",self.locale) local text=targettxt..": "..CoordText local name=task.Target:GetName() local group=GROUP:FindByName(name) local clientcoord=_client:GetCoordinate() if group and clientcoord and group:IsAlive()and task.Type==AUFTRAG.Type.INTERCEPT then local speed=math.max(UTILS.KnotsToMps(350)or _client:GetVelocityMPS()) local dist=Coordinate:Get3DDistance(clientcoord) local iTime=math.floor(dist/speed)+5 if iTime<10 then iTime=10 elseif iTime>600 then iTime=600 end local npos=self:_CalcGroupFuturePosition(group,iTime) local BR=npos:ToStringBearing(clientcoord,nil,self.ShowMagnetic,0) local Intercepttext=self.gettext:GetEntry("INTERCEPTCOURSE",self.locale) text=text.."\n"..Intercepttext.." "..BR end local m=MESSAGE:New(text,10,"Tasking"):ToClient(_client) end end end end return self end function PLAYERTASKCONTROLLER:_FindLasingDroneForTaskID(ID) local drone=nil self.LasingDroneSet:ForEachGroup( function(grp) if grp and grp:IsAlive()and grp.playertask and grp.playertask.id and grp.playertask.id==ID then drone=grp end end ) return drone end function PLAYERTASKCONTROLLER:_ActiveTaskInfo(Task,Group,Client) self:T(self.lid.."_ActiveTaskInfo") local playername,ttsplayername=self:_GetPlayerName(Client) local text="" local textTTS="" local task=nil if type(Task)~="string"then task=Task end if self.TasksPerPlayer:HasUniqueID(playername)or task then local task=task or self.TasksPerPlayer:ReadByID(playername) local tname=self.gettext:GetEntry("TASKNAME",self.locale) local ttsname=self.gettext:GetEntry("TASKNAMETTS",self.locale) local taskname=string.format(tname,task.Type,task.PlayerTaskNr) local ttstaskname=string.format(ttsname,task.TTSType,task.PlayerTaskNr) local Coordinate=task.Target:GetCoordinate()or COORDINATE:New(0,0,0) local Elevation=Coordinate:GetLandHeight()or 0 local CoordText="" local CoordTextLLDM=nil local ShowThreatInfo=task.ShowThreatDetails local LasingDrone=self:_FindLasingDroneForTaskID(task.PlayerTaskNr) if self.Type~=PLAYERTASKCONTROLLER.Type.A2A and task.Type~=AUFTRAG.Type.INTERCEPT then CoordText=Coordinate:ToStringA2G(Client,nil,self.ShowMagnetic) else CoordText=Coordinate:ToStringA2A(Client,nil,self.ShowMagnetic) end local ThreatLevel=task.Target:GetThreatLevelMax() local ThreatLevelText=self.gettext:GetEntry("THREATHIGH",self.locale) if ThreatLevel>3 and ThreatLevel<8 then ThreatLevelText=self.gettext:GetEntry("THREATMEDIUM",self.locale) elseif ThreatLevel<=3 then ThreatLevelText=self.gettext:GetEntry("THREATLOW",self.locale) end local targets=task.Target:CountTargets()or 0 local clientlist,clientcount=task:GetClients() local ThreatGraph="["..string.rep("■",ThreatLevel)..string.rep("□",10-ThreatLevel).."]: "..ThreatLevel local ThreatLocaleText=self.gettext:GetEntry("THREATTEXT",self.locale) if ShowThreatInfo==true then text=string.format(ThreatLocaleText,taskname,ThreatGraph,targets,CoordText) else ThreatLocaleText=self.gettext:GetEntry("NOTHREATTEXT",self.locale) text=string.format(ThreatLocaleText,taskname) end local settings=_DATABASE:GetPlayerSettings(playername)or _SETTINGS local elevationmeasure=self.gettext:GetEntry("FEET",self.locale) if settings:IsMetric()then elevationmeasure=self.gettext:GetEntry("METER",self.locale) else Elevation=math.floor(UTILS.MetersToFeet(Elevation)) end if task.Type~=AUFTRAG.Type.INTERCEPT then local elev=self.gettext:GetEntry("ELEVATION",self.locale) text=text..string.format(elev,tostring(math.floor(Elevation)),elevationmeasure) end if task.Type==AUFTRAG.Type.PRECISIONBOMBING and self.precisionbombing then if LasingDrone and LasingDrone.playertask then local yes=self.gettext:GetEntry("YES",self.locale) local no=self.gettext:GetEntry("NO",self.locale) local inreach=LasingDrone.playertask.inreach==true and yes or no local islasing=LasingDrone:IsLasing()==true and yes or no local prectext=self.gettext:GetEntry("POINTERTARGETREPORT",self.locale) prectext=string.format(prectext,inreach,islasing) text=text..prectext.." ("..LasingDrone.playertask.lasercode..")" end end if task.Type==AUFTRAG.Type.PRECISIONBOMBING and self.buddylasing then if self.PlayerRecce then local yes=self.gettext:GetEntry("YES",self.locale) local no=self.gettext:GetEntry("NO",self.locale) local reachdist=8000 local inreach=false local pset=self.PlayerRecce.PlayerSet:GetAliveSet() for _,_player in pairs(pset)do local player=_player local pcoord=player:GetCoordinate() if pcoord:Get2DDistance(Coordinate)<=reachdist then inreach=true local callsign=player:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs) local playername=player:GetPlayerName() local islasing=no if self.PlayerRecce.CanLase[player:GetTypeName()]and self.PlayerRecce.AutoLase[playername]then islasing=yes end local inrtext=inreach==true and yes or no local prectext=self.gettext:GetEntry("RECCETARGETREPORT",self.locale) prectext=string.format(prectext,callsign,inrtext,islasing) text=text..prectext end end end elseif task.Type==AUFTRAG.Type.CTLD or task.Type==AUFTRAG.Type.CSAR then text=taskname textTTS=taskname local detail=task:GetFreetext() local detailTTS=task:GetFreetextTTS() local brieftxt=self.gettext:GetEntry("BRIEFING",self.locale) local locatxt=self.gettext:GetEntry("TARGETLOCATION",self.locale) text=text..string.format("\n%s: %s\n%s %s",brieftxt,detail,locatxt,CoordText) textTTS=textTTS..string.format("; %s: %s; %s %s",brieftxt,detailTTS,locatxt,CoordText) end local clienttxt=self.gettext:GetEntry("PILOTS",self.locale) if clientcount>0 then for _,_name in pairs(clientlist)do if self.customcallsigns[_name]then _name=self.customcallsigns[_name] _name=string.gsub(_name,"(%d) ","%1") end clienttxt=clienttxt.._name..", " end clienttxt=string.gsub(clienttxt,", $",".") else local keine=self.gettext:GetEntry("NONE",self.locale) clienttxt=clienttxt..keine end text=text..clienttxt textTTS=textTTS..clienttxt if self.InfoHasCoordinate then if self.InfoHasLLDDM then CoordTextLLDM=Coordinate:ToStringLLDDM() else CoordTextLLDM=Coordinate:ToStringLLDMS() end local locatxt=self.gettext:GetEntry("COORDINATE",self.locale) text=string.format("%s\n%s: %s",text,locatxt,CoordTextLLDM) end if task:HasFreetext()and not(task.Type==AUFTRAG.Type.CTLD or task.Type==AUFTRAG.Type.CSAR)then local brieftxt=self.gettext:GetEntry("BRIEFING",self.locale) text=text..string.format("\n%s: ",brieftxt)..task:GetFreetext() end if self.UseSRS then if string.find(CoordText," BR, ")then CoordText=string.gsub(CoordText," BR, "," Bee, Arr; ") end if self.ShowMagnetic then text=string.gsub(text,"°M|","° magnetic; ") end if string.find(CoordText,"MGRS")then local Text=string.gsub(CoordText,"MGRS ","") Text=string.gsub(Text,"%s+","") Text=string.gsub(Text,"([%a%d])","%1;") Text=string.gsub(Text,"0","zero") Text=string.gsub(Text,"9","niner") CoordText="MGRS;"..Text if self.PathToGoogleKey then end end local ttstext local ThreatLocaleTextTTS=self.gettext:GetEntry("THREATTEXTTTS",self.locale) if ShowThreatInfo==true then ttstext=string.format(ThreatLocaleTextTTS,ttsplayername,self.MenuName or self.Name,ttstaskname,ThreatLevelText,targets,CoordText) else ThreatLocaleTextTTS=self.gettext:GetEntry("NOTHREATTEXTTTS",self.locale) ttstext=string.format(ThreatLocaleTextTTS,ttsplayername,self.MenuName or self.Name) end if task.Type==AUFTRAG.Type.PRECISIONBOMBING and self.precisionbombing then if LasingDrone and LasingDrone.playertask.inreach and LasingDrone:IsLasing()then local lasingtext=self.gettext:GetEntry("POINTERTARGETLASINGTTS",self.locale) ttstext=ttstext..lasingtext end elseif task.Type==AUFTRAG.Type.CTLD or task.Type==AUFTRAG.Type.CSAR then ttstext=textTTS if string.find(ttstext," BR, ")then CoordText=string.gsub(ttstext," BR, "," Bee, Arr, ") end elseif task:HasFreetext()then local brieftxt=self.gettext:GetEntry("BRIEFING",self.locale) ttstext=ttstext..string.format("; %s: ",brieftxt)..task:GetFreetextTTS() end self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,2) end else text=self.gettext:GetEntry("NOACTIVETASK",self.locale) end if not self.NoScreenOutput then local m=MESSAGE:New(text,self.TaskInfoDuration or 30,"Tasking"):ToClient(Client) end return self end function PLAYERTASKCONTROLLER:_MarkTask(Group,Client) self:T(self.lid.."_MarkTask") local playername,ttsplayername=self:_GetPlayerName(Client) local text="" if self.TasksPerPlayer:HasUniqueID(playername)then local task=self.TasksPerPlayer:ReadByID(playername) text=string.format("Task ID #%03d | Type: %s | Threat: %d",task.PlayerTaskNr,task.Type,task.Target:GetThreatLevelMax()) task:MarkTargetOnF10Map(text,self.Coalition,self.MarkerReadOnly) local textmark=self.gettext:GetEntry("MARKTASK",self.locale) text=string.format(textmark,ttsplayername,self.MenuName or self.Name,task.PlayerTaskNr) self:T(self.lid..text) if self.UseSRS then self.SRSQueue:NewTransmission(text,nil,self.SRS,nil,2) end else text=self.gettext:GetEntry("NOACTIVETASK",self.locale) end if not self.NoScreenOutput then local m=MESSAGE:New(text,"10","Tasking"):ToClient(Client) end return self end function PLAYERTASKCONTROLLER:_SmokeTask(Group,Client) self:T(self.lid.."_SmokeTask") local playername,ttsplayername=self:_GetPlayerName(Client) local text="" if self.TasksPerPlayer:HasUniqueID(playername)then local task=self.TasksPerPlayer:ReadByID(playername) if task.CanSmoke==true then task:SmokeTarget() local textmark=self.gettext:GetEntry("SMOKETASK",self.locale) text=string.format(textmark,ttsplayername,self.MenuName or self.Name,task.PlayerTaskNr) self:T(self.lid..text) if self.UseSRS then self.SRSQueue:NewTransmission(text,nil,self.SRS,nil,2) end self:__TaskTargetSmoked(5,task) else local textmark=self.gettext:GetEntry("NOSMOKETASK",self.locale) text=string.format(textmark,ttsplayername,self.MenuName or self.Name,task.PlayerTaskNr) self:T(self.lid..text) if self.UseSRS then self.SRSQueue:NewTransmission(text,nil,self.SRS,nil,2) end end else text=self.gettext:GetEntry("NOACTIVETASK",self.locale) end if not self.NoScreenOutput then local m=MESSAGE:New(text,15,"Tasking"):ToClient(Client) end return self end function PLAYERTASKCONTROLLER:_FlareTask(Group,Client) self:T(self.lid.."_FlareTask") local playername,ttsplayername=self:_GetPlayerName(Client) local text="" if self.TasksPerPlayer:HasUniqueID(playername)then local task=self.TasksPerPlayer:ReadByID(playername) task:FlareTarget() local textmark=self.gettext:GetEntry("FLARETASK",self.locale) text=string.format(textmark,ttsplayername,self.MenuName or self.Name,task.PlayerTaskNr) self:T(self.lid..text) if self.UseSRS then self.SRSQueue:NewTransmission(text,nil,self.SRS,nil,2) end self:__TaskTargetFlared(5,task) else text=self.gettext:GetEntry("NOACTIVETASK",self.locale) end if not self.NoScreenOutput then local m=MESSAGE:New(text,15,"Tasking"):ToClient(Client) end return self end function PLAYERTASKCONTROLLER:_IlluminateTask(Group,Client) self:T(self.lid.."_IlluminateTask") local playername,ttsplayername=self:_GetPlayerName(Client) local text="" if self.TasksPerPlayer:HasUniqueID(playername)then local task=self.TasksPerPlayer:ReadByID(playername) task:FlareTarget() local textmark=self.gettext:GetEntry("FLARETASK",self.locale) text=string.format(textmark,ttsplayername,self.MenuName or self.Name,task.PlayerTaskNr) self:T(self.lid..text) if self.UseSRS then self.SRSQueue:NewTransmission(text,nil,self.SRS,nil,2) end self:__TaskTargetIlluminated(5,task) else text=self.gettext:GetEntry("NOACTIVETASK",self.locale) end if not self.NoScreenOutput then local m=MESSAGE:New(text,15,"Tasking"):ToClient(Client) end return self end function PLAYERTASKCONTROLLER:_AbortTask(Group,Client) self:T(self.lid.."_AbortTask") local playername,ttsplayername=self:_GetPlayerName(Client) local text="" if self.TasksPerPlayer:HasUniqueID(playername)then local task=self.TasksPerPlayer:PullByID(playername) task:ClientAbort(Client) local textmark=self.gettext:GetEntry("ABORTTASK",self.locale) text=string.format(textmark,self.MenuName or self.Name,ttsplayername,task.TTSType,task.PlayerTaskNr) self:T(self.lid..text) if self.UseSRS then self.SRSQueue:NewTransmission(text,nil,self.SRS,nil,2) end self:__PlayerAbortedTask(1,Group,Client,task) else text=self.gettext:GetEntry("NOACTIVETASK",self.locale) end if not self.NoScreenOutput then local m=MESSAGE:New(text,15,"Tasking"):ToClient(Client) end self:_SwitchMenuForClient(Client,"Info",1) return self end function PLAYERTASKCONTROLLER:_UpdateJoinMenuTemplate() self:T("_UpdateJoinMenuTemplate") if self.TaskQueue:Count()>0 then local taskpertype=self:_GetTasksPerType() local JoinMenu=self.JoinMenu local controller=self.JoinTaskMenuTemplate local actcontroller=self.ActiveTaskMenuTemplate local actinfomenu=self.ActiveInfoMenu if self.TaskQueue:Count()==0 and self.MenuNoTask==nil then local menunotasks=self.gettext:GetEntry("MENUNOTASKS",self.locale) self.MenuNoTask=controller:NewEntry(menunotasks,self.JoinMenu) controller:AddEntry(self.MenuNoTask) end if self.TaskQueue:Count()>0 and self.MenuNoTask~=nil then controller:DeleteGenericEntry(self.MenuNoTask) controller:DeleteF10Entry(self.MenuNoTask) self.MenuNoTask=nil end local maxn=self.menuitemlimit for _type,_ in pairs(taskpertype)do local found=controller:FindEntriesByText(_type) if#found==0 then local newentry=controller:NewEntry(_type,JoinMenu) controller:AddEntry(newentry) if self.JoinInfoMenu then local newentry=controller:NewEntry(_type,self.JoinInfoMenu) controller:AddEntry(newentry) end if actinfomenu then local newentry=actcontroller:NewEntry(_type,self.ActiveInfoMenu) actcontroller:AddEntry(newentry) end end end local typelist=self:_GetAvailableTaskTypes() for _tasktype,_data in pairs(typelist)do self:T("**** Building for TaskType: ".._tasktype) for _,_task in pairs(taskpertype[_tasktype])do _task=_task self:T("**** Building for Task: ".._task.Target:GetName()) if _task.InMenu then self:T("**** Task already in Menu ".._task.Target:GetName()) else local menutaskno=self.gettext:GetEntry("MENUTASKNO",self.locale) local text=string.format("%s %03d",menutaskno,_task.PlayerTaskNr) if self.UseGroupNames then local name=_task.Target:GetName() if name~="Unknown"then text=string.format("%s (%03d)",name,_task.PlayerTaskNr) end end local parenttable,number=controller:FindEntriesByText(_tasktype,JoinMenu) if number>0 then local Parent=parenttable[1] local matches,count=controller:FindEntriesByParent(Parent) self:T("***** Join Menu ".._tasktype.." # of entries: "..count) if count0 then local Parent=parenttable[1] local matches,count=controller:FindEntriesByParent(Parent) self:T("***** Join Info Menu ".._tasktype.." # of entries: "..count) if count0 then local Parent=parenttable[1] local matches,count=actcontroller:FindEntriesByParent(Parent) self:T("***** Active Info Menu ".._tasktype.." # of entries: "..count) if count0 and self.MenuNoTask~=nil then JoinTaskMenuTemplate:DeleteGenericEntry(self.MenuNoTask) self.MenuNoTask=nil end if self.InformationMenu then local radioinfo=self.gettext:GetEntry("RADIOS",self.locale) JoinTaskMenuTemplate:NewEntry(radioinfo,self.JoinTopMenu,self._ShowRadioInfo,self) end self.JoinTaskMenuTemplate=JoinTaskMenuTemplate return self end function PLAYERTASKCONTROLLER:_CreateActiveTaskMenuTemplate() self:T("_CreateActiveTaskMenuTemplate") local menuactive=self.gettext:GetEntry("MENUACTIVE",self.locale) local menuinfo=self.gettext:GetEntry("MENUINFO",self.locale) local menumark=self.gettext:GetEntry("MENUMARK",self.locale) local menusmoke=self.gettext:GetEntry("MENUSMOKE",self.locale) local menuflare=self.gettext:GetEntry("MENUFLARE",self.locale) local menuillu=self.gettext:GetEntry("MENUILLU",self.locale) local menuabort=self.gettext:GetEntry("MENUABORT",self.locale) local ActiveTaskMenuTemplate=CLIENTMENUMANAGER:New(self.ActiveClientSet,"ActiveTask") if not self.ActiveTopMenu then local taskings=self.gettext:GetEntry("MENUTASKING",self.locale) local longname=self.Name..taskings..self.Type local menuname=self.MenuName or longname self.ActiveTopMenu=ActiveTaskMenuTemplate:NewEntry(menuname,self.MenuParent) end if self.AllowFlash then local flashtext=self.gettext:GetEntry("FLASHMENU",self.locale) ActiveTaskMenuTemplate:NewEntry(flashtext,self.ActiveTopMenu,self._SwitchFlashing,self) end local active=ActiveTaskMenuTemplate:NewEntry(menuactive,self.ActiveTopMenu) ActiveTaskMenuTemplate:NewEntry(menuinfo,active,self._ActiveTaskInfo,self,"NONE") ActiveTaskMenuTemplate:NewEntry(menumark,active,self._MarkTask,self) if self.Type~=PLAYERTASKCONTROLLER.Type.A2A and self.noflaresmokemenu~=true then ActiveTaskMenuTemplate:NewEntry(menusmoke,active,self._SmokeTask,self) ActiveTaskMenuTemplate:NewEntry(menuflare,active,self._FlareTask,self) if self.illumenu then ActiveTaskMenuTemplate:NewEntry(menuillu,active,self._IlluminateTask,self) end end ActiveTaskMenuTemplate:NewEntry(menuabort,active,self._AbortTask,self) self.ActiveTaskMenuTemplate=ActiveTaskMenuTemplate if self.taskinfomenu and self.activehasinfomenu then local menutaskinfo=self.gettext:GetEntry("MENUTASKINFO",self.locale) self.ActiveInfoMenu=ActiveTaskMenuTemplate:NewEntry(menutaskinfo,self.ActiveTopMenu) end return self end function PLAYERTASKCONTROLLER:_SwitchMenuForClient(Client,MenuType,Delay) self:T(self.lid.."_SwitchMenuForClient") if Delay then self:ScheduleOnce(Delay,self._SwitchMenuForClient,self,Client,MenuType) return self end if MenuType=="Info"then self.ClientSet:AddClientsByName(Client:GetName()) self.ActiveClientSet:Remove(Client:GetName(),true) self.ActiveTaskMenuTemplate:ResetMenu(Client) self.JoinTaskMenuTemplate:ResetMenu(Client) self.JoinTaskMenuTemplate:Propagate(Client) elseif MenuType=="Active"then self.ActiveClientSet:AddClientsByName(Client:GetName()) self.ClientSet:Remove(Client:GetName(),true) self.ActiveTaskMenuTemplate:ResetMenu(Client) self.JoinTaskMenuTemplate:ResetMenu(Client) self.ActiveTaskMenuTemplate:Propagate(Client) else self:E(self.lid.."Unknown menu type in _SwitchMenuForClient:"..tostring(MenuType)) end return self end function PLAYERTASKCONTROLLER:AddAgent(Recce) self:T(self.lid.."AddAgent") if self.Intel then self.Intel:AddAgent(Recce) else self:E(self.lid.."*****NO detection has been set up (yet)!") end return self end function PLAYERTASKCONTROLLER:AddAgentSet(RecceSet) self:T(self.lid.."AddAgentSet") if self.Intel then local Set=RecceSet:GetAliveSet() for _,_Recce in pairs(Set)do self.Intel:AddAgent(_Recce) end else self:E(self.lid.."*****NO detection has been set up (yet)!") end return self end function PLAYERTASKCONTROLLER:SwitchDetectStatics(OnOff) self:T(self.lid.."SwitchDetectStatics") if self.Intel then self.Intel:SetDetectStatics(OnOff) else self:E(self.lid.."***** NO detection has been set up (yet)!") end return self end function PLAYERTASKCONTROLLER:AddAcceptZone(AcceptZone) self:T(self.lid.."AddAcceptZone") if self.Intel then self.Intel:AddAcceptZone(AcceptZone) else self:E(self.lid.."*****NO detection has been set up (yet)!") end return self end function PLAYERTASKCONTROLLER:AddAcceptZoneSet(AcceptZoneSet) self:T(self.lid.."AddAcceptZoneSet") if self.Intel then self.Intel.acceptzoneset:AddSet(AcceptZoneSet) else self:E(self.lid.."*****NO detection has been set up (yet)!") end return self end function PLAYERTASKCONTROLLER:AddRejectZone(RejectZone) self:T(self.lid.."AddRejectZone") if self.Intel then self.Intel:AddRejectZone(RejectZone) else self:E(self.lid.."*****NO detection has been set up (yet)!") end return self end function PLAYERTASKCONTROLLER:AddRejectZoneSet(RejectZoneSet) self:T(self.lid.."AddRejectZoneSet") if self.Intel then self.Intel.rejectzoneset:AddSet(RejectZoneSet) else self:E(self.lid.."*****NO detection has been set up (yet)!") end return self end function PLAYERTASKCONTROLLER:AddConflictZone(ConflictZone) self:T(self.lid.."AddConflictZone") if self.Intel then self.Intel:AddConflictZone(ConflictZone) else self:E(self.lid.."*****NO detection has been set up (yet)!") end return self end function PLAYERTASKCONTROLLER:AddConflictZoneSet(ConflictZoneSet) self:T(self.lid.."AddConflictZoneSet") if self.Intel then self.Intel.conflictzoneset:AddSet(ConflictZoneSet) else self:E(self.lid.."*****NO detection has been set up (yet)!") end return self end function PLAYERTASKCONTROLLER:RemoveAcceptZone(AcceptZone) self:T(self.lid.."RemoveAcceptZone") if self.Intel then self.Intel:RemoveAcceptZone(AcceptZone) else self:E(self.lid.."*****NO detection has been set up (yet)!") end return self end function PLAYERTASKCONTROLLER:RemoveRejectZone(RejectZone) self:T(self.lid.."RemoveRejectZone") if self.Intel then self.Intel:RemoveRejectZone(RejectZone) else self:E(self.lid.."*****NO detection has been set up (yet)!") end return self end function PLAYERTASKCONTROLLER:RemoveConflictZone(ConflictZone) self:T(self.lid.."RemoveConflictZone") if self.Intel then self.Intel:RemoveConflictZone(ConflictZone) else self:E(self.lid.."*****NO detection has been set up (yet)!") end return self end function PLAYERTASKCONTROLLER:AddCorridorZone(CorridorZone) self:T(self.lid.."AddCorridorZone") if self.Intel then self.Intel:AddCorridorZone(CorridorZone) else self:E(self.lid.."*****NO detection has been set up (yet)!") end return self end function PLAYERTASKCONTROLLER:AddCorridorZoneSet(CorridorZoneSet) self:T(self.lid.."AddCorridorZoneSet") if self.Intel then self.Intel.corridorzoneset:AddSet(CorridorZoneSet) else self:E(self.lid.."*****NO detection has been set up (yet)!") end return self end function PLAYERTASKCONTROLLER:RemoveCorridorZone(CorridorZone) self:T(self.lid.."RemoveCorridorZone") if self.Intel then self.Intel:RemoveCorridorZone(CorridorZone) else self:E(self.lid.."*****NO detection has been set up (yet)!") end return self end function PLAYERTASKCONTROLLER:SetCorridorZoneFloorAndCeiling(Floor,Ceiling) if self.Intel then self.Intel:SetCorridorLimitsFeet(Floor,Ceiling) else self:E(self.lid.."*****NO detection has been set up (yet)!") end return self end function PLAYERTASKCONTROLLER:SetCorridorZoneFloorAndCeilingMeters(Floor,Ceiling) if self.Intel then self.Intel:SetCorridorLimits(Floor,Ceiling) else self:E(self.lid.."*****NO detection has been set up (yet)!") end return self end function PLAYERTASKCONTROLLER:SetMenuName(Name) self:T(self.lid.."SetMenuName: "..Name) self.MenuName=Name return self end function PLAYERTASKCONTROLLER:SetParentMenu(Menu) self:T(self.lid.."SetParentMenu") return self end function PLAYERTASKCONTROLLER:SetupIntel(RecceName) self:T(self.lid.."SetupIntel") self.RecceSet=SET_GROUP:New():FilterCoalitions(self.CoalitionName):FilterPrefixes(RecceName):FilterStart() self.Intel=INTEL:New(self.RecceSet,self.Coalition,self.Name.."-Intel") self.Intel:SetClusterAnalysis(true,false,false) self.Intel:SetClusterRadius(self.ClusterRadius or 0.5) self.Intel.statusupdate=25 self.Intel:SetAcceptZones() self.Intel:SetRejectZones() if self.Type==PLAYERTASKCONTROLLER.Type.A2G or self.Type==PLAYERTASKCONTROLLER.Type.A2GS then self.Intel:SetDetectStatics(true) end self.Intel:__Start(2) local function NewCluster(Cluster) if not self.usecluster then return self end local cluster=Cluster local type=cluster.ctype self:T({type,self.Type}) if(type==INTEL.Ctype.AIRCRAFT and self.Type==PLAYERTASKCONTROLLER.Type.A2A)or(type==INTEL.Ctype.NAVAL and(self.Type==PLAYERTASKCONTROLLER.Type.A2S or self.Type==PLAYERTASKCONTROLLER.Type.A2GS))then self:T("A2A or A2S") local contacts=cluster.Contacts local targetset=SET_GROUP:New() for _,_object in pairs(contacts)do local contact=_object self:T("Adding group: "..contact.groupname) targetset:AddGroup(contact.group,true) end self:AddTarget(targetset) elseif(type==INTEL.Ctype.GROUND or type==INTEL.Ctype.STRUCTURE)and(self.Type==PLAYERTASKCONTROLLER.Type.A2G or self.Type==PLAYERTASKCONTROLLER.Type.A2GS)then self:T("A2G") local contacts=cluster.Contacts local targetset=nil if type==INTEL.Ctype.GROUND then targetset=SET_GROUP:New() for _,_object in pairs(contacts)do local contact=_object self:T("Adding group: "..contact.groupname) targetset:AddGroup(contact.group,true) end elseif type==INTEL.Ctype.STRUCTURE then targetset=SET_STATIC:New() for _,_object in pairs(contacts)do local contact=_object self:T("Adding static: "..contact.groupname) targetset:AddStatic(contact.group) end end if targetset then self:AddTarget(targetset) end end end local function NewContact(Contact) if self.usecluster then return self end local contact=Contact local type=contact.ctype self:T({type,self.Type}) if(type==INTEL.Ctype.AIRCRAFT and self.Type==PLAYERTASKCONTROLLER.Type.A2A)or(type==INTEL.Ctype.NAVAL and(self.Type==PLAYERTASKCONTROLLER.Type.A2S or self.Type==PLAYERTASKCONTROLLER.Type.A2GS))then self:T("A2A or A2S") self:T("Adding group: "..contact.groupname) self:AddTarget(contact.group) elseif(type==INTEL.Ctype.GROUND or type==INTEL.Ctype.STRUCTURE)and(self.Type==PLAYERTASKCONTROLLER.Type.A2G or self.Type==PLAYERTASKCONTROLLER.Type.A2GS)then self:T("A2G") self:T("Adding group: "..contact.groupname) self:AddTarget(contact.group) end end function self.Intel:OnAfterNewCluster(From,Event,To,Cluster) NewCluster(Cluster) end function self.Intel:OnAfterNewContact(From,Event,To,Contact) NewContact(Contact) end return self end function PLAYERTASKCONTROLLER:SetSRS(Frequency,Modulation,PathToSRS,Gender,Culture,Port,Voice,Volume,PathToGoogleKey,AccessKey,Coordinate,Backend,Provider,Speaker) self:T(self.lid.."SetSRS") self.PathToSRS=PathToSRS or MSRS.path or"C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio" self.Gender=Gender or MSRS.gender or"male" self.Culture=Culture or MSRS.culture or"en-US" self.Port=Port or MSRS.port or 5002 self.Voice=Voice or MSRS.voice self.PathToGoogleKey=PathToGoogleKey self.AccessKey=AccessKey self.Volume=Volume or 1.0 self.UseSRS=true self.Frequency=Frequency or{127,251} self.BCFrequency=self.Frequency self.Modulation=Modulation or{radio.modulation.FM,radio.modulation.AM} self.BCModulation=self.Modulation self.SRS=MSRS:New(self.PathToSRS,self.Frequency,self.Modulation,Backend) self.SRS:SetCoalition(self.Coalition) self.SRS:SetLabel(self.MenuName or self.Name) self.SRS:SetGender(self.Gender) self.SRS:SetCulture(self.Culture) self.SRS:SetPort(self.Port) self.SRS:SetVolume(self.Volume) if self.PathToGoogleKey then self.SRS:SetProviderOptionsGoogle(self.PathToGoogleKey,self.AccessKey) self.SRS:SetProvider(MSRS.Provider.GOOGLE) end if(not PathToGoogleKey)and self.SRS:GetProvider()==MSRS.Provider.GOOGLE then self.PathToGoogleKey=MSRS.poptions.gcloud.credentials self.Voice=Voice or MSRS.poptions.gcloud.voice self.AccessKey=AccessKey or MSRS.poptions.gcloud.key end if Provider then self.SRS:SetProvider(Provider) end if Coordinate then self.SRS:SetCoordinate(Coordinate) end self.SRS:SetVoice(self.Voice) if Speaker then self.SRS:SetSpeakerPiper(Speaker) end self.SRSQueue=MSRSQUEUE:New(self.MenuName or self.Name) self.SRSQueue:SetTransmitOnlyWithPlayers(self.TransmitOnlyWithPlayers) return self end function PLAYERTASKCONTROLLER:SetSRSBroadcast(Frequency,Modulation) self:T(self.lid.."SetSRSBroadcast") if self.SRS then self.BCFrequency=Frequency self.BCModulation=Modulation end return self end function PLAYERTASKCONTROLLER:_UpdateTargetsAlive(Task,TargetsLeft) self:T(self.lid.."_UpdateTargetsAlive") local delta=Task.Target:CountTargets()-TargetsLeft if delta>0 then self:T("Delta targets to be removed: "..delta) local count=0 local targets=Task.Target:GetObjects() for _,_object in pairs(targets or{})do if _object and _object.ClassName and(_object:IsInstanceOf("GROUP")or _object:IsInstanceOf("UNIT")or _object:IsInstanceOf("STATIC")or _object:IsInstanceOf("SCENERY"))then if count12 then clock=clock-12 end if clock==0 then clock=12 end end return clock end function PLAYERRECCE:SetLaserCodes(LaserCodes) self.LaserCodes=(type(LaserCodes)=="table")and LaserCodes or{LaserCodes} return self end function PLAYERRECCE:SetReferencePoint(Coordinate,Name) self.ReferencePoint=Coordinate self.RPName=Name if self.RPMarker then self.RPMarker:Remove() end local llddm=Coordinate:ToStringLLDDM() local lldms=Coordinate:ToStringLLDMS() local mgrs=Coordinate:ToStringMGRS() local text=string.format("%s RP %s\n%s\n%s\n%s",self.Name,Name,llddm,lldms,mgrs) self.RPMarker=MARKER:New(Coordinate,text) self.RPMarker:ReadOnly() self.RPMarker:ToCoalition(self.Coalition) return self end function PLAYERRECCE:SetPlayerTaskController(Controller) self.UseController=true self.Controller=Controller return self end function PLAYERRECCE:SetAttackSet(AttackSet) self.AttackSet=AttackSet return self end function PLAYERRECCE:_CameraOn(client,playername) local camera=true local unit=client if unit and unit:IsAlive()then local typename=unit:GetTypeName() if string.find(typename,"SA342")then local dcsunit=Unit.getByName(client:GetName()) local vivihorizontal=dcsunit:getDrawArgumentValue(215)or 0 if vivihorizontal<-0.7 or vivihorizontal>0.7 then camera=false end elseif string.find(typename,"OH58")then local dcsunit=Unit.getByName(client:GetName()) local vivihorizontal=dcsunit:getDrawArgumentValue(528)or 0 if vivihorizontal<-0.527 or vivihorizontal>0.527 then camera=false end elseif string.find(typename,"Ka-50")then camera=true end end return camera end function PLAYERRECCE:_GetKiowaMMSSight(Kiowa) self:T(self.lid.."_GetKiowaMMSSight") local unit=Kiowa if unit and unit:IsAlive()then local dcsunit=Unit.getByName(Kiowa:GetName()) local mmshorizontal=dcsunit:getDrawArgumentValue(528)or 0 local mmsvertical=dcsunit:getDrawArgumentValue(527)or 0 self:T(string.format("Kiowa MMS Arguments Read: H %.3f V %.3f",mmshorizontal,mmsvertical)) local mmson=true if mmshorizontal<-0.527 or mmshorizontal>0.527 then mmson=false end local horizontalview=mmshorizontal/0.527*190 local heading=unit:GetHeading() local mmsheading=(heading+horizontalview)%360 local mmsyaw=math.atan(mmsvertical)*40 local maxview=self:_GetActualMaxLOSight(unit,mmsheading,mmsyaw,not mmson) if maxview>8000 then maxview=8000 end self:T(string.format("Kiowa MMS Heading %d, Yaw %d, MaxView %dm MMS On %s",mmsheading,mmsyaw,maxview,tostring(mmson))) return mmsheading,mmsyaw,maxview,mmson end return 0,0,0,false end function PLAYERRECCE:_GetGazelleVivianneSight(Gazelle) self:T(self.lid.."GetGazelleVivianneSight") local unit=Gazelle if unit and unit:IsAlive()then local dcsunit=Unit.getByName(Gazelle:GetName()) local vivihorizontal=dcsunit:getDrawArgumentValue(215)or 0 local vivivertical=dcsunit:getDrawArgumentValue(216)or 0 local vivioff=false if vivihorizontal<-0.67 then vivihorizontal=-0.67 vivioff=false elseif vivihorizontal>0.67 then vivihorizontal=0.67 vivioff=true return 0,0,0,false end local horizontalview=vivihorizontal*-180 local verticalview=math.atan(vivivertical) local heading=unit:GetHeading() local viviheading=(heading+horizontalview)%360 local maxview=self:_GetActualMaxLOSight(unit,viviheading,verticalview,vivioff) if maxview>8000 then maxview=8000 end return viviheading,verticalview,maxview,not vivioff end return 0,0,0,false end function PLAYERRECCE:_GetActualMaxLOSight(unit,vheading,vnod,vivoff) self:T(self.lid.."_GetActualMaxLOSight") if vivoff then return 0 end local maxview=0 if unit and unit:IsAlive()then local typename=unit:GetTypeName() maxview=self.MaxViewDistance[typename]or 8000 local CamHeight=self.Cameraheight[typename]or 1 if vnod<-2 then local beta=90 local gamma=90-math.abs(vnod) local alpha=90-gamma local a=unit:GetHeight()-unit:GetCoordinate():GetLandHeight()+CamHeight local b=a/math.sin(math.rad(alpha)) local c=b*math.sin(math.rad(gamma)) maxview=c*1.2 end end return math.ceil(math.abs(maxview)) end function PLAYERRECCE:SetCallSignOptions(ShortCallsign,Keepnumber,CallsignTranslations,CallsignCustomFunc,...) if not ShortCallsign or ShortCallsign==false then self.ShortCallsign=false else self.ShortCallsign=true end self.Keepnumber=Keepnumber or false self.CallsignTranslations=CallsignTranslations self.CallsignCustomFunc=CallsignCustomFunc self.CallsignCustomArgs=arg or{} return self end function PLAYERRECCE:_GetViewZone(unit,vheading,minview,maxview,angle,camon,laser) self:T(self.lid.."_GetViewZone") local viewzone=nil if not camon then return nil end if unit and unit:IsAlive()then local unitname=unit:GetName() if not laser then local startpos=unit:GetCoordinate() local heading1=(vheading+angle)%360 local heading2=(vheading-angle)%360 local pos1=startpos:Translate(maxview,heading1) local pos2=startpos:Translate(maxview,heading2) local array={} table.insert(array,startpos:GetVec2()) table.insert(array,pos1:GetVec2()) table.insert(array,pos2:GetVec2()) viewzone=ZONE_POLYGON:NewFromPointsArray(unitname,array) else local startp=unit:GetCoordinate() local heading1=(vheading+90)%360 local heading2=(vheading-90)%360 self:T({heading1,heading2}) local startpos=startp:Translate(minview,vheading) local pos1=startpos:Translate(12.5,heading1) local pos2=startpos:Translate(12.5,heading2) local pos3=pos1:Translate(maxview,vheading) local pos4=pos2:Translate(maxview,vheading) local array={} table.insert(array,pos1:GetVec2()) table.insert(array,pos2:GetVec2()) table.insert(array,pos4:GetVec2()) table.insert(array,pos3:GetVec2()) viewzone=ZONE_POLYGON:NewFromPointsArray(unitname,array) end end return viewzone end function PLAYERRECCE:_GetKnownTargets(client) self:T(self.lid.."_GetKnownTargets") local finaltargets=SET_UNIT:New() local targets=self.TargetCache:GetDataTable() local playername=client:GetPlayerName() for _,_target in pairs(targets)do local targetdata=_target.PlayerRecceDetected if targetdata.playername==playername then finaltargets:Add(_target:GetName(),_target) end end return finaltargets,finaltargets:CountAlive() end function PLAYERRECCE:_CleanupTargetCache() self:T(self.lid.."_CleanupTargetCache") local cleancache=FIFO:New() self.TargetCache:ForEach( function(unit) local pull=false if unit and unit:IsAlive()and unit:GetLife()>1 then if unit.PlayerRecceDetected and unit.PlayerRecceDetected.timestamp then local TNow=timer.getTime() if TNow-unit.PlayerRecceDetected.timestamp>self.TForget then pull=true unit.PlayerRecceDetected=nil end else pull=true end else pull=true end if not pull then cleancache:Push(unit,unit:GetName()) end end ) self.TargetCache=nil self.TargetCache=cleancache return self end function PLAYERRECCE:_GetTargetSet(unit,camera,laser) self:T(self.lid.."_GetTargetSet") local finaltargets=SET_UNIT:New() local finalcount=0 local minview=0 local typename=unit:GetTypeName() local playername=unit:GetPlayerName() local maxview=self.MaxViewDistance[typename]or 8000 local heading,nod,maxview,angle=0,30,8000,10 local camon=false local name=unit:GetName() if string.find(typename,"SA342")and camera then heading,nod,maxview,camon=self:_GetGazelleVivianneSight(unit) angle=10 maxview=self.MaxViewDistance[typename]or 8000 elseif string.find(typename,"Ka-50")and camera then heading=unit:GetHeading() nod,maxview,camon=10,1000,true angle=10 maxview=self.MaxViewDistance[typename]or 8000 elseif string.find(typename,"OH58")and camera then nod,maxview,camon=0,8000,true heading,nod,maxview,camon=self:_GetKiowaMMSSight(unit) angle=8 if maxview==0 then maxview=self.MaxViewDistance[typename]or 8000 end else heading=unit:GetHeading() nod,maxview,camon=10,3000,true maxview=self.MaxViewDistance[typename]or 3000 angle=45 end if laser then if not self.LaserFOV[playername]then minview=100 maxview=2000 self.LaserFOV[playername]={ min=100, max=2000, } else minview=self.LaserFOV[playername].min maxview=self.LaserFOV[playername].max end end local zone=self:_GetViewZone(unit,heading,minview,maxview,angle,camon,laser) if zone then local redcoalition="red" if self.Coalition==coalition.side.RED then redcoalition="blue" end local startpos=unit:GetCoordinate() local targetset=SET_UNIT:New():FilterCategories("ground"):FilterActive(true):FilterZones({zone}):FilterCoalitions(redcoalition):FilterOnce() self:T("Prefilter Target Count = "..targetset:CountAlive()) targetset:ForEach( function(_unit) local _unit=_unit local _unitpos=_unit:GetCoordinate() if startpos:IsLOS(_unitpos)and _unit:IsAlive()and _unit:GetLife()>1 then self:T("Adding to final targets: ".._unit:GetName()) finaltargets:Add(_unit:GetName(),_unit) end end ) finalcount=finaltargets:CountAlive() self:T(string.format("%s Unit: %s | Targets in view %s",self.lid,name,finalcount)) end return finaltargets,finalcount,zone end function PLAYERRECCE:_GetHVTTarget(targetset) self:T(self.lid.."_GetHVTTarget") local unitsbythreat={} local minthreat=self.minthreatlevel or 0 for _,_unit in pairs(targetset.Set)do local unit=_unit if unit and unit:IsAlive()and unit:GetLife()>1 then local threat=unit:GetThreatLevel() if threat>=minthreat then if unit:HasAttribute("RADAR_BAND1_FOR_ARM")or unit:HasAttribute("RADAR_BAND2_FOR_ARM")or unit:HasAttribute("Optical Tracker")then threat=11 end table.insert(unitsbythreat,{unit,threat}) end end end table.sort(unitsbythreat,function(a,b) local aNum=a[2] local bNum=b[2] return aNum>bNum end) if unitsbythreat[1]and unitsbythreat[1][1]then return unitsbythreat[1][1] else return nil end end function PLAYERRECCE:_LaseTarget(client,targetset) self:T(self.lid.."_LaseTarget") local target=self:_GetHVTTarget(targetset) local playername=client:GetPlayerName() local laser=nil if not self.LaserSpots[playername]then laser=SPOT:New(client) if not self.UnitLaserCodes[playername]then self.UnitLaserCodes[playername]=1688 end laser.LaserCode=self.UnitLaserCodes[playername]or 1688 self.LaserSpots[playername]=laser else laser=self.LaserSpots[playername] end if self.LaserTarget[playername]then local target=self.LaserTarget[playername] local oldtarget=target:GetObject() self:T("Targetstate: "..target:GetState()) self:T("Laser State: "..tostring(laser:IsLasing())) if(not oldtarget)or targetset:IsNotInSet(oldtarget)or target:IsDead()or target:IsDestroyed()then laser:LaseOff() self:T(self.lid.."Target Life Points: "..target:GetLife()or"none") if target:IsDead()or target:IsDestroyed()or target:GetDamage()>79 or target:GetLife()<=1 then self:__Shack(-1,client,oldtarget) else self:__TargetLOSLost(-1,client,oldtarget) end self.LaserTarget[playername]=nil oldtarget=nil self.LaserSpots[playername]=nil elseif oldtarget and laser and(not laser:IsLasing())then self:T("Switching laser back on ..") local lasercode=self.UnitLaserCodes[playername]or laser.LaserCode or 1688 local lasingtime=self.lasingtime or 60 laser:LaseOn(oldtarget,lasercode,lasingtime) else self:T("Target alive and laser is on!") end elseif(not laser:IsLasing())and target then local relativecam=self.LaserRelativePos[client:GetTypeName()] laser:SetRelativeStartPosition(relativecam) local lasercode=self.UnitLaserCodes[playername]or laser.LaserCode or 1688 local lasingtime=self.lasingtime or 60 laser:LaseOn(target,lasercode,lasingtime) self.LaserTarget[playername]=TARGET:New(target) self:__TargetLasing(-1,client,target,lasercode,lasingtime) end return self end function PLAYERRECCE:_SetClientLaserCode(client,group,playername,code) self:T(self.lid.."_SetClientLaserCode") self.UnitLaserCodes[playername]=code or 1688 if self.ClientMenus[playername]then self.ClientMenus[playername]:Remove() self.ClientMenus[playername]=nil end self:_BuildMenus() return self end function PLAYERRECCE:_SwitchOnStation(client,group,playername) self:T(self.lid.."_SwitchOnStation") if not self.OnStation[playername]then self.OnStation[playername]=true self:__RecceOnStation(-1,client,playername) else self.OnStation[playername]=false self:__RecceOffStation(-1,client,playername) end if self.ClientMenus[playername]then self.ClientMenus[playername]:Remove() self.ClientMenus[playername]=nil end self:_BuildMenus(client) return self end function PLAYERRECCE:_SwitchSmoke(client,group,playername) self:T(self.lid.."_SwitchLasing") if not self.SmokeOwn[playername]then self.SmokeOwn[playername]=true MESSAGE:New("Smoke self is now ON",10,self.Name or"FACA"):ToClient(client) else self.SmokeOwn[playername]=false MESSAGE:New("Smoke self is now OFF",10,self.Name or"FACA"):ToClient(client) end if self.ClientMenus[playername]then self.ClientMenus[playername]:Remove() self.ClientMenus[playername]=nil end self:_BuildMenus(client) return self end function PLAYERRECCE:_SwitchLasing(client,group,playername) self:T(self.lid.."_SwitchLasing") if not self.AutoLase[playername]then self.AutoLase[playername]=true MESSAGE:New("Lasing is now ON",10,self.Name or"FACA"):ToClient(client) else self.AutoLase[playername]=false if self.LaserSpots[playername]then local laser=self.LaserSpots[playername] if laser:IsLasing()then laser:LaseOff() end self.LaserSpots[playername]=nil end MESSAGE:New("Lasing is now OFF",10,self.Name or"FACA"):ToClient(client) end if self.ClientMenus[playername]then self.ClientMenus[playername]:Remove() self.ClientMenus[playername]=nil end self:_BuildMenus(client) return self end function PLAYERRECCE:_SwitchLasingDist(client,group,playername,mindist,maxdist) self:T(self.lid.."_SwitchLasingDist") local mind=mindist or 100 local maxd=maxdist or 2000 if not self.LaserFOV[playername]then self.LaserFOV[playername]={ min=mind, max=maxd, } else self.LaserFOV[playername].min=mind self.LaserFOV[playername].max=maxd end MESSAGE:New(string.format("Laser distance set to %d-%dm!",mindist,maxdist),10,"FACA"):ToClient(client) if self.ClientMenus[playername]then self.ClientMenus[playername]:Remove() self.ClientMenus[playername]=nil end self:_BuildMenus(client) return self end function PLAYERRECCE:_WIP(client,group,playername) self:T(self.lid.."_WIP") return self end function PLAYERRECCE:_SmokeTargets(client,group,playername) self:T(self.lid.."_SmokeTargets") local cameraset=self:_GetTargetSet(client,true) local visualset=self:_GetTargetSet(client,false) if cameraset:CountAlive()>0 or visualset:CountAlive()>0 then self:__TargetsSmoked(-1,client,playername,cameraset) else return self end local highsmoke=self.SmokeColor.highsmoke local medsmoke=self.SmokeColor.medsmoke local lowsmoke=self.SmokeColor.lowsmoke local lasersmoke=self.SmokeColor.lasersmoke local laser=self.LaserSpots[playername] if laser and laser.Target and laser.Target:IsAlive()then laser.Target:GetCoordinate():Smoke(lasersmoke) end local coord=visualset:GetCoordinate() if coord and self.smokeaveragetargetpos then coord:SetAtLandheight() coord:Smoke(medsmoke) else for _,_unit in pairs(visualset.Set)do local unit=_unit if unit and unit:IsAlive()then local coord=unit:GetCoordinate() local threat=unit:GetThreatLevel() if coord then local color=lowsmoke if threat>7 then color=highsmoke elseif threat>2 then color=medsmoke end coord:Smoke(color) end end end end if self.SmokeOwn[playername]then local cc=client:GetVec2() local lc=COORDINATE:NewFromVec2(cc,1) local color=self.SmokeColor.ownsmoke lc:Smoke(color) end return self end function PLAYERRECCE:_FlareTargets(client,group,playername) self:T(self.lid.."_FlareTargets") local cameraset=self:_GetTargetSet(client,true) local visualset=self:_GetTargetSet(client,false) cameraset:AddSet(visualset) if cameraset:CountAlive()>0 then self:__TargetsFlared(-1,client,playername,cameraset) end local highsmoke=self.FlareColor.highflare local medsmoke=self.FlareColor.medflare local lowsmoke=self.FlareColor.lowflare local lasersmoke=self.FlareColor.laserflare local laser=self.LaserSpots[playername] if laser and laser.Target and laser.Target:IsAlive()then laser.Target:GetCoordinate():Flare(lasersmoke) if cameraset:IsInSet(laser.Target)then cameraset:Remove(laser.Target:GetName(),true) end end for _,_unit in pairs(cameraset.Set)do local unit=_unit if unit and unit:IsAlive()then local coord=unit:GetCoordinate() local threat=unit:GetThreatLevel() if coord then local color=lowsmoke if threat>7 then color=highsmoke elseif threat>2 then color=medsmoke end coord:Flare(color) end end end return self end function PLAYERRECCE:_IlluTargets(client,group,playername) self:T(self.lid.."_IlluTargets") local totalset,count=self:_GetKnownTargets(client) if count>0 then local coord=totalset:GetCoordinate() coord.y=coord.y+200 coord:IlluminationBomb(nil,1) self:__Illumination(1,client,playername,totalset) end return self end function PLAYERRECCE:_UploadTargets(client,group,playername) self:T(self.lid.."_UploadTargets") local totalset,count=self:_GetKnownTargets(client) if count>0 then self.Controller:AddTarget(totalset) self:__TargetReportSent(1,client,playername,totalset) end return self end function PLAYERRECCE:_ReportLaserTargets(client,group,playername) self:T(self.lid.."_ReportLaserTargets") local targetset,number=self:_GetTargetSet(client,true,true) if number>0 and self.AutoLase[playername]then local Settings=(client and _DATABASE:GetPlayerSettings(playername))or _SETTINGS local target=self:_GetHVTTarget(targetset) local ThreatLevel=target:GetThreatLevel()or 1 local ThreatLevelText="high" if ThreatLevel>3 and ThreatLevel<8 then ThreatLevelText="medium" elseif ThreatLevel<=3 then ThreatLevelText="low" end local ThreatGraph="["..string.rep("■",ThreatLevel)..string.rep("□",10-ThreatLevel).."]: "..ThreatLevel local report=REPORT:New("Lasing Report") report:Add(string.rep("-",15)) report:Add("Target type: "..target:GetTypeName()or"unknown") report:Add("Threat Level: "..ThreatGraph.." ("..ThreatLevelText..")") if not self.ReferencePoint then report:Add("Location: "..client:GetCoordinate():ToStringBULLS(self.Coalition,Settings)) if self.reporttostringbullsonly~=true then report:Add("Location: "..client:GetCoordinate():ToStringA2G(nil,Settings)) end else report:Add("Location: "..client:GetCoordinate():ToStringFromRPShort(self.ReferencePoint,self.RPName,client,Settings)) end report:Add("Laser Code: "..self.UnitLaserCodes[playername]or 1688) report:Add(string.rep("-",15)) local text=report:Text() self:__TargetReport(1,client,targetset,target,text) else local report=REPORT:New("Lasing Report") report:Add(string.rep("-",15)) report:Add("N O T A R G E T S") report:Add(string.rep("-",15)) local text=report:Text() self:__TargetReport(1,client,nil,nil,text) end return self end function PLAYERRECCE:_ReportVisualTargets(client,group,playername) self:T(self.lid.."_ReportVisualTargets") local targetset,number=self:_GetKnownTargets(client) if number>0 then local Settings=(client and _DATABASE:GetPlayerSettings(playername))or _SETTINGS local ThreatLevel=targetset:CalculateThreatLevelA2G() local ThreatLevelText="high" if ThreatLevel>3 and ThreatLevel<8 then ThreatLevelText="medium" elseif ThreatLevel<=3 then ThreatLevelText="low" end local ThreatGraph="["..string.rep("■",ThreatLevel)..string.rep("□",10-ThreatLevel).."]: "..ThreatLevel local report=REPORT:New("Target Report") report:Add(string.rep("-",15)) report:Add("Target count: "..number) report:Add("Threat Level: "..ThreatGraph.." ("..ThreatLevelText..")") if not self.ReferencePoint then report:Add("Location: "..client:GetCoordinate():ToStringBULLS(self.Coalition,Settings)) if self.reporttostringbullsonly~=true then report:Add("Location: "..client:GetCoordinate():ToStringA2G(nil,Settings)) end else report:Add("Location: "..client:GetCoordinate():ToStringFromRPShort(self.ReferencePoint,self.RPName,client,Settings)) if self.reporttostringbullsonly~=true then report:Add("Location: "..client:GetCoordinate():ToStringA2G(nil,Settings)) end end report:Add(string.rep("-",15)) local text=report:Text() self:__TargetReport(1,client,targetset,nil,text) else local report=REPORT:New("Target Report") report:Add(string.rep("-",15)) report:Add("N O T A R G E T S") report:Add(string.rep("-",15)) local text=report:Text() self:__TargetReport(1,client,nil,nil,text) end return self end function PLAYERRECCE:_BuildMenus(Client) self:T(self.lid.."_BuildMenus") local clients=self.PlayerSet local clientset=clients:GetSetObjects() if Client then clientset={Client}end for _,_client in pairs(clientset)do local client=_client if client and client:IsAlive()then local playername=client:GetPlayerName() self:T("Menu for "..playername) if not self.UnitLaserCodes[playername]then self:_SetClientLaserCode(nil,nil,playername,1688) end if self.SmokeOwn[playername]==nil then self.SmokeOwn[playername]=self.smokeownposition end local group=client:GetGroup() if not self.ClientMenus[playername]then self:T("Start Menubuild for "..playername) local canlase=self.CanLase[client:GetTypeName()] self.ClientMenus[playername]=MENU_GROUP:New(group,self.MenuName or self.Name or"RECCE") local txtonstation=self.OnStation[playername]and"ON"or"OFF" local text=string.format("Switch On-Station (%s)",txtonstation) local onstationmenu=MENU_GROUP_COMMAND:New(group,text,self.ClientMenus[playername],self._SwitchOnStation,self,client,group,playername) if self.OnStation[playername]then local smoketopmenu=MENU_GROUP:New(group,"Visual Markers",self.ClientMenus[playername]) local smokemenu=MENU_GROUP_COMMAND:New(group,"Smoke Targets",smoketopmenu,self._SmokeTargets,self,client,group,playername) local flaremenu=MENU_GROUP_COMMAND:New(group,"Flare Targets",smoketopmenu,self._FlareTargets,self,client,group,playername) local illumenu=MENU_GROUP_COMMAND:New(group,"Illuminate Area",smoketopmenu,self._IlluTargets,self,client,group,playername) local ownsm=self.SmokeOwn[playername]and"ON"or"OFF" local owntxt=string.format("Switch smoke self (%s)",ownsm) local ownsmoke=MENU_GROUP_COMMAND:New(group,owntxt,smoketopmenu,self._SwitchSmoke,self,client,group,playername) if canlase then local txtonstation=self.AutoLase[playername]and"ON"or"OFF" local text=string.format("Switch Lasing (%s)",txtonstation) local lasemenu=MENU_GROUP_COMMAND:New(group,text,self.ClientMenus[playername],self._SwitchLasing,self,client,group,playername) local lasedist=MENU_GROUP:New(group,"Set Laser Distance",self.ClientMenus[playername]) local mindist=100 local maxdist=2000 if self.LaserFOV[playername]and self.LaserFOV[playername].max then maxdist=self.LaserFOV[playername].max end local laselist={} for i=2,8 do local dist1=(i*1000)-1000 local dist2=i*1000 dist1=dist1==1000 and 100 or dist1 local text=string.format("%d-%dm",dist1,dist2) if dist2==maxdist then text=text.." (*)" end laselist[i]=MENU_GROUP_COMMAND:New(group,text,lasedist,self._SwitchLasingDist,self,client,group,playername,dist1,dist2) end end local targetmenu=MENU_GROUP:New(group,"Target Report",self.ClientMenus[playername]) if canlase then local reportL=MENU_GROUP_COMMAND:New(group,"Laser Target",targetmenu,self._ReportLaserTargets,self,client,group,playername) end local reportV=MENU_GROUP_COMMAND:New(group,"Visual Targets",targetmenu,self._ReportVisualTargets,self,client,group,playername) if self.UseController then local text=string.format("Target Upload to %s",self.Controller.MenuName or self.Controller.Name) local upload=MENU_GROUP_COMMAND:New(group,text,targetmenu,self._UploadTargets,self,client,group,playername) end if canlase then local lasecodemenu=MENU_GROUP:New(group,"Set Laser Code",self.ClientMenus[playername]) local codemenu={} for _,_code in pairs(self.LaserCodes)do if _code==self.UnitLaserCodes[playername]then _code=tostring(_code).."(*)" end codemenu[playername.._code]=MENU_GROUP_COMMAND:New(group,tostring(_code),lasecodemenu,self._SetClientLaserCode,self,client,group,playername,_code) end end end end end end return self end function PLAYERRECCE:_CheckNewTargets(targetset,client,playername) self:T(self.lid.."_CheckNewTargets") local tempset=SET_UNIT:New() targetset:ForEach( function(unit) if unit and unit:IsAlive()then self:T("Report unit: "..unit:GetName()) if not unit.PlayerRecceDetected then self:T("New unit: "..unit:GetName()) unit.PlayerRecceDetected={ detected=true, recce=client, playername=playername, timestamp=timer.getTime() } tempset:Add(unit:GetName(),unit) if not self.TargetCache:HasUniqueID(unit:GetName())then self.TargetCache:Push(unit,unit:GetName()) end end if unit.PlayerRecceDetected and unit.PlayerRecceDetected.timestamp then local TNow=timer.getTime() if TNow-unit.PlayerRecceDetected.timestamp>self.TForget then unit.PlayerRecceDetected={ detected=true, recce=client, playername=playername, timestamp=timer.getTime() } if not self.TargetCache:HasUniqueID(unit:GetName())then self.TargetCache:Push(unit,unit:GetName()) end tempset:Add(unit:GetName(),unit) end end end end ) local targetsbyclock={} for i=1,12 do targetsbyclock[i]={} end tempset:ForEach( function(object) local obj=object local clock=self:_GetClockDirection(client,obj) table.insert(targetsbyclock[clock],obj) end ) self:T("Known target Count: "..self.TargetCache:Count()) if tempset:CountAlive()>0 then self:TargetDetected(targetsbyclock,client,playername) end return self end function PLAYERRECCE:SetSRS(Frequency,Modulation,PathToSRS,Gender,Culture,Port,Voice,Volume,PathToGoogleKey,Backend,Provider,Speaker) self:T(self.lid.."SetSRS") self.PathToSRS=PathToSRS or MSRS.path or"C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio" self.Gender=Gender or MSRS.gender or"male" self.Culture=Culture or MSRS.culture or"en-US" self.Port=Port or MSRS.port or 5002 self.Voice=Voice or MSRS.voice self.PathToGoogleKey=PathToGoogleKey self.Volume=Volume or 1.0 self.UseSRS=true self.Frequency=Frequency or{127,251} self.BCFrequency=self.Frequency self.Modulation=Modulation or{radio.modulation.FM,radio.modulation.AM} self.BCModulation=self.Modulation self.SRS=MSRS:New(self.PathToSRS,self.Frequency,self.Modulation) self.SRS:SetCoalition(self.Coalition) self.SRS:SetLabel(self.MenuName or self.Name) self.SRS:SetGender(self.Gender) self.SRS:SetCulture(self.Culture) self.SRS:SetPort(self.Port) self.SRS:SetVolume(self.Volume) if Backend then self.SRS:SetBackend(Backend) end if self.PathToGoogleKey then self.SRS:SetProviderOptionsGoogle(self.PathToGoogleKey,self.PathToGoogleKey) self.SRS:SetProvider(MSRS.Provider.GOOGLE) end if Provider then self.SRS:SetProvider(Provider) end if(not PathToGoogleKey)and self.SRS:GetProvider()==MSRS.Provider.GOOGLE then self.PathToGoogleKey=MSRS.poptions.gcloud.credentials self.Voice=Voice or MSRS.poptions.gcloud.voice end self.SRS:SetVoice(self.Voice) if Speaker then self.SRS:SetSpeakerPiper(Speaker) end self.SRSQueue=MSRSQUEUE:New(self.MenuName or self.Name) self.SRSQueue:SetTransmitOnlyWithPlayers(self.TransmitOnlyWithPlayers) return self end function PLAYERRECCE:SetTransmitOnlyWithPlayers(Switch) self.TransmitOnlyWithPlayers=Switch if self.SRSQueue then self.SRSQueue:SetTransmitOnlyWithPlayers(Switch) end return self end function PLAYERRECCE:SetMenuName(Name) self:T(self.lid.."SetMenuName: "..Name) self.MenuName=Name return self end function PLAYERRECCE:SetReportBullsOnly(OnOff) self:T(self.lid.."SetReportBullsOnly: "..tostring(OnOff)) self.reporttostringbullsonly=OnOff return self end function PLAYERRECCE:EnableSmokeOwnPosition() self:T(self.lid.."EnableSmokeOwnPosition") self.smokeownposition=true return self end function PLAYERRECCE:EnableKiowaAutolase() self:T(self.lid.."EnableKiowaAutolase") self.CanLase.OH58D=true return self end function PLAYERRECCE:DisableSmokeOwnPosition() self:T(self.lid.."DisableSmokeOwnPosition") self.smokeownposition=false return self end function PLAYERRECCE:EnableSmokeAverageTargetPosition() self:T(self.lid.."ENableSmokeOwnPosition") self.smokeaveragetargetpos=true return self end function PLAYERRECCE:DisableSmokeAverageTargetPosition() self:T(self.lid.."DisableSmokeAverageTargetPosition") self.smokeaveragetargetpos=false return self end function PLAYERRECCE:_GetTextForSpeech(text) text=string.gsub(text,"%d","%1 ") text=string.gsub(text,"^%s*","") text=string.gsub(text,"%s*$","") text=string.gsub(text,"0","zero") text=string.gsub(text,"9","niner") text=string.gsub(text," "," ") return text end function PLAYERRECCE:onafterStatus(From,Event,To) self:T({From,Event,To}) if not self.timestamp then self.timestamp=timer.getTime() else local tNow=timer.getTime() if tNow-self.timestamp>=60 then self:_CleanupTargetCache() self.timestamp=timer.getTime() end end self:_BuildMenus() self.PlayerSet:ForEachClient( function(Client) local client=Client local playername=client:GetPlayerName() local cameraison=self:_CameraOn(client,playername) if client and client:IsAlive()and self.OnStation[playername]then local targetset,targetcount,tzone=nil,0,nil local laserset,lzone=nil,nil local vistargetset,vistargetcount,viszone=nil,0,nil if cameraison then targetset,targetcount,tzone=self:_GetTargetSet(client,true) if targetset then if self.ViewZone[playername]then self.ViewZone[playername]:UndrawZone() end if self.debug and tzone then self.ViewZone[playername]=tzone:DrawZone(self.Coalition,{0,0,1},nil,nil,nil,1) end end self:T({targetcount=targetcount}) end if self.AutoLase[playername]and cameraison then laserset,targetcount,lzone=self:_GetTargetSet(client,true,true) if targetcount>0 or self.LaserTarget[playername]then if self.CanLase[client:GetTypeName()]then self:_LaseTarget(client,laserset) end end if lzone then if self.ViewZoneLaser[playername]then self.ViewZoneLaser[playername]:UndrawZone() end if self.debug and tzone then self.ViewZoneLaser[playername]=lzone:DrawZone(self.Coalition,{0,1,0},nil,nil,nil,1) end end self:T({lasercount=targetcount}) end vistargetset,vistargetcount,viszone=self:_GetTargetSet(client,false) if vistargetset then if self.ViewZoneVisual[playername]then self.ViewZoneVisual[playername]:UndrawZone() end if self.debug and viszone then self.ViewZoneVisual[playername]=viszone:DrawZone(self.Coalition,{1,0,0},nil,nil,nil,3) end end self:T({visualtargetcount=vistargetcount}) if targetset then vistargetset:AddSet(targetset) end if laserset then vistargetset:AddSet(laserset) end if not cameraison and self.debug then if self.ViewZoneLaser[playername]then self.ViewZoneLaser[playername]:UndrawZone() end if self.ViewZone[playername]then self.ViewZone[playername]:UndrawZone() end end self:_CheckNewTargets(vistargetset,client,playername) end end ) self:__Status(-10) return self end function PLAYERRECCE:onafterRecceOnStation(From,Event,To,Client,Playername) self:T({From,Event,To}) local callsign=Client:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs) local coord=Client:GetCoordinate() local coordtext=coord:ToStringBULLS(self.Coalition) if self.ReferencePoint then local Settings=Client and _DATABASE:GetPlayerSettings(Playername)or _SETTINGS coordtext=coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,Client,Settings) end local text1="Party time!" local text2=string.format("All stations, FACA %s on station\nat %s!",callsign,coordtext) local text2tts=string.format(" All stations, FACA %s on station at %s!",callsign,coordtext) text2tts=self:_GetTextForSpeech(text2tts) if self.debug then self:T(text2.."\n"..text2tts) end if self.UseSRS then local grp=Client:GetGroup() local coord=grp:GetCoordinate() if coord then self.SRS:SetCoordinate(coord) end self.SRSQueue:NewTransmission(text1,nil,self.SRS,nil,2) self.SRSQueue:NewTransmission(text2tts,nil,self.SRS,nil,3) MESSAGE:New(text2,10,self.Name or"FACA"):ToCoalition(self.Coalition) else MESSAGE:New(text1,10,self.Name or"FACA"):ToClient(Client) MESSAGE:New(text2,10,self.Name or"FACA"):ToCoalition(self.Coalition) end return self end function PLAYERRECCE:onafterRecceOffStation(From,Event,To,Client,Playername) self:T({From,Event,To}) local callsign=Client:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs) local coord=Client:GetCoordinate() local coordtext=coord:ToStringBULLS(self.Coalition) if self.ReferencePoint then local Settings=Client and _DATABASE:GetPlayerSettings(Playername)or _SETTINGS coordtext=coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,Client,Settings) end local text=string.format("All stations, FACA %s leaving station\nat %s, good bye!",callsign,coordtext) local texttts=string.format("All stations, FACA %s leaving station at %s, good bye!",callsign,coordtext) texttts=self:_GetTextForSpeech(texttts) if self.debug then self:T(text.."\n"..texttts) end local text1="Going home!" if self.UseSRS then local grp=Client:GetGroup() local coord=grp:GetCoordinate() if coord then self.SRS:SetCoordinate(coord) end self.SRSQueue:NewTransmission(text1,nil,self.SRS,nil,2) self.SRSQueue:NewTransmission(texttts,nil,self.SRS,nil,3) MESSAGE:New(text,10,self.Name or"FACA"):ToCoalition(self.Coalition) else MESSAGE:New(text,10,self.Name or"FACA"):ToCoalition(self.Coalition) end return self end function PLAYERRECCE:onafterTargetDetected(From,Event,To,Targetsbyclock,Client,Playername) self:T({From,Event,To}) local dunits="meters" local Settings=Client and _DATABASE:GetPlayerSettings(Playername)or _SETTINGS local clientcoord=Client:GetCoordinate() for i=1,12 do local targets=Targetsbyclock[i] local targetno=#targets if targetno==1 then local targetdistance=clientcoord:Get2DDistance(targets[1]:GetCoordinate())or 100 local Threatlvl=targets[1]:GetThreatLevel() local ThreatTxt="Low" if Threatlvl>=7 then ThreatTxt="Medium" elseif Threatlvl>=3 then ThreatTxt="High" end if Settings:IsMetric()then targetdistance=UTILS.Round(targetdistance,-2) if targetdistance>=1000 then targetdistance=UTILS.Round(targetdistance/1000,0) dunits="kilometer" end else if UTILS.MetersToNM(targetdistance)>=1 then targetdistance=UTILS.Round(UTILS.MetersToNM(targetdistance),0) dunits="miles" else targetdistance=UTILS.Round(UTILS.MetersToFeet(targetdistance),-2) dunits="feet" end end local text=string.format("Target! %s! %s o\'clock, %d %s!",ThreatTxt,i,targetdistance,dunits) local ttstext=string.format("Target! %s! %s oh clock, %d %s!",ThreatTxt,i,targetdistance,dunits) if self.UseSRS then local grp=Client:GetGroup() if clientcoord then self.SRS:SetCoordinate(clientcoord) end self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,1,{grp},text,10) else MESSAGE:New(text,10,self.Name or"FACA"):ToClient(Client) end elseif targetno>1 then local function GetNearest(TTable) local distance=10000000 for _,_unit in pairs(TTable)do local dist=clientcoord:Get2DDistance(_unit:GetCoordinate())or 100 if dist=1000 then targetdistance=UTILS.Round(targetdistance/1000,0) dunits="kilometer" end else if UTILS.MetersToNM(targetdistance)>=1 then targetdistance=UTILS.Round(UTILS.MetersToNM(targetdistance),0) dunits="miles" else targetdistance=UTILS.Round(UTILS.MetersToFeet(targetdistance),-2) dunits="feet" end end local text=string.format(" %d targets! %s o\'clock, %d %s!",targetno,i,targetdistance,dunits) local ttstext=string.format("%d targets! %s oh clock, %d %s!",targetno,i,targetdistance,dunits) if self.UseSRS then local grp=Client:GetGroup() local coord=grp:GetCoordinate() if coord then self.SRS:SetCoordinate(coord) end self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,1,{grp},text,10) else MESSAGE:New(text,10,self.Name or"FACA"):ToClient(Client) end end end return self end function PLAYERRECCE:onafterIllumination(From,Event,To,Client,Playername,TargetSet) self:T({From,Event,To}) local callsign=Client:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs) local coord=Client:GetCoordinate() local coordtext=coord:ToStringBULLS(self.Coalition) if self.AttackSet then for _,_client in pairs(self.AttackSet.Set)do local client=_client if client and client:IsAlive()then local Settings=client and _DATABASE:GetPlayerSettings(client:GetPlayerName())or _SETTINGS local coordtext=coord:ToStringA2G(client,Settings) if self.ReferencePoint then coordtext=coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,client,Settings) end local text=string.format("All stations, %s fired illumination\nat %s!",callsign,coordtext) MESSAGE:New(text,15,self.Name or"FACA"):ToClient(client) end end end local text="Sunshine!" local ttstext="Sunshine!" if self.UseSRS then local grp=Client:GetGroup() local coord=grp:GetCoordinate() if coord then self.SRS:SetCoordinate(coord) end self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,1,{grp},text,10) else MESSAGE:New(text,10,self.Name or"FACA"):ToClient(Client) end return self end function PLAYERRECCE:onafterTargetsSmoked(From,Event,To,Client,Playername,TargetSet) self:T({From,Event,To}) local callsign=Client:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs) local coord=Client:GetCoordinate() local coordtext=coord:ToStringBULLS(self.Coalition) if self.AttackSet then for _,_client in pairs(self.AttackSet.Set)do local client=_client if client and client:IsAlive()then local Settings=client and _DATABASE:GetPlayerSettings(client:GetPlayerName())or _SETTINGS local coordtext=coord:ToStringA2G(client,Settings) if self.ReferencePoint then coordtext=coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,client,Settings) end local text=string.format("All stations, %s smoked targets\nat %s!",callsign,coordtext) MESSAGE:New(text,15,self.Name or"FACA"):ToClient(client) end end end local text="Smoke on!" local ttstext="Smoke and Mirrors!" if self.UseSRS then local grp=Client:GetGroup() local coord=grp:GetCoordinate() if coord then self.SRS:SetCoordinate(coord) end self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,1,{grp},text,10) else MESSAGE:New(text,10,self.Name or"FACA"):ToClient(Client) end return self end function PLAYERRECCE:onafterTargetsFlared(From,Event,To,Client,Playername,TargetSet) self:T({From,Event,To}) local callsign=Client:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs) local coord=Client:GetCoordinate() local coordtext=coord:ToStringBULLS(self.Coalition) if self.AttackSet then for _,_client in pairs(self.AttackSet.Set)do local client=_client if client and client:IsAlive()then local Settings=client and _DATABASE:GetPlayerSettings(client:GetPlayerName())or _SETTINGS if self.ReferencePoint then coordtext=coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,client,Settings) end local coordtext=coord:ToStringA2G(client,Settings) local text=string.format("All stations, %s flared targets\nat %s!",callsign,coordtext) MESSAGE:New(text,15,self.Name or"FACA"):ToClient(client) end end end local text="Fireworks!" local ttstext="Fire works!" if self.UseSRS then local grp=Client:GetGroup() local coord=grp:GetCoordinate() if coord then self.SRS:SetCoordinate(coord) end self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,1,{grp},text,10) else MESSAGE:New(text,10,self.Name or"FACA"):ToClient(Client) end return self end function PLAYERRECCE:onafterTargetLasing(From,Event,To,Client,Target,Lasercode,Lasingtime) self:T({From,Event,To}) local callsign=Client:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs) local Settings=(Client and _DATABASE:GetPlayerSettings(Client:GetPlayerName()))or _SETTINGS local coord=Client:GetCoordinate() local coordtext=coord:ToStringBULLS(self.Coalition,Settings) if self.ReferencePoint then coordtext=coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,Client,Settings) end local targettype=Target:GetTypeName() if self.AttackSet then for _,_client in pairs(self.AttackSet.Set)do local client=_client if client and client:IsAlive()then local Settings=client and _DATABASE:GetPlayerSettings(client:GetPlayerName())or _SETTINGS if self.ReferencePoint then coordtext=coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,client,Settings) end local coordtext=coord:ToStringA2G(client,Settings) local text=string.format("All stations, %s lasing %s\nat %s!\nCode %d, Duration %d plus seconds!",callsign,targettype,coordtext,Lasercode,Lasingtime) MESSAGE:New(text,15,self.Name or"FACA"):ToClient(client) end end end local text="Lasing!" local ttstext="Laser on!" if self.UseSRS then local grp=Client:GetGroup() local coord=grp:GetCoordinate() if coord then self.SRS:SetCoordinate(coord) end self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,1,{grp},text,10) else MESSAGE:New(text,10,self.Name or"FACA"):ToClient(Client) end return self end function PLAYERRECCE:onafterShack(From,Event,To,Client,Target) self:T({From,Event,To}) local callsign=Client:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs) local Settings=(Client and _DATABASE:GetPlayerSettings(Client:GetPlayerName()))or _SETTINGS local coord=Client:GetCoordinate() local coordtext=coord:ToStringBULLS(self.Coalition,Settings) if self.ReferencePoint then coordtext=coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,Client,Settings) end local targettype="target" if self.AttackSet then for _,_client in pairs(self.AttackSet.Set)do local client=_client if client and client:IsAlive()then local Settings=client and _DATABASE:GetPlayerSettings(client:GetPlayerName())or _SETTINGS if self.ReferencePoint then coordtext=coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,client,Settings) end local coordtext=coord:ToStringA2G(client,Settings) local text=string.format("All stations, %s good hit on %s\nat %s!",callsign,targettype,coordtext) MESSAGE:New(text,15,self.Name or"FACA"):ToClient(client) end end end local text="Shack!" local ttstext="Shack!" if self.UseSRS then local grp=Client:GetGroup() local coord=grp:GetCoordinate() if coord then self.SRS:SetCoordinate(coord) end self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,1) else MESSAGE:New(text,10,self.Name or"FACA"):ToClient(Client) end return self end function PLAYERRECCE:onafterTargetLOSLost(From,Event,To,Client,Target) self:T({From,Event,To}) local callsign=Client:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs) local Settings=(Client and _DATABASE:GetPlayerSettings(Client:GetPlayerName()))or _SETTINGS local coord=Client:GetCoordinate() local coordtext=coord:ToStringBULLS(self.Coalition,Settings) if self.ReferencePoint then coordtext=coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,Client,Settings) end local targettype="target" if self.AttackSet then for _,_client in pairs(self.AttackSet.Set)do local client=_client if client and client:IsAlive()then local Settings=client and _DATABASE:GetPlayerSettings(client:GetPlayerName())or _SETTINGS if self.ReferencePoint then coordtext=coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,client,Settings) end local coordtext=coord:ToStringA2G(client,Settings) local text=string.format("All stations, %s lost sight of %s\nat %s!",callsign,targettype,coordtext) MESSAGE:New(text,15,self.Name or"FACA"):ToClient(client) end end end local text="Lost LOS!" local ttstext="Lost L O S!" if self.UseSRS then local grp=Client:GetGroup() local coord=grp:GetCoordinate() if coord then self.SRS:SetCoordinate(coord) end self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,1,{grp},text,10) else MESSAGE:New(text,10,self.Name or"FACA"):ToClient(Client) end return self end function PLAYERRECCE:onafterTargetReport(From,Event,To,Client,TargetSet,Target,Text) self:T({From,Event,To}) MESSAGE:New(Text,45,self.Name or"FACA"):ToClient(Client) if self.AttackSet then for _,_client in pairs(self.AttackSet.Set)do local client=_client if client and client:IsAlive()and client~=Client then MESSAGE:New(Text,45,self.Name or"FACA"):ToClient(client) end end end return self end function PLAYERRECCE:onafterTargetReportSent(From,Event,To,Client,Playername,TargetSet) self:T({From,Event,To}) local text="Upload completed!" if self.UseSRS then local grp=Client:GetGroup() local coord=grp:GetCoordinate() if coord then self.SRS:SetCoordinate(coord) end self.SRSQueue:NewTransmission(text,nil,self.SRS,nil,1,{grp},text,10) else MESSAGE:New(text,10,self.Name or"FACA"):ToClient(Client) end return self end function PLAYERRECCE:onafterStop(From,Event,To) self:I({From,Event,To}) self:UnHandleEvent(EVENTS.PlayerLeaveUnit) self:UnHandleEvent(EVENTS.Ejection) self:UnHandleEvent(EVENTS.Crash) self:UnHandleEvent(EVENTS.PilotDead) self:UnHandleEvent(EVENTS.PlayerEnterAircraft) return self end SQUADRON={ ClassName="SQUADRON", verbose=0, modex=nil, modexcounter=0, callsignName=nil, callsigncounter=11, tankerSystem=nil, refuelSystem=nil, } SQUADRON.version="0.8.1" function SQUADRON:New(TemplateGroupName,Ngroups,SquadronName) local self=BASE:Inherit(self,COHORT:New(TemplateGroupName,Ngroups,SquadronName)) self:AddMissionCapability(AUFTRAG.Type.ORBIT) self.isAir=true self.refuelSystem=select(2,self.templategroup:GetUnit(1):IsRefuelable()) self.tankerSystem=select(2,self.templategroup:GetUnit(1):IsTanker()) return self end function SQUADRON:SetGrouping(nunits) self.ngrouping=nunits or 2 if self.ngrouping<1 then self.ngrouping=1 end if self.ngrouping>4 then self.ngrouping=4 end return self end function SQUADRON:SetParkingIDs(ParkingIDs) if type(ParkingIDs)~="table"then ParkingIDs={ParkingIDs} end self.parkingIDs=ParkingIDs return self end function SQUADRON:SetTakeoffType(TakeoffType) TakeoffType=TakeoffType or"Cold" if TakeoffType:lower()=="hot"then self.takeoffType=COORDINATE.WaypointType.TakeOffParkingHot elseif TakeoffType:lower()=="cold"then self.takeoffType=COORDINATE.WaypointType.TakeOffParking elseif TakeoffType:lower()=="air"then self.takeoffType=COORDINATE.WaypointType.TurningPoint else self.takeoffType=COORDINATE.WaypointType.TakeOffParking end return self end function SQUADRON:SetTakeoffCold() self:SetTakeoffType("Cold") return self end function SQUADRON:SetTakeoffHot() self:SetTakeoffType("Hot") return self end function SQUADRON:SetTakeoffAir() self:SetTakeoffType("Air") return self end function SQUADRON:SetDespawnAfterLanding(Switch) if Switch then self.despawnAfterLanding=Switch else self.despawnAfterLanding=true end return self end function SQUADRON:SetDespawnAfterHolding(Switch) if Switch then self.despawnAfterHolding=Switch else self.despawnAfterHolding=true end return self end function SQUADRON:SetFuelLowThreshold(LowFuel) self.fuellow=LowFuel or 25 return self end function SQUADRON:SetFuelLowRefuel(switch) if switch==false then self.fuellowRefuel=false else self.fuellowRefuel=true end return self end function SQUADRON:SetAirwing(Airwing) self.legion=Airwing return self end function SQUADRON:GetAirwing(Airwing) return self.legion end function SQUADRON:onafterStart(From,Event,To) local text=string.format("Starting SQUADRON",self.name) self:T(self.lid..text) self:__Status(-1) end function SQUADRON:onafterStatus(From,Event,To) if self.verbose>=1 then local fsmstate=self:GetState() local callsign=self.callsignName and UTILS.GetCallsignName(self.callsignName)or"N/A" local modex=self.modex and self.modex or-1 local skill=self.skill and tostring(self.skill)or"N/A" local NassetsTot=#self.assets local NassetsInS=self:CountAssets(true) local NassetsQP=0;local NassetsP=0;local NassetsQ=0 if self.legion then NassetsQP,NassetsP,NassetsQ=self.legion:CountAssetsOnMission(nil,self) end local text=string.format("%s [Type=%s, Call=%s, Modex=%d, Skill=%s]: Assets Total=%d, Stock=%d, Mission=%d [Active=%d, Queue=%d]", fsmstate,self.aircrafttype,callsign,modex,skill,NassetsTot,NassetsInS,NassetsQP,NassetsP,NassetsQ) self:I(self.lid..text) self:_CheckAssetStatus() end if not self:IsStopped()then self:__Status(-60) end end TARGET={ ClassName="TARGET", verbose=0, lid=nil, targets={}, targetcounter=0, life=0, life0=0, N0=0, Ntargets0=0, Ndestroyed=0, Ndead=0, elements={}, casualties={}, threatlevel0=0, conditionStart={}, TStatus=30, } TARGET.ObjectType={ GROUP="Group", UNIT="Unit", STATIC="Static", SCENERY="Scenery", COORDINATE="Coordinate", AIRBASE="Airbase", ZONE="Zone", OPSZONE="OpsZone" } TARGET.Category={ AIRCRAFT="Aircraft", GROUND="Ground", NAVAL="Naval", AIRBASE="Airbase", COORDINATE="Coordinate", ZONE="Zone", } TARGET.ObjectStatus={ ALIVE="Alive", DEAD="Dead", DAMAGED="Damaged", } _TARGETID=0 TARGET.version="0.7.1" function TARGET:New(TargetObject) local self=BASE:Inherit(self,FSM:New()) _TARGETID=_TARGETID+1 self.uid=_TARGETID if TargetObject then self:AddObject(TargetObject) end self:SetPriority() self:SetImportance() self.TStatus=30 self.lid=string.format("TARGET #%03d | ",_TARGETID) self:SetStartState("Stopped") self:AddTransition("Stopped","Start","Alive") self:AddTransition("*","Status","*") self:AddTransition("*","Stop","Stopped") self:AddTransition("*","ObjectDamaged","*") self:AddTransition("*","ObjectDestroyed","*") self:AddTransition("*","ObjectDead","*") self:AddTransition("*","Damaged","Damaged") self:AddTransition("*","Destroyed","Dead") self:AddTransition("*","Dead","Dead") self:__Start(-0.1) return self end function TARGET:AddObject(Object) if Object:IsInstanceOf("SET_GROUP")or Object:IsInstanceOf("SET_UNIT")or Object:IsInstanceOf("SET_STATIC")or Object:IsInstanceOf("SET_SCENERY")or Object:IsInstanceOf("SET_OPSGROUP")or Object:IsInstanceOf("SET_OPSZONE")then local set=Object for _,object in pairs(set.Set)do self:AddObject(object) end elseif Object:IsInstanceOf("SET_ZONE")then local set=Object set:SortByName() for index,ZoneName in pairs(set.Index)do local zone=set.Set[ZoneName] self:_AddObject(zone) end else if Object:IsInstanceOf("OPSGROUP")then self:_AddObject(Object:GetGroup()) else self:_AddObject(Object) end end return self end function TARGET:SetPriority(Priority) self.prio=Priority or 50 return self end function TARGET:SetImportance(Importance) self.importance=Importance return self end function TARGET:AddConditionStart(ConditionFunction,...) local condition={} condition.func=ConditionFunction condition.arg={} if arg then condition.arg=arg end table.insert(self.conditionStart,condition) return self end function TARGET:AddConditionStop(ConditionFunction,...) local condition={} condition.func=ConditionFunction condition.arg={} if arg then condition.arg=arg end table.insert(self.conditionStop,condition) return self end function TARGET:EvalConditionsAll(Conditions) for _,_condition in pairs(Conditions or{})do local condition=_condition local istrue=condition.func(unpack(condition.arg)) if not istrue then return false end end return true end function TARGET:EvalConditionsAny(Conditions) for _,_condition in pairs(Conditions or{})do local condition=_condition local istrue=condition.func(unpack(condition.arg)) if istrue then return true end end return false end function TARGET:AddResource(MissionType,Nmin,Nmax,Attributes,Properties) if Attributes and type(Attributes)~="table"then Attributes={Attributes} end if Properties and type(Properties)~="table"then Properties={Properties} end local resource={} resource.MissionType=MissionType resource.Nmin=Nmin or 1 resource.Nmax=Nmax or 1 resource.Attributes=Attributes or{} resource.Properties=Properties or{} self.resources=self.resources or{} table.insert(self.resources,resource) if self.verbose>10 then local text="Resource:" for _,_r in pairs(self.resources)do local r=_r text=text..string.format("\nmission=%s, Nmin=%d, Nmax=%d, attribute=%s, properties=%s",r.MissionType,r.Nmin,r.Nmax,tostring(r.Attributes[1]),tostring(r.Properties[1])) end self:I(self.lid..text) end return resource end function TARGET:IsAlive() for _,_target in pairs(self.targets)do local target=_target if target.Status~=TARGET.ObjectStatus.DEAD then if self.isDestroyed then self:E(self.lid..string.format("ERROR: target is DESTROYED but target object status is not DEAD but %s for object %s",target.Status,target.Name)) elseif self:IsDead()then self:E(self.lid..string.format("ERROR: target is DEAD but target object status is not DEAD but %s for object %s",target.Status,target.Name)) end return true end end return false end function TARGET:IsDestroyed() return self.isDestroyed end function TARGET:IsDead() local is=self:Is("Dead") return is end function TARGET:IsTargetDead(TargetObject) local isDead=TargetObject.Status==TARGET.ObjectStatus.DEAD return isDead end function TARGET:IsTargetAlive(TargetObject) local isAlive=TargetObject.Status==TARGET.ObjectStatus.ALIVE return isAlive end function TARGET:onafterStart(From,Event,To) self:T({From,Event,To}) local text=string.format("Starting Target") self:T(self.lid..text) self:HandleEvent(EVENTS.Dead,self.OnEventUnitDeadOrLost) self:HandleEvent(EVENTS.UnitLost,self.OnEventUnitDeadOrLost) self:HandleEvent(EVENTS.RemoveUnit,self.OnEventUnitDeadOrLost) self:__Status(-1) return self end function TARGET:onafterStatus(From,Event,To) local fsmstate=self:GetState() local damaged=false for i,_target in pairs(self.targets)do local target=_target local life=target.Life target.Life=self:GetTargetLife(target) if target.Life>target.Life0 then local delta=2*(target.Life-target.Life0) target.Life0=target.Life0+delta life=target.Life0 self.life0=self.life0+delta end if target.Life object dead now for target object %s!",tostring(target.Name))) self:ObjectDead(target) damaged=true end end if damaged then self:Damaged() end if self.verbose>=1 then local text=string.format("%s: Targets=%d/%d [%d, %d], Life=%.1f/%.1f, Damage=%.1f", fsmstate,self:CountTargets(),self.N0,self.Ndestroyed,self.Ndead,self:GetLife(),self:GetLife0(),self:GetDamage()) if self:CountTargets()==0 or self:GetDamage()>=100 then text=text.." - Dead!" elseif damaged then text=text.." - Damaged!" end self:I(self.lid..text) end if self.verbose>=2 then local text="Target:" for i,_target in pairs(self.targets)do local target=_target local damage=(1-target.Life/target.Life0)*100 text=text..string.format("\n[%d] %s %s %s: Life=%.1f/%.1f, Damage=%.1f, N0=%d, Ndestroyed=%d, Ndead=%d", i,target.Type,target.Name,target.Status,target.Life,target.Life0,damage,target.N0,target.Ndestroyed,target.Ndead) end self:I(self.lid..text) end if self:IsAlive()and(self:CountTargets()==0 or self:GetDamage()>=100)then self:Dead() end for i,_target in pairs(self.targets)do local target=_target if target.Ndestroyed>target.N0 then self:E(self.lid..string.format("ERROR: Number of destroyed target objects greater than number of initial target objects: %d>%d!",target.Ndestroyed,target.N0)) end if target.Ndestroyed>target.N0 then self:E(self.lid..string.format("ERROR: Number of dead target objects greater than number of initial target objects: %d>%d!",target.Ndead,target.N0)) end end if self:IsAlive()then self:__Status(-self.TStatus) else self:I(self.lid..string.format("Target is not alive any more ==> no further status updates are carried out")) end return self end function TARGET:onafterObjectDamaged(From,Event,To,Target) self:T({From,Event,To}) self:T(self.lid..string.format("Object %s damaged",Target.Name)) return self end function TARGET:onafterObjectDestroyed(From,Event,To,Target) self:T({From,Event,To}) self:T(self.lid..string.format("Object %s destroyed",Target.Name)) self.Ndestroyed=self.Ndestroyed+1 Target.Ndestroyed=Target.Ndestroyed+1 Target.Life=0 self:ObjectDead(Target) return self end function TARGET:onafterObjectDead(From,Event,To,Target) self:T({From,Event,To}) self:T(self.lid..string.format("Object %s dead",Target.Name)) Target.Status=TARGET.ObjectStatus.DEAD Target.Ndead=Target.Ndead+1 Target.Life=0 self.Ndead=self.Ndead+1 local dead=true for _,_target in pairs(self.targets)do local target=_target if target.Status==TARGET.ObjectStatus.ALIVE then dead=false break end end if dead then if self.Ndestroyed==self.Ntargets0 then self.isDestroyed=true self:Destroyed() else self:Dead() end else self:Damaged() end return self end function TARGET:onafterDamaged(From,Event,To) self:T({From,Event,To}) self:T(self.lid..string.format("TARGET damaged")) return self end function TARGET:onafterDestroyed(From,Event,To) self:T({From,Event,To}) self:T(self.lid..string.format("TARGET destroyed")) self:Dead() return self end function TARGET:onafterDead(From,Event,To) self:T({From,Event,To}) self:T(self.lid..string.format("TARGET dead")) return self end function TARGET:OnEventUnitDeadOrLost(EventData) local Name=EventData and EventData.IniUnitName or nil if self:IsElement(Name)and not self:IsCasualty(Name)then self:T(self.lid..string.format("EVENT ID=%d: Unit %s dead or lost!",EventData.id,tostring(Name))) table.insert(self.casualties,Name) local target=self:GetTargetByName(EventData.IniGroupName) if not target then target=self:GetTargetByName(EventData.IniUnitName) end if target then local Ndead=target.Ndead local Ndestroyed=target.Ndestroyed if EventData.id==EVENTS.RemoveUnit then Ndead=Ndead+1 else Ndestroyed=Ndestroyed+1 Ndead=Ndead+1 end if Ndead==target.N0 then if Ndestroyed>=target.N0 then self:T2(self.lid..string.format("EVENT ID=%d: target %s dead/lost ==> destroyed",EventData.id,tostring(target.Name))) target.Life=0 self:ObjectDestroyed(target) else self:T2(self.lid..string.format("EVENT ID=%d: target %s removed ==> dead",EventData.id,tostring(target.Name))) target.Life=0 self:ObjectDead(target) end end end end return self end function TARGET:_AddObject(Object) local target={} target.N0=0 target.Ndead=0 target.Ndestroyed=0 if Object:IsInstanceOf("GROUP")then local group=Object target.Type=TARGET.ObjectType.GROUP target.Name=group:GetName() target.Coordinate=group:GetCoordinate() local units=group:GetUnits() target.Life=0;target.Life0=0 for _,_unit in pairs(units or{})do local unit=_unit local life=unit:GetLife() target.Life=target.Life+life target.Life0=target.Life0+math.max(unit:GetLife0(),life) self.threatlevel0=self.threatlevel0+unit:GetThreatLevel() table.insert(self.elements,unit:GetName()) target.N0=target.N0+1 end elseif Object:IsInstanceOf("UNIT")then local unit=Object target.Type=TARGET.ObjectType.UNIT target.Name=unit:GetName() target.Coordinate=unit:GetCoordinate() if unit then target.Life=unit:GetLife() target.Life0=math.max(unit:GetLife0(),target.Life) self.threatlevel0=self.threatlevel0+unit:GetThreatLevel() table.insert(self.elements,unit:GetName()) target.N0=target.N0+1 end elseif Object:IsInstanceOf("STATIC")then local static=Object target.Type=TARGET.ObjectType.STATIC target.Name=static:GetName() target.Coordinate=static:GetCoordinate() if static and static:IsAlive()then target.Life0=static:GetLife0() target.Life=static:GetLife() target.N0=target.N0+1 table.insert(self.elements,target.Name) end elseif Object:IsInstanceOf("SCENERY")then local scenery=Object target.Type=TARGET.ObjectType.SCENERY target.Name=scenery:GetName() target.Coordinate=scenery:GetCoordinate() target.Life0=scenery:GetLife0() if target.Life0==0 then target.Life0=1 end target.Life=scenery:GetLife() target.N0=target.N0+1 table.insert(self.elements,target.Name) elseif Object:IsInstanceOf("AIRBASE")then local airbase=Object target.Type=TARGET.ObjectType.AIRBASE target.Name=airbase:GetName() target.Coordinate=airbase:GetCoordinate() target.Life0=1 target.Life=1 target.N0=target.N0+1 table.insert(self.elements,target.Name) elseif Object:IsInstanceOf("COORDINATE")then local coord=UTILS.DeepCopy(Object) target.Type=TARGET.ObjectType.COORDINATE target.Name=coord:ToStringMGRS() target.Coordinate=coord target.Life0=1 target.Life=1 target.N0=target.N0+1 elseif Object:IsInstanceOf("ZONE_BASE")then local zone=Object Object=zone target.Type=TARGET.ObjectType.ZONE target.Name=zone:GetName() target.Coordinate=zone:GetCoordinate() target.Life0=1 target.Life=1 target.N0=target.N0+1 elseif Object:IsInstanceOf("OPSZONE")then local zone=Object Object=zone target.Type=TARGET.ObjectType.OPSZONE target.Name=zone:GetName() target.Coordinate=zone:GetCoordinate() target.N0=target.N0+1 target.Life0=1 target.Life=1 else self:E(self.lid.."ERROR: Unknown object type!") return nil end self.life=self.life+target.Life self.life0=self.life0+target.Life0 self.N0=self.N0+target.N0 self.Ntargets0=self.Ntargets0+1 self.targetcounter=self.targetcounter+1 target.ID=self.targetcounter target.Status=TARGET.ObjectStatus.ALIVE target.Object=Object table.insert(self.targets,target) if self.name==nil then self.name=self:GetTargetName(target) end if self.category==nil then self.category=self:GetTargetCategory(target) end return self end function TARGET:GetLife0() return self.life0 end function TARGET:GetDamage() local life=self:GetLife()/self:GetLife0() local damage=1-life return damage*100 end function TARGET:GetTargetLife(Target) if Target.Type==TARGET.ObjectType.GROUP then if Target.Object and Target.Object:IsAlive()then local units=Target.Object:GetUnits() local life=0 for _,_unit in pairs(units or{})do local unit=_unit life=life+unit:GetLife() end return life else return 0 end elseif Target.Type==TARGET.ObjectType.UNIT then local unit=Target.Object if unit and unit:IsAlive()then local life=unit:GetLife() return life else return 0 end elseif Target.Type==TARGET.ObjectType.STATIC then if Target.Object and Target.Object:IsAlive()then local life=Target.Object:GetLife() return life else return 0 end elseif Target.Type==TARGET.ObjectType.SCENERY then if Target.Object and Target.Object:IsAlive(25)then local life=Target.Object:GetLife() return life else return 0 end elseif Target.Type==TARGET.ObjectType.AIRBASE then if Target.Status==TARGET.ObjectStatus.ALIVE then return 1 else return 0 end elseif Target.Type==TARGET.ObjectType.COORDINATE then return 1 elseif Target.Type==TARGET.ObjectType.ZONE or Target.Type==TARGET.ObjectType.OPSZONE then return 1 else self:E("ERROR: unknown target object type in GetTargetLife!") end return self end function TARGET:GetLife() local N=0 for _,_target in pairs(self.targets)do local Target=_target N=N+self:GetTargetLife(Target) end return N end function TARGET:GetTargetThreatLevelMax(Target) if Target.Type==TARGET.ObjectType.GROUP then local group=Target.Object if group and group:IsAlive()then local tl=group:GetThreatLevel() return tl else return 0 end elseif Target.Type==TARGET.ObjectType.UNIT then local unit=Target.Object if unit and unit:IsAlive()then local life=unit:GetThreatLevel() return life else return 0 end elseif Target.Type==TARGET.ObjectType.STATIC then return 0 elseif Target.Type==TARGET.ObjectType.SCENERY then return 0 elseif Target.Type==TARGET.ObjectType.AIRBASE then return 0 elseif Target.Type==TARGET.ObjectType.COORDINATE then return 0 elseif Target.Type==TARGET.ObjectType.ZONE then local zone=Target.Object local foundunits={} if zone:IsInstanceOf("ZONE_RADIUS")or zone:IsInstanceOf("ZONE_POLYGON")then zone:Scan({Object.Category.UNIT},{Unit.Category.GROUND_UNIT,Unit.Category.SHIP}) foundunits=zone:GetScannedSetUnit() else foundunits=SET_UNIT:New():FilterZones({zone}):FilterOnce() end local ThreatMax=foundunits:GetThreatLevelMax()or 0 return ThreatMax elseif Target.Type==TARGET.ObjectType.OPSZONE then local unitset=Target.Object:GetScannedUnitSet() local ThreatMax=unitset:GetThreatLevelMax() return ThreatMax else self:E("ERROR: unknown target object type in GetTargetThreatLevel!") return 0 end return self end function TARGET:GetThreatLevelMax() local N=0 for _,_target in pairs(self.targets)do local Target=_target local n=self:GetTargetThreatLevelMax(Target) if n>N then N=n end end return N end function TARGET:GetTargetVec2(Target) local vec3=self:GetTargetVec3(Target) if vec3 then return{x=vec3.x,y=vec3.z} end return nil end function TARGET:GetTargetVec3(Target,Average) if Target.Type==TARGET.ObjectType.GROUP then local object=Target.Object if object and object:IsAlive()then local vec3=object:GetVec3() if Average then vec3=object:GetAverageVec3() end if vec3 then return vec3 else return nil end else return nil end elseif Target.Type==TARGET.ObjectType.UNIT then local object=Target.Object if object and object:IsAlive()then local vec3=object:GetVec3() return vec3 else return nil end elseif Target.Type==TARGET.ObjectType.STATIC then local object=Target.Object if object and object:IsAlive()then local vec3=object:GetVec3() return vec3 else return nil end elseif Target.Type==TARGET.ObjectType.SCENERY then local object=Target.Object if object then local vec3=object:GetVec3() return vec3 else return nil end elseif Target.Type==TARGET.ObjectType.AIRBASE then local object=Target.Object local vec3=object:GetVec3() return vec3 elseif Target.Type==TARGET.ObjectType.COORDINATE then local object=Target.Object local vec3={x=object.x,y=object.y,z=object.z} return vec3 elseif Target.Type==TARGET.ObjectType.ZONE then local object=Target.Object local vec3=object:GetVec3() return vec3 elseif Target.Type==TARGET.ObjectType.OPSZONE then local object=Target.Object local vec3=object:GetZone():GetVec3() return vec3 end self:E(self.lid.."ERROR: Unknown TARGET type! Cannot get Vec3") end function TARGET:GetTargetHeading(Target) if Target.Type==TARGET.ObjectType.GROUP then local object=Target.Object if object and object:IsAlive()then local heading=object:GetHeading() if heading then return heading else return nil end else return nil end elseif Target.Type==TARGET.ObjectType.UNIT then local object=Target.Object if object and object:IsAlive()then local heading=object:GetHeading() return heading else return nil end elseif Target.Type==TARGET.ObjectType.STATIC then local object=Target.Object if object and object:IsAlive()then local heading=object:GetHeading() return heading else return nil end elseif Target.Type==TARGET.ObjectType.SCENERY then local object=Target.Object if object then local heading=object:GetHeading() return heading else return nil end elseif Target.Type==TARGET.ObjectType.AIRBASE then local object=Target.Object return 0 elseif Target.Type==TARGET.ObjectType.COORDINATE then local object=Target.Object return 0 elseif Target.Type==TARGET.ObjectType.ZONE or Target.Type==TARGET.ObjectType.OPSZONE then local object=Target.Object return 0 end self:E(self.lid.."ERROR: Unknown TARGET type! Cannot get heading") end function TARGET:GetTargetCoordinate(Target,Average) if Target.Type==TARGET.ObjectType.COORDINATE then return Target.Object else local vec3=self:GetTargetVec3(Target,Average) if vec3 then Target.Coordinate.x=vec3.x Target.Coordinate.y=vec3.y Target.Coordinate.z=vec3.z end return Target.Coordinate end return nil end function TARGET:GetTargetName(Target) if Target.Type==TARGET.ObjectType.GROUP then if Target.Object and Target.Object:IsAlive()then return Target.Object:GetName() end elseif Target.Type==TARGET.ObjectType.UNIT then if Target.Object and Target.Object:IsAlive()then return Target.Object:GetName() end elseif Target.Type==TARGET.ObjectType.STATIC then if Target.Object and Target.Object:IsAlive()then return Target.Object:GetName() end elseif Target.Type==TARGET.ObjectType.AIRBASE then if Target.Status==TARGET.ObjectStatus.ALIVE then return Target.Object:GetName() end elseif Target.Type==TARGET.ObjectType.COORDINATE then local coord=Target.Object return coord:ToStringMGRS() elseif Target.Type==TARGET.ObjectType.ZONE then local Zone=Target.Object return Zone:GetName() elseif Target.Type==TARGET.ObjectType.SCENERY then local Zone=Target.Object return Zone:GetName() end return"Unknown" end function TARGET:GetName() local name=self.name or"Unknown" return name end function TARGET:GetVec2() for _,_target in pairs(self.targets)do local Target=_target local coordinate=self:GetTargetVec2(Target) if coordinate then return coordinate end end self:E(self.lid..string.format("ERROR: Cannot get Vec2 of target %s",self.name)) return nil end function TARGET:GetVec3() for _,_target in pairs(self.targets)do local Target=_target local coordinate=self:GetTargetVec3(Target) if coordinate then return coordinate end end self:E(self.lid..string.format("ERROR: Cannot get Vec3 of target %s",self.name)) return nil end function TARGET:GetCoordinate() for _,_target in pairs(self.targets)do local Target=_target local coordinate=self:GetTargetCoordinate(Target) if coordinate then return coordinate end end self:E(self.lid..string.format("ERROR: Cannot get coordinate of target %s",tostring(self.name))) return nil end function TARGET:GetAverageCoordinate() for _,_target in pairs(self.targets)do local Target=_target local coordinate=self:GetTargetCoordinate(Target,true) if coordinate then return coordinate end end self:E(self.lid..string.format("ERROR: Cannot get average coordinate of target %s",tostring(self.name))) return nil end function TARGET:GetCoordinates() local coordinates={} for _,_target in pairs(self.targets)do local target=_target local coordinate=self:GetTargetCoordinate(target) if coordinate then table.insert(coordinates,coordinate) end end return coordinates end function TARGET:GetHeading() for _,_target in pairs(self.targets)do local Target=_target local heading=self:GetTargetHeading(Target) if heading then return heading end end self:E(self.lid..string.format("ERROR: Cannot get heading of target %s",tostring(self.name))) return nil end function TARGET:GetCategory() return self.category end function TARGET:GetTargetCategory(Target) local category=nil if Target.Type==TARGET.ObjectType.GROUP then if Target.Object and Target.Object:IsAlive()~=nil then local group=Target.Object local cat=group:GetCategory() if cat==Group.Category.AIRPLANE or cat==Group.Category.HELICOPTER then category=TARGET.Category.AIRCRAFT elseif cat==Group.Category.GROUND or cat==Group.Category.TRAIN then category=TARGET.Category.GROUND elseif cat==Group.Category.SHIP then category=TARGET.Category.NAVAL end end elseif Target.Type==TARGET.ObjectType.UNIT then if Target.Object and Target.Object:IsAlive()~=nil then local unit=Target.Object local group=unit:GetGroup() local cat=group:GetCategory() if cat==Group.Category.AIRPLANE or cat==Group.Category.HELICOPTER then category=TARGET.Category.AIRCRAFT elseif cat==Group.Category.GROUND or cat==Group.Category.TRAIN then category=TARGET.Category.GROUND elseif cat==Group.Category.SHIP then category=TARGET.Category.NAVAL end end elseif Target.Type==TARGET.ObjectType.STATIC then return TARGET.Category.GROUND elseif Target.Type==TARGET.ObjectType.SCENERY then return TARGET.Category.GROUND elseif Target.Type==TARGET.ObjectType.AIRBASE then return TARGET.Category.AIRBASE elseif Target.Type==TARGET.ObjectType.COORDINATE then return TARGET.Category.COORDINATE elseif Target.Type==TARGET.ObjectType.ZONE then return TARGET.Category.ZONE elseif Target.Type==TARGET.ObjectType.OPSZONE then return TARGET.Category.OPSZONE else self:E("ERROR: unknown target category!") end return category end function TARGET:GetTargetCoalition(Target) local coal=coalition.side.NEUTRAL if Target.Type==TARGET.ObjectType.GROUP then if Target.Object and Target.Object:IsAlive()~=nil then local object=Target.Object coal=object:GetCoalition() end elseif Target.Type==TARGET.ObjectType.UNIT then if Target.Object and Target.Object:IsAlive()~=nil then local object=Target.Object coal=object:GetCoalition() end elseif Target.Type==TARGET.ObjectType.STATIC then local object=Target.Object coal=object:GetCoalition() elseif Target.Type==TARGET.ObjectType.SCENERY then elseif Target.Type==TARGET.ObjectType.AIRBASE then local object=Target.Object coal=object:GetCoalition() elseif Target.Type==TARGET.ObjectType.COORDINATE then elseif Target.Type==TARGET.ObjectType.ZONE then elseif Target.Type==TARGET.ObjectType.OPSZONE then local object=Target.Object coal=object:GetOwner() else self:E("ERROR: unknown target category!") end return coal end function TARGET:GetTargetByName(ObjectName) for _,_target in pairs(self.targets)do local target=_target if ObjectName==target.Name then return target end end return nil end function TARGET:GetObjective(RefCoordinate,Coalitions) if RefCoordinate then local dmin=math.huge local tmin=nil for _,_target in pairs(self.targets)do local target=_target if target.Status~=TARGET.ObjectStatus.DEAD and(Coalitions==nil or UTILS.IsInTable(UTILS.EnsureTable(Coalitions),self:GetTargetCoalition(target)))then local vec3=self:GetTargetVec3(target) local d=UTILS.VecDist3D(vec3,RefCoordinate) if d1 then if Coalitions==nil or UTILS.IsInTable(UTILS.EnsureTable(Coalitions),unit:GetCoalition())then N=N+1 end end end elseif Target.Type==TARGET.ObjectType.UNIT then local target=Target.Object if target and target:IsAlive()~=nil and target:GetLife()>1 then if Coalitions==nil or UTILS.IsInTable(Coalitions,target:GetCoalition())then N=N+1 end end elseif Target.Type==TARGET.ObjectType.STATIC then local target=Target.Object if target and target:IsAlive()then if Coalitions==nil or UTILS.IsInTable(Coalitions,target:GetCoalition())then N=N+1 end end elseif Target.Type==TARGET.ObjectType.SCENERY then if Target.Status~=TARGET.ObjectStatus.DEAD then N=N+1 end elseif Target.Type==TARGET.ObjectType.AIRBASE then local target=Target.Object if Target.Status==TARGET.ObjectStatus.ALIVE then if Coalitions==nil or UTILS.IsInTable(Coalitions,target:GetCoalition())then N=N+1 end end elseif Target.Type==TARGET.ObjectType.COORDINATE then if not OnlyReallyAlive then N=N+1 end elseif Target.Type==TARGET.ObjectType.ZONE then if not OnlyReallyAlive then N=N+1 end elseif Target.Type==TARGET.ObjectType.OPSZONE then local target=Target.Object if Coalitions==nil or UTILS.IsInTable(Coalitions,target:GetOwner())then N=N+1 end else self:E(self.lid.."ERROR: Unknown target type! Cannot count targets") end return N end function TARGET:CountTargets(Coalitions,OnlyReallyAlive) local N=0 for _,_target in pairs(self.targets)do local Target=_target N=N+self:CountObjectives(Target,Coalitions,OnlyReallyAlive) end return N end function TARGET:IsElement(Name) if Name==nil then return false end for _,name in pairs(self.elements)do if name==Name then return true end end return false end function TARGET:IsCasualty(Name) if Name==nil then return false end for _,name in pairs(self.casualties)do if tostring(name)==tostring(Name)then return true end end return false end EASYGCICAP={ ClassName="EASYGCICAP", overhead=0.75, capgrouping=2, airbasename=nil, airbase=nil, coalition="blue", alias=nil, wings={}, Intel=nil, resurrection=900, capspeed=300, capalt=25000, capdir=45, capleg=15, maxinterceptsize=2, missionrange=100, noalert5=4, ManagedAW={}, ManagedSQ={}, ManagedCP={}, ManagedTK={}, ManagedEWR={}, ManagedREC={}, MaxAliveMissions=8, debug=false, engagerange=50, repeatsonfailure=3, GoZoneSet=nil, NoGoZoneSet=nil, ConflictZoneSet=nil, Monitor=false, TankerInvisible=true, CapFormation=nil, ReadyFlightGroups={}, DespawnAfterLanding=false, DespawnAfterHolding=true, ListOfAuftrag={}, defaulttakeofftype="hot", FuelLowThreshold=25, FuelCriticalThreshold=10, showpatrolpointmarks=false, EngageTargetTypes={"Air"}, } EASYGCICAP.version="0.1.35" function EASYGCICAP:New(Alias,AirbaseName,Coalition,EWRName) local self=BASE:Inherit(self,FSM:New()) self.alias=Alias or AirbaseName.." CAP Wing" self.lid=string.format("EASYGCICAP %s | ",self.alias) self.coalitionname=string.lower(Coalition)or"blue" self.coalition=self.coalitionname=="blue"and coalition.side.BLUE or coalition.side.RED self.wings={} if type(EWRName)=="string"then EWRName={EWRName}end self.EWRName=EWRName self.airbasename=AirbaseName self.airbase=AIRBASE:FindByName(self.airbasename) self.GoZoneSet=SET_ZONE:New() self.NoGoZoneSet=SET_ZONE:New() self.ConflictZoneSet=SET_ZONE:New() self.resurrection=900 self.capspeed=300 self.capalt=25000 self.capdir=90 self.capleg=15 self.capgrouping=2 self.missionrange=100 self.noalert5=2 self.MaxAliveMissions=8 self.engagerange=50 self.repeatsonfailure=3 self.Monitor=false self.TankerInvisible=true self.CapFormation=ENUMS.Formation.FixedWing.FingerFour.Group self.DespawnAfterLanding=false self.DespawnAfterHolding=true self.ListOfAuftrag={} self.defaulttakeofftype="hot" self.FuelLowThreshold=25 self.FuelCriticalThreshold=10 self.showpatrolpointmarks=false self.EngageTargetTypes={"Air"} self:SetDefaultTurnoverTime() self:SetStartState("Stopped") self:AddTransition("Stopped","Start","Running") self:AddTransition("Stopped","Restart","Running") self:AddTransition("Running","Stop","Stopped") self:AddTransition("*","Status","*") self:AddAirwing(self.airbasename,self.alias,self.CapZoneName) self:I(self.lid.."Created new instance (v"..self.version..")") self:__Start(math.random(6,12)) return self end function EASYGCICAP:GetAirwing(AirbaseName) self:T(self.lid.."GetAirwing") if self.wings[AirbaseName]then return self.wings[AirbaseName][1] end return nil end function EASYGCICAP:AddAgent(Group) self:T(self.lid.."AddAgent") if Group:IsInstanceOf("GROUP")and self.Intel~=nil then self.Intel:AddAgent(Group) if self.TankerInvisible==true then Group:SetCommandInvisible(true) Group:OptionROEHoldFire() if Group:IsAir()then Group:OptionROTEvadeFire() else Group:OptionDisperseOnAttack(30) end end end return self end function EASYGCICAP:GetAirwingTable() self:T(self.lid.."GetAirwingTable") local Wingtable={} for _,_object in pairs(self.wings or{})do table.insert(Wingtable,_object[1]) end return Wingtable end function EASYGCICAP:SetFuelLow(Percent) self:T(self.lid.."SetFuelLow") self.FuelLowThreshold=Percent or 25 return self end function EASYGCICAP:ShowPatrolPointMarkers(onoff) if onoff then self.showpatrolpointmarks=true else self.showpatrolpointmarks=false end return self end function EASYGCICAP:SetFuelCritical(Percent) self:T(self.lid.."SetFuelCritical") self.FuelCriticalThreshold=Percent or 10 return self end function EASYGCICAP:SetCAPFormation(Formation) self:T(self.lid.."SetCAPFormation") self.CapFormation=Formation return self end function EASYGCICAP:SetTankerAndAWACSInvisible(Switch) self:T(self.lid.."SetTankerAndAWACSInvisible") self.TankerInvisible=Switch return self end function EASYGCICAP:_CountAliveAuftrags() local alive=0 for _,_auftrag in pairs(self.ListOfAuftrag)do local auftrag=_auftrag if auftrag and(not(auftrag:IsCancelled()or auftrag:IsDone()or auftrag:IsOver()))then alive=alive+1 end end return alive end function EASYGCICAP:SetMaxAliveMissions(Maxiumum) self:T(self.lid.."SetMaxAliveMissions") self.MaxAliveMissions=Maxiumum or 8 return self end function EASYGCICAP:SetDefaultResurrection(Seconds) self:T(self.lid.."SetDefaultResurrection") self.resurrection=Seconds or 900 return self end function EASYGCICAP:SetDefaultRepeatOnFailure(Retries) self:T(self.lid.."SetDefaultRepeatOnFailure") self.repeatsonfailure=Retries or 3 return self end function EASYGCICAP:SetDefaultTakeOffType(Takeoff) self:T(self.lid.."SetDefaultTakeOffType") self.defaulttakeofftype=Takeoff or"hot" return self end function EASYGCICAP:SetDefaultCAPSpeed(Speed) self:T(self.lid.."SetDefaultSpeed") self.capspeed=Speed or 300 return self end function EASYGCICAP:SetDefaultCAPAlt(Altitude) self:T(self.lid.."SetDefaultAltitude") self.capalt=Altitude or 25000 return self end function EASYGCICAP:SetDefaultCAPDirection(Direction) self:T(self.lid.."SetDefaultDirection") self.capdir=Direction or 90 return self end function EASYGCICAP:SetDefaultCAPLeg(Leg) self:T(self.lid.."SetDefaultLeg") self.capleg=Leg or 15 return self end function EASYGCICAP:SetDefaultCAPGrouping(Grouping) self:T(self.lid.."SetDefaultCAPGrouping") self.capgrouping=Grouping or 2 return self end function EASYGCICAP:SetDefaultMissionRange(Range) self:T(self.lid.."SetDefaultMissionRange") self.missionrange=Range or 100 return self end function EASYGCICAP:SetDefaultTurnoverTime(MaintenanceTime,RepairTime) self:T(self.lid.."SetDefaultTurnoverTime") self.maintenancetime=MaintenanceTime or 5 self.repairtime=RepairTime or 10 return self end function EASYGCICAP:SetDefaultNumberAlert5Standby(Airframes) self:T(self.lid.."SetDefaultNumberAlert5Standby") self.noalert5=math.abs(Airframes)or 2 return self end function EASYGCICAP:SetDefaultEngageRange(Range) self:T(self.lid.."SetDefaultEngageRange") self.engagerange=Range or 50 return self end function EASYGCICAP:SetDefaultOverhead(Overhead) self:T(self.lid.."SetDefaultOverhead") self.overhead=Overhead or 0.75 return self end function EASYGCICAP:SetDefaultDespawnAfterLanding() self:T(self.lid.."SetDefaultDespawnAfterLanding") self.DespawnAfterLanding=true self.DespawnAfterHolding=false return self end function EASYGCICAP:SetDefaultDespawnAfterHolding() self:T(self.lid.."SetDefaultDespawnAfterLanding") self.DespawnAfterLanding=false self.DespawnAfterHolding=true return self end function EASYGCICAP:SetCapStartTimeVariation(Start,End) self.capOptionVaryStartTime=Start or 5 self.capOptionVaryEndTime=End or 60 return self end function EASYGCICAP:SetCAPEngageTargetTypes(types) self.EngageTargetTypes=types or{"Air"} return self end function EASYGCICAP:AddAirwing(Airbasename,Alias) self:T(self.lid.."AddAirwing "..Airbasename) local AWEntry={} AWEntry.AirbaseName=Airbasename AWEntry.Alias=Alias self.ManagedAW[Airbasename]=AWEntry return self end function EASYGCICAP:_CreateAirwings() self:T(self.lid.."_CreateAirwings") for airbase,data in pairs(self.ManagedAW)do local wing=data local afb=wing.AirbaseName local alias=wing.Alias self:_AddAirwing(airbase,alias) end return self end function EASYGCICAP:_AddAirwing(Airbasename,Alias) self:T(self.lid.."_AddAirwing "..Airbasename) local CapFormation=self.CapFormation local DespawnAfterLanding=self.DespawnAfterLanding local DespawnAfterHolding=self.DespawnAfterHolding local check=STATIC:FindByName(Airbasename,false)or UNIT:FindByName(Airbasename) if check==nil then MESSAGE:New(self.lid.."There's no warehouse static on the map (wrong naming?) for airbase "..tostring(Airbasename).."!",30,"CHECK"):ToAllIf(self.debug):ToLog() return end local CAP_Wing=AIRWING:New(Airbasename,Alias) CAP_Wing:SetVerbosityLevel(0) CAP_Wing:SetReportOff() CAP_Wing:SetMarker(false) CAP_Wing:SetAirbase(AIRBASE:FindByName(Airbasename)) CAP_Wing:SetRespawnAfterDestroyed() CAP_Wing:SetNumberCAP(self.capgrouping) CAP_Wing:SetCapCloseRaceTrack(true) if self.showpatrolpointmarks then CAP_Wing:ShowPatrolPointMarkers(true) end if self.capOptionVaryStartTime then CAP_Wing:SetCapStartTimeVariation(self.capOptionVaryStartTime,self.capOptionVaryEndTime) end if CapFormation then CAP_Wing:SetCAPFormation(CapFormation) end if#self.ManagedTK>0 then CAP_Wing:SetNumberTankerBoom(1) CAP_Wing:SetNumberTankerProbe(1) end if#self.ManagedEWR>0 then CAP_Wing:SetNumberAWACS(1) end if#self.ManagedREC>0 then CAP_Wing:SetNumberRecon(1) end CAP_Wing:SetTakeoffType(self.defaulttakeofftype) CAP_Wing:SetLowFuelThreshold(0.3) CAP_Wing.RandomAssetScore=math.random(50,100) CAP_Wing:Start() local Intel=self.Intel local TankerInvisible=self.TankerInvisible local engagerange=self.engagerange local GoZoneSet=self.GoZoneSet local NoGoZoneSet=self.NoGoZoneSet local FuelLow=self.FuelLowThreshold or 25 local FuelCritical=self.FuelCriticalThreshold or 10 local EngageTypes=self.EngageTargetTypes or{"Air"} function CAP_Wing:onbeforeFlightOnMission(From,Event,To,Flightgroup,Mission) local flightgroup=Flightgroup if DespawnAfterLanding then flightgroup:SetDespawnAfterLanding() elseif DespawnAfterHolding then flightgroup:SetDespawnAfterHolding() end flightgroup:SetDestinationbase(AIRBASE:FindByName(Airbasename)) flightgroup:GetGroup():CommandEPLRS(true,5) flightgroup:GetGroup():SetOptionRadarUsingForContinousSearch() flightgroup:GetGroup():SetOptionLandingOverheadBreak() if Mission.type~=AUFTRAG.Type.TANKER and Mission.type~=AUFTRAG.Type.AWACS and Mission.type~=AUFTRAG.Type.RECON then flightgroup:SetDetection(true) flightgroup:SetEngageDetectedOn(engagerange,EngageTypes,GoZoneSet,NoGoZoneSet) flightgroup:SetOutOfAAMRTB() flightgroup:SetFuelLowRTB(true) flightgroup:SetFuelLowThreshold(FuelLow) flightgroup:SetFuelCriticalRTB(true) flightgroup:SetFuelCriticalThreshold(FuelCritical) if CapFormation then flightgroup:GetGroup():SetOption(AI.Option.Air.id.FORMATION,CapFormation) end end if Mission.type==AUFTRAG.Type.TANKER or Mission.type==AUFTRAG.Type.AWACS or Mission.type==AUFTRAG.Type.RECON then if TankerInvisible then flightgroup:GetGroup():SetCommandInvisible(true) end if Mission.type==AUFTRAG.Type.RECON then flightgroup:SetDetection(true) end end flightgroup:GetGroup():OptionROTEvadeFire() flightgroup:SetFuelLowRTB(true) Intel:AddAgent(flightgroup) if DespawnAfterHolding then function flightgroup:onbeforeHolding(From,Event,To) self:Despawn(1,true) end end end if self.noalert5>0 then local alert if self.ClassName=="EASYGCICAP"then alert=AUFTRAG:NewALERT5(AUFTRAG.Type.INTERCEPT) elseif self.ClassName=="EASYA2G"then alert=AUFTRAG:NewALERT5(AUFTRAG.Type.BAI) end alert:SetRequiredAssets(self.noalert5) alert:SetRepeat(99) CAP_Wing:AddMission(alert) table.insert(self.ListOfAuftrag,alert) end self.wings[Airbasename]={CAP_Wing,AIRBASE:FindByName(Airbasename):GetZone(),Airbasename} return self end function EASYGCICAP:AddPatrolPointCAP(AirbaseName,Coordinate,Altitude,Speed,Heading,LegLength) self:T(self.lid.."AddPatrolPointCAP") local coordinate=Coordinate local EntryCAP={} if Coordinate:IsInstanceOf("ZONE_BASE")then coordinate=Coordinate:GetCoordinate() EntryCAP.Zone=Coordinate end EntryCAP.AirbaseName=AirbaseName EntryCAP.Coordinate=coordinate EntryCAP.Altitude=Altitude or 25000 EntryCAP.Speed=Speed or 300 EntryCAP.Heading=Heading or 90 EntryCAP.LegLength=LegLength or 15 self.ManagedCP[#self.ManagedCP+1]=EntryCAP if self.debug then local mark=MARKER:New(coordinate,self.lid.."Patrol Point"):ToAll() end return self end function EASYGCICAP:AddPatrolPointRecon(AirbaseName,Coordinate,Altitude,Speed,Heading,LegLength) self:T(self.lid.."AddPatrolPointRecon "..Coordinate:ToStringLLDDM()) local EntryCAP={} EntryCAP.AirbaseName=AirbaseName EntryCAP.Coordinate=Coordinate EntryCAP.Altitude=Altitude or 25000 EntryCAP.Speed=Speed or 300 EntryCAP.Heading=Heading or 90 EntryCAP.LegLength=LegLength or 15 self.ManagedREC[#self.ManagedREC+1]=EntryCAP if self.debug then local mark=MARKER:New(Coordinate,self.lid.."Patrol Point Recon"):ToAll() end return self end function EASYGCICAP:AddPatrolPointTanker(AirbaseName,Coordinate,Altitude,Speed,Heading,LegLength) self:T(self.lid.."AddPatrolPointTanker "..Coordinate:ToStringLLDDM()) local EntryCAP={} EntryCAP.AirbaseName=AirbaseName EntryCAP.Coordinate=Coordinate EntryCAP.Altitude=Altitude or 25000 EntryCAP.Speed=Speed or 300 EntryCAP.Heading=Heading or 90 EntryCAP.LegLength=LegLength or 15 self.ManagedTK[#self.ManagedTK+1]=EntryCAP if self.debug then local mark=MARKER:New(Coordinate,self.lid.."Patrol Point Tanker"):ToAll() end return self end function EASYGCICAP:AddPatrolPointAwacs(AirbaseName,Coordinate,Altitude,Speed,Heading,LegLength) self:T(self.lid.."AddPatrolPointAwacs "..Coordinate:ToStringLLDDM()) local EntryCAP={} EntryCAP.AirbaseName=AirbaseName EntryCAP.Coordinate=Coordinate EntryCAP.Altitude=Altitude or 25000 EntryCAP.Speed=Speed or 300 EntryCAP.Heading=Heading or 90 EntryCAP.LegLength=LegLength or 15 self.ManagedEWR[#self.ManagedEWR+1]=EntryCAP if self.debug then local mark=MARKER:New(Coordinate,self.lid.."Patrol Point AWACS"):ToAll() end return self end function EASYGCICAP:_SetTankerPatrolPoints() self:T(self.lid.."_SetTankerPatrolPoints") for _,_data in pairs(self.ManagedTK)do local data=_data self:T("Airbasename = "..data.AirbaseName) if not self.wings[data.AirbaseName]then MESSAGE:New(self.lid.."You are trying to create a TANKER point for which there is no wing! "..tostring(data.AirbaseName),30,"CHECK"):ToAllIf(self.debug):ToLog() return end local Wing=self.wings[data.AirbaseName][1] local Coordinate=data.Coordinate local Altitude=data.Altitude local Speed=data.Speed local Heading=data.Heading local LegLength=data.LegLength Wing:AddPatrolPointTANKER(Coordinate,Altitude,Speed,Heading,LegLength) end return self end function EASYGCICAP:_SetAwacsPatrolPoints() self:T(self.lid.."_SetAwacsPatrolPoints") for _,_data in pairs(self.ManagedEWR)do local data=_data self:T("Airbasename = "..data.AirbaseName) if not self.wings[data.AirbaseName]then MESSAGE:New(self.lid.."You are trying to create an AWACS point for which there is no wing! "..tostring(data.AirbaseName),30,"CHECK"):ToAllIf(self.debug):ToLog() return end local Wing=self.wings[data.AirbaseName][1] local Coordinate=data.Coordinate local Altitude=data.Altitude local Speed=data.Speed local Heading=data.Heading local LegLength=data.LegLength Wing:AddPatrolPointAWACS(Coordinate,Altitude,Speed,Heading,LegLength) end return self end function EASYGCICAP:_SetCAPPatrolPoints() self:T(self.lid.."_SetCAPPatrolPoints") for _,_data in pairs(self.ManagedCP)do local data=_data self:T("Airbasename = "..data.AirbaseName) if not self.wings[data.AirbaseName]then MESSAGE:New(self.lid.."You are trying to create a CAP point for which there is no wing! "..tostring(data.AirbaseName),30,"CHECK"):ToAllIf(self.debug):ToLog() return end local Wing=self.wings[data.AirbaseName][1] local Coordinate=data.Coordinate local Altitude=data.Altitude local Speed=data.Speed local Heading=data.Heading local LegLength=data.LegLength local Zone=_data.Zone if Zone then Wing:AddPatrolPointCAP(Zone,Altitude,Speed,Heading,LegLength) else Wing:AddPatrolPointCAP(Coordinate,Altitude,Speed,Heading,LegLength) end end return self end function EASYGCICAP:_SetReconPatrolPoints() self:T(self.lid.."_SetReconPatrolPoints") for _,_data in pairs(self.ManagedREC)do local data=_data self:T("Airbasename = "..data.AirbaseName) if not self.wings[data.AirbaseName]then MESSAGE:New(self.lid.."You are trying to create a RECON point for which there is no wing! "..tostring(data.AirbaseName),30,"CHECK"):ToAllIf(self.debug):ToLog() return end local Wing=self.wings[data.AirbaseName][1] local Coordinate=data.Coordinate local Altitude=data.Altitude local Speed=data.Speed local Heading=data.Heading local LegLength=data.LegLength Wing:AddPatrolPointRecon(Coordinate,Altitude,Speed,Heading,LegLength) end return self end function EASYGCICAP:_CreateSquads() self:T(self.lid.."_CreateSquads") for name,data in pairs(self.ManagedSQ)do local squad=data local SquadName=name local TemplateName=squad.TemplateName local AirbaseName=squad.AirbaseName local AirFrames=squad.AirFrames local Skill=squad.Skill local Modex=squad.Modex local Livery=squad.Livery local Frequeny=squad.Frequency local Modulation=squad.Modulation local TACAN=squad.TACAN if squad.Tanker then self:_AddTankerSquadron(TemplateName,SquadName,AirbaseName,AirFrames,Skill,Modex,Livery,Frequeny,Modulation,TACAN) elseif squad.AWACS then self:_AddAWACSSquadron(TemplateName,SquadName,AirbaseName,AirFrames,Skill,Modex,Livery,Frequeny,Modulation) elseif squad.RECON then self:_AddReconSquadron(TemplateName,SquadName,AirbaseName,AirFrames,Skill,Modex,Livery) else self:_AddSquadron(TemplateName,SquadName,AirbaseName,AirFrames,Skill,Modex,Livery) end end return self end function EASYGCICAP:AddSquadron(TemplateName,SquadName,AirbaseName,AirFrames,Skill,Modex,Livery) self:T(self.lid.."AddSquadron "..SquadName) local EntrySQ={} EntrySQ.TemplateName=TemplateName EntrySQ.SquadName=SquadName EntrySQ.AirbaseName=AirbaseName EntrySQ.AirFrames=AirFrames or 20 EntrySQ.Skill=Skill or AI.Skill.AVERAGE EntrySQ.Modex=Modex or 402 EntrySQ.Livery=Livery self.ManagedSQ[SquadName]=EntrySQ return self end function EASYGCICAP:AddReconSquadron(TemplateName,SquadName,AirbaseName,AirFrames,Skill,Modex,Livery) self:T(self.lid.."AddReconSquadron "..SquadName) local EntrySQ={} EntrySQ.TemplateName=TemplateName EntrySQ.SquadName=SquadName EntrySQ.AirbaseName=AirbaseName EntrySQ.AirFrames=AirFrames or 20 EntrySQ.Skill=Skill or AI.Skill.AVERAGE EntrySQ.Modex=Modex or 402 EntrySQ.Livery=Livery EntrySQ.RECON=true self.ManagedSQ[SquadName]=EntrySQ return self end function EASYGCICAP:AddTankerSquadron(TemplateName,SquadName,AirbaseName,AirFrames,Skill,Modex,Livery,Frequency,Modulation,TACAN) self:T(self.lid.."AddTankerSquadron "..SquadName) local EntrySQ={} EntrySQ.TemplateName=TemplateName EntrySQ.SquadName=SquadName EntrySQ.AirbaseName=AirbaseName EntrySQ.AirFrames=AirFrames or 20 EntrySQ.Skill=Skill or AI.Skill.AVERAGE EntrySQ.Modex=Modex or 602 EntrySQ.Livery=Livery EntrySQ.Frequency=Frequency EntrySQ.Modulation=Livery EntrySQ.TACAN=TACAN EntrySQ.Tanker=true self.ManagedSQ[SquadName]=EntrySQ return self end function EASYGCICAP:AddAWACSSquadron(TemplateName,SquadName,AirbaseName,AirFrames,Skill,Modex,Livery,Frequency,Modulation) self:T(self.lid.."AddAWACSSquadron "..SquadName) local EntrySQ={} EntrySQ.TemplateName=TemplateName EntrySQ.SquadName=SquadName EntrySQ.AirbaseName=AirbaseName EntrySQ.AirFrames=AirFrames or 20 EntrySQ.Skill=Skill or AI.Skill.AVERAGE EntrySQ.Modex=Modex or 702 EntrySQ.Livery=Livery EntrySQ.Frequency=Frequency EntrySQ.Modulation=Livery EntrySQ.AWACS=true self.ManagedSQ[SquadName]=EntrySQ return self end function EASYGCICAP:_AddSquadron(TemplateName,SquadName,AirbaseName,AirFrames,Skill,Modex,Livery,Frequency,Modulation) self:T(self.lid.."_AddSquadron "..SquadName) local Squadron_One=SQUADRON:New(TemplateName,AirFrames,SquadName) Squadron_One:AddMissionCapability({AUFTRAG.Type.CAP,AUFTRAG.Type.GCICAP,AUFTRAG.Type.INTERCEPT,AUFTRAG.Type.PATROLRACETRACK,AUFTRAG.Type.ALERT5}) Squadron_One:SetFuelLowThreshold(0.3) Squadron_One:SetTurnoverTime(self.maintenancetime,self.repairtime) Squadron_One:SetModex(Modex) Squadron_One:SetLivery(Livery) Squadron_One:SetSkill(Skill or AI.Skill.AVERAGE) Squadron_One:SetMissionRange(self.missionrange) local wing=self.wings[AirbaseName][1] wing:AddSquadron(Squadron_One) wing:NewPayload(TemplateName,-1,{AUFTRAG.Type.CAP,AUFTRAG.Type.GCICAP,AUFTRAG.Type.INTERCEPT,AUFTRAG.Type.PATROLRACETRACK,AUFTRAG.Type.ALERT5},100) return self end function EASYGCICAP:_AddReconSquadron(TemplateName,SquadName,AirbaseName,AirFrames,Skill,Modex,Livery) self:T(self.lid.."_AddReconSquadron "..SquadName) local Squadron_One=SQUADRON:New(TemplateName,AirFrames,SquadName) Squadron_One:AddMissionCapability({AUFTRAG.Type.RECON}) Squadron_One:SetFuelLowThreshold(0.3) Squadron_One:SetTurnoverTime(self.maintenancetime,self.repairtime) Squadron_One:SetModex(Modex) Squadron_One:SetLivery(Livery) Squadron_One:SetSkill(Skill or AI.Skill.AVERAGE) Squadron_One:SetMissionRange(self.missionrange) local wing=self.wings[AirbaseName][1] wing:AddSquadron(Squadron_One) wing:NewPayload(TemplateName,-1,{AUFTRAG.Type.RECON},75) return self end function EASYGCICAP:_AddTankerSquadron(TemplateName,SquadName,AirbaseName,AirFrames,Skill,Modex,Livery,Frequency,Modulation,TACAN) self:T(self.lid.."_AddTankerSquadron "..SquadName) local Squadron_One=SQUADRON:New(TemplateName,AirFrames,SquadName) Squadron_One:AddMissionCapability({AUFTRAG.Type.TANKER}) Squadron_One:SetFuelLowThreshold(0.3) Squadron_One:SetTurnoverTime(self.maintenancetime,self.repairtime) Squadron_One:SetModex(Modex) Squadron_One:SetLivery(Livery) Squadron_One:SetSkill(Skill or AI.Skill.AVERAGE) Squadron_One:SetMissionRange(self.missionrange) Squadron_One:SetRadio(Frequency,Modulation) if TACAN then Squadron_One:AddTacanChannel(TACAN,TACAN) end local wing=self.wings[AirbaseName][1] wing:AddSquadron(Squadron_One) wing:NewPayload(TemplateName,-1,{AUFTRAG.Type.TANKER},75) return self end function EASYGCICAP:_AddAWACSSquadron(TemplateName,SquadName,AirbaseName,AirFrames,Skill,Modex,Livery,Frequency,Modulation) self:T(self.lid.."_AddAWACSSquadron "..SquadName) local Squadron_One=SQUADRON:New(TemplateName,AirFrames,SquadName) Squadron_One:AddMissionCapability({AUFTRAG.Type.AWACS}) Squadron_One:SetFuelLowThreshold(0.3) Squadron_One:SetTurnoverTime(self.maintenancetime,self.repairtime) Squadron_One:SetModex(Modex) Squadron_One:SetLivery(Livery) Squadron_One:SetSkill(Skill or AI.Skill.AVERAGE) Squadron_One:SetMissionRange(self.missionrange) Squadron_One:SetRadio(Frequency,Modulation) local wing=self.wings[AirbaseName][1] wing:AddSquadron(Squadron_One) wing:NewPayload(TemplateName,-1,{AUFTRAG.Type.AWACS},75) return self end function EASYGCICAP:AddAcceptZone(Zone) self:T(self.lid.."AddAcceptZone") self.GoZoneSet:AddZone(Zone) return self end function EASYGCICAP:AddRejectZone(Zone) self:T(self.lid.."AddRejectZone") self.NoGoZoneSet:AddZone(Zone) return self end function EASYGCICAP:AddConflictZone(Zone) self:T(self.lid.."AddConflictZone") self.ConflictZoneSet:AddZone(Zone) self.GoZoneSet:AddZone(Zone) return self end function EASYGCICAP:SetCorridorZones(CorridorZones) self:T(self.lid.."SetCorridorZones") if CorridorZones and CorridorZones:IsInstanceOf("SET_ZONE")then self.corridorzones=CorridorZones self.usecorridors=true elseif CorridorZones and CorridorZones:IsInstanceOf("ZONE_BASE")then if not self.corridorzones then self.corridorzones=SET_ZONE:New()end self.corridorzones:AddZone(CorridorZones) self.usecorridors=true end return self end function EASYGCICAP:AddCorridorZone(CorridorZone) self:T(self.lid.."AddCorridorZone") self:SetCorridorZones(CorridorZone) return self end function EASYGCICAP:SetCorridorZoneFloorAndCeiling(Floor,Ceiling) self.corridorfloor=UTILS.FeetToMeters(Floor) self.corridorceiling=UTILS.FeetToMeters(Ceiling) return self end function EASYGCICAP:SetCorridorZoneFloorAndCeilingMeters(Floor,Ceiling) self.corridorfloor=Floor self.corridorceiling=Ceiling return self end function EASYGCICAP:_TryAssignIntercept(ReadyFlightGroups,InterceptAuftrag,Group,WingSize) self:T("_TryAssignIntercept for size "..WingSize or 1) local assigned=false local wingsize=WingSize or 1 local mindist=0 local disttable={} if Group and Group:IsAlive()then local gcoord=Group:GetCoordinate()or COORDINATE:New(0,0,0) self:T(self.lid..string.format("Assignment for %s",Group:GetName())) for _name,_FG in pairs(ReadyFlightGroups or{})do local FG=_FG local fcoord=FG:GetCoordinate() local dist=math.floor(UTILS.Round(fcoord:Get2DDistance(gcoord)/1000,1)) self:T(self.lid..string.format("FG %s Distance %dkm",_name,dist)) disttable[#disttable+1]={FG=FG,dist=dist} if dist>mindist then mindist=dist end end local function sortDistance(a,b) return a.distmaxsize then wingsize=maxsize end local retrymission=true if Cluster.mission and(not Cluster.mission:IsOver())then retrymission=false end if(retrymission)and(wingsize>=1)then MESSAGE:New(string.format("**** %s Interceptors need wingsize %d",UTILS.GetCoalitionName(self.coalition),wingsize),15,"CAPGCI"):ToAllIf(self.debug):ToLog() for _,_data in pairs(wings)do local airwing=_data[1] local zone=_data[2] local zonecoord=zone:GetCoordinate() local name=_data[3] local coa=AIRBASE:FindByName(name):GetCoalition() local distance=position:DistanceFromPointVec2(zonecoord) local airframes=airwing:CountAssets(true) local samecoalitionab=coa==self.coalition and true or false if distance=wingsize and samecoalitionab==true then bestdistance=distance targetairwing=airwing targetawname=name end end for _,_data in pairs(ctlpts)do local data=_data local name=data.AirbaseName local zonecoord=data.Coordinate if data.Zone then zonecoord=data.Zone:GetCoordinate() end local airwing=wings[name][1] local coa=AIRBASE:FindByName(name):GetCoalition() local samecoalitionab=coa==self.coalition and true or false local distance=position:DistanceFromPointVec2(zonecoord) local airframes=airwing:CountAssets(true) if distance=wingsize and samecoalitionab==true then bestdistance=distance targetairwing=airwing targetawname=name end end local text=string.format("Closest Airwing is %s",targetawname) local m=MESSAGE:New(text,10,"CAPGCI"):ToAllIf(self.debug):ToLog() if targetairwing then local AssetCount=targetairwing:CountAssetsOnMission(MissionTypes,Cohort) local missioncount=self:_CountAliveAuftrags() self:T(self.lid.." Assets on Mission "..AssetCount) if missioncount0 then InterceptAuftrag:AddConditionSuccess( function(group,zoneset,conflictset) local success=false if group and group:IsAlive()then local coord=group:GetCoordinate() if coord and zoneset:Count()>0 and zoneset:IsCoordinateInZone(coord)then success=true end if coord and conflictset:Count()>0 and conflictset:IsCoordinateInZone(coord)then success=false end else success=true end return success end, contact.group, nogozoneset, conflictzoneset ) end InterceptAuftrag:AddConditionFailure( function() local failure=false if InterceptAuftrag:CountOpsGroups()==0 and InterceptAuftrag:IsExecuting()then failure=true end return failure end ) table.insert(self.ListOfAuftrag,InterceptAuftrag) local assigned,rest=self:_TryAssignIntercept(ReadyFlightGroups,InterceptAuftrag,contact.group,wingsize) if not assigned then InterceptAuftrag:SetRequiredAssets(rest) targetairwing:AddMission(InterceptAuftrag) end Cluster.mission=InterceptAuftrag end else MESSAGE:New("**** Not enough airframes available or max mission limit reached!",15,"CAPGCI"):ToAllIf(self.debug):ToLog() end end end function EASYGCICAP:_StartIntel() self:T(self.lid.."_StartIntel") local BlueAir_DetectionSetGroup=SET_GROUP:New() BlueAir_DetectionSetGroup:FilterPrefixes(self.EWRName) BlueAir_DetectionSetGroup:FilterStart() local BlueIntel=INTEL:New(BlueAir_DetectionSetGroup,self.coalitionname,self.alias) BlueIntel:SetClusterAnalysis(true,false,false) BlueIntel:SetForgetTime(300) BlueIntel:SetAcceptZones(self.GoZoneSet) BlueIntel:SetRejectZones(self.NoGoZoneSet) BlueIntel:SetConflictZones(self.ConflictZoneSet) BlueIntel:SetVerbosity(0) if self.usecorridors==true then BlueIntel:SetCorridorZones(self.corridorzones) if self.corridorfloor or self.corridorceiling then BlueIntel:SetCorridorLimits(self.corridorfloor,self.corridorceiling) end end BlueIntel:Start() if self.debug then BlueIntel.debug=true end local function AssignCluster(Cluster) self:_AssignIntercept(Cluster) end function BlueIntel:onbeforeNewCluster(From,Event,To,Cluster) AssignCluster(Cluster) end self.Intel=BlueIntel return self end function EASYGCICAP:onafterStart(From,Event,To) self:T({From,Event,To}) self:_StartIntel() self:_CreateAirwings() self:_CreateSquads() self:_SetCAPPatrolPoints() self:_SetTankerPatrolPoints() self:_SetAwacsPatrolPoints() self:_SetReconPatrolPoints() self:__Status(-10) return self end function EASYGCICAP:onbeforeStatus(From,Event,To) self:T({From,Event,To}) if self:GetState()=="Stopped"then return false end return self end function EASYGCICAP:onafterStatus(From,Event,To) self:T({From,Event,To}) local cleaned=false local cleanlist={} for _,_auftrag in pairs(self.ListOfAuftrag)do local auftrag=_auftrag if auftrag and(not(auftrag:IsCancelled()or auftrag:IsDone()or auftrag:IsOver()))then table.insert(cleanlist,auftrag) cleaned=true end end if cleaned==true then self.ListOfAuftrag=nil self.ListOfAuftrag=cleanlist end local function counttable(tbl) local count=0 for _,_data in pairs(tbl)do count=count+1 end return count end local wings=counttable(self.ManagedAW) local squads=counttable(self.ManagedSQ) local caps=counttable(self.ManagedCP) local assets=0 local instock=0 local capmission=0 local interceptmission=0 local reconmission=0 local awacsmission=0 local tankermission=0 for _,_wing in pairs(self.wings)do local count=_wing[1]:CountAssetsOnMission(MissionTypes,Cohort) local count2=_wing[1]:CountAssets(true,MissionTypes,Attributes) capmission=capmission+_wing[1]:CountMissionsInQueue({AUFTRAG.Type.GCICAP,AUFTRAG.Type.PATROLRACETRACK}) interceptmission=interceptmission+_wing[1]:CountMissionsInQueue({AUFTRAG.Type.INTERCEPT}) reconmission=reconmission+_wing[1]:CountMissionsInQueue({AUFTRAG.Type.RECON}) awacsmission=awacsmission+_wing[1]:CountMissionsInQueue({AUFTRAG.Type.AWACS}) tankermission=tankermission+_wing[1]:CountMissionsInQueue({AUFTRAG.Type.TANKER}) assets=assets+count instock=instock+count2 local assetsonmission=_wing[1]:GetAssetsOnMission({AUFTRAG.Type.ALERT5,AUFTRAG.Type.GCICAP,AUFTRAG.Type.PATROLRACETRACK}) self.ReadyFlightGroups=nil self.ReadyFlightGroups={} for _,_asset in pairs(assetsonmission or{})do local asset=_asset local FG=asset.flightgroup if FG then local name=FG:GetName() local engage=FG:IsEngaging() local hasmissiles=FG:IsOutOfMissiles()==nil and true or false local isalert5=(FG:GetMissionCurrent()~=nil and FG:GetMissionCurrent().type==AUFTRAG.Type.ALERT5)and true or false local ready=hasmissiles and FG:IsFuelGood()and(FG:IsAirborne()or isalert5) if ready then self.ReadyFlightGroups[name]=FG end end end end if self.Monitor then local threatcount=#self.Intel.Clusters or 0 local text="GCICAP "..self.alias text=text.."\nWings: "..wings.."\nSquads: "..squads.."\nCapPoints: "..caps.."\nAssets on Mission: "..assets.."\nAssets in Stock: "..instock text=text.."\nThreats: "..threatcount text=text.."\nAirWing managed Missions: "..capmission+awacsmission+tankermission+reconmission text=text.."\n - CAP: "..capmission text=text.."\n - AWACS: "..awacsmission text=text.."\n - TANKER: "..tankermission text=text.."\n - Recon: "..reconmission text=text.."\nSelf managed Missions:" text=text.."\n - Mission Limit: "..self.MaxAliveMissions text=text.."\n - Alert5+Intercept "..self:_CountAliveAuftrags() MESSAGE:New(text,15,"GCICAP"):ToAll():ToLogIf(self.debug) end self:__Status(30) return self end function EASYGCICAP:onafterStop(From,Event,To) self:T({From,Event,To}) self.Intel:Stop() for _,_wing in pairs(self.wings or{})do for _,_aw in pairs(_wing)do _wing[1]:Stop() end end return self end function EASYGCICAP:onafterRestart(From,Event,To) self:T({From,Event,To}) if self:Is("Stopped")then self.Intel:Start() for _,_wing in pairs(self.wings or{})do for _,_aw in pairs(_wing)do _wing[1]:Restart() end end end self:__Status(5) return self end EASYA2G={ ClassName="EASYA2G", overhead=0.2, capgrouping=1, airbasename=nil, airbase=nil, coalition="blue", alias=nil, wings={}, Intel=nil, resurrection=900, capspeed=300, capalt=25000, capdir=45, capleg=5, maxinterceptsize=2, missionrange=100, noalert5=2, ManagedAW={}, ManagedSQ={}, ManagedCP={}, ManagedTK={}, ManagedEWR={}, ManagedREC={}, MaxAliveMissions=8, debug=false, engagerange=50, repeatsonfailure=3, GoZoneSet=nil, NoGoZoneSet=nil, ConflictZoneSet=nil, Monitor=false, TankerInvisible=true, CapFormation=nil, ReadyFlightGroups={}, DespawnAfterLanding=false, DespawnAfterHolding=true, ListOfAuftrag={}, defaulttakeofftype="hot", FuelLowThreshold=25, FuelCriticalThreshold=10, showpatrolpointmarks=false, EngageTargetTypes={"Ground"}, } EASYA2G.version="0.1.4" function EASYA2G:New(Alias,AirbaseName,Coalition,ScoutName) local self=BASE:Inherit(self,EASYGCICAP:New(Alias,AirbaseName,Coalition,ScoutName)) self.alias=Alias or AirbaseName.." A2G Wing" self.lid=string.format("EASYA2G %s | ",self.alias) self.coalitionname=string.lower(Coalition)or"blue" self.coalition=self.coalitionname=="blue"and coalition.side.BLUE or coalition.side.RED self.wings={} if type(ScoutName)=="string"then ScoutName={ScoutName}end self.EWRName=ScoutName self.airbasename=AirbaseName self.airbase=AIRBASE:FindByName(self.airbasename) self.GoZoneSet=SET_ZONE:New() self.NoGoZoneSet=SET_ZONE:New() self.ConflictZoneSet=SET_ZONE:New() self.resurrection=900 self.capspeed=225 self.capalt=5000 self.capdir=90 self.capleg=5 self.capgrouping=2 self.missionrange=100 self.noalert5=2 self.MaxAliveMissions=8 self.engagerange=50 self.repeatsonfailure=3 self.Monitor=false self.TankerInvisible=true self.CapFormation=ENUMS.Formation.FixedWing.FingerFour.Group self.DespawnAfterLanding=false self.DespawnAfterHolding=true self.ListOfAuftrag={} self.defaulttakeofftype="hot" self.FuelLowThreshold=25 self.FuelCriticalThreshold=10 self.showpatrolpointmarks=false self.EngageTargetTypes={"Ground"} self:SetDefaultTurnoverTime() self:SetStartState("Stopped") self:AddTransition("Stopped","Start","Running") self:AddTransition("Running","Stop","Stopped") self:AddTransition("*","Status","*") self:AddAirwing(self.airbasename,self.alias,self.CapZoneName) self:I(self.lid.."Created new instance (v"..self.version..")") self:__Start(math.random(6,12)) return self end function EASYA2G:SetTankerAndScoutsInvisible(Switch) self:T(self.lid.."SetTankerAndScoutsInvisible") self.TankerInvisible=Switch return self end function EASYA2G:SetDefaultA2GSpeed(Speed) self:T(self.lid.."SetDefaultSpeed") self.capspeed=Speed or 300 return self end function EASYA2G:SetA2GFormation(Formation) self:T(self.lid.."SetA2GFormation") self.CapFormation=Formation return self end function EASYA2G:SetDefaultA2GAlt(Altitude) self:T(self.lid.."SetDefaultAltitude") self.capalt=Altitude or 25000 return self end function EASYA2G:SetDefaultA2GDirection(Direction) self:T(self.lid.."SetDefaultDirection") self.capdir=Direction or 90 return self end function EASYA2G:SetDefaultA2GLeg(Leg) self:T(self.lid.."SetDefaultLeg") self.capleg=Leg or 5 return self end function EASYA2G:SetDefaultA2GGrouping(Grouping) self:T(self.lid.."SetDefaultA2GGrouping") self.capgrouping=Grouping or 2 return self end function EASYA2G:SetA2GStartTimeVariation(Start,End) self.capOptionVaryStartTime=Start or 5 self.capOptionVaryEndTime=End or 60 return self end function EASYA2G:SetA2GEngageTargetTypes(types) self.EngageTargetTypes=types or{"Ground"} return self end function EASYA2G:AddHoldingPointA2G(AirbaseName,Coordinate,Altitude,Speed,Heading,LegLength) self:T(self.lid.."AddHoldingPointA2G") local coordinate=Coordinate local EntryCAP={} if Coordinate:IsInstanceOf("ZONE_BASE")then coordinate=Coordinate:GetCoordinate() EntryCAP.Zone=Coordinate end EntryCAP.AirbaseName=AirbaseName EntryCAP.Coordinate=coordinate EntryCAP.Altitude=Altitude or 25000 EntryCAP.Speed=Speed or 300 EntryCAP.Heading=Heading or 90 EntryCAP.LegLength=LegLength or 5 self.ManagedCP[#self.ManagedCP+1]=EntryCAP if self.debug then local mark=MARKER:New(coordinate,self.lid.."Holding Point"):ToAll() end return self end function EASYA2G:_AddSquadron(TemplateName,SquadName,AirbaseName,AirFrames,Skill,Modex,Livery,Frequency,Modulation) self:T(self.lid.."_AddSquadron "..SquadName) local Squadron_One=SQUADRON:New(TemplateName,AirFrames,SquadName) Squadron_One:AddMissionCapability({AUFTRAG.Type.CAS,AUFTRAG.Type.CASENHANCED,AUFTRAG.Type.BAI,AUFTRAG.Type.ALERT5,AUFTRAG.Type.BOMBING,AUFTRAG.Type.STRIKE}) Squadron_One:SetFuelLowThreshold(0.3) Squadron_One:SetTurnoverTime(self.maintenancetime,self.repairtime) Squadron_One:SetModex(Modex) Squadron_One:SetLivery(Livery) Squadron_One:SetSkill(Skill or AI.Skill.AVERAGE) Squadron_One:SetMissionRange(self.missionrange) local wing=self.wings[AirbaseName][1] wing:AddSquadron(Squadron_One) wing:NewPayload(TemplateName,-1,{AUFTRAG.Type.CAS,AUFTRAG.Type.CASENHANCED,AUFTRAG.Type.BAI,AUFTRAG.Type.ALERT5,AUFTRAG.Type.BOMBING,AUFTRAG.Type.STRIKE},75) return self end function EASYA2G:_TryAssignMission(ReadyFlightGroups,Auftrag,Group,WingSize) self:T("_TryAssignMission for size "..WingSize or 1) local assigned=false local wingsize=WingSize or 1 local mindist=0 local disttable={} if Group and Group:IsAlive()then local gcoord=Group:GetCoordinate()or COORDINATE:New(0,0,0) self:T(self.lid..string.format("Assignment for %s",Group:GetName())) for _name,_FG in pairs(ReadyFlightGroups or{})do local FG=_FG local fcoord=FG:GetCoordinate() local dist=math.floor(UTILS.Round(fcoord:Get2DDistance(gcoord)/1000,1)) self:T(self.lid..string.format("FG %s Distance %dkm",_name,dist)) disttable[#disttable+1]={FG=FG,dist=dist} if dist>mindist then mindist=dist end end local function sortDistance(a,b) return a.distmindist then mindist=dist point=data.Coordinate end end end return point end function EASYA2G:_AssignMission(Cluster) self:I(self.lid.."_AssignMission") local overhead=self.overhead local capspeed=self.capspeed+100 local capalt=self.capalt or 5000 local maxsize=self.maxinterceptsize local repeatsonfailure=self.repeatsonfailure local wings=self.wings local ctlpts=self.ManagedCP local MaxAliveMissions=self.MaxAliveMissions local nogozoneset=self.NoGoZoneSet local conflictzoneset=self.ConflictZoneSet local ReadyFlightGroups=self.ReadyFlightGroups if Cluster.ctype==INTEL.Ctype.AIRCRAFT then return end local contact=self.Intel:GetHighestThreatContact(Cluster) local name=contact.groupname local threat=contact.threatlevel local position=self.Intel:CalcClusterFuturePosition(Cluster,300) local bestdistance=2000*1000 local targetairwing=nil local targetawname="" local clustersize=self.Intel:ClusterCountUnits(Cluster)or 1 local wingsize=math.abs(overhead*(clustersize+1)) if wingsize>maxsize then wingsize=maxsize end local retrymission=true if Cluster.mission and(not Cluster.mission:IsOver())then retrymission=false end if(retrymission)and(wingsize>=1)then MESSAGE:New(string.format("**** %s Attackers need wingsize %d",UTILS.GetCoalitionName(self.coalition),wingsize),15,"A2G"):ToAllIf(self.debug):ToLog() for _,_data in pairs(wings)do local airwing=_data[1] local zone=_data[2] local zonecoord=zone:GetCoordinate() local name=_data[3] local coa=AIRBASE:FindByName(name):GetCoalition() local distance=position:DistanceFromPointVec2(zonecoord) local airframes=airwing:CountAssets(true) local samecoalitionab=coa==self.coalition and true or false if distance=wingsize and samecoalitionab==true then bestdistance=distance targetairwing=airwing targetawname=name end end for _,_data in pairs(ctlpts)do local data=_data local name=data.AirbaseName local zonecoord=data.Coordinate if data.Zone then zonecoord=data.Zone:GetCoordinate() end local airwing=wings[name][1] local coa=AIRBASE:FindByName(name):GetCoalition() local samecoalitionab=coa==self.coalition and true or false local distance=position:DistanceFromPointVec2(zonecoord) local airframes=airwing:CountAssets(true) if distance=wingsize and samecoalitionab==true then bestdistance=distance targetairwing=airwing targetawname=name end end local text=string.format("Closest Airwing is %s",targetawname) local m=MESSAGE:New(text,10,"EasyA2G"):ToAllIf(self.debug):ToLog() if targetairwing then local AssetCount=targetairwing:CountAssetsOnMission(MissionTypes,Cohort) local missioncount=self:_CountAliveAuftrags() self:T(self.lid.." Assets on Mission "..AssetCount) if missioncount0 then InterceptAuftrag:AddConditionSuccess( function(group,zoneset,conflictset) local success=false if group and group:IsAlive()then local coord=group:GetCoordinate() if coord and zoneset:Count()>0 and zoneset:IsCoordinateInZone(coord)then success=true end if coord and conflictset:Count()>0 and conflictset:IsCoordinateInZone(coord)then success=false end else success=true end return success end, contact.group, nogozoneset, conflictzoneset ) end InterceptAuftrag:AddConditionFailure( function() local failure=false if InterceptAuftrag:CountOpsGroups()==0 and InterceptAuftrag:IsExecuting()then failure=true end return failure end ) table.insert(self.ListOfAuftrag,InterceptAuftrag) local assigned,rest=self:_TryAssignMission(ReadyFlightGroups,InterceptAuftrag,contact.group,wingsize) if not assigned then InterceptAuftrag:SetRequiredAssets(rest) targetairwing:AddMission(InterceptAuftrag) end Cluster.mission=InterceptAuftrag end else MESSAGE:New("**** Not enough airframes available or max mission limit reached!",15,"EasyA2G"):ToAllIf(self.debug):ToLog() end end end function EASYA2G:_StartIntel() self:T(self.lid.."_StartIntel") local BlueAir_DetectionSetGroup=SET_GROUP:New() BlueAir_DetectionSetGroup:FilterPrefixes(self.EWRName) BlueAir_DetectionSetGroup:FilterStart() local BlueIntel=INTEL:New(BlueAir_DetectionSetGroup,self.coalitionname,self.alias) BlueIntel:SetClusterAnalysis(true,false,false) BlueIntel:SetForgetTime(300) BlueIntel:SetAcceptZones(self.GoZoneSet) BlueIntel:SetRejectZones(self.NoGoZoneSet) BlueIntel:SetConflictZones(self.ConflictZoneSet) BlueIntel:SetVerbosity(0) if self.usecorridors==true then BlueIntel:SetCorridorZones(self.corridorzones) if self.corridorfloor or self.corridorceiling then BlueIntel:SetCorridorLimitsFeet(self.corridorfloor,self.corridorceiling) end end BlueIntel:Start() if self.debug then BlueIntel.debug=true end local function AssignCluster(Cluster) self:_AssignMission(Cluster) end function BlueIntel:onbeforeNewCluster(From,Event,To,Cluster) AssignCluster(Cluster) end self.Intel=BlueIntel return self end function EASYA2G:onafterStart(From,Event,To) self:T({From,Event,To}) self:_StartIntel() self:_CreateAirwings() self:_CreateSquads() self:_SetTankerPatrolPoints() self:_SetAwacsPatrolPoints() self:_SetReconPatrolPoints() self:__Status(-10) return self end function EASYA2G:onafterStatus(From,Event,To) self:T({From,Event,To}) local cleaned=false local cleanlist={} for _,_auftrag in pairs(self.ListOfAuftrag)do local auftrag=_auftrag if auftrag and(not(auftrag:IsCancelled()or auftrag:IsDone()or auftrag:IsOver()))then table.insert(cleanlist,auftrag) cleaned=true end end if cleaned==true then self.ListOfAuftrag=nil self.ListOfAuftrag=cleanlist end local function counttable(tbl) local count=0 for _,_data in pairs(tbl)do count=count+1 end return count end local wings=counttable(self.ManagedAW) local squads=counttable(self.ManagedSQ) local caps=counttable(self.ManagedCP) local assets=0 local instock=0 local capmission=0 local interceptmission=0 local reconmission=0 local awacsmission=0 local tankermission=0 local alert5mission=0 for _,_wing in pairs(self.wings)do local count=_wing[1]:CountAssetsOnMission(MissionTypes,Cohort) local count2=_wing[1]:CountAssets(true,MissionTypes,Attributes) interceptmission=interceptmission+_wing[1]:CountMissionsInQueue({AUFTRAG.Type.BAI}) reconmission=reconmission+_wing[1]:CountMissionsInQueue({AUFTRAG.Type.RECON}) awacsmission=awacsmission+_wing[1]:CountMissionsInQueue({AUFTRAG.Type.AWACS}) tankermission=tankermission+_wing[1]:CountMissionsInQueue({AUFTRAG.Type.TANKER}) alert5mission=alert5mission+_wing[1]:CountMissionsInQueue({AUFTRAG.Type.ALERT5}) assets=assets+count instock=instock+count2 local assetsonmission=_wing[1]:GetAssetsOnMission({AUFTRAG.Type.BAI,AUFTRAG.Type.ALERT5}) self.ReadyFlightGroups=nil self.ReadyFlightGroups={} for _,_asset in pairs(assetsonmission or{})do local asset=_asset local FG=asset.flightgroup if FG then local name=FG:GetName() local engage=FG:IsEngaging() local hasmissiles=FG:CanAirToGround() local isalert5=(FG:GetMissionCurrent()~=nil and FG:GetMissionCurrent().type==AUFTRAG.Type.ALERT5)and true or false local ready=hasmissiles and FG:IsFuelGood()and(FG:IsAirborne()or isalert5) self:T(string.format("Flightgroup %s Engaging = %s Ready = %s (HasAmmo = %s HasFuel = %s Alert5 = %s)",tostring(name),tostring(engage),tostring(ready),tostring(hasmissiles),tostring(FG:IsFuelGood()),tostring(isalert5))) if ready then self.ReadyFlightGroups[name]=FG end end end end if self.Monitor then local threatcount=#self.Intel.Clusters or 0 local text=self.alias text=text.."\nWings: "..wings.."\nSquads: "..squads.."\nHoldPoints: "..caps.."\nAssets on Mission: "..assets.."\nAssets in Stock: "..instock text=text.."\nThreats: "..threatcount text=text.."\nAirWing alive Missions: "..capmission+awacsmission+tankermission+reconmission+interceptmission+alert5mission text=text.."\n - A2G Attack: "..interceptmission text=text.."\n - AWACS: "..awacsmission text=text.."\n - TANKER: "..tankermission text=text.."\n - Recon: "..reconmission text=text.."\n - Alert5 "..alert5mission text=text.."\nMission Limit: "..self.MaxAliveMissions MESSAGE:New(text,15,"A2G"):ToAll():ToLogIf(self.debug) end self:__Status(30) return self end TARS_SESSION={} TARS_SESSION.debug=false TARS_SESSION.debugunitsearch=false TARS={} TARS.version="v2.3.1" TARS.locale=TARS.locale or"en" TARS.debug=false TARS.mooseScoring=true TARS.valueScoring=100 TARS.landingDelay=30 TARS.debriefDelay=60 TARS.landingDistance=2500 TARS.PilotParameterHelper=false TARS._vAltMin=100 TARS._vRangeMin=TARS._vAltMin*20 TARS._vAltOpti=500 TARS._vRangeOpti=TARS._vAltOpti*5 TARS._vAltMax=1500 TARS._vRangeMax=TARS._vAltMax*3 TARS.filmLimitEnabled=true TARS.filmLimitMax=25 TARS.detectUnits=true TARS.detectStatics=false TARS.units={air=false,ground=true,ship=true} TARS.statics={ farps=true, captureExceptions=false,captureExceptionsList={}, captureUnique=false,captureUniqueList={}, } TARS.recoNameFilter={enabled=false,keyword="Reco"} TARS.targetNameFilter={ enabled=true, keywords={ [coalition.side.BLUE]={"USA"}, [coalition.side.RED]={"USSR"}, }, } TARS.reconTypes={ ["MiG-21Bis"]=true,["AJS37"]=true,["Mirage-F1EE"]=true, ["F-5E-3"]=true,["F-14A-135-GR"]=true,["F-14B"]=true, ["F-4E-45MC"]=true,["P-51D"]=true,["P-51D-30-NA"]=true, ["SpitfireLFMkIX"]=true,["FW-190A8"]=true,["FW-190D9"]=true, ["SA342M"]=true,["SA342L"]=true,["UH-1H"]=true,["OH58D"]=true, ["Mi-8MT"]=true,["MH-6J"]=true, ["OH-6A"]=true, } TARS.parameters={} TARS.parameters["F-4E-45MC"]={minAlt=100,maxAlt=8000,maxRoll=10,maxPitch=15,fov=23,duration=120,offset=math.rad(40),overlap=0.25,min_interval=3,name="RF-4E with KS-87 Forward Oblique Camera"} TARS.parameters["MiG-21Bis"]={minAlt=500,maxAlt=8000,maxRoll=10,maxPitch=15,fov=52,duration=140,offset=math.rad(40),overlap=0.25,min_interval=3,name="MiG-21R with Day recce pod"} TARS.parameters["AJS37"]={minAlt=15,maxAlt=8000,maxRoll=10,maxPitch=15,fov=25,duration=120,offset=math.rad(40),overlap=0.25,min_interval=3,name="SF 37"} TARS.parameters["Mirage-F1EE"]={minAlt=1524,maxAlt=8000,maxRoll=10,maxPitch=15,fov=20,duration=400,offset=math.rad(40),overlap=0.25,min_interval=3,name="Mirage-F1CR with Omera 33"} TARS.parameters["F-5E-3"]={minAlt=762,maxAlt=8000,maxRoll=15,maxPitch=15,fov=70,duration=300,offset=math.rad(40),overlap=0.25,min_interval=3,name="F-5E Tigereye"} TARS.parameters["F-14A-135-GR"]={minAlt=750,maxAlt=8000,maxRoll=10,maxPitch=20,fov=14,duration=400,offset=math.rad(45),overlap=0.25,min_interval=3,name="F-14A TARPS KS-87D"} TARS.parameters["F-14B"]={minAlt=228,maxAlt=8000,maxRoll=10,maxPitch=20,fov=85,duration=400,offset=math.rad(40),overlap=0.25,min_interval=3,name="F-14B TARPS KA-99A"} TARS.parameters["TF-51D"]={minAlt=250,maxAlt=2500,maxRoll=15,maxPitch=15,fov=60,duration=400,offset=math.rad(10),overlap=0.5,min_interval=5,name="TF-51D Mustang RF-51D Photo Recon"} TARS.parameters["P-51D"]={minAlt=250,maxAlt=2500,maxRoll=15,maxPitch=15,fov=60,duration=400,offset=math.rad(10),overlap=0.5,min_interval=5,name="P-51D Mustang F-6D Photo Recon"} TARS.parameters["P-51D-30-NA"]={minAlt=250,maxAlt=2500,maxRoll=15,maxPitch=15,fov=60,duration=400,offset=math.rad(10),overlap=0.5,min_interval=5,name="P-51D-30 Mustang F-6D Photo Recon"} TARS.parameters["SpitfireLFMkIX"]={minAlt=150,maxAlt=2500,maxRoll=15,maxPitch=15,fov=55,duration=350,offset=math.rad(10),overlap=0.5,min_interval=5,name="Spitfire LF Mk IX PR Recon"} TARS.parameters["FW-190A8"]={minAlt=200,maxAlt=2500,maxRoll=15,maxPitch=15,fov=60,duration=350,offset=math.rad(10),overlap=0.5,min_interval=5,name="FW-190 A-8 Tactical Recon"} TARS.parameters["FW-190D9"]={minAlt=250,maxAlt=2500,maxRoll=15,maxPitch=15,fov=60,duration=350,offset=math.rad(10),overlap=0.5,min_interval=5,name="FW-190 D-9 Tactical Recon"} TARS.parameters["SA342M"]={minAlt=20,maxAlt=1000,maxRoll=35,maxPitch=25,fov=18,duration=350,offset=math.rad(10),overlap=0.5,min_interval=5,name="SA342M EO/IR LIGHT RECO"} TARS.parameters["SA342L"]={minAlt=20,maxAlt=1000,maxRoll=35,maxPitch=25,fov=18,duration=350,offset=math.rad(10),overlap=0.5,min_interval=5,name="SA342L EO/IR LIGHT RECO"} TARS.parameters["OH58D"]={minAlt=30,maxAlt=1200,maxRoll=35,maxPitch=25,fov=12,duration=350,offset=math.rad(12),overlap=0.5,min_interval=5,name="OH-58D MMS EO/IR RECO"} TARS.parameters["UH-1H"]={maxRoll=50,maxPitch=45,duration=900,offset=math.rad(6),name="UH-1H VISUAL/CREW RECO",minAlt=TARS._vAltMin,minRange=TARS._vRangeMin,optimalAlt=TARS._vAltOpti,optimalRange=TARS._vRangeOpti,maxAlt=TARS._vAltMax,maxRange=TARS._vRangeMax} TARS.parameters["Mi-8MT"]={maxRoll=50,maxPitch=45,duration=900,offset=math.rad(6),name="Mi-8MT VISUAL/CREW RECO",minAlt=TARS._vAltMin,minRange=TARS._vRangeMin,optimalAlt=TARS._vAltOpti,optimalRange=TARS._vRangeOpti,maxAlt=TARS._vAltMax,maxRange=TARS._vRangeMax} TARS.parameters["MH-6J"]={maxRoll=50,maxPitch=45,duration=900,offset=math.rad(6),name="MH-6J VISUAL CLOSE RECO",minAlt=TARS._vAltMin,minRange=TARS._vRangeMin,optimalAlt=TARS._vAltOpti,optimalRange=TARS._vRangeOpti,maxAlt=TARS._vAltMax,maxRange=TARS._vRangeMax} TARS.parameters["OH-6A"]={maxRoll=50,maxPitch=45,duration=900,offset=math.rad(6),name="OH-6A Cayuse VISUAL CLOSE RECO",minAlt=TARS._vAltMin,minRange=TARS._vRangeMin,optimalAlt=TARS._vAltOpti,optimalRange=TARS._vRangeOpti,maxAlt=TARS._vAltMax,maxRange=TARS._vRangeMax} TARS.allowedAmmo={ ["AIM-9B"]=true,["AIM-9D"]=true,["AIM-9E"]=true,["AIM-9G"]=true, ["AIM-9H"]=true,["AIM-9J"]=true,["AIM-9L"]=true,["AIM-9M"]=true, ["AIM-9N"]=true,["AIM-9P"]=true,["AIM-9P3"]=true,["AIM-9P5"]=true, ["AIM-9JULI"]=true, ["R-3S"]=true,["R-13M"]=true,["R-13M1"]=true,["R-60"]=true,["R-60M"]=true, ["R550 Magic II"]=true, ["7_62x51"]=true, } TARS.instances={} TARS.groundMenus={} TARS.detectedTargets={} TARS.marks={blue={},red={}} TARS.redMarkCount=150000 TARS.blueMarkCount=160000 TARS.scoring=nil TARS.locale="en" TARS.Messages={ en={ TARS_FILM_START="[TARS] Capture activated. Expositions remaining: %d", TARS_FILM_EXHAUSTED="[TARS] Film exhausted. Return to base for debrief.", TARS_FILM_STOP="[TARS] Session capture ended. Return to base for debrief.", TARS_FILM_TIME_UP="[TARS] Film time exhausted. Return to base.", TARS_FILM_CAP_REACHED="[TARS] Maximum captures reached (%d). Return to base for debrief.", TARS_FILM_STB_MANUAL="[TARS] <>Manual<> Film manual STB.", TARS_FILM_RESUME_MANUAL="[TARS] <>Manual<> Film manual resume.", TARS_FILM_STB_LAND="[TARS] <>Landing<> Film auto STB.", TARS_FILM_RESUME_TO="[TARS] <>TakeOff<> Film auto resume. %d expositions", TARS_FILM_STB_LOCKED="[TARS] Film is STB — takeoff to resume.", TARS_FILM_ALREADY_ACTIVE="[TARS] Film already active.", TARS_FILM_NO_CAPTURE="[TARS] No active film.", TARS_FILM_NO_CAPTURE_STOP="[TARS] No active film to stop.", TARS_CAPTURE_TICK="EXPOSITIONS REMAINING: %d", TARS_CAPTURE_HIT="[TARS] +1 Captured target (%d total)", TARS_CAPTURE_HIT_MAX="[TARS] +1 Captured target (%d total) / %d max", TARS_SESSION_ENDED="[TARS] Session ended. Return to base for debrief.", TARS_LAND_VALIDATED="[TARS] Landing validated, await your debriefing!", TARS_LAND_VALIDATED_TIME="[TARS] Targets shown in %d seconds.", TARS_NOT_AT_BASE="[TARS] Not on allied base or FARP. Return for debrief.", TARS_DEBRIEF_TARGETS="[TARS] %d targets captured. +%d points.", TARS_DEBRIEF_CREDITS="You received %d credits for reconnaissance.", TARS_DEBRIEF_COALITION="%s gathered intel on %d targets.", TARS_READY="[TARS] Ready for recon.", TARS_VALID_OK_HDR="[TARS] Configuration is valid - Ready for takeoff", TARS_VALID_REFUSED_WPN="[TARS] Your configuration loadout is not ready. Check your weapons.", TARS_VALID_REFUSED_AMMO="Refused : ammo %s", TARS_VALID_AIRBORNE="[TARS] Validation only on ground.", TARS_VALID_RUNNING="[TARS] Validation already done — film is running.", TARS_VALID_GROUP_FILTER="[TARS] Task available for group name >%s< only.", TARS_VALIDATE_FIRST="[TARS] Validate first on ground.", TARS_NO_SESSION="[TARS] No session actived.", TARS_CONFIG_CHANGED="[TARS] Config changed — session ended.", TARS_LOADOUT_BAD="[TARS] Loadout not ready. Check your weapons.", TARS_PLATFORM_INFO="[TARS] Platform information", TARS_PLATFORM_LABEL="Platform", TARS_PLATFORM_ALT="Altitude", TARS_PLATFORM_FOV="FOV", TARS_PLATFORM_FILM="Film", TARS_MENU_ROOT="Task TARS", TARS_MENU_VALIDATE="TARS validation", TARS_MENU_INFO="TARS my capture config", TARS_MENU_START="TARS mode : Start filming", TARS_MENU_STB="TARS mode : Standby & Resume", TARS_MENU_STOP="TARS mode : Stop filming", }, de={ TARS_FILM_START="[TARS] Aufnahme aktiviert. Verbleibende Aufnahmen: %d.", TARS_FILM_EXHAUSTED="[TARS] Film aufgebraucht. Kehren Sie zur Basis für das Briefing zurück.", TARS_FILM_STOP="[TARS] Aufnahmesitzung beendet. Kehren Sie zur Basis zurück.", TARS_FILM_TIME_UP="[TARS] Filmzeit abgelaufen. Kehren Sie zur Basis zurück.", TARS_FILM_CAP_REACHED="[TARS] Maximale Aufnahmen erreicht (%d). Kehren Sie zur Basis zurück.", TARS_FILM_STB_MANUAL="[TARS] <>Manuell<> Film manuell auf Standby.", TARS_FILM_RESUME_MANUAL="[TARS] <>Manuell<> Film manuell fortgesetzt.", TARS_FILM_STB_LAND="[TARS] <>Landung<> Film automatisch auf Standby.", TARS_FILM_RESUME_TO="[TARS] <>Takeoff<> Film fortgesetzt. %d Aufnahmen.", TARS_FILM_STB_LOCKED="[TARS] Film ist auf Standby — starten Sie, um fortzufahren.", TARS_FILM_ALREADY_ACTIVE="[TARS] Aufnahme bereits aktiv.", TARS_FILM_NO_CAPTURE="[TARS] Keine aktive Aufnahme.", TARS_FILM_NO_CAPTURE_STOP="[TARS] Keine aktive Aufnahme zum Stoppen.", TARS_CAPTURE_TICK="Verbleibende Aufnahmen: %d.", TARS_CAPTURE_HIT="[TARS] +1 Ziel erfasst (%d gesamt)", TARS_CAPTURE_HIT_MAX="[TARS] +1 Ziel erfasst (%d gesamt) / %d max", TARS_SESSION_ENDED="[TARS] Sitzung beendet. Kehren Sie zur Basis für das Briefing zurück.", TARS_LAND_VALIDATED="[TARS] Landung erfolgreich, bitte warten Sie auf Ihr Debriefing!", TARS_LAND_VALIDATED_TIME="[TARS] Ziele werden in %d Sekunden angezeigt.", TARS_NOT_AT_BASE="[TARS] Nicht auf verbündeter Basis oder FARP. Kehren Sie zurück.", TARS_DEBRIEF_TARGETS="[TARS] %d Ziele erfasst. +%d Punkte.", TARS_DEBRIEF_CREDITS="Sie erhalten %d Credits für die Aufklärung.", TARS_DEBRIEF_COALITION="%s hat Informationen über %d Ziele gesammelt.", TARS_READY="[TARS] Bereit zur Aufklärung.", TARS_VALID_OK_HDR="[TARS] Konfiguration gültig - Bereit zum Start", TARS_VALID_REFUSED_WPN="[TARS] Ihre Konfiguration ist nicht bereit. Überprüfen Sie Ihre Waffen.", TARS_VALID_REFUSED_AMMO="Abgelehnt : Munition %s", TARS_VALID_AIRBORNE="[TARS] Validierung nur am Boden möglich.", TARS_VALID_RUNNING="[TARS] Validierung bereits erfolgt — Film läuft.", TARS_VALID_GROUP_FILTER="[TARS] Aufgabe nur für Gruppenname >%s< verfügbar.", TARS_VALIDATE_FIRST="[TARS] Zuerst am Boden validieren.", TARS_NO_SESSION="[TARS] Keine aktive Sitzung.", TARS_CONFIG_CHANGED="[TARS] Konfiguration geändert — Aufnahme beendet.", TARS_LOADOUT_BAD="[TARS] Ausrüstung nicht bereit. Überprüfen Sie Ihre Waffen.", TARS_PLATFORM_INFO="[TARS] Plattforminformationen", TARS_PLATFORM_LABEL="Plattform", TARS_PLATFORM_ALT="Höhe", TARS_PLATFORM_FOV="Sichtfeld", TARS_PLATFORM_FILM="Film", TARS_MENU_ROOT="Aufgabe TARS", TARS_MENU_VALIDATE="TARS Validierung", TARS_MENU_INFO="TARS meine Aufnahmekonfiguration", TARS_MENU_START="TARS Modus : Aufnahme starten", TARS_MENU_STB="TARS Modus : Standby & Fortsetzen", TARS_MENU_STOP="TARS Modus : Aufnahme stoppen", }, fr={ TARS_FILM_START="[TARS] Capture activée. Expositions restantes : %d", TARS_FILM_EXHAUSTED="[TARS] Film épuisé. Retournez à la base pour le compte-rendu.", TARS_FILM_STOP="[TARS] Session de capture terminée. Retournez à la base.", TARS_FILM_TIME_UP="[TARS] Temps de film épuisé. Retournez à la base.", TARS_FILM_CAP_REACHED="[TARS] Nombre maximum de captures atteint (%d). Retournez à la base.", TARS_FILM_STB_MANUAL="[TARS] <>Manuel<> Film en STB manuel.", TARS_FILM_RESUME_MANUAL="[TARS] <>Manuel<> Reprise manuel du film.", TARS_FILM_STB_LAND="[TARS] <>Atterrissage<> Film en STB automatique.", TARS_FILM_RESUME_TO="[TARS] <>Décollage<> Film repris. %d expositions", TARS_FILM_STB_LOCKED="[TARS] Film en STB — décollez pour reprendre.", TARS_FILM_ALREADY_ACTIVE="[TARS] Film déjà activé.", TARS_FILM_NO_CAPTURE="[TARS] Aucun film activé.", TARS_FILM_NO_CAPTURE_STOP="[TARS] Aucun film actif à stopper.", TARS_CAPTURE_TICK="[TARS] EXPOSITIONS RESTANTES : %d", TARS_CAPTURE_HIT="[TARS] +1 Cible capturée (%d au total)", TARS_CAPTURE_HIT_MAX="[TARS] +1 Cible capturée (%d au total) / %d max", TARS_SESSION_ENDED="[TARS] Session terminée. Retournez à la base pour le debriefing.", TARS_LAND_VALIDATED="[TARS] Atterrissage validé, attendez votre debriefing !", TARS_LAND_VALIDATED_TIME="[TARS] Cibles affichées dans %d seconds.", TARS_NOT_AT_BASE="[TARS] Vous n'êtes pas sur une base ou FARP alliée. Retourné pour le debriefing.", TARS_DEBRIEF_TARGETS="[TARS] %d cibles capturées. +%d points.", TARS_DEBRIEF_CREDITS="Vous avez reçu %d crédits pour la reconnaissance.", TARS_DEBRIEF_COALITION="%s a recueilli des renseignements sur %d cibles.", TARS_READY="[TARS] Prêt pour la reconnaissance.", TARS_VALID_OK_HDR="[TARS] Configuration valide - Prêt au décollage", TARS_VALID_REFUSED_WPN="[TARS] Votre configuration n'est pas prête. Vérifiez vos armes.", TARS_VALID_REFUSED_AMMO="Refusé : munition %s", TARS_VALID_AIRBORNE="[TARS] Validation uniquement au sol.", TARS_VALID_RUNNING="[TARS] Validation déjà effectuée — le film tourne.", TARS_VALID_GROUP_FILTER="[TARS] Tâche disponible pour le groupe >%s< uniquement.", TARS_VALIDATE_FIRST="[TARS] Validez d'abord au sol.", TARS_NO_SESSION="[TARS] Aucune session active.", TARS_CONFIG_CHANGED="[TARS] Configuration modifiée — session terminée.", TARS_LOADOUT_BAD="[TARS] Chargement pas prêt. Vérifiez vos armes.", TARS_PLATFORM_INFO="[TARS] Informations sur la plateforme", TARS_PLATFORM_LABEL="Plateforme", TARS_PLATFORM_ALT="Altitude", TARS_PLATFORM_FOV="Champ de vision", TARS_PLATFORM_FILM="Film", TARS_MENU_ROOT="Mission TARS", TARS_MENU_VALIDATE="TARS validation", TARS_MENU_INFO="TARS ma config de capture", TARS_MENU_START="TARS mode : Démarrer le film", TARS_MENU_STB="TARS mode : Standby & Reprise", TARS_MENU_STOP="TARS mode : Arrêter le film", }, } function TARS.getRoll(mooseUnit) return mooseUnit:GetRoll()or 0 end function TARS.getPitch(mooseUnit) return mooseUnit:GetPitch()or 0 end function TARS.life2text(life) if life==nil then return"Undefined" elseif life>90 then return"No damage" elseif life>70 then return"Slightly damage" elseif life>40 then return"Damaged" elseif life>20 then return"Major damage" elseif life>0 then return"Destroyed" else return"Undefined" end end function TARS_SESSION:_SetSharedParams(unit) self:T(self.lid.."_SetSharedParams") self.unit=unit self.vec3=unit:GetVec3() self.coa=unit:GetCoalition() self.type=unit:GetTypeName() self.group=unit:GetGroup() self.groupID=unit:GetGroup():GetID() self.objectName=unit:GetName() self.playerName=unit:GetPlayerName() self.playerID=unit:GetID() self.ammo=unit:GetAmmo() self.time=timer.getTime() end function TARS_SESSION:SetObjectParamsLight(unit) self:T2(self.lid.."SetObjectParamsLight") self:_SetSharedParams(unit) end function TARS_SESSION:SetObjectParams(unit) self:T(self.lid.."SetObjectParams") self:_SetSharedParams(unit) self.category=unit:GetGroup():GetCategory() self.capturing=false self.duration=TARS.parameters[self.type].duration self.targetList={} self.captureCount=0 self.loop=false self.standby=false self.filmExhausted=false self.sessionEnded=false self.wasCapturing=false self.landingScheduled=false if self.playerName and TARS.groundMenus[self.playerName]and self.Callback then self.Callback:_MenuAddValidation(self.playerName) end return self end function TARS_SESSION:New(unit,Callback) local self=BASE:Inherit(self,BASE:New()) self.lid=string.format("TARS_SESSION %s | ",TARS.version) self.Callback=Callback self.PilotParameterHelper=Callback.PilotParameterHelper self:SetObjectParams(unit) self:T("TARS_SESSION created — unit="..tostring(self.objectName) .." type="..tostring(self.type)) return self end function TARS_SESSION:AddToTargetList(list) self:T(self.lid.."AddToTargetList") for k,v in pairs(list)do if self.targetList[k]==nil then self.targetList[k]=self:_FreezeUnit(v) self.captureCount=(self.captureCount or 0)+1 local msg if TARS.filmLimitEnabled then msg=self.Callback:_Txt("TARS_CAPTURE_HIT_MAX", self.captureCount,TARS.filmLimitMax) else msg=self.Callback:_Txt("TARS_CAPTURE_HIT",self.captureCount) end self.Callback:_MsgUnit(msg,4,self.playerName) end end end function TARS_SESSION:ReturnReconTargets() self:T(self.lid.."ReturnReconTargets") local count=0 for k,v in next,self.targetList do if v.unit and v.unit:IsAlive()then local existing=self.Callback.detectedTargets[v.name] if not existing then count=count+1 self.Callback:OutMark(v,self.coa) self.Callback.detectedTargets[v.name]=v self:T("New target: "..v.type.."/"..v.name) elseif existing.life~=v.life then local markID=TARS.marks.blue[v.name]or TARS.marks.red[v.name] if markID then trigger.action.removeMark(markID)end count=count+1 self.Callback:OutMark(v,self.coa) self.Callback.detectedTargets[v.name]=v self:T("Updated "..v.name.." life " ..tostring(existing.life).."→"..tostring(v.life)) end end self.targetList[k]=nil end return count end function TARS_SESSION:CaptureData() self:T(self.lid.."CaptureData") if self.duration<=0 then self.Callback:_MsgUnit( self.Callback:_Txt("TARS_FILM_EXHAUSTED"),2,self.playerName) return end self.capturing=true self.loop=true self.standby=false self:T("FILM START — film="..self.duration.."s") self.Callback:_MsgUnit( self.Callback:_Txt("TARS_FILM_START",self.duration),5,self.playerName) timer.scheduleFunction(TARS_SESSION.CaptureLoop,self,timer.getTime()+2) end function TARS_SESSION:Delete() self:T(self.lid.."Delete") TARS.instances[self.objectName]=nil end function TARS_SESSION:_NormalizeLife(unit) if not unit or not unit:IsAlive()then return nil end local rlife=unit:GetLifeRelative()*100 if rlife==-1 then return nil end return rlife end function TARS_SESSION:_FreezeUnit(_Object) self:T(self.lid.."_FreezeUnit") local snap={} snap.unit=_Object snap.dcsObj=_Object:GetDCSObject() snap.category=_Object:GetCategory() snap.type=_Object:GetTypeName() snap.name=_Object:GetName() snap.point=_Object:GetVec3() snap.time=timer.getTime() if snap.category==Object.Category.UNIT and _Object then snap.groupID=_Object:GetGroup():GetID() snap.groupCat=_Object:GetGroup():GetCategory() snap.coa=_Object:GetCoalition() snap.ammo=_Object:GetAmmo() snap.life=self:_NormalizeLife(_Object) end snap.playername=self.playerName return snap end function TARS_SESSION:_OffsetCalc(unit,params,center_shift) center_shift=center_shift or 0 local pos=unit:GetPosition() local vec3=unit:GetVec3() local MSL=land.getHeight({x=vec3.x,y=vec3.z}) local alt=vec3.y-MSL local rad=math.atan2(pos.x.x,pos.x.z) local dist=(alt/math.tan(params.offset))+center_shift return{ x=vec3.x+math.sin(rad)*dist, z=vec3.z+math.cos(rad)*dist, MSL=MSL, alt=alt, rad=rad, } end function TARS_SESSION:_ValidateObjectFound(_Object) self:T(self.lid.."_ValidateObjectFound "..tostring(_Object:GetName())) if not(_Object and _Object:IsAlive())then return false end if _Object:GetCoalition()==self.coa then return false end if self.Callback.targetNameFilter.enabled then local keywords=self.Callback.targetNameFilter.keywords[_Object:GetCoalition()] local targetName=string.lower(_Object:GetName()or"") local targetGroup local targetGroupName if _Object:IsInstanceOf("UNIT")then targetGroup=_Object:GetGroup() if targetGroup then targetGroupName=string.lower(targetGroup:GetName())end end if type(keywords)=="string"then keywords={keywords}end local matched=false for _,kw in pairs(keywords or{})do if string.find(targetName,string.lower(kw))then matched=true;break end if targetGroupName and string.find(targetName,string.lower(kw))then matched=true;break end end if not matched then return false end end local typeName=_Object:GetTypeName() local typeNameLower=string.lower(typeName) local objCat=_Object:GetCategory() self:T(self.lid.."_ValidateObjectFound Name Filter Passed!") if objCat==Object.Category.UNIT then local desc=_Object:GetDesc() local unitCat=desc and desc.category self:T(self.lid.."_ValidateObjectFound Name Category Check "..tostring(unitCat)) if unitCat==Unit.Category.AIRPLANE or unitCat==Unit.Category.HELICOPTER then return self.Callback.units.air elseif unitCat==Unit.Category.GROUND_UNIT then return self.Callback.units.ground elseif unitCat==Unit.Category.SHIP then return self.Callback.units.ship end elseif objCat==Object.Category.STATIC or objCat==Object.Category.BASE then if self.Callback.statics.farps and string.find(typeNameLower,"farp")then return true end if self.Callback.statics.captureUnique then return self.Callback.statics.captureUniqueList[typeName]==true elseif self.Callback.statics.captureExceptions then for _,exName in pairs(self.Callback.statics.captureExceptionsList)do if string.find(typeNameLower,string.lower(exName))then return true end end end end return false end function TARS_SESSION:CaptureLoop() if not self or not self.loop then return end if self.capturing and self.standby then timer.scheduleFunction(TARS_SESSION.CaptureLoop,self,timer.getTime()+10) return end if self.capturing and self.duration>0 then local params=self.Callback.parameters[self.type] local interval=self:_CalcInterval(self.unit,params) self.duration=self.duration-1 self.Callback:_MsgUnit( self.Callback:_Txt("TARS_CAPTURE_TICK",math.max(0,math.floor(self.duration))), math.min(interval,9),self.playerName,true) self:AddToTargetList(self:FindTargets()) if self.Callback.filmLimitEnabled and self.captureCount>=self.Callback.filmLimitMax then self.Callback:_MsgUnit( self.Callback:_Txt("TARS_FILM_CAP_REACHED",self.Callback.filmLimitMax), 8,self.playerName) self.Callback:StopCapture(self) return end if self.debugunitsearch then self:T(self.lid..string.format( "CaptureLoop interval=%.1fs duration=%.0fs",interval,self.duration)) end timer.scheduleFunction(TARS_SESSION.CaptureLoop,self,timer.getTime()+interval) end if self.duration<=0 and self.loop then self.loop=false self.Callback:_MsgUnit( self.Callback:_Txt("TARS_FILM_TIME_UP"),8,self.playerName) self.Callback:StopCapture(self) end end function TARS_SESSION:_CalcVisualRange(params,altitude) if altitude<=params.minAlt then return params.minRange elseif altitude<=params.optimalAlt then local t=(altitude-params.minAlt)/(params.optimalAlt-params.minAlt) return params.minRange+t*(params.optimalRange-params.minRange) elseif altitude<=params.maxAlt then local t=(altitude-params.optimalAlt)/(params.maxAlt-params.optimalAlt) return params.optimalRange+t*(params.maxRange-params.optimalRange) else return params.maxRange end end function TARS_SESSION:_CalcInterval(unit,params) local vec3=unit:GetVec3() local alt=vec3.y-land.getHeight({x=vec3.x,y=vec3.z}) alt=math.max(alt,1) local elev=params.offset local half_fov=math.rad(params.fov/2) local d_ground=alt/math.tan(elev) local b=d_ground*math.tan(half_fov) local diameter=2*b local vel=unit:GetVelocityVec3() local speed=math.sqrt(vel.x*vel.x+vel.y*vel.y+vel.z*vel.z) speed=math.max(speed,10) local overlap=params.overlap or 0.5 local interval=(diameter*(1-overlap))/speed local min_interval=params.min_interval or 2 return math.max(min_interval,math.min(120,interval)) end function TARS_SESSION.isInEllipse(ox,oz,cx,cz,a,b,heading_rad) local dx=ox-cx local dz=oz-cz local cos_h=math.cos(heading_rad) local sin_h=math.sin(heading_rad) local lx=dx*sin_h+dz*cos_h local ly=-dx*cos_h+dz*sin_h return(lx/a)^2+(ly/b)^2<=1 end function TARS_SESSION:FindTargets() local unit=self.unit local params=self.Callback.parameters[self.type] local roll=math.abs(TARS.getRoll(unit)) local pitch=math.abs(TARS.getPitch(unit)) local isFlat=rollmath.rad(2)then local d_near=alt/math.tan(elev+half_fov) local d_far=alt/math.tan(elev-half_fov) a=(d_far-d_near)/2 center_shift=(d_far+d_near)/2-d_ground else a=b center_shift=0 self:T(self.lid.."FindTargets: elev-fov margin too small, fallback to circle") end local offset=self:_OffsetCalc(unit,params,center_shift) local unit_pos=unit:GetVec3() local hdg=unit:GetHeading() local expected_x=unit_pos.x+math.sin(math.rad(hdg))*(d_ground+center_shift) local expected_z=unit_pos.z+math.cos(math.rad(hdg))*(d_ground+center_shift) self:T(string.format( "OFFSET DRIFT: _OffsetCalc=(%.0f,%.0f) expected=(%.0f,%.0f) drift=(Δx=%.0f,Δz=%.0f)", offset.x,offset.z, expected_x,expected_z, offset.x-expected_x, offset.z-expected_z)) local coordinate=self.coordinate or COORDINATE:New(offset.x,offset.MSL,offset.z) coordinate=coordinate:UpdateFromVec3({x=offset.x,y=offset.MSL,z=offset.z}) self.coordinate=coordinate local scan_radius=math.max(a,b) local heading=unit:GetHeading() if self.debugunitsearch then local searchzone=self.searchzone or ZONE_RADIUS:New("TARS Debug",coordinate:GetVec2(),scan_radius) if searchzone.DrawID then searchzone:UndrawZone()end searchzone:UpdateFromVec2(coordinate:GetVec2(),scan_radius) searchzone:DrawZone(-1,{0,0,1},1,{0,1,0},.2,2,true) self.searchzone=searchzone self:ScheduleOnce(30,ZONE_BASE.UndrawZone,searchzone) end if self.PilotParameterHelper==true then self:T({Roll=roll,Pitch=pitch,AGL=alt,a=a,b=b,shift=center_shift}) if roll>params.maxRoll then MESSAGE:New(string.format("Roll - NOK out of parameters (%d°)!",roll),9,"PARAM"):ToUnit(unit) elseif pitch>params.maxPitch then MESSAGE:New(string.format("Pitch - NOK out of parameters (%d°)!",pitch),9,"PARAM"):ToUnit(unit) elseif altparams.maxAlt then MESSAGE:New(string.format("AGL - NOK too high or too low (%dm)!",alt),9,"PARAM"):ToUnit(unit) else MESSAGE:New(string.format("Params OK | a=%.0fm b=%.0fm shift=%.0fm",a,b,center_shift),9,"PARAM"):ToUnit(unit) end end local ScannedUnits=self.Callback.detectUnits and coordinate:ScanUnits(scan_radius)or nil local ScannedStatics=self.Callback.detectStatics and coordinate:ScanStatics(scan_radius)or nil local targetList={} if alt>params.minAlt and alt0 then local ok,result=pcall(string.format,text,...) return ok and result or text end return text end function TARS:_MsgUnit(text,seconds,playerName,Silent) local unit=CLIENT:FindByPlayerName(playerName) if unit then MESSAGE:New(text,seconds,"TARS"):ToUnit(unit) end if self.debug==true then MESSAGE:New(text,seconds,"TARS"):ToAll() end if unit and self.SRS and(not Silent)then local srsText=string.gsub(text,"^%[TARS%] ?",playerName..", ") srsText=string.gsub(srsText,"[<>]","") MESSAGE:New(srsText,seconds,"TARS"):ToSRS() end end function TARS:_MsgCoalition(text,seconds,coa) MESSAGE:New(text,seconds,"TARS"):ToCoalition(coa) end function TARS:_AddUserPoints(name,points) if dcsbot and dcsbot.addUserPoints then dcsbot.addUserPoints(name,points) self:T(self.lid.."AddUserPoints +"..tostring(points).." for "..tostring(name)) return true end return false end function TARS:_GetUnitFromPlayerName(playerName) local data=TARS.groundMenus[playerName] if not data or not data.unitName then return nil end return UNIT:FindByName(data.unitName) end function TARS:_CbValidate(playerName) local u=self:_GetUnitFromPlayerName(playerName) if u then self:CheckTask(u)end end function TARS:_CbInfo(playerName) local u=self:_GetUnitFromPlayerName(playerName) if not u then return end local inst=self:GetInstance(u:GetName()) if inst then self:ShowPlatformInfo(inst) else self:_MsgUnit(self:_Txt("TARS_NO_SESSION"),4,playerName)end end function TARS:_CbStart(playerName) local u=self:_GetUnitFromPlayerName(playerName) if not u then return end local inst=self:GetInstance(u:GetName()) if inst then self:Control(inst) else self:_MsgUnit(self:_Txt("TARS_VALIDATE_FIRST"),5,playerName)end end function TARS:_CbStb(playerName) local u=self:_GetUnitFromPlayerName(playerName) if not u then return end local inst=self:GetInstance(u:GetName()) if inst then self:StandbyCapture(inst) else self:_MsgUnit(self:_Txt("TARS_FILM_NO_CAPTURE"),4,playerName)end end function TARS:_CbStop(playerName) local u=self:_GetUnitFromPlayerName(playerName) if not u then return end local inst=self:GetInstance(u:GetName()) if inst then self:StopCapture(inst) else self:_MsgUnit(self:_Txt("TARS_FILM_NO_CAPTURE"),4,playerName)end end function TARS:CreateInstance(unit) self:T(self.lid.."CreateInstance") local inst=TARS_SESSION:New(unit,self) self.instances[inst.objectName]=inst return inst end function TARS:CheckIfRecon(unit) if not unit then return false end local typeName=unit:GetTypeName() if not TARS.reconTypes[typeName]then return false end if TARS.recoNameFilter.enabled then local grp=unit:GetGroup() local name=(grp and grp:GetName())or unit:GetName()or"" if not string.find(string.lower(name),string.lower(TARS.recoNameFilter.keyword))then return false end end if not TARS.parameters[typeName]then return false end local ammo=unit:GetAmmo() if type(ammo)~="table"then return true end for _,w in ipairs(ammo)do if w and w.desc then local name=w.desc.displayName or w.desc.typeName or w.desc.name if name and not TARS.allowedAmmo[name]then return false,name end end end return true end function TARS:CheckTask(unit) if not unit or not unit:IsAlive()then return end local typeName=unit:GetTypeName() local params=self.parameters[typeName] if unit:InAir(false)then local inst=self:GetInstance(unit:GetName()) local msg=(inst and inst.capturing) and self:_Txt("TARS_VALID_RUNNING") or self:_Txt("TARS_VALID_AIRBORNE") self:_MsgUnit(msg,5,unit:GetPlayerName()or unit:GetName()) return end if self.recoNameFilter.enabled then local grp=unit:GetGroup() local groupName=grp and grp:GetName()or"" if not string.find(string.lower(groupName),string.lower(self.recoNameFilter.keyword))then self:_MsgUnit( self:_Txt("TARS_VALID_GROUP_FILTER",self.recoNameFilter.keyword), 10,unit:GetPlayerName()or unit:GetName(),true) return end end local playerName=unit:GetPlayerName()or unit:GetName() local reconOk,refusedWeapon=self:CheckIfRecon(unit) TARS.groundMenus[playerName]=TARS.groundMenus[playerName]or{} TARS.groundMenus[playerName].approved=reconOk TARS.groundMenus[playerName].playerName=playerName if reconOk then self:T("VALIDATE OK — "..unit:GetName().." / "..tostring(playerName)) self:_MenuRemoveValidation(playerName) local msg=self:_Txt("TARS_VALID_OK_HDR") self:_MsgUnit(msg,15,playerName) local msg="" ..self:_Txt("TARS_PLATFORM_LABEL").." : "..params.name.."\n" ..self:_Txt("TARS_PLATFORM_ALT").." : "..params.minAlt.."m - "..params.maxAlt.."m AGL\n" ..self:_Txt("TARS_PLATFORM_FOV").." : "..tostring(params.fov or"-").."\xc2\xb0\n" ..self:_Txt("TARS_PLATFORM_FILM").." : "..params.duration.." expositions" self:_MsgUnit(msg,15,playerName,true) else self:T("VALIDATE REFUSED — "..unit:GetName().." ammo="..tostring(refusedWeapon)) local msg=self:_Txt("TARS_VALID_REFUSED_WPN") if refusedWeapon then msg=msg.."\n"..self:_Txt("TARS_VALID_REFUSED_AMMO",refusedWeapon) end self:_MsgUnit(msg,10,playerName,true) end end function TARS:Control(instance) if not instance then return end if instance.sessionEnded then self:_MsgUnit(self:_Txt("TARS_SESSION_ENDED"),5,instance.playerName) return end if instance.capturing then self:_MsgUnit(self:_Txt("TARS_FILM_ALREADY_ACTIVE"),4,instance.playerName) return end instance:CaptureData() end function TARS:StopCapture(instance) if not instance then return end if not instance.capturing then self:_MsgUnit(self:_Txt("TARS_FILM_NO_CAPTURE_STOP"),4,instance.playerName) return end instance.capturing=false instance.standby=false instance.loop=false instance.sessionEnded=true instance.filmExhausted=(instance.duration<=0) instance:I("FILM STOP — captures="..instance.captureCount .." filmLeft="..instance.duration.."s") self:_MsgUnit( instance.filmExhausted and self:_Txt("TARS_FILM_EXHAUSTED") or self:_Txt("TARS_FILM_STOP"), 8,instance.playerName) end function TARS:StandbyCapture(instance) if not instance then return end if not instance.capturing then self:_MsgUnit(self:_Txt("TARS_FILM_NO_CAPTURE"),4,instance.playerName) return end if instance.standby and instance.wasCapturing then self:_MsgUnit(self:_Txt("TARS_FILM_STB_LOCKED"),4,instance.playerName) return end instance.standby=not instance.standby instance:I("FILM "..(instance.standby and"STB"or"RESUME")) self:_MsgUnit( instance.standby and self:_Txt("TARS_FILM_STB_MANUAL") or self:_Txt("TARS_FILM_RESUME_MANUAL"), 5,instance.playerName) end function TARS:ShowPlatformInfo(instance) if not instance or not instance.unit or not instance.unit:IsAlive()then return end local params=TARS.parameters[instance.type] if not params then return end local msg=self:_Txt("TARS_PLATFORM_INFO").."\n" ..self:_Txt("TARS_PLATFORM_LABEL").." : "..params.name.."\n" ..self:_Txt("TARS_PLATFORM_ALT").." : "..params.minAlt.."m - "..params.maxAlt.."m AGL\n" ..self:_Txt("TARS_PLATFORM_FOV").." : "..tostring(params.fov or"-").."\xc2\xb0\n" ..self:_Txt("TARS_PLATFORM_FILM").." : "..instance.duration.." / "..params.duration.." expositions" self:_MsgUnit(msg,15,instance.playerName,true) end function TARS:OutMark(snap,coa) if not snap then return end local c=COORDINATE:NewFromVec3(snap.point) local lat,lon=c:GetLLDDM() local hPa=UTILS.Round(c:GetPressure(),2) local inHg=UTILS.Round(hPa*0.02953,2) local text=string.format( "%.4f, %.4f | %.2f hPa / %.2f inHg\nTYPE: %s STATUS: %s", lat,lon,hPa,inHg,snap.type,TARS.life2text(snap.life)) local markTable=(coa==1)and self.marks.red or self.marks.blue local counter=(coa==1)and self.redMarkCount or self.blueMarkCount trigger.action.markToCoalition(counter,text,snap.point,coa,true) markTable[snap.name]=counter if coa==1 then self.redMarkCount=self.redMarkCount+1 else self.blueMarkCount=self.blueMarkCount+1 end local out=true if self.OnBeforeDataProcessing then out=self:OnBeforeDataProcessing(snap)end if out==true and self.OnAfterDataProcessing then self:OnAfterDataProcessing(snap)end return counter end function TARS:ProcessLanding(instance) if not instance or not instance.unit or not instance.unit:IsAlive()then return end local unit=instance.unit if unit:InAir(false)or not instance.sessionEnded then return end instance.wasCapturing=false instance.landingScheduled=false if not self:IsNearAlliedBase(unit)then self:_MsgUnit(self:_Txt("TARS_NOT_AT_BASE"),10,instance.playerName) return end local count=instance:ReturnReconTargets() instance:I("DEBRIEF — targets="..count.." player="..tostring(instance.playerName)) if TARS.mooseScoring and count>0 and TARS.scoring then local pts=count*TARS.valueScoring local mooseUnit=UNIT:FindByName(instance.objectName) if mooseUnit and mooseUnit:IsAlive()then TARS.scoring:_AddPlayerFromUnit(mooseUnit) TARS.scoring:AddGoalScore(mooseUnit, string.format("RECCE_%s_T%d",instance.objectName,math.floor(timer.getTime())), string.format("[TARS] %d target(s) captured +%d pts",count,pts),pts) self:_MsgUnit(self:_Txt("TARS_DEBRIEF_TARGETS",count,pts),8,instance.playerName,true) end else local pts=math.ceil(count/4) self:_AddUserPoints(instance.playerName,pts) self:_MsgUnit(self:_Txt("TARS_DEBRIEF_CREDITS",pts),8,instance.playerName) end self:_MsgCoalition( self:_Txt("TARS_DEBRIEF_COALITION",unit:GetPlayerName(),count), 8,instance.coa) instance:I("SESSION RESET") instance:SetObjectParams(unit) end function TARS:RemoveUnusedMarks(_,time) local function sweep(markTable) for unitName,markID in next,markTable do local u=UNIT:FindByName(unitName) if not u or not u:IsAlive()then trigger.action.removeMark(markID) markTable[unitName]=nil self.detectedTargets[unitName]=nil end end end sweep(self.marks.blue) sweep(self.marks.red) return time+120 end function TARS:IsNearAlliedBase(unit) if self.debug then return true end local pos=unit:GetCoordinate() local _,distance=pos:GetClosestAirbase(nil,unit:GetCoalition()) return distance0 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() math.random() math.random() math.random() 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() math.random() math.random() math.random() 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) math.random() math.random() math.random() 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:SetSpeed() 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 function SOUNDTEXT:SetSpeed(Speed) self.speed=Speed or 1.0 return self end function SOUNDTEXT:SetSpeaker(Speaker) self.speaker=Speaker 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) 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 init: %s",sender:GetName(),tostring(self.senderinit))) 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():ToLog() 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():ToLog() 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() self:T("_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.7" 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_IN_Standard_E"]='en-IN-Standard-E', ["en_IN_Standard_F"]='en-IN-Standard-F', ["en_GB_Standard_A"]='en-GB-Standard-A', ["en_GB_Standard_B"]='en-GB-Standard-B', ["en_GB_Standard_C"]='en-GB-Standard-C', ["en_GB_Standard_D"]='en-GB-Standard-D', ["en_GB_Standard_F"]='en-GB-Standard-F', ["en_GB_Standard_N"]='en-GB-Standard-N', ["en_GB_Standard_O"]='en-GB-Standard-O', ["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-A', ["de_DE_Standard_B"]='de-DE-Standard-B', ["de_DE_Standard_C"]='de-DE-Standard-C', ["de_DE_Standard_D"]='de-DE-Standard-D', ["de_DE_Standard_E"]='de-DE-Standard-E', ["de_DE_Standard_F"]='de-DE-Standard-F', ["de_DE_Standard_G"]='de-DE-Standard-G', ["de_DE_Standard_H"]='de-DE-Standard-H', ["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_IN_Wavenet_E"]='en-IN-Wavenet-E', ["en_IN_Wavenet_F"]='en-IN-Wavenet-F', ["en_GB_Wavenet_A"]='en-GB-Wavenet-A', ["en_GB_Wavenet_B"]='en-GB-Wavenet-B', ["en_GB_Wavenet_C"]='en-GB-Wavenet-C', ["en_GB_Wavenet_D"]='en-GB-Wavenet-D', ["en_GB_Wavenet_F"]='en-GB-Wavenet-F', ["en_GB_Wavenet_O"]='en-GB-Wavenet-O', ["en_GB_Wavenet_N"]='en-GB-Wavenet-N', ["en_US_Wavenet_A"]='en-US-Wavenet-A', ["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-A', ["de_DE_Wavenet_B"]='de-DE-Wavenet-B', ["de_DE_Wavenet_C"]='de-DE-Wavenet-C', ["de_DE_Wavenet_D"]='de-DE-Wavenet-D', ["de_DE_Wavenet_E"]='de-DE-Wavenet-E', ["de_DE_Wavenet_F"]='de-DE-Wavenet-F', ["de_DE_Wavenet_G"]='de-DE-Wavenet-G', ["de_DE_Wavenet_H"]='de-DE-Wavenet-H', ["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", }, Chirp3HD={ ["en_GB_Chirp3_HD_Aoede"]='en-GB-Chirp3-HD-Aoede', ["en_GB_Chirp3_HD_Charon"]='en-GB-Chirp3-HD-Charon', ["en_GB_Chirp3_HD_Fenrir"]='en-GB-Chirp3-HD-Fenrir', ["en_GB_Chirp3_HD_Kore"]='en-GB-Chirp3-HD-Kore', ["en_GB_Chirp3_HD_Leda"]='en-GB-Chirp3-HD-Leda', ["en_GB_Chirp3_HD_Orus"]='en-GB-Chirp3-HD-Orus', ["en_GB_Chirp3_HD_Puck"]='en-GB-Chirp3-HD-Puck', ["en_GB_Chirp3_HD_Zephyr"]='en-GB-Chirp3-HD-Zephyr', ["en_US_Chirp3_HD_Charon"]='en-US-Chirp3-HD-Charon', ["en_US_Chirp3_HD_Fenrir"]='en-US-Chirp3-HD-Fenrir', ["en_US_Chirp3_HD_Kore"]='en-US-Chirp3-HD-Kore', ["en_US_Chirp3_HD_Leda"]='en-US-Chirp3-HD-Leda', ["en_US_Chirp3_HD_Orus"]='en-US-Chirp3-HD-Orus', ["en_US_Chirp3_HD_Puck"]='en-US-Chirp3-HD-Puck', ["de_DE_Chirp3_HD_Aoede"]='de-DE-Chirp3-HD-Aoede', ["de_DE_Chirp3_HD_Charon"]='de-DE-Chirp3-HD-Charon', ["de_DE_Chirp3_HD_Fenrir"]='de-DE-Chirp3-HD-Fenrir', ["de_DE_Chirp3_HD_Kore"]='de-DE-Chirp3-HD-Kore', ["de_DE_Chirp3_HD_Leda"]='de-DE-Chirp3-HD-Leda', ["de_DE_Chirp3_HD_Orus"]='de-DE-Chirp3-HD-Orus', ["de_DE_Chirp3_HD_Puck"]='de-DE-Chirp3-HD-Puck', ["de_DE_Chirp3_HD_Zephyr"]='de-DE-Chirp3-HD-Zephyr', ["en_AU_Chirp3_HD_Aoede"]='en-AU-Chirp3-HD-Aoede', ["en_AU_Chirp3_HD_Charon"]='en-AU-Chirp3-HD-Charon', ["en_AU_Chirp3_HD_Fenrir"]='en-AU-Chirp3-HD-Fenrir', ["en_AU_Chirp3_HD_Kore"]='en-AU-Chirp3-HD-Kore', ["en_AU_Chirp3_HD_Leda"]='en-AU-Chirp3-HD-Leda', ["en_AU_Chirp3_HD_Orus"]='en-AU-Chirp3-HD-Orus', ["en_AU_Chirp3_HD_Puck"]='en-AU-Chirp3-HD-Puck', ["en_AU_Chirp3_HD_Zephyr"]='en-AU-Chirp3-HD-Zephyr', ["en_IN_Chirp3_HD_Aoede"]='en-IN-Chirp3-HD-Aoede', ["en_IN_Chirp3_HD_Charon"]='en-IN-Chirp3-HD-Charon', ["en_IN_Chirp3_HD_Fenrir"]='en-IN-Chirp3-HD-Fenrir', ["en_IN_Chirp3_HD_Kore"]='en-IN-Chirp3-HD-Kore', ["en_IN_Chirp3_HD_Leda"]='en-IN-Chirp3-HD-Leda', ["en_IN_Chirp3_HD_Orus"]='en-IN-Chirp3-HD-Orus', }, ChirpHD={ ["en_US_Chirp_HD_D"]='en-US-Chirp-HD-D', ["en_US_Chirp_HD_F"]='en-US-Chirp-HD-F', ["en_US_Chirp_HD_O"]='en-US-Chirp-HD-O', ["de_DE_Chirp_HD_D"]='de-DE-Chirp-HD-D', ["de_DE_Chirp_HD_F"]='de-DE-Chirp-HD-F', ["de_DE_Chirp_HD_O"]='de-DE-Chirp-HD-O', ["en_AU_Chirp_HD_D"]='en-AU-Chirp-HD-D', ["en_AU_Chirp_HD_F"]='en-AU-Chirp-HD-F', ["en_AU_Chirp_HD_O"]='en-AU-Chirp-HD-O', ["en_IN_Chirp_HD_D"]='en-IN-Chirp-HD-D', ["en_IN_Chirp_HD_F"]='en-IN-Chirp-HD-F', ["en_IN_Chirp_HD_O"]='en-IN-Chirp-HD-O', }, }, Neural2={ ["en_GB_Neural2_A"]='en-GB-Neural2-A', ["en_GB_Neural2_B"]='en-GB-Neural2-B', ["en_GB_Neural2_C"]='en-GB-Neural2-C', ["en_GB_Neural2_D"]='en-GB-Neural2-D', ["en_GB_Neural2_F"]='en-GB-Neural2-F', ["en_GB_Neural2_N"]='en-GB-Neural2-N', ["en_GB_Neural2_O"]='en-GB-Neural2-O', ["en_US_Neural2_A"]='en-US-Neural2-A', ["en_US_Neural2_C"]='en-US-Neural2-C', ["en_US_Neural2_D"]='en-US-Neural2-D', ["en_US_Neural2_E"]='en-US-Neural2-E', ["en_US_Neural2_F"]='en-US-Neural2-F', ["en_US_Neural2_G"]='en-US-Neural2-G', ["en_US_Neural2_H"]='en-US-Neural2-H', ["en_US_Neural2_I"]='en-US-Neural2-I', ["en_US_Neural2_J"]='en-US-Neural2-J', ["de_DE_Neural2_G"]='de-DE-Neural2-G', ["de_DE_Neural2_H"]='de-DE-Neural2-H', ["en_AU_Neural2_A"]='en-AU-Neural2-A', ["en_AU_Neural2_B"]='en-AU-Neural2-B', ["en_AU_Neural2_C"]='en-AU-Neural2-C', ["en_AU_Neural2_D"]='en-AU-Neural2-D', ["en_IN_Neural2_A"]='en-IN-Neural2-A', ["en_IN_Neural2_B"]='en-IN-Neural2-B', ["en_IN_Neural2_C"]='en-IN-Neural2-C', ["en_IN_Neural2_D"]='en-IN-Neural2-D', }, News={ ["en_GB_News_G"]='en-GB-News-G', ["en_GB_News_H"]='en-GB-News-H', ["en_GB_News_I"]='en-GB-News-I', ["en_GB_News_J"]='en-GB-News-J', ["en_GB_News_K"]='en-GB-News-K', ["en_GB_News_L"]='en-GB-News-L', ["en_GB_News_M"]='en-GB-News-M', ["en_US_News_K"]='en-US-News-K', ["en_US_News_L"]='en-US-News-L', ["en_US_News_N"]='en-US-News-N', ["en_AU_News_E"]='en-AU-News-E', ["en_AU_News_F"]='en-AU-News-F', ["en_AU_News_G"]='en-AU-News-G', }, Casual={ ["en_US_Casual_K"]='en-US-Casual-K', }, Polyglot={ ["en_US_Polyglot_1"]='en-US-Polyglot-1', ["de_DE_Polyglot_1"]='de-DE-Polyglot-1', ["en_AU_Polyglot_1"]='en-AU-Polyglot-1', }, Studio={ ["en_GB_Studio_B"]='en-GB-Studio-B', ["en_GB_Studio_C"]='en-GB-Studio-C', ["en_US_Studio_O"]='en-US-Studio-O', ["en_US_Studio_Q"]='en-US-Studio-Q', ["de_DE_Studio_B"]='de-DE-Studio-B', ["de_DE_Studio_C"]='de-DE-Studio-C', }, } MSRS.Backend={ SRSEXE="srsexe", GRPC="grpc", HOUND="hound", } MSRS.Provider={ WINDOWS="win", GOOGLE="gcloud", AZURE="azure", AMAZON="aws", PIPER="piper", KITTEN="openai", OPENAI="openai", } 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:SetBackendHound() self:F() self:SetBackend(MSRS.Backend.HOUND) 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.SetDefaultBackendHound() MSRS.backend=MSRS.Backend.HOUND 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\\ExternalAudio" 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:SetVoicePiper(Voice) self:F({Voice=Voice}) self:SetVoiceProvider(Voice or"en_US-ryan-low",MSRS.Provider.PIPER) return self end function MSRS:SetSpeakerPiper(Speaker) self:F({Speaker=Speaker}) self.Speaker=Speaker 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:SetTTSProviderPiper() self:F() self:SetProvider(MSRS.Provider.PIPER) return self end function MSRS:SetTTSProviderKitten() self:F() self:SetProvider(MSRS.Provider.KITTEN) return self end function MSRS:SetTTSProviderOpenAI() self:F() self:SetProvider(MSRS.Provider.OPENAI) 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:SetAutoTranslate(Provider,Language) self:T(self.lid.."SetAutoTranslate") self.SRSTranslate=true self.SRSTranslateProvider=Provider or MSRS.Provider.GOOGLE self.SRSTranslateLanguage=Language or"de" 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) elseif self.backend==MSRS.Backend.HOUND then self:_HoundTextToSpeech(SoundText.text,nil,nil,SoundText.volume,SoundText.label,self.coalition,SoundText.coordinate,SoundText.Speed,SoundText.gender,SoundText.culture,SoundText.voice,nil,SoundText.speaker) 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,Speed,Speaker) self:F({Text,Delay,Coordinate}) if Delay and Delay>0 then self:ScheduleOnce(Delay,MSRS.PlayText,self,Text,nil,Coordinate,Speed,Speaker) else local speaker=Speaker or self.Speaker if self.backend==MSRS.Backend.GRPC then self:T(self.lid.."Transmitting") self:_DCSgRPCtts(Text,nil,nil,nil,nil,nil,nil,Coordinate) elseif self.backend==MSRS.Backend.HOUND then self:_HoundTextToSpeech(Text,nil,nil,nil,nil,nil,Coordinate,Speed,nil,speaker) else self:PlayTextExt(Text,Delay,nil,nil,nil,nil,nil,nil,nil,Coordinate,Speed,speaker) end end return self end function MSRS:PlayTextExt(Text,Delay,Frequencies,Modulations,Gender,Culture,Voice,Volume,Label,Coordinate,Speed,Speaker) self:T({Text,Delay,Frequencies,Modulations,Gender,Culture,Voice,Volume,Label,Coordinate,Speed,Speaker}) if Delay and Delay>0 then self:ScheduleOnce(Delay,self.PlayTextExt,self,Text,0,Frequencies,Modulations,Gender,Culture,Voice,Volume,Label,Coordinate,Speed,Speaker) 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) elseif self.backend==MSRS.Backend.HOUND then local speaker=Speaker or self.Speaker local UseGoogle=(self.provider==MSRS.Provider.GOOGLE)and true or nil self:_HoundTextToSpeech(Text,Frequencies,Modulations,Volume,Label,self.coalition,Coordinate,Speed,Gender,Culture,Voice,UseGoogle,speaker) 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:_HoundTextToSpeech(Message,Frequencies,Modulations,Volume,Label,Coalition,Point,Speed,Gender,Culture,Voice,UseGoogle,Speaker,Translated) self:T(self.lid.."_HoundTextToSpeech") if self.SRSTranslate==true and Translated~=true then MSRS._HoundTranslate(Message,{provider=self.SRSTranslateProvider,language=self.SRSTranslateLanguage}, function(translated,err) if translated then return MSRS._HoundTextToSpeech(self,translated,Frequencies,Modulations,Volume,Label,Coalition,Point,Speed,Gender,Culture,Voice,UseGoogle,Speaker,true) else env.error("Translation failed: "..tostring(err)) end end ) return end Frequencies=UTILS.EnsureTable(Frequencies or self.frequencies) Modulations=UTILS.EnsureTable(Modulations or self.modulations) local ffs={} for _,_f in pairs(Frequencies)do table.insert(ffs,string.format("%.1f",_f)) end local freqs=table.concat(ffs,",") local modus=table.concat(Modulations,",") local coal=Coalition or self.coalition local gender=Gender or self.gender local voice=Voice or self:GetVoice(self.provider)or self.voice local culture=Culture or self.culture local volume=Volume or self.volume or 1.0 local speed=Speed or self.speed or 1.0 local label=Label or self.Label or"MSRS" local coordinate=Point or self.coordinate local point=(coordinate~=nil)and coordinate:GetVec3()or nil local port=self.port or 5002 modus=modus:gsub("0","AM") modus=modus:gsub("1","FM") self:T({T=Message,F=freqs,M=modus,V=voice,Vx=volume,L=label,C=coal,GGL=tostring(UseGoogle)}) local provider=self.provider local TransmissionP={ freqs=freqs, modulations=modus, coalition=coal, name=label, point=point, volume=volume, port=port, } local ProviderP={ provider=provider, voice=voice, speed=speed, culture=culture, gender=gender, speaker=Speaker or self.Speaker, } local speechtime=HoundTTS.Transmit(Message,TransmissionP,ProviderP) return speechtime end function MSRS:_HoundTransmit(Message,Transmission_params,Provider_params) self:T(self.lid.."_HoundTransmit") self:T({Message,Transmission_params,Provider_params}) local speechtime=HoundTTS.Transmit(Message,Transmission_params,Provider_params) return speechtime end function MSRS:_HoundTestTone(Frequencies,Modulations,Coalition) self:T(self.lid.."_HoundTestTone") Frequencies=UTILS.EnsureTable(Frequencies) Modulations=UTILS.EnsureTable(Modulations) local ffs={} for _,_f in pairs(Frequencies or self.frequencies)do table.insert(ffs,string.format("%.1f",_f)) end local freqs=table.concat(ffs,",") local modus=table.concat(Modulations or self.modulations,",") modus=modus:gsub("0","AM") modus=modus:gsub("1","FM") local coal=Coalition or self.coalition HoundTTS.TestTone(freqs,modus,coal) return self end function MSRS:_HoundSpeechTime(Message,Speed,UseGoogle) self:T(self.lid.."_HoundSpeechTime") local speed=Speed or 1.0 local speechtime=HoundTTS.getSpeechTime(Message,speed,UseGoogle) return speechtime end function MSRS._HoundTranslate(Message,Parameters,CallbackFunction) local text=Message local parameters=Parameters or{} local callback=CallbackFunction if not callback then env.error("_HoundTranslate - not callback function provided!",true) return end if not parameters.provider then parameters.provider=MSRS.Provider.GOOGLE end parameters.provider=string.gsub(parameters.provider,"gcloud","google") if not parameters.language then parameters.language="de"end HoundTTS.Translate(text,parameters,callback) return end function MSRS:RadioJammerOn(Frequencies,Modulations,Coalition,Noisetype,Volume,Seconds,Label,Vec3,Encrypt,EncKey) self:T(self.lid.."RadioJammerOn") Frequencies=UTILS.EnsureTable(Frequencies) Modulations=UTILS.EnsureTable(Modulations) local ffs={} for _,_f in pairs(Frequencies or self.frequencies)do table.insert(ffs,string.format("%.1f",_f)) end local freqs=table.concat(ffs,",") local modus=table.concat(Modulations or self.modulations,",") modus=modus:gsub("0","AM") modus=modus:gsub("1","FM") local coal=Coalition or self.coalition or coalition.side.RED local secs=Seconds or 30 local TransmissionP={} local ProviderP={} TransmissionP.transmitter="srs" TransmissionP.freqs=freqs TransmissionP.modulations=modus TransmissionP.coalition=coal or self.coalition TransmissionP.name=Label or self.Label TransmissionP.point=Vec3 TransmissionP.encrypt=Encrypt TransmissionP.encKey=EncKey ProviderP.noiseType=Noisetype or"white" ProviderP.volume=Volume or 1 local ID=HoundTTS.TransmitNoise(TransmissionP,ProviderP) self.NoiseID=ID self:ScheduleOnce(secs,MSRS.RadioJammerOff,self,ID) return ID end function MSRS:RadioJammerOff(ID) return HoundTTS.KillSession(ID or self.NoiseID) 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\\ExternalAudio" 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) if MSRS.backend==MSRS.Backend.HOUND then local speechtime=HoundTTS.getSpeechTime(length,speed,isGoogle) return speechtime else 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 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,speed,speaker,priority) 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,S=speed,P=priority}) self:T({TEXT=text,PRIO=tostring(priority)}) 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 transmission.speed=speed or 1.0 if speaker then transmission.speaker=speaker elseif msrs.Speaker then transmission.speaker=msrs.speaker end transmission.priority=priority or 50 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,transmission.speed,transmission.speaker) else transmission.msrs:PlayText(transmission.text,nil,transmission.coordinate,transmission.speed,transmission.speaker) end local function texttogroup(gid) trigger.action.outTextForGroup(gid,transmission.subtitle,transmission.subduration,true) end if transmission.subgroups and#transmission.subgroups>0 and transmission.subtitle 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 return end 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 nextTx=nil local remove=nil local function getPriority(tx) local p=tx.priority if p==nil then return 50 end if p<1 then return 1 end if p>100 then return 100 end return p end local bestPrio=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 local eligible=false if transmission.interval==nil then eligible=true else if(Tlast==nil)or(time-Tlast>=transmission.interval)then eligible=true end end if eligible and not playing then local prio=getPriority(transmission) if bestPrio==nil or prio>bestPrio then bestPrio=prio nextTx=transmission end end end else end end if nextTx~=nil and not playing then self:T(self.lid..string.format('Broadcasting text="%s" at T=%.3f (prio=%d)',nextTx.text,time,(nextTx.priority or 50))) self:Broadcast(nextTx) nextTx.isplaying=true nextTx.Tstarted=time dt=nextTx.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 MSRS.LoadConfigFile() NAVFIX={ ClassName="NAVFIX", verbose=0, } NAVFIX.Type={ POINT="Point", INTERSECTION="Intersection", AIRPORT="Airport", NDB="NDB", VOR="VOR", DME="DME", VORDME="VOR/DME", LOC="Localizer", ILS="ILS", TACAN="TACAN" } NAVFIX.version="0.1.0" function NAVFIX:NewFromVector(Name,Type,Vector) self=BASE:Inherit(self,BASE:New()) self.vector=Vector self.name=Name self.typePoint=Type or NAVFIX.Type.POINT local coord=COORDINATE:NewFromVec3(self.vector) self.marker=MARKER:New(coord,self:_GetMarkerText()) self.lid=string.format("NAVFIX %s [%s] | ",tostring(self.name),tostring(self.typePoint)) self:I(self.lid..string.format("Created NAVFIX")) return self end function NAVFIX:NewFromCoordinate(Name,Type,Coordinate) local Vector=VECTOR:NewFromVec(Coordinate) self=NAVFIX:NewFromVector(Name,Type,Vector) return self end function NAVFIX:NewFromLLDMS(Name,Type,Latitude,Longitude) local Vector=VECTOR:NewFromLLDMS(Latitude,Longitude) self=NAVFIX:NewFromVector(Name,Type,Vector) return self end function NAVFIX:NewFromLLDD(Name,Type,Latitude,Longitude) local Vector=VECTOR:NewFromLLDD(Latitude,Longitude) self=NAVFIX:NewFromVector(Name,Type,Vector) return self end function NAVFIX:NewFromNavFix(Name,Type,NavFix,Distance,Bearing,Reciprocal) Bearing=Bearing+UTILS.GetMagneticDeclination() if Reciprocal then Bearing=Bearing-180 end local Vector=NavFix.vector:Translate(UTILS.NMToMeters(Distance),Bearing,true) self=NAVFIX:NewFromVector(Name,Type,Vector) return self end function NAVFIX:NewFromBeacon(Beacon) local frequency,unit=BEACONS:_GetFrequency(Beacon.frequency) frequency=string.format("%.3f",frequency) if Beacon.typeName=="TACAN"then frequency=Beacon.channel unit="X" end self=NAVFIX:NewFromVector(string.format("%s %s %s",Beacon.typeName,frequency,unit),Beacon.typeName,Beacon.vec3) return self end function NAVFIX:SetIntermediateFix(IntermediateFix) self.isIF=IntermediateFix return self end function NAVFIX:SetInitialApproachFix(IntermediateFix) self.isIAF=IntermediateFix return self end function NAVFIX:SetFinalApproachFix(FinalApproachFix) self.isFAF=FinalApproachFix return self end function NAVFIX:SetMissedApproachFix(MissedApproachFix) self.isMAF=MissedApproachFix return self end function NAVFIX:SetAltMin(Altitude) self.altMin=Altitude return self end function NAVFIX:SetAltMax(Altitude) self.altMax=Altitude return self end function NAVFIX:SetAltMandatory(Altitude) self.altMin=Altitude self.altMax=Altitude return self end function NAVFIX:SetSpeedMin(Speed) self.speedMin=Speed return self end function NAVFIX:SetSpeedMax(Speed) self.speedMax=Speed return self end function NAVFIX:SetSpeedMandatory(Speed) self.speedMin=Speed self.speedMax=Speed return self end function NAVFIX:SetCompulsory(Compulsory) self.isCompulsory=Compulsory return self end function NAVFIX:SetFlyOver(FlyOver) self.isFlyover=FlyOver return self end function NAVFIX:GetAltitude() local alt=nil if self.altMin and self.altMax and self.altMin~=self.altMax then alt=math.random(self.altMin,self.altMax) elseif self.altMin then alt=self.altMin elseif self.altMax then alt=self.altMax end return alt end function NAVFIX:GetSpeed() local speed=nil if self.speedMin and self.speedMax and self.speedMin~=self.speedMax then speed=math.random(self.speedMin,self.speedMax) elseif self.speedMin then speed=self.speedMin elseif self.speedMax then speed=self.speedMax end return speed end function NAVFIX:MarkerShow() self.marker:ToAll() return self end function NAVFIX:MarkerRemove() self.marker:Remove() return self end function NAVFIX:_GetMarkerText() local altmin=self.altMin and tostring(self.altMin)or"" local altmax=self.altMax and tostring(self.altMax)or"" local speedmin=self.speedMin and tostring(self.speedMin)or"" local speedmax=self.speedMax and tostring(self.speedMax)or"" local text=string.format("NAVFIX %s",self.name) if self.isIAF then text=text..string.format(" (IAF)") end if self.isIF then text=text..string.format(" (IF)") end text=text..string.format("\nAltitude [ft]: %s - %s",altmin,altmax) text=text..string.format("\nSpeed [knots]: %s - %s",speedmin,speedmax) text=text..string.format("\nCompulsory: %s",tostring(self.isCompulsory)) text=text..string.format("\nFly Over: %s",tostring(self.isFlyover)) return text end NAVAID={ ClassName="NAVAID", verbose=0, } NAVAID.version="0.1.0" function NAVAID:NewFromScenery(Name,Type,ZoneName,SceneryName) local zone=ZONE:FindByName(ZoneName) local Coordinate=zone:GetCoordinate() self=BASE:Inherit(self,NAVFIX:NewFromCoordinate(Name,Type,Coordinate)) self.zone=ZONE:FindByName(ZoneName) if SceneryName then self.scenery=SCENERY:FindByNameInZone(SceneryName,ZoneName) if not self.scenery then self:E(string.format("ERROR: Could not find scenery object %s in zone %s",SceneryName,ZoneName)) end end self.alias=string.format("%s %s %s",tostring(ZoneName),tostring(SceneryName),tostring(Type)) self.lid=string.format("NAVAID %s | ",self.alias) self:I(self.lid..string.format("Created NAVAID!")) return self end function NAVAID:SetFrequency(Frequency) self.frequency=Frequency return self end function NAVAID:SetChannel(Channel,Band) self.channel=Channel self.band=Band or"X" return self end BEACONS={ ClassName="BEACONS", verbose=1, beacons={}, } BEACONS.version="0.1.0" function BEACONS:NewFromTable(BeaconTable) self=BASE:Inherit(self,BASE:New()) for _,_beacon in pairs(BeaconTable)do local beacon=_beacon beacon.vec3={x=beacon.position[1],y=beacon.position[2],z=beacon.position[3]} beacon.coordinate=COORDINATE:NewFromVec3(beacon.vec3) beacon.typeName=self:_GetTypeName(beacon.type) beacon.scenery=beacon.coordinate:FindClosestScenery(20) if false then if beacon.scenery then env.info(string.format("FF Beacon %s %s %s got scenery object %s, %s",beacon.callsign,beacon.beaconId,beacon.typeName,beacon.scenery:GetName(),beacon.scenery:GetTypeName())) UTILS.PrintTableToLog(beacon.scenery.SceneryObject) UTILS.PrintTableToLog(beacon.sceneObjects) else env.info(string.format("FF NO scenery object %s %s %s ",beacon.callsign,beacon.beaconId,beacon.typeName)) end end table.insert(self.beacons,beacon) end self:I(string.format("Added %d beacons",#self.beacons)) if self.verbose>0 then local text="Beacon types:" for typeName,typeID in pairs(BEACON.Type)do local n=self:CountBeacons(typeID) text=text..string.format("\n%s = %d",typeName,n) end self:I(text) end return self end function BEACONS:NewFromFile(FileName) self=BASE:Inherit(self,BASE:New()) local exists=UTILS.FileExists(FileName) if exists==false then self:E(string.format("ERROR: file with beacon info does not exist!")) return nil end dofile(FileName) self=self:NewFromTable(beacons) return self end function BEACONS:GetVec3(beacon) return beacon.vec3 end function BEACONS:GetCoordinate(beacon) local coordinate=COORDINATE:NewFromVec3(beacon.vec3) return coordinate end function BEACONS:GetClosestBeacon(Coordinate,TypeID,DistMax,ExcludeList) local beacon=nil local distmin=math.huge ExcludeList=ExcludeList or{} for _,_beacon in pairs(self.beacons)do local bc=_beacon if(TypeID==nil or TypeID==bc.type)and(not UTILS.IsInTable(ExcludeList,bc,"beaconId"))then local dist=Coordinate:Get2DDistance(bc.vec3) if dist=1e6 then freq=freq/1e6 unit="MHz" elseif freq>=1e3 then freq=freq/1e3 unit="kHz" end return freq,unit end function BEACONS:_GetTypeName(typeID) if typeID~=nil then for typeName,_typeID in pairs(BEACON.Type)do if _typeID==typeID then return typeName end end end return"Unknown" end RADIOS={ ClassName="RADIOS", verbose=0, radios={}, } RADIOS.version="0.1.0" function RADIOS:NewFromTable(RadioTable) self=BASE:Inherit(self,BASE:New()) local airdromes=AIRBASE.GetAllAirbases(nil,Airbase.Category.AIRDROME) for _,_radio in pairs(RadioTable)do local radio=_radio if false then local cs=radio.callsign[1] if cs and cs.common then radio.name=cs.common[1] elseif cs and cs.nato then radio.name=cs.nato[1] else radio.name="Unknown" end radio.name=self:_GetAirbaseName(airbasenames,radio.name) radio.airbase=AIRBASE:FindByName(radio.name) end local aid=tonumber(string.match(radio.radioId,"airfield(%d+)_")) radio.airbase=self:_GetAirbaseByID(airdromes,aid) if radio.airbase then radio.coordinate=radio.airbase:GetCoordinate() radio.vec3=radio.airbase:GetVec3() radio.name=radio.airbase:GetName() end table.insert(self.radios,radio) end self:I(string.format("Added %d radios",#self.radios)) return self end function RADIOS:NewFromFile(FileName) self=BASE:Inherit(self,BASE:New()) local exists=UTILS.FileExists(FileName) if exists==false then self:E(string.format("ERROR: file with radios info does not exist! File=%s",tostring(FileName))) return nil end local radiobak=UTILS.DeepCopy(radio) dofile(FileName) self=self:NewFromTable(radio) radio=UTILS.DeepCopy(radiobak) return self end function RADIOS:GetVec3(radio) return radio.vec3 end function RADIOS:GetCoordinate(radio) return radio.coordinate end function RADIOS:GetClosestRadio(Coordinate,DistMax,ExcludeList) local radio=nil local distmin=math.huge ExcludeList=ExcludeList or{} for _,_radio in pairs(self.radios)do local ra=_radio if(not UTILS.IsInTable(ExcludeList,ra,"radioId"))then local dist=Coordinate:Get2DDistance(ra.coordinate) if dist=1e6 then freq=freq/1e6 unit="MHz" elseif freq>=1e3 then freq=freq/1e3 unit="kHz" end return freq,unit end function RADIOS:_GetBandName(BandNumber) if BandNumber~=nil then for bandName,bandNumber in pairs(ENUMS.FrequencyBand)do if bandNumber==BandNumber then return bandName end end end return"Unknown" end function RADIOS:_GetAirbaseName(airbasenames,name) local airbase=AIRBASE:FindByName(name) if airbase then return name else for _,airbasename in pairs(airbasenames)do if string.find(airbasename,name)then return airbasename end end end return"Unknown" end function RADIOS:_GetAirbaseByID(airbases,aid) for _,_airbase in pairs(airbases)do local airbase=_airbase local id=airbase:GetID(true) if id==aid then return airbase end end return nil end TOWNS={ ClassName="TOWNS", verbose=0, towns={}, } TOWNS.version="0.1.0" function TOWNS:NewFromTable(TownTable) self=BASE:Inherit(self,BASE:New()) for TownName,_town in pairs(TownTable)do local town=_town town.name=TownName town.coordinate=COORDINATE:NewFromLLDD(town.latitude,town.longitude) town.coordRoad=town.coordinate:GetClosestPointToRoad() town.coordRail=town.coordinate:GetClosestPointToRoad(true) table.insert(self.towns,town) end self:I(string.format("Added %d towns",#self.towns)) return self end function TOWNS:NewFromFile(FileName) self=BASE:Inherit(self,BASE:New()) local exists=UTILS.FileExists(FileName) if exists==false then self:E(string.format("ERROR: file with towns info does not exist!")) return nil end dofile(FileName) self=self:NewFromTable(towns) return self end function TOWNS:GetVec3(town) return town.vec3 end function TOWNS:GetCoordinate(town) return town.coordinate end function TOWNS:GetCoordRoad(town) return town.coordRoad end function TOWNS:GetCoordRail(town) return town.coordRail end function TOWNS:GetConnectionRoad(townA,townB,Railroad) local path=townA.coordRoad:GetPathlineOnRoad(townB.coordRoad,false,Railroad) return path end function TOWNS:GetClosestTown(Coordinate,DistMax,ExcludeList) local Town=nil local distmin=math.huge ExcludeList=ExcludeList or{} for _,_town in pairs(self.towns)do local town=_town if(not UTILS.IsInTable(ExcludeList,town,"name"))then local dist=Coordinate:Get2DDistance(town.coordinate) if dist/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 *** ')