diff --git a/Moose Development/Moose/Functional/Autolase.lua b/Moose Development/Moose/Functional/Autolase.lua new file mode 100644 index 000000000..a1085f1b6 --- /dev/null +++ b/Moose Development/Moose/Functional/Autolase.lua @@ -0,0 +1,926 @@ +--- **Functional** - Autolase targets in the field. +-- +-- === +-- +-- **AUOTLASE** - Autolase targets in the field. +-- +-- === +-- +-- ## Missions: +-- +-- ### [Autolase](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/) +-- +-- === +-- +-- **Main Features:** +-- +-- * Detect and lase contacts automatically +-- * Targets are lased by threat priority order +-- * Use FSM events to link functionality into your scripts +-- * Easy setup +-- +-- === +-- +--- Spot on! +-- +-- === +-- +-- # 1 Autolase concept +-- +-- * Detect and lase contacts automatically +-- * Targets are lased by threat priority order +-- * Use FSM events to link functionality into your scripts +-- * Easy set-up +-- +-- # 2 Basic usage +-- +-- ## 2.2 Set up a group of Recce Units: +-- +-- local FoxSet = SET_GROUP:New():FilterPrefixes("Recce"):FilterCoalitions("blue"):FilterStart() +-- +-- ## 2.3 (Optional) Set up a group of pilots, this will drive who sees the F10 menu entry: +-- +-- local Pilotset = SET_CLIENT:New():FilterCoalitions("blue"):FilterActive(true):FilterStart() +-- +-- ## 2.4 Set up and start Autolase: +-- +-- local autolaser = AUTOLASE:New(FoxSet,coalition.side.BLUE,"Wolfpack",Pilotset) +-- +-- ## 2.5 Example - Using a fixed laser code for a specific Recce unit: +-- +-- local recce = SPAWN:New("Reaper") +-- :InitDelayOff() +-- :OnSpawnGroup( +-- function (group) +-- local unit = group:GetUnit(1) +-- local name = unit:GetName() +-- autolaser:SetRecceLaserCode(name,1688) +-- end +-- ) +-- :InitCleanUp(60) +-- :InitLimit(1,0) +-- :SpawnScheduled(30,0.5) +-- +-- ## 2.6 Example - Inform pilots about events: +-- +-- autolaser:SetNotifyPilots(true) -- defaults to true, also shown if debug == true +-- -- Note - message are shown to pilots in the #SET_CLIENT only if using the pilotset option, else to the coalition. +-- +-- +-- ### Author: **applevangelist** +-- @module Functional.Autolase +-- @image Designation.JPG +-- +-- Date: Oct 2021 +-- +--- Class AUTOLASE +-- @type AUTOLASE +-- @field #string ClassName +-- @field #string lid +-- @field #number verbose +-- @field #string alias +-- @field #boolean debug +-- @field #string version +-- @extends Ops.Intel#INTEL + +--- +-- @field #AUTOLASE +AUTOLASE = { + ClassName = "AUTOLASE", + lid = "", + verbose = 0, + alias = "", + debug = false, +} + +--- Laser spot info +-- @type AUTOLASE.LaserSpot +-- @field Core.Spot#SPOT laserspot +-- @field Wrapper.Unit#UNIT lasedunit +-- @field Wrapper.Unit#UNIT lasingunit +-- @field #number lasercode +-- @field #string location +-- @field #number timestamp +-- @field #string unitname +-- @field #string reccename +-- @field #string unittype + +--- AUTOLASE class version. +-- @field #string version +AUTOLASE.version = "0.0.8" + +------------------------------------------------------------------- +-- Begin Functional.Autolase.lua +------------------------------------------------------------------- + +--- Constructor for a new Autolase instance. +-- @param #AUTOLASE self +-- @param Core.Set#SET_GROUP RecceSet Set of detecting and lasing units +-- @param #number Coalition Coalition side. Can also be passed as a string "red", "blue" or "neutral". +-- @param #string Alias (Optional) An alias how this object is called in the logs etc. +-- @param Core.Set#SET_CLIENT PilotSet (Optional) Set of clients for precision bombing, steering menu creation. Leave nil for a coalition-wide F10 entry and display. +-- @return #AUTOLASE self +function AUTOLASE:New(RecceSet, Coalition, Alias, PilotSet) + BASE:T({RecceSet, Coalition, Alias, PilotSet}) + + -- Inherit everything from BASE class. + local self=BASE:Inherit(self, BASE:New()) -- #AUTOLASE + + 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 + + -- Set alias. + 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 + + -- inherit from INTEL + local self=BASE:Inherit(self, INTEL:New(RecceSet, Coalition, Alias)) -- #AUTOLASE + + 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.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.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 + + -- Set some string id for output to DCS.log file. + self.lid=string.format("AUTOLASE %s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("*", "Monitor", "*") -- Start FSM + self:AddTransition("*", "Lasing", "*") -- Lasing target + self:AddTransition("*", "TargetLost", "*") -- Lost target + self:AddTransition("*", "TargetDestroyed", "*") -- Target destroyed + self:AddTransition("*", "RecceKIA", "*") -- Recce KIA + self:AddTransition("*", "LaserTimeout", "*") -- Laser timed out + self:AddTransition("*", "Cancel", "*") -- Stop Autolase + + -- Menu Entry + if not PilotSet then + self.Menu = MENU_COALITION_COMMAND:New(self.coalition,"Autolase",nil,self.ShowStatus,self) + else + self.usepilotset = true + self.pilotset = PilotSet + self:HandleEvent(EVENTS.PlayerEnterAircraft) + self:SetPilotMenu() + end + + self:SetClusterAnalysis(false, false) + + self:__Start(2) + self:__Monitor(math.random(5,10)) + + return self + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Monitor". + -- @function [parent=#AUTOLASE] Status + -- @param #AUTOLASE self + + --- Triggers the FSM event "Monitor" after a delay. + -- @function [parent=#AUTOLASE] __Status + -- @param #AUTOLASE self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Cancel". + -- @function [parent=#AUTOLASE] Cancel + -- @param #AUTOLASE self + + --- Triggers the FSM event "Cancel" after a delay. + -- @function [parent=#AUTOLASE] __Cancel + -- @param #AUTOLASE self + -- @param #number delay Delay in seconds. + + --- On After "RecceKIA" event. + -- @function [parent=#AUTOLASE] OnAfterRecceKIA + -- @param #AUTOLASE self + -- @param #string From The from state + -- @param #string Event The event + -- @param #string To The to state + -- @param #string RecceName The lost Recce + + --- On After "TargetDestroyed" event. + -- @function [parent=#AUTOLASE] OnAfterTargetDestroyed + -- @param #AUTOLASE self + -- @param #string From The from state + -- @param #string Event The event + -- @param #string To The to state + -- @param #string UnitName The destroyed unit\'s name + -- @param #string RecceName The Recce name lasing + + --- On After "TargetLost" event. + -- @function [parent=#AUTOLASE] OnAfterTargetLost + -- @param #AUTOLASE self + -- @param #string From The from state + -- @param #string Event The event + -- @param #string To The to state + -- @param #string UnitName The lost unit\'s name + -- @param #string RecceName The Recce name lasing + + --- On After "LaserTimeout" event. + -- @function [parent=#AUTOLASE] OnAfterLaserTimeout + -- @param #AUTOLASE self + -- @param #string From The from state + -- @param #string Event The event + -- @param #string To The to state + -- @param #string UnitName The lost unit\'s name + -- @param #string RecceName The Recce name lasing + + --- On After "Lasing" event. + -- @function [parent=#AUTOLASE] OnAfterLasing + -- @param #AUTOLASE self + -- @param #string From The from state + -- @param #string Event The event + -- @param #string To The to state + -- @param Functional.Autolase#AUTOLASE.LaserSpot LaserSpot The LaserSpot data table + +end + +------------------------------------------------------------------- +-- Helper Functions +------------------------------------------------------------------- + +--- (Internal) Function to set pilot menu. +-- @param #AUTOLASE self +-- @return #AUTOLASE self +function AUTOLASE:SetPilotMenu() + local pilottable = self.pilotset:GetSetObjects() or {} + for _,_unit in pairs (pilottable) do + local Unit = _unit -- Wrapper.Unit#UNIT + if Unit and Unit:IsAlive() then + local Group = Unit:GetGroup() + local lasemenu = MENU_GROUP_COMMAND:New(Group,"Autolase Status",nil,self.ShowStatus,self,Group) + lasemenu:Refresh() + end + end + return self +end + +--- (Internal) Event function for new pilots. +-- @param #AUTOLASE self +-- @param Core.Event#EVENTDATA EventData +-- @return #AUTOLASE self +function AUTOLASE:OnEventPlayerEnterAircraft(EventData) + self:SetPilotMenu() + return self +end + +--- (Internal) Function to get a laser code by recce name +-- @param #AUTOLASE self +-- @param #string RecceName Unit(!) name of the Recce +-- @return #AUTOLASE self +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 + +--- (User) Function enable sending messages via SRS. +-- @param #AUTOLASE self +-- @param #boolean OnOff Switch usage on and off +-- @param #string Path Path to SRS directory, e.g. C:\\Program Files\\DCS-SimpleRadio-Standalon +-- @param #number Frequency Frequency to send, e.g. 243 +-- @param #number Modulation Modulation i.e. radio.modulation.AM or radio.modulation.FM +-- @return #AUTOLASE self +function AUTOLASE:SetUsingSRS(OnOff,Path,Frequency,Modulation) + self.useSRS = OnOff or true + self.SRSPath = Path or "E:\\Program Files\\DCS-SimpleRadio-Standalone" + self.SRSFreq = Frequency or 271 + self.SRSMod = Modulation or radio.modulation.AM + return self +end + +--- (User) Function set max lasing targets +-- @param #AUTOLASE self +-- @param #number Number Max number of targets to lase at once +-- @return #AUTOLASE self +function AUTOLASE:SetMaxLasingTargets(Number) + self.maxlasing = Number or 4 + return self +end + +--- (Internal) Function set notify pilots on events +-- @param #AUTOLASE self +-- @param #boolean OnOff Switch messaging on (true) or off (false) +-- @return #AUTOLASE self +function AUTOLASE:SetNotifyPilots(OnOff) + self.notifypilots = OnOff and true + return self +end + +--- (User) Function to set a specific code to a Recce. +-- @param #AUTOLASE self +-- @param #string RecceName (Unit!) Name of the Recce +-- @param #number Code The lase code +-- @return #AUTOLASE self +function AUTOLASE:SetRecceLaserCode(RecceName, Code) + local code = Code or 1688 + self.RecceLaserCode[RecceName] = code + return self +end + +--- (User) Function to force laser cooldown and cool down time +-- @param #AUTOLASE self +-- @param #boolean OnOff Switch cool down on (true) or off (false) - defaults to true +-- @param #number Seconds Number of seconds for cooldown - dafaults to 60 seconds +-- @return #AUTOLASE self +function AUTOLASE:SetLaserCoolDown(OnOff, Seconds) + self.forcecooldown = OnOff and true + self.cooldowntime = Seconds or 60 + return self +end + +--- (User) Function to set message show times. +-- @param #AUTOLASE self +-- @param #number long Longer show time +-- @param #number short Shorter show time +-- @return #AUTOLASE self +function AUTOLASE:SetReportingTimes(long, short) + self.reporttimeshort = short or 10 + self.reporttimelong = long or 30 + return self +end + +--- (User) Function to set lasing distance in meters and duration in seconds +-- @param #AUTOLASE self +-- @param #number Distance (Max) distance for lasing in meters - default 5000 meters +-- @param #number Duration (Max) duration for lasing in seconds - default 300 secs +-- @return #AUTOLASE self +function AUTOLASE:SetLasingParameters(Distance, Duration) + self.LaseDistance = Distance or 5000 + self.LaseDuration = Duration or 300 + return self +end + +--- (User) Function to set smoking of targets. +-- @param #AUTOLASE self +-- @param #boolean OnOff Switch smoking on or off +-- @param #number Color Smokecolor, e.g. SMOKECOLOR.Red +-- @return #AUTOLASE self +function AUTOLASE:SetSmokeTargets(OnOff,Color) + self.smoketargets = OnOff + self.smokecolor = Color or SMOKECOLOR.Red + return self +end + +--- (Internal) Function to calculate line of sight. +-- @param #AUTOLASE self +-- @param Wrapper.Unit#UNIT Unit +-- @return #number LOS Line of sight in meters +function AUTOLASE:GetLosFromUnit(Unit) + local lasedistance = self.LaseDistance + local unitheight = Unit:GetHeight() + local coord = Unit:GetCoordinate() + 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 + +--- (Internal) Function to check on lased targets. +-- @param #AUTOLASE self +-- @return #AUTOLASE self +function AUTOLASE:CleanCurrentLasing() + local lasingtable = self.CurrentLasing + local newtable = {} + local newreccecount = {} + local lasing = 0 + + for _ind,_entry in pairs(lasingtable) do + local entry = _entry -- #AUTOLASE.LaserSpot + if not newreccecount[entry.reccename] then + newreccecount[entry.reccename] = 0 + end + end + + for _,_recce in pairs (self.RecceSet:GetSetObjects()) do + local recce = _recce --Wrapper.Group#GROUP + if recce and recce:IsAlive() then + local unit = recce:GetUnit(1) + local name = unit:GetName() + if not self.RecceUnits[name] then + self.RecceUnits[name] = { name=name, unit=unit, cooldown = false, timestamp = timer.getAbsTime() } + end + end + end + + for _ind,_entry in pairs(lasingtable) do + local entry = _entry -- #AUTOLASE.LaserSpot + local valid = 0 + local reccedead = false + local unitdead = false + local lostsight = false + local timeout = false + local Tnow = timer.getAbsTime() + -- check recce dead + local recce = entry.lasingunit + if recce and recce:IsAlive() then + valid = valid + 1 + else + reccedead = true + self:__RecceKIA(2,entry.reccename) + end + -- check entry dead + 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 + -- check entry out of sight + 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 + -- check timed out + local timestamp = entry.timestamp + if Tnow - timestamp < self.LaseDuration and not lostsight then + valid = valid + 1 + else + timeout = true + entry.laserspot:LaseOff() + + self.RecceUnits[entry.reccename].cooldown = true + self.RecceUnits[entry.reccename].timestamp = timer.getAbsTime() + + if not lostsight then + self:__LaserTimeout(2,entry.unitname,entry.reccename) + end + end + if valid == 4 then + self.lasingindex = self.lasingindex + 1 + newtable[self.lasingindex] = entry + newreccecount[entry.reccename] = newreccecount[entry.reccename] + 1 + lasing = lasing + 1 + end + end + self.CurrentLasing = newtable + self.targetsperrecce = newreccecount + return lasing +end + +--- (Internal) Function to show status. +-- @param #AUTOLASE self +-- @param Wrapper.Group#GROUP Group (Optional) show to a certain group +-- @return #AUTOLASE self +function AUTOLASE:ShowStatus(Group) + local report = REPORT:New("Autolase") + local reccetable = self.RecceSet:GetSetObjects() + for _,_recce in pairs(reccetable) do + if _recce and _recce:IsAlive() then + local unit = _recce:GetUnit(1) + local name = unit:GetName() + local code = self:GetLaserCode(name) + report:Add(string.format("Recce %s has code %d",name,code)) + end + end + local lines = 0 + for _ind,_entry in pairs(self.CurrentLasing) do + local entry = _entry -- #AUTOLASE.LaserSpot + local reccename = entry.reccename + local typename = entry.unittype + local code = entry.lasercode + local locationstring = entry.location + local text = string.format("%s lasing %s code %d\nat %s",reccename,typename,code,locationstring) + report:Add(text) + lines = lines + 1 + end + if lines == 0 then + report:Add("No targets!") + end + local reporttime = self.reporttimelong + if lines == 0 then reporttime = self.reporttimeshort end + if Group and Group:IsAlive() then + local m = MESSAGE:New(report:Text(),reporttime,"Info"):ToGroup(Group) + else + local m = MESSAGE:New(report:Text(),reporttime,"Info"):ToCoalition(self.coalition) + end + return self +end + +--- (Internal) Function to show messages. +-- @param #AUTOLASE self +-- @param #string Message The message to be sent +-- @param #number Duration Duration in seconds +-- @return #AUTOLASE self +function AUTOLASE:NotifyPilots(Message,Duration) + if self.usepilotset then + local pilotset = self.pilotset:GetSetObjects() --#table + for _,_pilot in pairs(pilotset) do + local pilot = _pilot -- Wrapper.Unit#UNIT + if pilot and pilot:IsAlive() then + local Group = pilot:GetGroup() + local m = MESSAGE:New(Message,Duration,"Autolase"):ToGroup(Group) + end + end + elseif not self.debug then + local m = MESSAGE:New(Message,Duration,"Autolase"):ToCoalition(self.coalition) + else + local m = MESSAGE:New(Message,Duration,"Autolase"):ToAll() + end + if self.debug then self:I(Message) end + return self +end + +--- (User) Send messages via SRS. +-- @param #AUTOLASE self +-- @param #string Message The (short!) message to be sent, e.g. "Lasing target!" +-- @return #AUTOLASE self +-- @usage Step 1 - set up the radio basics **once** with +-- my_autolase:SetUsingSRS(true,"C:\\path\\SRS-Folder",251,radio.modulation.AM) +-- Step 2 - send a message, e.g. +-- function my_autolase:OnAfterLasing(From, Event, To, LaserSpot) +-- my_autolase:NotifyPilotsWithSRS("Reaper lasing new target!") +-- end +function AUTOLASE:NotifyPilotsWithSRS(Message) + if self.useSRS then + -- Create a SOUNDTEXT object. + if self.debug then + BASE:TraceOn() + BASE:TraceClass("SOUNDTEXT") + BASE:TraceClass("MSRS") + end + local path = self.SRSPath or "C:\\Program Files\\DCS-SimpleRadio-Standalone" + local freq = self.SRSFreq or 271 + local mod = self.SRSMod or radio.modulation.AM + local text=SOUNDTEXT:New(Message) + -- MOOSE SRS + local msrs=MSRS:New(path, freq, mod) + -- Text-to speech with default voice after 2 seconds. + msrs:PlaySoundText(text, 2) + end + if self.debug then self:I(Message) end + return self +end + +--- (Internal) Function to check if a unit is already lased. +-- @param #AUTOLASE self +-- @param #string unitname Name of the unit to check +-- @return #boolean outcome True or false +function AUTOLASE:CheckIsLased(unitname) + local outcome = false + for _,_laserspot in pairs(self.CurrentLasing) do + local spot = _laserspot -- #AUTOLASE.LaserSpot + if spot.unitname == unitname then + outcome = true + break + end + end + return outcome +end + +--- (Internal) Function to check if a unit can be lased. +-- @param #AUTOLASE self +-- @param Wrapper.Unit#UNIT Recce The Recce #UNIT +-- @param Wrapper.Unit#UNIT Unit The lased #UNIT +-- @return #boolean outcome True or false +function AUTOLASE:CanLase(Recce,Unit) + local canlase = false + -- cooldown? + local name = Recce:GetName() + local cooldown = self.RecceUnits[name].cooldown and self.forcecooldown + if cooldown then + local Tdiff = timer.getAbsTime() - self.RecceUnits[name].timestamp + if Tdiff < self.cooldowntime then + return false + else + self.RecceUnits[name].cooldown = false + end + end + -- calculate LOS + local reccecoord = Recce:GetCoordinate() + local unitcoord = Unit:GetCoordinate() + local islos = reccecoord:IsLOS(unitcoord,2.5) + -- calculate distance + local distance = math.floor(reccecoord:Get3DDistance(unitcoord)) + local lasedistance = self:GetLosFromUnit(Recce) + if distance <= lasedistance and islos then + canlase = true + end + return canlase +end + +------------------------------------------------------------------- +-- FSM Functions +------------------------------------------------------------------- + +--- (Internal) FSM Function for monitoring +-- @param #AUTOLASE self +-- @param #string From The from state +-- @param #string Event The event +-- @param #string To The to state +-- @return #AUTOLASE self +function AUTOLASE:onbeforeMonitor(From, Event, To) + self:T({From, Event, To}) + -- Check if group has detected any units. + self:UpdateIntel() + return self +end + +--- (Internal) FSM Function for monitoring +-- @param #AUTOLASE self +-- @param #string From The from state +-- @param #string Event The event +-- @param #string To The to state +-- @return #AUTOLASE self +function AUTOLASE:onafterMonitor(From, Event, To) + self:T({From, Event, To}) + + -- Housekeeping + local countlases = self:CleanCurrentLasing() + + self:SetPilotMenu() + + local detecteditems = self.Contacts or {} -- #table of Ops.Intelligence#INTEL.Contact + local groupsbythreat = {} + local report = REPORT:New("Detections") + local lines = 0 + for _,_contact in pairs(detecteditems) do + local contact = _contact -- Ops.Intelligence#INTEL.Contact + local grp = contact.group + local coord = contact.position + local reccename = contact.recce + local reccegrp = UNIT:FindByName(reccename) + local reccecoord = reccegrp:GetCoordinate() + 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 + -- sort out groups beyond sight + local lasedistance = self:GetLosFromUnit(reccegrp) + if grp:IsGround() and lasedistance >= distance then + table.insert(groupsbythreat,{contact.group,contact.threatlevel}) + self.RecceNames[contact.groupname] = contact.recce + 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] -- Coin value of a + local bNum = b[2] -- Coin value of b + return aNum > bNum -- Return their comparisons, < for ascending, > for descending + end) + + -- build table of Units + local unitsbythreat = {} + for _,_entry in pairs(self.GroupsByThreat) do + local group = _entry[1] -- Wrapper.Group#GROUP + 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 -- Wrapper.Unit#UNIT + if unit and unit:IsAlive() then + local threat = unit:GetThreatLevel() + local coord = unit:GetCoordinate() + if threat > 0 then + local unitname = unit:GetName() + 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] -- Coin value of a + local bNum = b[2] -- Coin value of b + return aNum > bNum -- Return their comparisons, < for ascending, > for descending + 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] -- Wrapper.Unit#UNIT + local unitname = unit:GetName() + local canlase = self:CanLase(recce,unit) + if targets+reccecount < self.maxlasing and not self:CheckIsLased(unitname) and unit:IsAlive() and canlase then + targets = targets + 1 + local code = self:GetLaserCode(reccename) + local spot = SPOT:New(recce) + spot:LaseOn(unit,code,self.LaseDuration) + local locationstring = unit:GetCoordinate():ToStringLLDDM() + local laserspot = { -- #AUTOLASE.LaserSpot + laserspot = spot, + lasedunit = unit, + lasingunit = recce, + lasercode = code, + location = locationstring, + timestamp = timer.getAbsTime(), + unitname = unitname, + reccename = reccename, + unittype = unit:GetTypeName(), + } + if self.smoketargets then + local coord = unit:GetCoordinate() + coord:Smoke(self.smokecolor) + end + self.lasingindex = self.lasingindex + 1 + self.CurrentLasing[self.lasingindex] = laserspot + self:__Lasing(2,laserspot) + end + end + end + + self:__Monitor(-30) + return self +end + +--- (Internal) FSM Function onbeforeRecceKIA +-- @param #AUTOLASE self +-- @param #string From The from state +-- @param #string Event The event +-- @param #string To The to state +-- @param #string RecceName The lost Recce +-- @return #AUTOLASE self +function AUTOLASE:onbeforeRecceKIA(From,Event,To,RecceName) + self:T({From, Event, To, RecceName}) + if self.notifypilots or self.debug then + local text = string.format("Recce %s KIA!",RecceName) + self:NotifyPilots(text,self.reporttimeshort) + end + return self +end + +--- (Internal) FSM Function onbeforeTargetDestroyed +-- @param #AUTOLASE self +-- @param #string From The from state +-- @param #string Event The event +-- @param #string To The to state +-- @param #string UnitName The destroyed unit\'s name +-- @param #string RecceName The Recce name lasing +-- @return #AUTOLASE self +function AUTOLASE:onbeforeTargetDestroyed(From,Event,To,UnitName,RecceName) + self:T({From, Event, To, UnitName, RecceName}) + if self.notifypilots or self.debug then + local text = string.format("Unit %s destroyed! Good job!",UnitName) + self:NotifyPilots(text,self.reporttimeshort) + end + return self +end + +--- (Internal) FSM Function onbeforeTargetLost +-- @param #AUTOLASE self +-- @param #string From The from state +-- @param #string Event The event +-- @param #string To The to state +-- @param #string UnitName The lost unit\'s name +-- @param #string RecceName The Recce name lasing +-- @return #AUTOLASE self +function AUTOLASE:onbeforeTargetLost(From,Event,To,UnitName,RecceName) + self:T({From, Event, To, UnitName,RecceName}) + if self.notifypilots or self.debug then + local text = string.format("%s lost sight of unit %s.",RecceName,UnitName) + self:NotifyPilots(text,self.reporttimeshort) + end + return self +end + +--- (Internal) FSM Function onbeforeLaserTimeout +-- @param #AUTOLASE self +-- @param #string From The from state +-- @param #string Event The event +-- @param #string To The to state +-- @param #string UnitName The lost unit\'s name +-- @param #string RecceName The Recce name lasing +-- @return #AUTOLASE self +function AUTOLASE:onbeforeLaserTimeout(From,Event,To,UnitName,RecceName) + self:T({From, Event, To, UnitName,RecceName}) + if self.notifypilots or self.debug then + local text = string.format("%s laser timeout on unit %s.",RecceName,UnitName) + self:NotifyPilots(text,self.reporttimeshort) + end + return self +end + +--- (Internal) FSM Function onbeforeLasing +-- @param #AUTOLASE self +-- @param #string From The from state +-- @param #string Event The event +-- @param #string To The to state +-- @param Functional.Autolase#AUTOLASE.LaserSpot LaserSpot The LaserSpot data table +-- @return #AUTOLASE self +function AUTOLASE:onbeforeLasing(From,Event,To,LaserSpot) + self:T({From, Event, To, LaserSpot.unittype}) + if self.notifypilots or self.debug then + local laserspot = LaserSpot -- #AUTOLASE.LaserSpot + local text = string.format("%s is lasing %s code %d\nat %s",laserspot.reccename,laserspot.unittype,laserspot.lasercode,laserspot.location) + self:NotifyPilots(text,self.reporttimeshort+5) + end + return self +end + +--- (Internal) FSM Function onbeforeCancel +-- @param #AUTOLASE self +-- @param #string From The from state +-- @param #string Event The event +-- @param #string To The to state +-- @return #AUTOLASE self +function AUTOLASE:onbeforeCancel(From,Event,To) + self:UnHandleEvent(EVENTS.PlayerEnterAircraft) + self:__Stop(2) + return self +end + +------------------------------------------------------------------- +-- End Functional.Autolase.lua +------------------------------------------------------------------- diff --git a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua index 07eea7e21..d90dfcca7 100644 --- a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua +++ b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua @@ -715,20 +715,21 @@ do -- ZONE_CAPTURE_COALITION local UnitHit = EventData.TgtUnit + if UnitHit.ClassName ~= "SCENERY" then -- Check if unit is inside the capture zone and that it is of the defending coalition. - if UnitHit and UnitHit:IsInZone(self) and UnitHit:GetCoalition()==self.Coalition then - - -- Update last hit time. - self.HitTimeLast=timer.getTime() - - -- Only trigger attacked event if not already in state "Attacked". - if self:GetState()~="Attacked" then - self:F2("Hit ==> Attack") - self:Attack() - end - + if UnitHit and UnitHit:IsInZone(self) and UnitHit:GetCoalition()==self.Coalition then + + -- Update last hit time. + self.HitTimeLast=timer.getTime() + + -- Only trigger attacked event if not already in state "Attacked". + if self:GetState()~="Attacked" then + self:F2("Hit ==> Attack") + self:Attack() + end + + end end - end end diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index fdb043c2e..7d47cbba1 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -70,6 +70,7 @@ __Moose.Include( 'Scripts/Moose/Functional/Warehouse.lua' ) __Moose.Include( 'Scripts/Moose/Functional/Fox.lua' ) __Moose.Include( 'Scripts/Moose/Functional/Mantis.lua' ) __Moose.Include( 'Scripts/Moose/Functional/Shorad.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Autolase.lua' ) __Moose.Include( 'Scripts/Moose/Ops/Airboss.lua' ) __Moose.Include( 'Scripts/Moose/Ops/RecoveryTanker.lua' ) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 5d5fe5161..1afe387ca 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -31,6 +31,7 @@ -- * [USS Abraham Lincoln](https://en.wikipedia.org/wiki/USS_Abraham_Lincoln_(CVN-72)) (CVN-72) [Super Carrier Module] -- * [USS George Washington](https://en.wikipedia.org/wiki/USS_George_Washington_(CVN-73)) (CVN-73) [Super Carrier Module] -- * [USS Harry S. Truman](https://en.wikipedia.org/wiki/USS_Harry_S._Truman) (CVN-75) [Super Carrier Module] +-- * [USS Forrestal](https://en.wikipedia.org/wiki/USS_Forrestal_(CV-59)) (CV-59) [Heatblur Carrier Module] -- * [USS Tarawa](https://en.wikipedia.org/wiki/USS_Tarawa_(LHA-1)) (LHA-1) [**WIP**] -- * [USS America](https://en.wikipedia.org/wiki/USS_America_(LHA-6)) (LHA-6) [**WIP**] -- * [Juan Carlos I](https://en.wikipedia.org/wiki/Spanish_amphibious_assault_ship_Juan_Carlos_I) (L61) [**WIP**] @@ -1295,6 +1296,7 @@ AIRBOSS.AircraftCarrier={ -- @field #string WASHINGTON USS George Washington (CVN-73) [Super Carrier Module] -- @field #string STENNIS USS John C. Stennis (CVN-74) -- @field #string TRUMAN USS Harry S. Truman (CVN-75) [Super Carrier Module] +-- @field #string FORRESTAL USS Forrestal (CV-59) [Heatblur Carrier Module] -- @field #string VINSON USS Carl Vinson (CVN-70) [Obsolete] -- @field #string TARAWA USS Tarawa (LHA-1) -- @field #string AMERICA USS America (LHA-6) @@ -1306,6 +1308,7 @@ AIRBOSS.CarrierType={ WASHINGTON="CVN_73", TRUMAN="CVN_75", STENNIS="Stennis", + FORRESTAL="Forrestal", VINSON="VINSON", TARAWA="LHA_Tarawa", AMERICA="USS America LHA-6", @@ -1723,7 +1726,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="1.1.6" +AIRBOSS.version="1.2.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1974,6 +1977,8 @@ function AIRBOSS:New(carriername, alias) 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.VINSON then -- TODO: Carl Vinson parameters. self:_InitStennis() @@ -2041,7 +2046,7 @@ function AIRBOSS:New(carriername, alias) local stern=self:_GetSternCoord() -- Bow pos. - local bow=stern:Translate(self.carrierparam.totlength, hdg) + local bow=stern:Translate(self.carrierparam.totlength, hdg, true) -- End of rwy. local rwy=stern:Translate(self.carrierparam.rwylength, FB, true) @@ -2059,31 +2064,31 @@ function AIRBOSS:New(carriername, alias) bow:FlareYellow() -- Runway half width = 10 m. - local r1=stern:Translate(self.carrierparam.rwywidth*0.5, FB+90) - local r2=stern:Translate(self.carrierparam.rwywidth*0.5, FB-90) - r1:FlareWhite() - r2:FlareWhite() + local r1=stern:Translate(self.carrierparam.rwywidth*0.5, FB+90, true) + local r2=stern:Translate(self.carrierparam.rwywidth*0.5, FB-90, true) + --r1:FlareWhite() + --r2:FlareWhite() -- End of runway. rwy:FlareRed() -- Right 30 meters from stern. - local cR=stern:Translate(self.carrierparam.totwidthstarboard, hdg+90) - cR:FlareYellow() + local cR=stern:Translate(self.carrierparam.totwidthstarboard, hdg+90, true) + --cR:FlareYellow() -- Left 40 meters from stern. - local cL=stern:Translate(self.carrierparam.totwidthport, hdg-90) - cL:FlareYellow() + local cL=stern:Translate(self.carrierparam.totwidthport, hdg-90, true) + --cL:FlareYellow() -- Carrier specific. if self.carrier:GetTypeName()~=AIRBOSS.CarrierType.TARAWA or self.carrier:GetTypeName()~=AIRBOSS.CarrierType.AMERICA or self.carrier:GetTypeName()~=AIRBOSS.CarrierType.JCARLOS then -- Flare wires. - local w1=stern:Translate(self.carrierparam.wire1, FB) - local w2=stern:Translate(self.carrierparam.wire2, FB) - local w3=stern:Translate(self.carrierparam.wire3, FB) - local w4=stern:Translate(self.carrierparam.wire4, FB) + 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() @@ -4380,6 +4385,35 @@ function AIRBOSS:_InitNimitz() end +--- Init parameters for Forrestal class super carriers. +-- @param #AIRBOSS self +function AIRBOSS:_InitForrestal() + + -- Init Nimitz as default. + self:_InitNimitz() + + -- Carrier Parameters. + self.carrierparam.sterndist =-135.5 + self.carrierparam.deckheight = 20 --20.1494 --DCS World OpenBeta\CoreMods\tech\USS_Nimitz\Database\USS_CVN_7X.lua + + -- Total size of the carrier (approx as rectangle). + self.carrierparam.totlength=315 -- Wiki says 325 meters overall length. + self.carrierparam.totwidthport=45 -- Wiki says 73 meters overall beam. + self.carrierparam.totwidthstarboard=35 + + -- Landing runway. + self.carrierparam.rwyangle = -9.1359 --DCS World OpenBeta\CoreMods\tech\USS_Nimitz\scripts\USS_Nimitz_RunwaysAndRoutes.lua + self.carrierparam.rwylength = 212 + self.carrierparam.rwywidth = 25 + + -- Wires. + self.carrierparam.wire1 = 42 -- Distance from stern to first wire. + self.carrierparam.wire2 = 51.5 + self.carrierparam.wire3 = 62 + self.carrierparam.wire4 = 72.5 + +end + --- Init parameters for LHA-1 Tarawa carrier. -- @param #AIRBOSS self function AIRBOSS:_InitTarawa() @@ -10549,6 +10583,9 @@ function AIRBOSS:_GetSternCoord() elseif self.carriertype==AIRBOSS.CarrierType.STENNIS then -- Stennis: translate 7 meters starboard wrt Final bearing. self.sterncoord:Translate(self.carrierparam.sterndist, hdg, true, true):Translate(7, FB+90, true, true) + elseif self.carriertype==AIRBOSS.CarrierType.FORRESTAL then + -- Forrestal + self.sterncoord:Translate(self.carrierparam.sterndist, hdg, true, true):Translate(7.5, FB+90, true, true) else -- Nimitz SC: translate 8 meters starboard wrt Final bearing. self.sterncoord:Translate(self.carrierparam.sterndist, hdg, true, true):Translate(9.5, FB+90, true, true) diff --git a/Moose Development/Moose/Ops/CSAR.lua b/Moose Development/Moose/Ops/CSAR.lua index 6817db0e1..49a5ebb90 100644 --- a/Moose Development/Moose/Ops/CSAR.lua +++ b/Moose Development/Moose/Ops/CSAR.lua @@ -22,7 +22,7 @@ -- @module Ops.CSAR -- @image OPS_CSAR.jpg --- Date: Sep 2021 +-- Date: Oct 2021 ------------------------------------------------------------------------- --- **CSAR** class, extends Core.Base#BASE, Core.Fsm#FSM @@ -97,6 +97,14 @@ -- self.pilotmustopendoors = false -- switch to true to enable check of open doors -- -- (added 0.1.9) -- self.suppressmessages = false -- switch off all messaging if you want to do your own +-- -- (added 0.1.11) +-- self.rescuehoverheight = 20 -- max height for a hovering rescue in meters +-- self.rescuehoverdistance = 10 -- max distance for a hovering rescue in meters +-- -- (added 0.1.12) +-- -- Country codes for spawned pilots +-- self.countryblue= country.id.USA +-- self.countryred = country.id.RUSSIA +-- self.countryneutral = country.id.UN_PEACEKEEPERS -- -- ## 2.1 Experimental Features -- @@ -233,7 +241,7 @@ CSAR.AircraftType["Mi-24V"] = 8 --- CSAR class version. -- @field #string version -CSAR.version="0.1.10r5" +CSAR.version="0.1.11r1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list @@ -362,6 +370,15 @@ function CSAR:New(Coalition, Template, Alias) self.approachdist_near = 3000 -- switch to 5 sec interval approach mode, meters self.pilotmustopendoors = false -- switch to true to enable check on open doors self.suppressmessages = false + + -- added 0.1.11r1 + self.rescuehoverheight = 20 + self.rescuehoverdistance = 10 + + -- added 0.1.12 + self.countryblue= country.id.USA + self.countryred = country.id.RUSSIA + self.countryneutral = country.id.UN_PEACEKEEPERS -- WARNING - here\'ll be dragons -- for this to work you need to de-sanitize your mission environment in \Scripts\MissionScripting.lua @@ -549,6 +566,7 @@ function CSAR:_SpawnPilotInField(country,point,frequency) for i=1,10 do math.random(i,10000) end + if point:IsSurfaceTypeWater() then point.y = 0 end local template = self.template local alias = string.format("Pilot %.2fkHz-%d", freq, math.random(1,99)) local coalition = self.coalition @@ -687,11 +705,11 @@ function CSAR:_SpawnCsarAtZone( _zone, _coalition, _description, _randomPoint, _ local _country = 0 if _coalition == coalition.side.BLUE then - _country = country.id.USA + _country = self.countryblue elseif _coalition == coalition.side.RED then - _country = country.id.RUSSIA + _country = self.countryred else - _country = country.id.UN_PEACEKEEPERS + _country = self.countryneutral end self:_AddCsar(_coalition, _country, pos, typename, unitname, _description, freq, _nomessage, _description, forcedesc) @@ -1120,7 +1138,6 @@ end function CSAR:_IsLoadingDoorOpen( unit_name ) self:T(self.lid .. " _IsLoadingDoorOpen") return UTILS.IsLoadingDoorOpen(unit_name) - end --- (Internal) Function to check if heli is close to group. @@ -1200,15 +1217,16 @@ function CSAR:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedG end if _heliUnit:InAir() and _unitsInHelicopter + 1 <= _maxUnits then - - if _distance < 8.0 then + -- TODO - make variable + if _distance < self.rescuehoverdistance then --check height! local leaderheight = _woundedLeader:GetHeight() if leaderheight < 0 then leaderheight = 0 end local _height = _heliUnit:GetHeight() - leaderheight - - if _height <= 20.0 then + + -- TODO - make variable + if _height <= self.rescuehoverheight then local _time = self.hoverStatus[_lookupKeyHeli] diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index 97b33e734..8cdfbcdd6 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -22,7 +22,7 @@ -- @module Ops.CTLD -- @image OPS_CTLD.jpg --- Date: Sep 2021 +-- Date: Oct 2021 do ------------------------------------------------------ @@ -669,6 +669,7 @@ do -- my_ctld.cratecountry = country.id.GERMANY -- ID of crates. Will default to country.id.RUSSIA for RED coalition setups. -- my_ctld.allowcratepickupagain = true -- allow re-pickup crates that were dropped. -- my_ctld.enableslingload = false -- allow cargos to be slingloaded - might not work for all cargo types +-- my_ctld.pilotmustopendoors = false -- -- force opening of doors -- -- ## 2.1 User functions -- @@ -987,7 +988,7 @@ CTLD.UnitTypes = { --- CTLD class version. -- @field #string version -CTLD.version="0.2.2a4" +CTLD.version="0.2.4" --- Instantiate a new CTLD. -- @param #CTLD self @@ -1131,6 +1132,9 @@ function CTLD:New(Coalition, Prefixes, Alias) -- country of crates spawned self.cratecountry = country.id.GERMANY + -- for opening doors + self.pilotmustopendoors = false + if self.coalition == coalition.side.RED then self.cratecountry = country.id.RUSSIA end @@ -1436,6 +1440,7 @@ function CTLD:_LoadTroops(Group, Unit, Cargotype) -- landed or hovering over load zone? local grounded = not self:IsUnitInAir(Unit) local hoverload = self:CanHoverLoad(Unit) + --local dooropen = UTILS.IsLoadingDoorOpen(Unit:GetName()) and self.pilotmustopendoors -- check if we are in LOAD zone local inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) if not inzone then @@ -1447,6 +1452,9 @@ function CTLD:_LoadTroops(Group, Unit, Cargotype) elseif not grounded and not hoverload then self:_SendMessage("You need to land or hover in position to load!", 10, false, Group) if not self.debug then return self end + elseif self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then + self:_SendMessage("You need to open the door(s) to load troops!", 10, false, Group) + if not self.debug then return self end end -- load troops into heli local group = Group -- Wrapper.Group#GROUP @@ -1618,6 +1626,10 @@ end self:_SendMessage("You need to land or hover in position to load!", 10, false, Group) if not self.debug then return self end end + if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then + self:_SendMessage("You need to open the door(s) to extract troops!", 10, false, Group) + if not self.debug then return self end + end -- load troops into heli local unit = Unit -- Wrapper.Unit#UNIT local unitname = unit:GetName() @@ -1887,14 +1899,18 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop) return self end ---- Inject crates and static cargo objects. +--- (Internal) Inject crates and static cargo objects. -- @param #CTLD self -- @param Core.Zone#ZONE Zone Zone to spawn in. -- @param #CTLD_CARGO Cargo The cargo type to spawn. +-- @param #boolean RandomCoord Randomize coordinate. -- @return #CTLD self -function CTLD:InjectStatics(Zone, Cargo) +function CTLD:InjectStatics(Zone, Cargo, RandomCoord) 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 @@ -1930,6 +1946,19 @@ function CTLD:InjectStatics(Zone, Cargo) return self end +--- (User) Inject static cargo objects. +-- @param #CTLD self +-- @param Core.Zone#ZONE Zone Zone to spawn in. Will be a somewhat random coordinate. +-- @param #string Template Unit(!) name of the static cargo object to be used as template. +-- @param #number Mass Mass of the static in kg. +-- @return #CTLD self +function CTLD:InjectStaticFromTemplate(Zone, Template, Mass) + self:T(self.lid .. " InjectStaticFromTemplate") + local cargotype = self:GetStaticsCargoFromTemplate(Template,Mass) -- #CTLD_CARGO + self:InjectStatics(Zone,cargotype,true) + return self +end + --- (Internal) Function to find and list nearby crates. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group @@ -2343,6 +2372,11 @@ function CTLD:_UnloadTroops(Group, Unit) self:T(self.lid .. " _UnloadTroops") -- check if we are in LOAD zone local droppingatbase = false + local canunload = true + if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then + self:_SendMessage("You need to open the door(s) to unload troops!", 10, false, Group) + if not self.debug then return self end + end local inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) if not inzone then inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP) @@ -2961,7 +2995,7 @@ end --- User function - Add *generic* static-type loadable as cargo. This type will create cargo that needs to be loaded, moved and dropped. -- @param #CTLD self --- @param #string Name Unique name of this type of cargo as set in the mission editor (not: UNIT name!), e.g. "Ammunition-1". +-- @param #string Name Unique name of this type of cargo as set in the mission editor (note: UNIT name!), e.g. "Ammunition-1". -- @param #number Mass Mass in kg of each static in kg, e.g. 100. -- @param #number Stock Number of groups in stock. Nil for unlimited. function CTLD:AddStaticsCargo(Name,Mass,Stock) @@ -2975,6 +3009,22 @@ function CTLD:AddStaticsCargo(Name,Mass,Stock) return self end +--- User function - Get a *generic* static-type loadable as #CTLD_CARGO object. +-- @param #CTLD self +-- @param #string Name Unique Unit(!) name of this type of cargo as set in the mission editor (not: GROUP name!), e.g. "Ammunition-1". +-- @param #number Mass Mass in kg of each static in kg, e.g. 100. +-- @return #CTLD_CARGO Cargo object +function CTLD:GetStaticsCargoFromTemplate(Name,Mass) + self:T(self.lid .. " GetStaticsCargoFromTemplate") + self.CargoCounter = self.CargoCounter + 1 + local type = CTLD_CARGO.Enum.STATIC + local template = STATIC:FindByName(Name,true):GetTypeName() + -- Crates are not directly loadable + local cargo = CTLD_CARGO:New(self.CargoCounter,Name,template,type,false,false,1,nil,nil,Mass,1) + --table.insert(self.Cargo_Statics,cargo) + return cargo +end + --- User function - Add *generic* repair crates loadable as cargo. This type will create crates that need to be loaded, moved, dropped and built. -- @param #CTLD self -- @param #string Name Unique name of this type of cargo. E.g. "Humvee". @@ -3013,7 +3063,7 @@ end --- User function - Activate Name #CTLD.CargoZone.Type ZoneType for this CTLD instance. -- @param #CTLD self -- @param #string Name Name of the zone to change in the ME. --- @param #CTLD.CargoZoneTyp ZoneType Type of zone this belongs to. +-- @param #CTLD.CargoZoneType ZoneType Type of zone this belongs to. -- @param #boolean NewState (Optional) Set to true to activate, false to switch off. function CTLD:ActivateZone(Name,ZoneType,NewState) self:T(self.lid .. " AddZone") @@ -3049,7 +3099,7 @@ end --- User function - Deactivate Name #CTLD.CargoZoneType ZoneType for this CTLD instance. -- @param #CTLD self -- @param #string Name Name of the zone to change in the ME. --- @param #CTLD.CargoZoneTyp ZoneType Type of zone this belongs to. +-- @param #CTLD.CargoZoneType ZoneType Type of zone this belongs to. function CTLD:DeactivateZone(Name,ZoneType) self:T(self.lid .. " AddZone") self:ActivateZone(Name,ZoneType,false) diff --git a/Moose Development/Moose/Ops/Intelligence.lua b/Moose Development/Moose/Ops/Intelligence.lua index 0dbf99004..1eaf89c3b 100644 --- a/Moose Development/Moose/Ops/Intelligence.lua +++ b/Moose Development/Moose/Ops/Intelligence.lua @@ -5,7 +5,7 @@ -- * Detect and track contacts consistently -- * Detect and track clusters of contacts consistently -- * Use FSM events to link functionality into your scripts --- * Easy setup +-- * Easy setup -- -- === -- @@ -31,7 +31,7 @@ -- @field #table ContactsUnknown Table of new detected items. -- @field #table Clusters Clusters of detected groups. -- @field #boolean clusteranalysis If true, create clusters of detected targets. --- @field #boolean clustermarkers If true, create cluster markers on F10 map. +-- @field #boolean clustermarkers If true, create cluster markers on F10 map. -- @field #number clustercounter Running number of clusters. -- @field #number dTforget Time interval in seconds before a known contact which is not detected any more is forgotten. -- @field #number clusterradius Radius im kilometers in which groups/units are considered to belong to a cluster @@ -45,42 +45,42 @@ -- ![Banner Image](..\Presentations\CarrierAirWing\INTEL_Main.jpg) -- -- # The INTEL Concept --- +-- -- * Lightweight replacement for @{Functional.Detection#DETECTION} -- * Detect and track contacts consistently -- * Detect and track clusters of contacts consistently -- * Once detected and still alive, planes will be tracked 10 minutes, helicopters 20 minutes, ships and trains 1 hour, ground units 2 hours -- * Use FSM events to link functionality into your scripts --- --- # Basic Usage -- --- ## set up a detection SET_GROUP --- --- `Red_DetectionSetGroup = SET_GROUP:New()` --- `Red_DetectionSetGroup:FilterPrefixes( { "Red EWR" } )` --- `Red_DetectionSetGroup:FilterOnce()` --- --- ## New Intel type detection for the red side, logname "KGB" --- --- `RedIntel = INTEL:New(Red_DetectionSetGroup,"red","KGB")` --- `RedIntel:SetClusterAnalysis(true,true)` --- `RedIntel:SetVerbosity(2)` --- `RedIntel:__Start(2)` --- --- ## Hook into new contacts found --- --- `function RedIntel:OnAfterNewContact(From, Event, To, Contact)` --- `local text = string.format("NEW contact %s detected by %s", Contact.groupname, Contact.recce or "unknown")` --- `local m = MESSAGE:New(text,15,"KGB"):ToAll()` --- `end` --- --- ## And/or new clusters found --- --- `function RedIntel:OnAfterNewCluster(From, Event, To, Contact, Cluster)` --- `local text = string.format("NEW cluster %d size %d with contact %s", Cluster.index, Cluster.size, Contact.groupname)` --- `local m = MESSAGE:New(text,15,"KGB"):ToAll()` --- `end` --- +-- # Basic Usage +-- +-- ## set up a detection SET_GROUP +-- +-- `Red_DetectionSetGroup = SET_GROUP:New()` +-- `Red_DetectionSetGroup:FilterPrefixes( { "Red EWR" } )` +-- `Red_DetectionSetGroup:FilterOnce()` +-- +-- ## New Intel type detection for the red side, logname "KGB" +-- +-- `RedIntel = INTEL:New(Red_DetectionSetGroup,"red","KGB")` +-- `RedIntel:SetClusterAnalysis(true,true)` +-- `RedIntel:SetVerbosity(2)` +-- `RedIntel:__Start(2)` +-- +-- ## Hook into new contacts found +-- +-- `function RedIntel:OnAfterNewContact(From, Event, To, Contact)` +-- `local text = string.format("NEW contact %s detected by %s", Contact.groupname, Contact.recce or "unknown")` +-- `local m = MESSAGE:New(text,15,"KGB"):ToAll()` +-- `end` +-- +-- ## And/or new clusters found +-- +-- `function RedIntel:OnAfterNewCluster(From, Event, To, Contact, Cluster)` +-- `local text = string.format("NEW cluster %d size %d with contact %s", Cluster.index, Cluster.size, Contact.groupname)` +-- `local m = MESSAGE:New(text,15,"KGB"):ToAll()` +-- `end` +-- -- -- @field #INTEL INTEL = { @@ -136,7 +136,7 @@ INTEL = { --- INTEL class version. -- @field #string version -INTEL.version="0.2.6" +INTEL.version="0.2.7" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list @@ -167,7 +167,7 @@ function INTEL:New(DetectionSet, Coalition, Alias) -- Detection set. self.detectionset=DetectionSet or SET_GROUP:New() - + if Coalition and type(Coalition)=="string" then if Coalition=="blue" then Coalition=coalition.side.BLUE @@ -179,24 +179,24 @@ function INTEL:New(DetectionSet, Coalition, Alias) self:E("ERROR: Unknown coalition in INTEL!") end end - + -- Determine coalition from first group in set. self.coalition=Coalition or DetectionSet:CountAlive()>0 and DetectionSet:GetFirst():GetCoalition() or nil - + -- Filter coalition. if self.coalition then local coalitionname=UTILS.GetCoalitionName(self.coalition):lower() self.detectionset:FilterCoalitions(coalitionname) end - + -- Filter once. self.detectionset:FilterOnce() - + -- Set alias. if Alias then self.alias=tostring(Alias) else - self.alias="INTEL SPECTRE" + self.alias="INTEL SPECTRE" if self.coalition then if self.coalition==coalition.side.RED then self.alias="INTEL KGB" @@ -204,15 +204,17 @@ function INTEL:New(DetectionSet, Coalition, Alias) self.alias="INTEL CIA" end end - end - + end + self.DetectVisual = true self.DetectOptical = true self.DetectRadar = true self.DetectIRST = true self.DetectRWR = true self.DetectDLINK = true - + + self.statusupdate = -60 + -- Set some string id for output to DCS.log file. self.lid=string.format("%s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown") @@ -224,16 +226,16 @@ function INTEL:New(DetectionSet, Coalition, Alias) self:AddTransition("Stopped", "Start", "Running") -- Start FSM. self:AddTransition("*", "Status", "*") -- INTEL status update. self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. - + self:AddTransition("*", "Detect", "*") -- Start detection run. Not implemented yet! - + self:AddTransition("*", "NewContact", "*") -- New contact has been detected. self:AddTransition("*", "LostContact", "*") -- Contact could not be detected any more. - + self:AddTransition("*", "NewCluster", "*") -- New cluster has been detected. self:AddTransition("*", "LostCluster", "*") -- Cluster could not be detected any more. - + -- Defaults self:SetForgetTime() self:SetAcceptZones() @@ -268,7 +270,7 @@ function INTEL:New(DetectionSet, Coalition, Alias) -- @function [parent=#INTEL] __Status -- @param #INTEL self -- @param #number delay Delay in seconds. - + --- On After "NewContact" event. -- @function [parent=#INTEL] OnAfterNewContact -- @param #INTEL self @@ -276,7 +278,7 @@ function INTEL:New(DetectionSet, Coalition, Alias) -- @param #string Event Event. -- @param #string To To state. -- @param #INTEL.Contact Contact Detected contact. - + --- On After "LostContact" event. -- @function [parent=#INTEL] OnAfterLostContact -- @param #INTEL self @@ -284,7 +286,7 @@ function INTEL:New(DetectionSet, Coalition, Alias) -- @param #string Event Event. -- @param #string To To state. -- @param #INTEL.Contact Contact Lost contact. - + --- On After "NewCluster" event. -- @function [parent=#INTEL] OnAfterNewCluster -- @param #INTEL self @@ -293,7 +295,7 @@ function INTEL:New(DetectionSet, Coalition, Alias) -- @param #string To To state. -- @param #INTEL.Contact Contact Detected contact. -- @param #INTEL.Cluster Cluster Detected cluster - + --- On After "LostCluster" event. -- @function [parent=#INTEL] OnAfterLostCluster -- @param #INTEL self @@ -309,7 +311,7 @@ end -- User functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Set accept zones. Only contacts detected in this/these zone(s) are considered. +--- Set accept zones. Only contacts detected in this/these zone(s) are considered. -- @param #INTEL self -- @param Core.Set#SET_ZONE AcceptZoneSet Set of accept zones. -- @return #INTEL self @@ -365,7 +367,7 @@ function INTEL:RemoveRejectZone(RejectZone) return self end ---- Set forget contacts time interval. +--- Set forget contacts time interval. -- Previously known contacts that are not detected any more, are "lost" after this time. -- This avoids fast oscillations between a contact being detected and undetected. -- @param #INTEL self @@ -377,13 +379,13 @@ function INTEL:SetForgetTime(TimeInterval) end --- Filter unit categories. Valid categories are: --- +-- -- * Unit.Category.AIRPLANE -- * Unit.Category.HELICOPTER -- * Unit.Category.GROUND_UNIT -- * Unit.Category.SHIP -- * Unit.Category.STRUCTURE --- +-- -- @param #INTEL self -- @param #table Categories Filter categories, e.g. {Unit.Category.AIRPLANE, Unit.Category.HELICOPTER}. -- @return #INTEL self @@ -391,26 +393,26 @@ 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 --- Filter group categories. Valid categories are: --- +-- -- * Group.Category.AIRPLANE -- * Group.Category.HELICOPTER -- * Group.Category.GROUND -- * Group.Category.SHIP -- * Group.Category.TRAIN --- +-- -- @param #INTEL self -- @param #table GroupCategories Filter categories, e.g. `{Group.Category.AIRPLANE, Group.Category.HELICOPTER}`. -- @return #INTEL self @@ -418,15 +420,15 @@ 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 @@ -442,7 +444,7 @@ function INTEL:SetClusterAnalysis(Switch, Markers) return self end ---- Set verbosity level for debugging. +--- Set verbosity level for debugging. -- @param #INTEL self -- @param #number Verbosity The higher, the noisier, e.g. 0=off, 2=debug -- @return #INTEL self @@ -495,7 +497,7 @@ end -- @param #boolean DetectDLINK Data link detection -- @return self function INTEL:SetDetectionTypes(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) - self.DetectVisual = DetectVisual and true + self.DetectVisual = DetectVisual and true self.DetectOptical = DetectOptical and true self.DetectRadar = DetectRadar and true self.DetectIRST = DetectIRST and true @@ -554,14 +556,14 @@ function INTEL:onafterStatus(From, Event, To) -- FSM state. local fsmstate=self:GetState() - + -- Fresh arrays. self.ContactsLost={} self.ContactsUnknown={} - + -- Check if group has detected any units. self:UpdateIntel() - + -- Number of total contacts. local Ncontacts=#self.Contacts local Nclusters=#self.Clusters @@ -571,7 +573,7 @@ function INTEL:onafterStatus(From, Event, To) 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 - + -- Detailed info. if self.verbose>=2 and Ncontacts>0 then local text="Detected Contacts:" @@ -585,9 +587,9 @@ function INTEL:onafterStatus(From, Event, To) end end self:I(self.lid..text) - end + end - self:__Status(-60) + self:__Status(self.statusupdate) end @@ -602,24 +604,24 @@ function INTEL:UpdateIntel() -- Loop over all units providing intel. for _,_group in pairs(self.detectionset.Set or {}) do local group=_group --Wrapper.Group#GROUP - + if group and group:IsAlive() then - + for _,_recce in pairs(group:GetUnits()) do local recce=_recce --Wrapper.Unit#UNIT - + -- Get detected units. self:GetDetectedUnits(recce, DetectedUnits, RecceDetecting, self.DetectVisual, self.DetectOptical, self.DetectRadar, self.DetectIRST, self.DetectRWR, self.DetectDLINK) - + end - - end + + end end - + local remove={} for unitname,_unit in pairs(DetectedUnits) do local unit=_unit --Wrapper.Unit#UNIT - + -- Check if unit is in any of the accept zones. if self.acceptzoneset:Count()>0 then local inzone=false @@ -630,7 +632,7 @@ function INTEL:UpdateIntel() break end end - + -- Unit is not in accept zone ==> remove! if not inzone then table.insert(remove, unitname) @@ -647,13 +649,13 @@ function INTEL:UpdateIntel() break end end - + -- Unit is inside a reject zone ==> remove! if inzone then table.insert(remove, unitname) end end - + -- Filter unit categories. if #self.filterCategory>0 then local unitcategory=unit:GetUnitCategory() @@ -668,18 +670,18 @@ function INTEL:UpdateIntel() self:T(self.lid..string.format("Removing unit %s category=%d", unitname, unit:GetCategory())) table.insert(remove, unitname) end - end - + end + end - + -- Remove filtered units. for _,unitname in pairs(remove) do DetectedUnits[unitname]=nil end - + -- Create detected groups. local DetectedGroups={} - local RecceGroups={} + local RecceGroups={} for unitname,_unit in pairs(DetectedUnits) do local unit=_unit --Wrapper.Unit#UNIT local group=unit:GetGroup() @@ -689,15 +691,15 @@ function INTEL:UpdateIntel() RecceGroups[groupname]=RecceDetecting[unitname] end end - - -- Create detected contacts. + + -- Create detected contacts. self:CreateDetectedItems(DetectedGroups, RecceGroups) - + -- Paint a picture of the battlefield. if self.clusteranalysis then self:PaintPicture() end - + end @@ -710,35 +712,35 @@ end -- @param #table RecceDetecting Table of detecting recce names function INTEL:CreateDetectedItems(DetectedGroups, RecceDetecting) self:F({RecceDetecting=RecceDetecting}) - + -- Current time. local Tnow=timer.getAbsTime() - + for groupname,_group in pairs(DetectedGroups) do local group=_group --Wrapper.Group#GROUP - - + + -- Get contact if already known. local detecteditem=self:GetContactByName(groupname) - + if detecteditem then --- -- Detected item already exists ==> Update data. --- - + detecteditem.Tdetected=Tnow detecteditem.position=group:GetCoordinate() detecteditem.velocity=group:GetVelocityVec3() detecteditem.speed=group:GetVelocityMPS() - - else + + else --- -- Detected item does not exist in our list yet. --- - + -- Create new contact. local item={} --#INTEL.Contact - + item.groupname=groupname item.group=group item.Tdetected=Tnow @@ -751,32 +753,32 @@ function INTEL:CreateDetectedItems(DetectedGroups, RecceDetecting) item.velocity=group:GetVelocityVec3() item.speed=group:GetVelocityMPS() item.recce=RecceDetecting[groupname] - - -- Debug info. + item.isground = group:IsGround() or false + item.isship = group:IsShip() or false self:T(string.format("%s group detect by %s/%s", groupname, RecceDetecting[groupname] or "unknown", item.recce or "unknown")) - - -- Add contact to table. + + -- Add contact to table. self:AddContact(item) - + -- Trigger new contact event. self:NewContact(item) end - + end - + -- Now check if there some groups could not be detected any more. for i=#self.Contacts,1,-1 do local item=self.Contacts[i] --#INTEL.Contact - + -- Check if deltaT>Tforget. We dont want quick oscillations between detected and undetected states. if self:_CheckContactLost(item) then - + -- Trigger LostContact event. This also adds the contact to the self.ContactsLost table. self:LostContact(item) - + -- Remove contact from table. self:RemoveContact(item) - + end end @@ -798,32 +800,32 @@ end function INTEL:GetDetectedUnits(Unit, DetectedUnits, RecceDetecting, DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) -- Get detected DCS units. - local detectedtargets=Unit:GetDetectedTargets(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 -- DCS#Object -- NOTE: Got an object that exists but when trying UNIT:Find() the DCS getName() function failed. ID of the object was 5,000,031 if DetectedObject and DetectedObject:isExist() and DetectedObject.id_<50000000 then - + -- Protected call to get the name of the object. 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 DetectedUnits[name]=unit RecceDetecting[name]=reccename self:T(string.format("Unit %s detect by %s", name, reccename)) end - + else -- Warning! self:T(self.lid..string.format("WARNING: Could not get name of detected object ID=%s! Detected by %s", DetectedObject.id_, reccename)) @@ -845,7 +847,7 @@ end -- @param #INTEL.Contact Contact Detected contact. function INTEL:onafterNewContact(From, Event, To, Contact) self:F(self.lid..string.format("NEW contact %s", Contact.groupname)) - table.insert(self.ContactsUnknown, Contact) + table.insert(self.ContactsUnknown, Contact) end --- On after "LostContact" event. @@ -920,11 +922,11 @@ function INTEL:RemoveContact(Contact) for i,_contact in pairs(self.Contacts) do local contact=_contact --#INTEL.Contact - + if contact.groupname==Contact.groupname then table.remove(self.Contacts, i) end - + end end @@ -942,7 +944,7 @@ function INTEL:_CheckContactLost(Contact) -- Time since last detected. local dT=timer.getAbsTime()-Contact.Tdetected - + local dTforget=self.dTforget if Contact.category==Group.Category.GROUND then dTforget=60*60*2 -- 2 hours @@ -955,13 +957,13 @@ function INTEL:_CheckContactLost(Contact) elseif Contact.category==Group.Category.TRAIN then dTforget=60*60 -- 1 hour end - + if dT>dTforget then return true else return false end - + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -969,7 +971,7 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- [Internal] Paint picture of the battle field. Does Cluster analysis and updates clusters. Sets markers if markers are enabled. --- @param #INTEL self +-- @param #INTEL self function INTEL:PaintPicture() -- First remove all lost contacts from clusters. @@ -1001,64 +1003,64 @@ function INTEL:PaintPicture() self.Clusters = ClusterSet -- update positions self:_UpdateClusterPositions() - + for _,_contact in pairs(self.Contacts) do local contact=_contact --#INTEL.Contact self:T(string.format("Paint Picture: checking for %s",contact.groupname)) -- Check if this contact is in any cluster. local isincluster=self:CheckContactInClusters(contact) - + -- Get the current cluster (if any) this contact belongs to. local currentcluster=self:GetClusterOfContact(contact) - + if currentcluster then --self:I(string.format("Paint Picture: %s has current cluster",contact.groupname)) --- -- Contact is currently part of a cluster. --- - + -- Check if the contact is still connected to the cluster. local isconnected=self:IsContactConnectedToCluster(contact, currentcluster) - + if (not isconnected) and (currentcluster.size > 1) then --self:I(string.format("Paint Picture: %s has LOST current cluster",contact.groupname)) local cluster=self:IsContactPartOfAnyClusters(contact) - + if cluster then self:AddContactToCluster(contact, cluster) else - + local newcluster=self:CreateCluster(contact.position) self:AddContactToCluster(contact, newcluster) self:NewCluster(contact, newcluster) end - + end - - + + else - + --- -- Contact is not in any cluster yet. --- --self:I(string.format("Paint Picture: %s has NO current cluster",contact.groupname)) local cluster=self:IsContactPartOfAnyClusters(contact) - + if cluster then self:AddContactToCluster(contact, cluster) else - + local newcluster=self:CreateCluster(contact.position) self:AddContactToCluster(contact, newcluster) self:NewCluster(contact, newcluster) end - - end - - end - - + end + + end + + + -- Update F10 marker text if cluster has changed. if self.clustermarkers then for _,_cluster in pairs(self.Clusters) do @@ -1074,24 +1076,24 @@ end --- Create a new cluster. -- @param #INTEL self -- @param Core.Point#COORDINATE coordinate The coordinate of the cluster. --- @return #INTEL.Cluster cluster The cluster. +-- @return #INTEL.Cluster cluster The cluster. function INTEL:CreateCluster(coordinate) -- Create new cluster local cluster={} --#INTEL.Cluster - + cluster.index=self.clustercounter - cluster.coordinate=coordinate + cluster.coordinate=coordinate cluster.threatlevelSum=0 cluster.threatlevelMax=0 cluster.size=0 cluster.Contacts={} - + -- Add cluster. table.insert(self.Clusters, cluster) - + -- Increase counter. - self.clustercounter=self.clustercounter+1 + self.clustercounter=self.clustercounter+1 return cluster end @@ -1099,16 +1101,16 @@ end --- Add a contact to the cluster. -- @param #INTEL self -- @param #INTEL.Contact contact The contact. --- @param #INTEL.Cluster cluster The cluster. +-- @param #INTEL.Cluster cluster The cluster. function INTEL:AddContactToCluster(contact, cluster) if contact and cluster then - + -- Add neighbour to cluster contacts. table.insert(cluster.Contacts, contact) - + cluster.threatlevelSum=cluster.threatlevelSum+contact.threatlevel - + cluster.size=cluster.size+1 end @@ -1117,26 +1119,26 @@ end --- Remove a contact from a cluster. -- @param #INTEL self -- @param #INTEL.Contact contact The contact. --- @param #INTEL.Cluster cluster The cluster. +-- @param #INTEL.Cluster cluster The cluster. function INTEL:RemoveContactFromCluster(contact, cluster) if contact and cluster then - + for i,_contact in pairs(cluster.Contacts) do local Contact=_contact --#INTEL.Contact - + if Contact.groupname==contact.groupname then - + cluster.threatlevelSum=cluster.threatlevelSum-contact.threatlevel cluster.size=cluster.size-1 - + table.remove(cluster.Contacts, i) - + return end - + end - + end end @@ -1144,16 +1146,16 @@ end --- Calculate cluster threat level sum. -- @param #INTEL self -- @param #INTEL.Cluster cluster The cluster of contacts. --- @return #number Sum of all threat levels of all groups in the cluster. +-- @return #number Sum of all threat levels of all groups in the cluster. function INTEL:CalcClusterThreatlevelSum(cluster) local threatlevel=0 - + for _,_contact in pairs(cluster.Contacts) do local contact=_contact --#INTEL.Contact - + threatlevel=threatlevel+contact.threatlevel - + end cluster.threatlevelSum = threatlevel return threatlevel @@ -1162,10 +1164,10 @@ end --- Calculate cluster threat level average. -- @param #INTEL self -- @param #INTEL.Cluster cluster The cluster of contacts. --- @return #number Average of all threat levels of all groups in the cluster. +-- @return #number Average of all threat levels of all groups in the cluster. function INTEL:CalcClusterThreatlevelAverage(cluster) - local threatlevel=self:CalcClusterThreatlevelSum(cluster) + local threatlevel=self:CalcClusterThreatlevelSum(cluster) threatlevel=threatlevel/cluster.size cluster.threatlevelAve = threatlevel return threatlevel @@ -1174,19 +1176,19 @@ end --- Calculate max cluster threat level. -- @param #INTEL self -- @param #INTEL.Cluster cluster The cluster of contacts. --- @return #number Max threat levels of all groups in the cluster. +-- @return #number Max threat levels of all groups in the cluster. function INTEL:CalcClusterThreatlevelMax(cluster) local threatlevel=0 - + for _,_contact in pairs(cluster.Contacts) do - + local contact=_contact --#INTEL.Contact - + if contact.threatlevel>threatlevel then threatlevel=contact.threatlevel end - + end cluster.threatlevelMax = threatlevel return threatlevel @@ -1195,7 +1197,7 @@ end --- Calculate cluster heading. -- @param #INTEL self -- @param #INTEL.Cluster cluster The cluster of contacts. --- @return #number Heading average of all groups in the cluster. +-- @return #number Heading average of all groups in the cluster. function INTEL:CalcClusterDirection(cluster) local direction = 0 @@ -1206,15 +1208,15 @@ function INTEL:CalcClusterDirection(cluster) direction = direction + group:GetHeading() n=n+1 end - end + end return math.floor(direction / n) - + end --- Calculate cluster speed. -- @param #INTEL self -- @param #INTEL.Cluster cluster The cluster of contacts. --- @return #number Speed average of all groups in the cluster in MPS. +-- @return #number Speed average of all groups in the cluster in MPS. function INTEL:CalcClusterSpeed(cluster) local velocity = 0 @@ -1225,9 +1227,9 @@ function INTEL:CalcClusterSpeed(cluster) velocity = velocity + group:GetVelocityMPS() n=n+1 end - end + end return math.floor(velocity / n) - + end --- Calculate cluster future position after given seconds. @@ -1255,15 +1257,15 @@ end --- Check if contact is in any known cluster. -- @param #INTEL self -- @param #INTEL.Contact contact The contact. --- @return #boolean If true, contact is in clusters +-- @return #boolean If true, contact is in clusters function INTEL:CheckContactInClusters(contact) for _,_cluster in pairs(self.Clusters) do local cluster=_cluster --#INTEL.Cluster - + for _,_contact in pairs(cluster.Contacts) do local Contact=_contact --#INTEL.Contact - + if Contact.groupname==contact.groupname then return true end @@ -1282,21 +1284,21 @@ function INTEL:IsContactConnectedToCluster(contact, cluster) for _,_contact in pairs(cluster.Contacts) do local Contact=_contact --#INTEL.Contact - + if Contact.groupname~=contact.groupname then - + --local dist=Contact.position:Get2DDistance(contact.position) local dist=Contact.position:DistanceFromPointVec2(contact.position) - + local radius = self.clusterradius or 15 if dist1000 then return true else @@ -1430,25 +1432,25 @@ function INTEL:UpdateClusterMarker(cluster) cluster.marker=MARKER:New(cluster.coordinate, text):ToBlue() else cluster.marker=MARKER:New(cluster.coordinate, text):ToNeutral() - end + end else - + local refresh=false - + if cluster.marker.text~=text then cluster.marker.text=text refresh=true end - + if cluster.marker.coordinate~=cluster.coordinate then cluster.marker.coordinate=cluster.coordinate refresh=true end - + if refresh then cluster.marker:Refresh() end - + end return self @@ -1469,7 +1471,7 @@ end -- * Overcome limitations of (non-available) datalinks between ground radars -- * Detect and track contacts consistently across INTEL instances -- * Use FSM events to link functionality into your scripts --- * Easy setup +-- * Easy setup -- --- === -- @@ -1512,12 +1514,12 @@ INTEL_DLINK.version = "0.0.1" -- @param #string Alias (optional) Name of this instance. Default "SPECTRE" -- @param #number Interval (optional) When to query #INTEL objects for detected items (default 20 seconds). -- @param #number Cachetime (optional) How long to cache detected items (default 300 seconds). --- @usage Use #INTEL_DLINK if you want to merge data from a number of #INTEL objects into one. This might be useful to simulate a +-- @usage Use #INTEL_DLINK if you want to merge data from a number of #INTEL objects into one. This might be useful to simulate a -- Data Link, e.g. for Russian-tech based EWR, realising a Star Topology @{https://en.wikipedia.org/wiki/Network_topology#Star} --- in a basic setup. It will collect the contacts and clusters from the #INTEL objects. +-- in a basic setup. It will collect the contacts and clusters from the #INTEL objects. -- Contact duplicates are removed. Clusters might contain duplicates (Might fix that later, WIP). -- --- Basic setup: +-- Basic setup: -- local datalink = INTEL_DLINK:New({myintel1,myintel2}), "FSB", 20, 300) -- datalink:__Start(2) -- @@ -1529,36 +1531,36 @@ INTEL_DLINK.version = "0.0.1" -- datalink:GetClusterTable() -- #table of #INTEL.Cluster clusters. -- datalink:GetDetectedItemCoordinates() -- #table of contact coordinates, to be compatible with @{Functional.Detection#DETECTION}. -- --- Gather data with the event function: --- function datalink:OnAfterCollected(From, Event, To, Contacts, Clusters) +-- Gather data with the event function: +-- function datalink:OnAfterCollected(From, Event, To, Contacts, Clusters) -- ... ... -- end --- +-- function INTEL_DLINK:New(Intels, Alias, Interval, Cachetime) -- Inherit everything from FSM class. local self=BASE:Inherit(self, FSM:New()) -- #INTEL - + self.intels = Intels or {} self.contacts = {} self.clusters = {} self.contactcoords = {} - + -- Set alias. if Alias then self.alias=tostring(Alias) else self.alias="SPECTRE" end - + -- Cache time self.cachetime = Cachetime or 300 - + -- Interval self.interval = Interval or 20 - + -- Set some string id for output to DCS.log file. self.lid=string.format("INTEL_DLINK %s | ", self.alias) - + -- Start State. self:SetStartState("Stopped") @@ -1568,7 +1570,7 @@ function INTEL_DLINK:New(Intels, Alias, Interval, Cachetime) self:AddTransition("*", "Collect", "*") -- Collect data. self:AddTransition("*", "Collected", "*") -- Collection of data done. self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. - + ---------------------------------------------------------------------------------------------- -- Pseudo Functions ---------------------------------------------------------------------------------------------- @@ -1597,7 +1599,7 @@ function INTEL_DLINK:New(Intels, Alias, Interval, Cachetime) -- @function [parent=#INTEL_DLINK] __Status -- @param #INTEL_DLINK self -- @param #number delay Delay in seconds. - + --- On After "Collected" event. Data tables have been refreshed. -- @function [parent=#INTEL_DLINK] OnAfterCollected -- @param #INTEL_DLINK self @@ -1606,7 +1608,7 @@ function INTEL_DLINK:New(Intels, Alias, Interval, Cachetime) -- @param #string To To state. -- @param #table Contacts Table of #INTEL.Contact Contacts. -- @param #table Clusters Table of #INTEL.Cluster Clusters. - + return self end ---------------------------------------------------------------------------------------------- @@ -1703,7 +1705,7 @@ function INTEL_DLINK:onbeforeCollect(From, Event, To) self:__Collected(1, contacttable, newclusters) -- make table available via FSM Event -- schedule next round local interv = self.interval * -1 - self:__Collect(interv) + self:__Collect(interv) return self end diff --git a/Moose Development/Moose/Sound/Radio.lua b/Moose Development/Moose/Sound/Radio.lua index 872ad6e26..40343e2ac 100644 --- a/Moose Development/Moose/Sound/Radio.lua +++ b/Moose Development/Moose/Sound/Radio.lua @@ -1,28 +1,28 @@ --- **Sound** - Radio transmissions. --- +-- -- === --- +-- -- ## Features: --- +-- -- * Provide radio functionality to broadcast radio transmissions. --- +-- -- What are radio communications in DCS? --- +-- -- * Radio transmissions consist of **sound files** that are broadcasted on a specific **frequency** (e.g. 115MHz) and **modulation** (e.g. AM), -- * They can be **subtitled** for a specific **duration**, the **power** in Watts of the transmiter's antenna can be set, and the transmission can be **looped**. --- +-- -- How to supply DCS my own Sound Files? --- +-- -- * Your sound files need to be encoded in **.ogg** or .wav, -- * Your sound files should be **as tiny as possible**. It is suggested you encode in .ogg with low bitrate and sampling settings, -- * They need to be added in .\l10n\DEFAULT\ in you .miz file (wich can be decompressed like a .zip file), -- * For simplicity sake, you can **let DCS' Mission Editor add the file** itself, by creating a new Trigger with the action "Sound to Country", and choosing your sound file and a country you don't use in your mission. --- +-- -- Due to weird DCS quirks, **radio communications behave differently** if sent by a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP} or by any other @{Wrapper.Positionable#POSITIONABLE} --- +-- -- * If the transmitter is a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}, DCS will set the power of the transmission automatically, -- * If the transmitter is any other @{Wrapper.Positionable#POSITIONABLE}, the transmisison can't be subtitled or looped. --- +-- -- Note that obviously, the **frequency** and the **modulation** of the transmission are important only if the players are piloting an **Advanced System Modelling** enabled aircraft, -- like the A10C or the Mirage 2000C. They will **hear the transmission** if they are tuned on the **right frequency and modulation** (and if they are close enough - more on that below). -- If an FC3 aircraft is used, it will **hear every communication, whatever the frequency and the modulation** is set to. The same is true for TACAN beacons. If your aircraft isn't compatible, @@ -37,41 +37,41 @@ --- *It's not true I had nothing on, I had the radio on.* -- Marilyn Monroe --- +-- -- # RADIO usage --- +-- -- There are 3 steps to a successful radio transmission. --- +-- -- * First, you need to **"add a @{#RADIO} object** to your @{Wrapper.Positionable#POSITIONABLE}. This is done using the @{Wrapper.Positionable#POSITIONABLE.GetRadio}() function, -- * Then, you will **set the relevant parameters** to the transmission (see below), -- * When done, you can actually **broadcast the transmission** (i.e. play the sound) with the @{RADIO.Broadcast}() function. --- +-- -- Methods to set relevant parameters for both a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP} or any other @{Wrapper.Positionable#POSITIONABLE} --- +-- -- * @{#RADIO.SetFileName}() : Sets the file name of your sound file (e.g. "Noise.ogg"), -- * @{#RADIO.SetFrequency}() : Sets the frequency of your transmission. -- * @{#RADIO.SetModulation}() : Sets the modulation of your transmission. -- * @{#RADIO.SetLoop}() : Choose if you want the transmission to be looped. If you need your transmission to be looped, you might need a @{#BEACON} instead... --- +-- -- Additional Methods to set relevant parameters if the transmitter is a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP} --- +-- -- * @{#RADIO.SetSubtitle}() : Set both the subtitle and its duration, -- * @{#RADIO.NewUnitTransmission}() : Shortcut to set all the relevant parameters in one method call --- +-- -- Additional Methods to set relevant parameters if the transmitter is any other @{Wrapper.Positionable#POSITIONABLE} --- +-- -- * @{#RADIO.SetPower}() : Sets the power of the antenna in Watts -- * @{#RADIO.NewGenericTransmission}() : Shortcut to set all the relevant parameters in one method call --- +-- -- What is this power thing? --- +-- -- * If your transmission is sent by a @{Wrapper.Positionable#POSITIONABLE} other than a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}, you can set the power of the antenna, -- * Otherwise, DCS sets it automatically, depending on what's available on your Unit, -- * If the player gets **too far** from the transmitter, or if the antenna is **too weak**, the transmission will **fade** and **become noisyer**, -- * This an automated DCS calculation you have no say on, -- * For reference, a standard VOR station has a 100 W antenna, a standard AA TACAN has a 120 W antenna, and civilian ATC's antenna usually range between 300 and 500 W, --- * Note that if the transmission has a subtitle, it will be readable, regardless of the quality of the transmission. --- +-- * Note that if the transmission has a subtitle, it will be readable, regardless of the quality of the transmission. +-- -- @type RADIO -- @field Wrapper.Controllable#CONTROLLABLE Positionable The @{#CONTROLLABLE} that will transmit the radio calls. -- @field #string FileName Name of the sound file played. @@ -105,12 +105,12 @@ function RADIO:New(Positionable) -- Inherit base local self = BASE:Inherit( self, BASE:New() ) -- Core.Radio#RADIO self:F(Positionable) - + if Positionable:GetPointVec2() then -- It's stupid, but the only way I found to make sure positionable is valid self.Positionable = Positionable return self end - + self:E({error="The passed positionable is invalid, no RADIO created!", positionable=Positionable}) return nil end @@ -137,19 +137,19 @@ end -- @return #RADIO self 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 @@ -157,39 +157,39 @@ end --- Set the frequency for the radio transmission. -- If the transmitting positionable is a unit or group, this also set the command "SetFrequency" with the defined frequency and modulation. -- @param #RADIO self --- @param #number Frequency Frequency in MHz. Ranges allowed for radio transmissions in DCS : 30-87.995 / 108-173.995 / 225-399.975MHz. +-- @param #number Frequency Frequency in MHz. -- @return #RADIO self function RADIO:SetFrequency(Frequency) self:F2(Frequency) - + if type(Frequency) == "number" then - + -- If frequency is in range - if (Frequency >= 30 and Frequency <= 87.995) or (Frequency >= 108 and Frequency <= 173.995) or (Frequency >= 225 and Frequency <= 399.975) then - + --if (Frequency >= 30 and Frequency <= 87.995) or (Frequency >= 108 and Frequency <= 173.995) or (Frequency >= 225 and Frequency <= 399.975) then + -- Convert frequency from MHz to Hz self.Frequency = Frequency * 1000000 - + -- If the RADIO is attached to a UNIT or a GROUP, we need to send the DCS Command "SetFrequency" to change the UNIT or GROUP frequency if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then - + local commandSetFrequency={ id = "SetFrequency", params = { frequency = self.Frequency, modulation = self.Modulation, } - } - + } + self:T2(commandSetFrequency) self.Positionable:SetCommand(commandSetFrequency) end - + return self - end + --end end - - self:E({"Frequency is outside of DCS Frequency ranges (30-80, 108-152, 225-400). Frequency unchanged.", Frequency}) + + self:E({"Frequency is not a number. Frequency unchanged.", Frequency}) return self end @@ -215,13 +215,13 @@ end -- @return #RADIO self function RADIO:SetPower(Power) self:F2(Power) - + if type(Power) == "number" then self.Power = math.floor(math.abs(Power)) --TODO Find what is the maximum power allowed by DCS and limit power to that else self:E({"Power is invalid. Power unchanged.", self.Power}) end - + return self end @@ -249,7 +249,7 @@ end -- -- create the broadcaster and attaches it a RADIO -- local MyUnit = UNIT:FindByName("MyUnit") -- local MyUnitRadio = MyUnit:GetRadio() --- +-- -- -- add a subtitle for the next transmission, which will be up for 10s -- MyUnitRadio:SetSubtitle("My Subtitle, 10) function RADIO:SetSubtitle(Subtitle, SubtitleDuration) @@ -264,14 +264,14 @@ function RADIO:SetSubtitle(Subtitle, SubtitleDuration) self.SubtitleDuration = SubtitleDuration else self.SubtitleDuration = 0 - self:E({"SubtitleDuration is invalid. SubtitleDuration reset.", self.SubtitleDuration}) + self:E({"SubtitleDuration is invalid. SubtitleDuration reset.", self.SubtitleDuration}) end return self end --- Create a new transmission, that is to say, populate the RADIO with relevant data -- In this function the data is especially relevant if the broadcaster is anything but a UNIT or a GROUP, --- but it will work with a UNIT or a GROUP anyway. +-- but it will work with a UNIT or a GROUP anyway. -- Only the #RADIO and the Filename are mandatory -- @param #RADIO self -- @param #string FileName Name of the sound file that will be transmitted. @@ -281,20 +281,20 @@ end -- @return #RADIO self 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 --- Create a new transmission, that is to say, populate the RADIO with relevant data -- In this function the data is especially relevant if the broadcaster is a UNIT or a GROUP, --- but it will work for any @{Wrapper.Positionable#POSITIONABLE}. +-- but it will work for any @{Wrapper.Positionable#POSITIONABLE}. -- Only the RADIO and the Filename are mandatory. -- @param #RADIO self -- @param #string FileName Name of sound file. @@ -316,20 +316,20 @@ function RADIO:NewUnitTransmission(FileName, Subtitle, SubtitleDuration, Frequen end -- Set frequency. - if Frequency then + if Frequency then self:SetFrequency(Frequency) end - + -- Set subtitle. if Subtitle then self:SetSubtitle(Subtitle, SubtitleDuration or 0) end - + -- Set Looping. - if Loop then + if Loop then self:SetLoop(Loop) end - + return self end @@ -346,7 +346,7 @@ end -- @return #RADIO self function RADIO:Broadcast(viatrigger) self:F({viatrigger=viatrigger}) - + -- If the POSITIONABLE is actually a UNIT or a GROUP, use the more complicated DCS command system. if (self.Positionable.ClassName=="UNIT" or self.Positionable.ClassName=="GROUP") and (not viatrigger) then self:T("Broadcasting from a UNIT or a GROUP") @@ -359,7 +359,7 @@ function RADIO:Broadcast(viatrigger) subtitle = self.Subtitle, loop = self.Loop, }} - + self:T3(commandTransmitMessage) self.Positionable:SetCommand(commandTransmitMessage) else @@ -368,7 +368,7 @@ function RADIO:Broadcast(viatrigger) 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 @@ -380,11 +380,11 @@ end -- @return #RADIO self function RADIO:StopBroadcast() self:F() - -- If the POSITIONABLE is a UNIT or a GROUP, stop the transmission with the DCS "StopTransmission" command + -- If the POSITIONABLE is a UNIT or a GROUP, stop the transmission with the DCS "StopTransmission" command if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then - + local commandStopTransmission={id="StopTransmission", params={}} - + self.Positionable:SetCommand(commandStopTransmission) else -- Else, we use the appropriate singleton funciton diff --git a/Moose Development/Moose/Tasking/Task_Cargo_Dispatcher.lua b/Moose Development/Moose/Tasking/Task_Cargo_Dispatcher.lua index 6833f197c..e1018bdf0 100644 --- a/Moose Development/Moose/Tasking/Task_Cargo_Dispatcher.lua +++ b/Moose Development/Moose/Tasking/Task_Cargo_Dispatcher.lua @@ -683,6 +683,7 @@ do -- TASK_CARGO_DISPATCHER -- If no TaskPrefix is given, then "Transport" will be used as the prefix. -- @param Core.SetCargo#SET_CARGO SetCargo The SetCargo to be transported. -- @param #string Briefing The briefing of the task transport to be shown to the player. + -- @param #boolean Silent If true don't send a message that a new task is available. -- @return Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT -- @usage -- @@ -705,10 +706,12 @@ do -- TASK_CARGO_DISPATCHER -- -- Here we set a TransportDeployZone. We use the WorkplaceTask as the reference, and provide a ZONE object. -- TaskDispatcher:SetTransportDeployZone( WorkplaceTask, ZONE:New( "Workplace" ) ) -- - function TASK_CARGO_DISPATCHER:AddTransportTask( TaskPrefix, SetCargo, Briefing ) + function TASK_CARGO_DISPATCHER:AddTransportTask( TaskPrefix, SetCargo, Briefing, Silent ) self.TransportCount = self.TransportCount + 1 + local verbose = Silent and true + local TaskName = string.format( ( TaskPrefix or "Transport" ) .. ".%03d", self.TransportCount ) self.Transport[TaskName] = {} @@ -717,7 +720,7 @@ do -- TASK_CARGO_DISPATCHER self.Transport[TaskName].Task = nil self.Transport[TaskName].TaskPrefix = TaskPrefix - self:ManageTasks() + self:ManageTasks(verbose) return self.Transport[TaskName] and self.Transport[TaskName].Task end @@ -785,10 +788,11 @@ do -- TASK_CARGO_DISPATCHER --- Assigns tasks to the @{Core.Set#SET_GROUP}. -- @param #TASK_CARGO_DISPATCHER self + -- @param #boolean Silent Announce new task (nil/false) or not (true). -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop. - function TASK_CARGO_DISPATCHER:ManageTasks() + function TASK_CARGO_DISPATCHER:ManageTasks(Silent) self:F() - + local verbose = Silent and true local AreaMsg = {} local TaskMsg = {} local ChangeMsg = {} @@ -897,7 +901,7 @@ do -- TASK_CARGO_DISPATCHER local TaskText = TaskReport:Text(", ") for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do - if ( not Mission:IsGroupAssigned(TaskGroup) ) and TaskText ~= "" then + if ( not Mission:IsGroupAssigned(TaskGroup) ) and TaskText ~= "" and not verbose then Mission:GetCommandCenter():MessageToGroup( string.format( "%s has tasks %s. Subscribe to a task using the radio menu.", Mission:GetShortText(), TaskText ), TaskGroup ) end end diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 0e25e3751..655e48b1f 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -1611,7 +1611,7 @@ function UTILS.GetOSTime() end --- Shuffle a table accoring to Fisher Yeates algorithm ---@param #table table to be shuffled +--@param #table t Table to be shuffled --@return #table function UTILS.ShuffleTable(t) if t == nil or type(t) ~= "table" then @@ -1640,7 +1640,7 @@ function UTILS.IsLoadingDoorOpen( unit_name ) if unit ~= nil then local type_name = unit:getTypeName() - if type_name == "Mi-8MT" and unit:getDrawArgumentValue(38) == 1 or unit:getDrawArgumentValue(86) == 1 or unit:getDrawArgumentValue(250) == 1 then + 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") ret_val = true end @@ -1660,6 +1660,21 @@ function UTILS.IsLoadingDoorOpen( unit_name ) ret_val = 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") + ret_val = 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") + ret_val = true + end + + if string.find(type_name, "Hercules") and unit:getDrawArgumentValue(1217) == 1 then + BASE:T(unit_name .. " side door is open") + ret_val = true + end + if ret_val == false then BASE:T(unit_name .. " all doors are closed") end diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 2b9198330..db98fb789 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -1211,7 +1211,7 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, parkingdata=parkingdata or self:GetParkingSpotsTable(terminaltype) -- Get the aircraft size, i.e. it's longest side of x,z. - local aircraft = nil + local aircraft = nil -- fix local problem below local _aircraftsize, ax,ay,az if group and group.ClassName == "GROUP" then aircraft=group:GetUnit(1) diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index ea63c7da0..88bfceac5 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -2573,8 +2573,10 @@ end -- @return #GROUP self function GROUP:SetCommandInvisible(switch) self:F2( self.GroupName ) - local switch = switch or false - local SetInvisible = {id = 'SetInvisible', params = {value = true}} + if switch==nil then + switch=false + end + local SetInvisible = {id = 'SetInvisible', params = {value = switch}} self:SetCommand(SetInvisible) return self end @@ -2585,9 +2587,11 @@ end -- @return #GROUP self function GROUP:SetCommandImmortal(switch) self:F2( self.GroupName ) - local switch = switch or false - local SetInvisible = {id = 'SetImmortal', params = {value = true}} - self:SetCommand(SetInvisible) + if switch==nil then + switch=false + end + local SetImmortal = {id = 'SetImmortal', params = {value = switch}} + self:SetCommand(SetImmortal) return self end diff --git a/Moose Setup/Moose.files b/Moose Setup/Moose.files index 113667df3..9608179cf 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -69,6 +69,7 @@ Functional/Warehouse.lua Functional/Fox.lua Functional/Mantis.lua Functional/Shorad.lua +Functional/Autolase.lua Ops/Airboss.lua Ops/RecoveryTanker.lua diff --git a/README.md b/README.md index b622326a8..5a1979522 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,6 @@ MOOSE has a living (chat and video) community of users, beta testers and contrib Kind regards, -FlightControl (FC) +The Moose Team