------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- **Functional** - Control artillery units. -- -- ![Banner Image](..\Presentations\ARTY\Artillery_Main.png) -- -- ==== -- -- The ARTY class can be used to easily assign targets for artillery units. Multiple targets can be assigned. -- -- -- ==== -- -- # Demo Missions -- -- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) -- -- ==== -- -- # YouTube Channel -- -- ### [MOOSE YouTube Channel](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl1jirWIo4t4YxqN-HxjqRkL) -- -- === -- -- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)** -- -- ### Contributions: **Sven van de Velde ([FlightControl](https://forums.eagle.ru/member.php?u=89536))** -- -- ==== -- @module Arty ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- ARTY class -- @type ARTY -- @field #string ClassName Name of the class. -- @field #boolean Debug Write Debug messages to DCS log file and send Debug messages to all players. -- @field #table targets Targets assigned. -- @field #table currentTarget Holds the current target, if there is one assigned. -- @field #number Nammo0 Initial amount total ammunition (shells+rockets+missiles) of the whole group. -- @field #number Nshells0 Initial amount of shells of the whole group. -- @field #number Nrockets0 Initial amount of rockets of the whole group. -- @field #number Nmissiles0 Initial amount of missiles of the whole group. -- @field Core.Scheduler#SCHEDULER TargetQueueSched Scheduler updating the target queue and calling OpenFire event. -- @field #number TargetQueueUpdate Interval between updates of the target queue. -- @field Core.Scheduler#SCHEDULER CheckRearmedSched Scheduler checking whether reaming of the ARTY group is complete. -- @field #table DCSdesc DCS descriptors of the ARTY group. -- @field #string Type Type of the ARTY group. -- @field #number IniGroupStrength Inital number of units in the ARTY group. -- @field #boolean IsArtillery If true, ARTY group has attribute "Artillery". -- @field #number Speed Max speed of ARTY group. -- @field Wrapper.Unit#UNIT RearmingUnit Unit designated to rearm the ARTY group. -- @field #boolean report Arty group sends messages about their current state or target to its coaliton. -- @field #table ammoshells Table holding names of the shell types which are included when counting the ammo. Default is {"weapons.shells"} which include most shells. -- @field #table ammorockets Table holding names of the rocket types which are included when counting the ammo. Default is {"weapons.nurs"} which includes most unguided rockets. -- @field #table ammomissiles Table holding names of the missile types which are included when counting the ammo. Default is {"weapons.missiles"} which includes some guided missiles. -- @field #number Nshots Number of shots fired on current target. -- @field #number WaitForShotTime Max time in seconds to wait until fist shot event occurs after target is assigned. If time is passed without shot, the target is deleted. -- @extends Core.Fsm#FSM_CONTROLLABLE -- ---# ARTY class, extends @{Core.Fsm#FSM_CONTROLLABLE} -- Artillery class.. -- -- ## Target aquisition... -- -- ![Process](..\Presentations\ARTY\Artillery_Process.png) -- -- The arty process can be described as follows. -- -- ### Submenu -- -- @field #ARTY ARTY={ ClassName = "ARTY", Debug = true, targets = {}, currentTarget = nil, Nammo0=0, Nshells0=0, Nrockets0=0, Nmissiles0=0, TargetQueueSched=nil, TargetQueueUpdate=5, CheckRearmedSched=nil, DCSdesc=nil, Type=nil, IniGroupStrength=0, IsArtillery=nil, RearmingUnit=nil, report=true, ammoshells={"weapons.shells"}, ammorockets={"weapons.nurs"}, ammomissiles={"weapons.missiles"}, Nshots=0, WaitForShotTime=300, } --- Weapong type ID. http://wiki.hoggit.us/view/DCS_enum_weapon_flag -- @list WeaponType ARTY.WeaponType={ Auto=1073741822, UnguidedAny=805339120, UnguidedCannon=805306368, UnguidedRockets=30720, GuidedAny=268402702, GuidedMissile=268402688, CruiseMissile=2097152, } --- Some ID to identify who we are in output of the DCS.log file. -- @field #string id ARTY.id="ARTY | " --- Range script version. -- @field #number version ARTY.version="0.3.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list: -- DONE: Delete targets from queue user function. -- TODO: Delete entire target queue user function. -- TODO: Add weapon types. -- DONE: Add user defined rearm weapon types. -- TODO: Check if target is in range. Maybe this requires a data base with the ranges of all arty units. Pfff... -- TODO: Make ARTY move to reaming position. -- TODO: Check that right reaming vehicle is specified. Blue M818, Red Ural-375. Are there more? -- TODO: Check if ARTY group is still alive. -- TODO: Handle dead events. -- TODO: Abort firing task if no shooting event occured with 5(?) minutes. Something went wrong then. Min/max range for example. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Creates a new ARTY object. -- @param #ARTY self -- @param Wrapper.Group#GROUP group The GROUP object for which artillery tasks should be assigned. -- @return #ARTY ARTY object. -- @return nil If group does not exist or is not a ground group. function ARTY:New(group) BASE:F2(group) -- Inherits from FSM_CONTROLLABLE local self=BASE:Inherit(self, FSM_CONTROLLABLE:New()) -- #ARTY -- Check that group is present. if group then self:T(ARTY.id..string.format("ARTY script version %s. Added group %s.", ARTY.version, group:GetName())) else self:E(ARTY.id.."ERROR: Requested ARTY group does not exist! (Has to be a MOOSE group.)") return nil end -- Check that we actually have a GROUND group. if group:IsGround()==false and group:IsShip()==false then self:E(ARTY.id..string.format("ERROR: ARTY group %s has to be a GROUND or SHIP group!", group:GetName())) return nil end -- Set the controllable for the FSM. self:SetControllable(group) -- Get DCS descriptors of group. local DCSgroup=Group.getByName(group:GetName()) local DCSunit=DCSgroup:getUnit(1) self.DCSdesc=DCSunit:getDesc() -- DCS descriptors. self:T3(ARTY.id.."DCS descriptors for group "..group:GetName()) for id,desc in pairs(self.DCSdesc) do self:T3({id=id, desc=desc}) end -- Set speed to maximum in km/h. self.Speed=self.DCSdesc.speedMax*3.6 -- Displayed name (similar to type name below) self.DisplayName=self.DCSdesc.displayName -- Is this infantry or not. self.IsArtillery=DCSunit:hasAttribute("Artillery") -- Type of group. self.Type=group:GetTypeName() -- Initial group strength. self.IniGroupStrength=#group:GetUnits() -- Set ROE and Alarm State. --self:SetDefaultROE("Free") --self:SetDefaultAlarmState("Auto") -- Transitions self:AddTransition("*", "Start", "CombatReady") self:AddTransition("CombatReady", "OpenFire", "Firing") self:AddTransition("Firing", "OpenFire", "Firing") -- Other target assigned self:AddTransition("Firing", "CeaseFire", "CombatReady") self:AddTransition("*", "Winchester", "OutOfAmmo") self:AddTransition("OutOfAmmo", "Rearm", "Rearming") self:AddTransition("Rearming", "Rearmed", "CombatReady") --self:AddTransition("*", "Dead", "*") return self end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Add a group of target(s) for the ARTY group. -- @param #ARTY self -- @param Wrapper.Group#GROUP group Group of targets. -- @param #number prio (Optional) Priority of target. Number between 1 (high) and 100 (low). Default 50. -- @param #number radius (Optional) Radius. Default is 100 m. -- @param #number nshells (Optional) How many shells (or rockets) are fired on target per engagement. Default 5. -- @param #number maxengage (Optional) How many times a target is engaged. Default 9999. -- @param #string time Day time at which the target should be engaged. Passed as a string in format "08:13:45". Current task will be canceled. -- @param #number weapontype Type of weapon to be used to attack this target. Default ARTY.WeaponType.Auto. -- @return #string Name of the target. Can be used for further reference, e.g. deleting the target from the list. -- @usage ARTY:AssignTargetGroup(GROUP:FindByName("Red Target"), 10, 250, 10, 2, "13:25:45") function ARTY:AssignTargetGroup(group, prio, radius, nshells, maxengage, time, weapontype) self:E({group=group, prio=prio, radius=radius, nshells=nshells, maxengage=maxengage, time=time, weapontype=weapontype}) -- Set default values. nshells=nshells or 5 radius=radius or 100 maxengage=maxengage or 9999 prio=prio or 50 prio=math.max( 1, prio) prio=math.min(100, prio) weapontype=weapontype or ARTY.WeaponType.Auto -- Coordinate of target. local coord=group:GetCoordinate() local name=group:GetName() -- Name of target defined my Lat/long in Degree Minute Second format. --local name=coord:ToStringLLDMS() -- Check if the name has already been used for another target. If so, the function returns a new unique name. name=self:_CheckTargetName(name) -- Time in seconds. local _time=self:_ClockToSeconds(time) -- Prepare target array. local _target={name=name, coord=coord, radius=radius, nshells=nshells, engaged=0, underfire=false, prio=prio, maxengage=maxengage, time=_time, weapontype=weapontype} -- Add to table. table.insert(self.targets, _target) -- Clock. local _clock=self:_SecondsToClock(_target.time) -- Debug info. self:T(ARTY.id..string.format("Added target %s, prio=%d, radius=%d, nshells=%d, maxengage=%d, time=%s, weapontype=%d", name, prio, radius, nshells, maxengage, _clock, weapontype)) end --- Assign coordinates of a target for the ARTY group. -- @param #ARTY self -- @param Wrapper.Point#COORDINATE coord Coordinates of the target. -- @param #number prio (Optional) Priority of target. Number between 1 (high) and 100 (low). Default 50. -- @param #number radius (Optional) Radius. Default is 100 m. -- @param #number nshells (Optional) How many shells are fired on target per engagement. Default 5. -- @param #number maxengage (Optional) How many times a target is engaged. Default 9999. -- @return #string targetname Name of the target. function ARTY:AssignTargetCoord(coord, prio, radius, nshells, maxengage) self:E({coord=coord, prio=prio, radius=radius, nshells=nshells, maxengage=maxengage}) -- Set default values. nshells=nshells or 5 radius=radius or 100 maxengage=maxengage or 9999 prio=prio or 50 prio=math.max( 1, prio) prio=math.min(100, prio) -- Coordinate and name. local name=coord:ToStringLLDMS() -- Prepare target array. local _target={name=name, coord=coord, radius=radius, nshells=nshells, engaged=0, underfire=false, prio=prio, maxengage=maxengage} -- Add to table. table.insert(self.targets, _target) -- Debug info. self:T(ARTY.id..string.format("Added target %s, radius=%d, nshells=%d, prio=%d, maxengage=%d.", name, prio, radius, nshells, maxengage)) return name end --- Assign a unit which is responsible for rearming the ARTY group. If the unit is too far away from the ARTY group it will be guided towards the ARTY group. -- @param #ARTY self -- @param Wrapper.Unit#UNIT unit Unit that is supposed to rearm the ARTY group. function ARTY:SetRearmingUnit(unit) self:F({unit=unit}) self.RearmingUnit=unit end --- Delete target from target list. -- @param #ARTY self -- @param #string name Name of the target. function ARTY:RemoveTarget(name) self:F2(name) local id=self:_GetTargetByName(name) if id then table.remove(self.targets, id) end end --- Define shell types that are counted to determine the ammo amount the ARTY group has. -- @param #ARTY self -- @param #table tableofnames Table of shell type names. function ARTY:SetShellTypes(tableofnames) self:F2(tableofnames) self.ammoshells={} for _,_type in pairs(tableofnames) do table.insert(self.ammoshells, _type) end end --- Define rocket types that are counted to determine the ammo amount the ARTY group has. -- @param #ARTY self -- @param #table tableofnames Table of rocket type names. function ARTY:SetRocketTypes(tableofnames) self:F2(tableofnames) self.ammorockets={} for _,_type in pairs(tableofnames) do table.insert(self.ammorockets, _type) end end --- Define missile types that are counted to determine the ammo amount the ARTY group has. -- @param #ARTY self -- @param #table tableofnames Table of rocket type names. function ARTY:SetMissileTypes(tableofnames) self:F2(tableofnames) self.ammomissiles={} for _,_type in pairs(tableofnames) do table.insert(self.ammomissiles, _type) end end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- After "Start" event. Initialized ROE and alarm state. Starts the event handler. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. function ARTY:onafterStart(Controllable, From, Event, To) self:_EventFromTo("onafterStart", Event, From, To) local text=string.format("Started ARTY for group %s.", Controllable:GetName()) MESSAGE:New(text, 10):ToAllIf(self.Debug) -- Set the current ROE and alam state. --self:_SetAlarmState(self.DefaultAlarmState) --self:_SetROE(self.DefaultROE) -- Get Ammo. self.Nammo0, self.Nshells0, self.Nrockets0, self.Nmissiles0=self:_GetAmmo(self.Controllable) local text=string.format("\n******************************************************\n") text=text..string.format("Arty group = %s\n", Controllable:GetName()) text=text..string.format("Artillery attribute = %s\n", tostring(self.IsArtillery)) text=text..string.format("Type = %s\n", self.Type) text=text..string.format("Number of units = %d\n", self.IniGroupStrength) text=text..string.format("Max Speed [km/h] = %d\n", self.Speed) text=text..string.format("Total ammo count = %d\n", self.Nammo0) text=text..string.format("Number of shells = %d\n", self.Nshells0) text=text..string.format("Number of rockets = %d\n", self.Nrockets0) text=text..string.format("Number of missiles = %d\n", self.Nmissiles0) text=text..string.format("******************************************************\n") text=text..string.format("Targets:\n") for _, target in pairs(self.targets) do local _clock=self:_SecondsToClock(target.time) local _weapon=self:_WeaponTypeName(target.weapontype) text=text..string.format("- %s, prio=%3d, radius=%5d, nshells=%4d, maxengage=%3d, time=%s, weapon=%s\n", target.name, target.prio, target.radius, target.nshells, target.maxengage, _clock, _weapon) end text=text..string.format("******************************************************\n") text=text..string.format("Shell types:\n") for _,_type in pairs(self.ammoshells) do text=text..string.format("- %s\n", _type) end text=text..string.format("Rocket types:\n") for _,_type in pairs(self.ammorockets) do text=text..string.format("- %s\n", _type) end text=text..string.format("Missile types:\n") for _,_type in pairs(self.ammomissiles) do text=text..string.format("- %s\n", _type) end text=text..string.format("******************************************************") self:T(ARTY.id..text) -- Add event handler. self:HandleEvent(EVENTS.Shot, self._OnEventShot) self:HandleEvent(EVENTS.Dead, self._OnEventDead) -- Start scheduler to monitor task queue. self.TargetQueueSched=SCHEDULER:New(nil, ARTY._TargetQueue, {self}, 5, self.TargetQueueUpdate) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Eventhandler for shot event. -- @param #ARTY self -- @param Core.Event#EVENTDATA EventData function ARTY:_OnEventShot(EventData) self:F(EventData) -- Weapon data. local _weapon = EventData.Weapon:getTypeName() -- should be the same as Event.WeaponTypeName local _weaponStrArray = self:_split(_weapon,"%.") local _weaponName = _weaponStrArray[#_weaponStrArray] -- Debug info. self:T3(ARTY.id.."EVENT SHOT: Ini unit = "..EventData.IniUnitName) self:T3(ARTY.id.."EVENT SHOT: Ini group = "..EventData.IniGroupName) self:T3(ARTY.id.."EVENT SHOT: Weapon type = ".._weapon) self:T3(ARTY.id.."EVENT SHOT: Weapon name = ".._weaponName) local group = EventData.IniGroup --Wrapper.Group#GROUP if group and group:IsAlive() then if EventData.IniGroupName == self.Controllable:GetName() then if self.currentTarget then -- Increase number of shots fired by this group on this target. self.Nshots=self.Nshots+1 -- Debug output. local text=string.format("Group %s fired shot %d of %d with weapon %s on target %s.", self.Controllable:GetName(), self.Nshots, self.currentTarget.nshells, _weaponName, self.currentTarget.name) self:T(ARTY.id..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) -- Get current ammo. local _nammo,_nshells,_nrockets,_nmissiles=self:_GetAmmo(self.Controllable) if _nammo==0 then self:E(ARTY.id.."completely out of ammo") self.Nshots=0 self:Winchester() -- Current target is deallocated ==> return return end -- Weapon type name for current target. local _weapontype=self:_WeaponTypeName(self.currentTarget.weapontype) self:E(ARTY.id..string.format("nammo=%d, nshells=%d, nrockets=%d, nmissiles=%d", _nammo, _nshells, _nrockets, _nmissiles)) self:E(ARTY.id..string.format("Weapontype = %s", _weapontype)) -- Special weapon type requested ==> Check if corresponding ammo is empty. if self.currentTarget.weapontype==ARTY.WeaponType.UnguidedCannon and _nshells==0 then self:E(ARTY.id.."cannons requested and shells empty") self.Nshots=0 self:CeaseFire(self.currentTarget) return elseif self.currentTarget.weapontype==ARTY.WeaponType.UnguidedRockets and _nrockets==0 then self:E(ARTY.id.."rockets requested and rockets empty") self.Nshots=0 self:CeaseFire(self.currentTarget) return elseif self.currentTarget.weapontype==ARTY.WeaponType.UnguidedAny and _nshells+_nrockets==0 then self:E(ARTY.id.."unguided weapon requested and shells+rockets empty") self.Nshots=0 self:CeaseFire(self.currentTarget) return elseif self.currentTarget.weapontype==ARTY.WeaponType.CruiseMissile and _nmissiles==0 then self:E(ARTY.id.."cruise missiles requested and missiles empty") self.Nshots=0 self:CeaseFire(self.currentTarget) return end -- Check if number of shots reached max. if self.Nshots >= self.currentTarget.nshells then local text=string.format("Group %s stop firing on target %s.", self.Controllable:GetName(), self.currentTarget.name) self:T(ARTY.id..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) self.Nshots=0 self:CeaseFire(self.currentTarget) end else self:E(ARTY.id..string.format("ERROR: No current target?!")) end end end end --- Eventhandler for dead event. -- @param #ARTY self -- @param Core.Event#EVENTDATA EventData function ARTY:_OnEventDead(EventData) self:F(EventData) end --- Set task for firing at a coordinate. -- @param #ARTY self -- @param Core.Point#COORDINATE coord Coordinates to fire upon. -- @param #number radius Radius around coordinate. -- @param #number nshells Number of shells to fire. -- @param #number weapontype Type of weapon to use. function ARTY:_FireAtCoord(coord, radius, nshells, weapontype) self:E({coord=coord, radius=radius, nshells=nshells}) -- Controllable. local group=self.Controllable --Wrapper.Controllable#CONTROLLABLE -- Set ROE to weapon free. group:OptionROEOpenFire() -- Get Vec2 local vec2=coord:GetVec2() -- Get task. local fire=group:TaskFireAtPoint(vec2, radius, nshells, weapontype) -- Execute task. group:SetTask(fire) --group:PushTask(fire) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Before "OpenFire" event. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -- @param #table target Array holding the target info. -- @return #boolean If true proceed to onafterOpenfire. function ARTY:onbeforeOpenFire(Controllable, From, Event, To, target) self:_EventFromTo("onbeforeOpenFire", Event, From, To) -- If this target has an attack time and it's prio is higher than the current task, we allow the transition. if target.time~=nil and self.currentTarget~=nil and self.currentTarget.prio > target.prio then -- Debug info. self:T(ARTY.id..string.format("Group %s current target %s has lower prio than new target %s with attack time.", self.Controllable:GetName(), self.currentTarget.name, target.name)) -- Reset current task. --self.Controllable:ClearTasks() -- Set number of shots counter to zero. self.Nshots=0 -- Stop firing on current target. self:CeaseFire(self.currentTarget) -- Alow transition to onafterOpenfire. return true end -- Check that group has no current target already. if self.currentTarget then -- Debug info. self:T(ARTY.id..string.format("Group %s already has a target %s.", self.Controllable:GetName(), self.currentTarget.name)) -- Deny transition. return false end return true end --- After "OpenFire" event. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -- @param #table target Array holding the target info. _target={coord=coord, radius=radius, nshells=nshells, engaged=0, underattack=false} function ARTY:onafterOpenFire(Controllable, From, Event, To, target) self:_EventFromTo("onafterOpenFire", Event, From, To) local _coord=target.coord --Core.Point#COORDINATE --_coord:MarkToAll("Arty Target") -- Get target array index. local id=self:_GetTargetByName(target.name) -- Target is now under fire and has been engaged once more. if id then -- Set under fire flag. self.targets[id].underfire=true -- Increase engaged counter self.targets[id].engaged=self.targets[id].engaged+1 -- Clear the attack time. self.targets[id].time=nil -- Set current target. self.currentTarget=target end -- Distance to target local range=Controllable:GetCoordinate():Get2DDistance(target.coord) -- Send message. local text=string.format("%s, opening fire on target %s with %s shells. Distance %.1f km.", Controllable:GetName(), target.name, target.nshells, range/1000) self:T(ARTY.id..text) MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report) -- Start firing. self:_FireAtCoord(target.coord, target.radius, target.nshells, target.weapontype) -- Check that after a certain time a shot event occured. --self.CheckShootingSched, self.CheckRearmedSchedID=SCHEDULER:New(nil, self._CheckShootingStarted, {self}, self.WaitForShotTime) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Before "CeaseFire" event. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -- @param #table target Array holding the target info. -- @return #boolean function ARTY:onbeforeCeaseFire(Controllable, From, Event, To, target) self:_EventFromTo("onbeforeCeaseFire", Event, From, To) return true end --- After "CeaseFire" event. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -- @param #table target Array holding the target info. function ARTY:onafterCeaseFire(Controllable, From, Event, To, target) self:_EventFromTo("onafterCeaseFire", Event, From, To) -- Send message. local text=string.format("%s, ceasing fire on target %s.", Controllable:GetName(), target.name) self:T(ARTY.id..text) MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report) -- ARTY group has no current target any more. self.currentTarget=nil -- Get target array index. local id=self:_GetTargetByName(target.name) -- Target is not under fire any more. self.targets[id].underfire=false -- If number of engagements has been reached, the target is removed. if target.engaged >= target.maxengage then self:RemoveTarget(target.name) end end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- After "Winchester" event. Group is out of ammo. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. function ARTY:onafterWinchester(Controllable, From, Event, To) self:_EventFromTo("onafterWinchester", Event, From, To) local text=string.format("Group %s is winchester (out of ammo)!", Controllable:GetName()) self:T(ARTY.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) -- Send message. local text=string.format("%s, winchester.", Controllable:GetName()) self:T(ARTY.id..text) MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report) -- Remove current target. if self.currentTarget then local id=self:_GetTargetByName(self.currentTarget.name) self.targets[id].underfire=false self.currentTarget=nil end -- Init rearming. self:Rearm() end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Before "Rearm" event. Check if a unit to rearm the ARTY group has been defined. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. function ARTY:onbeforeRearm(Controllable, From, Event, To) self:_EventFromTo("onbeforeRearm", Event, From, To) if self.RearmingUnit and self.RearmingUnit:IsAlive() then return true else return false end end --- After "Rearm" event. Send message if reporting is on. Route rearming unit to ARTY group. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. function ARTY:onafterRearm(Controllable, From, Event, To) self:_EventFromTo("onafterRearm", Event, From, To) -- Send message. local text=string.format("%s, %s, request rearming.", Controllable:GetName(), self.RearmingUnit:GetName()) self:T(ARTY.id..text) MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report) -- Random point 20-100 m away from unit. local coord=self.Controllable:GetCoordinate() local vec2=coord:GetRandomVec2InRadius(20, 100) local pops=COORDINATE:NewFromVec2(vec2) -- Route unit to ARTY group. self.RearmingUnit:RouteGroundOnRoad(pops, 50, 5) -- Start scheduler to monitor ammo count until rearming is complete. self.CheckRearmedSched=SCHEDULER:New(nil,self._CheckRearmed, {self}, 5, 10) end --- Check if ARTY group is reamed. -- @param #ARTY self function ARTY:_CheckRearmed() self:F2() -- Get current ammo. local nammo,nshells,nrockets,nmissiles=self:_GetAmmo(self.Controllable) -- Rearming --> Rearmed --> CombatReady if nammo==self.Nammo0 then self:Rearmed() end end --- After "Rearmed" event. Send message if reporting is on and stop the scheduler. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. function ARTY:onafterRearmed(Controllable, From, Event, To) self:_EventFromTo("onafterRearmed", Event, From, To) -- Send message. local text=string.format("%s, rearming complete.", Controllable:GetName()) self:T(ARTY.id..text) MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report) -- Stop scheduler. self.CheckRearmedSched:Stop() end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Go through queue of assigned tasks. -- @param #ARTY self function ARTY:_TargetQueue() self:F2() -- Debug info self:T(ARTY.id..string.format("Group %s, number of targets = %d", self.Controllable:GetName(), #self.targets)) -- We already have a target. -- if self.currentTarget then -- self:T(ARTY.id..string.format("Group %s already has a target %s.", self.Controllable:GetName(), self.currentTarget.name)) -- return -- end -- First check if there is a target with a certain time for attack. for i=1,#self.targets do local _target=self.targets[i] if _target and _target.time then if timer.getAbsTime() >= _target.time and _target.underfire==false then -- Clock time format. local _clock=self:_SecondsToClock(_target.time) local _Cnow=self:_SecondsToClock(timer.getAbsTime()) -- Debug info. self:T(ARTY.id..string.format("Engaging timed target %s. Prio=%d, engaged=%d, time=%s, tnow=%s",_target.name,_target.prio,_target.engaged,_clock,_Cnow)) -- Call OpenFire event. self:OpenFire(_target) end end end -- Sort targets w.r.t. prio and number times engaged already. self:_SortTargetQueuePrio() -- Loop over all sorted targets. for i=1,#self.targets do local _target=self.targets[i] if _target.underfire==false and _target.time==nil and _target.maxengage > _target.engaged then -- Debug info. self:T(ARTY.id..string.format("Engaging target %s. Prio = %d, engaged = %d", _target.name, _target.prio, _target.engaged)) -- Call OpenFire event. self:OpenFire(_target) break end end end --- Sort targets with respect to priority and number of times it was already engaged. -- @param #ARTY self function ARTY:_SortTargetQueuePrio() self:F2() -- Sort results table wrt times they have already been engaged. local function _sort(a, b) return (a.engaged < b.engaged) or (a.engaged==b.engaged and a.prio < b.prio) end table.sort(self.targets, _sort) -- Debug output. self:T2(ARTY.id.."Sorted targets wrt prio and number of engagements:") for i=1,#self.targets do self:T2(ARTY.id..string.format("Target %s, prio=%d, engaged=%d", self.targets[i].name, self.targets[i].prio, self.targets[i].engaged)) end end --- Sort targets with respect to engage time. -- @param #ARTY self function ARTY:_SortTargetQueueTime() self:F2() -- Sort targets w.r.t attack time. local function _sort(a, b) if a.time == nil and b.time == nil then return false end if a.time == nil then return false end if b.time == nil then return true end return a.time < b.time end table.sort(self.targets, _sort) -- Debug output. self:T2(ARTY.id.."Sorted targets wrt time:") for i=1,#self.targets do self:T(ARTY.id..string.format("Target %s, prio=%d, engaged=%d", self.targets[i].name, self.targets[i].prio, self.targets[i].engaged)) end end --- Get the number of shells a unit or group currently has. For a group the ammo count of all units is summed up. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE controllable -- @return Number of ALL shells left from the whole group. function ARTY:_GetAmmo(controllable) self:F2(controllable) -- Get all units. local units=controllable:GetUnits() -- Init counter. local nammo=0 local nshells=0 local nrockets=0 local nmissiles=0 for _,unit in pairs(units) do if unit and unit:IsAlive() then local ammotable=unit:GetAmmo() self:T({ammotable=ammotable}) local name=unit:GetName() if ammotable ~= nil then local weapons=#ammotable self:T2(ARTY.id..string.format("Number of weapons %d.", weapons)) self:T2(ammotable) -- Loop over all weapons. for w=1,weapons do -- Number of current weapon. local Nammo=ammotable[w]["count"] -- Typename of current weapon local Tammo=ammotable[w]["desc"]["typeName"] -- Check for correct shell type. local _gotshell=false for _,_type in pairs(self.ammoshells) do if string.match(Tammo, _type) then _gotshell=true end end -- Check for correct rocket type. local _gotrocket=false for _,_type in pairs(self.ammorockets) do if string.match(Tammo, _type) then _gotrocket=true end end -- Check for correct missile type. local _gotmissile=false for _,_type in pairs(self.ammomissiles) do if string.match(Tammo,_type) then _gotmissile=true end end -- We are specifically looking for shells or rockets here. if _gotshell then -- Add up all shells. nshells=nshells+Nammo -- Debug info. local text=string.format("Unit %s has %d shells of type %s", name, Nammo, Tammo) self:T2(ARTY.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug and not self.report) elseif _gotrocket then -- Add up all rockets. nrockets=nrockets+Nammo -- Debug info. local text=string.format("Unit %s has %d rockets of type %s", name, Nammo, Tammo) self:T2(ARTY.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug and not self.report) elseif _gotmissile then -- Add up all rockets. nmissiles=nmissiles+Nammo -- Debug info. local text=string.format("Unit %s has %d missiles of type %s", name, Nammo, Tammo) self:T2(ARTY.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug and not self.report) else -- Debug info. local text=string.format("Unit %s has %d ammo of type %s", name, Nammo, Tammo) self:T2(ARTY.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug and not self.report) end end end end end -- Total amount of ammunition. nammo=nshells+nrockets+nmissiles return nammo, nshells, nrockets, nmissiles end --- Check whether shooting started within a certain time (~5 min). If not, the current target is considered invalid and removed from the target list. -- @param #ARTY self function ARTY:_CheckShootingStarted() self:F2() if self.currentTarget and self.Nshots==0 then -- Get name and id of target. local name=self.currentTarget.name local id=self:_GetTargetByName(name) -- Debug info. self:T(ARTY.id..string.format("No shot event after %d seconds. Removing current target %s from list.", self.WaitForShotTime, name)) -- CeaseFire. self:CeaseFire(self.currentTarget) -- Remove target from list. self:RemoveTarget(name) end end --- Get a target by its name. -- @param #ARTY self -- @param #string name Name of target. -- @return #number Arrayindex of target. function ARTY:_GetTargetByName(name) self:F2(name) for i=1,#self.targets do local targetname=self.targets[i].name if targetname==name then self:T2(ARTY.id..string.format("Found target with name %s. Index = %d", name, i)) return i end end self:E(ARTY.id..string.format("ERROR: Target with name %s could not be found!", name)) return nil end --- Get the weapon type name, which should be used to attack the target. -- @param #ARTY self -- @param #string name Desired target name. -- @return #string Unique name, which is not already given for another target. function ARTY:_CheckTargetName(name) self:F2(name) local newname=name local counter=1 repeat -- We assume the name is unique. local unique=true -- Loop over all targets already defined. for _,_target in pairs(self.targets) do -- Target name. local _targetname=_target.name if _targetname==newname then -- Define new name = "name #01" newname=string.format("%s #%02d", name, counter) -- Increase counter. counter=counter+1 -- Name is already used for another target ==> try again with new name. unique=false end end until (unique) -- Debug output and return new name. self:T(string.format("Original name %s, new name = %s", name, newname)) return newname end --- Get the weapon type name, which should be used to attack the target. -- @param #ARTY self -- @param #number tnumber Number of weapon type ARTY.WeaponType.XXX -- @return #number tnumber of weapon type. function ARTY:_WeaponTypeName(tnumber) local name="unknown" if tnumber==ARTY.WeaponType.Auto then name="Auto (Cannon, Rockets, Missiles)" elseif tnumber==ARTY.WeaponType.CruiseMissile then name="Cruise Missile" elseif tnumber==ARTY.WeaponType.GuidedAny then name="Any Guided Missile" elseif tnumber==ARTY.WeaponType.GuidedMissile then name="Guided Missile" elseif tnumber==ARTY.WeaponType.UnguidedAny then name="Any Unguided Weapon (Cannon or Rockets)" elseif tnumber==ARTY.WeaponType.UnguidedCannon then name="Unguided Cannon" elseif tnumber==ARTY.WeaponType.UnguidedRockets then name="Unguided Rockets" end return name end --- Print event-from-to string to DCS log file. -- @param #ARTY self -- @param #string BA Before/after info. -- @param #string Event Event. -- @param #string From From state. -- @param #string To To state. function ARTY:_EventFromTo(BA, Event, From, To) local text=string.format("%s: %s EVENT %s: %s --> %s", BA, self.Controllable:GetName(), Event, From, To) self:T3(ARTY.id..text) end --- Split string. C.f. http://stackoverflow.com/questions/1426954/split-string-in-lua -- @param #ARTY self -- @param #string str Sting to split. -- @param #string sep Speparator for split. -- @return #table Split text. function ARTY:_split(str, sep) self:F3({str=str, sep=sep}) local result = {} local regex = ("([^%s]+)"):format(sep) for each in str:gmatch(regex) do table.insert(result, each) end return result end --- Convert time in seconds to hours, minutes and seconds. -- @param #ARTY self -- @param #number seconds Time in seconds. -- @return #string Time in format Hours:minutes:seconds. function ARTY:_SecondsToClock(seconds) self:F3({seconds=seconds}) -- Seconds local seconds = tonumber(seconds) if seconds==nil then return "00:00:00" end if seconds <= 0 then return "00:00:00" else local hours = string.format("%02.f", math.floor(seconds/3600)) local mins = string.format("%02.f", math.floor(seconds/60 - (hours*60))) local secs = string.format("%02.f", math.floor(seconds - hours*3600 - mins *60)) return hours..":"..mins..":"..secs --return hours, mins, secs end end --- Convert clock time from hours, minutes and seconds to seconds. -- @param #ARTY self -- @param #string clock String of clock time. E.g., "06:12:35". function ARTY:_ClockToSeconds(clock) self:F3({clock=clock}) if clock==nil then return nil end -- Split string by ":" local tsplit=string.gmatch(clock, '([^:]+)') -- Get time in seconds local seconds=0 local i=1 for time in tsplit do if i==1 then -- Hours seconds=seconds+tonumber(time)*60*60 elseif i==2 then -- Minutes seconds=seconds+tonumber(time)*60 elseif i==3 then -- Seconds seconds=seconds+tonumber(time) end i=i+1 end self:T3(ARTY.id..string.format("Clock %s = %d seconds", clock, seconds)) return seconds end -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------