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 ebdedcfba..49e4e9e30 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/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 aee683f96..f1d14a23b 100644 --- a/Moose Development/Moose/Ops/Intelligence.lua +++ b/Moose Development/Moose/Ops/Intelligence.lua @@ -135,7 +135,7 @@ INTEL = { --- INTEL class version. -- @field #string version -INTEL.version="0.2.6" +INTEL.version="0.2.7" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list @@ -212,6 +212,8 @@ function INTEL:New(DetectionSet, Coalition, Alias) self.DetectRWR = true self.DetectDLINK = true + self.statusupdate = -60 + -- Set some string id for output to DCS.log file. self.lid=string.format("INTEL %s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown") @@ -585,7 +587,7 @@ function INTEL:onafterStatus(From, Event, To) self:I(self.lid..text) end - self:__Status(-60) + self:__Status(self.statusupdate) end @@ -748,6 +750,8 @@ function INTEL:CreateDetectedItems(DetectedGroups, RecceDetecting) item.velocity=group:GetVelocityVec3() item.speed=group:GetVelocityMPS() item.recce=RecceDetecting[groupname] + item.isground = group:IsGround() or false + item.isship = group:IsShip() or false self:T(string.format("%s group detect by %s/%s", groupname, RecceDetecting[groupname] or "unknown", item.recce or "unknown")) -- Add contact to table. self:AddContact(item) @@ -792,8 +796,8 @@ 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 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 8311f403a..b4a1cc39e 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -1189,7 +1189,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 077e75168..4f0e34d30 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