diff --git a/Moose Development/Moose/Core/SpawnStatic.lua b/Moose Development/Moose/Core/SpawnStatic.lua index e5a7b4e59..66c3581d0 100644 --- a/Moose Development/Moose/Core/SpawnStatic.lua +++ b/Moose Development/Moose/Core/SpawnStatic.lua @@ -291,7 +291,7 @@ end -- @param #SPAWNSTATIC self -- @param Core.Zone#ZONE_BASE Zone The Zone where to spawn the static. -- @param #number Heading The heading of the static, which is a number in degrees from 0 to 360. --- @param #string (optional) The name of the new static. +-- @param #string NewName (optional) The name of the new static. -- @return #SPAWNSTATIC function SPAWNSTATIC:SpawnFromZone( Zone, Heading, NewName ) --R2.1 self:F( { Zone, Heading, NewName } ) diff --git a/Moose Development/Moose/Functional/Fox.lua b/Moose Development/Moose/Functional/Fox.lua new file mode 100644 index 000000000..e6fb352f4 --- /dev/null +++ b/Moose Development/Moose/Functional/Fox.lua @@ -0,0 +1,1591 @@ +--- **Functional** - (R2.5) - Yet Another Missile Trainer. +-- +-- +-- Practice to evade missiles without being destroyed. +-- +-- +-- ## Main Features: +-- +-- * Handles air-to-air and surface-to-air missiles. +-- * Define your own training zones on the map. Players in this zone will be protected. +-- * Define launch zones. Only missiles launched in these zones are tracked. +-- * Define protected AI groups. +-- * F10 radio menu to adjust settings for each player. +-- * Alert on missile launch (optional). +-- * Marker of missile launch position (optional). +-- * Adaptive update of missile-to-player distance. +-- * Finite State Machine (FSM) implementation. +-- * Easy to use. See examples below. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Functional.FOX +-- @image Functional_FOX.png + + +--- FOX class. +-- @type FOX +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode. Messages to all about status. +-- @field #string lid Class id string for output to DCS log file. +-- @field #table menuadded Table of groups the menu was added for. +-- @field #boolean menudisabled If true, F10 menu for players is disabled. +-- @field #boolean destroy Default player setting for destroying missiles. +-- @field #boolean launchalert Default player setting for launch alerts. +-- @field #boolean marklaunch Default player setting for mark launch coordinates. +-- @field #table players Table of players. +-- @field #table missiles Table of tracked missiles. +-- @field #table safezones Table of practice zones. +-- @field #table launchzones Table of launch zones. +-- @field Core.Set#SET_GROUP protectedset Set of protected groups. +-- @field #number explosionpower Power of explostion when destroying the missile in kg TNT. Default 5 kg TNT. +-- @field #number explosiondist Missile player distance in meters for destroying the missile. Default 100 m. +-- @field #number dt50 Time step [sec] for missile position updates if distance to target > 50 km. Default 5 sec. +-- @field #number dt10 Time step [sec] for missile position updates if distance to target > 10 km and < 50 km. Default 1 sec. +-- @field #number dt05 Time step [sec] for missile position updates if distance to target > 5 km and < 10 km. Default 0.5 sec. +-- @field #number dt01 Time step [sec] for missile position updates if distance to target > 1 km and < 5 km. Default 0.1 sec. +-- @field #number dt00 Time step [sec] for missile position updates if distance to target < 1 km. Default 0.01 sec. +-- @field #boolean +-- @extends Core.Fsm#FSM + +--- Fox 3! +-- +-- === +-- +-- ![Banner Image](..\Presentations\FOX\FOX_Main.png) +-- +-- # The FOX Concept +-- +-- As you probably know [Fox](https://en.wikipedia.org/wiki/Fox_(code_word)) is a NATO brevity code for launching air-to-air munition. Therefore, the class name is not 100% accurate as this +-- script handles air-to-air but also surface-to-air missiles. +-- +-- # Basic Script +-- +-- -- Create a new missile trainer object. +-- fox=FOX:New() +-- +-- -- Start missile trainer. +-- fox:Start() +-- +-- # Training Zones +-- +-- Players are only protected if they are inside one of the training zones. +-- +-- -- Create a new missile trainer object. +-- fox=FOX:New() +-- +-- -- Add training zones. +-- fox:AddSafeZone(ZONE:New("Training Zone Alpha") +-- fox:AddSafeZone(ZONE:New("Training Zone Bravo") +-- +-- -- Start missile trainer. +-- fox:Start() +-- +-- # Launch Zones +-- +-- Missile launches are only monitored if the shooter is inside the defined launch zone. +-- +-- -- Create a new missile trainer object. +-- fox=FOX:New() +-- +-- -- Add training zones. +-- fox:AddLaunchZone(ZONE:New("Launch Zone SA-10 Krim") +-- fox:AddLaunchZone(ZONE:New("Training Zone Bravo") +-- +-- -- Start missile trainer. +-- fox:Start() +-- +-- # Protected AI Groups +-- +-- Define AI protected groups. These groups cannot be harmed by missiles. +-- +-- ## Add Individual Groups +-- +-- -- Create a new missile trainer object. +-- fox=FOX:New() +-- +-- -- Add single protected group(s). +-- fox:AddProtectedGroup(GROUP:FindByName("A-10 Protected")) +-- fox:AddProtectedGroup(GROUP:FindByName("Yak-40")) +-- +-- -- Start missile trainer. +-- fox:Start() +-- +-- # Fine Tuning +-- +-- Todo! +-- +-- # Special Events +-- +-- Todo! +-- +-- +-- @field #FOX +FOX = { + ClassName = "FOX", + Debug = false, + lid = nil, + menuadded = {}, + menudisabled = nil, + destroy = nil, + launchalert = nil, + marklaunch = nil, + missiles = {}, + players = {}, + safezones = {}, + launchzones = {}, + protectedset = nil, + explosionpower = 5, + explosiondist = 100, + destroy = nil, + dt50 = 5, + dt10 = 1, + dt05 = 0.5, + dt01 = 0.1, + dt00 = 0.01, +} + + +--- Player data table holding all important parameters of each player. +-- @type FOX.PlayerData +-- @field Wrapper.Unit#UNIT unit Aircraft of the player. +-- @field #string unitname Name of the unit. +-- @field Wrapper.Client#CLIENT client Client object of player. +-- @field #string callsign Callsign of player. +-- @field Wrapper.Group#GROUP group Aircraft group of player. +-- @field #string groupname Name of the the player aircraft group. +-- @field #string name Player name. +-- @field #number coalition Coalition number of player. +-- @field #boolean destroy Destroy missile. +-- @field #boolean launchalert Alert player on detected missile launch. +-- @field #boolean marklaunch Mark position of launched missile on F10 map. +-- @field #number defeated Number of missiles defeated. +-- @field #number dead Number of missiles not defeated. +-- @field #boolean inzone Player is inside a protected zone. + +--- Missile data table. +-- @type FOX.MissileData +-- @field Wrapper.Unit#UNIT weapon Missile weapon unit. +-- @field #boolean active If true the missile is active. +-- @field #string missileType Type of missile. +-- @field #number missileRange Range of missile in meters. +-- @field Wrapper.Unit#UNIT shooterUnit Unit that shot the missile. +-- @field Wrapper.Group#GROUP shooterGroup Group that shot the missile. +-- @field #number shooterCoalition Coalition side of the shooter. +-- @field #string shooterName Name of the shooter unit. +-- @field #number shotTime Abs mission time in seconds the missile was fired. +-- @field Core.Point#COORDINATE shotCoord Coordinate where the missile was fired. +-- @field Wrapper.Unit#UNIT targetUnit Unit that was targeted. +-- @field #FOX.PlayerData targetPlayer Player that was targeted or nil. + +--- Main radio menu on group level. +-- @field #table MenuF10 Root menu table on group level. +FOX.MenuF10={} + +--- Main radio menu on mission level. +-- @field #table MenuF10Root Root menu on mission level. +FOX.MenuF10Root=nil + +--- FOX class version. +-- @field #string version +FOX.version="0.5.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO list: +-- DONE: safe zones +-- DONE: mark shooter on F10 + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new FOX class object. +-- @param #FOX self +-- @return #FOX self. +function FOX:New() + + self.lid="FOX | " + + -- Inherit everthing from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #FOX + + -- Defaults: + self:SetDefaultMissileDestruction(true) + self:SetDefaultLaunchAlerts(true) + self:SetDefaultLaunchMarks(true) + self:SetExplosionPower() + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FOX script. + self:AddTransition("*", "Status", "*") -- Status update. + self:AddTransition("*", "MissileLaunch", "*") -- Missile was launched. + self:AddTransition("*", "MissileDestroyed", "*") -- Missile was destroyed before impact. + self:AddTransition("*", "EnterSafeZone", "*") -- Player enters a safe zone. + self:AddTransition("*", "ExitSafeZone", "*") -- Player exists a safe zone. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the FOX. Initializes parameters and starts event handlers. + -- @function [parent=#FOX] Start + -- @param #FOX self + + --- Triggers the FSM event "Start" after a delay. Starts the FOX. Initializes parameters and starts event handlers. + -- @function [parent=#FOX] __Start + -- @param #FOX self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the FOX and all its event handlers. + -- @param #FOX self + + --- Triggers the FSM event "Stop" after a delay. Stops the FOX and all its event handlers. + -- @function [parent=#FOX] __Stop + -- @param #FOX self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Status". + -- @function [parent=#FOX] Status + -- @param #FOX self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#FOX] __Status + -- @param #FOX self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "MissileLaunch". + -- @function [parent=#FOX] MissileLaunch + -- @param #FOX self + -- @param #FOX.MissileData missile Data of the fired missile. + + --- Triggers the FSM delayed event "MissileLaunch". + -- @function [parent=#FOX] __MissileLaunch + -- @param #FOX self + -- @param #number delay Delay in seconds before the function is called. + -- @param #FOX.MissileData missile Data of the fired missile. + + --- On after "MissileLaunch" event user function. Called when a missile was launched. + -- @function [parent=#FOX] OnAfterMissileLaunch + -- @param #FOX self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #FOX.MissileData missile Data of the fired missile. + + --- Triggers the FSM event "MissileDestroyed". + -- @function [parent=#FOX] MissileDestroyed + -- @param #FOX self + -- @param #FOX.MissileData missile Data of the destroyed missile. + + --- Triggers the FSM delayed event "MissileDestroyed". + -- @function [parent=#FOX] __MissileDestroyed + -- @param #FOX self + -- @param #number delay Delay in seconds before the function is called. + -- @param #FOX.MissileData missile Data of the destroyed missile. + + --- On after "MissileDestroyed" event user function. Called when a missile was destroyed. + -- @function [parent=#FOX] OnAfterMissileDestroyed + -- @param #FOX self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #FOX.MissileData missile Data of the destroyed missile. + + + --- Triggers the FSM event "EnterSafeZone". + -- @function [parent=#FOX] EnterSafeZone + -- @param #FOX self + -- @param #FOX.PlayerData player Player data. + + --- Triggers the FSM delayed event "EnterSafeZone". + -- @function [parent=#FOX] __EnterSafeZone + -- @param #FOX self + -- @param #number delay Delay in seconds before the function is called. + -- @param #FOX.PlayerData player Player data. + + --- On after "EnterSafeZone" event user function. Called when a player enters a safe zone. + -- @function [parent=#FOX] OnAfterEnterSafeZone + -- @param #FOX self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #FOX.PlayerData player Player data. + + + --- Triggers the FSM event "ExitSafeZone". + -- @function [parent=#FOX] ExitSafeZone + -- @param #FOX self + -- @param #FOX.PlayerData player Player data. + + --- Triggers the FSM delayed event "ExitSafeZone". + -- @function [parent=#FOX] __ExitSafeZone + -- @param #FOX self + -- @param #number delay Delay in seconds before the function is called. + -- @param #FOX.PlayerData player Player data. + + --- On after "ExitSafeZone" event user function. Called when a player exists a safe zone. + -- @function [parent=#FOX] OnAfterExitSafeZone + -- @param #FOX self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #FOX.PlayerData player Player data. + + + return self +end + +--- On after Start event. Starts the missile trainer and adds event handlers. +-- @param #FOX self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FOX:onafterStart(From, Event, To) + + -- Short info. + local text=string.format("Starting FOX Missile Trainer %s", FOX.version) + env.info(text) + + -- Handle events: + self:HandleEvent(EVENTS.Birth) + self:HandleEvent(EVENTS.Shot) + + if self.Debug then + self:TraceClass(self.ClassName) + self:TraceLevel(2) + end + + self:__Status(-10) +end + +--- On after Stop event. Stops the missile trainer and unhandles events. +-- @param #FOX self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FOX:onafterStop(From, Event, To) + + -- Short info. + local text=string.format("Stopping FOX Missile Trainer %s", FOX.version) + env.info(text) + + -- Handle events: + self:UnhandleEvent(EVENTS.Birth) + self:UnhandleEvent(EVENTS.Shot) + +end + +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add a training zone. Players in the zone are safe. +-- @param #FOX self +-- @param Core.Zone#ZONE zone Training zone. +-- @return #FOX self +function FOX:AddSafeZone(zone) + + table.insert(self.safezones, zone) + + return self +end + +--- Add a launch zone. Only missiles launched within these zones will be tracked. +-- @param #FOX self +-- @param Core.Zone#ZONE zone Training zone. +-- @return #FOX self +function FOX:AddLaunchZone(zone) + + table.insert(self.launchzones, zone) + + return self +end + +--- Add a protected set of groups. +-- @param #FOX self +-- @param Core.Set#SET_GROUP groupset The set of groups. +-- @return #FOX self +function FOX:SetProtectedGroupSet(groupset) + self.protectedset=groupset + return self +end + +--- Add a group to the protected set. +-- @param #FOX self +-- @param Wrapper.Group#GROUP group Protected group. +-- @return #FOX self +function FOX:AddProtectedGroup(group) + + if not self.protectedset then + self.protectedset=SET_GROUP:New() + end + + self.protectedset:AddGroup(group) + + return self +end + +--- Set explosion power. +-- @param #FOX self +-- @param #number power Explosion power in kg TNT. Default 5. +-- @return #FOX self +function FOX:SetExplosionPower(power) + + self.explosionpower=power or 5 + + return self +end + + +--- Disable F10 menu for all players. +-- @param #FOX self +-- @param #boolean switch If true debug mode on. If false/nil debug mode off +-- @return #FOX self +function FOX:SetDisableF10Menu() + + self.menudisabled=true + + return self +end + +--- Set default player setting for missile destruction. +-- @param #FOX self +-- @param #boolean switch If true missiles are destroyed. If false/nil missiles are not destroyed. +-- @return #FOX self +function FOX:SetDefaultMissileDestruction(switch) + + if switch==nil then + self.destroy=false + else + self.destroy=switch + end + + return self +end + +--- Set default player setting for launch alerts. +-- @param #FOX self +-- @param #boolean switch If true launch alerts to players are active. If false/nil no launch alerts are given. +-- @return #FOX self +function FOX:SetDefaultLaunchAlerts(switch) + + if switch==nil then + self.launchalert=false + else + self.launchalert=switch + end + + return self +end + +--- Set default player setting for marking missile launch coordinates +-- @param #FOX self +-- @param #boolean switch If true missile launches are marked. If false/nil marks are disabled. +-- @return #FOX self +function FOX:SetDefaultLaunchMarks(switch) + + if switch==nil then + self.marklaunch=false + else + self.marklaunch=switch + end + + return self +end + + +--- Set debug mode on/off. +-- @param #FOX self +-- @param #boolean switch If true debug mode on. If false/nil debug mode off. +-- @return #FOX self +function FOX:SetDebugOnOff(switch) + + if switch==nil then + self.Debug=false + else + self.Debug=switch + end + + return self +end + +--- Set debug mode on. +-- @param #FOX self +-- @return #FOX self +function FOX:SetDebugOn() + self:SetDebugOnOff(true) + return self +end + +--- Set debug mode off. +-- @param #FOX self +-- @return #FOX self +function FOX:SetDebugOff() + self:SetDebugOff(false) + return self +end + +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Status Functions +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check spawn queue and spawn aircraft if necessary. +-- @param #FOX self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FOX:onafterStatus(From, Event, To) + + -- Get FSM state. + local fsmstate=self:GetState() + + -- Status. + self:I(self.lid..string.format("Missile trainer status: %s", fsmstate)) + + -- Check missile status. + self:_CheckMissileStatus() + + -- Check player status. + self:_CheckPlayers() + + if fsmstate=="Running" then + self:__Status(-10) + end +end + +--- Check status of players. +-- @param #FOX self +function FOX:_CheckPlayers() + + for playername,_playersettings in pairs(self.players) do + local playersettings=_playersettings --#FOX.PlayerData + + local unitname=playersettings.unitname + local unit=UNIT:FindByName(unitname) + + if unit and unit:IsAlive() then + + local coord=unit:GetCoordinate() + + local issafe=self:_CheckCoordSafe(coord) + + + if issafe then + + ----------------------------- + -- Player INSIDE Safe Zone -- + ----------------------------- + + if not playersettings.inzone then + self:EnterSafeZone(playersettings) + playersettings.inzone=true + end + + else + + ------------------------------ + -- Player OUTSIDE Safe Zone -- + ------------------------------ + + if playersettings.inzone==true then + self:ExitSafeZone(playersettings) + playersettings.inzone=false + end + + end + end + end + +end + +--- Remove missile. +-- @param #FOX self +-- @param #FOX.MissileData missile Missile data. +function FOX:_RemoveMissile(missile) + + if missile then + for i,_missile in pairs(self.missiles) do + local m=_missile --#FOX.MissileData + if missile.missileName==m.missileName then + table.remove(self.missiles, i) + return + end + end + end + +end + +--- Missile status. +-- @param #FOX self +function FOX:_CheckMissileStatus() + + local text="Missiles:" + local inactive={} + for i,_missile in pairs(self.missiles) do + local missile=_missile --#FOX.MissileData + + local targetname="unkown" + if missile.targetUnit then + targetname=missile.targetUnit:GetName() + end + local playername="none" + if missile.targetPlayer then + playername=missile.targetPlayer.name + end + local active=tostring(missile.active) + local mtype=missile.missileType + local dtype=missile.missileType + local range=UTILS.MetersToNM(missile.missileRange) + + if not active then + table.insert(inactive,i) + end + local heading=self:_GetWeapongHeading(missile.weapon) + + text=text..string.format("\n[%d] %s: active=%s, range=%.1f NM, heading=%03d, target=%s, player=%s, missilename=%s", i, mtype, active, range, heading, targetname, playername, missile.missileName) + + end + self:I(self.lid..text) + + -- Remove inactive missiles. + for i=#self.missiles,1,-1 do + local missile=self.missiles[i] --#FOX.MissileData + if missile and not missile.active then + table.remove(self.missiles, i) + end + end + +end + +--- Check if missile target is protected. +-- @param #FOX self +-- @param Wrapper.Unit#UNIT targetunit Target unit. +-- @return #boolean If true, unit is protected. +function FOX:_IsProtected(targetunit) + + if not self.protectedset then + return false + end + + if targetunit and targetunit:IsAlive() then + + -- Get Group. + local targetgroup=targetunit:GetGroup() + + if targetgroup then + local targetname=targetgroup:GetName() + + for _,_group in pairs(self.protectedset:GetSetObjects()) do + local group=_group --Wrapper.Group#GROUP + + if group then + local groupname=group:GetName() + + -- Target belongs to a protected set. + if targetname==groupname then + return true + end + end + + end + end + end + + return false +end + +--- Missle launch event. +-- @param #FOX self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #FOX.MissileData missile Fired missile +function FOX:onafterMissileLaunch(From, Event, To, missile) + + -- Tracking info and init of last bomb position. + self:I(FOX.lid..string.format("FOX: Tracking %s - %s.", missile.missileType, missile.missileName)) + + -- Loop over players. + for _,_player in pairs(self.players) do + local player=_player --#FOX.PlayerData + + -- Player position. + local playerUnit=player.unit + + -- Check that player is alive and of the opposite coalition. + if playerUnit and playerUnit:IsAlive() and player.coalition~=missile.shooterCoalition then + + -- Player missile distance. + local distance=playerUnit:GetCoordinate():Get3DDistance(missile.shotCoord) + + -- Player bearing to missile. + local bearing=playerUnit:GetCoordinate():HeadingTo(missile.shotCoord) + + -- Alert that missile has been launched. + if player.launchalert then + + -- Alert directly targeted players or players that are within missile max range. + if (missile.targetPlayer and player.unitname==missile.targetPlayer.unitname) or (distance destroy missile if in safe zone. + if distance<=self.explosiondist and self:_CheckCoordSafe(targetCoord)then + + -- Destroy missile. + self:T(self.lid..string.format("Destroying missile at distance %.1f m", distance)) + _ordnance:destroy() + + -- Missile is not active any more. + missile.active=false + + -- Create event. + self:MissileDestroyed(missile) + + -- Little explosion for the visual effect. + if self.explosionpower>0 then + missileCoord:Explosion(self.explosionpower) + end + + local text=string.format("Destroying missile. %s", self:_DeadText()) + MESSAGE:New(text, 10):ToGroup(target:GetGroup()) + + -- Increase dead counter. + if missile.targetPlayer then + missile.targetPlayer.dead=missile.targetPlayer.dead+1 + end + + -- Terminate timer. + return nil + else + + -- Time step. + local dt=1.0 + if distance>50000 then + -- > 50 km + dt=self.dt50 --=5.0 + elseif distance>10000 then + -- 10-50 km + dt=self.dt10 --=1.0 + elseif distance>5000 then + -- 5-10 km + dt=self.dt05 --0.5 + elseif distance>1000 then + -- 1-5 km + dt=self.dt01 --0.1 + else + -- < 1 km + dt=self.dt00 --0.01 + end + + -- Check again in dt seconds. + return timer.getTime()+dt + end + else + + -- No target ==> terminate timer. + return nil + end + + else + + ------------------------------------- + -- Missile does not exist any more -- + ------------------------------------- + + if target then + + -- Get human player. + local player=self:_GetPlayerFromUnit(target) + + -- Check for player and distance < 10 km. + if player and player.unit:IsAlive() then -- and missileCoord and player.unit:GetCoordinate():Get3DDistance(missileCoord)<10*1000 then + local text=string.format("Missile defeated. Well done, %s!", player.name) + MESSAGE:New(text, 10):ToClient(player.client) + + -- Increase defeated counter. + player.defeated=player.defeated+1 + end + + end + + -- Missile is not active any more. + missile.active=false + + --Terminate the timer. + self:T(FOX.lid..string.format("Terminating missile track timer.")) + return nil + + end -- _status check + + end -- end function trackBomb + + -- Weapon is not yet "alife" just yet. Start timer with a little delay. + self:T(FOX.lid..string.format("Tracking of missile starts in 0.0001 seconds.")) + timer.scheduleFunction(trackMissile, missile.weapon, timer.getTime()+0.0001) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Event Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- FOX event handler for event birth. +-- @param #FOX self +-- @param Core.Event#EVENTDATA EventData +function FOX:OnEventBirth(EventData) + self:F3({eventbirth = EventData}) + + -- Nil checks. + if EventData==nil then + self:E(self.lid.."ERROR: EventData=nil in event BIRTH!") + self:E(EventData) + return + end + if EventData.IniUnit==nil then + self:E(self.lid.."ERROR: EventData.IniUnit=nil in event BIRTH!") + self:E(EventData) + return + end + + -- Player unit and name. + local _unitName=EventData.IniUnitName + local playerunit, playername=self:_GetPlayerUnitAndName(_unitName) + + -- Debug info. + self:T(self.lid.."BIRTH: unit = "..tostring(EventData.IniUnitName)) + self:T(self.lid.."BIRTH: group = "..tostring(EventData.IniGroupName)) + self:T(self.lid.."BIRTH: player = "..tostring(playername)) + + -- Check if player entered. + if playerunit and playername then + + local _uid=playerunit:GetID() + local _group=playerunit:GetGroup() + local _callsign=playerunit:GetCallsign() + + -- Debug output. + local text=string.format("Pilot %s, callsign %s entered unit %s of group %s.", playername, _callsign, _unitName, _group:GetName()) + self:T(self.lid..text) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + + -- Add F10 radio menu for player. + if not self.menudisabled then + SCHEDULER:New(nil, self._AddF10Commands, {self,_unitName}, 0.1) + end + + -- Player data. + local playerData={} --#FOX.PlayerData + + -- Player unit, client and callsign. + playerData.unit = playerunit + playerData.unitname = _unitName + playerData.group = _group + playerData.groupname = _group:GetName() + playerData.name = playername + playerData.callsign = playerData.unit:GetCallsign() + playerData.client = CLIENT:FindByName(_unitName, nil, true) + playerData.coalition = _group:GetCoalition() + + playerData.destroy=playerData.destroy or self.destroy + playerData.launchalert=playerData.launchalert or self.launchalert + playerData.marklaunch=playerData.marklaunch or self.marklaunch + + playerData.defeated=playerData.defeated or 0 + playerData.dead=playerData.dead or 0 + + -- Init player data. + self.players[playername]=playerData + + end +end + +--- FOX event handler for event shot (when a unit releases a rocket or bomb (but not a fast firing gun). +-- @param #FOX self +-- @param Core.Event#EVENTDATA EventData +function FOX:OnEventShot(EventData) + self:I({eventshot = EventData}) + + if EventData.Weapon==nil then + return + end + if EventData.IniDCSUnit==nil then + return + end + + -- Weapon data. + local _weapon = EventData.WeaponName + local _target = EventData.Weapon:getTarget() + local _targetName = "unknown" + local _targetUnit = nil --Wrapper.Unit#UNIT + + -- Weapon descriptor. + local desc=EventData.Weapon:getDesc() + self:E({desc=desc}) + + -- Weapon category: 0=Shell, 1=Missile, 2=Rocket, 3=BOMB + local weaponcategory=desc.category + + -- Missile category: 1=AAM, 2=SAM, 6=OTHER + local missilecategory=desc.missileCategory + + local missilerange=nil + if missilecategory then + missilerange=desc.rangeMaxAltMax + end + + -- Debug info. + self:E(FOX.lid.."EVENT SHOT: FOX") + self:E(FOX.lid..string.format("EVENT SHOT: Ini unit = %s", tostring(EventData.IniUnitName))) + self:E(FOX.lid..string.format("EVENT SHOT: Ini group = %s", tostring(EventData.IniGroupName))) + self:E(FOX.lid..string.format("EVENT SHOT: Weapon type = %s", tostring(_weapon))) + self:E(FOX.lid..string.format("EVENT SHOT: Weapon categ = %s", tostring(weaponcategory))) + self:E(FOX.lid..string.format("EVENT SHOT: Missil categ = %s", tostring(missilecategory))) + self:E(FOX.lid..string.format("EVENT SHOT: Missil range = %s", tostring(missilerange))) + + + -- Check if fired in launch zone. + if not self:_CheckCoordLaunch(EventData.IniUnit:GetCoordinate()) then + self:T(self.lid.."Missile was not fired in launch zone. No tracking!") + return + end + + -- Get the target unit. Note if if _target is not nil, the unit can sometimes not be found! + if _target then + self:E({target=_target}) + --_targetName=Unit.getName(_target) + --_targetUnit=UNIT:FindByName(_targetName) + _targetUnit=UNIT:Find(_target) + end + self:E(FOX.lid..string.format("EVENT SHOT: Target name = %s", tostring(_targetName))) + + -- Track missiles of type AAM=1, SAM=2 or OTHER=6 + local _track = weaponcategory==1 and missilecategory and (missilecategory==1 or missilecategory==2 or missilecategory==6) + + -- Only track missiles + if _track then + + local missile={} --#FOX.MissileData + + missile.active=true + missile.weapon=EventData.weapon + missile.missileType=_weapon + missile.missileRange=missilerange + missile.missileName=EventData.weapon:getName() + missile.shooterUnit=EventData.IniUnit + missile.shooterGroup=EventData.IniGroup + missile.shooterCoalition=EventData.IniUnit:GetCoalition() + missile.shooterName=EventData.IniUnitName + missile.shotTime=timer.getAbsTime() + missile.shotCoord=EventData.IniUnit:GetCoordinate() + missile.targetUnit=_targetUnit + missile.targetPlayer=self:_GetPlayerFromUnit(missile.targetUnit) + + -- Only track if target was a player or target is protected. + if missile.targetPlayer or self:_IsProtected(missile.targetUnit) then + + -- Add missile table. + table.insert(self.missiles, missile) + + -- Trigger MissileLaunch event. + self:__MissileLaunch(0.1, missile) + + end + + end --if _track + +end + + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- RADIO MENU Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add menu commands for player. +-- @param #FOX self +-- @param #string _unitName Name of player unit. +function FOX:_AddF10Commands(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check for player unit. + if _unit and playername then + + -- Get group and ID. + local group=_unit:GetGroup() + local gid=group:GetID() + + if group and gid then + + if not self.menuadded[gid] then + + -- Enable switch so we don't do this twice. + self.menuadded[gid]=true + + -- Set menu root path. + local _rootPath=nil + if FOX.MenuF10Root then + ------------------------ + -- MISSON LEVEL MENUE -- + ------------------------ + + -- F10/FOX/... + _rootPath=FOX.MenuF10Root + + else + ------------------------ + -- GROUP LEVEL MENUES -- + ------------------------ + + -- Main F10 menu: F10/FOX/ + if FOX.MenuF10[gid]==nil then + FOX.MenuF10[gid]=missionCommands.addSubMenuForGroup(gid, "FOX") + end + + -- F10/FOX/... + _rootPath=FOX.MenuF10[gid] + + end + + + -------------------------------- + -- F10/F FOX/F1 Help + -------------------------------- + --local _helpPath=missionCommands.addSubMenuForGroup(gid, "Help", _rootPath) + -- F10/FOX/F1 Help/ + --missionCommands.addCommandForGroup(gid, "Subtitles On/Off", _helpPath, self._SubtitlesOnOff, self, _unitName) -- F7 + --missionCommands.addCommandForGroup(gid, "Trapsheet On/Off", _helpPath, self._TrapsheetOnOff, self, _unitName) -- F8 + + ------------------------- + -- F10/F FOX/ + ------------------------- + + missionCommands.addCommandForGroup(gid, "Destroy Missiles On/Off", _rootPath, self._ToggleDestroyMissiles, self, _unitName) -- F1 + missionCommands.addCommandForGroup(gid, "Launch Alerts On/Off", _rootPath, self._ToggleLaunchAlert, self, _unitName) -- F2 + missionCommands.addCommandForGroup(gid, "Mark Launch On/Off", _rootPath, self._ToggleLaunchMark, self, _unitName) -- F3 + missionCommands.addCommandForGroup(gid, "My Status", _rootPath, self._MyStatus, self, _unitName) -- F4 + + end + else + self:E(self.lid..string.format("ERROR: Could not find group or group ID in AddF10Menu() function. Unit name: %s.", _unitName)) + end + else + self:E(self.lid..string.format("ERROR: Player unit does not exist in AddF10Menu() function. Unit name: %s.", _unitName)) + end + +end + + +--- Turn player's launch alert on/off. +-- @param #FOX self +-- @param #string _unitname Name of the player unit. +function FOX:_MyStatus(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#FOX.PlayerData + + if playerData then + + local m,mtext=self:_GetTargetMissiles(playerData.name) + + local text=string.format("Status of player %s:\n", playerData.name) + local safe=self:_CheckCoordSafe(playerData.unit:GetCoordinate()) + + text=text..string.format("Destroy missiles? %s\n", tostring(playerData.destroy)) + text=text..string.format("Launch alert? %s\n", tostring(playerData.launchalert)) + text=text..string.format("Launch marks? %s\n", tostring(playerData.marklaunch)) + text=text..string.format("Am I safe? %s\n", tostring(safe)) + text=text..string.format("Missiles defeated: %d\n", playerData.defeated) + text=text..string.format("Missiles destroyed: %d\n", playerData.dead) + text=text..string.format("Me target: %d\n%s", m, mtext) + + MESSAGE:New(text, 10, nil, true):ToClient(playerData.client) + + end + end +end + +--- Turn player's launch alert on/off. +-- @param #FOX self +-- @param #string playername Name of the player. +-- @return #number Number of missiles targeting the player. +-- @return #string Missile info. +function FOX:_GetTargetMissiles(playername) + + local text="" + local n=0 + for _,_missile in pairs(self.missiles) do + local missile=_missile --#FOX.MissileData + + if missile.targetPlayer and missile.targetPlayer.name==playername then + n=n+1 + text=text..string.format("Type %s: active %s\n", missile.missileType, tostring(missile.active)) + end + + end + + return n,text +end + +--- Turn player's launch alert on/off. +-- @param #FOX self +-- @param #string _unitname Name of the player unit. +function FOX:_ToggleLaunchAlert(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#FOX.PlayerData + + if playerData then + + -- Invert state. + playerData.launchalert=not playerData.launchalert + + -- Inform player. + local text="" + if playerData.launchalert==true then + text=string.format("%s, missile launch alerts are now ENABLED.", playerData.name) + else + text=string.format("%s, missile launch alerts are now DISABLED.", playerData.name) + end + MESSAGE:New(text, 5):ToClient(playerData.client) + + end + end +end + +--- Turn player's launch marks on/off. +-- @param #FOX self +-- @param #string _unitname Name of the player unit. +function FOX:_ToggleLaunchMark(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#FOX.PlayerData + + if playerData then + + -- Invert state. + playerData.marklaunch=not playerData.marklaunch + + -- Inform player. + local text="" + if playerData.marklaunch==true then + text=string.format("%s, missile launch marks are now ENABLED.", playerData.name) + else + text=string.format("%s, missile launch marks are now DISABLED.", playerData.name) + end + MESSAGE:New(text, 5):ToClient(playerData.client) + + end + end +end + + +--- Turn destruction of missiles on/off for player. +-- @param #FOX self +-- @param #string _unitname Name of the player unit. +function FOX:_ToggleDestroyMissiles(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#FOX.PlayerData + + if playerData then + + -- Invert state. + playerData.destroy=not playerData.destroy + + -- Inform player. + local text="" + if playerData.destroy==true then + text=string.format("%s, incoming missiles will be DESTROYED.", playerData.name) + else + text=string.format("%s, incoming missiles will NOT be DESTROYED.", playerData.name) + end + MESSAGE:New(text, 5):ToClient(playerData.client) + + end + end +end + + +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get a random text message in case you die. +-- @param #FOX self +-- @return #string Text in case you die. +function FOX:_DeadText() + + local texts={} + texts[1]="You're dead!" + texts[2]="Meet your maker!" + texts[3]="Time to meet your maker!" + texts[4]="Well, I guess that was it!" + texts[5]="Bye, bye!" + texts[6]="Cheers buddy, was nice knowing you!" + + local r=math.random(#texts) + + return texts[r] +end + + +--- Check if a coordinate lies within a safe training zone. +-- @param #FOX self +-- @param Core.Point#COORDINATE coord Coordinate to check. +-- @return #boolean True if safe. +function FOX:_CheckCoordSafe(coord) + + -- No safe zones defined ==> Everything is safe. + if #self.safezones==0 then + return true + end + + -- Loop over all zones. + for _,_zone in pairs(self.safezones) do + local zone=_zone --Core.Zone#ZONE + local inzone=zone:IsCoordinateInZone(coord) + if inzone then + return true + end + end + + return false +end + +--- Check if a coordinate lies within a launch zone. +-- @param #FOX self +-- @param Core.Point#COORDINATE coord Coordinate to check. +-- @return #boolean True if in launch zone. +function FOX:_CheckCoordLaunch(coord) + + -- No safe zones defined ==> Everything is safe. + if #self.launchzones==0 then + return true + end + + -- Loop over all zones. + for _,_zone in pairs(self.launchzones) do + local zone=_zone --Core.Zone#ZONE + local inzone=zone:IsCoordinateInZone(coord) + if inzone then + return true + end + end + + return false +end + +--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. +-- @param #FOX self +-- @param DCS#Weapon weapon The weapon. +-- @return #number Heading of weapon in degrees or -1. +function FOX:_GetWeapongHeading(weapon) + + if weapon and weapon:isExist() then + + local wp=weapon:getPosition() + + local wph = math.atan2(wp.x.z, wp.x.x) + + if wph < 0 then + wph=wph+2*math.pi + end + + wph=math.deg(wph) + + return wph + end + + return -1 +end + +--- Tell player notching headings. +-- @param #FOX self +-- @param #FOX.PlayerData playerData Player data. +-- @param DCS#Weapon weapon The weapon. +function FOX:_SayNotchingHeadings(playerData, weapon) + + if playerData and playerData.unit and playerData.unit:IsAlive() then + + local nr, nl=self:_GetNotchingHeadings(weapon) + + if nr and nl then + local text=string.format("Notching heading %03d° or %03d°", nr, nl) + MESSAGE:New(text, 5, "FOX"):ToClient(playerData.client) + end + + end + +end + +--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. +-- @param #FOX self +-- @param DCS#Weapon weapon The weapon. +-- @return #number Notching heading right, i.e. missile heading +90� +-- @return #number Notching heading left, i.e. missile heading -90�. +function FOX:_GetNotchingHeadings(weapon) + + if weapon then + + local hdg=self:_GetWeapongHeading(weapon) + + local hdg1=hdg+90 + if hdg1>360 then + hdg1=hdg1-360 + end + + local hdg2=hdg-90 + if hdg2<0 then + hdg2=hdg2+360 + end + + return hdg1, hdg2 + end + + return nil, nil +end + +--- Returns the player data from a unit name. +-- @param #FOX self +-- @param #string unitName Name of the unit. +-- @return #FOX.PlayerData Player data. +function FOX:_GetPlayerFromUnitname(unitName) + + for _,_player in pairs(self.players) do + local player=_player --#FOX.PlayerData + + if player.unitname==unitName then + return player + end + end + + return nil +end + +--- Retruns the player data from a unit. +-- @param #FOX self +-- @param Wrapper.Unit#UNIT unit +-- @return #FOX.PlayerData Player data. +function FOX:_GetPlayerFromUnit(unit) + + if unit and unit:IsAlive() then + + -- Name of the unit + local unitname=unit:GetName() + + for _,_player in pairs(self.players) do + local player=_player --#FOX.PlayerData + + if player.unitname==unitname then + return player + end + end + + end + + return nil +end + +--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. +-- @param #FOX self +-- @param #string _unitName Name of the player unit. +-- @return Wrapper.Unit#UNIT Unit of player or nil. +-- @return #string Name of the player or nil. +function FOX:_GetPlayerUnitAndName(_unitName) + self:F2(_unitName) + + if _unitName ~= nil then + + -- Get DCS unit from its name. + local DCSunit=Unit.getByName(_unitName) + + if DCSunit then + + -- Get player name if any. + local playername=DCSunit:getPlayerName() + + -- Unit object. + local unit=UNIT:Find(DCSunit) + + -- Debug. + self:T2({DCSunit=DCSunit, unit=unit, playername=playername}) + + -- Check if enverything is there. + if DCSunit and unit and playername then + self:T(self.lid..string.format("Found DCS unit %s with player %s.", tostring(_unitName), tostring(playername))) + return unit, playername + end + + end + + end + + -- Return nil if we could not find a player. + return nil,nil +end + + +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index 364a29660..eb3009ee4 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -1,39 +1,39 @@ --- **Functional** - Range Practice. --- +-- -- === --- +-- -- The RANGE class enables easy set up of bombing and strafing ranges within DCS World. --- +-- -- Implementation is based on the [Simple Range Script](https://forums.eagle.ru/showthread.php?t=157991) by [Ciribob](https://forums.eagle.ru/member.php?u=112175), which itself was motivated -- by a script by SNAFU [see here](https://forums.eagle.ru/showthread.php?t=109174). --- +-- -- [476th - Air Weapons Range Objects mod](http://www.476vfightergroup.com/downloads.php?do=file&id=287) is highly recommended for this class. --- +-- -- ## Features: -- -- * Impact points of bombs, rockets and missiles 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. +-- * 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. -- * 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. --- +-- -- More information and examples can be found below. --- +-- -- === --- --- ### [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) +-- +-- ### [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) -- ### [MOOSE - On the Range - Demonstration Video](https://www.youtube.com/watch?v=kIXcxNB9_3M) --- +-- -- === --- +-- -- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)** --- +-- -- ### Contributions: [FlightControl](https://forums.eagle.ru/member.php?u=89536), [Ciribob](https://forums.eagle.ru/member.php?u=112175) --- +-- -- === -- @module Functional.Range -- @image Range.JPG @@ -47,7 +47,7 @@ -- @field #string rangename Name of the range. -- @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 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. @@ -64,11 +64,11 @@ -- @field #number Tmsg Time [sec] messages to players are displayed. Default 30 sec. -- @field #string examinergroupname Name of the examiner group which should get all messages. -- @field #boolean examinerexclusive If true, only the examiner gets messages. If false, clients and examiner get messages. --- @field #number strafemaxalt Maximum altitude above ground for registering for a strafe run. Default is 914 m = 3000 ft. +-- @field #number strafemaxalt Maximum altitude above ground for registering for a strafe run. Default is 914 m = 3000 ft. -- @field #number ndisplayresult Number of (player) results that a displayed. Default is 10. -- @field Utilities.Utils#SMOKECOLOR BombSmokeColor Color id used for smoking bomb targets. -- @field Utilities.Utils#SMOKECOLOR StrafeSmokeColor Color id used to smoke strafe targets. --- @field Utilities.Utils#SMOKECOLOR StrafePitSmokeColor Color id used to smoke strafe pit approach boxes. +-- @field Utilities.Utils#SMOKECOLOR StrafePitSmokeColor Color id used to smoke strafe pit approach boxes. -- @field #number illuminationminalt Minimum altitude AGL in meters at which illumination bombs are fired. Default is 500 m. -- @field #number illuminationmaxalt Maximum altitude AGL in meters at which illumination bombs are fired. Default is 1000 m. -- @field #number scorebombdistance Distance from closest target up to which bomb hits are counted. Default 1000 m. @@ -83,26 +83,26 @@ --- 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" defines the name of the range. It has to be unique since this is also the name displayed in the radio menu. --- +-- -- 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. --- +-- -- **IMPORTANT** --- +-- -- Due to a DCS bug, it is not possible to directly monitor when a player enters a plane. So in a mission with client slots, it is vital that -- a player first enters as spectator or hits ESC twice and **after that** jumps into the slot of his aircraft! -- If that is not done, the script is not started correctly. This can be checked by looking at the radio menues. If the mission was entered correctly, --- there should be an "On the Range" menu items in the "F10. Other..." menu. --- +-- there should be an "On the Range" menu items in the "F10. Other..." menu. +-- -- ## Strafe Pits -- Each strafe pit can consist of multiple targets. Often one finds two or three strafe targets next to each other. --- +-- -- A strafe pit can be added to the range by the @{#RANGE.AddStrafePit}(*targetnames, boxlength, boxwidth, heading, inverseheading, goodpass, foulline*) function. --- --- * The first parameter *targetnames* defines the target or targets. This has to be given as a lua table which contains the names of @{Wrapper.Unit} or @{Static} objects 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 @{Wrapper.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. @@ -110,107 +110,107 @@ -- wrong/opposite direction. -- * The parameter *goodpass* defines the number of hits a pilot has to achieve 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! --- +-- -- 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 @{Wrapper.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}(targetnames, goodhitrange, randommove) function. --- +-- -- * The first parameter *targetnames* has to be a lua table, which contains the names of @{Wrapper.Unit} and/or @{Static} objects defined in the mission editor. -- Note that the @{Range} logic **automatically** determines, if a name belongs to a @{Wrapper.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. --- +-- 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 @{Wrapper.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: --- +-- -- * @{#RANGE.SetMaxStrafeAlt}() sets the max altitude for valid strafing runs. -- * @{#RANGE.SetMessageTimeDuration}() sets the duration how long (most) messages are displayed. -- * @{#RANGE.SetDisplayedMaxPlayerResults}() sets the number of results displayed. --- * @{#RANGE.SetRangeRadius}() defines the total range area. --- * @{#RANGE.SetBombTargetSmokeColor}() sets the color used to smoke bombing targets. +-- * @{#RANGE.SetRangeRadius}() defines the total range area. +-- * @{#RANGE.SetBombTargetSmokeColor}() sets the color used to smoke bombing targets. -- * @{#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. --- +-- -- The main range menu can be found at "F10. Other..." --> "Fxx. On the Range..." --> "F1. Your Range Name...". -- -- The range menu contains the following submenues: --- --- * "F1. Mark Targets": Various ways to mark targets. +-- +-- * "F1. Mark Targets": Various ways to mark targets. -- * "F2. My Settings": Player specific settings. -- * "F3. Stats" Player: statistics and scores. -- * "Range Information": Information about the range, such as bearing and range. Also range and player specific settings are displayed. --- * "Weather Report": Temperature, wind and QFE pressure information is provided. --- +-- * "Weather Report": Temperature, wind and QFE pressure information is provided. +-- -- ## Examples --- +-- -- ### Goldwater Range -- 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. --- +-- -- -- 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"} --- +-- -- -- Table of bombing target names. Again these are the names of the corresponding units as defined in the ME. -- local bombtargets={"GWR Bomb Target Circle Left", "GWR Bomb Target Circle Right", "GWR Bomb Target Hard"} --- +-- -- -- Create a range object. -- GoldwaterRange=RANGE:New("Goldwater Range") --- +-- -- -- 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, 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) --- +-- -- -- 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. --- +-- +-- 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. --- +-- -- Note that it can happen that the RANGE radio menu is not shown. Check that the range object is defined as a **global** variable rather than a local one. --- The could avoid the lua garbage collection to accidentally/falsely deallocate the RANGE objects. --- --- --- +-- The could avoid the lua garbage collection to accidentally/falsely deallocate the RANGE objects. +-- +-- +-- -- @field #RANGE RANGE={ ClassName = "RANGE", @@ -324,7 +324,7 @@ RANGE.TargetType={ -- @field #string quality Hit quality. -- @field #string player Player name. -- @field #string airframe Aircraft type of player. --- @field #number time Time via timer.getAbsTime() in seconds of impact. +-- @field #number time Time via timer.getAbsTime() in seconds of impact. --- Global list of all defined range names. -- @field #table Names @@ -340,7 +340,7 @@ RANGE.MenuF10Root=nil --- Range script version. -- @field #string version -RANGE.version="2.1.0" +RANGE.version="2.1.1" --TODO list: --TODO: Verbosity level for messages. @@ -367,22 +367,22 @@ function RANGE:New(rangename) -- Inherit BASE. local self=BASE:Inherit(self, FSM: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" - + -- Log id. - self.id=string.format("RANGE %s | ", self.rangename) - + self.id=string.format("RANGE %s | ", self.rangename) + -- Debug info. local text=string.format("Script version %s - creating new RANGE object %s.", RANGE.version, self.rangename) self:I(self.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) - + -- Defaults self:SetDefaultPlayerSmokeBomb() - + -- Start State. self:SetStartState("Stopped") @@ -393,10 +393,10 @@ function RANGE:New(rangename) self:AddTransition("*", "Status", "*") -- Status of RANGE script. self:AddTransition("*", "Impact", "*") -- Impact of bomb/rocket/missile. self:AddTransition("*", "EnterRange", "*") -- Player enters the range. - self:AddTransition("*", "ExitRange", "*") -- Player leaves the range. + self:AddTransition("*", "ExitRange", "*") -- Player leaves the range. self:AddTransition("*", "Save", "*") -- Save player results. self:AddTransition("*", "Load", "*") -- Load player results. - + ------------------------ --- Pseudo Functions --- ------------------------ @@ -486,7 +486,7 @@ function RANGE:New(rangename) -- @param #string Event Event. -- @param #string To To state. -- @param #RANGE.Playerdata player Data of player settings etc. - + -- Return object. return self end @@ -500,12 +500,12 @@ function RANGE:onafterStart() -- Location/coordinate of range. local _location=nil - + -- Count bomb targets. local _count=0 for _,_target in pairs(self.bombingTargets) do _count=_count+1 - + -- Get range location. if _location==nil then _location=self:_GetBombTargetCoordinate(_target) @@ -517,7 +517,7 @@ function RANGE:onafterStart() _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() @@ -525,28 +525,28 @@ function RANGE:onafterStart() end end self.nstrafetargets=_count - + -- 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) + local text=string.format("ERROR! No range location found. Number of strafe targets = %d. Number of bomb targets = %d.", self.nstrafetargets, self.nbombtargets) self:E(self.id..text) 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. local text=string.format("Starting RANGE %s. Number of strafe targets = %d. Number of bomb targets = %d.", self.rangename, self.nstrafetargets, self.nbombtargets) self:I(self.id..text) MESSAGE:New(text,10):ToAllIf(self.Debug) - + -- Event handling. if self.eventmoose then -- Events are handled my MOOSE. @@ -559,21 +559,21 @@ function RANGE:onafterStart() self:T(self.id.."Events are handled directly by DCS.") 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()) local _static=_target.type==RANGE.TargetType.STATIC - + 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() @@ -582,7 +582,7 @@ function RANGE:onafterStart() self:_SmokeStrafeTargetBoxes() self.rangezone:SmokeZone(SMOKECOLOR.White) end - + self:__Status(-60) end @@ -685,7 +685,7 @@ function RANGE:SetBombtrackThreshold(distance) end --- Set range location. If this is not done, one (random) unit position of the range is used to determine the location of the range. --- The range location determines the position at which the weather data is evaluated. +-- The range location determines the position at which the weather data is evaluated. -- @param #RANGE self -- @param Core.Point#COORDINATE coordinate Coordinate of the range. -- @return #RANGE self @@ -846,44 +846,44 @@ end 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. + -- Create table if necessary. if type(targetnames) ~= "table" then targetnames={targetnames} end - + -- Make targets local _targets={} local center=nil --Wrapper.Unit#UNIT local ntargets=0 - + for _i,_name in ipairs(targetnames) do - + -- Check if we have a static or unit object. local _isstatic=self:_CheckStatic(_name) - local unit=nil + local unit=nil if _isstatic==true then - + -- Add static object. self:T(self.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(self.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(self.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) - + end - - -- Add object to targets. + + -- Add object to targets. if unit then table.insert(_targets, unit) -- Define center as the first unit we find @@ -892,24 +892,24 @@ function RANGE:AddStrafePit(targetnames, boxlength, boxwidth, heading, inversehe end ntargets=ntargets+1 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(self.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) - return + return end -- Approach box dimensions. 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() - + -- Invert the heading since some units point in the "wrong" direction. In particular the strafe pit from 476th range objects. if inverseheading ~= nil then if inverseheading then @@ -922,37 +922,37 @@ function RANGE:AddStrafePit(targetnames, boxlength, boxwidth, heading, inversehe if heading>360 then heading=heading-360 end - + -- Number of hits called a "good" pass. goodpass=goodpass or RANGE.Defaults.goodpass - + -- Foule line distance. foulline=foulline or RANGE.Defaults.foulline - + -- Coordinate of the range. local Ccenter=center:GetCoordinate() - + -- Name of the target defined as its unit name. local _name=center:GetName() - -- Points defining the approach area. + -- Points defining the approach area. local p={} p[#p+1]=Ccenter:Translate( w, heading+90) p[#p+1]= p[#p]:Translate( l, heading) p[#p+1]= p[#p]:Translate(2*w, heading-90) p[#p+1]= p[#p]:Translate( -l, heading) - + local pv2={} for i,p in ipairs(p) do pv2[i]={x=p.x, y=p.z} end - + -- Create polygon zone. local _polygon=ZONE_POLYGON_BASE:New(_name, pv2) - + -- Create tires --_polygon:BoundZone() - + local st={} --#RANGE.StrafeTarget st.name=_name st.polygon=_polygon @@ -962,15 +962,15 @@ function RANGE:AddStrafePit(targetnames, boxlength, boxwidth, heading, inversehe st.foulline=foulline st.smokepoints=p st.heading=heading - + -- Add zone to table. table.insert(self.strafeTargets, st) - + -- 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, l, w, 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(self.id..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) - + return self end @@ -992,25 +992,25 @@ function RANGE:AddStrafePitGroup(group, boxlength, boxwidth, heading, inversehea self:F({group=group, boxlength=boxlength, boxwidth=boxwidth, heading=heading, inverseheading=inverseheading, goodpass=goodpass, foulline=foulline}) if group and group:IsAlive() then - + -- Get units of group. local _units=group:GetUnits() - + -- 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) + self:AddStrafePit(_names, boxlength, boxwidth, heading, inverseheading, goodpass, foulline) end return self @@ -1029,15 +1029,15 @@ function RANGE:AddBombingTargets(targetnames, goodhitrange, randommove) 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(self.id..string.format("Adding static bombing target %s with hit range %d.", name, goodhitrange, false)) @@ -1049,9 +1049,9 @@ function RANGE:AddBombingTargets(targetnames, goodhitrange, randommove) else self:E(self.id..string.format("ERROR! Could not find bombing target %s.", name)) end - + end - + return self end @@ -1063,21 +1063,21 @@ end -- @return #RANGE self function RANGE:AddBombingTargetUnit(unit, goodhitrange, randommove) self:F({unit=unit, goodhitrange=goodhitrange, randommove=randommove}) - - -- Get name of positionable. + + -- Get name of positionable. local name=unit:GetName() - + -- Check if we have a static or unit object. local _isstatic=self:_CheckStatic(name) - + -- 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 - + end + -- Debug or error output. if _isstatic==true then self:T(self.id..string.format("Adding STATIC bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring(randommove))) @@ -1086,16 +1086,16 @@ function RANGE:AddBombingTargetUnit(unit, goodhitrange, randommove) else self:E(self.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 - + local target={} --#RANGE.BombTarget target.name=name - target.target=target + target.target=unit target.goodhitrange=goodhitrange target.move=randommove target.speed=speed @@ -1105,15 +1105,15 @@ function RANGE:AddBombingTargetUnit(unit, goodhitrange, randommove) else target.type=RANGE.TargetType.UNIT end - + -- Insert target to table. table.insert(self.bombingTargets, target) - + return self end ---- Add a coordinate of a bombing target. This +--- Add a coordinate of a bombing target. This -- @param #RANGE self -- @param Core.Point#COORDINATE coord The coordinate. -- @param #string name Name of target. @@ -1129,9 +1129,9 @@ function RANGE:AddBombingTargetCoordinate(coord, name, goodhitrange) target.speed=0 target.coordinate=coord target.type=RANGE.TargetType.COORD - + -- Insert target to table. - table.insert(self.bombingTargets, target) + table.insert(self.bombingTargets, target) return self end @@ -1144,18 +1144,18 @@ end -- @return #RANGE self 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 - + return self end @@ -1167,10 +1167,10 @@ end function RANGE:GetFoullineDistance(namepit, namefoulline) self:F({namepit=namepit, namefoulline=namefoulline}) - -- Check if we have units or statics. + -- 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 @@ -1180,7 +1180,7 @@ function RANGE:GetFoullineDistance(namepit, namefoulline) else self:E(self.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 @@ -1190,7 +1190,7 @@ function RANGE:GetFoullineDistance(namepit, namefoulline) else self:E(self.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 @@ -1229,26 +1229,26 @@ function RANGE:onEvent(Event) local EventData={} local _playerunit=nil local _playername=nil - + if Event.initiator then EventData.IniUnitName = Event.initiator:getName() EventData.IniDCSGroup = Event.initiator:getGroup() EventData.IniGroupName = Event.initiator:getGroup():getName() - -- Get player unit and name. This returns nil,nil if the event was not fired by a player unit. And these are the only events we are interested in. - _playerunit, _playername = self:_GetPlayerUnitAndName(EventData.IniUnitName) + -- Get player unit and name. This returns nil,nil if the event was not fired by a player unit. And these are the only events we are interested in. + _playerunit, _playername = self:_GetPlayerUnitAndName(EventData.IniUnitName) end - if Event.target then + if Event.target then EventData.TgtUnitName = Event.target:getName() EventData.TgtUnit = UNIT:FindByName(EventData.TgtUnitName) end - + if Event.weapon then EventData.Weapon = Event.weapon EventData.weapon = Event.weapon EventData.WeaponTypeName = Event.weapon:getTypeName() - end - + end + -- Event info. self:T3(self.id..string.format("EVENT: Event in onEvent with ID = %s", tostring(Event.id))) self:T3(self.id..string.format("EVENT: Ini unit = %s" , tostring(EventData.IniUnitName))) @@ -1256,22 +1256,22 @@ function RANGE:onEvent(Event) self:T3(self.id..string.format("EVENT: Ini player = %s" , tostring(_playername))) self:T3(self.id..string.format("EVENT: Tgt unit = %s" , tostring(EventData.TgtUnitName))) self:T3(self.id..string.format("EVENT: Wpn type = %s" , tostring(EventData.WeaponTypeName))) - + -- Call event Birth function. if Event.id==world.event.S_EVENT_BIRTH and _playername then self:OnEventBirth(EventData) end - + -- Call event Shot function. if Event.id==world.event.S_EVENT_SHOT and _playername and Event.weapon then self:OnEventShot(EventData) end - + -- Call event Hit function. if Event.id==world.event.S_EVENT_HIT and _playername and DCStgtunit then self:OnEventHit(EventData) end - + end @@ -1280,32 +1280,32 @@ end -- @param Core.Event#EVENTDATA EventData function RANGE:OnEventBirth(EventData) self:F({eventbirth = EventData}) - - local _unitName=EventData.IniUnitName + + local _unitName=EventData.IniUnitName local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) - + self:T3(self.id.."BIRTH: unit = "..tostring(EventData.IniUnitName)) self:T3(self.id.."BIRTH: group = "..tostring(EventData.IniGroupName)) - self:T3(self.id.."BIRTH: player = "..tostring(_playername)) - + self:T3(self.id.."BIRTH: player = "..tostring(_playername)) + if _unit and _playername then - + local _uid=_unit:GetID() local _group=_unit:GetGroup() local _gid=_group:GetID() local _callsign=_unit:GetCallsign() - + -- Debug output. local text=string.format("Player %s, callsign %s entered unit %s (UID %d) of group %s (GID %d)", _playername, _callsign, _unitName, _uid, _group:GetName(), _gid) self:T(self.id..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) - + -- Reset current strafe status. self.strafeStatus[_uid] = nil - + -- Add Menu commands after a delay of 0.1 seconds. SCHEDULER:New(nil, self._AddF10Commands, {self,_unitName}, 0.1) - + -- By default, some bomb impact points and do not flare each hit on target. self.PlayerSettings[_playername]={} --#RANGE.PlayerData self.PlayerSettings[_playername].smokebombimpact=self.defaultsmokebomb @@ -1319,14 +1319,14 @@ function RANGE:OnEventBirth(EventData) self.PlayerSettings[_playername].playername=_playername self.PlayerSettings[_playername].airframe=EventData.IniUnit:GetTypeName() self.PlayerSettings[_playername].inzone=false - + -- Start check in zone timer. if self.planes[_uid] ~= true then SCHEDULER:New(nil, self._CheckInZone, {self, EventData.IniUnitName}, 1, 1) self.planes[_uid] = true end - - end + + end end --- Range event handler for event hit. @@ -1334,7 +1334,7 @@ end -- @param Core.Event#EVENTDATA EventData function RANGE:OnEventHit(EventData) self:F({eventhit = EventData}) - + -- Debug info. self:T3(self.id.."HIT: Ini unit = "..tostring(EventData.IniUnitName)) self:T3(self.id.."HIT: Ini group = "..tostring(EventData.IniGroupName)) @@ -1346,43 +1346,43 @@ function RANGE:OnEventHit(EventData) if _unit==nil or _playername==nil then return end - + -- Unit ID local _unitID = _unit:GetID() -- Target local target = EventData.TgtUnit local targetname = EventData.TgtUnitName - + -- Current strafe target of player. local _currentTarget = self.strafeStatus[_unitID] -- Player has rolled in on a strafing target. if _currentTarget and target:IsAlive() then - + local playerPos = _unit:GetCoordinate() local targetPos = target:GetCoordinate() -- Loop over valid targets for this run. for _,_target in pairs(_currentTarget.zone.targets) do - + -- Check the the target is the same that was actually hit. if _target and _target:IsAlive() and _target:GetName() == targetname then - + -- Get distance between player and target. local dist=playerPos:Get2DDistance(targetPos) - - if dist > _currentTarget.zone.foulline then + + if dist > _currentTarget.zone.foulline then -- Increase hit counter of this run. _currentTarget.hits = _currentTarget.hits + 1 - + -- Flare target. if _unit and _playername and self.PlayerSettings[_playername].flaredirecthits then targetPos:Flare(self.PlayerSettings[_playername].flarecolor) end else -- Too close to the target. - if _currentTarget.pastfoulline==false and _unit and _playername then + if _currentTarget.pastfoulline==false and _unit and _playername then 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) @@ -1390,92 +1390,92 @@ function RANGE:OnEventHit(EventData) _currentTarget.pastfoulline=true end end - + end end end - + -- Bombing Targets 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 and _target:IsAlive() and _bombtarget.name == targetname then - + if _unit and _playername then - + -- Position of target. local targetPos = _target:GetCoordinate() - + -- Message to player. --local text=string.format("%s, direct hit on target %s.", self:_myname(_unitName), targetname) --self:DisplayMessageToGroup(_unit, text, 10, true) - + -- Flare target. if self.PlayerSettings[_playername].flaredirecthits then targetPos:Flare(self.PlayerSettings[_playername].flarecolor) end - + end end end end ---- Range event handler for event shot (when a unit releases a rocket or bomb (but not a fast firing gun). +--- Range event handler for event shot (when a unit releases a rocket or bomb (but not a fast firing gun). -- @param #RANGE self -- @param Core.Event#EVENTDATA EventData function RANGE:OnEventShot(EventData) self:F({eventshot = EventData}) - + -- Nil checks. if EventData.Weapon==nil then return end if EventData.IniDCSUnit==nil then return - end - + end + -- Weapon data. local _weapon = EventData.Weapon:getTypeName() -- should be the same as Event.WeaponTypeName local _weaponStrArray = UTILS.Split(_weapon,"%.") local _weaponName = _weaponStrArray[#_weaponStrArray] - + -- Weapon descriptor. local desc=EventData.Weapon:getDesc() - + -- Weapon category: 0=SHELL, 1=MISSILE, 2=ROCKET, 3=BOMB (Weapon.Category.X) local weaponcategory=desc.category - + -- Debug info. self:T(self.id.."EVENT SHOT: Range "..self.rangename) self:T(self.id.."EVENT SHOT: Ini unit = "..EventData.IniUnitName) self:T(self.id.."EVENT SHOT: Ini group = "..EventData.IniGroupName) self:T(self.id.."EVENT SHOT: Weapon type = ".._weapon) self:T(self.id.."EVENT SHOT: Weapon name = ".._weaponName) - self:T(self.id.."EVENT SHOT: Weapon cate = "..weaponcategory) - + self:T(self.id.."EVENT SHOT: Weapon cate = "..weaponcategory) + -- 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 = weaponcategory==Weapon.Category.BOMB --string.match(_weapon, "weapons.bombs") - local _rockets = weaponcategory==Weapon.Category.ROCKET --string.match(_weapon, "weapons.nurs") + local _bombs = weaponcategory==Weapon.Category.BOMB --string.match(_weapon, "weapons.bombs") + local _rockets = weaponcategory==Weapon.Category.ROCKET --string.match(_weapon, "weapons.nurs") local _missiles = weaponcategory==Weapon.Category.MISSILE --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) - + -- Get unit name. local _unitName = EventData.IniUnitName - + -- Get player unit and name. local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) -- Set this to larger value than the threshold. local dPR=self.BombtrackThreshold*2 - - -- Distance player to range. + + -- Distance player to range. if _unit and _playername then dPR=_unit:GetCoordinate():Get2DDistance(self.location) self:T(self.id..string.format("Range %s, player %s, player-range distance = %d km.", self.rangename, _playername, dPR/1000)) @@ -1483,16 +1483,16 @@ function RANGE:OnEventShot(EventData) -- Only track if distance player to range is < 25 km. Also check that a player shot. No need to track AI weapons. if _track and dPR<=self.BombtrackThreshold and _unit and _playername then - + -- Player data. local playerData=self.PlayerSettings[_playername] --#RANGE.PlayerData -- Tracking info and init of last bomb position. self:T(self.id..string.format("RANGE %s: Tracking %s - %s.", self.rangename, _weapon, EventData.weapon:getName())) - + -- Init bomb position. local _lastBombPos = {x=0,y=0,z=0} - + -- Function monitoring the position of a bomb until impact. local function trackBomb(_ordnance) @@ -1504,43 +1504,43 @@ function RANGE:OnEventShot(EventData) self:T2(self.id..string.format("Range %s: Bomb still in air: %s", self.rangename, tostring(_status))) if _status then - + ---------------------------- -- Weapon is still in air -- ---------------------------- - + -- Remember this position. _lastBombPos = {x = _bombPos.x, y = _bombPos.y, z= _bombPos.z } -- Check again in ~0.005 seconds ==> 200 checks per second. return timer.getTime() + self.dtBombtrack - + else - + ----------------------------- -- Bomb did hit the ground -- ----------------------------- - + -- Get closet target to last position. local _closetTarget=nil --#RANGE.BombTarget local _distance=nil local _closeCoord=nil local _hitquality="POOR" - + -- Get callsign. local _callsign=self:_myname(_unitName) - + -- Coordinate of impact point. local impactcoord=COORDINATE:NewFromVec3(_lastBombPos) - + -- Check if impact happened in range zone. local insidezone=self.rangezone:IsCoordinateInZone(impactcoord) - + -- Impact point of bomb. if self.Debug then impactcoord:MarkToAll("Bomb impact point") end - + -- Smoke impact point of bomb. if playerData.smokebombimpact and insidezone then if playerData.delaysmoke then @@ -1549,18 +1549,18 @@ function RANGE:OnEventShot(EventData) impactcoord:Smoke(playerData.smokecolor) end end - + -- Loop over defined bombing targets. for _,_bombtarget in pairs(self.bombingTargets) do - -- Get target coordinate. + -- Get target coordinate. local targetcoord=self:_GetBombTargetCoordinate(_bombtarget) - + if targetcoord then - + -- Distance between bomb and target. local _temp = impactcoord:Get2DDistance(targetcoord) - + -- Find closest target to last known position of the bomb. if _distance == nil or _temp < _distance then _distance = _temp @@ -1575,7 +1575,7 @@ function RANGE:OnEventShot(EventData) else _hitquality = "POOR" end - + end end end @@ -1589,7 +1589,7 @@ function RANGE:OnEventShot(EventData) -- Local results. local _results=self.bombPlayerResults[_playername] - + local result={} --#RANGE.BombResult result.name=_closetTarget.name or "unknown" result.distance=_distance @@ -1599,37 +1599,37 @@ function RANGE:OnEventShot(EventData) result.player=playerData.playername result.time=timer.getAbsTime() result.airframe=playerData.airframe - + -- Add to table. table.insert(_results, result) - + -- Call impact. self:Impact(result, playerData) elseif insidezone then - + -- Send message. local _message=string.format("%s, weapon fell more than %.1f km away from nearest range target. No score!", _callsign, self.scorebombdistance/1000) self:_DisplayMessageToGroup(_unit, _message, nil, false) - + else self:T(self.id.."Weapon impacted outside range zone.") end - + --Terminate the timer self:T(self.id..string.format("Range %s, player %s: Terminating bomb track timer.", self.rangename, _playername)) return nil end -- _status check - + end -- end function trackBomb -- Weapon is not yet "alife" just yet. Start timer in one second. self:T(self.id..string.format("Range %s, player %s: Tracking of weapon starts in 0.1 seconds.", self.rangename, _playername)) timer.scheduleFunction(trackBomb, EventData.weapon, timer.getTime()+0.1) - + end --if _track (string.match) and player-range distance < threshold. - + end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1645,10 +1645,10 @@ function RANGE:onafterStatus(From, Event, To) -- Check range status. self:I(self.id..string.format("Range status: %s", self:GetState())) - + -- Check player status. self:_CheckPlayers() - + -- Save results. if self.autosafe then self:Save() @@ -1682,14 +1682,14 @@ function RANGE:onafterImpact(From, Event, To, result, player) text=text.."." end text=text..string.format(" %s hit.", result.quality) - + -- Unit. local unit=UNIT:FindByName(player.unitname) - + -- Send message. self:_DisplayMessageToGroup(unit, text, nil, true) self:T(self.id..text) - + end --- Function called before save event. Checks that io and lfs are desanitized. @@ -1723,7 +1723,7 @@ function RANGE:onafterSave(From, Event, To) self:E(self.id..string.format("ERROR: Could not save results to file %s", tostring(filename))) end end - + -- Path. local path=lfs.writedir() @@ -1732,10 +1732,10 @@ function RANGE:onafterSave(From, Event, To) -- Header line. local scores="Name,Pass,Target,Distance,Radial,Quality,Weapon,Airframe,Mission Time" - + -- Loop over all players. for playername,results in pairs(self.bombPlayerResults) do - + -- Loop over player grades table. for i,_result in pairs(results) do local result=_result --#RANGE.BombResult @@ -1749,7 +1749,7 @@ function RANGE:onafterSave(From, Event, To) scores=scores..string.format("\n%s,%d,%s,%.2f,%03d,%s,%s,%s,%s", playername, i, target, distance, radial, quality, weapon, airframe, time) end end - + _savefile(filename, scores) end @@ -1787,10 +1787,10 @@ function RANGE:onafterLoad(From, Event, To) return nil end end - + -- Path. - local path=lfs.writedir() - + local path=lfs.writedir() + -- Set file name. local filename=path..string.format("\\RANGE-%s_BombingResults.csv", self.rangename) @@ -1800,45 +1800,45 @@ function RANGE:onafterLoad(From, Event, To) -- Load asset data from file. local data=_loadfile(filename) - + if data then -- Split by line break. local results=UTILS.Split(data,"\n") - + -- Remove first header line. table.remove(results, 1) - + -- Init player scores table. self.bombPlayerResults={} - + -- Loop over all lines. for _,_result in pairs(results) do - + -- Parameters are separated by commata. local resultdata=UTILS.Split(_result, ",") - + -- Grade table local result={} --#RANGE.BombResult - + -- Player name. local playername=resultdata[1] result.player=playername - + -- Results data. result.name=tostring(resultdata[3]) - result.distance=tonumber(resultdata[4]) + result.distance=tonumber(resultdata[4]) result.radial=tonumber(resultdata[5]) result.quality=tostring(resultdata[6]) result.weapon=tostring(resultdata[7]) result.airframe=tostring(resultdata[8]) result.time=UTILS.ClockToSeconds(resultdata[9] or "00:00:00") - + -- Create player array if necessary. self.bombPlayerResults[playername]=self.bombPlayerResults[playername] or {} - + -- Add result to table. - table.insert(self.bombPlayerResults[playername], result) + table.insert(self.bombPlayerResults[playername], result) end end end @@ -1858,57 +1858,57 @@ end -- @param #string _unitName Name of the player unit. function RANGE:_DisplayMyStrafePitResults(_unitName) self:F(_unitName) - + -- Get player unit and name local _unit,_playername = self:_GetPlayerUnitAndName(_unitName) - + if _unit and _playername then - + -- Message header. local _message = string.format("My Top %d Strafe Pit Results:\n", self.ndisplayresult) - + -- Get player results. local _results = self.strafePlayerResults[_playername] - + -- Create message. if _results == nil then -- No score yet. _message = string.format("%s: No Score yet.", _playername) else - + -- Sort results table wrt number of hits. local _sort = function( a,b ) return a.hits > b.hits end table.sort(_results,_sort) - + -- Prepare message of best results. local _bestMsg = "" local _count = 1 - + -- Loop over results for _,_result in pairs(_results) do - + -- Message text. _message = _message..string.format("\n[%d] Hits %d - %s - %s", _count, _result.hits, _result.zone.name, _result.text) - + -- Best result. - if _bestMsg == "" then + if _bestMsg == "" then _bestMsg = string.format("Hits %d - %s - %s", _result.hits, _result.zone.name, _result.text) end - + -- 10 runs if _count == self.ndisplayresult then break end - + -- Increase counter _count = _count+1 end - + -- Message text. _message = _message .."\n\nBEST: ".._bestMsg end - -- Send message to group. + -- Send message to group. self:_DisplayMessageToGroup(_unit, _message, nil, true, true) end end @@ -1918,52 +1918,52 @@ end -- @param #string _unitName Name fo the player unit. function RANGE:_DisplayStrafePitResults(_unitName) self:F(_unitName) - + -- Get player unit and name. local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) - + -- Check if we have a unit which is a player. if _unit and _playername then - + -- Results table. local _playerResults = {} - + -- Message text. local _message = string.format("Strafe Pit Results - Top %d Players:\n", self.ndisplayresult) - + -- Loop over player results. for _playerName,_results in pairs(self.strafePlayerResults) do - + -- Get the best result of the player. local _best = nil - for _,_result in pairs(_results) do + for _,_result in pairs(_results) do if _best == nil or _result.hits > _best.hits then _best = _result end end - - -- Add best result to table. + + -- Add best result to table. if _best ~= nil then local text=string.format("%s: Hits %i - %s - %s", _playerName, _best.hits, _best.zone.name, _best.text) table.insert(_playerResults,{msg = text, hits = _best.hits}) end - + end - + --Sort list! local _sort = function( a,b ) return a.hits > b.hits end table.sort(_playerResults,_sort) - + -- Add top 10 results. for _i = 1, math.min(#_playerResults, self.ndisplayresult) do _message = _message..string.format("\n[%d] %s", _i, _playerResults[_i].msg) end - + -- In case there are no scores yet. if #_playerResults<1 then _message = _message.."No player scored yet." end - + -- Send message. self:_DisplayMessageToGroup(_unit, _message, nil, true, true) end @@ -1975,50 +1975,50 @@ end function RANGE:_DisplayMyBombingResults(_unitName) self:F(_unitName) - -- Get player unit and name. + -- Get player unit and name. local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) - + if _unit and _playername then - + -- Init message. local _message = string.format("My Top %d Bombing Results:\n", self.ndisplayresult) - + -- Results from player. local _results = self.bombPlayerResults[_playername] - + -- No score so far. if _results == nil then _message = _playername..": No Score yet." else - + -- Sort results wrt to distance. local _sort = function( a,b ) return a.distance < b.distance end table.sort(_results,_sort) - + -- Loop over results. local _bestMsg = "" for i,_result in pairs(_results) do local result=_result --#RANGE.BombResult - + -- Message with name, weapon and distance. _message = _message.."\n"..string.format("[%d] %d m %03d° - %s - %s - %s hit", i, result.distance, result.radial, result.name, result.weapon, result.quality) - + -- Store best/first result. if _bestMsg == "" then _bestMsg = string.format("%d m %03d° - %s - %s - %s hit", result.distance, result.radial, result.name, result.weapon, result.quality) end - + -- Best 10 runs only. if i==self.ndisplayresult then break end - + end - + -- Message. _message = _message .."\n\nBEST: ".._bestMsg end - + -- Send message. self:_DisplayMessageToGroup(_unit, _message, nil, true, true) end @@ -2029,22 +2029,22 @@ end -- @param #string _unitName Name of player unit. function RANGE:_DisplayBombingResults(_unitName) self:F(_unitName) - + -- Results table. local _playerResults = {} - + -- Get player unit and name. local _unit, _player = self:_GetPlayerUnitAndName(_unitName) - + -- Check if we have a unit with a player. if _unit and _player then - + -- Message header. local _message = string.format("Bombing Results - Top %d Players:\n", self.ndisplayresult) - + -- Loop over players. for _playerName,_results in pairs(self.bombPlayerResults) do - + -- Find best result of player. local _best = nil for _,_result in pairs(_results) do @@ -2052,29 +2052,29 @@ function RANGE:_DisplayBombingResults(_unitName) _best = _result end end - + -- Put best result of player into table. if _best ~= nil then local bestres=string.format("%s: %d m - %s - %s - %s hit", _playerName, _best.distance, _best.name, _best.weapon, _best.quality) table.insert(_playerResults, {msg = bestres, distance = _best.distance}) end - + end - + -- Sort list of player results. local _sort = function( a,b ) return a.distance < b.distance end table.sort(_playerResults,_sort) - + -- Loop over player results. - for _i = 1, math.min(#_playerResults, self.ndisplayresult) do + for _i = 1, math.min(#_playerResults, self.ndisplayresult) do _message = _message..string.format("\n[%d] %s", _i, _playerResults[_i].msg) end - + -- In case there are no scores yet. if #_playerResults<1 then _message = _message.."No player scored yet." end - + -- Send message. self:_DisplayMessageToGroup(_unit, _message, nil, true, true) end @@ -2088,20 +2088,20 @@ function RANGE:_DisplayRangeInfo(_unitname) -- Get player unit and player name. local unit, playername = self:_GetPlayerUnitAndName(_unitname) - + -- Check if we have a player. if unit and playername then - + -- Message text. local text="" - + -- Current coordinates. local coord=unit:GetCoordinate() - + if self.location then - - local settings=_DATABASE:GetPlayerSettings(playername) or _SETTINGS --Core.Settings#SETTINGS - + + local settings=_DATABASE:GetPlayerSettings(playername) or _SETTINGS --Core.Settings#SETTINGS + -- Direction vector from current position (coord) to target (position). local position=self.location --Core.Point#COORDINATE local bulls=position:ToStringBULLS(unit:GetCoalition(), settings) @@ -2110,10 +2110,10 @@ function RANGE:_DisplayRangeInfo(_unitname) local vec3=coord:GetDirectionVec3(position) local angle=coord:GetAngleDegrees(vec3) local range=coord:Get2DDistance(position) - + -- Bearing string. local Bs=string.format('%03d°', angle) - + local texthit if self.PlayerSettings[playername].flaredirecthits then texthit=string.format("Flare direct hits: ON (flare color %s)\n", self:_flarecolor2text(self.PlayerSettings[playername].flarecolor)) @@ -2132,8 +2132,8 @@ function RANGE:_DisplayRangeInfo(_unitname) else textdelay=string.format("Smoke bomb delay: OFF") end - - -- Player unit settings. + + -- Player unit settings. local trange=string.format("%.1f km", range/1000) local trangealt=string.format("%d m", rangealt) local tstrafemaxalt=string.format("%d m", self.strafemaxalt) @@ -2142,7 +2142,7 @@ function RANGE:_DisplayRangeInfo(_unitname) trangealt=string.format("%d feet", UTILS.MetersToFeet(rangealt)) tstrafemaxalt=string.format("%d feet", UTILS.MetersToFeet(self.strafemaxalt)) end - + -- Message. text=text..string.format("Information on %s:\n", self.rangename) text=text..string.format("-------------------------------------------------------\n") @@ -2156,10 +2156,10 @@ function RANGE:_DisplayRangeInfo(_unitname) text=text..texthit text=text..textbomb text=text..textdelay - + -- Send message to player group. self:_DisplayMessageToGroup(unit, text, nil, true, true) - + -- Debug output. self:T2(self.id..text) end @@ -2174,29 +2174,29 @@ function RANGE:_DisplayBombTargets(_unitname) -- Get player unit and player name. local _unit, _playername = self:_GetPlayerUnitAndName(_unitname) - + -- Check if we have a player. if _unit and _playername then - + -- Player settings. local _settings=_DATABASE:GetPlayerSettings(_playername) or _SETTINGS --Core.Settings#SETTINGS - + -- Message text. local _text="Bomb Target Locations:" - + for _,_bombtarget in pairs(self.bombingTargets) do local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE - + -- Coordinate of bombtarget. local coord=self:_GetBombTargetCoordinate(_bombtarget) - + if coord then - + local mycoord=coord:ToStringA2G(_unit, _settings) _text=_text..string.format("\n- %s: %s",_bombtarget.name or "unknown", mycoord) end end - + self:_DisplayMessageToGroup(_unit,_text, nil, true, true) end end @@ -2209,23 +2209,23 @@ function RANGE:_DisplayStrafePits(_unitname) -- Get player unit and player name. local _unit, _playername = self:_GetPlayerUnitAndName(_unitname) - + -- Check if we have a player. if _unit and _playername then - + -- Player settings. local _settings=_DATABASE:GetPlayerSettings(_playername) or _SETTINGS --Core.Settings#SETTINGS - + -- Message text. local _text="Strafe Target Locations:" - + for _,_strafepit in pairs(self.strafeTargets) do local _target=_strafepit --Wrapper.Positionable#POSITIONABLE - + -- Pit parameters. local coord=_strafepit.coordinate --Core.Point#COORDINATE local heading=_strafepit.heading - + -- Turn heading around ==> approach heading. if heading>180 then heading=heading-180 @@ -2236,7 +2236,7 @@ function RANGE:_DisplayStrafePits(_unitname) local mycoord=coord:ToStringA2G(_unit, _settings) _text=_text..string.format("\n- %s: %s - heading %03d°",_strafepit.name, mycoord, heading) end - + self:_DisplayMessageToGroup(_unit,_text, nil, true, true) end end @@ -2250,33 +2250,33 @@ function RANGE:_DisplayRangeWeather(_unitname) -- Get player unit and player name. local unit, playername = self:_GetPlayerUnitAndName(_unitname) - + -- Check if we have a player. if unit and playername then - + -- Message text. local text="" - + -- Current coordinates. local coord=unit:GetCoordinate() - + if self.location then - + -- Get atmospheric data at range location. local position=self.location --Core.Point#COORDINATE local T=position:GetTemperature() local P=position:GetPressure() local Wd,Ws=position:GetWind() - + -- Get Beaufort wind scale. - local Bn,Bd=UTILS.BeaufortScale(Ws) - + local Bn,Bd=UTILS.BeaufortScale(Ws) + local WD=string.format('%03d°', Wd) local Ts=string.format("%d°C",T) - + local hPa2inHg=0.0295299830714 local hPa2mmHg=0.7500615613030 - + local settings=_DATABASE:GetPlayerSettings(playername) or _SETTINGS --Core.Settings#SETTINGS local tT=string.format("%d°C",T) local tW=string.format("%.1f m/s", Ws) @@ -2284,10 +2284,10 @@ function RANGE:_DisplayRangeWeather(_unitname) if settings:IsImperial() then tT=string.format("%d°F", UTILS.CelciusToFarenheit(T)) tW=string.format("%.1f knots", UTILS.MpsToKnots(Ws)) - tP=string.format("%.2f inHg", P*hPa2inHg) + tP=string.format("%.2f inHg", P*hPa2inHg) end - - + + -- Message text. text=text..string.format("Weather Report at %s:\n", self.rangename) text=text..string.format("--------------------------------------------------\n") @@ -2297,15 +2297,15 @@ function RANGE:_DisplayRangeWeather(_unitname) else text=string.format("No range location defined for range %s.", self.rangename) end - + -- Send message to player group. self:_DisplayMessageToGroup(unit, text, nil, true, true) - + -- Debug output. self:T2(self.id..text) else self:T(self.id..string.format("ERROR! Could not find player unit in RangeInfo! Name = %s", _unitname)) - end + end end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2318,34 +2318,34 @@ function RANGE:_CheckPlayers() for playername,_playersettings in pairs(self.PlayerSettings) do local playersettings=_playersettings --#RANGE.PlayerData - + local unitname=playersettings.unitname local unit=UNIT:FindByName(unitname) - + if unit and unit:IsAlive() then - + if unit:IsInZone(self.rangezone) then - + ------------------------------ -- Player INSIDE Range Zone -- ------------------------------ - + if not playersettings.inzone then self:EnterRange(playersettings) playersettings.inzone=true end - + else - + ------------------------------- -- Player OUTSIDE Range Zone -- - ------------------------------- - + ------------------------------- + if playersettings.inzone==true then self:ExitRange(playersettings) playersettings.inzone=false end - + end end end @@ -2370,56 +2370,56 @@ function RANGE:_CheckInZone(_unitName) local _currentStrafeRun = self.strafeStatus[_unitID] if _currentStrafeRun then -- player has already registered for a strafing run. - + -- Get the current approach zone and check if player is inside. local zone=_currentStrafeRun.zone.polygon --Core.Zone#ZONE_POLYGON_BASE - + local unitheading = _unit:GetHeading() local pitheading = _currentStrafeRun.zone.heading - 180 local deltaheading = unitheading-pitheading local towardspit = math.abs(deltaheading)<=90 or math.abs(deltaheading-360)<=90 - local unitalt=_unit:GetHeight()-_unit:GetCoordinate():GetLandHeight() - + local unitalt=_unit:GetHeight()-_unit:GetCoordinate():GetLandHeight() + -- Check if unit is inside zone and below max height AGL. local unitinzone=_unit:IsInZone(zone) and unitalt <= self.strafemaxalt and towardspit - + -- Debug output local text=string.format("Checking still in zone. Unit = %s, player = %s in zone = %s. alt = %d, delta heading = %d", _unitName, _playername, tostring(unitinzone), unitalt, deltaheading) self:T2(self.id..text) - + -- Check if player is in strafe zone and below max alt. - if unitinzone then - + if unitinzone then + -- Still in zone, keep counting hits. Increase counter. _currentStrafeRun.time = _currentStrafeRun.time+1 - + else - + -- Increase counter _currentStrafeRun.time = _currentStrafeRun.time+1 - + if _currentStrafeRun.time <= 3 then - + -- Reset current run. self.strafeStatus[_unitID] = nil - + -- Message text. local _msg = string.format("%s left strafing zone %s too quickly. No Score.", _playername, _currentStrafeRun.zone.name) - + -- Send message. self:_DisplayMessageToGroup(_unit, _msg, nil, true) - + else - + -- Get current ammo. local _ammo=self:_GetAmmo(_unitName) - + -- Result. local _result = self.strafeStatus[_unitID] -- Judge this pass. Text is displayed on summary. if _result.hits >= _result.zone.goodPass*2 then - _result.text = "EXCELLENT PASS" + _result.text = "EXCELLENT PASS" elseif _result.hits >= _result.zone.goodPass then _result.text = "GOOD PASS" elseif _result.hits >= _result.zone.goodPass/2 then @@ -2427,81 +2427,81 @@ 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. + + -- 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) - + -- Set strafe status to nil. self.strafeStatus[_unitID] = nil - + -- Save stats so the player can retrieve them. local _stats = self.strafePlayerResults[_playername] or {} table.insert(_stats, _result) self.strafePlayerResults[_playername] = _stats end - + end else - + -- Check to see if we're in any of the strafing zones (first time). for _,_targetZone in pairs(self.strafeTargets) do - + -- Get the current approach zone and check if player is inside. local zonenname=_targetZone.name local zone=_targetZone.polygon --Core.Zone#ZONE_POLYGON_BASE - + -- Check if player is in zone and below max alt and flying towards the target. local unitheading = _unit:GetHeading() local pitheading = _targetZone.heading - 180 local deltaheading = unitheading-pitheading local towardspit = math.abs(deltaheading)<=90 or math.abs(deltaheading-360)<=90 - local unitalt =_unit:GetHeight()-_unit:GetCoordinate():GetLandHeight() - + local unitalt =_unit:GetHeight()-_unit:GetCoordinate():GetLandHeight() + -- Check if unit is inside zone and below max height AGL. local unitinzone=_unit:IsInZone(zone) and unitalt <= self.strafemaxalt and towardspit - + -- 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:T2(self.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, ammo=_ammo, pastfoulline=false } - + -- Rolling in! local _msg=string.format("%s, rolling in on strafe pit %s.", self:_myname(_unitName), _targetZone.name) - + -- Send message. self:_DisplayMessageToGroup(_unit, _msg, 10, true) -- We found our player. Skip remaining checks. break - - end -- unit in zone check - + + end -- unit in zone check + end -- loop over zones end end - + end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2513,25 +2513,25 @@ end -- @param #string _unitName Name of player unit. function RANGE:_AddF10Commands(_unitName) self:F(_unitName) - + -- Get player unit and name. local _unit, playername = self:_GetPlayerUnitAndName(_unitName) - + -- Check for player unit. if _unit and playername then -- Get group and ID. local group=_unit:GetGroup() local _gid=group:GetID() - + if group and _gid then - + if not self.MenuAddedTo[_gid] then - + -- Enable switch so we don't do this twice. self.MenuAddedTo[_gid] = true - - -- Range root menu path. + + -- Range root menu path. local _rangePath=nil if RANGE.MenuF10Root then @@ -2539,24 +2539,24 @@ function RANGE:_AddF10Commands(_unitName) ------------------- -- MISSION LEVEL -- ------------------- - + _rangePath = missionCommands.addSubMenuForGroup(_gid, self.rangename, RANGE.MenuF10Root) - + else - + ----------------- -- GROUP LEVEL -- ----------------- - - -- 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 + end _rangePath = missionCommands.addSubMenuForGroup(_gid, self.rangename, RANGE.MenuF10[_gid]) - - end - - + + end + + local _statsPath = missionCommands.addSubMenuForGroup(_gid, "Statistics", _rangePath) local _markPath = missionCommands.addSubMenuForGroup(_gid, "Mark Targets", _rangePath) local _settingsPath = missionCommands.addSubMenuForGroup(_gid, "My Settings", _rangePath) @@ -2567,8 +2567,8 @@ function RANGE:_AddF10Commands(_unitName) -- 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, "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/ @@ -2593,7 +2593,7 @@ function RANGE:_AddF10Commands(_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) missionCommands.addCommandForGroup(_gid, "All Messages On/Off", _settingsPath, self._MessagesToPlayerOnOff, self, _unitName) - + -- F10/On the Range//Range Information missionCommands.addCommandForGroup(_gid, "General Info", _infoPath, self._DisplayRangeInfo, self, _unitName) missionCommands.addCommandForGroup(_gid, "Weather Report", _infoPath, self._DisplayRangeWeather, self, _unitName) @@ -2608,7 +2608,7 @@ function RANGE:_AddF10Commands(_unitName) end end - + ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Helper Functions ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2620,9 +2620,9 @@ end function RANGE:_GetBombTargetCoordinate(target) local coord=nil --Core.Point#COORDINATE - + if target.type==RANGE.TargetType.UNIT then - + if not target.move then -- Target should not move. coord=target.coordinate @@ -2632,21 +2632,21 @@ function RANGE:_GetBombTargetCoordinate(target) coord=target.target:GetCoordinate() end end - + elseif target.type==RANGE.TargetType.STATIC then - -- Static targets dont move. + -- Static targets dont move. coord=target.coordinate - + elseif target.type==RANGE.TargetType.COORD then - + -- Coordinates dont move. coord=target.coordinate - + else self:E(self.id.."ERROR: Unknown target type.") end - + return coord end @@ -2657,35 +2657,35 @@ end -- @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(self.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(self.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) @@ -2697,7 +2697,7 @@ function RANGE:_GetAmmo(unitname) end end end - + return ammo end @@ -2712,7 +2712,7 @@ function RANGE:_MarkTargetsOnMap(_unitName) if _unitName then group=UNIT:FindByName(_unitName):GetGroup() end - + -- Mark bomb targets. for _,_bombtarget in pairs(self.bombingTargets) do local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE @@ -2725,7 +2725,7 @@ function RANGE:_MarkTargetsOnMap(_unitName) end end end - + -- Mark strafe targets. for _,_strafepit in pairs(self.strafeTargets) do for _,_target in pairs(_strafepit.targets) do @@ -2740,13 +2740,13 @@ function RANGE:_MarkTargetsOnMap(_unitName) 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. @@ -2765,16 +2765,16 @@ function RANGE:_IlluminateBombTargets(_unitName) table.insert(bomb, coord) end end - + if #bomb>0 then local coord=bomb[math.random(#bomb)] --Core.Point#COORDINATE local c=COORDINATE:New(coord.x,coord.y+math.random(self.illuminationminalt,self.illuminationmaxalt),coord.z) c:IlluminationBomb() end - + -- All strafe target coordinates. local strafe={} - + for _,_strafepit in pairs(self.strafeTargets) do for _,_target in pairs(_strafepit.targets) do local _target=_target --Wrapper.Positionable#POSITIONABLE @@ -2784,14 +2784,14 @@ function RANGE:_IlluminateBombTargets(_unitName) end end end - + -- Pick a random strafe target. if #strafe>0 then local coord=strafe[math.random(#strafe)] --Core.Point#COORDINATE local c=COORDINATE:New(coord.x,coord.y+math.random(self.illuminationminalt,self.illuminationmaxalt),coord.z) c:IlluminationBomb() end - + if _unitName then local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) local text=string.format("%s, %s, range targets are illuminated.", self.rangename, _playername) @@ -2805,10 +2805,10 @@ end function RANGE:_ResetRangeStats(_unitName) self:F(_unitName) - -- Get player unit and name. + -- Get player unit and name. local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) - - if _unit and _playername then + + if _unit and _playername then self.strafePlayerResults[_playername] = nil self.bombPlayerResults[_playername] = nil local text=string.format("%s, %s, your range stats were cleared.", self.rangename, _playername) @@ -2825,7 +2825,7 @@ end -- @param #boolean display If true, display message regardless of player setting "Messages Off". function RANGE:_DisplayMessageToGroup(_unit, _text, _time, _clear, display) self:F({unit=_unit, text=_text, time=_time, clear=_clear}) - + -- Defaults _time=_time or self.Tmsg if _clear==nil or _clear==false then @@ -2833,36 +2833,36 @@ function RANGE:_DisplayMessageToGroup(_unit, _text, _time, _clear, display) else _clear=true end - + -- Messages globally disabled. if self.messages==false then return end - + -- Check if unit is alive. if _unit and _unit:IsAlive() then - + -- Group ID. local _gid=_unit:GetGroup():GetID() - + -- Get playername and player settings local _, playername=self:_GetPlayerUnitAndName(_unit:GetName()) local playermessage=self.PlayerSettings[playername].messages - + -- Send message to player if messages enabled and not only for the examiner. if _gid and (playermessage==true or display) and (not self.examinerexclusive) then trigger.action.outTextForGroup(_gid, _text, _time, _clear) end - + -- Send message to examiner. if self.examinergroupname~=nil then local _examinerid=GROUP:FindByName(self.examinergroupname):GetID() if _examinerid then trigger.action.outTextForGroup(_examinerid, _text, _time, _clear) - end + end end end - + end --- Toggle status of smoking bomb impact points. @@ -2870,7 +2870,7 @@ end -- @param #string unitname Name of the player unit. function RANGE:_SmokeBombImpactOnOff(unitname) self:F(unitname) - + local unit, playername = self:_GetPlayerUnitAndName(unitname) if unit and playername then local text @@ -2883,7 +2883,7 @@ function RANGE:_SmokeBombImpactOnOff(unitname) end self:_DisplayMessageToGroup(unit, text, 5, false, true) end - + end --- Toggle status of time delay for smoking bomb impact points @@ -2891,7 +2891,7 @@ end -- @param #string unitname Name of the player unit. function RANGE:_SmokeBombDelayOnOff(unitname) self:F(unitname) - + local unit, playername = self:_GetPlayerUnitAndName(unitname) if unit and playername then local text @@ -2904,7 +2904,7 @@ function RANGE:_SmokeBombDelayOnOff(unitname) end self:_DisplayMessageToGroup(unit, text, 5, false, true) end - + end --- Toggle display messages to player. @@ -2912,7 +2912,7 @@ end -- @param #string unitname Name of the player unit. function RANGE:_MessagesToPlayerOnOff(unitname) self:F(unitname) - + local unit, playername = self:_GetPlayerUnitAndName(unitname) if unit and playername then local text @@ -2924,7 +2924,7 @@ function RANGE:_MessagesToPlayerOnOff(unitname) self:_DisplayMessageToGroup(unit, text, 5, false, true) self.PlayerSettings[playername].messages=not self.PlayerSettings[playername].messages end - + end --- Toggle status of flaring direct hits of range targets. @@ -2932,7 +2932,7 @@ end -- @param #string unitname Name of the player unit. function RANGE:_FlareDirectHitsOnOff(unitname) self:F(unitname) - + local unit, playername = self:_GetPlayerUnitAndName(unitname) if unit and playername then local text @@ -2945,7 +2945,7 @@ function RANGE:_FlareDirectHitsOnOff(unitname) end self:_DisplayMessageToGroup(unit, text, 5, false, true) end - + end --- Mark bombing targets with smoke. @@ -2953,7 +2953,7 @@ end -- @param #string unitname Name of the player unit. function RANGE:_SmokeBombTargets(unitname) self:F(unitname) - + for _,_bombtarget in pairs(self.bombingTargets) do local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE local coord=self:_GetBombTargetCoordinate(_bombtarget) @@ -2961,13 +2961,13 @@ function RANGE:_SmokeBombTargets(unitname) coord:Smoke(self.BombSmokeColor) end end - + if unitname then local unit, playername = self:_GetPlayerUnitAndName(unitname) local text=string.format("%s, %s, bombing targets are now marked with %s smoke.", self.rangename, playername, self:_smokecolor2text(self.BombSmokeColor)) self:_DisplayMessageToGroup(unit, text, 5) end - + end --- Mark strafing targets with smoke. @@ -2975,17 +2975,17 @@ end -- @param #string unitname Name of the player unit. function RANGE:_SmokeStrafeTargets(unitname) self:F(unitname) - + for _,_target in pairs(self.strafeTargets) do _target.coordinate:Smoke(self.StrafeSmokeColor) end - + if unitname then local unit, playername = self:_GetPlayerUnitAndName(unitname) local text=string.format("%s, %s, strafing tragets are now marked with %s smoke.", self.rangename, playername, self:_smokecolor2text(self.StrafeSmokeColor)) self:_DisplayMessageToGroup(unit, text, 5) end - + end --- Mark approach boxes of strafe targets with smoke. @@ -2993,7 +2993,7 @@ end -- @param #string unitname Name of the player unit. function RANGE:_SmokeStrafeTargetBoxes(unitname) self:F(unitname) - + for _,_target in pairs(self.strafeTargets) do local zone=_target.polygon --Core.Zone#ZONE zone:SmokeZone(self.StrafePitSmokeColor) @@ -3001,13 +3001,13 @@ function RANGE:_SmokeStrafeTargetBoxes(unitname) _point:SmokeOrange() --Corners are smoked orange. end end - + if unitname then local unit, playername = self:_GetPlayerUnitAndName(unitname) local text=string.format("%s, %s, strafing pit approach boxes are now marked with %s smoke.", self.rangename, playername, self:_smokecolor2text(self.StrafePitSmokeColor)) self:_DisplayMessageToGroup(unit, text, 5) end - + end --- Sets the smoke color used to smoke players bomb impact points. @@ -3016,14 +3016,14 @@ end -- @param Utilities.Utils#SMOKECOLOR color ID of the smoke color. function RANGE:_playersmokecolor(_unitName, color) self:F({unitname=_unitName, color=color}) - + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) if _unit and _playername then self.PlayerSettings[_playername].smokecolor=color local text=string.format("%s, %s, your bomb impacts are now smoked in %s.", self.rangename, _playername, self:_smokecolor2text(color)) self:_DisplayMessageToGroup(_unit, text, 5) end - + end --- Sets the flare color used when player makes a direct hit on target. @@ -3032,14 +3032,14 @@ end -- @param Utilities.Utils#FLARECOLOR color ID of flare color. function RANGE:_playerflarecolor(_unitName, color) self:F({unitname=_unitName, color=color}) - + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) if _unit and _playername then self.PlayerSettings[_playername].flarecolor=color local text=string.format("%s, %s, your direct hits are now flared in %s.", self.rangename, _playername, self:_flarecolor2text(color)) self:_DisplayMessageToGroup(_unit, text, 5) end - + end --- Converts a smoke color id to text. E.g. SMOKECOLOR.Blue --> "blue". @@ -3048,7 +3048,7 @@ end -- @return #string Color text. function RANGE:_smokecolor2text(color) self:F(color) - + local txt="" if color==SMOKECOLOR.Blue then txt="blue" @@ -3063,7 +3063,7 @@ function RANGE:_smokecolor2text(color) else txt=string.format("unknown color (%s)", tostring(color)) end - + return txt end @@ -3073,7 +3073,7 @@ end -- @return #string Color text. function RANGE:_flarecolor2text(color) self:F(color) - + local txt="" if color==FLARECOLOR.Green then txt="green" @@ -3086,7 +3086,7 @@ function RANGE:_flarecolor2text(color) else txt=string.format("unknown color (%s)", tostring(color)) end - + return txt end @@ -3099,23 +3099,23 @@ function RANGE:_CheckStatic(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(self.id..string.format("Adding DCS static to MOOSE database. Name = %s.", name)) _DATABASE:AddStatic(name) end - + return true else self:T3(self.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 @@ -3136,18 +3136,18 @@ function RANGE:_GetSpeed(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. +--- 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. -- @return Wrapper.Unit#UNIT Unit of player. @@ -3157,38 +3157,38 @@ function RANGE:_GetPlayerUnitAndName(_unitName) self:F2(_unitName) if _unitName ~= nil then - + -- Get DCS unit from its name. local DCSunit=Unit.getByName(_unitName) - + if DCSunit then - + local playername=DCSunit:getPlayerName() local unit=UNIT:Find(DCSunit) - + self:T2({DCSunit=DCSunit, unit=unit, playername=playername}) if DCSunit and unit and playername then return unit, playername end - + end - + end - + -- Return nil if we could not find a player. return nil,nil end ---- Returns a string which consits of this callsign and the player name. +--- Returns a string which consits of this callsign and the player name. -- @param #RANGE self -- @param #string unitname Name of the player unit. function RANGE:_myname(unitname) self:F2(unitname) - + local unit=UNIT:FindByName(unitname) local pname=unit:GetPlayerName() local csign=unit:GetCallsign() - + return string.format("%s (%s)", csign, pname) end diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 84fbb7cd0..49558c58d 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -17,6 +17,13 @@ -- -- === -- +-- ## Youtube Videos: +-- +-- * [Warehouse Trailer](https://www.youtube.com/watch?v=e98jzLi5fGk) +-- * [DCS Warehouse Airbase Resources Proof Of Concept](https://www.youtube.com/watch?v=YeuGL0duEgY) +-- +-- === +-- -- ## Missions: -- -- === @@ -25,8 +32,6 @@ -- in order to meet requirements of a potential conflict. In particular, this class is concerned with maintaining army supply lines while disrupting those of the enemy, since an armed -- force without resources and transportation is defenseless. -- --- Please note that his class is work in progress and in an **alpha** stage. --- -- === -- -- ### Author: **funkyfranky** @@ -71,6 +76,7 @@ -- @field #string autosavefile File name of the auto asset save file. Default is auto generated from warehouse id and name. -- @field #boolean safeparking If true, parking spots for aircraft are considered as occupied if e.g. a client aircraft is parked there. Default false. -- @field #boolean isunit If true, warehouse is represented by a unit instead of a static. +-- @field #number lowfuelthresh Low fuel threshold. Triggers the event AssetLowFuel if for any unit fuel goes below this number. -- @extends Core.Fsm#FSM --- Have your assets at the right place at the right time - or not! @@ -83,7 +89,7 @@ -- to another in a realistic and highly automatic fashion. In contrast to a "DCS warehouse" these assets have a physical representation in game. In particular, -- this means they can be destroyed during the transport and add more life to the DCS world. -- --- This comes along with some additional interesting stategic aspects since capturing/defending and destroying/protecting an enemy or your +-- This comes along with some additional interesting strategic aspects since capturing/defending and destroying/protecting an enemy or your -- own warehouse becomes of critical importance for the development of a conflict. -- -- In essence, creating an efficient network of warehouses is vital for the success of a battle or even the whole war. Likewise, of course, cutting off the enemy @@ -109,12 +115,12 @@ -- -- * Ground vehicles will use the road infrastructure. So a good road connection for both warehouses is important but also off road connections can be added if necessary. -- * Airborne units get a flightplan from the airbase of the sending warehouse to the airbase of the receiving warehouse. This already implies that for airborne --- assets both warehouses need an airbase. If either one of the warehouses does not have an associated airbase, direct transportation of airborne assest is not possible. +-- assets both warehouses need an airbase. If either one of the warehouses does not have an associated airbase, direct transportation of airborne assets is not possible. -- * Naval units can be exchanged between warehouses which possess a port, which can be defined by the user. Also shipping lanes must be specified manually but the user since DCS does not provide these. -- * Trains (would) use the available railroad infrastructure and both warehouses must have a connection to the railroad. Unfortunately, however, trains are not yet implemented to -- a reasonable degree in DCS at the moment and hence cannot be used yet. -- --- Furthermore, ground assets can be transferred between warehouses by transport units. These are APCs, helicopters and airplanes. The transportation process is modelled +-- Furthermore, ground assets can be transferred between warehouses by transport units. These are APCs, helicopters and airplanes. The transportation process is modeled -- in a realistic way by using the corresponding cargo dispatcher classes, i.e. -- -- * @{AI.AI_Cargo_Dispatcher_APC#AI_DISPATCHER_APC} @@ -239,13 +245,13 @@ -- -- warehouseBatumi:AddAsset("Mi-8", nil, nil, nil, nil, nil, nil, "China UN") -- --- would spawn the asset with a chinese UN livery. +-- would spawn the asset with a Chinese UN livery. -- -- Or -- -- warehouseBatumi:AddAsset("Mi-8", nil, nil, nil, nil, nil, nil, {"China UN", "German"}) -- --- would spawn the asset with either a chinese UN or German livery. Mind the curly brackets **{}** when you want to specify multiple liveries. +-- would spawn the asset with either a Chinese UN or German livery. Mind the curly brackets **{}** when you want to specify multiple liveries. -- -- Four each unit type, the livery names can be found in the DCS root folder under Bazar\Liveries. You have to use the name of the livery subdirectory. The names of the liveries -- as displayed in the mission editor might be different and won't work in general. @@ -524,7 +530,7 @@ -- -- # Why is my request not processed? -- --- For each request, the warehouse class logic does a lot of consistancy and validation checks under the hood. +-- For each request, the warehouse class logic does a lot of consistency and validation checks under the hood. -- This helps to circumvent a lot of DCS issues and shortcomings. For example, it is checked that enough free -- parking spots at an airport are available *before* the assets are spawned. -- However, this also means that sometimes a request is deemed to be *invalid* in which case they are deleted @@ -535,7 +541,7 @@ -- Invalid request are requests which can **never** be processes because there is some logical or physical argument against it. -- (Or simply because that feature was not implemented (yet).) -- --- * All airborne assets need an associated airbase of any kind on the sending *and* receiving warhouse. +-- * All airborne assets need an associated airbase of any kind on the sending *and* receiving warehouse. -- * Airplanes need an airdrome at the sending and receiving warehouses. -- * Not enough parking spots of the right terminal type at the sending warehouse. This avoids planes spawning on runways or on top of each other. -- * No parking spots of the right terminal type at the receiving warehouse. This avoids DCS despawning planes on landing if they have no valid parking spot. @@ -552,7 +558,7 @@ -- -- ## Temporarily Unprocessable Requests -- --- Temporarily unprocessable requests are possible in priciple, but cannot be processed at the given time the warehouse checks its queue. +-- Temporarily unprocessable requests are possible in principle, but cannot be processed at the given time the warehouse checks its queue. -- -- * No enough parking spaces are available for all requested assets but the airbase has enough parking spots in total so that this request is possible once other aircraft have taken off. -- * The requesting warehouse is not in state "Running" (could be paused, not yet started or under attack). @@ -564,11 +570,11 @@ -- -- ## Cargo Bay and Weight Limitations -- --- The transporation of cargo is handled by the AI\_Dispatcher classes. These take the cargo bay of a carrier and the weight of +-- The transportation of cargo is handled by the AI\_Dispatcher classes. These take the cargo bay of a carrier and the weight of -- the cargo into account so that a carrier can only load a realistic amount of cargo. -- -- However, if troops are supposed to be transported between warehouses, there is one important limitations one has to keep in mind. --- This is that **cargo asset groups cannot be split** and devided into separate carrier units! +-- This is that **cargo asset groups cannot be split** and divided into separate carrier units! -- -- For example, a TPz Fuchs has a cargo bay large enough to carry up to 10 soldiers at once, which is a realistic number. -- If a group consisting of more than ten soldiers needs to be transported, it cannot be loaded into the APC. @@ -601,7 +607,7 @@ -- # Strategic Considerations -- -- Due to the fact that a warehouse holds (or can hold) a lot of valuable assets, it makes a (potentially) juicy target for enemy attacks. --- There are several interesting situations, which can occurr. +-- There are several interesting situations, which can occur. -- -- ## Capturing a Warehouses Airbase -- @@ -621,7 +627,7 @@ -- -- A warehouse can be captured by the enemy coalition. If enemy ground troops enter the warehouse zone the event **Attacked** is triggered which can be captured by the -- @{#WAREHOUSE.OnAfterAttacked} event. By default the warehouse zone circular zone with a radius of 500 meters located at the center of the physical warehouse. --- The warehouse zone can be set via the @{#WAREHOUSE.SetWarehouseZone}(*zone*) function. The parameter *zone* must also be a cirular zone. +-- The warehouse zone can be set via the @{#WAREHOUSE.SetWarehouseZone}(*zone*) function. The parameter *zone* must also be a circular zone. -- -- The @{#WAREHOUSE.OnAfterAttacked} function can be used by the mission designer to react to the enemy attack. For example by deploying some or all ground troops -- currently in stock to defend the warehouse. Note that the warehouse also has a self defence option which can be enabled by the @{#WAREHOUSE.SetAutoDefenceOn}() @@ -632,8 +638,8 @@ -- If only ground troops of the enemy coalition are present in the warehouse zone, the warehouse and all its assets falls into the hands of the enemy. -- In this case the event **Captured** is triggered which can be captured by the @{#WAREHOUSE.OnAfterCaptured} function. -- --- The warehouse turns to the capturing coalition, i.e. its physical representation, and all assets as well. In paticular, all requests to the warehouse will --- spawn assets beloning to the new owner. +-- The warehouse turns to the capturing coalition, i.e. its physical representation, and all assets as well. In particular, all requests to the warehouse will +-- spawn assets belonging to the new owner. -- -- If the enemy troops could be defeated, i.e. no more troops of the opposite coalition are in the warehouse zone, the event **Defeated** is triggered and -- the @{#WAREHOUSE.OnAfterDefeated} function can be used to adapt to the new situation. For example putting back all spawned defender troops back into @@ -641,11 +647,11 @@ -- -- ## Destroying a Warehouse -- --- If an enemy destroy the physical warehouse structure, the warehouse will of course stop all its services. In priciple, all assets contained in the warehouse are +-- If an enemy destroy the physical warehouse structure, the warehouse will of course stop all its services. In principle, all assets contained in the warehouse are -- gone as well. So a warehouse should be properly defended. -- -- Upon destruction of the warehouse, the event **Destroyed** is triggered, which can be captured by the @{#WAREHOUSE.OnAfterDestroyed} function. --- So the mission designer can intervene at this point and for example choose to spawn all or paricular types of assets before the warehouse is gone for good. +-- So the mission designer can intervene at this point and for example choose to spawn all or particular types of assets before the warehouse is gone for good. -- -- === -- @@ -670,7 +676,9 @@ -- * "Attacked" --> "Captured" --> "Running" (warehouse was captured by the enemy) -- * "*" --> "AirbaseCaptured" --> "*" (airbase belonging to the warehouse was captured by the enemy) -- * "*" --> "AirbaseRecaptured" --> "*" (airbase was re-captured) --- * "*" --> "AssetDead" --> "*" (a whole asset group is dead) +-- * "*" --> "AssetSpawned" --> "*" (an asset has been spawned into the world) +-- * "*" --> "AssetLowFuel" --> "*" (an asset is running low on fuel) +-- * "*" --> "AssetDead" --> "*" (a whole asset, i.e. all its units/groups, is dead) -- * "*" --> "Destroyed" --> "Destroyed" (warehouse was destroyed) -- * "Running" --> "Pause" --> "Paused" (warehouse is paused) -- * "Paused" --> "Unpause" --> "Running" (warehouse is unpaused) @@ -822,7 +830,7 @@ -- ## Example 2: Self propelled Ground Troops -- -- Warehouse Berlin, which is a FARP near Batumi, requests infantry and troop transports from the warehouse at Batumi. --- The groups are spawned at Batumi and move by themselfs from Batumi to Berlin using the roads. +-- The groups are spawned at Batumi and move by themselves from Batumi to Berlin using the roads. -- Once the troops have arrived at Berlin, the troops are automatically added to the warehouse stock of Berlin. -- While on the road, Batumi has requested back two APCs from Berlin. Since Berlin does not have the assets in stock, -- the request is queued. After the troops have arrived, Berlin is sending back the APCs to Batumi. @@ -943,7 +951,7 @@ -- If the red coalition manages to capture our warehouse, all assets go into their possession. Now red tries to steal three F/A-18 flights and send them to -- Sukhumi. These aircraft will be spawned and begin to taxi. However, ... -- --- A blue Bradley is in the area and will attemt to recapture the warehouse. It might also catch the red F/A-18s before they take off. +-- A blue Bradley is in the area and will attempt to recapture the warehouse. It might also catch the red F/A-18s before they take off. -- -- -- Start warehouses. -- warehouse.Senaki:Start() @@ -1110,7 +1118,7 @@ -- After 30 and 45 seconds requests for five groups of armed speedboats are made. These will be spawned in the port zone right behind the carrier. -- The first five groups will go port of the carrier an form a left wing formation. The seconds groups will to the analogue on the starboard side. -- **Note** that in order to spawn naval assets a warehouse needs a port (zone). Since the carrier and hence the warehouse is mobile, we define a moving --- zone as @{Core.Zone#ZONE_UNIT} with the carrier as reference unit. The "port" of the Stennis at its stern so all naval assets are spawned behing the carrier. +-- zone as @{Core.Zone#ZONE_UNIT} with the carrier as reference unit. The "port" of the Stennis at its stern so all naval assets are spawned behind the carrier. -- -- -- Start warehouse on USS Stennis. -- warehouse.Stennis:Start() @@ -1317,7 +1325,7 @@ -- -- ## Example 14: Strategic Bombing -- --- This example shows how to employ stategic bombers in a mission. Three B-52s are lauched at Kobuleti with the assignment to wipe out the enemy warehouse at Sukhumi. +-- This example shows how to employ strategic bombers in a mission. Three B-52s are launched at Kobuleti with the assignment to wipe out the enemy warehouse at Sukhumi. -- The bombers will get a flight path and make their approach from the South at an altitude of 5000 m ASL. After their bombing run, they will return to Kobuleti and -- added back to stock. -- @@ -1560,6 +1568,7 @@ WAREHOUSE = { autosavefile = nil, saveparking = false, isunit = false, + lowfuelthresh = 0.15, } --- Item of the warehouse stock table. @@ -1582,7 +1591,7 @@ WAREHOUSE = { -- @field #number loadradius Distance when cargo is loaded into the carrier. -- @field DCS#AI.Skill skill Skill of AI unit. -- @field #string livery Livery of the asset. --- @field #string assignment Assignment of the asset. This could, e.g., be used in the @{#WAREHOUSE.OnAfterNewAsset) funktion. +-- @field #string assignment Assignment of the asset. This could, e.g., be used in the @{#WAREHOUSE.OnAfterNewAsset) function. --- Item of the warehouse queue table. -- @type WAREHOUSE.Queueitem @@ -1616,6 +1625,7 @@ WAREHOUSE = { -- @field Core.Set#SET_CARGO transportcargoset Set of cargo objects. -- @field #table carriercargo Table holding the cargo groups of each carrier unit. -- @field #number ntransporthome Number of transports back home. +-- @field #boolean lowfuel If true, at least one asset group is low on fuel. -- @extends #WAREHOUSE.Queueitem --- Descriptors enumerator describing the type of the asset. @@ -1733,7 +1743,7 @@ _WAREHOUSEDB = { --- Warehouse class version. -- @field #string version -WAREHOUSE.version="0.7.0" +WAREHOUSE.version="0.9.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Warehouse todo list. @@ -1747,7 +1757,7 @@ WAREHOUSE.version="0.7.0" -- TODO: Handle the case when units of a group die during the transfer. -- TODO: Added habours as interface for transport to from warehouses? Could make a rudimentary shipping dispatcher. -- DONE: Test capturing a neutral warehouse. --- DONE: Add save/load capability of warehouse <==> percistance after mission restart. Difficult in lua! +-- DONE: Add save/load capability of warehouse <==> persistance after mission restart. Difficult in lua! -- DONE: Get cargo bay and weight from CARGO_GROUP and GROUP. No necessary any more! -- DONE: Add possibility to set weight and cargo bay manually in AddAsset function as optional parameters. -- DONE: Check overlapping aircraft sometimes. @@ -1876,6 +1886,8 @@ function WAREHOUSE:New(warehouse, alias) self:AddTransition("Running", "Request", "*") -- Process a request. Only in running mode. self:AddTransition("Attacked", "Request", "*") -- Process a request. Only in running mode. self:AddTransition("*", "Unloaded", "*") -- Cargo has been unloaded from the carrier (unused ==> unnecessary?). + self:AddTransition("*", "AssetSpawned", "*") -- Asset has been spawned into the world. + self:AddTransition("*", "AssetLowFuel", "*") -- Asset is low on fuel. self:AddTransition("*", "Arrived", "*") -- Cargo or transport group has arrived. self:AddTransition("*", "Delivered", "*") -- All cargo groups of a request have been delivered to the requesting warehouse. self:AddTransition("Running", "SelfRequest", "*") -- Request to warehouse itself. Requested assets are only spawned but not delivered anywhere. @@ -2301,6 +2313,52 @@ function WAREHOUSE:New(warehouse, alias) -- @param #string To To state. + --- Triggers the FSM event "AssetSpawned" when the warehouse has spawned an asset. + -- @function [parent=#WAREHOUSE] AssetSpawned + -- @param #WAREHOUSE self + -- @param Wrapper.Group#GROUP group the group that was spawned. + -- @param #WAREHOUSE.Assetitem asset The asset that was spawned. + + --- Triggers the FSM event "AssetSpawned" with a delay when the warehouse has spawned an asset. + -- @function [parent=#WAREHOUSE] __AssetSpawned + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param Wrapper.Group#GROUP group the group that was spawned. + -- @param #WAREHOUSE.Assetitem asset The asset that was spawned. + + --- On after "AssetSpawned" event user function. Called when the warehouse has spawned an asset. + -- @function [parent=#WAREHOUSE] OnAfterAssetSpawned + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Group#GROUP group the group that was spawned. + -- @param #WAREHOUSE.Assetitem asset The asset that was spawned. + + + --- Triggers the FSM event "AssetLowFuel" when an asset runs low on fuel + -- @function [parent=#WAREHOUSE] AssetLowFuel + -- @param #WAREHOUSE self + -- @param #WAREHOUSE.Assetitem asset The asset that is low on fuel. + -- @param #WAREHOUSE.Pendingitem request The request of the asset that is low on fuel. + + --- Triggers the FSM event "AssetLowFuel" with a delay when an asset runs low on fuel. + -- @function [parent=#WAREHOUSE] __AssetLowFuel + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param #WAREHOUSE.Assetitem asset The asset that is low on fuel. + -- @param #WAREHOUSE.Pendingitem request The request of the asset that is low on fuel. + + --- On after "AssetLowFuel" event user function. Called when the an asset is low on fuel. + -- @function [parent=#WAREHOUSE] OnAfterAssetLowFuel + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #WAREHOUSE.Assetitem asset The asset that is low on fuel. + -- @param #WAREHOUSE.Pendingitem request The request of the asset that is low on fuel. + + --- Triggers the FSM event "Save" when the warehouse assets are saved to file on disk. -- @function [parent=#WAREHOUSE] Save -- @param #WAREHOUSE self @@ -2403,6 +2461,14 @@ function WAREHOUSE:SetSafeParkingOff() return self end +--- Set low fuel threshold. If one unit of an asset has less fuel than this number, the event AssetLowFuel will be fired. +-- @param #WAREHOUSE self +-- @param #number threshold Relative low fuel threshold, i.e. a number in [0,1]. Default 0.15 (15%). +-- @return #WAREHOUSE self +function WAREHOUSE:SetLowFuelThreshold(threshold) + self.lowfuelthresh=threshold or 0.15 + return self +end --- Set interval of status updates. Note that normally only one request can be processed per time interval. -- @param #WAREHOUSE self @@ -3030,7 +3096,7 @@ function WAREHOUSE:FindAssetInDB(group) if aid~=nil then local asset=_WAREHOUSEDB.Assets[aid] - self:E({asset=asset}) + self:T2({asset=asset}) if asset==nil then self:_ErrorMessage(string.format("ERROR: Asset for group %s not found in the data base!", group:GetName()), 0) end @@ -3212,7 +3278,7 @@ end function WAREHOUSE:onafterStatus(From, Event, To) self:I(self.wid..string.format("Checking status of warehouse %s. Current FSM state %s. Global warehouse assets = %d.", self.alias, self:GetState(), #_WAREHOUSEDB.Assets)) - -- Check if any pending jobs are done and can be deleted from the + -- Check if any pending jobs are done and can be deleted from the queue. self:_JobDone() -- Print status. @@ -3241,6 +3307,9 @@ function WAREHOUSE:onafterStatus(From, Event, To) self:_PrintQueue(self.queue, "Queue waiting") self:_PrintQueue(self.pending, "Queue pending") + -- Check fuel for all assets. + self:_CheckFuel() + -- Update warhouse marker on F10 map. self:_UpdateWarehouseMarkText() @@ -3675,7 +3744,7 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, local SpeedMax=group:GetSpeedMax() local RangeMin=group:GetRange() local smax,sx,sy,sz=_GetObjectSize(Descriptors) - + --self:E(Descriptors) -- Get weight and cargo bay size in kg. @@ -4259,6 +4328,9 @@ function WAREHOUSE:onafterRequest(From, Event, To, Request) -- Add transport assets. table.insert(_transportassets,_assetitem) + + -- Asset spawned FSM function. + self:__AssetSpawned(1, spawngroup, _assetitem) end end @@ -5175,7 +5247,7 @@ function WAREHOUSE:_SpawnAssetRequest(Request) -- Spawn train. if self.rail then --TODO: Rail should only get one asset because they would spawn on top! - + -- Spawn naval assets. _group=self:_SpawnAssetGroundNaval(_alias,_assetitem, Request, self.spawnzone) end @@ -5195,6 +5267,9 @@ function WAREHOUSE:_SpawnAssetRequest(Request) if _group then _groupset:AddGroup(_group) table.insert(_assets, _assetitem) + + -- Call FSM function. + self:__AssetSpawned(1,_group,_assetitem) else self:E(self.wid.."ERROR: Cargo asset could not be spawned!") end @@ -5235,7 +5310,7 @@ function WAREHOUSE:_SpawnAssetGroundNaval(alias, asset, request, spawnzone, aiof -- Get a random coordinate in the spawn zone. local coord=spawnzone:GetRandomCoordinate() - + -- For trains, we use the rail connection point. if asset.category==Group.Category.TRAIN then coord=self.rail @@ -5483,7 +5558,6 @@ end -- @param #WAREHOUSE self -- @param Wrapper.Group#GROUP group The ground group to be routed -- @param #WAREHOUSE.Queueitem request The request for this group. --- @param #number Speed Speed in km/h to drive to the destination coordinate. Default is 60% of max possible speed the unit can go. function WAREHOUSE:_RouteGround(group, request) if group and group:IsAlive() then @@ -5526,18 +5600,25 @@ function WAREHOUSE:_RouteGround(group, request) local FromWP=group:GetCoordinate():WaypointGround(_speed, "Off Road") table.insert(Waypoints, 1, FromWP) - -- Final coordinate. - local ToWP=request.warehouse.spawnzone:GetRandomCoordinate():WaypointGround(_speed, "Off Road") - table.insert(Waypoints, #Waypoints+1, ToWP) + -- Final coordinate. Note, this can lead to errors if the final WP is too close the the point on the road. The vehicle will stop driving and not reach the final WP! + --local ToCO=request.warehouse.spawnzone:GetRandomCoordinate() + --local ToWP=ToCO:WaypointGround(_speed, "Off Road") + --table.insert(Waypoints, #Waypoints+1, ToWP) end + for n,wp in ipairs(Waypoints) do + env.info(n) + local tf=self:_SimpleTaskFunctionWP("warehouse:_PassingWaypoint",group, n, #Waypoints) + group:SetTaskWaypoint(wp, tf) + end + -- Task function triggering the arrived event at the last waypoint. - local TaskFunction = self:_SimpleTaskFunction("warehouse:_Arrived", group) + --local TaskFunction = self:_SimpleTaskFunction("warehouse:_Arrived", group) -- Put task function on last waypoint. - local Waypoint = Waypoints[#Waypoints] - group:SetTaskWaypoint(Waypoint, TaskFunction) + --local Waypoint = Waypoints[#Waypoints] + --group:SetTaskWaypoint(Waypoint, TaskFunction) -- Route group to destination. group:Route(Waypoints, 1) @@ -5662,9 +5743,30 @@ end -- @param Wrapper.Group#GROUP group The group that arrived. function WAREHOUSE:_Arrived(group) self:_DebugMessage(string.format("Group %s arrived!", tostring(group:GetName()))) + --self:E(string.format("Group %s arrived!", tostring(group:GetName()))) if group then --Trigger "Arrived event. + --group:SmokeBlue() + self:__Arrived(1, group) + end + +end + +--- Task function for when passing a waypoint. +-- @param #WAREHOUSE self +-- @param Wrapper.Group#GROUP group The group that arrived. +-- @param #number n Waypoint passed. +-- @param #number N Final waypoint. +function WAREHOUSE:_PassingWaypoint(group,n,N) + self:T(string.format("Group %s passing waypoint %d of %d!", tostring(group:GetName()), n, N)) + + if group then + --group:SmokeGreen() + end + + if n==N then + --group:SmokeBlue() self:__Arrived(1, group) end @@ -5888,40 +5990,27 @@ end -- @param #WAREHOUSE.Pendingitem request Request that needs to be updated. function WAREHOUSE:_UnitDead(deadunit, request) - -- Flare unit - deadunit:FlareRed() + -- Flare unit. + if self.Debug then + deadunit:FlareRed() + end -- Group the dead unit belongs to. local group=deadunit:GetGroup() - -- Check if this was the last unit of the group ==> whole group dead. + -- Number of alive units in group. + local nalive=group:CountAliveUnits() + + -- Whole group is dead? local groupdead=true - local nunits=0 - local nunits0=0 - if group then - -- Get current size of group and substract the unit that just died because it is not counted yet! - nunits=group:GetSize()-1 - nunits0=group:GetInitialSize() - - if nunits > 0 then - groupdead=false - end + if nalive>0 then + groupdead=false end - -- Here I need to get rid of the #CARGO at the end to obtain the original name again! local unitname=self:_GetNameWithOut(deadunit) local groupname=self:_GetNameWithOut(group) - -- Debug message. - local text=string.format("Unit %s died! #units=%d/%d ==> Group dead=%s (IsAlive=%s).", unitname, nunits, nunits0, tostring(groupdead), tostring(group:IsAlive())) - self:T2(self.wid..text) - - -- Check if this really works as expected! - if nunits<0 then - self:E(self.wid.."ERROR: Number of units negative! This should not happen.") - end - -- Group is dead! if groupdead then self:T(self.wid..string.format("Group %s (transport=%s) is dead!", groupname, tostring(self:_GroupIsTransport(group,request)))) @@ -5943,7 +6032,7 @@ function WAREHOUSE:_UnitDead(deadunit, request) -- Easy case: Group can simply be removed from the cargogroupset. --- - -- Remove dead group from carg group set. + -- Remove dead group from cargo group set. if groupdead==true then request.cargogroupset:Remove(groupname, NoTriggerEvent) self:T(self.wid..string.format("Removed selfpropelled cargo %s: ncargo=%d.", groupname, request.cargogroupset:Count())) @@ -6953,15 +7042,46 @@ function WAREHOUSE:_SimpleTaskFunction(Function, group) -- Task script. local DCSScript = {} - --DCSScript[#DCSScript+1] = string.format('env.info(\"WAREHOUSE: Simple task function called!\") ') - DCSScript[#DCSScript+1] = string.format('local mygroup = GROUP:FindByName(\"%s\") ', groupname) -- The group that executes the task function. Very handy with the "...". + + DCSScript[#DCSScript+1] = string.format('local mygroup = GROUP:FindByName(\"%s\") ', groupname) -- The group that executes the task function. Very handy with the "...". if self.isunit then - DCSScript[#DCSScript+1] = string.format("local mywarehouse = UNIT:FindByName(\"%s\") ", warehouse) -- The unit that holds the warehouse self object. + DCSScript[#DCSScript+1] = string.format("local mywarehouse = UNIT:FindByName(\"%s\") ", warehouse) -- The unit that holds the warehouse self object. else - DCSScript[#DCSScript+1] = string.format("local mywarehouse = STATIC:FindByName(\"%s\") ", warehouse) -- The static that holds the warehouse self object. + DCSScript[#DCSScript+1] = string.format("local mywarehouse = STATIC:FindByName(\"%s\") ", warehouse) -- The static that holds the warehouse self object. end - DCSScript[#DCSScript+1] = string.format('local warehouse = mywarehouse:GetState(mywarehouse, \"WAREHOUSE\") ') -- Get the warehouse self object from the static. - DCSScript[#DCSScript+1] = string.format('%s(mygroup)', Function) -- Call the function, e.g. myfunction.(warehouse,mygroup) + DCSScript[#DCSScript+1] = string.format('local warehouse = mywarehouse:GetState(mywarehouse, \"WAREHOUSE\") ') -- Get the warehouse self object from the static. + DCSScript[#DCSScript+1] = string.format('%s(mygroup)', Function) -- Call the function, e.g. myfunction.(warehouse,mygroup) + + -- Create task. + local DCSTask = CONTROLLABLE.TaskWrappedAction(self, CONTROLLABLE.CommandDoScript(self, table.concat(DCSScript))) + + return DCSTask +end + +--- Simple task function. Can be used to call a function which has the warehouse and the executing group as parameters. +-- @param #WAREHOUSE self +-- @param #string Function The name of the function to call passed as string. +-- @param Wrapper.Group#GROUP group The group which is meant. +-- @param #number n Waypoint passed. +-- @param #number N Final waypoint number. +function WAREHOUSE:_SimpleTaskFunctionWP(Function, group, n, N) + self:F2({Function}) + + -- Name of the warehouse (static) object. + local warehouse=self.warehouse:GetName() + local groupname=group:GetName() + + -- Task script. + local DCSScript = {} + + DCSScript[#DCSScript+1] = string.format('local mygroup = GROUP:FindByName(\"%s\") ', groupname) -- The group that executes the task function. Very handy with the "...". + if self.isunit then + DCSScript[#DCSScript+1] = string.format("local mywarehouse = UNIT:FindByName(\"%s\") ", warehouse) -- The unit that holds the warehouse self object. + else + DCSScript[#DCSScript+1] = string.format("local mywarehouse = STATIC:FindByName(\"%s\") ", warehouse) -- The static that holds the warehouse self object. + end + DCSScript[#DCSScript+1] = string.format('local warehouse = mywarehouse:GetState(mywarehouse, \"WAREHOUSE\") ') -- Get the warehouse self object from the static. + DCSScript[#DCSScript+1] = string.format('%s(mygroup, %d, %d)', Function, n ,N) -- Call the function, e.g. myfunction.(warehouse,mygroup) -- Create task. local DCSTask = CONTROLLABLE.TaskWrappedAction(self, CONTROLLABLE.CommandDoScript(self, table.concat(DCSScript))) @@ -7053,6 +7173,17 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="unit"}) end + -- Check all clients. + --[[ + for _,_unit in pairs(_units) do + local unit=_unit --Wrapper.Unit#UNIT + local _coord=unit:GetCoordinate() + local _size=self:_GetObjectSize(unit:GetDCSObject()) + local _name=unit:GetName() + table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="client"}) + end + ]] + -- Check all statics. for _,static in pairs(_statics) do local _vec3=static:getPoint() @@ -7610,6 +7741,75 @@ function WAREHOUSE:_SortQueue() table.sort(self.queue, _sort) end +--- Checks fuel on all pening assets. +-- @param #WAREHOUSE self +function WAREHOUSE:_CheckFuel() + + for i,qitem in ipairs(self.pending) do + local qitem=qitem --#WAREHOUSE.Pendingitem + + if qitem.transportgroupset then + for _,_group in pairs(qitem.transportgroupset:GetSet()) do + local group=_group --Wrapper.Group#GROUP + + if group and group:IsAlive() then + + -- Get min fuel of group. + local fuel=group:GetFuelMin() + + -- Debug info. + self:T2(self.wid..string.format("Transport group %s min fuel state = %.2f", group:GetName(), fuel)) + + -- Check if fuel is below threshold for first time. + if fuel