diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 04da94f98..c6c422798 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -27,7 +27,7 @@ -- * All of the above can be customized by the user if necessary. -- * All current (Caucasus, Nevada, Normandy) and future maps are supported. -- --- The RAT class creates an entry in the F10 menu which allows to +-- The RAT class creates an entry in the F10 radio menu which allows to -- -- * Create new groups on-the-fly, i.e. at run time within the mission, -- * Destroy specific groups (e.g. if they get stuck or damaged and block a runway), @@ -49,8 +49,7 @@ -- -- # YouTube Channel -- --- ### RAT videos are work in progress. --- ### [MOOSE YouTube Channel](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl1jirWIo4t4YxqN-HxjqRkL) +-- ### [DCS WORLD - MOOSE - RAT - Random Air Traffic](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0u4Zxywtg-mx_ov4vi68CO) -- -- === -- @@ -123,7 +122,9 @@ -- @field #boolean respawn_at_landing Respawn aircraft the moment they land rather than at engine shutdown. -- @field #boolean norespawn Aircraft will not be respawned after they have finished their route. -- @field #boolean respawn_after_takeoff Aircraft will be respawned directly after take-off. --- @field #number respawn_delay Delay in seconds until repawn happens after landing. +-- @field #boolean respawn_after_crash Aircraft will be respawned after a crash, e.g. when they get shot down. +-- @field #boolean respawn_inair Aircraft are allowed to spawned in air if they cannot be respawned on ground because there is not free parking spot. Default is true. +-- @field #number respawn_delay Delay in seconds until a repawn happens. -- @field #table markerids Array with marker IDs. -- @field #table waypointdescriptions Table with strings for waypoint descriptions of markers. -- @field #table waypointstatus Table with strings of waypoint status. @@ -141,10 +142,13 @@ -- @field #number activate_delay Delay in seconds before first uncontrolled group is activated. Default is 5 seconds. -- @field #number activate_delta Time interval in seconds between activation of uncontrolled groups. Default is 5 seconds. -- @field #number activate_frand Randomization factor of time interval (activate_delta) between activating uncontrolled groups. Default is 0. --- @field #number activate_max=0 Maximal number of uncontrolle aircraft, which will be activated at a time. Default is 0 +-- @field #number activate_max Maximum number of uncontrolled aircraft, which will be activated at the same time. Default is 1. -- @field #string onboardnum Sets the onboard number prefix. Same as setting "TAIL #" in the mission editor. --- @field #number onboardnum0 (Optional) Starting value of the automatically appended numbering of aircraft within a flight. Default is one. +-- @field #number onboardnum0 (Optional) Starting value of the automatically appended numbering of aircraft within a flight. Default is 1. +-- @field #boolean checkonrunway Aircraft are checked if they were accidentally spawned on the runway. Default is true. +-- @field #boolean checkontop Aircraft are checked if they were accidentally spawned on top of another unit. Default is true. -- @field #number rbug_maxretry Number of respawn retries (on ground) at other airports if a group gets accidentally spawned on the runway. Default is 3. +-- @field #boolean useparkingdb Parking spots are added to data base once an aircraft has used it. These spots can later be used by other aircraft. Default is true. -- @extends Core.Spawn#SPAWN ---# RAT class, extends @{Spawn#SPAWN} @@ -352,6 +356,8 @@ RAT={ respawn_at_landing=false, -- Respawn aircraft the moment they land rather than at engine shutdown. norespawn=false, -- Aircraft will not get respawned. respawn_after_takeoff=false, -- Aircraft will be respawned directly after takeoff. + respawn_after_crash=true, -- Aircraft will be respawned after a crash. + respawn_inair=true, -- Aircraft are spawned in air if there is no free parking spot on the ground. respawn_delay=nil, -- Delay in seconds until repawn happens after landing. markerids={}, -- Array with marker IDs. waypointdescriptions={}, -- Array with descriptions for waypoint markers. @@ -371,10 +377,13 @@ RAT={ activate_delay=5, -- Delay in seconds before first uncontrolled group is activated. activate_delta=5, -- Time interval in seconds between activation of uncontrolled groups. activate_frand=0, -- Randomization factor of time interval (activate_delta) between activating uncontrolled groups. - activate_max=0, -- Max number of uncontrolle aircraft, which will be activated at a time. + activate_max=1, -- Max number of uncontrolle aircraft, which will be activated at a time. onboardnum=nil, -- Tail number. onboardnum0=1, -- (Optional) Starting value of the automatically appended numbering of aircraft within a flight. Default is one. rbug_maxretry=3, -- Number of respawn retries (on ground) at other airports if a group gets accidentally spawned on the runway. + checkonrunway=true, -- Check whether aircraft have been spawned on the runway. + checkontop=true, -- Check whether aircraft have been spawned on top of another unit. + useparkingdb=true, -- Put known parking spots into a data base. } ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -486,6 +495,10 @@ RAT.markerid=0 -- @field #string MenuF10 RAT.MenuF10=nil +--- RAT parking spots data base. +-- @list parking +RAT.parking={} + --- Some ID to identify who we are in output of the DCS.log file. -- @field #string id RAT.id="RAT | " @@ -493,10 +506,11 @@ RAT.id="RAT | " --- RAT version. -- @list version RAT.version={ - version = "2.2.0", + version = "2.2.1", print = true, } + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --TODO list: @@ -689,12 +703,16 @@ function RAT:Spawn(naircraft) text=text..string.format("Return Zone: %s\n", tostring(self.returnzone)) text=text..string.format("Spawn delay: %4.1f\n", self.spawndelay) text=text..string.format("Spawn interval: %4.1f\n", self.spawninterval) + text=text..string.format("Respawn delay: %s\n", tostring(self.respawn_delay)) + text=text..string.format("Respawn off: %s\n", tostring(self.norespawn)) text=text..string.format("Respawn after landing: %s\n", tostring(self.respawn_at_landing)) - text=text..string.format("Respawning off: %s\n", tostring(self.norespawn)) text=text..string.format("Respawn after take-off: %s\n", tostring(self.respawn_after_takeoff)) - text=text..string.format("Respawn delay: %s\n", tostring(self.respawn_delay)) + text=text..string.format("Respawn after crash: %s\n", tostring(self.respawn_after_crash)) + text=text..string.format("Respawn in air: %s\n", tostring(self.respawn_inair)) text=text..string.format("ROE: %s\n", tostring(self.roe)) text=text..string.format("ROT: %s\n", tostring(self.rot)) + text=text..string.format("Immortal: %s\n", tostring(self.immortal)) + text=text..string.format("Invisible: %s\n", tostring(self.invisible)) text=text..string.format("Vclimb: %4.1f\n", self.Vclimb) text=text..string.format("AlphaDescent: %4.2f\n", self.AlphaDescent) text=text..string.format("Vcruisemax: %s\n", tostring(self.Vcruisemax)) @@ -713,12 +731,16 @@ function RAT:Spawn(naircraft) text=text..string.format("Radio frequency : %s\n", tostring(self.frequency)) text=text..string.format("Radio modulation : %s\n", tostring(self.frequency)) text=text..string.format("Tail # prefix : %s\n", tostring(self.onboardnum)) + text=text..string.format("Check on runway: %s\n", tostring(self.checkonrunway)) + text=text..string.format("Check on top: %s\n", tostring(self.checkontop)) + text=text..string.format("Max respawn attempts: %s\n", tostring(self.rbug_maxretry)) + text=text..string.format("Parking DB: %s\n", tostring(self.useparkingdb)) text=text..string.format("Uncontrolled: %s\n", tostring(self.uncontrolled)) if self.uncontrolled and self.activate_uncontrolled then + text=text..string.format("Uncontrolled max : %4.1f\n", self.activate_max) text=text..string.format("Uncontrolled delay: %4.1f\n", self.activate_delay) text=text..string.format("Uncontrolled delta: %4.1f\n", self.activate_delta) text=text..string.format("Uncontrolled frand: %4.1f\n", self.activate_frand) - text=text..string.format("Uncontrolled max : %4.1f\n", self.activate_max) end if self.livery then text=text..string.format("Available liveries:\n") @@ -752,142 +774,34 @@ function RAT:Spawn(naircraft) -- Handle events. self:HandleEvent(EVENTS.Birth, self._OnBirth) - self:HandleEvent(EVENTS.EngineStartup, self._EngineStartup) + self:HandleEvent(EVENTS.EngineStartup, self._OnEngineStartup) self:HandleEvent(EVENTS.Takeoff, self._OnTakeoff) self:HandleEvent(EVENTS.Land, self._OnLand) self:HandleEvent(EVENTS.EngineShutdown, self._OnEngineShutdown) - self:HandleEvent(EVENTS.Dead, self._OnDead) - self:HandleEvent(EVENTS.Crash, self._OnCrash) - -- TODO: add hit event? + self:HandleEvent(EVENTS.Dead, self._OnDeadOrCrash) + self:HandleEvent(EVENTS.Crash, self._OnDeadOrCrash) + self:HandleEvent(EVENTS.Hit, self._OnHit) + -- No groups should be spawned. if self.ngroups==0 then return nil - elseif self.uncontrolled then - for i=1,self.ngroups do - self:_SpawnWithRoute() - end - if self.activate_uncontrolled then - SCHEDULER:New(nil, self._ActivateUncontrolled, {self}, self.activate_delay, self.activate_delta, self.activate_frand) - end - else - SCHEDULER:New(nil, self._SpawnWithRoute, {self}, Tstart, dt, 0.0, Tstop) end + -- Start scheduled spawning. + SCHEDULER:New(nil, self._SpawnWithRoute, {self}, Tstart, dt, 0.0, Tstop) + + -- Start scheduled activation of uncontrolled groups. + if self.uncontrolled and self.activate_uncontrolled then + SCHEDULER:New(nil, self._ActivateUncontrolled, {self}, self.activate_delay, self.activate_delta, self.activate_frand) + end end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Randomly activates an uncontrolled aircraft. --- @param #RAT self -function RAT:_ActivateUncontrolled() - if self.Debug then - env.info(RAT.id.."_ActivateUncontrolled") - end - - -- Spawn indices of uncontrolled inactive aircraft. - local idx={} - local rat={} - - -- Number of active aircraft. - local nactive=0 - - -- Loop over RAT groups and count the active ones. - for spawnindex,ratcraft in pairs(self.ratcraft) do - - local group=ratcraft.group --Wrapper.Group#GROUP - - if group and group:IsAlive() then - - if self.Debug then - local text=string.format("Spawnindex = %d, group name = %s, active = %s", spawnindex, ratcraft.group:GetName(), tostring(ratcraft.active)) - env.info(RAT.id..text) - end - - if ratcraft.active then - nactive=nactive+1 - else - table.insert(idx, spawnindex) - end - - end - end - - if self.Debug then - local text=string.format("Nactive = %d, Ninactive = %d, max active=%d", nactive, #idx, self.activate_max) - env.info(RAT.id..text) - end - - if #idx>0 and nactive001, 002, ... -- @param #number zero (Optional) Starting value of the automatically appended numbering of aircraft within a flight. Default is 0. function RAT:SetOnboardNum(tailnumprefix, zero) + self:F2({tailnumprefix=tailnumprefix, zero=zero}) self.onboardnum=tailnumprefix if zero ~= nil then self.onboardnum0=zero @@ -1610,6 +1654,7 @@ end -- @param #RAT self -- @param Dcs.DCSWrapper.Group#Group DCSgroup Group of the aircraft in the mission editor. function RAT:_InitAircraft(DCSgroup) + self:F2(DCSgroup) local DCSunit=DCSgroup:getUnit(1) local DCSdesc=DCSunit:getDesc() @@ -1647,6 +1692,15 @@ function RAT:_InitAircraft(DCSgroup) -- service ceiling in meters self.aircraft.ceiling=DCSdesc.Hmax + -- Store all descriptors. + --self.aircraft.descriptors=DCSdesc + + -- aircraft dimensions + self.aircraft.length=DCSdesc.box.max.x + self.aircraft.height=DCSdesc.box.max.y + self.aircraft.width=DCSdesc.box.max.z + self.aircraft.box=math.max(self.aircraft.length,self.aircraft.width) + -- info message local text=string.format("\n******************************************************\n") text=text..string.format("Aircraft parameters:\n") @@ -1654,6 +1708,9 @@ function RAT:_InitAircraft(DCSgroup) text=text..string.format("Alias = %s\n", self.alias) text=text..string.format("Category = %s\n", self.category) text=text..string.format("Type = %s\n", self.aircraft.type) + text=text..string.format("Length (x) = %6.1f m\n", self.aircraft.length) + text=text..string.format("Width (z) = %6.1f m\n", self.aircraft.width) + text=text..string.format("Height (y) = %6.1f m\n", self.aircraft.height) text=text..string.format("Max air speed = %6.1f m/s\n", self.aircraft.Vmax) text=text..string.format("Max climb speed = %6.1f m/s\n", self.aircraft.Vymax) text=text..string.format("Initial Fuel = %6.1f\n", self.aircraft.fuel*100) @@ -1702,19 +1759,13 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live local temp={RAT.wp.cold, RAT.wp.hot} takeoff=temp[math.random(2)] end - + -- Number of respawn attempts after spawning on runway. local nrespawn=0 if _nrespawn then nrespawn=_nrespawn end - -- Spawn position. - local lastpos=nil - if _lastpos then - lastpos=_lastpos - end - -- Set flight plan. local departure, destination, waypoints, WPholding, WPfinal = self:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) @@ -1722,6 +1773,12 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live if not (departure and destination and waypoints) then return nil end + + -- Find parking spot in RAT parking DB. Category 4 should be airports and farps. Ships would be caterory 1. + local _spawnpos=_lastpos + if self.useparkingdb and (takeoff==RAT.wp.cold or takeoff==RAT.wp.hot) and departure:GetCategory()==4 and _spawnpos==nil then + _spawnpos=self:_FindParkingSpot(departure) + end -- Set (another) livery. local livery @@ -1738,11 +1795,15 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live end -- Modify the spawn template to follow the flight plan. - self:_ModifySpawnTemplate(waypoints, livery, lastpos) + self:_ModifySpawnTemplate(waypoints, livery, _spawnpos) -- Actually spawn the group. local group=self:SpawnWithIndex(self.SpawnIndex) -- Wrapper.Group#GROUP + -- Increase counter of alive groups (also uncontrolled ones). + self.alive=self.alive+1 + self:T(RAT.id..string.format("Alive groups counter now = %d.",self.alive)) + -- ATC is monitoring this flight (if it is supposed to land). if self.ATCswitch and landing==RAT.wp.landing then if self.returnzone then @@ -1780,6 +1841,7 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live self.ratcraft[self.SpawnIndex]["departure"]=departure self.ratcraft[self.SpawnIndex]["waypoints"]=waypoints self.ratcraft[self.SpawnIndex]["airborne"]=group:InAir() + self.ratcraft[self.SpawnIndex]["nunits"]=group:GetInitialSize() -- Time and position on ground. For check if aircraft is stuck somewhere. if group:InAir() then self.ratcraft[self.SpawnIndex]["Tground"]=nil @@ -1815,6 +1877,12 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live -- Number of preformed spawn attempts for this group. self.ratcraft[self.SpawnIndex].nrespawn=nrespawn + + -- If we start at a parking position, we memorize the parking spot position for future use (DCS bug). + -- TODO: Check for ships and FARPS. + if self.useparkingdb and (takeoff==RAT.wp.cold or takeoff==RAT.wp.hot) and departure:GetCategory()==4 then + self:_AddParkingSpot(departure, group) + end -- Create submenu for this group. if self.f10menu then @@ -1887,7 +1955,10 @@ function RAT:_Respawn(group) -- Note: we have to check that it was supposed to land and not respawned directly after landing or after takeoff. -- TODO: Need to think if continuejourney with respawn_after_takeoff actually makes sense. if landing==RAT.wp.landing and lastpos and not (self.respawn_at_landing or self.respawn_after_takeoff) then - _lastpos=lastpos + -- Check that we have an airport or FARP but not a ship (which would be categroy 1). + if destination:GetCategory()==4 then + _lastpos=lastpos + end end if self.destinationzone then @@ -1933,7 +2004,10 @@ function RAT:_Respawn(group) -- Note: we have to check that it was supposed to land and not respawned directly after landing or after takeoff. -- TODO: Need to think if commute with respawn_after_takeoff actually makes sense. if landing==RAT.wp.landing and lastpos and not (self.respawn_at_landing or self.respawn_after_takeoff) then - _lastpos=lastpos + -- Check that we have landed on an airport or FARP but not a ship (which would be categroy 1). + if destination:GetCategory()==4 then + _lastpos=lastpos + end end -- Handle takeoff type. @@ -1982,7 +2056,7 @@ function RAT:_Respawn(group) end -- Debug - self:F({departure=_departure, destination=_destination, takeoff=_takeoff, landing=_landing, livery=_livery, lastwp=_lastwp}) + self:T2({departure=_departure, destination=_destination, takeoff=_takeoff, landing=_landing, livery=_livery, lastwp=_lastwp}) -- Spawn new group. local arg={} @@ -2009,10 +2083,11 @@ end --- Set the route of the AI plane. Due to DCS landing bug, this has to be done before the unit is spawned. -- @param #RAT self --- @param takeoff #RAT.wp Takeoff type. Could also be air start. --- @param landing #RAT.wp Landing type. Could also be a destination in air. +-- @param #number takeoff Takeoff type. Could also be air start. +-- @param #number landing Landing type. Could also be a destination in air. -- @param Wrapper.Airport#AIRBASE _departure (Optional) Departure airbase. -- @param Wrapper.Airport#AIRBASE _destination (Optional) Destination airbase. +-- @param #table _waypoint Initial waypoint. -- @return Wrapper.Airport#AIRBASE Departure airbase. -- @return Wrapper.Airport#AIRBASE Destination airbase. -- @return #table Table of flight plan waypoints. @@ -2075,8 +2150,8 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) -- If it's not an airport, check whether it's a zone. departure=ZONE:New(_departure) else - local text=string.format("ERROR: Specified departure airport %s does not exist for %s!", _departure, self.alias) - self:E(RAT.id.."ERROR: "..text) + local text=string.format("ERROR! Specified departure airport %s does not exist for %s.", _departure, self.alias) + self:E(RAT.id..text) end else @@ -2085,9 +2160,8 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) -- Return nil if no departure could be found. if not departure then - local text=string.format("No valid departure airport could be found for %s.", self.alias) - MESSAGE:New(text, 60):ToAll() - self:E(RAT.id.."ERROR: "..text) + local text=string.format("ERROR! No valid departure airport could be found for %s.", self.alias) + self:E(RAT.id..text) return nil end @@ -2471,7 +2545,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) text=text..string.format("h_descent_max = %6.1f m\n", h_descent_max) end text=text..string.format("******************************************************\n") - self:T(RAT.id..text) + self:T2(RAT.id..text) -- Ensure that cruise distance is positve. Can be slightly negative in special cases. And we don't want to turn back. if d_cruise<0 then @@ -2657,10 +2731,10 @@ function RAT:_PickDeparture(takeoff) if takeoff==RAT.wp.air then dep=ZONE:New(name) else - self:E(RAT.id.."ERROR: Takeoff is not in air. Cannot use "..name.." as departure!") + self:E(RAT.id..string.format("ERROR! Takeoff is not in air. Cannot use %s as departure.", name)) end else - self:E(RAT.id.."ERROR: No airport or zone found with name "..name) + self:E(RAT.id..string.format("ERROR: No airport or zone found with name %s.", name)) end -- Add to departures table. @@ -2685,10 +2759,10 @@ function RAT:_PickDeparture(takeoff) else text=string.format("%s: Chosen departure airport: %s (ID %d)", self.alias, departure:GetName(), departure:GetID()) end - MESSAGE:New(text, 30):ToAllIf(self.Debug) + --MESSAGE:New(text, 30):ToAllIf(self.Debug) self:T(RAT.id..text) else - self:E(RAT.id..string.format("ERROR: No departure airport or zone found for %s!", self.alias)) + self:E(RAT.id..string.format("ERROR! No departure airport or zone found for %s.", self.alias)) departure=nil end @@ -2753,10 +2827,10 @@ function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing) if landing==RAT.wp.air then dest=ZONE:New(name) else - self:E(RAT.id.."ERROR: Landing is not in air. Cannot use zone "..name.." as destination!") + self:E(RAT.id..string.format("ERROR! Landing is not in air. Cannot use zone %s as destination!", name)) end else - self:E(RAT.id.."ERROR: No airport or zone found with name "..name) + self:E(RAT.id..string.format("ERROR! No airport or zone found with name %s", name)) end if dest then @@ -2777,7 +2851,7 @@ function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing) end -- Info message. - self:T(RAT.id.."Number of possible destinations = "..#destinations) + self:T(RAT.id..string.format("Number of possible destinations = %s.", #destinations)) if #destinations > 0 then --- Compare distance of destination airports. @@ -2810,10 +2884,10 @@ function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing) text=string.format("%s Chosen destination airport: %s (ID %d).", self.alias, destination:GetName(), destination:GetID()) end self:T(RAT.id..text) - MESSAGE:New(text, 30):ToAllIf(self.Debug) + --MESSAGE:New(text, 30):ToAllIf(self.Debug) else - self:E(RAT.id.."ERROR: No destination airport or zone found.") + self:E(RAT.id.."ERROR! No destination airport or zone found.") destination=nil end @@ -2899,7 +2973,7 @@ function RAT:_GetAirportsOfMap() table.insert(self.airports_map, _myab) local text="MOOSE: Airport ID = ".._myab:GetID().." and Name = ".._myab:GetName()..", Category = ".._myab:GetCategory()..", TypeName = ".._myab:GetTypeName() - self:T2(RAT.id..text) + self:T(RAT.id..text) end end @@ -2923,9 +2997,9 @@ function RAT:_GetAirportsOfCoalition() end if #self.airports==0 then - local text="No possible departure/destination airports found!" - MESSAGE:New(text, 60):ToAll() - self:E(RAT.id.."ERROR: "..text) + local text="ERROR! No possible departure/destination airports found." + MESSAGE:New(text, 30):ToAll() + self:E(RAT.id..text) end end @@ -2937,9 +3011,7 @@ end -- @param #number forID (Optional) Send message only for this ID. function RAT:Status(message, forID) - --message=message or false - --forID=forID or false - + -- Optional arguments. if message==nil then message=false end @@ -2971,6 +3043,8 @@ function RAT:Status(message, forID) local type=self.aircraft.type local status=ratcraft.status local active=ratcraft.active + local Nunits=ratcraft.nunits -- group:GetSize() + local N0units=group:GetInitialSize() -- Monitor time and distance on ground. local Tg=0 @@ -2999,7 +3073,8 @@ function RAT:Status(message, forID) -- If aircraft did not move more than 50 m since last check, we call it stationary and despawn it. -- Aircraft which are spawned uncontrolled or starting their engines are not counted. - if Dg<50 and active and not status==RAT.status.EventBirth then + if Dg<50 and active and status~=RAT.status.EventBirth then + --if Dg<50 and active then stationary=true end @@ -3029,7 +3104,12 @@ function RAT:Status(message, forID) -- Status report. if (forID and spawnindex==forID) or (not forID) then - local text=string.format("ID %i of group %s\n", spawnindex, prefix) + local text=string.format("ID %i of flight %s", spawnindex, prefix) + if N0units>1 then + text=text..string.format(" (%d/%d)\n", Nunits, N0units) + else + text=text.."\n" + end if self.commute then text=text..string.format("%s commuting between %s and %s\n", type, departure, destination) elseif self.continuejourney then @@ -3064,22 +3144,24 @@ function RAT:Status(message, forID) -- Despawn unit if it did not move more then 50 m in the last 180 seconds. if stationary then - local text=string.format("Group %s is despawned after being %4.0f seconds inaktive on ground.", self.alias, dTlast) + local text=string.format("Group %s is despawned after being %d seconds inaktive on ground.", self.alias, dTlast) self:T(RAT.id..text) self:_Despawn(group) end -- Despawn group if life is < 10% and distance travelled < 100 m. if life<10 and Dtravel<100 then local text=string.format("Damaged group %s is despawned. Life = %3.0f", self.alias, life) + self:T(RAT.id..text) self:_Despawn(group) end end + -- Despawn groups after they have reached their destination zones. if ratcraft.despawnme then local text=string.format("Flight %s will be despawned NOW!", self.alias) self:T(RAT.id..text) -- Despawn old group. - if not self.norespawn then + if (not self.norespawn) and (not self.respawn_after_takeoff) then self:_Respawn(group) end self:_Despawn(group) @@ -3112,10 +3194,10 @@ function RAT:_GetLife(group) if unit then life=unit:GetLife()/unit:GetLife0()*100 else - self:T2(RAT.id.."ERROR: Unit does not exist in RAT_Getlife(). Returning zero.") + self:T2(RAT.id.."ERROR! Unit does not exist in RAT_Getlife(). Returning zero.") end else - self:T2(RAT.id.."ERROR: Group does not exist in RAT_Getlife(). Returning zero.") + self:T2(RAT.id.."ERROR! Group does not exist in RAT_Getlife(). Returning zero.") end return life end @@ -3131,10 +3213,6 @@ function RAT:_SetStatus(group, status) -- Get index from groupname. local index=self:GetSpawnIndexFromGroup(group) - if self.Debug or self.reportstatus then - env.info(RAT.id..string.format("Group %s has status %s, spawnindex = %d", group:GetName(), status, index)) - end - if self.ratcraft[index] then -- Set new status. @@ -3161,7 +3239,10 @@ end --- Function is executed when a unit is spawned. -- @param #RAT self +-- @param Core.Event#EVENTDATA EventData function RAT:_OnBirth(EventData) + self:F3(EventData) + self:T3(RAT.id.."Captured event birth!") local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP @@ -3178,9 +3259,6 @@ function RAT:_OnBirth(EventData) local text="Event: Group "..SpawnGroup:GetName().." was born." self:T(RAT.id..text) - -- Increase counter of alive groups (also uncontrolled ones). - self.alive=self.alive+1 - -- Set status. local status="unknown in birth" if SpawnGroup:InAir() then @@ -3204,8 +3282,13 @@ function RAT:_OnBirth(EventData) -- Check if aircraft group was accidentally spawned on the runway. -- This can happen due to no parking slots available and other DCS bugs. local onrunway=false - if _takeoff ~= RAT.wp.runway then - onrunway=self:_CheckOnRunway(SpawnGroup, _departure) + if _takeoff ~= RAT.wp.runway and self.checkonrunway then + for _,unit in pairs(SpawnGroup:GetUnits()) do + local _onrunway=self:_CheckOnRunway(unit, _departure) + if _onrunway then + onrunway=true + end + end end -- Workaround if group was spawned on runway. @@ -3214,7 +3297,7 @@ function RAT:_OnBirth(EventData) -- Error message. local text=string.format("ERROR: RAT group of %s was spawned on runway (DCS bug). Group #%d will be despawned immediately!", self.alias, i) MESSAGE:New(text,30):ToAllIf(self.Debug) - env.info(RAT.id..text) + self:T(RAT.id..text) if self.Debug then SpawnGroup:FlareRed() end @@ -3229,19 +3312,18 @@ function RAT:_OnBirth(EventData) -- This creates a completely new group, i.e. livery etc from earlier flights (continuejourney, commute) is not taken over. text=string.format("Try spawning new aircraft of group %s at another location. Attempt %d of max %d.", self.alias,_nrespawn,self.rbug_maxretry) - MESSAGE:New(text,30):ToAllIf(self.Debug) - env.info(RAT.id..text) + MESSAGE:New(text,10):ToAllIf(self.Debug) + self:T(RAT.id..text) -- Spawn new group. self:_SpawnWithRoute(nil, nil, nil, nil, nil, nil, nil, _nrespawn) else - -- This will respawn the same fight (maybe with a different route) but already in the air.l - -- Note: We could also try to spawn already on the runway but this might also lead to problems. - -- Uncontrolled aircraft are not respawned in air. - if not self.uncontrolled then + -- This will respawn the same fight (maybe with a different route) but already in the air. + -- Note: Uncontrolled aircraft are not respawned in air. + if self.respawn_inair and not self.uncontrolled then text=string.format("Spawning new aircraft of group %s in air since no parking slot is available at %s.", self.alias, _departure) - MESSAGE:New(text,30):ToAll() - env.info(RAT.id..text) + MESSAGE:New(text,10):ToAll() + self:T(RAT.id..text) -- Spawn new group at this airport but already in air. self:_SpawnWithRoute(_departure, _destination, RAT.wp.air, _landing, _livery) @@ -3249,6 +3331,23 @@ function RAT:_OnBirth(EventData) end end -- end of workaround + -- Check if any unit of the group was spawned on top of another unit in the MOOSE data base. + local ontop=false + if self.checkontop then + ontop=self:_CheckOnTop(SpawnGroup) + end + + if ontop then + local text=string.format("ERROR: RAT group of %s was spawned on top of another unit. Group #%d will be despawned immediately!", self.alias, i) + MESSAGE:New(text,30):ToAllIf(self.Debug) + self:T(RAT.id..text) + if self.Debug then + SpawnGroup:FlareYellow() + end + -- Despawn group. + self:_Despawn(SpawnGroup) + end + end end else @@ -3256,73 +3355,13 @@ function RAT:_OnBirth(EventData) end end ---- Function to check whether an aircraft is on the runway. --- @param #RAT self --- @param Wrapper.Group#GROUP group The group to be checked. --- @param #string airport The name of the airport we want to check. --- @param #boolean True if aircraft is on the runway and on the ground. -function RAT:_CheckOnRunway(group,airport) - - -- We use the tabulated points in the ATC_GROUND classes to find out if the group is on the runway. - -- Note that land.SurfaceType.RUNWAY also is true for the parking areas etc. Hence, not useful. - -- This is useful to check if an aircraft was accidentally spawned on the runway due to missing parking spots. - - --BASE:E(ATC_GROUND_CAUCASUS.Airbases[AIRBASE.Caucasus.Batumi].PointsRunways) - - -- Table holding the points around the runway. - local pointsrwy={} - - -- Loop over all airports on Caucaus map. - for id,name in pairs(AIRBASE.Caucasus) do - if name==airport then - --pointsrwy=ATC_GROUND_CAUCASUS.Airbases[AIRBASE.Caucasus.Batumi].PointsRunways - pointsrwy=ATC_GROUND_CAUCASUS.Airbases[name].PointsRunways - self:T2({name=name, points=pointsrwy}) - end - end - -- Loop over all airports on NTTR map. - for id,name in pairs(AIRBASE.Nevada) do - if name==airport then - pointsrwy=ATC_GROUND_NEVADA.Airbases[name].PointsRunways - self:T2({name=name, points=pointsrwy}) - end - end - -- Loop over all airports on Normandy map. - for id,name in pairs(AIRBASE.Normandy) do - if name==airport then - pointsrwy=ATC_GROUND_NORMANDY.Airbases[name].PointsRunways - self:T2({name=name, points=pointsrwy}) - end - end - - -- Assume we are not on the runway. - local onrunway=false - - -- Loop over all runways. Some airports have more than one. - for PointsRunwayID, PointsRunway in pairs(pointsrwy) do - -- Create zone around runway. - local runway = ZONE_POLYGON_BASE:New("Runway "..PointsRunwayID, PointsRunway) - - -- Check if group is completely or partly inside the zone. - -- Note that IsPartlyInZone is only true if units are inside AND outside of the zone. - if group:IsCompletelyInZone(runway) or group:IsPartlyInZone(runway) then - onrunway=true - end - end - - -- Check that aircraft is on ground. - onrunway=onrunway and group:InAir()==false - - if self.Debug then - env.info(RAT.id..string.format("Check on runway of %s airport for group %s = %s", airport, group:GetName(),tostring(onrunway))) - end - - return onrunway -end --- Function is executed when a unit starts its engines. -- @param #RAT self -function RAT:_EngineStartup(EventData) +-- @param Core.Event#EVENTDATA EventData +function RAT:_OnEngineStartup(EventData) + self:F3(EventData) + self:T3(RAT.id.."Captured event EngineStartup!") local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP @@ -3357,6 +3396,7 @@ end --- Function is executed when a unit takes off. -- @param #RAT self +-- @param Core.Event#EVENTDATA EventData function RAT:_OnTakeoff(EventData) local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP @@ -3382,8 +3422,9 @@ function RAT:_OnTakeoff(EventData) text="Event: Group "..SpawnGroup:GetName().." will be respawned." self:T(RAT.id..text) - -- Respawn group. - self:_Respawn(SpawnGroup) + -- Respawn group. We respawn with no parameters from the old flight. + self:_SpawnWithRoute(nil, nil, nil, nil, nil, nil, nil, nil) + --self:_Respawn(SpawnGroup) end end @@ -3396,6 +3437,7 @@ end --- Function is executed when a unit lands. -- @param #RAT self +-- @param Core.Event#EVENTDATA EventData function RAT:_OnLand(EventData) local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP @@ -3441,6 +3483,8 @@ end --- Function is executed when a unit shuts down its engines. -- @param #RAT self function RAT:_OnEngineShutdown(EventData) + self:F3(EventData) + self:T3(RAT.id.."Captured event EngineShutdown!") local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP @@ -3454,27 +3498,30 @@ function RAT:_OnEngineShutdown(EventData) -- Check that the template name actually belongs to this object. if EventPrefix == self.alias then - local text="Event: Group "..SpawnGroup:GetName().." shut down its engines." - self:T(RAT.id..text) - - -- Set status. - local status=RAT.status.EventEngineShutdown - self:_SetStatus(SpawnGroup, status) + -- Despawn group only if it on the ground. + if not SpawnGroup:InAir() then - if not self.respawn_at_landing and not self.norespawn then - text="Event: Group "..SpawnGroup:GetName().." will be respawned." + local text="Event: Group "..SpawnGroup:GetName().." shut down its engines." self:T(RAT.id..text) - - -- Respawn group. - self:_Respawn(SpawnGroup) - end - - - -- Despawn group. - text="Event: Group "..SpawnGroup:GetName().." will be destroyed now." - self:T(RAT.id..text) - self:_Despawn(SpawnGroup) + + -- Set status. + local status=RAT.status.EventEngineShutdown + self:_SetStatus(SpawnGroup, status) + + if not self.respawn_at_landing and not self.norespawn then + text="Event: Group "..SpawnGroup:GetName().." will be respawned." + self:T(RAT.id..text) + + -- Respawn group. + self:_Respawn(SpawnGroup) + end + + -- Despawn group. + text="Event: Group "..SpawnGroup:GetName().." will be destroyed now." + self:T(RAT.id..text) + self:_Despawn(SpawnGroup) + end end end @@ -3483,15 +3530,86 @@ function RAT:_OnEngineShutdown(EventData) end end ---- Function is executed when a unit is dead. +--- Function is executed when a unit is hit. -- @param #RAT self -function RAT:_OnDead(EventData) - +-- @param Core.Event#EVENTDATA EventData +function RAT:_OnHit(EventData) + self:F3(EventData) + self:T(RAT.id..string.format("Captured event Hit by %s! Initiator %s. Target %s", self.alias, tostring(EventData.IniUnitName), tostring(EventData.TgtUnitName))) + local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP if SpawnGroup then - env.info(string.format("%sGroup %s died!", RAT.id, SpawnGroup:GetName())) + -- Get the template name of the group. This can be nil if this was not a spawned group. + local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) + + if EventPrefix then + + -- Check that the template name actually belongs to this object. + if EventPrefix == self.alias then + + -- Debug info. + self:T(RAT.id..string.format("Event: Group %s was hit. Unit %s.", SpawnGroup:GetName(), EventData.IniUnitName)) + + end + end + end +end + +--- Function is executed when a unit is dead or crashes. +-- @param #RAT self +-- @param Core.Event#EVENTDATA EventData +function RAT:_OnDeadOrCrash(EventData) + self:F3(EventData) + self:T3(RAT.id.."Captured event DeadOrCrash!") + + local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP + + if SpawnGroup then + + -- Get the template name of the group. This can be nil if this was not a spawned group. + local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) + + if EventPrefix then + + -- Check that the template name actually belongs to this object. + if EventPrefix == self.alias then + + -- Decrease group alive counter. + self.alive=self.alive-1 + + -- Debug info. + local text=string.format("Event: Group %s crashed or died. Alive counter = %d.", SpawnGroup:GetName(), self.alive) + self:T(RAT.id..text) + + -- Split crash and dead events. + if EventData.id == world.event.S_EVENT_CRASH then + + -- Call crash event. This handles when a group crashed or + self:_OnCrash(EventData) + + elseif EventData.id == world.event.S_EVENT_DEAD then + + -- Call dead event. + self:_OnDead(EventData) + + end + end + end + end +end + +--- Function is executed when a unit is dead. +-- @param #RAT self +-- @param Core.Event#EVENTDATA EventData +function RAT:_OnDead(EventData) + self:F3(EventData) + self:T3(RAT.id.."Captured event Dead!") + + local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP + + if SpawnGroup then -- Get the template name of the group. This can be nil if this was not a spawned group. local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) @@ -3501,9 +3619,8 @@ function RAT:_OnDead(EventData) -- Check that the template name actually belongs to this object. if EventPrefix == self.alias then - local text="Event: Group "..SpawnGroup:GetName().." died." + local text=string.format("Event: Group %s died. Unit %s.", SpawnGroup:GetName(), EventData.IniUnitName) self:T(RAT.id..text) - env.info(RAT.id..text) -- Set status. local status=RAT.status.EventDead @@ -3513,20 +3630,20 @@ function RAT:_OnDead(EventData) end else - self:E(RAT.id.."ERROR: Group does not exist in RAT:_OnDead().") + self:T2(RAT.id.."ERROR: Group does not exist in RAT:_OnDead().") end end --- Function is executed when a unit crashes. -- @param #RAT self +-- @param Core.Event#EVENTDATA EventData function RAT:_OnCrash(EventData) + self:F3(EventData) + self:T3(RAT.id.."Captured event Crash!") local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP if SpawnGroup then - - self:T(string.format("%sGroup %s crashed!", RAT.id, SpawnGroup:GetName())) - env.info(string.format("%sGroup %s crashed!", RAT.id, SpawnGroup:GetName())) -- Get the template name of the group. This can be nil if this was not a spawned group. local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) @@ -3535,18 +3652,29 @@ function RAT:_OnCrash(EventData) -- Check that the template name actually belongs to this object. if EventPrefix == self.alias then - - local text="Event: Group "..SpawnGroup:GetName().." crashed." + + -- Update number of alive units in the group. + local _i=self:GetSpawnIndexFromGroup(SpawnGroup) + self.ratcraft[_i].nunits=self.ratcraft[_i].nunits-1 + local _n=self.ratcraft[_i].nunits + local _n0=SpawnGroup:GetInitialSize() + + -- Debug info. + local text=string.format("Event: Group %s crashed. Unit %s. Units still alive %d of %d.", SpawnGroup:GetName(), EventData.IniUnitName, _n, _n0) self:T(RAT.id..text) - env.info(RAT.id..text) - + -- Set status. - --self:_SetStatus(SpawnGroup, "Crashed") local status=RAT.status.EventCrash self:_SetStatus(SpawnGroup, status) - --TODO: Aircraft are not respawned if they crash. Should they? - + -- Respawn group if all units are dead. + if _n==0 and self.respawn_after_crash and not self.norespawn then + local text=string.format("No units left of group %s. Group will be respawned now.", SpawnGroup:GetName()) + self:T(RAT.id..text) + -- Respawn group. + self:_Respawn(SpawnGroup) + end + --TODO: Maybe spawn some people at the crash site and send a distress call. -- And define them as cargo which can be rescued. end @@ -3575,6 +3703,8 @@ function RAT:_Despawn(group) self.ratcraft[index].group=nil self.ratcraft[index]["status"]="Dead" + --TODO: Maybe here could be some more arrays deleted? + --TODO: Somehow this causes issues. --[[ --self.ratcraft[index]["group"]=group self.ratcraft[index]["destination"]=nil @@ -3597,26 +3727,77 @@ function RAT:_Despawn(group) self.ratcraft[index].despawnme=nil self.ratcraft[index].nrespawn=nil ]] - -- Remove ratcraft table entry. - --TODO: Somehow this causes issues. --table.remove(self.ratcraft, index) - --TODO: What events are actually fired when doing this? Crash and Dead or just Dead or...? - -- Destroy should create a crash event but for each unit. - group:Destroy() - - -- Decrease group alive counter. - self.alive=self.alive-1 + + -- This will destroy the DCS group and create a single DEAD event. + self:_Destroy(group) -- Remove submenu for this group. if self.f10menu and self.SubMenuName ~= nil then self.Menu[self.SubMenuName]["groups"][index]:Remove() end + end end +end - --TODO: Maybe here could be some more arrays deleted? +--- Destroys the RAT DCS group and all of its DCS units. +-- Note that this raises a DEAD event at run-time. +-- So all event listeners will catch the DEAD event of this DCS group. +-- @param #RAT self +-- @param Wrapper.Group#GROUP group The RAT group to be destroyed. +function RAT:_Destroy(group) + self:F2(group) + + local DCSGroup = group:GetDCSObject() -- Dcs.DCSGroup#Group + + if DCSGroup and DCSGroup:isExist() then + + --local DCSUnit = DCSGroup:getUnit(1) -- Dcs.DCSUnit#Unit + --if DCSUnit then + -- self:_CreateEventDead(timer.getTime(), DCSUnit) + --end + + -- Cread one single Dead event and delete units from database. + local triggerdead=true + for _,DCSUnit in pairs(DCSGroup:getUnits()) do + + -- Dead event. + if DCSUnit then + if triggerdead then + self:_CreateEventDead(timer.getTime(), DCSUnit) + triggerdead=false + end + + -- Delete from data base. + _DATABASE:DeleteUnit(DCSUnit:getName()) + end + end + + -- Destroy DCS group. + DCSGroup:destroy() + DCSGroup = nil + end + + return nil +end + +--- Create a Dead event. +-- @param #RAT self +-- @param Dcs.DCSTypes#Time EventTime The time stamp of the event. +-- @param Dcs.DCSWrapper.Object#Object Initiator The initiating object of the event. +function RAT:_CreateEventDead(EventTime, Initiator) + self:F( { EventTime, Initiator } ) + + local Event = { + id = world.event.S_EVENT_DEAD, + time = EventTime, + initiator = Initiator, + } + + world.onEvent( Event ) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -3723,9 +3904,7 @@ function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport text=text..string.format("No airport/zone specified\n") end text=text.."******************************************************\n" - if self.Debug then - self:T2(RAT.id..text) - end + self:T2(RAT.id..text) -- define waypoint local RoutePoint = {} @@ -3829,10 +4008,8 @@ function RAT:_Routeinfo(waypoints, comment) text=text..string.format("Total distance = %6.1f km\n", total/1000) text=text..string.format("******************************************************\n") - -- send message - if self.Debug then - env.info(RAT.id..text) - end + -- Debug info. + self:T2(RAT.id..text) -- return total route length in meters return total @@ -3919,8 +4096,6 @@ function RAT._WaypointFunction(group, rat, wp) -- New status. local status=rat.waypointstatus[wp] - - --rat.ratcraft[sdx].status=status rat:_SetStatus(group, status) if wp==WPholding then @@ -3945,7 +4120,7 @@ function RAT._WaypointFunction(group, rat, wp) if landing==RAT.wp.air then text=string.format("Activating despawn switch for flight %s! Group will be detroyed soon.", group:GetName()) - MESSAGE:New(text, 30):ToAllIf(rat.Debug) + MESSAGE:New(text, 10):ToAllIf(rat.Debug) BASE.T(rat, RAT.id..text) -- Enable despawn switch. Next time the status function is called, the aircraft will be despawned. rat.ratcraft[sdx].despawnme=true @@ -3995,36 +4170,359 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Calculate the max flight level for a given distance and fixed climb and descent rates. This function is obsolete now. --- In other words we have a distance between two airports and want to know how high we --- can climb before we must descent again to arrive at the destination without any level/cruising part. +--- Randomly activates an uncontrolled aircraft. -- @param #RAT self --- @param #number alpha Angle of climb [rad]. --- @param #number beta Angle of descent [rad]. --- @param #number d Distance between the two airports [m]. --- @param #number phi Angle between departure and destination [rad]. --- @param #number h0 Height [m] of departure airport. Note we implicitly assume that the height difference between departure and destination is negligible. --- @return #number Maximal flight level in meters. -function RAT:_FLmax(alpha, beta, d, phi, h0) --- Solve ASA triangle for one side (d) and two adjacent angles (alpha, beta) given. - local gamma=math.rad(180)-alpha-beta - local a=d*math.sin(alpha)/math.sin(gamma) - local b=d*math.sin(beta)/math.sin(gamma) - -- h1 and h2 should be equal. - local h1=b*math.sin(alpha) - local h2=a*math.sin(beta) - -- We also take the slope between departure and destination into account. - local h3=b*math.cos(math.pi/2-(alpha+phi)) - -- Debug message. - local text=string.format("\nFLmax = FL%3.0f = %6.1f m.\n", h1/RAT.unit.FL2m, h1) - text=text..string.format( "FLmax = FL%3.0f = %6.1f m.\n", h2/RAT.unit.FL2m, h2) - text=text..string.format( "FLmax = FL%3.0f = %6.1f m.", h3/RAT.unit.FL2m, h3) - if self.Debug then - self:T3(RAT.id..text) +function RAT:_ActivateUncontrolled() + self:F() + + -- Spawn indices of uncontrolled inactive aircraft. + local idx={} + local rat={} + + -- Number of active aircraft. + local nactive=0 + + -- Loop over RAT groups and count the active ones. + for spawnindex,ratcraft in pairs(self.ratcraft) do + + local group=ratcraft.group --Wrapper.Group#GROUP + + if group and group:IsAlive() then + + local text=string.format("Uncontrolled: Group = %s (spawnindex = %d), active = %s.", ratcraft.group:GetName(), spawnindex, tostring(ratcraft.active)) + self:T2(RAT.id..text) + + if ratcraft.active then + nactive=nactive+1 + else + table.insert(idx, spawnindex) + end + + end end - return h3+h0 + + -- Debug message. + local text=string.format("Uncontrolled: Ninactive = %d, Nactive = %d (of max %d).", #idx, nactive, self.activate_max) + self:T(RAT.id..text) + + if #idx>0 and nactive self.aircraft.box*2 + -- Or (if possible) even better to take our and the other object's size (plus 10% safety margin) + local size=self:_GetObjectSize(unit) + if size then + safe=_dist > (self.aircraft.box+size)*1.1 + end + self:T2(RAT.id..string.format("RAT aircraft size = %.1f m, other object size = %.1f m", self.aircraft.box, size or 0)) + if not safe then + occupied=true + end + self:T2(RAT.id..string.format("Unit %s to parking spot %d: distance = %.1f m (occupied = %s).", unit:GetName(), _i, _dist, tostring(safe))) + end + end + end + + if occupied then + self:T(RAT.id..string.format("Parking spot #%d occupied at %s.", _i, airport)) + else + parkingspot=spawnplace + self:T(RAT.id..string.format("Found free parking spot in DB at airport %s.", airport)) + break + end + + end + + return parkingspot + else + self:T2(RAT.id..string.format("No parking position in DB yet for %s.", airport)) + end + + self:T(RAT.id..string.format("No free parking position found in DB at airport %s.", airport)) + return nil +end + +--- Get aircraft dimensions length, width, height. +-- @param #RAT self +-- @param Wrapper.Unit#UNIT unit The unit which is we want the size of. +-- @return #number Size, i.e. max(length,width) of unit. +function RAT:_GetObjectSize(unit) + local DCSunit=unit:GetDCSObject() + if DCSunit then + local DCSdesc=DCSunit:getDesc() + -- dimensions + local length=DCSdesc.box.max.x + local height=DCSdesc.box.max.y + local width=DCSdesc.box.max.z + return math.max(length,width) + end + return nil +end + +--- Find aircraft that have accidentally been spawned on top of each other. +-- @param #RAT self +-- @param Wrapper.Group#GROUP group Units of this group will be checked. +-- @return #boolean True if group was destroyed because it was on top of another unit. False if otherwise. +function RAT:_CheckOnTop(group) + + -- Minimum allowed distance between two units + local distmin=5 + + for i,uniti in pairs(group:GetUnits()) do + local uniti=uniti --Wrapper.Unit#UNIT + + if uniti then + + local namei=uniti:GetName() + + for j,unitj in pairs(_DATABASE.UNITS) do + + if unitj then + local unitj=unitj --Wrapper.Unit#UNIT + local namej=unitj:GetName() + + if namei ~= namej then + + local DCSuniti=uniti:GetDCSObject() + local DCSunitj=unitj:GetDCSObject() + + if DCSuniti and DCSuniti:isExist() and DCSunitj and DCSunitj:isExist() then + + -- Distance between units. + local _dist=uniti:GetCoordinate():Get2DDistance(unitj:GetCoordinate()) + + -- Check for min distance. + if _dist < distmin then + if not uniti:InAir() and not unitj:InAir() then + --uniti:Destroy() + --self:_CreateEventDead(timer.getTime(), uniti) + --unitj:Destroy() + --self:_CreateEventDead(timer.getTime(), unitj) + return true + end + end + + end -- if DCSunit exists + end -- if namei==namej then + end --if unitj then + end -- for j, unitj + end -- if uniti then + end -- for i,uniti in + + return false +end + +--- Function to check whether an aircraft is on the runway. +-- @param #RAT self +-- @param Wrapper.Unit#UNIT unit The unit to be checked. +-- @param #string airport The name of the airport we want to check. +-- @return #boolean True if aircraft is on the runway and on the ground. +function RAT:_CheckOnRunway(unit, airport) + + -- We use the tabulated points in the ATC_GROUND classes to find out if the group is on the runway. + -- Note that land.SurfaceType.RUNWAY also is true for the parking areas etc. Hence, not useful. + -- This is useful to check if an aircraft was accidentally spawned on the runway due to missing parking spots. + --BASE:E(ATC_GROUND_CAUCASUS.Airbases[AIRBASE.Caucasus.Batumi].PointsRunways) + + -- Table holding the points around the runway. + local pointsrwy={} + + -- Loop over all airports on Caucaus map. + for id,name in pairs(AIRBASE.Caucasus) do + if name==airport then + --pointsrwy=ATC_GROUND_CAUCASUS.Airbases[AIRBASE.Caucasus.Batumi].PointsRunways + pointsrwy=ATC_GROUND_CAUCASUS.Airbases[name].PointsRunways + self:T2({name=name, points=pointsrwy}) + end + end + -- Loop over all airports on NTTR map. + for id,name in pairs(AIRBASE.Nevada) do + if name==airport then + pointsrwy=ATC_GROUND_NEVADA.Airbases[name].PointsRunways + self:T2({name=name, points=pointsrwy}) + end + end + -- Loop over all airports on Normandy map. + for id,name in pairs(AIRBASE.Normandy) do + if name==airport then + pointsrwy=ATC_GROUND_NORMANDY.Airbases[name].PointsRunways + self:T2({name=name, points=pointsrwy}) + end + end + + -- Assume we are not on the runway. + local onrunway=false + + -- Loop over all runways. Some airports have more than one. + for PointsRunwayID, PointsRunway in pairs(pointsrwy) do + -- Create zone around runway. + local runway = ZONE_POLYGON_BASE:New("Runway "..PointsRunwayID, PointsRunway) + + -- Check if unit is in on the runway. + if runway:IsVec3InZone(unit:GetVec3()) then + onrunway=true + end + end + + -- Check that aircraft is on ground. + onrunway=onrunway and unit:InAir()==false + + -- Debug + self:T(RAT.id..string.format("Check on runway of %s airport for unit %s = %s", airport, unit:GetName(),tostring(onrunway))) + + return onrunway +end + + --- Calculate minimum distance between departure and destination for given minimum flight level and climb/decent rates. -- @param #RAT self -- @param #number alpha Angle of climb [rad]. @@ -4298,6 +4796,8 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + --- Modifies the template of the group to be spawned. -- In particular, the waypoints of the group's flight plan are copied into the spawn template. -- This allows to spawn at airports and also land at other airports, i.e. circumventing the DCS "landing bug". @@ -4306,6 +4806,7 @@ end -- @param #string livery (Optional) Livery of the aircraft. All members of a flight will get the same livery. -- @param Core.Point#COORDINATE spawnplace (Optional) Place where spawning should happen. If not present, first waypoint is taken. function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace) + self:F2({waypoints=waypoints, livery=livery, spawnplace=spawnplace}) -- The 3D vector of the first waypoint, i.e. where we actually spawn the template group. local PointVec3 = {x=waypoints[1].x, y=waypoints[1].alt, z=waypoints[1].y} @@ -4346,6 +4847,11 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace) SpawnTemplate.units[UnitID].x = TX SpawnTemplate.units[UnitID].y = TY SpawnTemplate.units[UnitID].alt = PointVec3.y + + if self.Debug then + local unitspawn=COORDINATE:New(TX,PointVec3.y,TY) + unitspawn:MarkToAll(string.format("Spawnplace unit #%d", UnitID)) + end SpawnTemplate.units[UnitID].heading = heading SpawnTemplate.units[UnitID].psi = -heading @@ -4382,7 +4888,6 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace) UnitTemplate.alt=PointVec3.y self:T('After Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) - end -- Copy waypoints into spawntemplate. By this we avoid the nasty DCS "landing bug" :) @@ -4732,26 +5237,26 @@ end -- In this example, three different @{#RAT} objects are created (but not spawned manually). The @{#RATMANAGER} takes care that at least five aircraft of each type are alive and that the total number of aircraft -- spawned is 25. The @{#RATMANAGER} is started after 30 seconds and stopped after two hours. -- --- local a10c=RAT:New("RAT_A10C", "A-10C managed") --- a10c:SetDeparture({"Batumi"}) --- --- local f15c=RAT:New("RAT_F15C", "F15C managed") --- f15c:SetDeparture({"Sochi-Adler"}) --- f15c:DestinationZone() --- f15c:SetDestination({"Zone C"}) --- --- local av8b=RAT:New("RAT_AV8B", "AV8B managed") --- av8b:SetDeparture({"Zone C"}) --- av8b:SetTakeoff("air") --- av8b:DestinationZone() --- av8b:SetDestination({"Zone A"}) --- --- local manager=RATMANAGER:New(25) --- manager:Add(a10c, 5) --- manager:Add(f15c, 5) --- manager:Add(av8b, 5) --- manager:Start(30) --- manager:Stop(7200) +-- local a10c=RAT:New("RAT_A10C", "A-10C managed") +-- a10c:SetDeparture({"Batumi"}) +-- +-- local f15c=RAT:New("RAT_F15C", "F15C managed") +-- f15c:SetDeparture({"Sochi-Adler"}) +-- f15c:DestinationZone() +-- f15c:SetDestination({"Zone C"}) +-- +-- local av8b=RAT:New("RAT_AV8B", "AV8B managed") +-- av8b:SetDeparture({"Zone C"}) +-- av8b:SetTakeoff("air") +-- av8b:DestinationZone() +-- av8b:SetDestination({"Zone A"}) +-- +-- local manager=RATMANAGER:New(25) +-- manager:Add(a10c, 5) +-- manager:Add(f15c, 5) +-- manager:Add(av8b, 5) +-- manager:Start(30) +-- manager:Stop(7200) -- -- @field #RATMANAGER RATMANAGER={ @@ -5029,7 +5534,7 @@ function RATMANAGER:_RollDice(nrat,ntot,min,alive) end -- Debug info - --env.info(string.format("RATMANAGER: i=%d, alive=%d, min=%d, mini=%d, maxi=%d, add=%d, sumN=%d, sumP=%d", j, alive[j], min[j], mini[j], maxi[j], N[j],sN, sP)) + self:T3(string.format("RATMANAGER: i=%d, alive=%d, min=%d, mini=%d, maxi=%d, add=%d, sumN=%d, sumP=%d", j, alive[j], min[j], mini[j], maxi[j], N[j],sN, sP)) end diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index 3c946ea79..9748e89d6 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -16,12 +16,12 @@ -- -- ## Features -- --- * Bomb and rocket impact point from closest range target is measured and distance reported to the player. --- * Number of hits on strafing passes are counted. +-- * Impact points of bombs, rockets and missils are recorded and distance to closest range target is measured and reported to the player. +-- * Number of hits on strafing passes are counted and reported. Also the percentage of hits w.r.t fired shots is evaluated. -- * Results of all bombing and strafing runs are stored and top 10 results can be displayed. -- * Range targets can be marked by smoke. -- * Range can be illuminated by illumination bombs for night practices. --- * Rocket or bomb impact points can be marked by smoke. +-- * Bomb, rocket and missile impact points can be marked by smoke. -- * Direct hits on targets can trigger flares. -- * Smoke and flare colors can be adjusted for each player via radio menu. -- * Range information and weather report at the range can be reported via radio menu. @@ -55,8 +55,9 @@ -- @field #string ClassName Name of the Class. -- @field #boolean Debug If true, debug info is send as messages on the screen. -- @field #string rangename Name of the range. --- @field Core.Point#COORDINATE location Coordinate of the range. --- @field #number rangeradius Radius of range defining its total size for e.g. smoking bomb impact points and sending radio messages. Default 10 km. +-- @field Core.Point#COORDINATE location Coordinate of the range location. +-- @field #number rangeradius Radius of range defining its total size for e.g. smoking bomb impact points and sending radio messages. Default 5 km. +-- @field Core.Zone#ZONE rangezone MOOSE zone object of the range. For example, no bomb impacts are smoked if bombs fall outside of the range zone. -- @field #table strafeTargets Table of strafing targets. -- @field #table bombingTargets Table of targets to bomb. -- @field #number nbombtargets Number of bombing targets. @@ -79,14 +80,17 @@ -- @field #number scorebombdistance Distance from closest target up to which bomb hits are counted. Default 1000 m. -- @field #number TdelaySmoke Time delay in seconds between impact of bomb and starting the smoke. Default 3 seconds. -- @field #boolean eventmoose If true, events are handled by MOOSE. If false, events are handled directly by DCS eventhandler. Default true. +-- @field #boolean trackbombs If true (default), all bomb types are tracked and impact point to closest bombing target is evaluated. +-- @field #boolean trackrockets If true (default), all rocket types are tracked and impact point to closest bombing target is evaluated. +-- @field #boolean trackmissiles If true (default), all missile types are tracked and impact point to closest bombing target is evaluated. -- @extends Core.Base#BASE ---# RANGE class, extends @{Base#BASE} -- The RANGE class enables a mission designer to easily set up practice ranges in DCS. A new RANGE object can be created with the @{#RANGE.New}(rangename) contructor. -- The parameter "rangename" defindes the name of the range. It has to be unique since this is also the name displayed in the radio menu. -- --- Generally, a range consits of strafe pits and bombing targets. For strafe pits the number of hits for each pass is counted and tabulated. --- For bombing targets, the distance from the impact point of the bomb or rocket to the closest range target is measured and tabulated. +-- Generally, a range consists of strafe pits and bombing targets. For strafe pits the number of hits for each pass is counted and tabulated. +-- For bombing targets, the distance from the impact point of the bomb, rocket or missile to the closest range target is measured and tabulated. -- Each player can display his best results via a function in the radio menu or see the best best results from all players. -- -- When all targets have been defined in the script, the range is started by the @{#RANGE.Start}() command. @@ -101,31 +105,34 @@ -- ## Strafe Pits -- Each strafe pit can consist of multiple targets. Often one findes two or three strafe targets next to each other. -- --- A strafe pit can be added to the range by the @{#RANGE.AddStrafepit}(unitnames, boxlength, boxwidth, heading, inverseheading, goodpass, foulline) function. +-- A strafe pit can be added to the range by the @{#RANGE.AddStrafepit}(*targetnames, boxlength, boxwidth, heading, inverseheading, goodpass, foulline*) function. -- --- The first parameter defines the target. This has to be given as a lua table which contains the unit names of the targets as defined in the mission editor. +-- * The first parameter *targetnames* defines the target or targets. This has to be given as a lua table which contains the names of @{Unit} or @{Static} objects defined in the mission editor. +-- * In order to perform a valid pass on the strafe pit, the pilot has to begin his run from the correct direction. Therefore, an "approach box" is defined in front +-- of the strafe targets. The parameters *boxlength* and *boxwidth* define the size of the box while the parameter *heading* defines its direction. +-- If the parameter *heading* is passed as **nil**, the heading is automatically taken from the heading of the first target unit as defined in the ME. +-- The parameter *inverseheading* turns the heading around by 180 degrees. This is sometimes useful, since the default heading of strafe target units point in the +-- wrong/opposite direction. +-- * The parameter *goodpass* defines the number of hits a pilot has to achive during a run to be judged as a "good" pass. +-- * The last parameter *foulline* sets the distance from the pit targets to the foul line. Hit from closer than this line are not counted! -- --- In order to perform a valid pass on the strafe pit, the pilot has to begin his run from the correct direction. Therefore, an "approach box" is defined in front --- of the strafe targets. The parameters "boxlength" and "boxwidth" define the size of the box while the parameter "heading" defines its direction. --- If the parameter heading is passed as **nil**, the heading is automatically taken from the heading of the first target unit as defined in the ME. --- The parameter "inverseheading" turns the heading around by 180 degrees. This is sometimes useful, since the default heading of strafe target units point in the --- wrong/opposite direction. --- --- The parameter "goodpass" defines the number of hits a pilot has to achive during a run to be judges as a good pass. --- --- The last parameter "foulline" sets the distance from the pit targets to the foul line. Hit from closer than this line are not counted. +-- Another function to add a strafe pit is @{#RANGE.AddStrafePitGroup}(*group, boxlength, boxwidth, heading, inverseheading, goodpass, foulline*). Here, +-- the first parameter *group* is a MOOSE @{Group} object and **all** units in this group define **one** strafe pit. -- -- Finally, a valid approach has to be performed below a certain maximum altitude. The default is 914 meters (3000 ft) AGL. This is a parameter valid for all -- strafing pits of the range and can be adjusted by the @{#RANGE.SetMaxStrafeAlt}(maxalt) function. -- -- ## Bombing targets --- One ore multiple bombing targets can be added to the range by the @{#RANGE.AddBombingTargets}(unitnames goodhitrange,static) function. +-- One ore multiple bombing targets can be added to the range by the @{#RANGE.AddBombingTargets}(targetnames, goodhitrange, randommove) function. -- --- The first parameter "unitnames" has to be a lua table, which contains the names of the units as defined in the mission editor. --- --- The parameter "goodhitrange" specifies the radius around the target. If a bomb or rocket falls at a distance smaller than this number, the hit is considered to be "good". --- --- The final (optional) parameter "static" can be enabled (set to true) if static bomb targets are used rather than alive units. +-- * The first parameter *targetnames* has to be a lua table, which contains the names of @{Unit} and/or @{Static} objects defined in the mission editor. +-- Note that the @{Range} logic **automatically** determines, if a name belongs to a @{Unit} or @{Static} object now. +-- * The (optional) parameter *goodhitrange* specifies the radius around the target. If a bomb or rocket falls at a distance smaller than this number, the hit is considered to be "good". +-- * If final (optional) parameter "*randommove*" can be enabled to create moving targets. If this parameter is set to true, the units of this bombing target will randomly move within the range zone. +-- Note that there might be quirks since DCS units can get stuck in buildings etc. So it might be safer to manually define a route for the units in the mission editor if moving targets are desired. +-- +-- Another possibility to add bombing targets is the @{#RANGE.AddBombingTargetGroup}(*group, goodhitrange, randommove*) function. Here the parameter *group* is a MOOSE @{Group} object +-- and **all** units in this group are defined as bombing targets. -- -- ## Fine Tuning -- Many range parameters have good default values. However, the mission designer can change these settings easily with the supplied user functions: @@ -138,6 +145,9 @@ -- * @{#RANGE.SetStrafeTargetSmokeColor}() sets the color used to smoke strafe targets. -- * @{#RANGE.SetStrafePitSmokeColor}() sets the color used to smoke strafe pit approach boxes. -- * @{#RANGE.SetSmokeTimeDelay}() sets the time delay between smoking bomb/rocket impact points after impact. +-- * @{#RANGE.TrackBombsON}() or @{#RANGE.TrackBombsOFF}() can be used to enable/disable tracking and evaluating of all bomb types a player fires. +-- * @{#RANGE.TrackRocketsON}() or @{#RANGE.TrackRocketsOFF}() can be used to enable/disable tracking and evaluating of all rocket types a player fires. +-- * @{#RANGE.TrackMissilesON}() or @{#RANGE.TrackMissilesOFF}() can be used to enable/disable tracking and evaluating of all missile types a player fires. -- -- ## Radio Menu -- Each range gets a radio menu with various submenus where each player can adjust his individual settings or request information about the range or his scores. @@ -155,11 +165,11 @@ -- ## Examples -- -- ### Goldwater Range --- This example shows hot to set up the Barry M. Goldwater range. It consists of two strafe pits each has two targets plus three bombing targets. +-- This example shows hot to set up the [Barry M. Goldwater range](https://en.wikipedia.org/wiki/Barry_M._Goldwater_Air_Force_Range). +-- It consists of two strafe pits each has two targets plus three bombing targets. -- --- The [476th - Air Weapons Range Objects mod](http://www.476vfightergroup.com/downloads.php?do=file&id=287) is used in this example. --- --- -- Strafe pits. Each pit can consist of multiple targets. Here we have two pits and each of the pits has two targets. These are names of the corresponding units defined in the ME. +-- -- Strafe pits. Each pit can consist of multiple targets. Here we have two pits and each of the pits has two targets. +-- -- These are names of the corresponding units defined in the ME. -- local strafepit_left={"GWR Strafe Pit Left 1", "GWR Strafe Pit Left 2"} -- local strafepit_right={"GWR Strafe Pit Right 1", "GWR Strafe Pit Right 2"} -- @@ -167,16 +177,15 @@ -- local bombtargets={"GWR Bomb Target Circle Left", "GWR Bomb Target Circle Right", "GWR Bomb Target Hard"} -- -- -- Create a range object. --- local GoldwaterRange=RANGE:New("Goldwater Range") +-- GoldwaterRange=RANGE:New("Goldwater Range") -- --- -- Distance between foul line and strafe target. Note that this could also be done manually by simply measuring the distance between the target and the foul line in the ME. --- local strafe=UNIT:FindByName("GWR Strafe Pit Left 1") --- local foul=UNIT:FindByName("GWR Foul Line Left") --- local fouldist=strafe:GetCoordinate():Get2DDistance(foul:GetCoordinate()) +-- -- Distance between strafe target and foul line. You have to specify the names of the unit or static objects. +-- -- Note that this could also be done manually by simply measuring the distance between the target and the foul line in the ME. +-- GoldwaterRange:GetFoullineDistance("GWR Strafe Pit Left 1", "GWR Foul Line Left") -- -- -- Add strafe pits. Each pit (left and right) consists of two targets. -- GoldwaterRange:AddStrafePit(strafepit_left, 3000, 300, nil, true, 20, fouldist) --- GoldwaterRange:AddStrafePit(strafepit_right, 3000, 300, nil, true, 20, fouldist) +-- GoldwaterRange:AddStrafePit(strafepit_right, nil, nil, nil, true, nil, fouldist) -- -- -- Add bombing targets. A good hit is if the bomb falls less then 50 m from the target. -- GoldwaterRange:AddBombingTargets(bombtargets, 50) @@ -184,6 +193,24 @@ -- -- Start range. -- GoldwaterRange:Start() -- +-- The [476th - Air Weapons Range Objects mod](http://www.476vfightergroup.com/downloads.php?do=file&id=287) is (implicitly) used in this example. +-- +-- ## Debugging +-- +-- In case you have problems, it is always a good idea to have a look at your DCS log file. You find it in your "Saved Games" folder, so for example in +-- C:\Users\\Saved Games\DCS\Logs\dcs.log +-- All output concerning the RANGE class should have the string "RANGE" in the corresponding line. +-- +-- The verbosity of the output can be increased by adding the following lines to your script: +-- +-- BASE:TraceOnOff(true) +-- BASE:TraceLevel(1) +-- BASE:TraceClass("RANGE") +-- +-- To get even more output you can increase the trace level to 2 or even 3, c.f. @{BASE} for more details. +-- +-- The function @{#RANGE.DebugON}() can be used to send messages on screen. It also smokes all defined strafe and bombing targets, the strafe pit approach boxes and the range zone. +-- -- -- -- @field #RANGE @@ -192,7 +219,8 @@ RANGE={ Debug=false, rangename=nil, location=nil, - rangeradius=10000, + rangeradius=5000, + rangezone=nil, strafeTargets={}, bombingTargets={}, nbombtargets=0, @@ -215,8 +243,32 @@ RANGE={ scorebombdistance=1000, TdelaySmoke=3.0, eventmoose=true, + trackbombs=true, + trackrockets=true, + trackmissiles=true, } +--- Default range parameters. +-- @list Defaults +RANGE.Defaults={ + goodhitrange=25, + strafemaxalt=914, + dtBombtrack=0.005, + Tmsg=30, + ndisplayresult=10, + rangeradius=5000, + TdelaySmoke=3.0, + boxlength=3000, + boxwidth=300, + goodpass=20, + goodhitrange=25, + foulline=610, +} + +--- Global list of all defined range names. +-- @field #table Names +RANGE.Names={} + --- Main radio menu. -- @field #table MenuF10 RANGE.MenuF10={} @@ -227,10 +279,13 @@ RANGE.id="RANGE | " --- Range script version. -- @field #number version -RANGE.version="1.0.1" +RANGE.version="1.1.0" ---TODO list ---TODO: Add statics for strafe pits. +--TODO list: +--TODO: Add custom weapons, which can be specified by the user. +--TODO: Check if units are still alive. +--DONE: Add statics for strafe pits. +--DONE: Add missiles. --DONE: Convert env.info() to self:T() --DONE: Add user functions. --DONE: Rename private functions, i.e. start with _functionname. @@ -251,6 +306,7 @@ function RANGE:New(rangename) local self=BASE:Inherit(self, BASE:New()) -- #RANGE -- Get range name. + --TODO: make sure that the range name is not given twice. This would lead to problems in the F10 radio menu. self.rangename=rangename or "Practice Range" -- Debug info. @@ -274,9 +330,10 @@ function RANGE:Start() local _count=0 for _,_target in pairs(self.bombingTargets) do _count=_count+1 - --_target.name + + -- Get range location. if _location==nil then - _location=_target.point --Core.Point#COORDINATE + _location=_target.target:GetCoordinate() --Core.Point#COORDINATE end end self.nbombtargets=_count @@ -285,6 +342,7 @@ function RANGE:Start() _count=0 for _,_target in pairs(self.strafeTargets) do _count=_count+1 + for _,_unit in pairs(_target.targets) do if _location==nil then _location=_unit:GetCoordinate() @@ -293,13 +351,20 @@ function RANGE:Start() end self.nstrafetargets=_count - -- Location of the range. We simply take the first unit/target we find. - self.location=_location + -- Location of the range. We simply take the first unit/target we find if it was not explicitly specified by the user. + if self.location==nil then + self.location=_location + end if self.location==nil then local text=string.format("ERROR! No range location found. Number of strafe targets = %d. Number of bomb targets = %d.", self.rangename, self.nstrafetargets, self.nbombtargets) self:E(RANGE.id..text) - return nil + return + end + + -- Define a MOOSE zone of the range. + if self.rangezone==nil then + self.rangezone=ZONE_RADIUS:New(self.rangename, {x=self.location.x, y=self.location.z}, self.rangeradius) end -- Starting range. @@ -311,9 +376,6 @@ function RANGE:Start() if self.eventmoose then -- Events are handled my MOOSE. self:T(RANGE.id.."Events are handled by MOOSE.") - --self:HandleEvent(EVENTS.Birth, self._OnBirth) - --self:HandleEvent(EVENTS.Hit, self._OnHit) - --self:HandleEvent(EVENTS.Shot, self._OnShot) self:HandleEvent(EVENTS.Birth) self:HandleEvent(EVENTS.Hit) self:HandleEvent(EVENTS.Shot) @@ -323,6 +385,28 @@ function RANGE:Start() world.addEventHandler(self) end + -- Make bomb target move randomly within the range zone. + for _,_target in pairs(self.bombingTargets) do + + -- Check if it is a static object. + local _static=self:_CheckStatic(_target.target:GetName()) + + if _target.move and _static==false and _target.speed>1 then + local unit=_target.target --Wrapper.Unit#UNIT + _target.target:PatrolZones({self.rangezone}, _target.speed*0.75, "Off road") + end + + end + + -- Debug mode: smoke all targets and range zone. + if self.Debug then + self:_MarkTargetsOnMap() + self:_SmokeBombTargets() + self:_SmokeStrafeTargets() + self:_SmokeStrafeTargetBoxes() + self.rangezone:SmokeZone(SMOKECOLOR.White) + end + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -332,35 +416,50 @@ end -- @param #RANGE self -- @param #number maxalt Maximum altitude AGL in meters. Default is 914 m= 3000 ft. function RANGE:SetMaxStrafeAlt(maxalt) - self.strafemaxalt=maxalt or 914 + self.strafemaxalt=maxalt or RANGE.Defaults.strafemaxalt end --- Set time interval for tracking bombs. A smaller time step increases accuracy but needs more CPU time. -- @param #RANGE self -- @param #number dt Time interval in seconds. Default is 0.005 s. function RANGE:SetBombtrackTimestep(dt) - self.dtBombtrack=dt or 0.005 + self.dtBombtrack=dt or RANGE.Defaults.dtBombtrack end --- Set time how long (most) messages are displayed. -- @param #RANGE self -- @param #number time Time in seconds. Default is 30 s. function RANGE:SetMessageTimeDuration(time) - self.Tmsg=time or 30 + self.Tmsg=time or RANGE.Defaults.Tmsg end --- Set max number of player results that are displayed. -- @param #RANGE self -- @param #number nmax Number of results. Default is 10. function RANGE:SetDisplayedMaxPlayerResults(nmax) - self.ndisplayresult=nmax or 10 + self.ndisplayresult=nmax or RANGE.Defaults.ndisplayresult end --- Set range radius. Defines the area in which e.g. bomb impacts are smoked. -- @param #RANGE self --- @param #number radius Radius in km. Default 10 km. +-- @param #number radius Radius in km. Default 5 km. function RANGE:SetRangeRadius(radius) - self.rangeradius=radius*1000 or 10000 + self.rangeradius=radius*1000 or RANGE.Defaults.rangeradius +end + +--- Set range location. If this is not done, one (random) unit position of the range is used to determine the center of the range. +-- @param #RANGE self +-- @param Core.Point#COORDINATE coordinate Coordinate of the center of the range. +function RANGE:SetRangeLocation(coordinate) + self.location=coordinate +end + +--- Set range zone. For example, no bomb impact points are smoked if a bomb falls outside of this zone. +-- If a zone is not explicitly specified, the range zone is determined by its location and radius. +-- @param #RANGE self +-- @param Core.Zone#ZONE zone MOOSE zone defining the range perimeters. +function RANGE:SetRangeLocation(zone) + self.rangezone=zone end --- Set smoke color for marking bomb targets. By default bomb targets are marked by red smoke. @@ -388,7 +487,7 @@ end -- @param #RANGE self -- @param #number delay Time delay in seconds. Default is 3 seconds. function RANGE:SetSmokeTimeDelay(delay) - self.TdelaySmoke=delay or 3.0 + self.TdelaySmoke=delay or RANGE.Defaults.TdelaySmoke end --- Enable debug modus. @@ -403,24 +502,60 @@ function RANGE:DebugOFF() self.Debug=false end +--- Enables tracking of all bomb types. Note that this is the default setting. +-- @param #RANGE self +function RANGE:TrackBombsON() + self.trackbombs=true +end + +--- Disables tracking of all bomb types. +-- @param #RANGE self +function RANGE:TrackBombsOFF() + self.trackbombs=false +end + +--- Enables tracking of all rocket types. Note that this is the default setting. +-- @param #RANGE self +function RANGE:TrackRocketsON() + self.trackrockets=true +end + +--- Disables tracking of all rocket types. +-- @param #RANGE self +function RANGE:TrackRocketsOFF() + self.trackrockets=false +end + +--- Enables tracking of all missile types. Note that this is the default setting. +-- @param #RANGE self +function RANGE:TrackMissilesON() + self.trackmissiles=true +end + +--- Disables tracking of all missile types. +-- @param #RANGE self +function RANGE:TrackMissilesOFF() + self.trackmissiles=false +end + --- Add new strafe pit. For a strafe pit, hits from guns are counted. One pit can consist of several units. -- Note, an approach is only valid, if the player enters via a zone in front of the pit, which defined by boxlength and boxheading. -- Furthermore, the player must not be too high and fly in the direction of the pit to make a valid target apporoach. -- @param #RANGE self --- @param #table unitnames Table of unit names defining the strafe targets. The first target in the list determines the approach zone (heading and box). +-- @param #table targetnames Table of unit or static names defining the strafe targets. The first target in the list determines the approach zone (heading and box). -- @param #number boxlength (Optional) Length of the approach box in meters. Default is 3000 m. -- @param #number boxwidth (Optional) Width of the approach box in meters. Default is 300 m. -- @param #number heading (Optional) Approach heading in Degrees. Default is heading of the unit as defined in the mission editor. -- @param #boolean inverseheading (Optional) Take inverse heading (heading --> heading - 180 Degrees). Default is false. -- @param #number goodpass (Optional) Number of hits for a "good" strafing pass. Default is 20. -- @param #number foulline (Optional) Foul line distance. Hits from closer than this distance are not counted. Default 610 m = 2000 ft. Set to 0 for no foul line. -function RANGE:AddStrafePit(unitnames, boxlength, boxwidth, heading, inverseheading, goodpass, foulline) - self:F({unitnames=unitnames, boxlength=boxlength, boxwidth=boxwidth, heading=heading, inverseheading=inverseheading, goodpass=goodpass, foulline=foulline}) +function RANGE:AddStrafePit(targetnames, boxlength, boxwidth, heading, inverseheading, goodpass, foulline) + self:F({targetnames=targetnames, boxlength=boxlength, boxwidth=boxwidth, heading=heading, inverseheading=inverseheading, goodpass=goodpass, foulline=foulline}) -- Create table if necessary. - if type(unitnames) ~= "table" then - unitnames={unitnames} + if type(targetnames) ~= "table" then + targetnames={targetnames} end -- Make targets @@ -428,11 +563,34 @@ function RANGE:AddStrafePit(unitnames, boxlength, boxwidth, heading, inversehead local center=nil --Wrapper.Unit#UNIT local ntargets=0 - for _i,_name in ipairs(unitnames) do + for _i,_name in ipairs(targetnames) do - self:T(RANGE.id..string.format("Adding strafe target #%d %s", _i, _name)) - local unit=UNIT:FindByName(_name) + -- Check if we have a static or unit object. + local _isstatic=self:_CheckStatic(_name) + + local unit=nil + if _isstatic==true then + -- Add static object. + self:T(RANGE.id..string.format("Adding STATIC object %s as strafe target #%d.", _name, _i)) + unit=STATIC:FindByName(_name, false) + + elseif _isstatic==false then + + -- Add unit object. + self:T(RANGE.id..string.format("Adding UNIT object %s as strafe target #%d.", _name, _i)) + unit=UNIT:FindByName(_name) + + else + + -- Neither unit nor static object with this name could be found. + local text=string.format("ERROR! Could not find ANY strafe target object with name %s.", _name) + self:E(RANGE.id..text) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + + end + + -- Add object to targets. if unit then table.insert(_targets, unit) -- Define center as the first unit we find @@ -440,17 +598,21 @@ function RANGE:AddStrafePit(unitnames, boxlength, boxwidth, heading, inversehead center=unit end ntargets=ntargets+1 - else - local text=string.format("ERROR! Could not find strafe target with name %s.", _name) - self:E(RANGE.id..text) - MESSAGE:New(text, 10):ToAllIf(self.Debug) end end + + -- Check if at least one target could be found. + if ntargets==0 then + local text=string.format("ERROR! No strafe target could be found when calling RANGE:AddStrafePit() for range %s", self.rangename) + self:E(RANGE.id..text) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + return + end -- Approach box dimensions. - local l=boxlength or 3000 - local w=(boxwidth or 300)/2 + local l=boxlength or RANGE.Defaults.boxlength + local w=(boxwidth or RANGE.Defaults.boxwidth)/2 -- Heading: either manually entered or automatically taken from unit heading. local heading=heading or center:GetHeading() @@ -469,10 +631,10 @@ function RANGE:AddStrafePit(unitnames, boxlength, boxwidth, heading, inversehead end -- Number of hits called a "good" pass. - local goodpass=goodpass or 20 + goodpass=goodpass or RANGE.Defaults.goodpass -- Foule line distance. - local foulline=foulline or 610 + foulline=foulline or RANGE.Defaults.foulline -- Coordinate of the range. local Ccenter=center:GetCoordinate() @@ -499,96 +661,195 @@ function RANGE:AddStrafePit(unitnames, boxlength, boxwidth, heading, inversehead --_polygon:BoundZone() -- Add zone to table. - table.insert(self.strafeTargets, {name=_name, polygon=_polygon, goodPass=goodpass, targets=_targets, foulline=foulline, smokepoints=p, heading=heading}) + table.insert(self.strafeTargets, {name=_name, polygon=_polygon, coordinate= Ccenter, goodPass=goodpass, targets=_targets, foulline=foulline, smokepoints=p, heading=heading}) -- Debug info - local text=string.format("Adding new strafe target %s with %d targets: heading = %03d, box_L = %.1f, box_W = %.1f, goodpass = %d, foul line = %.1f", _name, ntargets, heading, boxlength, boxwidth, goodpass, foulline) + local text=string.format("Adding new strafe target %s with %d targets: heading = %03d, box_L = %.1f, box_W = %.1f, goodpass = %d, foul line = %.1f", _name, ntargets, heading, l, w, goodpass, foulline) self:T(RANGE.id..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) end ---- Add bombing target(s) to range. --- @param #RANGE self --- @param #table unitnames Table containing the unit names acting as bomb targets. --- @param #number goodhitrange (Optional) Max distance from target unit (in meters) which is considered as a good hit. Default is 25 m. --- @param #boolean static (Optional) Target is static. Default false. -function RANGE:AddBombingTargets(unitnames, goodhitrange, static) - self:F({unitnames=unitnames, goodhitrange=goodhitrange, static=static}) - -- Create a table if necessary. - if type(unitnames) ~= "table" then - unitnames={unitnames} - end - - if static == nil or static == false then - static=false - else - static=true - end - - -- Default range is 25 m. - goodhitrange=goodhitrange or 25 - - for _,name in pairs(unitnames) do - local _unit - local _static +--- Add all units of a group as one new strafe target pit. +-- For a strafe pit, hits from guns are counted. One pit can consist of several units. +-- Note, an approach is only valid, if the player enters via a zone in front of the pit, which defined by boxlength and boxheading. +-- Furthermore, the player must not be too high and fly in the direction of the pit to make a valid target apporoach. +-- @param #RANGE self +-- @param Wrapper.Group#GROUP group MOOSE group of unit names defining the strafe target pit. The first unit in the group determines the approach zone (heading and box). +-- @param #number boxlength (Optional) Length of the approach box in meters. Default is 3000 m. +-- @param #number boxwidth (Optional) Width of the approach box in meters. Default is 300 m. +-- @param #number heading (Optional) Approach heading in Degrees. Default is heading of the unit as defined in the mission editor. +-- @param #boolean inverseheading (Optional) Take inverse heading (heading --> heading - 180 Degrees). Default is false. +-- @param #number goodpass (Optional) Number of hits for a "good" strafing pass. Default is 20. +-- @param #number foulline (Optional) Foul line distance. Hits from closer than this distance are not counted. Default 610 m = 2000 ft. Set to 0 for no foul line. +function RANGE:AddStrafePitGroup(group, boxlength, boxwidth, heading, inverseheading, goodpass, foulline) + self:F({group=group, boxlength=boxlength, boxwidth=boxwidth, heading=heading, inverseheading=inverseheading, goodpass=goodpass, foulline=foulline}) + + if group and group:IsAlive() then - if static then - - -- Add static object. Workaround since cargo objects are not yet in database because DCS function does not add those. - local _DCSstatic=StaticObject.getByName(name) - if _DCSstatic and _DCSstatic:isExist() then - self:T(RANGE.id..string.format("Adding DCS static to database. Name = %s.", name)) - _DATABASE:AddStatic(name) - else - self:E(RANGE.id..string.format("ERROR! DCS static DOES NOT exist! Name = %s.", name)) - end - - -- Now we can find it... - _static=STATIC:FindByName(name) - if _static then - self:AddBombingTargetUnit(_static, goodhitrange) - self:T(RANGE.id..string.format("Adding static bombing target %s with hit range %d.", name, goodhitrange)) - else - self:E(RANGE.id..string.format("ERROR! Cound not find static bombing target %s.", name)) - end - - else + -- Get units of group. + local _units=group:GetUnits() - _unit=UNIT:FindByName(name) - if _unit then - self:AddBombingTargetUnit(_unit, goodhitrange) - self:T(RANGE.id..string.format("Adding bombing target %s with hit range %d.", name, goodhitrange)) - else - self:E(RANGE.id..string.format("ERROR! Could not find bombing target %s.", name)) + -- Make table of unit names. + local _names={} + for _,_unit in ipairs(_units) do + + local _unit=_unit --Wrapper.Unit#UNIT + + if _unit and _unit:IsAlive() then + local _name=_unit:GetName() + table.insert(_names,_name) end end + + -- Add strafe pit. + self:AddStrafePit(_names, boxlength, boxwidth, heading, inverseheading, goodpass, foulline) + end +end + +--- Add bombing target(s) to range. +-- @param #RANGE self +-- @param #table targetnames Table containing names of unit or static objects serving as bomb targets. +-- @param #number goodhitrange (Optional) Max distance from target unit (in meters) which is considered as a good hit. Default is 25 m. +-- @param #boolean randommove If true, unit will move randomly within the range. Default is false. +function RANGE:AddBombingTargets(targetnames, goodhitrange, randommove) + self:F({targetnames=targetnames, goodhitrange=goodhitrange, randommove=randommove}) + + -- Create a table if necessary. + if type(targetnames) ~= "table" then + targetnames={targetnames} + end + + -- Default range is 25 m. + goodhitrange=goodhitrange or RANGE.Defaults.goodhitrange + + for _,name in pairs(targetnames) do + + -- Check if we have a static or unit object. + local _isstatic=self:_CheckStatic(name) + + if _isstatic==true then + local _static=STATIC:FindByName(name) + self:T2(RANGE.id..string.format("Adding static bombing target %s with hit range %d.", name, goodhitrange, false)) + self:AddBombingTargetUnit(_static, goodhitrange) + elseif _isstatic==false then + local _unit=UNIT:FindByName(name) + self:T2(RANGE.id..string.format("Adding unit bombing target %s with hit range %d.", name, goodhitrange, randommove)) + self:AddBombingTargetUnit(_unit, goodhitrange) + else + self:E(RANGE.id..string.format("ERROR! Could not find bombing target %s.", name)) + end + end end ---- Add a unit as bombing target. +--- Add a unit or static object as bombing target. -- @param #RANGE self --- @param Wrapper.Unit#UNIT unit Unit of the strafe target. +-- @param Wrapper.Positionable#POSITIONABLE unit Positionable (unit or static) of the strafe target. -- @param #number goodhitrange Max distance from unit which is considered as a good hit. -function RANGE:AddBombingTargetUnit(unit, goodhitrange) - self:F({unit=unit, goodhitrange=goodhitrange}) +-- @param #boolean randommove If true, unit will move randomly within the range. Default is false. +function RANGE:AddBombingTargetUnit(unit, goodhitrange, randommove) + self:F({unit=unit, goodhitrange=goodhitrange, randommove=randommove}) - local coord=unit:GetCoordinate() + -- Get name of positionable. local name=unit:GetName() - -- Default range is 25 m. - goodhitrange=goodhitrange or 25 + -- Check if we have a static or unit object. + local _isstatic=self:_CheckStatic(name) - -- Create a zone around the unit. - local Vec2=coord:GetVec2() - local Rzone=ZONE_RADIUS:New(name, Vec2, goodhitrange) + -- Default range is 25 m. + goodhitrange=goodhitrange or RANGE.Defaults.goodhitrange + + -- Set randommove to false if it was not specified. + if randommove==nil or _isstatic==true then + randommove=false + end + + -- Debug or error output. + if _isstatic==true then + self:T(RANGE.id..string.format("Adding STATIC bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring(randommove))) + elseif _isstatic==false then + self:T(RANGE.id..string.format("Adding UNIT bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring(randommove))) + else + self:E(RANGE.id..string.format("ERROR! No bombing target with name %s could be found. Carefully check all UNIT and STATIC names defined in the mission editor!", name)) + end + + -- Get max speed of unit in km/h. + local speed=0 + if _isstatic==false then + speed=self:_GetSpeed(unit) + end -- Insert target to table. - table.insert(self.bombingTargets, {name=name, point=coord, zone=Rzone, target=unit, goodhitrange=goodhitrange}) + table.insert(self.bombingTargets, {name=name, target=unit, goodhitrange=goodhitrange, move=randommove, speed=speed}) end +--- Add all units of a group as bombing targets. +-- @param #RANGE self +-- @param Wrapper.Group#GROUP group Group of bombing targets. +-- @param #number goodhitrange Max distance from unit which is considered as a good hit. +-- @param #boolean randommove If true, unit will move randomly within the range. Default is false. +function RANGE:AddBombingTargetGroup(group, goodhitrange, randommove) + self:F({group=group, goodhitrange=goodhitrange, randommove=randommove}) + + if group then + + local _units=group:GetUnits() + + for _,_unit in pairs(_units) do + if _unit and _unit:IsAlive() then + self:AddBombingTargetUnit(_unit, goodhitrange, randommove) + end + end + end + +end + +--- Measures the foule line distance between two unit or static objects. +-- @param #RANGE self +-- @param #string namepit Name of the strafe pit target object. +-- @param #string namefoulline Name of the fould line distance marker object. +-- @return #number Foul line distance in meters. +function RANGE:GetFoullineDistance(namepit, namefoulline) + self:F({namepit=namepit, namefoulline=namefoulline}) + + -- Check if we have units or statics. + local _staticpit=self:_CheckStatic(namepit) + local _staticfoul=self:_CheckStatic(namefoulline) + + -- Get the unit or static pit object. + local pit=nil + if _staticpit==true then + pit=STATIC:FindByName(namepit, false) + elseif _staticpit==false then + pit=UNIT:FindByName(namepit) + else + self:E(RANGE.id..string.format("ERROR! Pit object %s could not be found in GetFoullineDistance function. Check the name in the ME.", namepit)) + end + + -- Get the unit or static foul line object. + local foul=nil + if _staticfoul==true then + foul=STATIC:FindByName(namefoulline, false) + elseif _staticfoul==false then + foul=UNIT:FindByName(namefoulline) + else + self:E(RANGE.id..string.format("ERROR! Foul line object %s could not be found in GetFoullineDistance function. Check the name in the ME.", namefoulline)) + end + + -- Get the distance between the two objects. + local fouldist=0 + if pit~=nil and foul~=nil then + fouldist=pit:GetCoordinate():Get2DDistance(foul:GetCoordinate()) + else + self:E(RANGE.id..string.format("ERROR! Foul line distance could not be determined. Check pit object name %s and foul line object name %s in the ME.", namepit, namefoulline)) + end + + self:T(RANGE.id..string.format("Foul line distance = %.1f m.", fouldist)) + return fouldist +end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Event Handling @@ -600,11 +861,11 @@ function RANGE:onEvent(Event) self:F3(Event) if Event == nil or Event.initiator == nil then - self:T2("Skipping onEvent. Event or Event.initiator unknown.") + self:T3("Skipping onEvent. Event or Event.initiator unknown.") return true end if Unit.getByName(Event.initiator:getName()) == nil then - self:T2("Skipping onEvent. Initiator unit name unknown.") + self:T3("Skipping onEvent. Initiator unit name unknown.") return true end @@ -646,19 +907,16 @@ function RANGE:onEvent(Event) -- Call event Birth function. if Event.id==world.event.S_EVENT_BIRTH and _playername then self:OnEventBirth(EventData) - --self:_OnBirth(EventData) end -- Call event Shot function. if Event.id==world.event.S_EVENT_SHOT and _playername and Event.weapon then self:OnEventShot(EventData) - --self:_OnShot(EventData) end -- Call event Hit function. if Event.id==world.event.S_EVENT_HIT and _playername and DCStgtunit then self:OnEventHit(EventData) - --self:_OnHit(EventData) end end @@ -668,7 +926,6 @@ end -- @param #RANGE self -- @param Core.Event#EVENTDATA EventData function RANGE:OnEventBirth(EventData) ---function RANGE:_OnBirth(EventData) self:F({eventbirth = EventData}) local _unitName=EventData.IniUnitName @@ -690,6 +947,8 @@ function RANGE:OnEventBirth(EventData) self:T(RANGE.id..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) + self:_GetAmmo(_unitName) + -- Reset current strafe status. self.strafeStatus[_uid] = nil @@ -717,7 +976,6 @@ end -- @param #RANGE self -- @param Core.Event#EVENTDATA EventData function RANGE:OnEventHit(EventData) ---function RANGE:_OnHit(EventData) self:F({eventhit = EventData}) -- Debug info. @@ -733,7 +991,7 @@ function RANGE:OnEventHit(EventData) end -- Unit ID - local _unitID = _unit:GetID() + local _unitID = _unit:GetID() -- Target local target = EventData.TgtUnit @@ -743,7 +1001,7 @@ function RANGE:OnEventHit(EventData) local _currentTarget = self.strafeStatus[_unitID] -- Player has rolled in on a strafing target. - if _currentTarget then + if _currentTarget and target:IsAlive() then local playerPos = _unit:GetCoordinate() local targetPos = target:GetCoordinate() @@ -752,7 +1010,7 @@ function RANGE:OnEventHit(EventData) for _,_target in pairs(_currentTarget.zone.targets) do -- Check the the target is the same that was actually hit. - if _target:GetName() == targetname then + if _target and _target:IsAlive() and _target:GetName() == targetname then -- Get distance between player and target. local dist=playerPos:Get2DDistance(targetPos) @@ -768,7 +1026,7 @@ function RANGE:OnEventHit(EventData) else -- Too close to the target. if _currentTarget.pastfoulline==false and _unit and _playername then - local _d=_currentTarget.zone.foulline + local _d=_currentTarget.zone.foulline local text=string.format("%s, Invalid hit!\nYou already passed foul line distance of %d m for target %s.", self:_myname(_unitName), _d, targetname) self:_DisplayMessageToGroup(_unit, text, 10) self:T2(RANGE.id..text) @@ -781,15 +1039,17 @@ function RANGE:OnEventHit(EventData) end -- Bombing Targets - for _,_target in pairs(self.bombingTargets) do + for _,_bombtarget in pairs(self.bombingTargets) do + + local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE -- Check if one of the bomb targets was hit. - if _target.name == targetname then + if _target and _target:IsAlive() and _bombtarget.name == targetname then if _unit and _playername then - local playerPos = _unit:GetCoordinate() - local targetPos = target:GetCoordinate() + -- Position of target. + local targetPos = _target:GetCoordinate() -- Message to player. --local text=string.format("%s, direct hit on target %s.", self:_myname(_unitName), targetname) @@ -809,7 +1069,6 @@ end -- @param #RANGE self -- @param Core.Event#EVENTDATA EventData function RANGE:OnEventShot(EventData) ---function RANGE:_OnShot(EventData) self:F({eventshot = EventData}) -- Weapon data. @@ -818,13 +1077,23 @@ function RANGE:OnEventShot(EventData) local _weaponName = _weaponStrArray[#_weaponStrArray] -- Debug info. - self:T3(RANGE.id.."EVENT SHOT: Ini unit = "..EventData.IniUnitName) - self:T3(RANGE.id.."EVENT SHOT: Ini group = "..EventData.IniGroupName) - self:T3(RANGE.id.."EVENT SHOT: Weapon type = ".._weapon) - self:T3(RANGE.id.."EVENT SHOT: Weapon name = ".._weaponName) + self:T(RANGE.id.."EVENT SHOT: Ini unit = "..EventData.IniUnitName) + self:T(RANGE.id.."EVENT SHOT: Ini group = "..EventData.IniGroupName) + self:T(RANGE.id.."EVENT SHOT: Weapon type = ".._weapon) + self:T(RANGE.id.."EVENT SHOT: Weapon name = ".._weaponName) - -- Monitor only bombs and rockets. - if (string.match(_weapon, "weapons.bombs") or string.match(_weapon, "weapons.nurs")) then + -- Special cases: + local _viggen=string.match(_weapon, "ROBOT") or string.match(_weapon, "RB75") or string.match(_weapon, "BK90") or string.match(_weapon, "RB15") or string.match(_weapon, "RB04") + + -- Tracking conditions for bombs, rockets and missiles. + local _bombs=string.match(_weapon, "weapons.bombs") + local _rockets=string.match(_weapon, "weapons.nurs") + local _missiles=string.match(_weapon, "weapons.missiles") or _viggen + + -- Check if any condition applies here. + local _track = (_bombs and self.trackbombs) or (_rockets and self.trackrockets) or (_missiles and self.trackmissiles) + + if _track then -- Weapon local _ordnance = EventData.weapon @@ -887,11 +1156,15 @@ function RANGE:OnEventShot(EventData) -- Loop over defined bombing targets. for _,_bombtarget in pairs(self.bombingTargets) do - -- Distance between bomb and target. - local _temp = impactcoord:Get2DDistance(_bombtarget.point) - - -- Find closest target to last known position of the bomb. - if _distance == nil or _temp < _distance then + local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE + + if _target and _target:IsAlive() then + + -- Distance between bomb and target. + local _temp = impactcoord:Get2DDistance(_target:GetCoordinate()) + + -- Find closest target to last known position of the bomb. + if _distance == nil or _temp < _distance then _distance = _temp _closetTarget = _bombtarget if _distance <= 0.5*_bombtarget.goodhitrange then @@ -903,6 +1176,8 @@ function RANGE:OnEventShot(EventData) else _hitquality = "POOR" end + + end end end @@ -1335,7 +1610,7 @@ end -- @param #RANGE self -- @param #string _unitName Name of player unit. function RANGE:_CheckInZone(_unitName) - self:F(_unitName) + self:F2(_unitName) -- Get player unit and name. local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) @@ -1364,7 +1639,7 @@ function RANGE:_CheckInZone(_unitName) -- Debug output local text=string.format("Checking stil in zone. Unit = %s, player = %s in zone = %s. alt = %d, delta heading = %d", _unitName, _playername, tostring(unitinzone), unitalt, deltaheading) - self:T(RANGE.id..text) + self:T2(RANGE.id..text) -- Check if player is in strafe zone and below max alt. if unitinzone then @@ -1390,6 +1665,9 @@ function RANGE:_CheckInZone(_unitName) else + -- Get current ammo. + local _ammo=self:_GetAmmo(_unitName) + -- Result. local _result = self.strafeStatus[_unitID] @@ -1403,9 +1681,19 @@ function RANGE:_CheckInZone(_unitName) else _result.text = "POOR PASS" end - + + -- Calculate accuracy of run. Number of hits wrt number of rounds fired. + local shots=_result.ammo-_ammo + local accur=0 + if shots>0 then + accur=_result.hits/shots*100 + end + -- Message text. local _text=string.format("%s, %s with %d hits on target %s.", self:_myname(_unitName), _result.text, _result.hits, _result.zone.name) + if shots and accur then + _text=_text..string.format("\nTotal rounds fired %d. Accuracy %.1f %%.", shots, accur) + end -- Send message. self:_DisplayMessageToGroup(_unit, _text) @@ -1442,13 +1730,16 @@ function RANGE:_CheckInZone(_unitName) -- Debug info. local text=string.format("Checking zone %s. Unit = %s, player = %s in zone = %s. alt = %d, delta heading = %d", _targetZone.name, _unitName, _playername, tostring(unitinzone), unitalt, deltaheading) - self:T(RANGE.id..text) + self:T2(RANGE.id..text) -- Player is inside zone. if unitinzone then + + -- Get ammo at the beginning of the run. + local _ammo=self:_GetAmmo(_unitName) -- Init strafe status for this player. - self.strafeStatus[_unitID] = {hits = 0, zone = _targetZone, time = 1, pastfoulline=false } + self.strafeStatus[_unitID] = {hits = 0, zone = _targetZone, time = 1, ammo=_ammo, pastfoulline=false } -- Rolling in! local _msg=string.format("%s, rolling in on strafe pit %s.", self:_myname(_unitName), _targetZone.name) @@ -1493,7 +1784,7 @@ function RANGE:_AddF10Commands(_unitName) -- Enable switch so we don't do this twice. self.MenuAddedTo[_gid] = true - -- Main F10 menu: F10/On the Range + -- Main F10 menu: F10/On the Range// if RANGE.MenuF10[_gid] == nil then RANGE.MenuF10[_gid]=missionCommands.addSubMenuForGroup(_gid, "On the Range") end @@ -1501,40 +1792,39 @@ function RANGE:_AddF10Commands(_unitName) local _statsPath = missionCommands.addSubMenuForGroup(_gid, "Statistics", _rangePath) local _markPath = missionCommands.addSubMenuForGroup(_gid, "Mark Targets", _rangePath) local _settingsPath = missionCommands.addSubMenuForGroup(_gid, "My Settings", _rangePath) - -- F10/On the Range/My Settings/ + -- F10/On the Range//My Settings/ local _mysmokePath = missionCommands.addSubMenuForGroup(_gid, "Smoke Color", _settingsPath) local _myflarePath = missionCommands.addSubMenuForGroup(_gid, "Flare Color", _settingsPath) - --TODO: Convert to MOOSE menu. - -- F10/On the Range/Mark Targets/ + -- F10/On the Range//Mark Targets/ missionCommands.addCommandForGroup(_gid, "Mark On Map", _markPath, self._MarkTargetsOnMap, self, _unitName) missionCommands.addCommandForGroup(_gid, "Illuminate Range", _markPath, self._IlluminateBombTargets, self, _unitName) missionCommands.addCommandForGroup(_gid, "Smoke Strafe Pits", _markPath, self._SmokeStrafeTargetBoxes, self, _unitName) missionCommands.addCommandForGroup(_gid, "Smoke Strafe Tgts", _markPath, self._SmokeStrafeTargets, self, _unitName) missionCommands.addCommandForGroup(_gid, "Smoke Bomb Tgts", _markPath, self._SmokeBombTargets, self, _unitName) - -- F10/On the Range/Stats/ + -- F10/On the Range//Stats/ missionCommands.addCommandForGroup(_gid, "All Strafe Results", _statsPath, self._DisplayStrafePitResults, self, _unitName) missionCommands.addCommandForGroup(_gid, "All Bombing Results", _statsPath, self._DisplayBombingResults, self, _unitName) missionCommands.addCommandForGroup(_gid, "My Strafe Results", _statsPath, self._DisplayMyStrafePitResults, self, _unitName) missionCommands.addCommandForGroup(_gid, "My Bomb Results", _statsPath, self._DisplayMyBombingResults, self, _unitName) missionCommands.addCommandForGroup(_gid, "Reset All Stats", _statsPath, self._ResetRangeStats, self, _unitName) - -- F10/On the Range/My Settings/Smoke Color/ + -- F10/On the Range//My Settings/Smoke Color/ missionCommands.addCommandForGroup(_gid, "Blue Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Blue) missionCommands.addCommandForGroup(_gid, "Green Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Green) missionCommands.addCommandForGroup(_gid, "Orange Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Orange) missionCommands.addCommandForGroup(_gid, "Red Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Red) missionCommands.addCommandForGroup(_gid, "White Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.White) - -- F10/On the Range/My Settings/Flare Color/ + -- F10/On the Range//My Settings/Flare Color/ missionCommands.addCommandForGroup(_gid, "Green Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.Green) missionCommands.addCommandForGroup(_gid, "Red Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.Red) missionCommands.addCommandForGroup(_gid, "White Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.White) missionCommands.addCommandForGroup(_gid, "Yellow Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.Yellow) - -- F10/On the Range/My Settings/ + -- F10/On the Range//My Settings/ missionCommands.addCommandForGroup(_gid, "Smoke Delay On/Off", _settingsPath, self._SmokeBombDelayOnOff, self, _unitName) missionCommands.addCommandForGroup(_gid, "Smoke Impact On/Off", _settingsPath, self._SmokeBombImpactOnOff, self, _unitName) missionCommands.addCommandForGroup(_gid, "Flare Hits On/Off", _settingsPath, self._FlareDirectHitsOnOff, self, _unitName) - -- F10/On the Range/ + -- F10/On the Range// missionCommands.addCommandForGroup(_gid, "Range Information", _rangePath, self._DisplayRangeInfo, self, _unitName) missionCommands.addCommandForGroup(_gid, "Weather Report", _rangePath, self._DisplayRangeWeather, self, _unitName) end @@ -1550,6 +1840,56 @@ end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Helper Functions +--- Get the number of shells a unit currently has. +-- @param #RANGE self +-- @param #string unitname Name of the player unit. +-- @return Number of shells left +function RANGE:_GetAmmo(unitname) + self:F2(unitname) + + -- Init counter. + local ammo=0 + + local unit, playername = self:_GetPlayerUnitAndName(unitname) + + if unit and playername then + + local has_ammo=false + + local ammotable=unit:GetAmmo() + self:T2({ammotable=ammotable}) + + if ammotable ~= nil then + + local weapons=#ammotable + self:T2(RANGE.id..string.format("Number of weapons %d.", weapons)) + + for w=1,weapons do + + local Nammo=ammotable[w]["count"] + local Tammo=ammotable[w]["desc"]["typeName"] + + -- We are specifically looking for shells here. + if string.match(Tammo, "shell") then + + -- Add up all shells + ammo=ammo+Nammo + + local text=string.format("Player %s has %d rounds ammo of type %s", playername, Nammo, Tammo) + self:T(RANGE.id..text) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + else + local text=string.format("Player %s has %d ammo of type %s", playername, Nammo, Tammo) + self:T(RANGE.id..text) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + end + end + end + end + + return ammo +end + --- Mark targets on F10 map. -- @param #RANGE self -- @param #string _unitName Name of the player unit. @@ -1557,31 +1897,45 @@ function RANGE:_MarkTargetsOnMap(_unitName) self:F(_unitName) -- Get group. - local group=UNIT:FindByName(_unitName):GetGroup() - - if group then + local group=nil + if _unitName then + group=UNIT:FindByName(_unitName):GetGroup() + end - -- Mark bomb targets. - for _,_target in pairs(self.bombingTargets) do - local coord=_target.point --Core.Point#COORDINATE - coord:MarkToGroup("Bomb target ".._target.name, group) - end - - -- Mark strafe targets. - for _,_strafepit in pairs(self.strafeTargets) do - for _,_target in pairs(_strafepit.targets) do - local coord=_target:GetCoordinate() --Core.Point#COORDINATE - coord:MarkToGroup("Strafe target ".._target:GetName(), group) + -- Mark bomb targets. + for _,_bombtarget in pairs(self.bombingTargets) do + local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE + if _target and _target:IsAlive() then + local coord=_target:GetCoordinate() --Core.Point#COORDINATE + if group then + coord:MarkToGroup("Bomb target ".._bombtarget.name, group) + else + coord:MarkToAll("Bomb target ".._bombtarget.name) end end - - if _unitName then - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) - local text=string.format("%s, %s, range targets are now marked on F10 map.", self.rangename, _playername) - self:_DisplayMessageToGroup(_unit, text, 5) - end - end + + -- Mark strafe targets. + for _,_strafepit in pairs(self.strafeTargets) do + for _,_target in pairs(_strafepit.targets) do + local _target=_target --Wrapper.Positionable#POSITIONABLE + if _target and _target:IsAlive() then + local coord=_target:GetCoordinate() --Core.Point#COORDINATE + if group then + coord:MarkToGroup("Strafe target ".._target:GetName(), group) + else + coord:MarkToAll("Strafe target ".._target:GetName()) + end + end + end + end + + if _unitName then + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + local text=string.format("%s, %s, range targets are now marked on F10 map.", self.rangename, _playername) + self:_DisplayMessageToGroup(_unit, text, 5) + end + end --- Illuminate targets. Fires illumination bombs at one random bomb and one random strafe target at a random altitude between 400 and 800 m. @@ -1593,9 +1947,12 @@ function RANGE:_IlluminateBombTargets(_unitName) -- All bombing target coordinates. local bomb={} - for _,_target in pairs(self.bombingTargets) do - local coord=_target.point --Core.Point#COORDINATE - table.insert(bomb, coord) + for _,_bombtarget in pairs(self.bombingTargets) do + local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE + if _target and _target:IsAlive() then + local coord=_target:GetCoordinate() --Core.Point#COORDINATE + table.insert(bomb, coord) + end end if #bomb>0 then @@ -1609,8 +1966,11 @@ function RANGE:_IlluminateBombTargets(_unitName) for _,_strafepit in pairs(self.strafeTargets) do for _,_target in pairs(_strafepit.targets) do - local coord=_target:GetCoordinate() --Core.Point#COORDINATE - table.insert(strafe, coord) + local _target=_target --Wrapper.Positionable#POSITIONABLE + if _target and _target:IsAlive() then + local coord=_target:GetCoordinate() --Core.Point#COORDINATE + table.insert(strafe, coord) + end end end @@ -1741,9 +2101,12 @@ end function RANGE:_SmokeBombTargets(unitname) self:F(unitname) - for _,_target in pairs(self.bombingTargets) do - local coord = _target.point --Core.Point#COORDINATE - coord:Smoke(self.BombSmokeColor) + for _,_bombtarget in pairs(self.bombingTargets) do + local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE + if _target and _target:IsAlive() then + local coord = _target:GetCoordinate() --Core.Point#COORDINATE + coord:Smoke(self.BombSmokeColor) + end end if unitname then @@ -1761,10 +2124,7 @@ function RANGE:_SmokeStrafeTargets(unitname) self:F(unitname) for _,_target in pairs(self.strafeTargets) do - for _,_unit in pairs(_target.targets) do - local coord = _unit:GetCoordinate() --Core.Point#COORDINATE - coord:Smoke(self.StrafeSmokeColor) - end + _target.coordinate:Smoke(self.StrafeSmokeColor) end if unitname then @@ -1877,6 +2237,63 @@ function RANGE:_flarecolor2text(color) return txt end +--- Checks if a static object with a certain name exists. It also added it to the MOOSE data base, if it is not already in there. +-- @param #RANGE self +-- @param #string name Name of the potential static object. +-- @return #boolean Returns true if a static with this name exists. Retruns false if a unit with this name exists. Returns nil if neither unit or static exist. +function RANGE:_CheckStatic(name) + self:F2(name) + + -- Get DCS static object. + local _DCSstatic=StaticObject.getByName(name) + + if _DCSstatic and _DCSstatic:isExist() then + + --Static does exist at least in DCS. Check if it also in the MOOSE DB. + local _MOOSEstatic=STATIC:FindByName(name, false) + + -- If static is not yet in MOOSE DB, we add it. Can happen for cargo statics! + if not _MOOSEstatic then + self:T(RANGE.id..string.format("Adding DCS static to MOOSE database. Name = %s.", name)) + _DATABASE:AddStatic(name) + end + + return true + else + self:T3(RANGE.id..string.format("No static object with name %s exists.", name)) + end + + -- Check if a unit has this name. + if UNIT:FindByName(name) then + return false + else + self:T3(RANGE.id..string.format("No unit object with name %s exists.", name)) + end + + -- If not unit or static exist, we return nil. + return nil +end + +--- Get max speed of controllable. +-- @param #RANGE self +-- @param Wrapper.Controllable#CONTROLLABLE controllable +-- @return Maximum speed in km/h. +function RANGE:_GetSpeed(controllable) + self:F2(controllable) + + -- Get DCS descriptors + local desc=controllable:GetDesc() + + -- Get speed + local speed=0 + if desc then + speed=desc.speedMax*3.6 + self:T({speed=speed}) + end + + return speed +end + --- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. -- @param #RANGE self -- @param #string _unitName Name of the player unit. @@ -1884,7 +2301,7 @@ end -- @return #string Name of the player. -- @return nil If player does not exist. function RANGE:_GetPlayerUnitAndName(_unitName) - self:F(_unitName) + self:F2(_unitName) if _unitName ~= nil then @@ -1896,7 +2313,7 @@ function RANGE:_GetPlayerUnitAndName(_unitName) local playername=DCSunit:getPlayerName() local unit=UNIT:Find(DCSunit) - self:T({DCSunit=DCSunit, unit=unit, playername=playername}) + self:T2({DCSunit=DCSunit, unit=unit, playername=playername}) if DCSunit and unit and playername then return unit, playername end @@ -1913,7 +2330,7 @@ end -- @param #RANGE self -- @param #string unitname Name of the player unit. function RANGE:_myname(unitname) - self:F(unitname) + self:F2(unitname) local unit=UNIT:FindByName(unitname) local pname=unit:GetPlayerName() @@ -1922,13 +2339,13 @@ function RANGE:_myname(unitname) return string.format("%s (%s)", csign, pname) end ---- http://stackoverflow.com/questions/1426954/split-string-in-lua +--- Split string. Cf http://stackoverflow.com/questions/1426954/split-string-in-lua -- @param #RANGE self -- @param #string str Sting to split. -- @param #string sep Speparator for split. -- @return #table Split text. function RANGE:_split(str, sep) - self:F({str=str, sep=sep}) + self:F2({str=str, sep=sep}) local result = {} local regex = ("([^%s]+)"):format(sep)