From e5268a29cf48f3dd9eec9810a8a1d53544df872e Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Mon, 30 Apr 2018 12:05:51 +0200 Subject: [PATCH] ARTY v0.3 First working version. But still WIP. --- .../Moose/Functional/Artillery.lua | 914 +++++++++++++++--- .../Moose/Wrapper/Controllable.lua | 9 +- 2 files changed, 789 insertions(+), 134 deletions(-) diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index 00c83a08a..d18a1b725 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -1,11 +1,12 @@ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- **Functional** - Control artillery units. -- --- ![Banner Image](..\Presentations\ARTILLERY\Artillery_Main.png) +-- ![Banner Image](..\Presentations\ARTY\Artillery_Main.png) -- -- ==== -- --- Make artillery fire on targets. +-- The ARTY class can be used to easily assign targets for artillery units. Multiple targets can be assigned. +-- -- -- ==== -- @@ -35,6 +36,26 @@ -- @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 -- @@ -43,7 +64,7 @@ -- -- ## Target aquisition... -- --- ![Process](..\Presentations\ART\Arty_Process.png) +-- ![Process](..\Presentations\ARTY\Artillery_Process.png) -- -- The arty process can be described as follows. -- @@ -55,41 +76,59 @@ 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, } ---- Enumerator of possible rules of engagement. --- @field #list ROE -ARTY.ROE={ - Hold="Weapon Hold", - Free="Weapon Free", - Return="Return Fire", +--- 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, } ---- Enumerator of possible alarm states. --- @field #list AlarmState -ARTY.AlarmState={ - Auto="Auto", - Green="Green", - Red="Red", -} - ---- Main F10 menu for suppresion, i.e. F10/Artillery. --- @field #string MenuF10 -ARTY.MenuF10=nil - --- 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.1.0" +ARTY.version="0.3.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list: --- TODO: don't know yet... +-- 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. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -108,13 +147,13 @@ function ARTY:New(group) 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.)") + 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())) + self:E(ARTY.id..string.format("ERROR: ARTY group %s has to be a GROUND or SHIP group!", group:GetName())) return nil end @@ -126,31 +165,40 @@ function ARTY:New(group) local DCSunit=DCSgroup:getUnit(1) self.DCSdesc=DCSunit:getDesc() - -- Get max speed the group can do and convert to km/h. - --self.SpeedMax=self.DCSdesc.speedMaxOffRoad*3.6 + -- 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. - --self.Speed=self.SpeedMax + -- 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.IsInfantry=DCSunit:hasAttribute("Infantry") + 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", "CeaseFire", "CombatReady") - self:AddTransition("*", "NoAmmo", "OutOfAmmo") - self:AddTransition("*", "Dead", "*") + 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 @@ -159,35 +207,140 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Assign a group of target(s). +--- Add a group of target(s) for the ARTY group. -- @param #ARTY self -- @param Wrapper.Group#GROUP group Group of targets. --- @param #number radius (Optional) Radius. Default is 100 m. --- @param #number nshells (Optional) How many shells are fired on target per unit. Default 5. -- @param #number prio (Optional) Priority of target. Number between 1 (high) and 100 (low). Default 50. -function ARTY:AssignTargetGroup(group, radius, nshells, prio) - self:E({group=group, radius=radius, nshells=nshells, prio=prio}) +-- @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 coord=group:GetCoordinate() - local name=group:GetName() + local name=coord:ToStringLLDMS() -- Prepare target array. - local _target={name=name, coord=coord, radius=radius, nshells=nshells, engaged=0, underfire=false, prio=prio} + 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. - env.info(ARTY.id..string.format("Added target %s, radius=%d, nshells=%d, prio=%d.", name, radius, nshells, prio)) + 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 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -206,37 +359,52 @@ function ARTY:onafterStart(Controllable, From, Event, To) local text=string.format("Started ARTY for group %s.", Controllable:GetName()) MESSAGE:New(text, 10):ToAllIf(self.Debug) - -- Create main F10 menu if it is not there yet. - if self.MenuON then - if not ARTY.MenuF10 then - ARTY.MenuF10 = MENU_MISSION:New("ARTY") - end - self:_CreateMenuGroup() - end - -- Set the current ROE and alam state. --self:_SetAlarmState(self.DefaultAlarmState) --self:_SetROE(self.DefaultROE) - local text=string.format("\n******************************************************\n") - text=text..string.format("Arty group = %s\n", Controllable:GetName()) - text=text..string.format("Type = %s\n", self.Type) - text=text..string.format("******************************************************\n") - self:T(ARTY.id..text) - -- Get Ammo. - self:_GetAmmo(self.Controllable) + 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 - env.info(ARTY.id..string.format("Target %s, radius=%d, nshells=%d, prio=%d.", target.name, target.radius, target.nshells, target.prio)) + 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.TaskQueueSched=SCHEDULER:New(nil, ARTY._CheckTaskQueue, {self}, 5, 10) + self.TargetQueueSched=SCHEDULER:New(nil, ARTY._TargetQueue, {self}, 5, self.TargetQueueUpdate) end @@ -256,10 +424,10 @@ function ARTY:_OnEventShot(EventData) local _weaponName = _weaponStrArray[#_weaponStrArray] -- Debug info. - self:T(ARTY.id.."EVENT SHOT: Ini unit = "..EventData.IniUnitName) - self:T(ARTY.id.."EVENT SHOT: Ini group = "..EventData.IniGroupName) - self:T(ARTY.id.."EVENT SHOT: Weapon type = ".._weapon) - self:T(ARTY.id.."EVENT SHOT: Weapon name = ".._weaponName) + 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 @@ -273,16 +441,70 @@ function ARTY:_OnEventShot(EventData) self.Nshots=self.Nshots+1 -- Debug output. - self:T(ARTY.id..string.format("Group %s fired shot # %d on target %s.", self.Controllable:GetName(), self.Nshots, self.currentTarget.name)) - + 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 - self:CeaseFire(self.currentTarget) + 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:T(ARTY.id..string.format("No current target?!")) + self:E(ARTY.id..string.format("ERROR: No current target?!")) end end end @@ -300,29 +522,25 @@ end -- @param Core.Point#COORDINATE coord Coordinates to fire upon. -- @param #number radius Radius around coordinate. -- @param #number nshells Number of shells to fire. -function ARTY:_FireAtCoord(coord, radius, nshells) +-- @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 - -- Number of units. - local units=group:GetUnits() - local nunits=#units - - local nshells_tot=nshells*nunits - -- Set ROE to weapon free. - group:OptionROEWeaponFree() + group:OptionROEOpenFire() -- Get Vec2 local vec2=coord:GetVec2() -- Get task. - local fire=group:TaskFireAtPoint(vec2, radius, nshells_tot) + local fire=group:TaskFireAtPoint(vec2, radius, nshells, weapontype) -- Execute task. group:SetTask(fire) + --group:PushTask(fire) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -340,11 +558,34 @@ end function ARTY:onbeforeOpenFire(Controllable, From, Event, To, target) self:_EventFromTo("onbeforeOpenFire", Event, From, To) - if self.currentTarget then - self:T(ARTY.id..string.format("Group %s already has a target %s.", self.Controllable:GetName(), target.name)) - return false + + -- 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 @@ -367,16 +608,34 @@ function ARTY:onafterOpenFire(Controllable, From, Event, To, target) -- 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) + 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. @@ -400,42 +659,195 @@ end -- @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) - local name=self.currentTarget.name + -- ARTY group has no current target any more. + self.currentTarget=nil - local id=self:_GetTargetByName(name) + -- Get target array index. + local id=self:_GetTargetByName(target.name) + -- Target is not under fire any more. self.targets[id].underfire=false - self.currentTarget=nil - - --Controllable:ClearTasks() + -- 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:_CheckTaskQueue() - self:F() +function ARTY:_TargetQueue() + self:F2() + + -- Debug info + self:T(ARTY.id..string.format("Group %s, number of targets = %d", self.Controllable:GetName(), #self.targets)) - -- Sort targets. - self:_SortTaskQueue() + -- 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 then - - env.info(ARTY.id..string.format("Opening fire on target %s. Prio = %d, engaged = %d", _target.name, _target.prio, _target.engaged)) + 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 @@ -445,8 +857,8 @@ end --- Sort targets with respect to priority and number of times it was already engaged. -- @param #ARTY self -function ARTY:_SortTaskQueue() - self:F() +function ARTY:_SortTargetQueuePrio() + self:F2() -- Sort results table wrt times they have already been engaged. local function _sort(a, b) @@ -455,17 +867,44 @@ function ARTY:_SortTaskQueue() table.sort(self.targets, _sort) -- Debug output. - env.info(ARTY.id.."Sorted targets:") + self:T2(ARTY.id.."Sorted targets wrt prio and number of engagements:") for i=1,#self.targets do - env.info(ARTY.id..string.format("Target %s. Prio = %d, engaged = %d", self.targets[i].name, self.targets[i].prio, self.targets[i].engaged)) + 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 shells left +-- @return Number of ALL shells left from the whole group. function ARTY:_GetAmmo(controllable) self:F2(controllable) @@ -473,48 +912,136 @@ function ARTY:_GetAmmo(controllable) local units=controllable:GetUnits() -- Init counter. - local ammo=0 + local nammo=0 + local nshells=0 + local nrockets=0 + local nmissiles=0 for _,unit in pairs(units) do - local ammotable=unit:GetAmmo() - self:T2({ammotable=ammotable}) - - local name=unit:GetName() - - if ammotable ~= nil then - - local weapons=#ammotable - self:T2(ARTY.id..string.format("Number of weapons %d.", weapons)) + if unit and unit:IsAlive() then + + local ammotable=unit:GetAmmo() + self:T({ammotable=ammotable}) - for w=1,weapons do + local name=unit:GetName() - local Nammo=ammotable[w]["count"] - local Tammo=ammotable[w]["desc"]["typeName"] + if ammotable ~= nil then + + local weapons=#ammotable - -- We are specifically looking for shells here. - if string.match(Tammo, "shell") then + self:T2(ARTY.id..string.format("Number of weapons %d.", weapons)) + self:T2(ammotable) - -- Add up all shells - ammo=ammo+Nammo + -- Loop over all weapons. + for w=1,weapons do - local text=string.format("Unit %s has %d rounds ammo of type %s (shells)", name, Nammo, Tammo) - self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToAllIf(self.Debug) - else - local text=string.format("Unit %s has %d ammo of type %s", name, Nammo, Tammo) - self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToAllIf(self.Debug) + -- 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 - return ammo + -- 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. @@ -525,7 +1052,7 @@ function ARTY:_GetTargetByName(name) for i=1,#self.targets do local targetname=self.targets[i].name if targetname==name then - self:E(ARTY.id..string.format("Found target with name %s. Index = %d", name, i)) + self:T2(ARTY.id..string.format("Found target with name %s. Index = %d", name, i)) return i end end @@ -535,6 +1062,70 @@ function ARTY:_GetTargetByName(name) 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. @@ -543,25 +1134,84 @@ end -- @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:T(ARTY.id..text) + self:T3(ARTY.id..text) end ---- Split string. Cf http://stackoverflow.com/questions/1426954/split-string-in-lua +--- 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:F2({str=str, sep=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) + 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 + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- \ No newline at end of file diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 0b68255c5..92d258b5f 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -1048,9 +1048,10 @@ end -- @param Dcs.DCSTypes#Vec2 Vec2 The point to fire at. -- @param Dcs.DCSTypes#Distance Radius The radius of the zone to deploy the fire at. -- @param #number AmmoCount (optional) Quantity of ammunition to expand (omit to fire until ammunition is depleted). +-- @param #number WeaponType (optional) Enum for weapon type ID. This value is only required if you want the group firing to use a specific weapon, for instance using the task on a ship to force it to fire guided missiles at targets within cannon range. See http://wiki.hoggit.us/view/DCS_enum_weapon_flag -- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount ) - self:F2( { self.ControllableName, Vec2, Radius, AmmoCount } ) +function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount, WeaponType ) + self:F2( { self.ControllableName, Vec2, Radius, AmmoCount, WeaponType } ) -- FireAtPoint = { -- id = 'FireAtPoint', @@ -1076,6 +1077,10 @@ function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount ) DCSTask.params.expendQty = AmmoCount DCSTask.params.expendQtyEnabled = true end + + if WeaponType then + DCSTask.params.weaponType=WeaponType + end self:T3( { DCSTask } ) return DCSTask